deep_itm_bull_spread_strategy.py 102 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064
  1. # 深度实值买购和卖购组合的牛差策略
  2. # 策略说明:使用深度实值买购期权替代ETF持仓,结合卖购期权构建牛差组合
  3. # 参考资料:基于加百列分享中的50ETF期权备兑认购策略改进
  4. import jqdata
  5. from jqdata import *
  6. import pandas as pd
  7. import numpy as np
  8. import datetime
  9. import matplotlib.pyplot as plt
  10. import os
  11. from enum import Enum
  12. plt.rcParams['font.sans-serif'] = ['SimHei']
  13. plt.rcParams['axes.unicode_minus'] = False
  14. class StrategyType(Enum):
  15. """策略类型枚举"""
  16. BULL_SPREAD = "bull_spread" # 牛差组合策略(深度实值买购+卖购)
  17. class DeepITMBullSpreadStrategy:
  18. """深度实值买购和卖购组合的牛差策略"""
  19. def __init__(self, underlying_symbol='510300.XSHG', start_date='2024-01-01', end_date='2024-12-31'):
  20. """初始化策略参数"""
  21. self.underlying_symbol = underlying_symbol
  22. self.start_date = start_date
  23. self.end_date = end_date
  24. # 策略参数设置 - 对应README.md中的阈值设定
  25. self.config = {
  26. 'contract_size': 30, # 一组张数
  27. 'min_premium': {'510300': 0.03, '510050': 0.05, '159915': 0.03}, # 最小权利金
  28. 'min_days_to_expiry': 15, # 最少开仓日期(距离到期日)
  29. 'call_time_value_threshold': 0.015, # 买购时间价值阈值(README中为0.015)
  30. 'put_close_premium_threshold': 0.005, # 卖购平仓权利金阈值(README中为50单位,转换为0.005)
  31. 'max_days_before_expiry': 3, # 合约到期移仓日期最大(交易日)
  32. 'min_days_before_expiry': 1, # 合约到期移仓日期最小(交易日)
  33. 'add_position_threshold': {'510300': 0.2, '510050': 0.1, '159915': 0.15}, # 加仓窗口阈值
  34. 'max_add_positions': 2, # 加仓次数上限
  35. 'max_profit_close_threshold': 0.83
  36. }
  37. # 持仓和交易记录
  38. self.positions = []
  39. self.trade_records = []
  40. # CSV文件输出设置
  41. self.transaction_csv_path = 'transaction.csv'
  42. self.position_csv_path = 'position.csv'
  43. # 验证合约数量
  44. self._validate_contract_size()
  45. self.daily_positions = [] # 每日持仓记录
  46. # 账户管理器回调(用于通知账户管理器更新汇总)
  47. self.account_manager_callback = None
  48. # 获取交易日历和月度分割点
  49. self.trade_days = pd.Series(index=jqdata.get_trade_days(start_date, end_date))
  50. self.trade_days.index = pd.to_datetime(self.trade_days.index)
  51. self.month_split = list(self.trade_days.resample('M', label='left').mean().index) + [pd.to_datetime(end_date)]
  52. # 调试输出:显示月份分割点
  53. # print(f"Month split 计算结果 ({len(self.month_split)}个分割点):")
  54. for i, split_date in enumerate(self.month_split):
  55. # print(f" 索引{i}: {split_date.strftime('%Y-%m-%d')}")
  56. if i < len(self.month_split) - 1:
  57. next_split = self.month_split[i + 1]
  58. # print(f" 月份{i}覆盖范围: {split_date.strftime('%Y-%m-%d')} 到 {next_split.strftime('%Y-%m-%d')}")
  59. # 用于存储前一日ETF价格,计算当日涨幅
  60. self.previous_etf_price = None
  61. def _validate_contract_size(self):
  62. """验证合约数量的有效性"""
  63. contract_size = self.config.get('contract_size', 30)
  64. if not isinstance(contract_size, (int, float)) or contract_size <= 0:
  65. print(f"警告: 合约数量无效({contract_size}),重置为默认值30张")
  66. self.config['contract_size'] = 30
  67. elif contract_size > 200: # 设置一个合理的上限
  68. print(f"警告: 合约数量过大({contract_size}),限制为200张")
  69. self.config['contract_size'] = 200
  70. else:
  71. # 确保为整数
  72. self.config['contract_size'] = int(contract_size)
  73. def get_safe_contract_size(self):
  74. """安全获取合约数量,确保返回有效值"""
  75. contract_size = self.config.get('contract_size', 30)
  76. if not isinstance(contract_size, (int, float)) or contract_size <= 0:
  77. print(f"警告: 运行时检测到合约数量无效({contract_size}),使用默认值30张")
  78. contract_size = 30
  79. self.config['contract_size'] = 30
  80. elif contract_size > 200:
  81. print(f"警告: 运行时检测到合约数量过大({contract_size}),限制为200张")
  82. contract_size = 200
  83. self.config['contract_size'] = 200
  84. return int(contract_size)
  85. def get_underlying_code(self):
  86. """获取标的ETF的简化代码"""
  87. if '510300' in self.underlying_symbol:
  88. return '510300'
  89. elif '510050' in self.underlying_symbol:
  90. return '510050'
  91. elif '159915' in self.underlying_symbol:
  92. return '159915'
  93. return '510300' # 默认
  94. def get_option_contracts(self, trade_date, month_idx, contract_type='CO'):
  95. """获取指定月份的期权合约信息"""
  96. underlying_code = self.get_underlying_code()
  97. # 基于自然月份计算期权筛选范围
  98. query_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
  99. current_year = query_date.year
  100. current_month = query_date.month
  101. # print(f" 期权筛选调试: month_idx={month_idx}, query_date={query_date}, 当前年月={current_year}-{current_month:02d}")
  102. # 计算目标月份
  103. if month_idx == 0:
  104. # 当月期权:到期日在当前月份
  105. target_year = current_year
  106. target_month = current_month
  107. elif month_idx == 1:
  108. # 下月期权:到期日在下个月份
  109. if current_month == 12:
  110. target_year = current_year + 1
  111. target_month = 1
  112. else:
  113. target_year = current_year
  114. target_month = current_month + 1
  115. else:
  116. # 其他月份:使用原来的month_split逻辑作为后备
  117. start_date = self.month_split[month_idx].date() if hasattr(self.month_split[month_idx], 'date') else self.month_split[month_idx]
  118. end_date = self.month_split[month_idx + 1].date() if hasattr(self.month_split[month_idx + 1], 'date') else self.month_split[month_idx + 1]
  119. print(f" 使用month_split后备逻辑: 月份索引{month_idx}, 范围{start_date}到{end_date}")
  120. if month_idx <= 1:
  121. # 计算目标月份的完整范围
  122. start_date = pd.to_datetime(f'{target_year}-{target_month:02d}-01').date()
  123. if target_month == 12:
  124. end_date = pd.to_datetime(f'{target_year + 1}-01-01').date() - pd.Timedelta(days=1)
  125. else:
  126. end_date = pd.to_datetime(f'{target_year}-{target_month + 1:02d}-01').date() - pd.Timedelta(days=1)
  127. # print(f" 月份索引{month_idx}({target_year}-{target_month:02d})期权筛选范围: {start_date} 到 {end_date}")
  128. q_contract_info = query(
  129. opt.OPT_CONTRACT_INFO.code,
  130. opt.OPT_CONTRACT_INFO.trading_code,
  131. opt.OPT_CONTRACT_INFO.name,
  132. opt.OPT_CONTRACT_INFO.exercise_price,
  133. opt.OPT_CONTRACT_INFO.last_trade_date,
  134. opt.OPT_CONTRACT_INFO.list_date
  135. ).filter(
  136. opt.OPT_CONTRACT_INFO.contract_type == contract_type,
  137. opt.OPT_CONTRACT_INFO.exchange_code == 'XSHG',
  138. opt.OPT_CONTRACT_INFO.last_trade_date >= start_date,
  139. opt.OPT_CONTRACT_INFO.last_trade_date <= end_date,
  140. opt.OPT_CONTRACT_INFO.list_date < query_date
  141. )
  142. contract_info = opt.run_query(q_contract_info)
  143. contract_info = contract_info[contract_info['trading_code'].str[:6] == underlying_code]
  144. # 调试输出:显示筛选到的期权到期日
  145. if not contract_info.empty:
  146. print(f" 筛选到{len(contract_info)}个期权,到期日范围:{contract_info['last_trade_date'].min()} 到 {contract_info['last_trade_date'].max()}")
  147. else:
  148. print(f" 未筛选到任何期权")
  149. return contract_info
  150. def get_monthly_option_candidates(self, trade_date, month_idx, silent=False):
  151. """获取指定月份的所有认购期权候选信息
  152. 返回: (contract_info, month_info, failure_reason) - contract_info为期权列表,month_info为月份信息,failure_reason为失败原因(成功时为None)
  153. """
  154. # if not silent:
  155. # print(f"{trade_date.strftime('%Y-%m-%d')} 获取月份索引 {month_idx} 的认购期权候选信息")
  156. # 获取期权合约信息
  157. contract_info = self.get_option_contracts(trade_date, month_idx, 'CO')
  158. if contract_info.empty:
  159. failure_reason = "无可用认购期权合约"
  160. if not silent:
  161. print(f" 月份索引 {month_idx} {failure_reason}")
  162. # 增加调试信息,显示查询的时间范围
  163. if month_idx < len(self.month_split) - 1:
  164. start_date = self.month_split[month_idx].date() if hasattr(self.month_split[month_idx], 'date') else self.month_split[month_idx]
  165. end_date = self.month_split[month_idx + 1].date() if hasattr(self.month_split[month_idx + 1], 'date') else self.month_split[month_idx + 1]
  166. print(f" 查询时间范围: {start_date} 到 {end_date}")
  167. print(f" 查询日期: {trade_date}")
  168. print(f" 标的代码: {self.get_underlying_code()}")
  169. print(f" month_split总长度: {len(self.month_split)}")
  170. return None, None, failure_reason
  171. # 获取月份信息
  172. underlying_code = self.get_underlying_code()
  173. min_days_to_expiry = self.config['min_days_to_expiry']
  174. # 为每个合约添加额外信息
  175. query_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
  176. candidates = []
  177. # 统计失败原因
  178. expiry_rejected_count = 0
  179. price_missing_count = 0
  180. price_error_count = 0
  181. # if not silent:
  182. # print(f" 查询到 {len(contract_info)} 个认购期权合约,开始获取价格信息:")
  183. for idx, contract in contract_info.iterrows():
  184. # 检查到期日
  185. expiry_date = pd.to_datetime(contract['last_trade_date'])
  186. trade_date_obj = trade_date.date() if hasattr(trade_date, 'date') else trade_date
  187. days_to_expiry = len(get_trade_days(trade_date_obj, expiry_date.date())) - 1
  188. if days_to_expiry < min_days_to_expiry:
  189. expiry_rejected_count += 1
  190. # if not silent:
  191. # print(f" 行权价 {contract['exercise_price']:.3f}: 到期时间不足 ({days_to_expiry} < {min_days_to_expiry})")
  192. continue
  193. # 查询期权价格
  194. try:
  195. q_price = query(opt.OPT_DAILY_PRICE.close).filter(
  196. opt.OPT_DAILY_PRICE.code == contract['code'],
  197. opt.OPT_DAILY_PRICE.date == query_date
  198. )
  199. price_result = opt.run_query(q_price)
  200. if price_result.empty:
  201. price_missing_count += 1
  202. if not silent:
  203. print(f" 行权价 {contract['exercise_price']:.3f}: 无价格数据")
  204. continue
  205. option_price = price_result['close'].iloc[0]
  206. candidate = {
  207. 'code': contract['code'],
  208. 'exercise_price': contract['exercise_price'],
  209. 'price': option_price,
  210. 'expiry_date': contract['last_trade_date'],
  211. 'days_to_expiry': days_to_expiry,
  212. 'contract_info': contract
  213. }
  214. candidates.append(candidate)
  215. # if not silent:
  216. # print(f" 行权价 {contract['exercise_price']:.3f}: 期权价格 {option_price:.4f}, 剩余天数 {days_to_expiry}")
  217. except Exception as e:
  218. price_error_count += 1
  219. if not silent:
  220. print(f" 行权价 {contract['exercise_price']:.3f}: 价格查询失败 ({str(e)})")
  221. continue
  222. if not candidates:
  223. # 构建详细的失败原因
  224. total_contracts = len(contract_info)
  225. failure_parts = []
  226. if expiry_rejected_count > 0:
  227. failure_parts.append(f"到期时间不足{expiry_rejected_count}个")
  228. if price_missing_count > 0:
  229. failure_parts.append(f"无价格数据{price_missing_count}个")
  230. if price_error_count > 0:
  231. failure_parts.append(f"价格查询失败{price_error_count}个")
  232. if failure_parts:
  233. failure_reason = f"共{total_contracts}个期权合约,但均不符合条件:" + "、".join(failure_parts)
  234. else:
  235. failure_reason = f"共{total_contracts}个期权合约,但均不符合基本条件"
  236. # if not silent:
  237. # print(f" 月份索引 {month_idx} 无符合基本条件的期权候选")
  238. return None, None, failure_reason
  239. month_info = {
  240. 'month_idx': month_idx,
  241. 'underlying_code': underlying_code,
  242. 'min_days_to_expiry': min_days_to_expiry,
  243. 'query_date': query_date
  244. }
  245. if not silent:
  246. print(f" 成功获取 {len(candidates)} 个有效期权候选")
  247. # print(f" 月份索引 {month_idx} 的认购期权候选信息: {candidates}")
  248. return candidates, month_info, None
  249. def select_sell_call_from_candidates(self, candidates, etf_price, min_premium, silent=False):
  250. """从候选期权中选择卖购期权(虚值期权)
  251. 返回: (selected_call, reason) - selected_call为期权信息或None,reason为失败原因或None
  252. """
  253. # if not silent:
  254. # print(f" 从 {len(candidates)} 个候选中筛选虚值卖购期权(行权价 > ETF价格 {etf_price:.4f}):")
  255. # 筛选虚值期权(行权价 > ETF价格)
  256. otm_candidates = [c for c in candidates if c['exercise_price'] > etf_price]
  257. if not otm_candidates:
  258. reason = f"无虚值期权:所有{len(candidates)}个期权的行权价都 <= ETF价格{etf_price:.4f}"
  259. if not silent:
  260. print(f" 筛选结果:无虚值期权(所有期权行权价都 <= ETF价格)")
  261. for c in candidates:
  262. option_type = "虚值" if c['exercise_price'] > etf_price else "实值"
  263. print(f" 行权价: {c['exercise_price']:.3f} ({option_type})")
  264. return None, reason
  265. if not silent:
  266. print(f" etf价格为{etf_price:.4f}时,筛选出 {len(otm_candidates)} 个虚值期权:")
  267. for c in otm_candidates:
  268. price_spread = c['exercise_price'] - etf_price
  269. print(f" 行权价: {c['exercise_price']:.3f}, 价差: {price_spread:.4f}, 价格: {c['price']:.4f}")
  270. # 按行权价排序,选择最接近ETF价格的虚值期权(行权价最小的)
  271. otm_candidates.sort(key=lambda x: x['exercise_price'])
  272. closest_otm = otm_candidates[0]
  273. if not silent:
  274. print(f" 选择最接近的虚值期权:行权价 {closest_otm['exercise_price']:.3f}, 价格 {closest_otm['price']:.4f}")
  275. # 检查权利金要求
  276. if closest_otm['price'] >= min_premium:
  277. if not silent:
  278. print(f" ✓ 满足权利金要求 ({closest_otm['price']:.4f} >= {min_premium:.4f})")
  279. # 构建返回结果
  280. selected_call = {
  281. 'code': closest_otm['code'],
  282. 'exercise_price': closest_otm['exercise_price'],
  283. 'price': closest_otm['price'],
  284. 'expiry_date': closest_otm['expiry_date'],
  285. 'price_spread': closest_otm['exercise_price'] - etf_price,
  286. 'days_to_expiry': closest_otm['days_to_expiry']
  287. }
  288. return selected_call, None
  289. else:
  290. reason = f"期权权利金不足:{closest_otm['price']:.4f} < {min_premium:.4f}"
  291. if not silent:
  292. print(f" ✗ 权利金不足 ({closest_otm['price']:.4f} < {min_premium:.4f})")
  293. return None, reason
  294. def select_buy_call_from_candidates(self, candidates, etf_price, time_value_threshold, trade_date, silent=False):
  295. """从候选期权中选择深度实值买购期权
  296. 返回: (selected_call, reason) - selected_call为期权信息或None,reason为失败原因或None
  297. """
  298. if not silent:
  299. print(f" 从 {len(candidates)} 个候选中筛选深度实值买购期权(行权价 < ETF价格 {etf_price:.4f}):")
  300. # 筛选深度实值期权(行权价 < ETF价格)
  301. itm_candidates = [c for c in candidates if c['exercise_price'] < etf_price]
  302. if not itm_candidates:
  303. reason = f"无深度实值期权:所有{len(candidates)}个期权的行权价都 >= ETF价格{etf_price:.4f}"
  304. if not silent:
  305. print(f" 筛选结果:无深度实值期权(所有期权行权价都 >= ETF价格)")
  306. for c in candidates:
  307. option_type = "实值" if c['exercise_price'] < etf_price else "虚值"
  308. print(f" 行权价: {c['exercise_price']:.3f} ({option_type})")
  309. return None, reason
  310. if not silent:
  311. print(f" 筛选出 {len(itm_candidates)} 个深度实值期权:")
  312. for c in itm_candidates:
  313. print(f" 行权价: {c['exercise_price']:.3f}, 价格: {c['price']:.4f}")
  314. # 按行权价与ETF价格的差异排序(升序,最接近的在前)
  315. itm_candidates.sort(key=lambda x: abs(x['exercise_price'] - etf_price))
  316. if not silent:
  317. print(f" 按距离ETF价格的差异排序,检查时间价值(阈值: {time_value_threshold:.4f}):")
  318. # 检查时间价值
  319. for candidate in itm_candidates:
  320. intrinsic_value = etf_price - candidate['exercise_price']
  321. time_value = round(max(0, candidate['price'] - intrinsic_value), 4)
  322. if not silent:
  323. print(f" 行权价 {candidate['exercise_price']:.3f}: 内在价值 {intrinsic_value:.4f}, 时间价值 {time_value:.4f}")
  324. if time_value < time_value_threshold:
  325. if not silent:
  326. print(f" ✓ 满足时间价值要求 (< {time_value_threshold:.4f})")
  327. # 构建返回结果
  328. selected_call = {
  329. 'code': candidate['code'],
  330. 'exercise_price': candidate['exercise_price'],
  331. 'price': candidate['price'],
  332. 'time_value': time_value,
  333. 'expiry_date': candidate['expiry_date'],
  334. 'price_diff': abs(candidate['exercise_price'] - etf_price),
  335. 'days_to_expiry': candidate['days_to_expiry']
  336. }
  337. return selected_call, None
  338. else:
  339. if not silent:
  340. print(f" ✗ 时间价值过高 ({time_value:.4f} >= {time_value_threshold:.4f})")
  341. reason = f"所有{len(itm_candidates)}个深度实值期权的时间价值都过高(需<{time_value_threshold:.4f})"
  342. if not silent:
  343. print(f" 深度实值买购期权选择失败:所有期权时间价值都过高")
  344. return None, reason
  345. def try_bull_spread_for_month(self, trade_date, etf_price, month_idx, is_current_month=True, silent=False):
  346. """尝试指定月份的牛差策略
  347. 返回: (buy_call, sell_call, reason) - 成功返回两个期权,失败返回None和原因
  348. """
  349. month_type = "当月" if is_current_month else "下月"
  350. underlying_code = self.get_underlying_code()
  351. base_premium = self.config['min_premium'][underlying_code]
  352. min_premium = base_premium * 0.6 if is_current_month else base_premium
  353. time_value_threshold = self.config['call_time_value_threshold']
  354. if not silent:
  355. print(f"{trade_date.strftime('%Y-%m-%d')} 尝试{month_type}牛差策略(月份索引:{month_idx})")
  356. print(f" 权利金阈值: {min_premium:.4f},时间价值阈值: {time_value_threshold:.4f}")
  357. # 1. 获取月份期权候选
  358. candidates, month_info, failure_reason = self.get_monthly_option_candidates(trade_date, month_idx, silent)
  359. if not candidates:
  360. reason = f"{month_type}期权候选获取失败:{failure_reason}"
  361. if not silent:
  362. print(f" {month_type}牛差策略失败:{reason}")
  363. return None, None, reason
  364. # 2. 选择卖购期权
  365. sell_call, sell_reason = self.select_sell_call_from_candidates(candidates, etf_price, min_premium, silent)
  366. if not sell_call:
  367. reason = f"{month_type}卖购期权选择失败:{sell_reason}"
  368. if not silent:
  369. print(f" {month_type}牛差策略失败:{reason}")
  370. return None, None, reason
  371. # 3. 选择买购期权(必须与卖购期权到期日一致)
  372. buy_call, buy_reason = self.select_buy_call_from_candidates(candidates, etf_price, time_value_threshold, trade_date, silent)
  373. if not buy_call:
  374. reason = f"{month_type}买购期权选择失败:{buy_reason}"
  375. if not silent:
  376. print(f" {month_type}牛差策略失败:{reason}")
  377. return None, None, reason
  378. # 4. 验证到期日是否一致
  379. sell_expiry = pd.to_datetime(sell_call['expiry_date'])
  380. buy_expiry = pd.to_datetime(buy_call['expiry_date'])
  381. if buy_expiry != sell_expiry:
  382. reason = f"买购和卖购期权到期日不一致:买购到期{buy_expiry.strftime('%Y-%m-%d')} vs 卖购到期{sell_expiry.strftime('%Y-%m-%d')}"
  383. if not silent:
  384. print(f" {month_type}牛差策略失败:{reason}")
  385. return None, None, reason
  386. if not silent:
  387. print(f" {month_type}牛差策略匹配成功:")
  388. print(f" 卖购期权:行权价 {sell_call['exercise_price']:.3f}, 价格 {sell_call['price']:.4f}")
  389. print(f" 买购期权:行权价 {buy_call['exercise_price']:.3f}, 价格 {buy_call['price']:.4f}")
  390. print(f" 到期日:{sell_expiry.strftime('%Y-%m-%d')}")
  391. return buy_call, sell_call, None
  392. def calculate_bull_spread_profit(self, buy_call, sell_call, contract_size=None):
  393. """计算牛差组合的盈利情况"""
  394. if contract_size is None:
  395. contract_size = self.get_safe_contract_size()
  396. # 单张最大盈利 = (卖购行权价 - 买购行权价 - 买购权利金 + 卖购权利金) * 10000
  397. max_profit_per_contract = (
  398. sell_call['exercise_price'] - buy_call['exercise_price']
  399. - buy_call['price'] + sell_call['price']
  400. ) * 10000
  401. # 最小盈利(卖购权利金)
  402. min_profit_per_contract = sell_call['price'] * 10000
  403. return {
  404. 'max_profit_per_contract': max_profit_per_contract,
  405. 'min_profit_per_contract': min_profit_per_contract,
  406. 'total_max_profit': max_profit_per_contract * contract_size,
  407. 'total_min_profit': min_profit_per_contract * contract_size
  408. }
  409. def _create_bull_spread_position(self, trade_date, etf_price, buy_call, sell_call, position_type='main', silent=False, save_to_csv=True):
  410. """直接使用已选择的期权信息创建牛差仓位,避免重复期权选择"""
  411. # 输出期权选择成功的信息
  412. if not silent:
  413. print(f"{trade_date.strftime('%Y-%m-%d')} 卖购期权选择成功,sell_call: {sell_call}")
  414. print(f"{trade_date.strftime('%Y-%m-%d')} 检查买购期权选择,buy_call: {buy_call}")
  415. # 安全获取合约数量
  416. contract_size = self.get_safe_contract_size()
  417. # 计算盈利信息
  418. profit_info = self.calculate_bull_spread_profit(buy_call, sell_call, contract_size)
  419. # 创建仓位记录
  420. position = {
  421. 'open_date': trade_date,
  422. 'etf_price': etf_price,
  423. 'buy_call': buy_call,
  424. 'sell_call': sell_call,
  425. 'contract_size': contract_size,
  426. 'profit_info': profit_info,
  427. 'position_type': position_type,
  428. 'status': 'open',
  429. 'strategy_type': StrategyType.BULL_SPREAD, # 标记策略类型
  430. 'add_position_trigger_price': etf_price - self.config['add_position_threshold'][self.get_underlying_code()] if position_type == 'main' else None
  431. }
  432. self.positions.append(position)
  433. # 记录交易(内存)
  434. trade_record = {
  435. '交易日期': trade_date,
  436. '交易类型': '开仓',
  437. '仓位类型': position_type,
  438. '策略类型': StrategyType.BULL_SPREAD.value, # 新增字段
  439. 'ETF标的': self.underlying_symbol,
  440. '买购期权价格': buy_call['price'],
  441. '买购期权行权价': buy_call['exercise_price'],
  442. '买购期权到期日': buy_call['expiry_date'],
  443. '卖购期权价格': sell_call['price'],
  444. '卖购期权行权价': sell_call['exercise_price'],
  445. '卖购期权到期日': sell_call['expiry_date'],
  446. '合约数量': contract_size,
  447. 'ETF价格': etf_price,
  448. '单张最大盈利': profit_info['max_profit_per_contract'],
  449. '单张最小盈利': profit_info['min_profit_per_contract'],
  450. '总最大盈利': profit_info['total_max_profit'],
  451. '总最小盈利': profit_info['total_min_profit']
  452. }
  453. self.trade_records.append(trade_record)
  454. # 保存交易记录到CSV(如果需要)
  455. if save_to_csv:
  456. self.save_transaction_to_csv(trade_record)
  457. return position
  458. def try_bull_spread_strategy(self, trade_date, etf_price, month_idx, position_type='main', silent=False, save_to_csv=True):
  459. """尝试牛差策略:深度实值买购+卖购期权"""
  460. # 1. 先尝试当月牛差策略
  461. buy_call, sell_call, reason = self.try_bull_spread_for_month(trade_date, etf_price, month_idx, is_current_month=True, silent=silent)
  462. if buy_call and sell_call:
  463. if not silent:
  464. print(f" 当月牛差策略匹配成功,执行开仓")
  465. return self._create_bull_spread_position(trade_date, etf_price, buy_call, sell_call, position_type, silent, save_to_csv), None
  466. # 2. 当月失败,尝试下月牛差策略
  467. # 计算下个月的月份索引
  468. trade_date_obj = trade_date.date() if hasattr(trade_date, 'date') else trade_date
  469. current_year = trade_date_obj.year
  470. current_month = trade_date_obj.month
  471. # 计算下个月
  472. if current_month == 12:
  473. next_year = current_year + 1
  474. next_month = 1
  475. else:
  476. next_year = current_year
  477. next_month = current_month + 1
  478. # 下个月第一天和最后一天
  479. next_month_start = pd.to_datetime(f'{next_year}-{next_month:02d}-01').date()
  480. if next_month == 12:
  481. next_month_end = pd.to_datetime(f'{next_year + 1}-01-01').date() - pd.Timedelta(days=1)
  482. else:
  483. next_month_end = pd.to_datetime(f'{next_year}-{next_month + 1:02d}-01').date() - pd.Timedelta(days=1)
  484. # 查找对应的月份索引
  485. next_month_idx = None
  486. for i, month_date in enumerate(self.month_split[:-1]):
  487. month_start = month_date.date() if hasattr(month_date, 'date') else month_date
  488. month_end = self.month_split[i + 1].date() if hasattr(self.month_split[i + 1], 'date') else self.month_split[i + 1]
  489. # 检查下月范围是否与month_split中的某个月份重叠
  490. if (next_month_start <= month_end and next_month_end >= month_start):
  491. next_month_idx = i
  492. break
  493. if next_month_idx is None:
  494. if not silent:
  495. print(f" 无法找到下月({next_year}-{next_month:02d})对应的月份索引")
  496. return None, f"牛差策略失败: 当月原因({reason}),且无下月期权可用"
  497. buy_call_next, sell_call_next, reason_next = self.try_bull_spread_for_month(trade_date, etf_price, next_month_idx, is_current_month=False, silent=silent)
  498. if buy_call_next and sell_call_next:
  499. if not silent:
  500. print(f" 下月牛差策略匹配成功,执行开仓")
  501. return self._create_bull_spread_position(trade_date, etf_price, buy_call_next, sell_call_next, position_type, silent, save_to_csv), None
  502. # 两个月份都失败
  503. combined_reason = f"当月失败({reason}),下月失败({reason_next})"
  504. return None, f"牛差策略失败: {combined_reason}"
  505. def open_position(self, trade_date, etf_price, position_type='main', silent=False, save_to_csv=True):
  506. """开仓方法 - 牛差策略(深度实值买购+卖购)"""
  507. # 确定月份索引
  508. month_idx = 0
  509. for i, month_date in enumerate(self.month_split[:-1]):
  510. if trade_date >= month_date:
  511. month_idx = i
  512. # 先输出开始尝试的日志,确保顺序正确
  513. if not silent:
  514. print(f"{trade_date.strftime('%Y-%m-%d')} 开始尝试牛差策略")
  515. # 尝试牛差策略(深度实值买购+卖购)
  516. result, reason = self.try_bull_spread_strategy(trade_date, etf_price, month_idx, position_type, silent, save_to_csv)
  517. if result is not None:
  518. return result
  519. # 牛差策略失败,输出详细原因
  520. print(f"{trade_date.strftime('%Y-%m-%d')} 牛差策略开仓失败,原因:{reason}")
  521. return None
  522. def should_close_position(self, position, current_date, etf_price):
  523. """判断是否应该平仓 - 支持两种策略类型"""
  524. if position['status'] != 'open':
  525. return False, None
  526. # 获取策略类型,兼容旧版本数据
  527. strategy_type = position.get('strategy_type', StrategyType.BULL_SPREAD)
  528. # 检查合约到期时间
  529. expiry_date = pd.to_datetime(position['sell_call']['expiry_date'])
  530. # 确保日期格式正确
  531. current_date_obj = current_date.date() if hasattr(current_date, 'date') else current_date
  532. expiry_date_obj = expiry_date.date() if hasattr(expiry_date, 'date') else expiry_date
  533. days_to_expiry = len(get_trade_days(current_date_obj, expiry_date_obj)) - 1
  534. # 到期日临近(对所有策略类型都适用)
  535. if days_to_expiry <= self.config['max_days_before_expiry']:
  536. return True, '过期时间平仓'
  537. # 获取卖购行权价
  538. sell_call_strike = position['sell_call']['exercise_price']
  539. # 牛差策略的平仓逻辑:根据ETF价格与卖购行权价关系选择条件
  540. if etf_price >= sell_call_strike:
  541. # ETF价格大于等于卖购行权价时,检查最大盈利平仓条件
  542. return self._check_max_profit_close_condition(position, current_date, etf_price, sell_call_strike)
  543. else:
  544. # ETF价格小于卖购行权价时,检查卖购权利金剩余平仓条件
  545. return self._check_sell_call_close_condition(position, current_date, etf_price, sell_call_strike)
  546. return False, None
  547. def _check_max_profit_close_condition(self, position, current_date, etf_price, sell_call_strike):
  548. """检查最大盈利平仓条件(牛差策略专用)"""
  549. try:
  550. query_date = current_date.date() if hasattr(current_date, 'date') else current_date
  551. # 获取买购期权价格
  552. q_buy_price = query(opt.OPT_DAILY_PRICE.close).filter(
  553. opt.OPT_DAILY_PRICE.code == position['buy_call']['code'],
  554. opt.OPT_DAILY_PRICE.date == query_date
  555. )
  556. buy_price_result = opt.run_query(q_buy_price)
  557. # 获取卖购期权价格
  558. q_sell_price = query(opt.OPT_DAILY_PRICE.close).filter(
  559. opt.OPT_DAILY_PRICE.code == position['sell_call']['code'],
  560. opt.OPT_DAILY_PRICE.date == query_date
  561. )
  562. sell_price_result = opt.run_query(q_sell_price)
  563. if not buy_price_result.empty and not sell_price_result.empty:
  564. current_buy_call_price = buy_price_result['close'].iloc[0]
  565. current_sell_call_price = sell_price_result['close'].iloc[0]
  566. # 计算当前盈利
  567. buy_call_pnl = (current_buy_call_price - position['buy_call']['price']) * 10000
  568. sell_call_pnl = (position['sell_call']['price'] - current_sell_call_price) * 10000
  569. current_pnl_per_contract = buy_call_pnl + sell_call_pnl
  570. # 获取最大盈利
  571. max_profit_per_contract = position['profit_info']['max_profit_per_contract']
  572. # 判断是否达到平仓阈值
  573. if max_profit_per_contract > 0 and current_pnl_per_contract >= max_profit_per_contract * self.config['max_profit_close_threshold']:
  574. print(f"{current_date.strftime('%Y-%m-%d')} 触发最大盈利平仓: 当前盈利: {current_pnl_per_contract:.2f}, 最大盈利: {max_profit_per_contract:.2f}")
  575. return True, '最大盈利平仓'
  576. except Exception as e:
  577. print(f"{current_date.strftime('%Y-%m-%d')} 检查最大盈利平仓条件时出错: {e}")
  578. return False, None
  579. def _check_sell_call_close_condition(self, position, current_date, etf_price, sell_call_strike):
  580. """检查卖购权利金剩余平仓条件(两种策略共用)"""
  581. try:
  582. query_date = current_date.date() if hasattr(current_date, 'date') else current_date
  583. q_price = query(opt.OPT_DAILY_PRICE.close).filter(
  584. opt.OPT_DAILY_PRICE.code == position['sell_call']['code'],
  585. opt.OPT_DAILY_PRICE.date == query_date
  586. )
  587. price_result = opt.run_query(q_price)
  588. if not price_result.empty:
  589. current_sell_call_price = price_result['close'].iloc[0]
  590. if current_sell_call_price < self.config['put_close_premium_threshold']:
  591. print(f"{current_date.strftime('%Y-%m-%d')} 触发卖购权利金平仓: 卖购期权价格{current_sell_call_price:.4f}")
  592. return True, '卖购权利金平仓'
  593. except Exception as e:
  594. print(f"{current_date.strftime('%Y-%m-%d')} 检查卖购权利金平仓条件时出错: {e}")
  595. return False, None
  596. def close_position(self, position, current_date, etf_price, reason):
  597. """平仓操作 - 支持两种策略类型"""
  598. try:
  599. # 获取策略类型,兼容旧版本数据
  600. strategy_type = position.get('strategy_type', StrategyType.BULL_SPREAD)
  601. query_date = current_date.date() if hasattr(current_date, 'date') else current_date
  602. # 获取卖购期权当前价格(两种策略都需要)
  603. q_sell_price = query(opt.OPT_DAILY_PRICE.close).filter(
  604. opt.OPT_DAILY_PRICE.code == position['sell_call']['code'],
  605. opt.OPT_DAILY_PRICE.date == query_date
  606. )
  607. sell_price_result = opt.run_query(q_sell_price)
  608. if sell_price_result.empty:
  609. raise Exception(f"无法获取卖购期权{position['sell_call']['code']}在{query_date}的价格数据")
  610. sell_call_close_price = sell_price_result['close'].iloc[0]
  611. # 计算卖购期权盈亏
  612. sell_call_pnl = (position['sell_call']['price'] - sell_call_close_price) * position['contract_size'] * 10000
  613. # 获取买购期权当前价格
  614. q_buy_price = query(opt.OPT_DAILY_PRICE.close).filter(
  615. opt.OPT_DAILY_PRICE.code == position['buy_call']['code'],
  616. opt.OPT_DAILY_PRICE.date == query_date
  617. )
  618. buy_price_result = opt.run_query(q_buy_price)
  619. if buy_price_result.empty:
  620. raise Exception(f"无法获取买购期权{position['buy_call']['code']}在{query_date}的价格数据")
  621. buy_call_close_price = buy_price_result['close'].iloc[0]
  622. buy_call_pnl = (buy_call_close_price - position['buy_call']['price']) * position['contract_size'] * 10000
  623. total_pnl = buy_call_pnl + sell_call_pnl - 10 # 两种策略都扣除相同手续费
  624. # 牛差策略平仓详情输出
  625. print(f"{current_date.strftime('%Y-%m-%d')} 牛差策略平仓详情:")
  626. print(f" 深度实值买购:开仓价格{position['buy_call']['price']:.4f} -> 平仓价格{buy_call_close_price:.4f}")
  627. print(f" 深度实值买购盈亏:({buy_call_close_price:.4f} - {position['buy_call']['price']:.4f}) × {position['contract_size']} × 10000 = {buy_call_pnl:.2f}元")
  628. print(f" 卖购期权:开仓价格{position['sell_call']['price']:.4f} -> 平仓价格{sell_call_close_price:.4f}")
  629. print(f" 卖购期权盈亏:({position['sell_call']['price']:.4f} - {sell_call_close_price:.4f}) × {position['contract_size']} × 10000 = {sell_call_pnl:.2f}元")
  630. print(f" 手续费:-10元")
  631. print(f" 组合总盈亏:{buy_call_pnl:.2f} + {sell_call_pnl:.2f} - 10 = {total_pnl:.2f}元")
  632. # 更新仓位状态
  633. position['status'] = 'closed'
  634. position['close_date'] = current_date
  635. position['close_etf_price'] = etf_price
  636. position['close_reason'] = reason
  637. position['buy_call_close_price'] = buy_call_close_price
  638. position['sell_call_close_price'] = sell_call_close_price
  639. position['pnl'] = total_pnl
  640. # 记录交易(内存)- 两种策略都有买购期权信息
  641. trade_record = {
  642. '交易日期': current_date,
  643. '交易类型': '平仓',
  644. '仓位类型': position['position_type'],
  645. '策略类型': strategy_type.value, # 策略类型字段
  646. 'ETF标的': self.underlying_symbol,
  647. '买购期权价格': position['buy_call']['price'], # 两种策略都有买购期权
  648. '买购期权行权价': position['buy_call']['exercise_price'],
  649. '买购期权到期日': position['buy_call']['expiry_date'],
  650. '卖购期权价格': position['sell_call']['price'],
  651. '卖购期权行权价': position['sell_call']['exercise_price'],
  652. '卖购期权到期日': position['sell_call']['expiry_date'],
  653. '合约数量': position['contract_size'],
  654. 'ETF价格': etf_price,
  655. '买购期权收盘价': buy_call_close_price,
  656. '卖购期权收盘价': sell_call_close_price,
  657. '开仓日期': position['open_date'],
  658. '开仓ETF价格': position['etf_price'],
  659. '买购期权盈亏': buy_call_pnl,
  660. '卖购期权盈亏': sell_call_pnl,
  661. '总盈亏': total_pnl,
  662. '平仓原因': reason,
  663. '单张最大盈利': '', # 平仓时不需要,保持字段一致性
  664. '单张最小盈利': '', # 平仓时不需要,保持字段一致性
  665. '总最大盈利': '', # 平仓时不需要,保持字段一致性
  666. '总最小盈利': '' # 平仓时不需要,保持字段一致性
  667. }
  668. self.trade_records.append(trade_record)
  669. # 保存交易记录到CSV
  670. self.save_transaction_to_csv(trade_record)
  671. except Exception as e:
  672. print(f"平仓时出错: {e}")
  673. position['status'] = 'error'
  674. def should_add_position(self, current_date, etf_price):
  675. """判断是否应该加仓"""
  676. # 检查是否有主仓位
  677. main_positions = [p for p in self.positions if p['position_type'] == 'main' and p['status'] == 'open']
  678. if not main_positions:
  679. return False
  680. # 获取最新主仓位
  681. latest_main_position = main_positions[-1]
  682. # 检查加仓次数是否超限
  683. add_positions = [p for p in self.positions if p['position_type'] == 'add' and p['status'] == 'open']
  684. if len(add_positions) >= self.config['max_add_positions']:
  685. return False
  686. # 检查是否触发加仓条件
  687. trigger_price = latest_main_position['add_position_trigger_price']
  688. if trigger_price and etf_price <= trigger_price:
  689. return True
  690. return False
  691. def save_transaction_to_csv(self, transaction_data):
  692. """保存交易记录到CSV文件"""
  693. try:
  694. # 数据验证 - 确保必需字段存在
  695. required_fields = ['交易日期', '交易类型', '仓位类型', 'ETF标的', '合约数量', 'ETF价格']
  696. for field in required_fields:
  697. if field not in transaction_data or transaction_data[field] is None:
  698. print(f"警告: 交易记录缺少必需字段 {field},跳过保存")
  699. return
  700. # 数据验证 - 确保数据类型正确
  701. if not isinstance(transaction_data.get('合约数量'), (int, float)) or transaction_data.get('合约数量') <= 0:
  702. print(f"警告: 合约数量无效 {transaction_data.get('合约数量')},跳过保存")
  703. return
  704. if not isinstance(transaction_data.get('ETF价格'), (int, float)) or transaction_data.get('ETF价格') <= 0:
  705. print(f"警告: ETF价格无效 {transaction_data.get('ETF价格')},跳过保存")
  706. return
  707. # 复制数据并标准化格式
  708. data_copy = transaction_data.copy()
  709. # 定义完整的字段顺序,确保开仓和平仓记录字段一致
  710. standard_fields = [
  711. '交易日期', '交易类型', '仓位类型', '策略类型', 'ETF标的', # 新增策略类型字段
  712. '买购期权价格', '买购期权行权价', '买购期权到期日',
  713. '卖购期权价格', '卖购期权行权价', '卖购期权到期日',
  714. '合约数量', 'ETF价格',
  715. '单张最大盈利', '单张最小盈利', '总最大盈利', '总最小盈利',
  716. '买购期权收盘价', '卖购期权收盘价', '开仓日期', '开仓ETF价格',
  717. '买购期权盈亏', '卖购期权盈亏', '总盈亏', '平仓原因'
  718. ]
  719. # 确保所有字段都存在,缺失的用空字符串填充
  720. standardized_data = {}
  721. for field in standard_fields:
  722. standardized_data[field] = data_copy.get(field, '')
  723. # 转换日期字段为YYYY-MM-DD格式
  724. date_fields = ['交易日期', '买购期权到期日', '卖购期权到期日', '开仓日期']
  725. for field in date_fields:
  726. if standardized_data[field] and standardized_data[field] != '':
  727. try:
  728. if hasattr(standardized_data[field], 'strftime'):
  729. standardized_data[field] = standardized_data[field].strftime('%Y-%m-%d')
  730. elif hasattr(standardized_data[field], 'date'):
  731. standardized_data[field] = standardized_data[field].date().strftime('%Y-%m-%d')
  732. else:
  733. # 如果是字符串,尝试解析后转换
  734. date_obj = pd.to_datetime(standardized_data[field])
  735. standardized_data[field] = date_obj.strftime('%Y-%m-%d')
  736. except:
  737. print(f"警告: 无法转换日期字段 {field}: {standardized_data[field]}")
  738. pass # 保持原值
  739. # 检查文件是否存在
  740. file_exists = os.path.exists(self.transaction_csv_path)
  741. # 转换为DataFrame,保持字段顺序
  742. df = pd.DataFrame([standardized_data], columns=standard_fields)
  743. # 保存到CSV
  744. if file_exists:
  745. df.to_csv(self.transaction_csv_path, mode='a', header=False, index=False, encoding='utf-8-sig')
  746. else:
  747. df.to_csv(self.transaction_csv_path, mode='w', header=True, index=False, encoding='utf-8-sig')
  748. except Exception as e:
  749. print(f"保存交易记录到CSV时出错: {e}")
  750. print(f"问题数据: {transaction_data}")
  751. def save_daily_position_to_csv(self, position_data):
  752. """保存每日持仓记录到CSV文件"""
  753. try:
  754. # 复制数据并标准化日期格式
  755. data_copy = position_data.copy()
  756. # 转换日期字段为YYYY-MM-DD格式(使用中文字段名)
  757. if '交易日期' in data_copy and data_copy['交易日期'] is not None:
  758. if hasattr(data_copy['交易日期'], 'strftime'):
  759. data_copy['交易日期'] = data_copy['交易日期'].strftime('%Y-%m-%d')
  760. elif hasattr(data_copy['交易日期'], 'date'):
  761. data_copy['交易日期'] = data_copy['交易日期'].date().strftime('%Y-%m-%d')
  762. else:
  763. # 如果是字符串,尝试解析后转换
  764. try:
  765. date_obj = pd.to_datetime(data_copy['交易日期'])
  766. data_copy['交易日期'] = date_obj.strftime('%Y-%m-%d')
  767. except:
  768. pass # 保持原值
  769. # 处理持仓详情字段 - 将列表转换为字符串
  770. if '持仓详情' in data_copy and isinstance(data_copy['持仓详情'], list):
  771. if data_copy['持仓详情']: # 如果列表不为空
  772. detail_strings = []
  773. for detail in data_copy['持仓详情']:
  774. detail_str = f"{detail['期权类别']}:{detail['持仓标的代码']}@{detail['行权价格']:.2f}×{detail['合约数量']}(盈亏{detail['盈亏金额']:.2f})"
  775. detail_strings.append(detail_str)
  776. data_copy['持仓详情'] = '; '.join(detail_strings)
  777. else:
  778. data_copy['持仓详情'] = '无持仓'
  779. # 添加到每日持仓列表
  780. self.daily_positions.append(data_copy)
  781. # 创建DataFrame,排除列表类型的复杂字段
  782. csv_data = {k: v for k, v in data_copy.items() if not isinstance(v, list)}
  783. df = pd.DataFrame([csv_data])
  784. # 检查文件是否存在
  785. file_exists = os.path.exists(self.position_csv_path)
  786. # 保存到CSV
  787. if file_exists:
  788. df.to_csv(self.position_csv_path, mode='a', header=False, index=False, encoding='utf-8-sig')
  789. else:
  790. df.to_csv(self.position_csv_path, mode='w', header=True, index=False, encoding='utf-8-sig')
  791. except Exception as e:
  792. print(f"保存持仓记录到CSV时出错: {e}")
  793. print(f"问题数据: {position_data}")
  794. def record_daily_positions(self, trade_date, etf_price):
  795. """记录每日持仓状况"""
  796. # 获取当前所有开仓状态的持仓
  797. open_positions = [p for p in self.positions if p['status'] == 'open']
  798. if not open_positions:
  799. # 如果没有持仓,记录一条空仓记录
  800. position_record = {
  801. '交易日期': trade_date,
  802. 'ETF价格': etf_price,
  803. '总仓位数': 0,
  804. '主仓位数': 0,
  805. '加仓仓位数': 0,
  806. '总合约数': 0,
  807. '总组合盈亏': 0,
  808. '总卖购盈亏': 0,
  809. '总买购盈亏': 0,
  810. '持仓详情': []
  811. }
  812. self.save_daily_position_to_csv(position_record)
  813. return
  814. # 统计持仓信息
  815. main_positions = [p for p in open_positions if p['position_type'] == 'main']
  816. add_positions = [p for p in open_positions if p['position_type'] == 'add']
  817. total_contracts = sum(p['contract_size'] for p in open_positions)
  818. # 记录详细持仓信息
  819. position_details = []
  820. total_combo_pnl = 0
  821. total_sell_call_pnl = 0
  822. total_buy_call_pnl = 0
  823. for i, pos in enumerate(open_positions):
  824. # 获取策略类型,兼容旧版本数据
  825. strategy_type = pos.get('strategy_type', StrategyType.BULL_SPREAD)
  826. # 获取当前期权价值(尽量获取,失败则使用开仓价格)
  827. try:
  828. query_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
  829. # 获取卖购期权当前价格(两种策略都需要)
  830. q_sell_price = query(opt.OPT_DAILY_PRICE.close).filter(
  831. opt.OPT_DAILY_PRICE.code == pos['sell_call']['code'],
  832. opt.OPT_DAILY_PRICE.date == query_date
  833. )
  834. sell_price_result = opt.run_query(q_sell_price)
  835. if sell_price_result.empty:
  836. raise Exception("无法获取卖购期权价格数据")
  837. sell_call_current_price = sell_price_result['close'].iloc[0]
  838. # 计算卖购期权盈亏
  839. sell_call_pnl = (pos['sell_call']['price'] - sell_call_current_price) * pos['contract_size'] * 10000
  840. # 两种策略都有买购期权,需要计算买购期权盈亏
  841. q_buy_price = query(opt.OPT_DAILY_PRICE.close).filter(
  842. opt.OPT_DAILY_PRICE.code == pos['buy_call']['code'],
  843. opt.OPT_DAILY_PRICE.date == query_date
  844. )
  845. buy_price_result = opt.run_query(q_buy_price)
  846. if buy_price_result.empty:
  847. raise Exception("无法获取买购期权价格数据")
  848. buy_call_current_price = buy_price_result['close'].iloc[0]
  849. buy_call_pnl = (buy_call_current_price - pos['buy_call']['price']) * pos['contract_size'] * 10000
  850. combo_pnl = buy_call_pnl + sell_call_pnl
  851. except:
  852. # 两种策略都有买购期权,异常处理相同
  853. buy_call_current_price = pos['buy_call']['price']
  854. sell_call_current_price = pos['sell_call']['price']
  855. buy_call_pnl = 0
  856. sell_call_pnl = 0
  857. combo_pnl = 0
  858. # 累计总盈亏
  859. total_combo_pnl += combo_pnl
  860. total_sell_call_pnl += sell_call_pnl
  861. total_buy_call_pnl += buy_call_pnl
  862. # 牛差策略:记录深度实值买购和卖购期权
  863. buy_call_detail = {
  864. '持仓标的代码': pos['buy_call']['code'],
  865. '期权类别': '买购(牛差)',
  866. '合约数量': pos['contract_size'],
  867. '行权价格': pos['buy_call']['exercise_price'],
  868. '成本价格': pos['buy_call']['price'],
  869. '当前价格': buy_call_current_price,
  870. '盈亏金额': buy_call_pnl,
  871. '仓位类型': pos['position_type'],
  872. '策略类型': '牛差组合',
  873. '开仓日期': pos['open_date']
  874. }
  875. sell_call_detail = {
  876. '持仓标的代码': pos['sell_call']['code'],
  877. '期权类别': '卖购(牛差)',
  878. '合约数量': pos['contract_size'],
  879. '行权价格': pos['sell_call']['exercise_price'],
  880. '成本价格': pos['sell_call']['price'],
  881. '当前价格': sell_call_current_price,
  882. '盈亏金额': sell_call_pnl,
  883. '仓位类型': pos['position_type'],
  884. '策略类型': '牛差组合',
  885. '开仓日期': pos['open_date']
  886. }
  887. position_details.extend([buy_call_detail, sell_call_detail])
  888. # 记录每日持仓汇总
  889. position_record = {
  890. '交易日期': trade_date,
  891. 'ETF价格': etf_price,
  892. '总仓位数': len(open_positions),
  893. '主仓位数': len(main_positions),
  894. '加仓仓位数': len(add_positions),
  895. '总合约数': total_contracts,
  896. '总组合盈亏': total_combo_pnl,
  897. '总卖购盈亏': total_sell_call_pnl,
  898. '总买购盈亏': total_buy_call_pnl,
  899. '持仓详情': position_details
  900. }
  901. self.save_daily_position_to_csv(position_record)
  902. # 注释掉单独的账户管理器回调,改为在多标的管理器中统一处理
  903. # 通知账户管理器更新每日汇总
  904. # if self.account_manager_callback:
  905. # try:
  906. # self.account_manager_callback(trade_date)
  907. # except Exception as e:
  908. # print(f"{trade_date.strftime('%Y-%m-%d')} 更新账户汇总回调出错: {e}")
  909. def run_strategy(self):
  910. """运行策略主逻辑"""
  911. print("开始运行深度实值买购和卖购组合的牛差策略...")
  912. for i, trade_date in enumerate(self.trade_days.index):
  913. # 获取ETF价格
  914. try:
  915. price_data = get_price(self.underlying_symbol, trade_date, trade_date, fields=['close'])['close']
  916. # print(f"{trade_date.strftime('%Y-%m-%d')} 获取ETF价格: {price_data.iloc[0]:.4f}")
  917. if price_data.empty:
  918. print(f"{trade_date.strftime('%Y-%m-%d')} 获取ETF价格失败,因为price_data为空")
  919. continue
  920. etf_price = price_data.iloc[0]
  921. except:
  922. print(f"{trade_date.strftime('%Y-%m-%d')} 获取ETF价格失败,因为获取{self.underlying_symbol}价格失败")
  923. continue
  924. # 记录每日持仓状况
  925. self.record_daily_positions(trade_date, etf_price)
  926. # 标记是否有交易发生
  927. has_trading = False
  928. # 检查是否需要平仓
  929. if len(self.positions) > 0:
  930. for position in self.positions:
  931. should_close, reason = self.should_close_position(position, trade_date, etf_price)
  932. # print(f"{trade_date.strftime('%Y-%m-%d')} 检查是否需要平仓: {should_close}, 平仓原因: {reason}")
  933. if should_close:
  934. self.close_position(position, trade_date, etf_price, reason)
  935. print(f"{trade_date.strftime('%Y-%m-%d')} 平仓: {reason}, ETF价格: {etf_price:.4f}")
  936. has_trading = True
  937. # 检查是否需要开新仓(首次开仓或平仓后重新开仓)
  938. open_positions = [p for p in self.positions if p['status'] == 'open']
  939. if not open_positions:
  940. new_position = self.open_position(trade_date, etf_price, 'main') # 使用新的统一开仓方法
  941. if new_position:
  942. max_profit = new_position['profit_info']['total_max_profit']
  943. contract_size = new_position['contract_size']
  944. # 牛差策略输出
  945. strategy_desc = f"牛差组合: 深度实值买购{new_position['buy_call']['exercise_price']:.2f}@{new_position['buy_call']['price']:.4f}, 卖购{new_position['sell_call']['exercise_price']:.2f}@{new_position['sell_call']['price']:.4f}"
  946. profit_desc = f"最大牛差收益: {max_profit:.2f}元"
  947. # 获取资金信息(如果有的话)
  948. if hasattr(self, 'config') and 'allocated_capital' in self.config:
  949. allocated_capital = self.config.get('allocated_capital', 0)
  950. estimated_margin = contract_size * 1000 # 每张约1000元保证金(粗略估算)
  951. print(f"{trade_date.strftime('%Y-%m-%d')} 开仓主仓位: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}, 可用资金: {allocated_capital:.0f}元, 预估保证金: {estimated_margin:.0f}元")
  952. else:
  953. print(f"{trade_date.strftime('%Y-%m-%d')} 开仓主仓位: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
  954. has_trading = True
  955. # 检查是否需要加仓
  956. elif self.should_add_position(trade_date, etf_price):
  957. add_position = self.open_position(trade_date, etf_price, 'add', silent=False) # 使用新的统一开仓方法
  958. if add_position:
  959. max_profit = add_position['profit_info']['total_max_profit']
  960. contract_size = add_position['contract_size']
  961. # 牛差策略输出
  962. strategy_desc = f"牛差组合: 深度实值买购{add_position['buy_call']['exercise_price']:.2f}@{add_position['buy_call']['price']:.4f}, 卖购{add_position['sell_call']['exercise_price']:.2f}@{add_position['sell_call']['price']:.4f}"
  963. profit_desc = f"最大牛差收益: {max_profit:.2f}元"
  964. # 获取资金信息(如果有的话)
  965. if hasattr(self, 'config') and 'allocated_capital' in self.config:
  966. allocated_capital = self.config.get('allocated_capital', 0)
  967. estimated_margin = contract_size * 1000 # 每张约1000元保证金(粗略估算)
  968. print(f"{trade_date.strftime('%Y-%m-%d')} 加仓: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}, 可用资金: {allocated_capital:.0f}元, 预估保证金: {estimated_margin:.0f}元")
  969. else:
  970. print(f"{trade_date.strftime('%Y-%m-%d')} 加仓: {strategy_desc}, ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
  971. has_trading = True
  972. # 更新前一日ETF价格,用于下一天计算涨幅
  973. self.previous_etf_price = etf_price
  974. print("策略运行完成!")
  975. def get_performance_summary(self):
  976. """获取策略表现总结"""
  977. if not self.trade_records:
  978. return "没有交易记录"
  979. closed_positions = [p for p in self.positions if p['status'] == 'closed']
  980. if not closed_positions:
  981. return "没有已平仓的交易"
  982. total_pnl = sum(p['pnl'] for p in closed_positions)
  983. winning_trades = len([p for p in closed_positions if p['pnl'] > 0])
  984. total_trades = len(closed_positions)
  985. win_rate = winning_trades / total_trades if total_trades > 0 else 0
  986. summary = f"""
  987. 策略表现总结:
  988. =============
  989. 总交易次数: {total_trades}
  990. 获利交易: {winning_trades}
  991. 胜率: {win_rate:.2%}
  992. 总盈亏: {total_pnl:.2f}元
  993. 平均每笔盈亏: {total_pnl/total_trades:.2f}元
  994. """
  995. return summary
  996. def plot_results(self):
  997. """绘制策略结果"""
  998. if not self.daily_positions:
  999. print("没有持仓数据可以绘制")
  1000. return
  1001. # 使用每日持仓记录绘制盈亏曲线
  1002. position_df = pd.DataFrame(self.daily_positions)
  1003. # 过滤有持仓的记录(排除空仓记录,但保留盈亏为0的记录用于显示完整曲线)
  1004. position_data = position_df.copy()
  1005. if position_data.empty:
  1006. print("没有持仓数据可以绘制")
  1007. return
  1008. # 确保交易日期是datetime格式
  1009. position_data['交易日期'] = pd.to_datetime(position_data['交易日期'])
  1010. position_data = position_data.sort_values('交易日期')
  1011. # 确保盈亏字段为数值类型
  1012. position_data['总组合盈亏'] = pd.to_numeric(position_data['总组合盈亏'], errors='coerce').fillna(0)
  1013. position_data['总卖购盈亏'] = pd.to_numeric(position_data['总卖购盈亏'], errors='coerce').fillna(0)
  1014. # 绘图
  1015. fig, ax = plt.subplots(1, 1, figsize=(12, 8))
  1016. # 绘制两条盈亏曲线
  1017. ax.plot(position_data['交易日期'], position_data['总组合盈亏'],
  1018. label='每日组合浮动盈亏(买购+卖购)', color='blue', linewidth=2)
  1019. ax.plot(position_data['交易日期'], position_data['总卖购盈亏'],
  1020. label='每日卖购浮动盈亏', color='red', linewidth=2, linestyle='--')
  1021. # 添加零线
  1022. ax.axhline(y=0, color='black', linestyle='-', alpha=0.3)
  1023. ax.set_title(f'{self.get_underlying_code()}策略每日浮动盈亏对比', fontsize=14)
  1024. ax.set_ylabel('浮动盈亏 (元)', fontsize=12)
  1025. ax.set_xlabel('日期', fontsize=12)
  1026. ax.legend(fontsize=10)
  1027. ax.grid(True, alpha=0.3)
  1028. # 格式化日期显示
  1029. import matplotlib.dates as mdates
  1030. ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
  1031. ax.xaxis.set_major_locator(mdates.MonthLocator())
  1032. plt.xticks(rotation=45)
  1033. # 添加数据标注
  1034. if len(position_data) > 0:
  1035. final_combo_pnl = position_data['总组合盈亏'].iloc[-1]
  1036. final_sell_pnl = position_data['总卖购盈亏'].iloc[-1]
  1037. max_combo_pnl = position_data['总组合盈亏'].max()
  1038. min_combo_pnl = position_data['总组合盈亏'].min()
  1039. ax.text(0.02, 0.98,
  1040. f'当前组合浮盈: {final_combo_pnl:.2f}元\n'
  1041. f'当前卖购浮盈: {final_sell_pnl:.2f}元\n'
  1042. f'最大组合浮盈: {max_combo_pnl:.2f}元\n'
  1043. f'最大组合浮亏: {min_combo_pnl:.2f}元',
  1044. transform=ax.transAxes, fontsize=10, verticalalignment='top',
  1045. bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
  1046. plt.tight_layout()
  1047. plt.show()
  1048. # 策略配置类
  1049. class StrategyConfig:
  1050. """策略配置管理"""
  1051. def __init__(self):
  1052. # ETF标的配置字典
  1053. self.etf_symbols = {
  1054. '50ETF': '510050.XSHG', # 上证50ETF
  1055. '300ETF': '510300.XSHG', # 沪深300ETF
  1056. '创业板ETF': '159915.XSHE' # 创业板ETF
  1057. }
  1058. # 时间范围配置
  1059. self.time_config = {
  1060. 'start_date': '2024-06-01',
  1061. 'end_date': '2024-08-31'
  1062. }
  1063. # 资金配置
  1064. self.capital_config = {
  1065. 'total_capital': 1000000, # 总资金额度(100万)
  1066. 'capital_allocation': { # 不同标的资金分配比例
  1067. '50ETF': 0.3, # 50ETF 30%
  1068. '300ETF': 0.5, # 300ETF 50%
  1069. '创业板ETF': 0.2 # 创业板ETF 20%
  1070. },
  1071. 'capital_usage_limit': 0.8, # 资金使用上限80%
  1072. 'bull_spread_margin_discount': 30, # 牛差组合策略保证金优惠(每单位组合加收30元)
  1073. 'contract_unit': 10000, # 合约单位(1张期权代表10000份标的)
  1074. 'margin_params': { # 保证金计算参数
  1075. 'volatility_factor': 0.12, # 波动率因子12%
  1076. 'min_margin_factor': 0.07 # 最小保证金因子7%
  1077. }
  1078. }
  1079. def get_allocated_capital(self, etf_code):
  1080. """获取指定ETF的分配资金"""
  1081. total_usable = self.capital_config['total_capital'] * self.capital_config['capital_usage_limit']
  1082. # 获取实际存在的ETF列表
  1083. available_etfs = list(self.etf_symbols.keys())
  1084. # 计算实际存在ETF的原始比例总和
  1085. original_allocation = self.capital_config['capital_allocation']
  1086. available_ratio_sum = sum(original_allocation.get(etf, 0) for etf in available_etfs)
  1087. if available_ratio_sum == 0:
  1088. print(f"警告: 无可用ETF或比例配置错误,{etf_code}分配资金为0")
  1089. return 0
  1090. # 获取当前ETF的原始比例
  1091. original_ratio = original_allocation.get(etf_code, 0)
  1092. if original_ratio == 0:
  1093. print(f"警告: {etf_code}未在capital_allocation中配置,分配资金为0")
  1094. return 0
  1095. # 重新调整比例:当前ETF比例 / 实际存在ETF的比例总和
  1096. adjusted_ratio = original_ratio / available_ratio_sum
  1097. allocated_capital = total_usable * adjusted_ratio
  1098. print(f"资金分配调整 - {etf_code}: 原始比例{original_ratio:.1%}, 调整后比例{adjusted_ratio:.1%}, 分配资金{allocated_capital:,.0f}元")
  1099. return allocated_capital
  1100. def calculate_option_margin(self, option_type, settlement_price, underlying_price, strike_price):
  1101. """
  1102. 计算单张期权的保证金
  1103. :param option_type: 'call' 或 'put'
  1104. :param settlement_price: 合约前结算价
  1105. :param underlying_price: 标的证券前收盘价
  1106. :param strike_price: 行权价
  1107. :return: 单张期权保证金
  1108. """
  1109. contract_unit = self.capital_config['contract_unit']
  1110. volatility_factor = self.capital_config['margin_params']['volatility_factor']
  1111. min_margin_factor = self.capital_config['margin_params']['min_margin_factor']
  1112. if option_type == 'call':
  1113. # 认购期权虚值 = max(行权价 - 标的价格, 0)
  1114. out_of_money = max(strike_price - underlying_price, 0)
  1115. margin = (settlement_price + max(
  1116. volatility_factor * underlying_price - out_of_money,
  1117. min_margin_factor * underlying_price
  1118. )) * contract_unit
  1119. elif option_type == 'put':
  1120. # 认沽期权虚值 = max(标的价格 - 行权价, 0)
  1121. out_of_money = max(underlying_price - strike_price, 0)
  1122. margin = min(
  1123. settlement_price + max(
  1124. volatility_factor * underlying_price - out_of_money,
  1125. min_margin_factor * strike_price
  1126. ),
  1127. strike_price
  1128. ) * contract_unit
  1129. else:
  1130. raise ValueError("option_type must be 'call' or 'put'")
  1131. return margin
  1132. def calculate_bull_spread_margin(self, buy_call_info, sell_call_info, underlying_price):
  1133. """
  1134. 计算牛差组合的保证金
  1135. :param buy_call_info: 买入认购期权信息
  1136. :param sell_call_info: 卖出认购期权信息
  1137. :param underlying_price: 标的价格
  1138. :return: 牛差组合保证金
  1139. """
  1140. # 计算行权价差
  1141. strike_diff = sell_call_info['exercise_price'] - buy_call_info['exercise_price']
  1142. contract_unit = self.capital_config['contract_unit']
  1143. margin_discount = self.capital_config['bull_spread_margin_discount']
  1144. # 判断组合类型
  1145. if strike_diff > 0:
  1146. # 传统牛差组合(买购行权价 < 卖购行权价)
  1147. # 保证金 = 行权价差 * 合约单位 + 保证金优惠
  1148. bull_spread_margin = strike_diff * contract_unit + margin_discount
  1149. else:
  1150. # 其他情况的保证金计算
  1151. # 使用净权利金作为保证金基础
  1152. net_premium = buy_call_info['price'] - sell_call_info['price']
  1153. if net_premium > 0:
  1154. # 需要支付净权利金
  1155. bull_spread_margin = abs(net_premium) * contract_unit + margin_discount
  1156. else:
  1157. # 收取净权利金,使用估算保证金
  1158. estimated_margin = max(sell_call_info['price'] * contract_unit * 0.1, 1000) # 最少1000元保证金
  1159. bull_spread_margin = estimated_margin
  1160. return bull_spread_margin
  1161. def calculate_contract_size(self, etf_code, etf_price, buy_call_info=None, sell_call_info=None):
  1162. """
  1163. 计算可开仓的合约数量
  1164. :param etf_code: ETF代码
  1165. :param etf_price: ETF价格
  1166. :param buy_call_info: 买入认购期权信息(可选,用于牛差组合计算)
  1167. :param sell_call_info: 卖出认购期权信息(可选,用于牛差组合计算)
  1168. """
  1169. allocated_capital = self.get_allocated_capital(etf_code)
  1170. if buy_call_info and sell_call_info:
  1171. # 牛差组合保证金计算
  1172. margin_per_contract = self.calculate_bull_spread_margin(buy_call_info, sell_call_info, etf_price)
  1173. else:
  1174. # 单一期权保证金估算(使用卖出认购期权)
  1175. estimated_settlement_price = etf_price * 0.02 # 估算权利金为标的价格的2%
  1176. estimated_strike_price = etf_price * 1.05 # 估算行权价为标的价格的105%
  1177. margin_per_contract = self.calculate_option_margin('call', estimated_settlement_price, etf_price, estimated_strike_price)
  1178. # 边界条件检查
  1179. if allocated_capital <= 0:
  1180. print(f"警告: {etf_code} 分配资金无效({allocated_capital}),返回默认合约数量30张")
  1181. return 30
  1182. if margin_per_contract <= 0:
  1183. print(f"警告: {etf_code} 保证金计算结果无效({margin_per_contract}),返回默认合约数量30张")
  1184. return 30
  1185. max_contracts = int(allocated_capital / margin_per_contract)
  1186. # 确保结果为正数
  1187. if max_contracts <= 0:
  1188. print(f"警告: {etf_code} 合约数量计算结果无效({max_contracts}),返回默认合约数量30张")
  1189. return 30
  1190. return min(max_contracts, 100) # 限制最大100张
  1191. # 多标的策略管理器
  1192. class MultiUnderlyingBullSpreadManager:
  1193. """多标的牛差策略管理器"""
  1194. def __init__(self, config: StrategyConfig):
  1195. self.config = config
  1196. self.strategies = {}
  1197. self.daily_account_records = [] # 每日账户资金记录
  1198. self.account_csv_path = 'account_summary.csv'
  1199. self.cumulative_realized_pnl = 0 # 累积已实现盈亏
  1200. self.previous_date_summary = None # 前一天的账户汇总记录
  1201. self.initialize_strategies()
  1202. def initialize_strategies(self):
  1203. """初始化各个标的的策略"""
  1204. for etf_code, symbol in self.config.etf_symbols.items():
  1205. allocated_capital = self.config.get_allocated_capital(etf_code)
  1206. if allocated_capital > 0:
  1207. strategy = DeepITMBullSpreadStrategy(
  1208. underlying_symbol=symbol,
  1209. start_date=self.config.time_config['start_date'],
  1210. end_date=self.config.time_config['end_date']
  1211. )
  1212. # 动态调整策略参数
  1213. strategy.config['allocated_capital'] = allocated_capital
  1214. strategy.config['etf_code'] = etf_code
  1215. # 设置账户管理器回调
  1216. strategy.account_manager_callback = self.record_daily_account_summary
  1217. self.strategies[etf_code] = strategy
  1218. print(f"初始化{etf_code}策略,分配资金: {allocated_capital:,.0f}元")
  1219. def calculate_dynamic_contract_size(self, strategy, etf_price, buy_call_info=None, sell_call_info=None):
  1220. """动态计算合约数量"""
  1221. etf_code = strategy.config['etf_code']
  1222. return self.config.calculate_contract_size(etf_code, etf_price, buy_call_info, sell_call_info)
  1223. def calculate_used_capital(self, strategy):
  1224. """计算策略已使用的资金 - 支持两种策略类型"""
  1225. used_capital = 0
  1226. open_positions = [p for p in strategy.positions if p['status'] == 'open']
  1227. for position in open_positions:
  1228. # 获取策略类型,兼容旧版本数据
  1229. strategy_type = position.get('strategy_type', StrategyType.BULL_SPREAD)
  1230. # 两种策略都有买购期权,资金使用计算相同
  1231. # 资金使用 = (买购权利金 - 卖购权利金) * 合约数量 * 10000
  1232. net_premium = position['buy_call']['price'] - position['sell_call']['price']
  1233. position_capital = net_premium * position['contract_size'] * 10000
  1234. used_capital += position_capital
  1235. return used_capital
  1236. def record_daily_account_summary(self, trade_date):
  1237. """记录每日账户资金汇总"""
  1238. initial_capital = self.config.capital_config['total_capital']
  1239. total_used_capital = 0
  1240. total_floating_pnl = 0
  1241. total_current_market_value = 0 # 所有策略的当前市值总和
  1242. strategy_details = {}
  1243. # 计算当天的已实现盈亏(通过检查平仓交易)
  1244. today_realized_pnl = 0
  1245. for etf_code, strategy in self.strategies.items():
  1246. # 检查当天是否有平仓交易
  1247. for trade_record in strategy.trade_records:
  1248. if (trade_record.get('交易类型') == '平仓' and
  1249. trade_record.get('交易日期') and
  1250. trade_record.get('总盈亏') is not None):
  1251. trade_date_record = trade_record.get('交易日期')
  1252. if hasattr(trade_date_record, 'date'):
  1253. trade_date_record = trade_date_record.date()
  1254. elif isinstance(trade_date_record, str):
  1255. try:
  1256. trade_date_record = pd.to_datetime(trade_date_record).date()
  1257. except:
  1258. continue
  1259. target_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
  1260. if trade_date_record == target_date:
  1261. today_realized_pnl += float(trade_record.get('总盈亏', 0))
  1262. # 更新累积已实现盈亏
  1263. self.cumulative_realized_pnl += today_realized_pnl
  1264. # 汇总各策略的资金使用和浮动盈亏
  1265. total_bull_spread_positions = 0
  1266. for etf_code, strategy in self.strategies.items():
  1267. used_capital = self.calculate_used_capital(strategy)
  1268. open_positions = [p for p in strategy.positions if p['status'] == 'open']
  1269. open_positions_count = len(open_positions)
  1270. # 统计牛差策略仓位数量
  1271. bull_spread_count = len([p for p in open_positions if p.get('strategy_type', StrategyType.BULL_SPREAD) == StrategyType.BULL_SPREAD])
  1272. total_bull_spread_positions += bull_spread_count
  1273. # 计算组合当前市值和浮动盈亏
  1274. current_market_value = 0
  1275. floating_pnl = 0
  1276. if strategy.daily_positions:
  1277. # 查找指定日期的持仓记录
  1278. target_record = None
  1279. for position_record in strategy.daily_positions:
  1280. record_date = position_record.get('交易日期')
  1281. if hasattr(record_date, 'date'):
  1282. record_date = record_date.date()
  1283. elif isinstance(record_date, str):
  1284. try:
  1285. record_date = pd.to_datetime(record_date).date()
  1286. except:
  1287. continue
  1288. target_date = trade_date.date() if hasattr(trade_date, 'date') else trade_date
  1289. if record_date == target_date:
  1290. target_record = position_record
  1291. break
  1292. # 如果找到了对应日期的记录,计算当前市值和浮动盈亏
  1293. if target_record:
  1294. floating_pnl = target_record.get('总组合盈亏', 0)
  1295. if isinstance(floating_pnl, str):
  1296. try:
  1297. floating_pnl = float(floating_pnl)
  1298. except:
  1299. floating_pnl = 0
  1300. # 计算组合当前市值 = 初始投入成本 + 浮动盈亏
  1301. current_market_value = used_capital + floating_pnl
  1302. total_used_capital += used_capital
  1303. total_floating_pnl += floating_pnl
  1304. total_current_market_value += current_market_value
  1305. strategy_details[etf_code] = {
  1306. '分配资金': strategy.config.get('allocated_capital', 0),
  1307. '初始投入': used_capital,
  1308. '当前市值': current_market_value,
  1309. '浮动盈亏': floating_pnl,
  1310. '持仓数量': open_positions_count,
  1311. '牛差策略数量': bull_spread_count
  1312. }
  1313. # 修正后的计算逻辑:
  1314. # 当前总资金 = 初始资金 + 累积已实现盈亏
  1315. current_total_capital = initial_capital + self.cumulative_realized_pnl
  1316. # 剩余现金 = 当前总资金 - 投入资金
  1317. remaining_cash = current_total_capital - total_used_capital
  1318. # 账户总价值 = 剩余现金 + 组合当前市值
  1319. total_account_value = remaining_cash + total_current_market_value
  1320. # 记录账户汇总
  1321. account_record = {
  1322. '交易日期': trade_date,
  1323. '总资金': current_total_capital, # 使用当前总资金而非初始资金
  1324. '初始投入总额': total_used_capital,
  1325. '剩余现金': remaining_cash,
  1326. '组合当前市值': total_current_market_value,
  1327. '总浮动盈亏': total_floating_pnl,
  1328. '账户总价值': total_account_value,
  1329. '累积已实现盈亏': self.cumulative_realized_pnl, # 新增字段
  1330. '当日已实现盈亏': today_realized_pnl, # 新增字段
  1331. '资金使用率': total_used_capital / current_total_capital if current_total_capital > 0 else 0,
  1332. '收益率': (total_account_value - initial_capital) / initial_capital if initial_capital > 0 else 0, # 修正收益率计算
  1333. '牛差策略总数': total_bull_spread_positions, # 新增字段
  1334. '策略详情': strategy_details
  1335. }
  1336. self.daily_account_records.append(account_record)
  1337. self.save_account_summary_to_csv(account_record)
  1338. # 更新前一天的记录
  1339. self.previous_date_summary = account_record
  1340. # 输出整体账户汇总(只在有活动时输出)
  1341. # if total_used_capital > 0 or total_floating_pnl != 0:
  1342. # print(f"{trade_date.strftime('%Y-%m-%d')} 【账户汇总】总资金{total_capital:.0f}元|已投入{total_used_capital:.0f}元|剩余现金{remaining_cash:.0f}元|组合市值{total_current_market_value:.0f}元|账户总值{total_account_value:.0f}元|总收益{total_floating_pnl:.0f}元|收益率{(total_floating_pnl/total_capital)*100:.2f}%")
  1343. def save_account_summary_to_csv(self, account_data):
  1344. """保存账户汇总到CSV文件"""
  1345. try:
  1346. # 复制数据并处理日期格式
  1347. data_copy = account_data.copy()
  1348. # 转换日期字段
  1349. if '交易日期' in data_copy and data_copy['交易日期'] is not None:
  1350. if hasattr(data_copy['交易日期'], 'strftime'):
  1351. data_copy['交易日期'] = data_copy['交易日期'].strftime('%Y-%m-%d')
  1352. elif hasattr(data_copy['交易日期'], 'date'):
  1353. data_copy['交易日期'] = data_copy['交易日期'].date().strftime('%Y-%m-%d')
  1354. else:
  1355. try:
  1356. date_obj = pd.to_datetime(data_copy['交易日期'])
  1357. data_copy['交易日期'] = date_obj.strftime('%Y-%m-%d')
  1358. except:
  1359. pass
  1360. # 处理策略详情字段
  1361. if '策略详情' in data_copy and isinstance(data_copy['策略详情'], dict):
  1362. strategy_strings = []
  1363. for etf_code, details in data_copy['策略详情'].items():
  1364. bull_count = details.get('牛差策略数量', 0)
  1365. strategy_str = f"{etf_code}(分配{details['分配资金']:.0f}|投入{details['初始投入']:.0f}|市值{details['当前市值']:.0f}|浮盈{details['浮动盈亏']:.0f}|持仓{details['持仓数量']}|牛差{bull_count})"
  1366. strategy_strings.append(strategy_str)
  1367. data_copy['策略详情'] = '; '.join(strategy_strings)
  1368. # 创建DataFrame,排除复杂字段
  1369. csv_data = {k: v for k, v in data_copy.items() if not isinstance(v, dict)}
  1370. df = pd.DataFrame([csv_data])
  1371. # 检查文件是否存在
  1372. file_exists = os.path.exists(self.account_csv_path)
  1373. # 保存到CSV
  1374. if file_exists:
  1375. df.to_csv(self.account_csv_path, mode='a', header=False, index=False, encoding='utf-8-sig')
  1376. else:
  1377. df.to_csv(self.account_csv_path, mode='w', header=True, index=False, encoding='utf-8-sig')
  1378. except Exception as e:
  1379. print(f"保存账户汇总到CSV时出错: {e}")
  1380. print(f"问题数据: {account_data}")
  1381. def plot_account_summary(self):
  1382. """绘制整体账户资金曲线"""
  1383. if not self.daily_account_records:
  1384. print("没有账户数据可以绘制")
  1385. return
  1386. # 转换为DataFrame
  1387. account_df = pd.DataFrame(self.daily_account_records)
  1388. if account_df.empty:
  1389. print("没有账户数据可以绘制")
  1390. return
  1391. # print(f"账户数据记录数量: {len(account_df)}")
  1392. # print(f"账户数据列: {account_df.columns.tolist()}")
  1393. # print(f"前几行数据:\n{account_df.head()}")
  1394. # 确保日期格式正确
  1395. account_df['交易日期'] = pd.to_datetime(account_df['交易日期'], errors='coerce')
  1396. account_df = account_df.dropna(subset=['交易日期']).sort_values('交易日期')
  1397. if account_df.empty:
  1398. print("日期转换后没有有效数据")
  1399. return
  1400. # 确保数值字段为数值类型,处理可能的字符串或其他类型
  1401. numeric_columns = ['总资金', '初始投入总额', '剩余现金', '组合当前市值', '总浮动盈亏', '账户总价值', '资金使用率', '收益率', '牛差策略总数']
  1402. for col in numeric_columns:
  1403. if col in account_df.columns:
  1404. # 先转换为字符串,移除可能的非数字字符
  1405. account_df[col] = account_df[col].astype(str).str.replace('[^\d.-]', '', regex=True)
  1406. # 再转换为数值类型
  1407. account_df[col] = pd.to_numeric(account_df[col], errors='coerce').fillna(0)
  1408. print(f"列 {col} 数据范围: {account_df[col].min()} 到 {account_df[col].max()}")
  1409. # 检查是否有有效的数值数据
  1410. if account_df[['总资金', '账户总价值']].isna().all().all():
  1411. print("所有数值数据都无效,无法绘图")
  1412. return
  1413. # 绘图
  1414. fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
  1415. try:
  1416. # 上图:账户总价值曲线
  1417. # 转换为numpy数组以确保数据类型
  1418. dates = account_df['交易日期'].values
  1419. total_values = account_df['账户总价值'].values.astype(float)
  1420. initial_capitals = account_df['总资金'].values.astype(float)
  1421. ax1.plot(dates, total_values,
  1422. label='账户总价值', color='green', linewidth=2)
  1423. ax1.plot(dates, initial_capitals,
  1424. label='初始资金', color='gray', linewidth=1, linestyle='--')
  1425. # 填充区域(只有在数据有效时)
  1426. if len(total_values) > 0 and len(initial_capitals) > 0:
  1427. profit_mask = total_values >= initial_capitals
  1428. loss_mask = total_values < initial_capitals
  1429. if profit_mask.any():
  1430. ax1.fill_between(dates, initial_capitals, total_values,
  1431. where=profit_mask,
  1432. color='green', alpha=0.3, label='盈利区域')
  1433. if loss_mask.any():
  1434. ax1.fill_between(dates, initial_capitals, total_values,
  1435. where=loss_mask,
  1436. color='red', alpha=0.3, label='亏损区域')
  1437. ax1.set_title('整体账户资金曲线', fontsize=14)
  1438. ax1.set_ylabel('资金 (元)', fontsize=12)
  1439. ax1.legend(fontsize=10)
  1440. ax1.grid(True, alpha=0.3)
  1441. # 下图:资金构成
  1442. remaining_cash = account_df['剩余现金'].values.astype(float)
  1443. current_market_value = account_df['组合当前市值'].values.astype(float)
  1444. ax2.plot(dates, remaining_cash,
  1445. label='剩余现金', color='blue', linewidth=2)
  1446. ax2.plot(dates, current_market_value,
  1447. label='组合当前市值', color='orange', linewidth=2, linestyle='--')
  1448. ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3)
  1449. ax2.set_title('资金构成分析', fontsize=14)
  1450. ax2.set_ylabel('金额 (元)', fontsize=12)
  1451. ax2.set_xlabel('日期', fontsize=12)
  1452. ax2.legend(fontsize=10)
  1453. ax2.grid(True, alpha=0.3)
  1454. # 格式化日期显示
  1455. import matplotlib.dates as mdates
  1456. for ax in [ax1, ax2]:
  1457. ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
  1458. ax.xaxis.set_major_locator(mdates.MonthLocator())
  1459. plt.xticks(rotation=45)
  1460. # 添加数据标注
  1461. if len(total_values) > 0:
  1462. final_value = float(total_values[-1])
  1463. initial_capital = float(initial_capitals[0])
  1464. total_return = final_value - initial_capital
  1465. return_rate = total_return / initial_capital if initial_capital > 0 else 0
  1466. max_value = float(np.max(total_values))
  1467. min_value = float(np.min(total_values))
  1468. # 获取最新的策略类型统计
  1469. final_bull_count = account_df['牛差策略总数'].iloc[-1] if '牛差策略总数' in account_df.columns else 0
  1470. ax1.text(0.02, 0.98,
  1471. f'当前账户价值: {final_value:,.0f}元\n'
  1472. f'总收益: {total_return:,.0f}元\n'
  1473. f'收益率: {return_rate:.2%}\n'
  1474. f'最高价值: {max_value:,.0f}元\n'
  1475. f'最低价值: {min_value:,.0f}元\n'
  1476. f'当前持仓:牛差{final_bull_count}个',
  1477. transform=ax1.transAxes, fontsize=10, verticalalignment='top',
  1478. bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8))
  1479. plt.tight_layout()
  1480. plt.show()
  1481. except Exception as e:
  1482. print(f"绘图过程中出错: {e}")
  1483. print(f"账户数据样本:\n{account_df[['交易日期', '总资金', '账户总价值', '剩余现金', '组合当前市值', '总浮动盈亏']].head()}")
  1484. # 尝试简化版本的绘图
  1485. try:
  1486. fig2, ax = plt.subplots(1, 1, figsize=(10, 6))
  1487. ax.plot(range(len(account_df)), account_df['账户总价值'].astype(float),
  1488. label='账户总价值', color='green', linewidth=2)
  1489. ax.set_title('简化版账户资金曲线')
  1490. ax.set_ylabel('资金 (元)')
  1491. ax.set_xlabel('交易日序号')
  1492. ax.legend()
  1493. ax.grid(True)
  1494. plt.show()
  1495. except Exception as e2:
  1496. print(f"简化绘图也失败: {e2}")
  1497. def run_all_strategies(self):
  1498. """运行所有策略"""
  1499. print("="*60)
  1500. print("开始运行多标的深度实值买购和卖购组合的牛差策略")
  1501. print("="*60)
  1502. # 获取统一的交易日历(取第一个策略的交易日历作为基准)
  1503. if not self.strategies:
  1504. print("没有可运行的策略")
  1505. return {}
  1506. first_strategy = list(self.strategies.values())[0]
  1507. trade_days = first_strategy.trade_days.index
  1508. # 为每个策略设置动态合约数量计算方法
  1509. for etf_code, strategy in self.strategies.items():
  1510. original_open_method = strategy.open_position # 使用新的统一开仓方法
  1511. def make_dynamic_open_position(strat, orig_method):
  1512. def dynamic_open_position(trade_date, etf_price, position_type='main', silent=False, save_to_csv=True):
  1513. # 首先用原方法获取期权信息,但不保存到CSV
  1514. original_contract_size = strat.config['contract_size']
  1515. strat.config['contract_size'] = 1 # 临时设置为1张以获取期权信息
  1516. temp_result = orig_method(trade_date, etf_price, position_type, silent=True, save_to_csv=False)
  1517. if temp_result:
  1518. # 根据策略类型获取期权信息进行动态计算
  1519. strategy_type = temp_result.get('strategy_type', StrategyType.BULL_SPREAD)
  1520. sell_call_info = temp_result['sell_call']
  1521. buy_call_info = temp_result['buy_call'] # 两种策略都有买购期权
  1522. # 两种策略都使用买购和卖购期权信息
  1523. dynamic_size = self.calculate_dynamic_contract_size(strat, etf_price, buy_call_info, sell_call_info)
  1524. # 验证动态合约数量,确保为正数
  1525. if dynamic_size <= 0:
  1526. print(f" {strat.config['etf_code']}: 动态合约数量计算结果无效({dynamic_size}),使用默认值30张")
  1527. dynamic_size = 30 # 使用默认值
  1528. # 更新合约数量并重新开仓
  1529. strat.config['contract_size'] = dynamic_size
  1530. strat._validate_contract_size() # 验证合约数量
  1531. # 移除临时仓位和交易记录
  1532. if strat.positions and strat.positions[-1] == temp_result:
  1533. strat.positions.pop()
  1534. if strat.trade_records and len(strat.trade_records) > 0:
  1535. # 检查最后一条记录是否是刚刚添加的临时记录
  1536. last_record = strat.trade_records[-1]
  1537. if (last_record.get('交易类型') == '开仓' and
  1538. not last_record.get('平仓原因') and
  1539. last_record.get('合约数量') == 1):
  1540. strat.trade_records.pop()
  1541. # 重新开仓,这次保存到CSV
  1542. result = orig_method(trade_date, etf_price, position_type, silent, save_to_csv)
  1543. else:
  1544. # 第一次调用失败,恢复原始合约数量设置,用传入的silent参数重新调用显示失败详情
  1545. strat.config['contract_size'] = original_contract_size
  1546. result = orig_method(trade_date, etf_price, position_type, silent, save_to_csv)
  1547. return result
  1548. # 恢复原始设置
  1549. strat.config['contract_size'] = original_contract_size
  1550. return result
  1551. return dynamic_open_position
  1552. strategy.open_position = make_dynamic_open_position(strategy, original_open_method) # 更新方法名
  1553. # 按日期统一运行所有策略
  1554. print(f"开始按日期统一运行策略,共{len(trade_days)}个交易日")
  1555. for i, trade_date in enumerate(trade_days):
  1556. # print(f"\n处理交易日 {trade_date.strftime('%Y-%m-%d')} ({i+1}/{len(trade_days)})")
  1557. # 为每个策略处理当天的交易
  1558. daily_has_activity = False
  1559. for etf_code, strategy in self.strategies.items():
  1560. try:
  1561. # 获取ETF价格
  1562. price_data = get_price(strategy.underlying_symbol, trade_date, trade_date, fields=['close'])['close']
  1563. if price_data.empty:
  1564. print(f" {etf_code}: 获取ETF价格失败")
  1565. continue
  1566. etf_price = price_data.iloc[0]
  1567. # 记录每日持仓状况(但不触发账户汇总回调)
  1568. strategy.record_daily_positions(trade_date, etf_price)
  1569. # 检查是否需要平仓
  1570. if len(strategy.positions) > 0:
  1571. for position in strategy.positions:
  1572. should_close, reason = strategy.should_close_position(position, trade_date, etf_price)
  1573. if should_close:
  1574. strategy.close_position(position, trade_date, etf_price, reason)
  1575. print(f" {etf_code}: 平仓 {reason}, ETF价格: {etf_price:.4f}")
  1576. daily_has_activity = True
  1577. # 检查是否需要开新仓(首次开仓或平仓后重新开仓)
  1578. open_positions = [p for p in strategy.positions if p['status'] == 'open']
  1579. if not open_positions:
  1580. new_position = strategy.open_position(trade_date, etf_price, 'main', silent=False) # 使用新的统一开仓方法
  1581. if new_position:
  1582. max_profit = new_position['profit_info']['total_max_profit']
  1583. contract_size = new_position['contract_size']
  1584. # 牛差策略输出
  1585. strategy_desc = f"牛差组合策略"
  1586. profit_desc = f"最大牛差收益: {max_profit:.2f}元"
  1587. print(f" {etf_code}: 开仓主仓位({strategy_desc}),ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
  1588. daily_has_activity = True
  1589. # else:
  1590. # print(f" {etf_code}: 开仓失败,无法找到合适的期权组合,ETF价格: {etf_price:.4f}")
  1591. # 检查是否需要加仓
  1592. elif strategy.should_add_position(trade_date, etf_price):
  1593. add_position = strategy.open_position(trade_date, etf_price, 'add', silent=False) # 使用新的统一开仓方法
  1594. if add_position:
  1595. max_profit = add_position['profit_info']['total_max_profit']
  1596. contract_size = add_position['contract_size']
  1597. # 牛差策略输出
  1598. strategy_desc = f"牛差组合策略"
  1599. profit_desc = f"最大牛差收益: {max_profit:.2f}元"
  1600. print(f" {etf_code}: 加仓({strategy_desc}),ETF价格: {etf_price:.4f}, 合约数量: {contract_size}张, {profit_desc}")
  1601. daily_has_activity = True
  1602. # 更新策略的前一日ETF价格
  1603. strategy.previous_etf_price = etf_price
  1604. except Exception as e:
  1605. print(f" {etf_code}: 处理失败 - {e}")
  1606. # 统一记录当天的账户汇总(无论是否有交易活动)
  1607. try:
  1608. self.record_daily_account_summary(trade_date)
  1609. if daily_has_activity:
  1610. print(f" 账户汇总已更新")
  1611. except Exception as e:
  1612. print(f" 记录账户汇总失败: {e}")
  1613. # 收集所有策略的结果
  1614. results = {}
  1615. for etf_code, strategy in self.strategies.items():
  1616. try:
  1617. results[etf_code] = {
  1618. 'strategy': strategy,
  1619. 'summary': strategy.get_performance_summary(),
  1620. 'allocated_capital': strategy.config['allocated_capital']
  1621. }
  1622. print(f"{etf_code}策略运行完成")
  1623. except Exception as e:
  1624. print(f"{etf_code}策略结果收集出错: {e}")
  1625. results[etf_code] = {'error': str(e)}
  1626. print("\n所有策略运行完成!")
  1627. return results
  1628. def generate_overall_report(self, results):
  1629. """生成总体报告"""
  1630. print("\n" + "="*60)
  1631. print("多标的牛差策略总体报告")
  1632. print("="*60)
  1633. total_pnl = 0
  1634. total_trades = 0
  1635. total_winning_trades = 0
  1636. for etf_code, result in results.items():
  1637. if 'error' in result:
  1638. print(f"\n{etf_code}: 策略执行出错 - {result['error']}")
  1639. continue
  1640. strategy = result['strategy']
  1641. allocated_capital = result['allocated_capital']
  1642. print(f"\n{etf_code} 策略结果:")
  1643. print(f"分配资金: {allocated_capital:,.0f}元")
  1644. print(result['summary'])
  1645. # 统计总体数据
  1646. closed_positions = [p for p in strategy.positions if p['status'] == 'closed']
  1647. if closed_positions:
  1648. strategy_pnl = sum(p['pnl'] for p in closed_positions)
  1649. strategy_trades = len(closed_positions)
  1650. strategy_winning = len([p for p in closed_positions if p['pnl'] > 0])
  1651. total_pnl += strategy_pnl
  1652. total_trades += strategy_trades
  1653. total_winning_trades += strategy_winning
  1654. # 总体统计
  1655. if total_trades > 0:
  1656. overall_win_rate = total_winning_trades / total_trades
  1657. print(f"\n总体策略表现:")
  1658. print(f"总交易次数: {total_trades}")
  1659. print(f"总获利交易: {total_winning_trades}")
  1660. print(f"总体胜率: {overall_win_rate:.2%}")
  1661. print(f"总盈亏: {total_pnl:.2f}元")
  1662. print(f"平均每笔盈亏: {total_pnl/total_trades:.2f}元")
  1663. print(f"总资金收益率: {total_pnl/self.config.capital_config['total_capital']:.2%}")
  1664. # 使用示例
  1665. def run_deep_itm_bull_spread_example():
  1666. """运行深度实值买购和卖购组合牛差策略示例"""
  1667. # 创建策略配置
  1668. config = StrategyConfig()
  1669. # 可以自定义配置
  1670. config.time_config = {
  1671. 'start_date': '2025-06-15',
  1672. 'end_date': '2025-08-25'
  1673. }
  1674. config.capital_config.update({
  1675. 'total_capital': 1000000, # 100万总资金
  1676. 'capital_allocation': {
  1677. '50ETF': 0.5, # 50ETF 50%
  1678. '300ETF': 0.3, # 300ETF 30%
  1679. '创业板ETF': 0.2 # 创业板ETF 20%
  1680. },
  1681. 'capital_usage_limit': 0.8, # 使用80%资金
  1682. 'bull_spread_margin_discount': 30, # 牛差组合策略保证金优惠(每单位组合加收30元)
  1683. 'contract_unit': 10000, # 合约单位
  1684. 'margin_params': { # 保证金计算参数(按交易所规定)
  1685. 'volatility_factor': 0.12, # 波动率因子12%
  1686. 'min_margin_factor': 0.07 # 最小保证金因子7%
  1687. }
  1688. })
  1689. # 创建多标的策略管理器
  1690. manager = MultiUnderlyingBullSpreadManager(config)
  1691. # 运行所有策略
  1692. results = manager.run_all_strategies()
  1693. # 生成报告
  1694. manager.generate_overall_report(results)
  1695. # 绘制整体账户资金曲线
  1696. try:
  1697. print(f"\n绘制整体账户资金曲线...")
  1698. manager.plot_account_summary()
  1699. except Exception as e:
  1700. print(f"绘制整体账户资金曲线时出错: {e}")
  1701. # 绘制各策略结果(可选)
  1702. for etf_code, result in results.items():
  1703. if 'strategy' in result:
  1704. try:
  1705. print(f"\n绘制{etf_code}策略结果...")
  1706. result['strategy'].plot_results()
  1707. except Exception as e:
  1708. print(f"绘制{etf_code}图表时出错: {e}")