deep_itm_bull_spread_strategy.py 111 KB

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