deep_itm_bull_spread_strategy.py 102 KB

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