deep_itm_bull_spread_strategy.py 86 KB

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