deep_itm_bull_spread_strategy.py 100 KB

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