future_grid_trading_analysis.py 144 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962
  1. """
  2. 期货网格交易研究分析工具(带主力合约切换)
  3. 研究期货网格交易策略在不同配置下的表现,支持多种交易场景的对比分析
  4. 核心功能包括主力合约自动切换、强制平仓和重新建仓逻辑
  5. 本程序实现完整的网格交易分析流程:
  6. 1. 主力合约监控与切换 - 自动检测主力合约变化并处理切换
  7. 2. 合约选择逻辑 - 基于主力合约选择算法
  8. 3. 基础头寸交易 - 价格-数量网格配置,支持合约切换时重新建仓
  9. 4. 网格交易策略 - 限价订单网格买入卖出,合约切换时根据价格条件重新建仓
  10. 5. 网格对冲策略 - 带止损的增强网格交易,合约切换时根据价格条件重新建仓
  11. 6. 统计分析对比 - 四种交易场景性能分析
  12. 主要特点:
  13. - 主力合约自动监控:每日检测主力合约变化
  14. - 强制平仓机制:合约切换时立即平掉旧合约所有头寸
  15. - 智能重新建仓:根据价格条件在新合约中重新建立头寸
  16. - 标准期货盈亏计算:使用正确的合约倍数和期货盈亏公式
  17. - 最终持仓结算:分析期结束时对所有未平仓头寸进行市值计价
  18. - 完整交易记录:记录所有交易包括合约切换引起的强制平仓
  19. 期货盈亏计算公式:
  20. - 多头:(出场价格 - 入场价格) × 合约倍数 × 数量
  21. - 空头:(入场价格 - 出场价格) × 合约倍数 × 数量
  22. 注:程序支持多个核心商品同时分析,生成详细的交易记录和统计报告
  23. 作者: jukuan研究团队
  24. 日期: 2025-09
  25. 适用平台: 聚宽在线研究平台
  26. """
  27. import pandas as pd
  28. import numpy as np
  29. from jqdata import *
  30. import datetime
  31. import warnings
  32. warnings.filterwarnings('ignore')
  33. # =====================================================================================
  34. # 分析配置参数 - 集中配置部分
  35. # =====================================================================================
  36. class GridTradingConfig:
  37. """期货网格交易分析配置参数"""
  38. # ==================== 时间范围设置 ====================
  39. START_DATE = datetime.datetime(2024, 11, 7) # 分析开始日期
  40. END_DATE = datetime.datetime(2025, 9, 19) # 分析结束日期(修正为9月19日避免数据缺失)
  41. # ==================== 期货合约倍数配置 ====================
  42. FUTURES_MULTIPLIER = {
  43. # 贵金属
  44. 'AU': 1000, # 黄金
  45. 'AG': 15, # 白银
  46. # 有色金属
  47. 'CU': 5, 'AL': 5, 'ZN': 5, 'PB': 5, 'NI': 1, 'SN': 1, 'SS': 5,
  48. # 黑色系
  49. 'RB': 10, 'HC': 10, 'I': 100, 'JM': 100, 'J': 60,
  50. # 能源化工
  51. 'SP': 10, 'FU': 10, 'BU': 10, 'RU': 10, 'BR': 5, 'SC': 1000,
  52. 'NR': 10, 'LU': 10, 'LC': 1,
  53. # 化工
  54. 'FG': 20, 'TA': 5, 'MA': 10, 'SA': 20, 'L': 5, 'V': 5, 'EG': 10,
  55. 'PP': 5, 'EB': 5, 'PG': 20,
  56. # 农产品
  57. 'RM': 10, 'OI': 10, 'CF': 5, 'SR': 10, 'PF': 5, 'C': 10, 'CS': 10,
  58. 'CY': 5, 'A': 10, 'B': 10, 'M': 10, 'Y': 10, 'P': 10,
  59. # 股指期货
  60. 'IF': 300, 'IH': 300, 'IC': 200, 'IM': 200,
  61. # 其他
  62. 'AP': 10, 'CJ': 5, 'PK': 5, 'JD': 10, 'LH': 16
  63. }
  64. # ==================== 核心商品配置 ====================
  65. CORE_COMMODITIES = {
  66. 'SA': ['SA2509.XZCE', 'SA2601.XZCE'], # 纯碱
  67. 'M': ['M2507.XDCE', 'M2605.XDCE'], # 豆粕
  68. # 'MA': ['MA2501.XZCE', 'MA2505.XZCE'], # 甲醇
  69. # 'AU': ['AU2412.XSGE', 'AU2502.XSGE'], # 黄金
  70. # 'AG': ['AG2412.XSGE', 'AG2502.XSGE'], # 白银
  71. # 'RU': ['RU2501.XSGE', 'RU2505.XSGE'], # 橡胶
  72. }
  73. # ==================== 合约切换配置 ====================
  74. REQUIRED_TRADING_DAYS = 30 # 合约切换前需要的最少有效交易日数
  75. # ==================== 基础头寸交易配置 ====================
  76. BASE_POSITION_GRID = {
  77. 'SA': {1400: 4, 1300: 6, 1200: 8, 1100: 12, 1000: 14, 900: 16},
  78. 'M': {2800: 4, 2750: 6, 2700: 8, 2650: 12, 2600: 14, 2550: 16},
  79. # 'MA': {2500: 8, 2400: 8, 2300: 8, 2200: 8, 2100: 8, 2000: 8},
  80. # 'AU': {520: 2, 510: 2, 500: 2, 490: 2, 480: 2, 470: 2},
  81. # 'AG': {6500: 10, 6000: 10, 5500: 10, 5000: 10, 4500: 10, 4000: 10},
  82. # 'RU': {18000: 5, 17000: 5, 16000: 5, 15000: 5, 14000: 5, 13000: 5}
  83. }
  84. # 统一退出价格(无止损)
  85. BASE_POSITION_EXIT_PRICE = {
  86. 'SA': 1500,
  87. 'M': 3800,
  88. # 'MA': 2600,
  89. # 'AU': 530,
  90. # 'AG': 7000,
  91. # 'RU': 19000
  92. }
  93. # ==================== 网格交易配置 ====================
  94. GRID_TRADING_CONFIG = {
  95. 'SA': {
  96. 'start_price': 1250, # 开始价格
  97. 'grid_size': 50, # 网格大小
  98. 'quantity_per_grid': 5, # 每网格数量
  99. 'exit_grid_size': 50 # 退出网格大小
  100. },
  101. 'M': {
  102. 'start_price': 2800,
  103. 'grid_size': 100,
  104. 'quantity_per_grid': 10,
  105. 'exit_grid_size': 100
  106. },
  107. # 'MA': {
  108. # 'start_price': 2300,
  109. # 'grid_size': 50,
  110. # 'quantity_per_grid': 4,
  111. # 'exit_grid_size': 50
  112. # },
  113. # 'AU': {
  114. # 'start_price': 480,
  115. # 'grid_size': 10,
  116. # 'quantity_per_grid': 1,
  117. # 'exit_grid_size': 10
  118. # },
  119. # 'AG': {
  120. # 'start_price': 5500,
  121. # 'grid_size': 200,
  122. # 'quantity_per_grid': 8,
  123. # 'exit_grid_size': 200
  124. # },
  125. # 'RU': {
  126. # 'start_price': 15000,
  127. # 'grid_size': 500,
  128. # 'quantity_per_grid': 3,
  129. # 'exit_grid_size': 500
  130. # }
  131. }
  132. # ==================== 网格对冲策略配置 ====================
  133. GRID_HEDGE_CONFIG = {
  134. 'stop_loss_percentage': 0.02, # 2%止损百分比
  135. 'enable_hedge_strategy': True # 是否启用网格对冲策略
  136. }
  137. # ==================== 输出设置 ====================
  138. OUTPUT_ENCODING = 'utf-8-sig' # 输出文件编码格式
  139. VERBOSE_LOGGING = True # 是否打印详细日志
  140. @classmethod
  141. def print_config(cls):
  142. """打印当前配置信息"""
  143. print("=== 期货网格交易分析配置 ===")
  144. print(f"分析时间范围: {cls.START_DATE.strftime('%Y-%m-%d')} 至 {cls.END_DATE.strftime('%Y-%m-%d')}")
  145. print(f"核心商品数量: {len(cls.CORE_COMMODITIES)}")
  146. print("核心商品列表:")
  147. for commodity, contracts in cls.CORE_COMMODITIES.items():
  148. print(f" {commodity}: {contracts}")
  149. print(f"网格对冲策略: {'启用' if cls.GRID_HEDGE_CONFIG['enable_hedge_strategy'] else '禁用'}")
  150. print(f"止损百分比: {cls.GRID_HEDGE_CONFIG['stop_loss_percentage']*100}%")
  151. print(f"详细日志: {'开启' if cls.VERBOSE_LOGGING else '关闭'}")
  152. print("=" * 50)
  153. class FutureGridTradingAnalyzer:
  154. """期货网格交易分析器"""
  155. def __init__(self, config=None):
  156. """初始化分析器"""
  157. if config is None:
  158. config = GridTradingConfig
  159. self.config = config
  160. self.start_date = config.START_DATE
  161. self.end_date = config.END_DATE
  162. self.core_commodities = config.CORE_COMMODITIES
  163. self.base_position_grid = config.BASE_POSITION_GRID
  164. self.base_position_exit_price = config.BASE_POSITION_EXIT_PRICE
  165. self.grid_trading_config = config.GRID_TRADING_CONFIG
  166. self.grid_hedge_config = config.GRID_HEDGE_CONFIG
  167. self.verbose_logging = config.VERBOSE_LOGGING
  168. self.output_encoding = config.OUTPUT_ENCODING
  169. # 存储结果的字典
  170. self.selected_contracts = {} # 选中的合约
  171. self.price_data = {} # 价格数据
  172. self.dominant_contract_history = {} # 主力合约历史变化
  173. self.active_positions = { # 当前活跃头寸跟踪
  174. 'base_position': {},
  175. 'grid_trading': {},
  176. 'grid_hedge': {}
  177. }
  178. self.trading_results = { # 四种交易场景的结果
  179. 'base_position': [],
  180. 'grid_trading': [],
  181. 'grid_hedge': [],
  182. 'combined': []
  183. }
  184. if self.verbose_logging:
  185. print("初始化期货网格交易分析器")
  186. print(f"核心商品: {list(self.core_commodities.keys())}")
  187. print(f"分析期间: {self.start_date.strftime('%Y-%m-%d')} - {self.end_date.strftime('%Y-%m-%d')}")
  188. def select_contracts(self):
  189. """
  190. 合约选择逻辑
  191. 1. 首先获取商品的主导合约
  192. 2. 如果主导合约在可用列表中,选择它
  193. 3. 如果主导合约不在列表中,选择未来到期日期最近且晚于主导合约的合约
  194. """
  195. if self.verbose_logging:
  196. print("\n=== 步骤1: 合约选择逻辑 ===")
  197. for commodity, available_contracts in self.core_commodities.items():
  198. if self.verbose_logging:
  199. print(f"\n处理商品: {commodity}")
  200. print(f"可用合约: {available_contracts}")
  201. try:
  202. # 获取商品的主导合约
  203. dominant_contract = get_dominant_future(commodity, self.start_date.date())
  204. if self.verbose_logging:
  205. print(f"主导合约: {dominant_contract}")
  206. if dominant_contract in available_contracts:
  207. # 主导合约在可用列表中,直接选择
  208. selected_contract = dominant_contract
  209. if self.verbose_logging:
  210. print(f"选择主导合约: {selected_contract}")
  211. else:
  212. # 主导合约不在列表中,选择最近的未来合约
  213. selected_contract = self._select_nearest_future_contract(
  214. commodity, dominant_contract, available_contracts
  215. )
  216. if self.verbose_logging:
  217. print(f"选择最近的未来合约: {selected_contract}")
  218. self.selected_contracts[commodity] = selected_contract
  219. except Exception as e:
  220. if self.verbose_logging:
  221. print(f"获取{commodity}主导合约失败: {str(e)}")
  222. # 默认选择第一个可用合约
  223. self.selected_contracts[commodity] = available_contracts[0]
  224. if self.verbose_logging:
  225. print(f"默认选择第一个合约: {available_contracts[0]}")
  226. if self.verbose_logging:
  227. print(f"\n合约选择完成,共选择{len(self.selected_contracts)}个合约")
  228. for commodity, contract in self.selected_contracts.items():
  229. print(f" {commodity}: {contract}")
  230. return self.selected_contracts
  231. def _select_nearest_future_contract(self, commodity, dominant_contract, available_contracts):
  232. """选择最近的未来到期合约"""
  233. if not dominant_contract:
  234. return available_contracts[0]
  235. # 解析主导合约的到期月份
  236. try:
  237. # 提取合约代码中的月份信息 (例如 SA2507 -> 2507)
  238. dominant_year_month = dominant_contract.split('.')[0][-4:] # 取最后4位
  239. dominant_year = int(dominant_year_month[:2]) + 2000 # 假设是21世纪
  240. dominant_month = int(dominant_year_month[2:])
  241. except:
  242. return available_contracts[0]
  243. # 找到最近的未来合约
  244. best_contract = available_contracts[0]
  245. best_diff = float('inf')
  246. for contract in available_contracts:
  247. try:
  248. contract_year_month = contract.split('.')[0][-4:]
  249. contract_year = int(contract_year_month[:2]) + 2000
  250. contract_month = int(contract_year_month[2:])
  251. # 计算月份差异
  252. contract_total_months = contract_year * 12 + contract_month
  253. dominant_total_months = dominant_year * 12 + dominant_month
  254. # 只选择晚于主导合约的合约
  255. if contract_total_months > dominant_total_months:
  256. diff = contract_total_months - dominant_total_months
  257. if diff < best_diff:
  258. best_diff = diff
  259. best_contract = contract
  260. except:
  261. continue
  262. return best_contract
  263. def _get_futures_multiplier(self, commodity):
  264. """获取期货合约倍数"""
  265. return self.config.FUTURES_MULTIPLIER.get(commodity, 10) # 默认倍数为10
  266. def _calculate_futures_pnl(self, entry_price, exit_price, quantity, commodity, is_long=True):
  267. """
  268. 计算期货盈亏
  269. 参数:
  270. entry_price: 入场价格
  271. exit_price: 出场价格
  272. quantity: 数量(手数)
  273. commodity: 商品代码
  274. is_long: 是否多头,True为多头,False为空头
  275. 返回:
  276. 实际盈亏金额
  277. """
  278. multiplier = self._get_futures_multiplier(commodity)
  279. if is_long:
  280. # 多头:(出场价格 - 入场价格) × 合约倍数 × 数量
  281. pnl = (exit_price - entry_price) * multiplier * quantity
  282. else:
  283. # 空头:(入场价格 - 出场价格) × 合约倍数 × 数量
  284. pnl = (entry_price - exit_price) * multiplier * quantity
  285. return pnl
  286. def build_dominant_contract_history(self):
  287. """
  288. 构建主力合约历史变化记录
  289. 为每个商品在整个分析期间构建主力合约变化的时间序列
  290. 只有当合约真正发生变化时才记录为合约切换
  291. """
  292. if self.verbose_logging:
  293. print("\n=== 步骤2:构建主力合约历史变化记录 ===")
  294. for commodity in self.core_commodities.keys():
  295. if self.verbose_logging:
  296. print(f"构建 {commodity} 主力合约历史...")
  297. contract_history = []
  298. current_date = self.start_date.date()
  299. end_date = self.end_date.date()
  300. current_selected_contract = None # 跟踪选择的合约而不是主力合约
  301. while current_date <= end_date:
  302. # 跳过非交易日
  303. if current_date.weekday() >= 5: # 周六周日
  304. current_date += datetime.timedelta(days=1)
  305. continue
  306. try:
  307. # 获取当日主力合约
  308. dominant_contract = get_dominant_future(commodity, current_date)
  309. # print(f"日期: {current_date}, 主力合约: {dominant_contract}")
  310. selected_contract = self._match_to_available_contract(commodity, dominant_contract)
  311. # 只有当选择的合约真正发生变化时才记录
  312. if selected_contract != current_selected_contract:
  313. contract_history.append({
  314. 'date': current_date,
  315. 'dominant_contract': dominant_contract,
  316. 'selected_contract': selected_contract,
  317. 'is_initial': current_selected_contract is None # 标记是否为初始合约
  318. })
  319. if self.verbose_logging:
  320. if current_selected_contract is None:
  321. print(f" {current_date}: 初始合约设置为 {selected_contract}")
  322. else:
  323. print(f" {current_date}: 合约切换 {current_selected_contract} -> {selected_contract}")
  324. current_selected_contract = selected_contract
  325. except Exception as e:
  326. if self.verbose_logging:
  327. print(f" 获取 {current_date} 的主力合约时出错: {str(e)}")
  328. current_date += datetime.timedelta(days=1)
  329. self.dominant_contract_history[commodity] = contract_history
  330. if self.verbose_logging:
  331. total_changes = sum(len(history) for history in self.dominant_contract_history.values())
  332. actual_switches = sum(
  333. sum(1 for change in history if not change.get('is_initial', False))
  334. for history in self.dominant_contract_history.values()
  335. )
  336. initial_setups = total_changes - actual_switches
  337. print(f"主力合约历史构建完成,共 {total_changes} 次记录({initial_setups} 次初始设置,{actual_switches} 次真实切换)")
  338. return self.dominant_contract_history
  339. def _match_to_available_contract(self, commodity, dominant_contract):
  340. """将主力合约匹配到可用合约列表"""
  341. available_contracts = self.core_commodities.get(commodity, [])
  342. if dominant_contract in available_contracts:
  343. return dominant_contract
  344. else:
  345. return self._select_nearest_future_contract(commodity, dominant_contract, available_contracts)
  346. def collect_price_data(self):
  347. """收集所有可能用到的合约价格数据(优化日期范围)"""
  348. if self.verbose_logging:
  349. print("\n=== 步骤3: 收集价格数据(优化日期范围) ===")
  350. # 清除之前的调整建议
  351. if hasattr(self, 'adjustment_suggestions'):
  352. self.adjustment_suggestions = []
  353. # 为每个商品创建数据存储结构
  354. for commodity in self.core_commodities.keys():
  355. print(f'收集{commodity}的价格数据:')
  356. self.price_data[commodity] = {}
  357. # 根据主力合约历史确定每个合约的数据获取范围
  358. contract_date_ranges = self._determine_contract_date_ranges(commodity)
  359. for contract, date_range in contract_date_ranges.items():
  360. start_date, end_date = date_range
  361. if self.verbose_logging:
  362. print(f"获取 {contract} 价格数据...")
  363. print(f" 优化日期范围: {start_date} 至 {end_date}")
  364. try:
  365. # 获取价格数据(使用优化的日期范围)
  366. data = get_price(
  367. contract,
  368. start_date=start_date,
  369. end_date=end_date,
  370. frequency='daily',
  371. fields=['open', 'close', 'high', 'low', 'volume'],
  372. skip_paused=False,
  373. panel=False
  374. )
  375. if data is not None and len(data) > 0:
  376. self.price_data[commodity][contract] = data
  377. # 检查这个数据里有多少条空值数据
  378. empty_data = data[data.isna().any(axis=1)]
  379. # 检查有效交易日数据并收集调整建议
  380. adjustment_info = self._check_thirty_day_trading_data(commodity, contract, data, start_date, end_date)
  381. if adjustment_info and adjustment_info.get('needs_adjustment'):
  382. # 暂存调整建议,稍后统一处理
  383. if not hasattr(self, 'adjustment_suggestions'):
  384. self.adjustment_suggestions = []
  385. self.adjustment_suggestions.append(adjustment_info)
  386. if self.verbose_logging:
  387. print(f" ✅ 成功获取{len(data)}条数据记录")
  388. print(f" 空值数据: {len(empty_data)}条")
  389. print(f" 价格范围: {data['low'].min():.2f} - {data['high'].max():.2f}")
  390. print(f" 数据日期范围: {data.index[0].date()} 至 {data.index[-1].date()}")
  391. else:
  392. if self.verbose_logging:
  393. print(f" ⚠️ 未获取到{contract}的数据")
  394. # 如果优化日期范围没有数据,尝试使用更宽泛的日期范围
  395. if self.verbose_logging:
  396. print(f" 尝试使用更宽泛的日期范围获取数据...")
  397. try:
  398. fallback_data = get_price(
  399. contract,
  400. start_date=self.start_date,
  401. end_date=self.end_date,
  402. frequency='daily',
  403. fields=['open', 'close', 'high', 'low', 'volume'],
  404. skip_paused=False,
  405. panel=False
  406. )
  407. if fallback_data is not None and len(fallback_data) > 0:
  408. self.price_data[commodity][contract] = fallback_data
  409. # 检查有效交易日数据并收集调整建议(回退方案)
  410. adjustment_info = self._check_thirty_day_trading_data(commodity, contract, fallback_data, self.start_date, self.end_date)
  411. if adjustment_info and adjustment_info.get('needs_adjustment'):
  412. if not hasattr(self, 'adjustment_suggestions'):
  413. self.adjustment_suggestions = []
  414. self.adjustment_suggestions.append(adjustment_info)
  415. if self.verbose_logging:
  416. print(f" ✅ 回退方案成功获取{len(fallback_data)}条数据记录")
  417. print(f" 数据日期范围: {fallback_data.index[0].date()} 至 {fallback_data.index[-1].date()}")
  418. else:
  419. if self.verbose_logging:
  420. print(f" ❌ 回退方案也未获取到{contract}的数据")
  421. except Exception as fallback_e:
  422. if self.verbose_logging:
  423. print(f" ❌ 回退方案出错: {str(fallback_e)}")
  424. except Exception as e:
  425. if self.verbose_logging:
  426. print(f" ❌ 获取{contract}数据时出错: {str(e)}")
  427. continue
  428. # 处理动态调整建议
  429. if hasattr(self, 'adjustment_suggestions') and self.adjustment_suggestions:
  430. self._apply_dynamic_adjustments()
  431. if self.verbose_logging:
  432. total_contracts = sum(len(contracts) for contracts in self.price_data.values())
  433. print(f"价格数据收集完成,共{total_contracts}个合约")
  434. return self.price_data
  435. def _determine_contract_date_ranges(self, commodity):
  436. """
  437. 根据主力合约历史确定每个合约的最优数据获取日期范围
  438. """
  439. contract_ranges = {}
  440. if commodity not in self.dominant_contract_history:
  441. # 如果没有主力合约历史,使用全范围
  442. for contract in self.core_commodities[commodity]:
  443. contract_ranges[contract] = (self.start_date, self.end_date)
  444. return contract_ranges
  445. contract_history = self.dominant_contract_history[commodity]
  446. # 分析每个合约的活跃期间
  447. for contract in self.core_commodities[commodity]:
  448. contract_start = self.start_date
  449. contract_end = self.end_date
  450. # 查找该合约在主力合约历史中的使用时间段
  451. for i, history_record in enumerate(contract_history):
  452. if history_record['selected_contract'] == contract:
  453. # 该合约开始使用的日期
  454. if history_record.get('is_initial', False):
  455. # 初始设置的合约,从分析开始日期或历史记录日期开始
  456. contract_start = max(self.start_date.date(), history_record['date'])
  457. else:
  458. # 切换到的合约,从切换日期开始
  459. contract_start = history_record['date']
  460. # 查找该合约结束使用的日期
  461. for j in range(i + 1, len(contract_history)):
  462. next_record = contract_history[j]
  463. if next_record['selected_contract'] != contract:
  464. # 找到下一次切换,该合约在此日期结束使用
  465. contract_end = next_record['date']
  466. break
  467. else:
  468. # 该合约一直使用到分析结束
  469. contract_end = self.end_date.date()
  470. break
  471. # 转换为datetime格式并添加缓冲区
  472. if isinstance(contract_start, datetime.date):
  473. contract_start = datetime.datetime.combine(contract_start, datetime.time.min)
  474. if isinstance(contract_end, datetime.date):
  475. contract_end = datetime.datetime.combine(contract_end, datetime.time.max)
  476. # 添加缓冲期(前后各3天)以确保数据充足
  477. buffer_days = 3
  478. contract_start_buffered = contract_start - datetime.timedelta(days=buffer_days)
  479. contract_end_buffered = contract_end + datetime.timedelta(days=buffer_days)
  480. # 确保不超出总体分析范围
  481. contract_start_final = max(contract_start_buffered, self.start_date)
  482. contract_end_final = min(contract_end_buffered, self.end_date)
  483. contract_ranges[contract] = (contract_start_final, contract_end_final)
  484. if self.verbose_logging:
  485. print(f" {contract}: {contract_start_final.date()} 至 {contract_end_final.date()}")
  486. return contract_ranges
  487. def _check_thirty_day_trading_data(self, commodity, contract, data, start_date, end_date):
  488. """
  489. 检查合约是否有足够的有效交易日数据并进行动态调整
  490. 返回调整建议信息
  491. """
  492. if data is None or len(data) == 0:
  493. print(f" ⚠️ {contract}: 无价格数据")
  494. return None
  495. required_days = self.config.REQUIRED_TRADING_DAYS
  496. # 检查空值数据
  497. empty_data = data[data.isna().any(axis=1)]
  498. empty_count = len(empty_data)
  499. # 过滤出非空的收盘价数据
  500. valid_close_data = data['close'].dropna()
  501. valid_count = len(valid_close_data)
  502. print(f" 📊 {contract}: 有效收盘价数据共{valid_count}天")
  503. adjustment_info = {
  504. 'contract': contract,
  505. 'commodity': commodity,
  506. 'empty_count': empty_count,
  507. 'valid_count': valid_count,
  508. 'required_days': required_days,
  509. 'needs_adjustment': False,
  510. 'suggested_switch_date': None
  511. }
  512. # 检查是否有空值数据且需要调整
  513. if empty_count > 0:
  514. print(f" ⚠️ {contract}: 检测到{empty_count}条空值数据")
  515. if valid_count >= required_days:
  516. # 找到第N个有效收盘价的日期
  517. nth_date = valid_close_data.index[required_days - 1] # 索引从0开始
  518. nth_price = valid_close_data.iloc[required_days - 1]
  519. print(f" 📍 {contract}: 第{required_days}个有效收盘价日期为{nth_date.date()},价格{nth_price:.2f}")
  520. # 检查当前切换日期是否需要调整
  521. if commodity in self.dominant_contract_history:
  522. for history_record in self.dominant_contract_history[commodity]:
  523. if (history_record['selected_contract'] == contract and
  524. not history_record.get('is_initial', False)):
  525. current_switch_date = history_record['date']
  526. # 转换日期格式进行比较
  527. if isinstance(current_switch_date, datetime.date):
  528. current_switch_datetime = datetime.datetime.combine(current_switch_date, datetime.time.min)
  529. else:
  530. current_switch_datetime = current_switch_date
  531. if nth_date > current_switch_datetime:
  532. print(f" ❌ {contract}: 切换日期过早(当前:{current_switch_date}),建议调整至{nth_date.date()}")
  533. adjustment_info.update({
  534. 'needs_adjustment': True,
  535. 'suggested_switch_date': nth_date.date(),
  536. 'current_switch_date': current_switch_date
  537. })
  538. else:
  539. print(f" ✅ {contract}: 切换日期{current_switch_date}合理,在第{required_days}个有效交易日之后")
  540. break
  541. else:
  542. print(f" ❌ {contract}: 有效交易日不足{required_days}天(仅{valid_count}天),不符合切换要求")
  543. adjustment_info['needs_adjustment'] = True
  544. else:
  545. # 没有空值数据,检查是否有足够的交易日
  546. if valid_count >= required_days:
  547. nth_date = valid_close_data.index[required_days - 1]
  548. nth_price = valid_close_data.iloc[required_days - 1]
  549. print(f" ✅ {contract}: 第{required_days}个有效收盘价日期为{nth_date.date()},价格{nth_price:.2f}")
  550. else:
  551. print(f" ❌ {contract}: 有效交易日不足{required_days}天(仅{valid_count}天)")
  552. return adjustment_info
  553. def _apply_dynamic_adjustments(self):
  554. """应用动态调整建议,更新合约切换日期并重新获取数据"""
  555. if self.verbose_logging:
  556. print(f"\n=== 应用动态调整建议(共{len(self.adjustment_suggestions)}个) ===")
  557. adjustments_applied = []
  558. for suggestion in self.adjustment_suggestions:
  559. if suggestion.get('suggested_switch_date'):
  560. commodity = suggestion['commodity']
  561. contract = suggestion['contract']
  562. new_switch_date = suggestion['suggested_switch_date']
  563. print(f"📅 调整{commodity}的{contract}切换日期至{new_switch_date}")
  564. # 更新合约历史
  565. if self._update_contract_switch_date(commodity, contract, new_switch_date):
  566. adjustments_applied.append(suggestion)
  567. # 如果有调整,重新获取相关的价格数据
  568. if adjustments_applied:
  569. print(f"✅ 完成{len(adjustments_applied)}个调整,重新获取相关价格数据")
  570. self._refresh_price_data_for_adjustments(adjustments_applied)
  571. def _update_contract_switch_date(self, commodity, contract, new_switch_date):
  572. """更新指定合约的切换日期"""
  573. if commodity not in self.dominant_contract_history:
  574. return False
  575. # 查找并更新对应的历史记录
  576. for history_record in self.dominant_contract_history[commodity]:
  577. if (history_record['selected_contract'] == contract and
  578. not history_record.get('is_initial', False)):
  579. old_date = history_record['date']
  580. history_record['date'] = new_switch_date
  581. print(f" 📝 {contract}: 切换日期从{old_date}更新为{new_switch_date}")
  582. return True
  583. return False
  584. def _refresh_price_data_for_adjustments(self, adjustments):
  585. """为调整的合约重新获取价格数据"""
  586. affected_commodities = set()
  587. for adjustment in adjustments:
  588. commodity = adjustment['commodity']
  589. affected_commodities.add(commodity)
  590. for commodity in affected_commodities:
  591. print(f"🔄 重新获取{commodity}的价格数据...")
  592. # 重新计算日期范围
  593. contract_date_ranges = self._determine_contract_date_ranges(commodity)
  594. # 重新获取每个合约的数据
  595. for contract, date_range in contract_date_ranges.items():
  596. start_date, end_date = date_range
  597. try:
  598. # 获取价格数据(使用新的日期范围)
  599. data = get_price(
  600. contract,
  601. start_date=start_date,
  602. end_date=end_date,
  603. frequency='daily',
  604. fields=['open', 'close', 'high', 'low', 'volume'],
  605. skip_paused=False,
  606. panel=False
  607. )
  608. if data is not None and len(data) > 0:
  609. self.price_data[commodity][contract] = data
  610. # 检查调整后的数据
  611. empty_data = data[data.isna().any(axis=1)]
  612. empty_count = len(empty_data)
  613. print(f" ✅ {contract}: 重新获取{len(data)}条数据记录,空值{empty_count}条")
  614. if empty_count == 0:
  615. print(f" 🎉 {contract}: 空值数据已消除")
  616. except Exception as e:
  617. print(f" ❌ 重新获取{contract}数据时出错: {str(e)}")
  618. def simulate_with_contract_switching(self):
  619. """
  620. 模拟带有主力合约切换逻辑的交易
  621. """
  622. if self.verbose_logging:
  623. print("\n=== 步骤3: 带合约切换的交易模拟 ===")
  624. # 按日期顺序处理所有交易日
  625. current_date = self.start_date.date()
  626. end_date = self.end_date.date()
  627. while current_date <= end_date:
  628. # 跳过非交易日
  629. if current_date.weekday() >= 5:
  630. current_date += datetime.timedelta(days=1)
  631. continue
  632. # 检查每个商品的主力合约切换
  633. for commodity in self.core_commodities.keys():
  634. self._check_and_handle_contract_switch(commodity, current_date)
  635. # 处理正常的交易逻辑
  636. self._process_daily_trading(current_date)
  637. current_date += datetime.timedelta(days=1)
  638. # 在交易循环结束后,计算所有未平仓头寸的最终盈亏
  639. self._calculate_final_positions_pnl()
  640. if self.verbose_logging:
  641. print("带合约切换的交易模拟完成")
  642. def _calculate_final_positions_pnl(self):
  643. """
  644. 计算分析期结束时所有未平仓头寸的最终盈亏
  645. 将这些盈亏作为最终交易记录加入结果中
  646. """
  647. if self.verbose_logging:
  648. print("\n=== 计算最终持仓盈亏 ===")
  649. final_date = self.end_date.date()
  650. final_pnl_records = []
  651. # 添加诊断信息
  652. if self.verbose_logging:
  653. print(f"分析结束日期: {final_date}")
  654. print(f"活跃头寸概览:")
  655. for strategy_name in ['base_position', 'grid_trading', 'grid_hedge']:
  656. strategy_positions = self.active_positions.get(strategy_name, {})
  657. total_positions = 0
  658. open_positions = 0
  659. for commodity, positions in strategy_positions.items():
  660. commodity_total = len(positions)
  661. commodity_open = sum(1 for p in positions.values() if p['status'] == 'open')
  662. total_positions += commodity_total
  663. open_positions += commodity_open
  664. if commodity_total > 0:
  665. print(f" {strategy_name} - {commodity}: 总计 {commodity_total} 个头寸, 未平仓 {commodity_open} 个")
  666. # 详细列出所有头寸信息
  667. print(f" 详细头寸列表:")
  668. for pos_id, pos_info in positions.items():
  669. status = pos_info.get('status', 'Unknown')
  670. entry_price = pos_info.get('entry_price', 'N/A')
  671. contract = pos_info.get('contract', 'N/A')
  672. entry_date = pos_info.get('entry_date', 'N/A')
  673. quantity = pos_info.get('quantity', 'N/A')
  674. print(f" {pos_id}: 状态={status}, 合约={contract}, 开仓价格={entry_price}, 日期={entry_date}, 数量={quantity}")
  675. print(f" {strategy_name} 策略总计: {open_positions}/{total_positions} 个未平仓头寸")
  676. # 验证头寸计数的准确性
  677. actual_count = len(positions)
  678. open_count_verify = len([p for p in positions.values() if p.get('status') == 'open'])
  679. if actual_count != commodity_total or open_count_verify != commodity_open:
  680. print(f" ⚠️ 计数不匹配!实际头寸数: {actual_count}, 预期: {commodity_total}; 实际未平仓: {open_count_verify}, 预期: {commodity_open}")
  681. # 检查是否有重复的开仓价格(同一合约同一状态)
  682. open_positions_by_price = {}
  683. for pos_id, pos_info in positions.items():
  684. if pos_info.get('status') == 'open':
  685. price = pos_info.get('entry_price')
  686. contract = pos_info.get('contract')
  687. key = f"{contract}_{price}"
  688. if key not in open_positions_by_price:
  689. open_positions_by_price[key] = []
  690. open_positions_by_price[key].append(pos_id)
  691. # for key, pos_ids in open_positions_by_price.items():
  692. # if len(pos_ids) > 1:
  693. # print(f" ⚠️ 发现重复的未平仓头寸: {key} -> {pos_ids}")
  694. print(f" {strategy_name} 策略总计: {open_positions}/{total_positions} 个未平仓头寸")
  695. for strategy_name in ['base_position', 'grid_trading', 'grid_hedge']:
  696. strategy_positions = self.active_positions.get(strategy_name, {})
  697. for commodity, positions in strategy_positions.items():
  698. # 获取当前合约和最终价格
  699. current_contract = self._get_current_contract(commodity, final_date)
  700. if not current_contract:
  701. if self.verbose_logging:
  702. print(f" 警告: 无法确定 {commodity} 在 {final_date} 的当前合约")
  703. continue
  704. final_price = self._get_price_on_date(commodity, current_contract, final_date, 'close')
  705. if final_price is None:
  706. if self.verbose_logging:
  707. print(f" 警告: 无法获取 {commodity} {current_contract} 在 {final_date} 的价格")
  708. continue
  709. if self.verbose_logging and len(positions) > 0:
  710. print(f" {commodity} {strategy_name}: 当前合约 {current_contract}, 结算价格 {final_price:.2f}")
  711. for position_id, position in positions.items():
  712. if self.verbose_logging:
  713. print(f" 检查头寸 {position_id}: 状态={position['status']}, 合约={position['contract']}, 开仓价格={position.get('entry_price', 'N/A')}")
  714. if position['status'] == 'open' and position['contract'] == current_contract:
  715. if self.verbose_logging:
  716. print(f" 匹配头寸进行结算: {position_id}")
  717. print(f" 头寸详情: 开仓日期={position.get('entry_date', 'N/A')}, 开仓价格={position['entry_price']}, 数量={position.get('quantity', 'N/A')}")
  718. # 计算最终盈亏
  719. if strategy_name == 'grid_hedge':
  720. # 网格对冲是做空
  721. profit_loss = self._calculate_futures_pnl(
  722. position['entry_price'], final_price, position['quantity'], commodity, is_long=False
  723. )
  724. else:
  725. # 基础头寸和网格交易都是做多
  726. profit_loss = self._calculate_futures_pnl(
  727. position['entry_price'], final_price, position['quantity'], commodity, is_long=True
  728. )
  729. profit_loss_pct = (final_price - position['entry_price']) / position['entry_price']
  730. if strategy_name == 'grid_hedge':
  731. profit_loss_pct = (position['entry_price'] - final_price) / position['entry_price']
  732. # 计算持有天数
  733. entry_date = datetime.datetime.strptime(position['entry_date'], '%Y-%m-%d').date()
  734. days_held = (final_date - entry_date).days
  735. # 创建最终持仓盈亏记录
  736. final_record = {
  737. 'commodity': commodity,
  738. 'contract': current_contract,
  739. 'strategy': strategy_name,
  740. 'entry_date': position['entry_date'],
  741. 'exit_date': final_date.strftime('%Y-%m-%d'),
  742. 'entry_price': position['entry_price'],
  743. 'exit_price': final_price,
  744. 'quantity': position['quantity'],
  745. 'profit_loss': profit_loss,
  746. 'profit_loss_pct': profit_loss_pct,
  747. 'days_held': days_held,
  748. 'exit_reason': 'final_settlement'
  749. }
  750. if self.verbose_logging:
  751. print(f" 创建最终结算记录: 头寸ID={position_id}, 开仓价格={position['entry_price']}, 结算价格={final_price:.2f}")
  752. final_pnl_records.append(final_record)
  753. # 将头寸标记为已平仓
  754. self.active_positions[strategy_name][commodity][position_id]['status'] = 'final_settled'
  755. if self.verbose_logging:
  756. print(f" {commodity} {strategy_name} 最终结算: {position['entry_price']} -> {final_price:.2f}, 盈亏: {profit_loss:.2f}")
  757. # 将最终盈亏记录添加到交易结果中
  758. for record in final_pnl_records:
  759. strategy_name = record['strategy']
  760. self.trading_results[strategy_name].append(record)
  761. # 更新组合策略结果
  762. combined_final_records = []
  763. for record in final_pnl_records:
  764. if record['strategy'] in ['grid_trading', 'grid_hedge']:
  765. combined_record = record.copy()
  766. combined_record['strategy'] = f"combined_{record['strategy']}"
  767. combined_final_records.append(combined_record)
  768. self.trading_results['combined'].extend(combined_final_records)
  769. if self.verbose_logging:
  770. total_final_records = len(final_pnl_records)
  771. total_final_pnl = sum(record['profit_loss'] for record in final_pnl_records)
  772. print(f"最终持仓结算完成,共 {total_final_records} 个头寸,总未实现盈亏: {total_final_pnl:.2f}")
  773. # 显示所有最终结算记录的详情
  774. if final_pnl_records:
  775. print(f"最终结算记录详情:")
  776. for i, record in enumerate(final_pnl_records, 1):
  777. print(f" {i}. {record['commodity']} {record['strategy']}: {record['entry_price']} -> {record['exit_price']:.2f}, 盈亏: {record['profit_loss']:.2f}, 合约: {record['contract']}")
  778. def _check_and_handle_contract_switch(self, commodity, current_date):
  779. """
  780. 检查并处理主力合约切换
  781. 只有真正的合约切换才会触发平仓和重新建仓,初始设置不会
  782. """
  783. if commodity not in self.dominant_contract_history:
  784. return
  785. # 检查当天是否有合约变化
  786. contract_changes = self.dominant_contract_history[commodity]
  787. for change in contract_changes:
  788. if change['date'] == current_date:
  789. # 检查是否为初始合约设置
  790. if change.get('is_initial', False):
  791. # 初始合约设置,不需要平仓和重新建仓,只需要启动正常交易逻辑
  792. if self.verbose_logging:
  793. print(f"\n{current_date}: {commodity} 初始合约设置为 {change['selected_contract']}")
  794. return
  795. # 真正的合约切换
  796. old_contract = self._get_current_contract(commodity, current_date - datetime.timedelta(days=1))
  797. new_contract = change['selected_contract']
  798. if self.verbose_logging:
  799. print(f"\n{current_date}: {commodity} 合约切换 {old_contract} -> {new_contract}")
  800. # 平掉旧合约的所有头寸
  801. self._close_all_positions_on_switch(commodity, old_contract, current_date)
  802. # 在新合约中重新建仓
  803. self._reestablish_positions_in_new_contract(commodity, new_contract, current_date)
  804. break
  805. def _get_current_contract(self, commodity, date):
  806. """获取指定日期的当前合约"""
  807. if commodity not in self.dominant_contract_history:
  808. return None
  809. contract_changes = self.dominant_contract_history[commodity]
  810. current_contract = None
  811. for change in contract_changes:
  812. if change['date'] <= date:
  813. current_contract = change['selected_contract']
  814. else:
  815. break
  816. return current_contract
  817. def _close_all_positions_on_switch(self, commodity, old_contract, switch_date):
  818. """
  819. 在合约切换时平掉旧合约的所有头寸
  820. """
  821. if self.verbose_logging:
  822. print(f" 平掉 {old_contract} 的所有头寸")
  823. # 获取当日收盘价
  824. close_price = self._get_price_on_date(commodity, old_contract, switch_date, 'close')
  825. if close_price is None:
  826. if self.verbose_logging:
  827. print(f" 无法获取 {switch_date} 的价格数据,跳过平仓")
  828. return
  829. # 平掉基础头寸交易的头寸
  830. if commodity in self.active_positions['base_position']:
  831. positions = self.active_positions['base_position'][commodity].copy()
  832. for position_id, position in positions.items():
  833. if position['contract'] == old_contract and position['status'] == 'open':
  834. # 使用正确的期货盈亏计算公式(基础头寸都是多头)
  835. profit_loss = self._calculate_futures_pnl(
  836. position['entry_price'], close_price, position['quantity'], commodity, is_long=True
  837. )
  838. profit_loss_pct = (close_price - position['entry_price']) / position['entry_price']
  839. trade_record = {
  840. 'commodity': commodity,
  841. 'contract': old_contract,
  842. 'strategy': 'base_position',
  843. 'entry_date': position['entry_date'],
  844. 'exit_date': switch_date.strftime('%Y-%m-%d'),
  845. 'entry_price': position['entry_price'],
  846. 'exit_price': close_price,
  847. 'quantity': position['quantity'],
  848. 'profit_loss': profit_loss,
  849. 'profit_loss_pct': profit_loss_pct,
  850. 'days_held': (switch_date - datetime.datetime.strptime(position['entry_date'], '%Y-%m-%d').date()).days,
  851. 'exit_reason': 'contract_switch'
  852. }
  853. self.trading_results['base_position'].append(trade_record)
  854. self.active_positions['base_position'][commodity][position_id]['status'] = 'closed'
  855. self.active_positions['base_position'][commodity][position_id]['close_reason'] = 'contract_switch'
  856. if self.verbose_logging:
  857. print(f" 基础头寸平仓: {position['entry_price']} -> {close_price:.2f}, 盈亏: {profit_loss:.2f}")
  858. # 平掉网格交易的头寸
  859. if commodity in self.active_positions['grid_trading']:
  860. positions = self.active_positions['grid_trading'][commodity].copy()
  861. for position_id, position in positions.items():
  862. if position['contract'] == old_contract and position['status'] == 'open':
  863. # 使用正确的期货盈亏计算公式(网格交易都是多头)
  864. profit_loss = self._calculate_futures_pnl(
  865. position['entry_price'], close_price, position['quantity'], commodity, is_long=True
  866. )
  867. profit_loss_pct = (close_price - position['entry_price']) / position['entry_price']
  868. trade_record = {
  869. 'commodity': commodity,
  870. 'contract': old_contract,
  871. 'strategy': 'grid_trading',
  872. 'entry_date': position['entry_date'],
  873. 'exit_date': switch_date.strftime('%Y-%m-%d'),
  874. 'entry_price': position['entry_price'],
  875. 'exit_price': close_price,
  876. 'quantity': position['quantity'],
  877. 'profit_loss': profit_loss,
  878. 'profit_loss_pct': profit_loss_pct,
  879. 'days_held': (switch_date - datetime.datetime.strptime(position['entry_date'], '%Y-%m-%d').date()).days,
  880. 'exit_reason': 'contract_switch'
  881. }
  882. self.trading_results['grid_trading'].append(trade_record)
  883. self.active_positions['grid_trading'][commodity][position_id]['status'] = 'closed'
  884. self.active_positions['grid_trading'][commodity][position_id]['close_reason'] = 'contract_switch'
  885. if self.verbose_logging:
  886. print(f" 网格头寸平仓: {position['entry_price']} -> {close_price:.2f}, 盈亏: {profit_loss:.2f}")
  887. # 平掉网格对冲的头寸
  888. if commodity in self.active_positions['grid_hedge']:
  889. positions = self.active_positions['grid_hedge'][commodity].copy()
  890. for position_id, position in positions.items():
  891. if position['contract'] == old_contract and position['status'] == 'open':
  892. # 使用正确的期货盈亏计算公式(网格对冲是做空)
  893. profit_loss = self._calculate_futures_pnl(
  894. position['entry_price'], close_price, position['quantity'], commodity, is_long=False
  895. )
  896. profit_loss_pct = (position['entry_price'] - close_price) / position['entry_price']
  897. trade_record = {
  898. 'commodity': commodity,
  899. 'contract': old_contract,
  900. 'strategy': 'grid_hedge',
  901. 'entry_date': position['entry_date'],
  902. 'exit_date': switch_date.strftime('%Y-%m-%d'),
  903. 'entry_price': position['entry_price'],
  904. 'exit_price': close_price,
  905. 'quantity': position['quantity'],
  906. 'profit_loss': profit_loss,
  907. 'profit_loss_pct': profit_loss_pct,
  908. 'days_held': (switch_date - datetime.datetime.strptime(position['entry_date'], '%Y-%m-%d').date()).days,
  909. 'exit_reason': 'contract_switch'
  910. }
  911. self.trading_results['grid_hedge'].append(trade_record)
  912. self.active_positions['grid_hedge'][commodity][position_id]['status'] = 'closed'
  913. self.active_positions['grid_hedge'][commodity][position_id]['close_reason'] = 'contract_switch'
  914. if self.verbose_logging:
  915. print(f" 对冲头寸平仓: {position['entry_price']} -> {close_price:.2f}, 盈亏: {profit_loss:.2f}")
  916. def _reestablish_positions_in_new_contract(self, commodity, new_contract, switch_date):
  917. """
  918. 在新合约中重新建仓
  919. """
  920. if self.verbose_logging:
  921. print(f" 在 {new_contract} 中重新建仓")
  922. # 获取当日收盘价
  923. close_price = self._get_price_on_date(commodity, new_contract, switch_date, 'close')
  924. if close_price is None:
  925. if self.verbose_logging:
  926. print(f" 无法获取 {switch_date} 的价格数据,跳过重新建仓")
  927. return
  928. # 基础头寸交易重新建仓
  929. self._reestablish_base_positions(commodity, new_contract, close_price, switch_date)
  930. # 网格交易重新建仓
  931. self._reestablish_grid_positions(commodity, new_contract, close_price, switch_date)
  932. # 网格对冲重新建仓
  933. self._reestablish_hedge_positions(commodity, new_contract, close_price, switch_date)
  934. def _reestablish_base_positions(self, commodity, new_contract, close_price, switch_date):
  935. """重新建立基础头寸"""
  936. if commodity not in self.base_position_grid:
  937. return
  938. # 获取之前被平掉的基础头寸信息(按价格水平记录)
  939. closed_positions = {} # price_level -> quantity
  940. if commodity in self.active_positions['base_position']:
  941. for position in self.active_positions['base_position'][commodity].values():
  942. if position['status'] == 'closed' and 'contract_switch' in position.get('close_reason', ''):
  943. # 只处理因合约切换而平掉的头寸
  944. original_price = position.get('original_price_level', position['entry_price'])
  945. if original_price not in closed_positions:
  946. closed_positions[original_price] = 0
  947. closed_positions[original_price] += position['quantity']
  948. if self.verbose_logging:
  949. print(f" 发现需重建的基础头寸: {original_price}水平 {position['quantity']}手 (原合约: {position['contract']})")
  950. # 根据当前价格和原始价格水平重建头寸
  951. price_grid = self.base_position_grid[commodity]
  952. reestablish_count = 0
  953. for target_price, configured_quantity in price_grid.items():
  954. # 只有当目标价格大于等于当前价格时才重建头寸
  955. # 这确保了只重建"应该持有"的价格水平头寸
  956. if target_price >= close_price:
  957. # 检查是否有该价格水平的平仓头寸需要重建
  958. if target_price in closed_positions:
  959. quantity_to_reestablish = closed_positions[target_price]
  960. if self.verbose_logging:
  961. print(f" 重建条件检查: {target_price} >= {close_price:.2f} ✓ (重建原有平仓头寸)")
  962. else:
  963. # 当前价格低于目标价格,应该建立该价格水平的头寸
  964. quantity_to_reestablish = configured_quantity
  965. if self.verbose_logging:
  966. print(f" 重建条件检查: {target_price} >= {close_price:.2f} ✓ (建立新头寸)")
  967. else:
  968. # 当前价格高于目标价格,不重建
  969. if self.verbose_logging:
  970. print(f" 重建条件检查: {target_price} >= {close_price:.2f} ✗ (跳过重建)")
  971. continue
  972. if quantity_to_reestablish > 0:
  973. position_id = f"{commodity}_{new_contract}_{switch_date}_base_reestablish_{target_price}"
  974. if commodity not in self.active_positions['base_position']:
  975. self.active_positions['base_position'][commodity] = {}
  976. self.active_positions['base_position'][commodity][position_id] = {
  977. 'contract': new_contract,
  978. 'entry_date': switch_date.strftime('%Y-%m-%d'),
  979. 'entry_price': close_price, # 实际成交价格
  980. 'original_price_level': target_price, # 原始价格水平
  981. 'quantity': quantity_to_reestablish,
  982. 'status': 'open',
  983. 'exit_target': self.base_position_exit_price.get(commodity)
  984. }
  985. if self.verbose_logging:
  986. print(f" 创建重建头寸: {position_id}")
  987. print(f" 实际成交价格: {close_price}, 原始价格水平: {target_price}, 数量: {quantity_to_reestablish}")
  988. reestablish_count += quantity_to_reestablish
  989. if self.verbose_logging:
  990. print(f" 重建基础头寸 {target_price}水平: {quantity_to_reestablish} 手 @ {close_price:.2f}")
  991. if reestablish_count > 0 and self.verbose_logging:
  992. print(f" 基础头寸重建完成,总计: {reestablish_count} 手")
  993. def _reestablish_grid_positions(self, commodity, new_contract, close_price, switch_date):
  994. """重新建立网格交易头寸"""
  995. if commodity not in self.grid_trading_config:
  996. return
  997. config = self.grid_trading_config[commodity]
  998. grid_size = config['grid_size']
  999. quantity_per_grid = config['quantity_per_grid']
  1000. exit_grid_size = config['exit_grid_size']
  1001. # 获取之前的网格头寸信息
  1002. previous_grid_levels = set()
  1003. if commodity in self.active_positions['grid_trading']:
  1004. for position in self.active_positions['grid_trading'][commodity].values():
  1005. if position['status'] == 'closed' and 'contract_switch' in position.get('close_reason', ''):
  1006. # 只处理因合约切换而平掉的头寸
  1007. previous_grid_levels.add(position['entry_price'])
  1008. if self.verbose_logging:
  1009. print(f" 发现需重建的网格头寸: {position['entry_price']}水平 {position['quantity']}手")
  1010. # 仅在原始网格开仓价格大于等于当前价格时重新建仓
  1011. # 这确保了只重建"应该持有"的网格水平头寸
  1012. reestablish_count = 0
  1013. for grid_level in previous_grid_levels:
  1014. if grid_level >= close_price:
  1015. if self.verbose_logging:
  1016. print(f" 网格重建条件检查: {grid_level} >= {close_price:.2f} ✓ (重建网格头寸)")
  1017. position_id = f"{commodity}_{new_contract}_{switch_date}_grid_{grid_level}"
  1018. if commodity not in self.active_positions['grid_trading']:
  1019. self.active_positions['grid_trading'][commodity] = {}
  1020. self.active_positions['grid_trading'][commodity][position_id] = {
  1021. 'contract': new_contract,
  1022. 'entry_date': switch_date.strftime('%Y-%m-%d'),
  1023. 'entry_price': close_price,
  1024. 'original_grid_level': grid_level,
  1025. 'quantity': quantity_per_grid,
  1026. 'status': 'open',
  1027. 'exit_target': grid_level + exit_grid_size # 保持原始退出价格
  1028. }
  1029. reestablish_count += 1
  1030. else:
  1031. if self.verbose_logging:
  1032. print(f" 网格重建条件检查: {grid_level} >= {close_price:.2f} ✗ (跳过重建)")
  1033. continue
  1034. # 同时检查是否需要开立新的网格头寸(价格更低的情况)
  1035. start_price = config['start_price']
  1036. current_level = start_price
  1037. while current_level > close_price:
  1038. current_level -= grid_size
  1039. if current_level not in previous_grid_levels and current_level > 0:
  1040. # 这是一个新的网格水平
  1041. position_id = f"{commodity}_{new_contract}_{switch_date}_grid_new_{current_level}"
  1042. if commodity not in self.active_positions['grid_trading']:
  1043. self.active_positions['grid_trading'][commodity] = {}
  1044. self.active_positions['grid_trading'][commodity][position_id] = {
  1045. 'contract': new_contract,
  1046. 'entry_date': switch_date.strftime('%Y-%m-%d'),
  1047. 'entry_price': close_price,
  1048. 'original_grid_level': current_level,
  1049. 'quantity': quantity_per_grid,
  1050. 'status': 'open',
  1051. 'exit_target': current_level + exit_grid_size
  1052. }
  1053. reestablish_count += 1
  1054. if self.verbose_logging and reestablish_count > 0:
  1055. print(f" 重建网格头寸: {reestablish_count} 个网格 @ {close_price:.2f}")
  1056. def _reestablish_hedge_positions(self, commodity, new_contract, close_price, switch_date):
  1057. """重新建立网格对冲头寸"""
  1058. if not self.grid_hedge_config['enable_hedge_strategy'] or commodity not in self.grid_trading_config:
  1059. return
  1060. config = self.grid_trading_config[commodity]
  1061. grid_size = config['grid_size']
  1062. quantity_per_grid = config['quantity_per_grid']
  1063. stop_loss_pct = self.grid_hedge_config['stop_loss_percentage']
  1064. # 获取之前的对冲头寸信息
  1065. previous_hedge_levels = set()
  1066. if commodity in self.active_positions['grid_hedge']:
  1067. for position in self.active_positions['grid_hedge'][commodity].values():
  1068. if position['status'] == 'closed' and 'contract_switch' in position.get('close_reason', ''):
  1069. # 只处理因合约切换而平掉的头寸
  1070. previous_hedge_levels.add(position['entry_price'])
  1071. if self.verbose_logging:
  1072. print(f" 发现需重建的对冲头寸: {position['entry_price']}水平 {position['quantity']}手")
  1073. # 仅在当前价格高于原始对冲开仓价格时重新建仓
  1074. # 对冲是做空策略,只有价格高于对冲水平时才适合重建做空头寸
  1075. reestablish_count = 0
  1076. for hedge_level in previous_hedge_levels:
  1077. if close_price > hedge_level:
  1078. if self.verbose_logging:
  1079. print(f" 对冲重建条件检查: {close_price:.2f} > {hedge_level} ✓ (重建对冲头寸)")
  1080. position_id = f"{commodity}_{new_contract}_{switch_date}_hedge_{hedge_level}"
  1081. if commodity not in self.active_positions['grid_hedge']:
  1082. self.active_positions['grid_hedge'][commodity] = {}
  1083. self.active_positions['grid_hedge'][commodity][position_id] = {
  1084. 'contract': new_contract,
  1085. 'entry_date': switch_date.strftime('%Y-%m-%d'),
  1086. 'entry_price': close_price,
  1087. 'original_grid_level': hedge_level,
  1088. 'quantity': quantity_per_grid,
  1089. 'status': 'open',
  1090. 'profit_target': hedge_level - grid_size, # 保持原始退出价格
  1091. 'stop_loss': close_price * (1 + stop_loss_pct)
  1092. }
  1093. reestablish_count += 1
  1094. else:
  1095. if self.verbose_logging:
  1096. print(f" 对冲重建条件检查: {close_price:.2f} > {hedge_level} ✗ (跳过重建)")
  1097. continue
  1098. if self.verbose_logging and reestablish_count > 0:
  1099. print(f" 重建对冲头寸: {reestablish_count} 个网格 @ {close_price:.2f}")
  1100. def _get_price_on_date(self, commodity, contract, date, price_type='close'):
  1101. """获取指定日期和合约的价格(增强NaN问题诊断)"""
  1102. if commodity not in self.price_data or contract not in self.price_data[commodity]:
  1103. if self.verbose_logging:
  1104. print(f" ❌ 价格数据不存在: {commodity} -> {contract}")
  1105. return None
  1106. price_data = self.price_data[commodity][contract]
  1107. # 找到日期对应的价格
  1108. target_date = date if isinstance(date, datetime.date) else date.date()
  1109. for idx, row in price_data.iterrows():
  1110. if idx.date() == target_date:
  1111. price_value = row[price_type]
  1112. if self.verbose_logging:
  1113. print(f'{price_type}的价格是: {price_value}')
  1114. # 如果价格为NaN,进行详细诊断
  1115. if pd.isna(price_value):
  1116. self._diagnose_nan_price_issue(commodity, contract, target_date, price_type, row)
  1117. return None
  1118. else:
  1119. return price_value
  1120. # 如果没有找到精确日期,尝试查找最近的交易日
  1121. if self.verbose_logging:
  1122. print(f" ⚠️ 未找到 {contract} 在 {target_date} 的数据,尝试查找最近交易日...")
  1123. return self._get_nearest_trading_day_price(commodity, contract, target_date, price_type)
  1124. def _diagnose_nan_price_issue(self, commodity, contract, date, price_type, row):
  1125. """诊断NaN价格问题的根本原因"""
  1126. if self.verbose_logging:
  1127. print(f" 🔍 NaN价格问题诊断: {commodity} {contract} {date}")
  1128. print(f" 目标价格类型: {price_type}")
  1129. print(f" 该日所有价格数据: 开盘={row['open']}, 收盘={row['close']}, 最高={row['high']}, 最低={row['low']}, 成交量={row['volume']}")
  1130. # 检查是否所有价格都是NaN
  1131. price_fields = ['open', 'close', 'high', 'low']
  1132. nan_fields = [field for field in price_fields if pd.isna(row[field])]
  1133. valid_fields = [field for field in price_fields if not pd.isna(row[field])]
  1134. if len(nan_fields) == len(price_fields):
  1135. print(f" ❌ 所有价格字段都为NaN - 可能该合约在此日期未开始交易")
  1136. else:
  1137. print(f" ⚠️ 部分价格字段为NaN: {nan_fields}")
  1138. print(f" ✅ 有效价格字段: {valid_fields}")
  1139. # 如果有有效价格,尝试使用替代方案
  1140. if valid_fields:
  1141. fallback_price = row[valid_fields[0]]
  1142. print(f" 💡 建议使用替代价格: {valid_fields[0]} = {fallback_price}")
  1143. # 检查成交量是否为0或NaN
  1144. if pd.isna(row['volume']) or row['volume'] == 0:
  1145. print(f" ⚠️ 成交量异常: {row['volume']} - 可能该合约在此日期无交易活动")
  1146. # 检查是否是合约刚上市的情况
  1147. price_data = self.price_data[commodity][contract]
  1148. first_valid_date = None
  1149. for idx, data_row in price_data.iterrows():
  1150. if not pd.isna(data_row['close']):
  1151. first_valid_date = idx.date()
  1152. break
  1153. if first_valid_date and date < first_valid_date:
  1154. print(f" 🔍 合约首次有效交易日: {first_valid_date} (查询日期 {date} 早于首次交易日)")
  1155. print(f" 💡 建议: 合约 {contract} 在 {date} 可能尚未开始交易")
  1156. # 提供解决建议
  1157. print(f" 📋 解决建议:")
  1158. print(f" 1. 检查合约 {contract} 的上市日期")
  1159. print(f" 2. 验证合约代码是否正确")
  1160. print(f" 3. 考虑调整合约切换日期")
  1161. if valid_fields:
  1162. print(f" 4. 临时使用替代价格: {valid_fields[0]} = {row[valid_fields[0]]}")
  1163. def _get_nearest_trading_day_price(self, commodity, contract, target_date, price_type):
  1164. """获取最近交易日的价格"""
  1165. price_data = self.price_data[commodity][contract]
  1166. # 查找最近的交易日(前后5天范围内)
  1167. search_range = 5
  1168. for offset in range(1, search_range + 1):
  1169. # 先查找之后的日期
  1170. future_date = target_date + datetime.timedelta(days=offset)
  1171. for idx, row in price_data.iterrows():
  1172. if idx.date() == future_date:
  1173. price_value = row[price_type]
  1174. if not pd.isna(price_value):
  1175. if self.verbose_logging:
  1176. print(f" ✅ 使用后续交易日 {future_date} 的价格: {price_value}")
  1177. return price_value
  1178. break
  1179. # 再查找之前的日期
  1180. past_date = target_date - datetime.timedelta(days=offset)
  1181. for idx, row in price_data.iterrows():
  1182. if idx.date() == past_date:
  1183. price_value = row[price_type]
  1184. if not pd.isna(price_value):
  1185. if self.verbose_logging:
  1186. print(f" ✅ 使用前期交易日 {past_date} 的价格: {price_value}")
  1187. return price_value
  1188. break
  1189. if self.verbose_logging:
  1190. print(f" ❌ 在 {search_range} 天范围内未找到有效的 {price_type} 价格")
  1191. return None
  1192. def _process_daily_trading(self, current_date):
  1193. """处理每日的正常交易逻辑"""
  1194. for commodity in self.core_commodities.keys():
  1195. current_contract = self._get_current_contract(commodity, current_date)
  1196. if not current_contract:
  1197. continue
  1198. # 获取当日价格数据
  1199. daily_prices = self._get_daily_prices(commodity, current_contract, current_date)
  1200. if not daily_prices:
  1201. continue
  1202. # 检查基础头寸入场和退出机会
  1203. self._check_base_position_trading(commodity, current_contract, current_date, daily_prices)
  1204. # 检查网格交易入场和退出机会
  1205. self._check_grid_trading(commodity, current_contract, current_date, daily_prices)
  1206. # 检查网格对冲入场和退出机会
  1207. self._check_grid_hedge_trading(commodity, current_contract, current_date, daily_prices)
  1208. def _get_daily_prices(self, commodity, contract, date):
  1209. """获取指定日期的价格数据"""
  1210. if commodity not in self.price_data or contract not in self.price_data[commodity]:
  1211. return None
  1212. price_data = self.price_data[commodity][contract]
  1213. for idx, row in price_data.iterrows():
  1214. if idx.date() == date:
  1215. return {
  1216. 'open': row['open'],
  1217. 'close': row['close'],
  1218. 'high': row['high'],
  1219. 'low': row['low'],
  1220. 'volume': row['volume']
  1221. }
  1222. return None
  1223. def _check_base_position_trading(self, commodity, contract, current_date, daily_prices):
  1224. """检查基础头寸交易机会"""
  1225. if commodity not in self.base_position_grid:
  1226. return
  1227. # 检查入场机会
  1228. price_grid = self.base_position_grid[commodity]
  1229. for entry_price, quantity in price_grid.items():
  1230. # 检查是否已经有这个价格水平的头寸(包括重建的头寸)
  1231. position_exists = False
  1232. if commodity in self.active_positions['base_position']:
  1233. for position in self.active_positions['base_position'][commodity].values():
  1234. if (position['contract'] == contract and position['status'] == 'open'):
  1235. # 检查原始价格水平或入场价格
  1236. position_price_level = position.get('original_price_level', position['entry_price'])
  1237. if position_price_level == entry_price:
  1238. position_exists = True
  1239. break
  1240. if not position_exists and daily_prices['low'] <= entry_price <= daily_prices['high']:
  1241. # 建立头寸
  1242. position_id = f"{commodity}_{contract}_{current_date}_base_{entry_price}"
  1243. if commodity not in self.active_positions['base_position']:
  1244. self.active_positions['base_position'][commodity] = {}
  1245. self.active_positions['base_position'][commodity][position_id] = {
  1246. 'contract': contract,
  1247. 'entry_date': current_date.strftime('%Y-%m-%d'),
  1248. 'entry_price': entry_price,
  1249. 'original_price_level': entry_price, # 记录原始价格水平
  1250. 'quantity': quantity,
  1251. 'status': 'open',
  1252. 'exit_target': self.base_position_exit_price.get(commodity)
  1253. }
  1254. if self.verbose_logging:
  1255. print(f" {current_date}: {commodity} 基础头寸入场 @ {entry_price}, 数量: {quantity},当天最低价格为{daily_prices['low']},最高价格为{daily_prices['high']}")
  1256. # print(f" 创建头寸: {position_id}")
  1257. # print(f" 当日价格范围: 最低 {daily_prices['low']}, 最高 {daily_prices['high']}")
  1258. # 检查退出机会
  1259. exit_target = self.base_position_exit_price.get(commodity)
  1260. if exit_target and commodity in self.active_positions['base_position']:
  1261. positions_to_close = []
  1262. for position_id, position in self.active_positions['base_position'][commodity].items():
  1263. if position['contract'] == contract and position['status'] == 'open' and daily_prices['high'] >= exit_target:
  1264. positions_to_close.append(position_id)
  1265. for position_id in positions_to_close:
  1266. position = self.active_positions['base_position'][commodity][position_id]
  1267. exit_price = min(exit_target, daily_prices['high'])
  1268. # 使用正确的期货盈亏计算公式(基础头寸都是多头)
  1269. profit_loss = self._calculate_futures_pnl(
  1270. position['entry_price'], exit_price, position['quantity'], commodity, is_long=True
  1271. )
  1272. profit_loss_pct = (exit_price - position['entry_price']) / position['entry_price']
  1273. trade_record = {
  1274. 'commodity': commodity,
  1275. 'contract': contract,
  1276. 'strategy': 'base_position',
  1277. 'entry_date': position['entry_date'],
  1278. 'exit_date': current_date.strftime('%Y-%m-%d'),
  1279. 'entry_price': position['entry_price'],
  1280. 'exit_price': exit_price,
  1281. 'quantity': position['quantity'],
  1282. 'profit_loss': profit_loss,
  1283. 'profit_loss_pct': profit_loss_pct,
  1284. 'days_held': (current_date - datetime.datetime.strptime(position['entry_date'], '%Y-%m-%d').date()).days,
  1285. 'exit_reason': 'target_reached'
  1286. }
  1287. self.trading_results['base_position'].append(trade_record)
  1288. self.active_positions['base_position'][commodity][position_id]['status'] = 'closed'
  1289. if self.verbose_logging:
  1290. print(f" {current_date}: {commodity} 基础头寸退出 {position['entry_price']} -> {exit_price:.2f}, 盈亏: {profit_loss:.2f}")
  1291. def _check_grid_trading(self, commodity, contract, current_date, daily_prices):
  1292. """检查网格交易机会"""
  1293. if commodity not in self.grid_trading_config:
  1294. return
  1295. config = self.grid_trading_config[commodity]
  1296. start_price = config['start_price']
  1297. grid_size = config['grid_size']
  1298. quantity_per_grid = config['quantity_per_grid']
  1299. exit_grid_size = config['exit_grid_size']
  1300. # 检查入场机会
  1301. current_level = start_price
  1302. while current_level > daily_prices['low'] and current_level > 0:
  1303. position_exists = False
  1304. if commodity in self.active_positions['grid_trading']:
  1305. for position in self.active_positions['grid_trading'][commodity].values():
  1306. if (position['contract'] == contract and
  1307. position.get('original_grid_level', position['entry_price']) == current_level and
  1308. position['status'] == 'open'):
  1309. position_exists = True
  1310. break
  1311. if not position_exists and daily_prices['low'] <= current_level <= daily_prices['high']:
  1312. position_id = f"{commodity}_{contract}_{current_date}_grid_{current_level}"
  1313. if commodity not in self.active_positions['grid_trading']:
  1314. self.active_positions['grid_trading'][commodity] = {}
  1315. self.active_positions['grid_trading'][commodity][position_id] = {
  1316. 'contract': contract,
  1317. 'entry_date': current_date.strftime('%Y-%m-%d'),
  1318. 'entry_price': current_level,
  1319. 'original_grid_level': current_level,
  1320. 'quantity': quantity_per_grid,
  1321. 'status': 'open',
  1322. 'exit_target': current_level + exit_grid_size
  1323. }
  1324. if self.verbose_logging:
  1325. print(f" {current_date}: {commodity} 网格入场 @ {current_level},数量:{quantity_per_grid},当天最低价为{daily_prices['low']},最高价为{daily_prices['high']}")
  1326. current_level -= grid_size
  1327. # 检查退出机会
  1328. if commodity in self.active_positions['grid_trading']:
  1329. positions_to_close = []
  1330. for position_id, position in self.active_positions['grid_trading'][commodity].items():
  1331. if position['contract'] == contract and position['status'] == 'open':
  1332. if daily_prices['high'] >= position['exit_target']:
  1333. positions_to_close.append(position_id)
  1334. for position_id in positions_to_close:
  1335. position = self.active_positions['grid_trading'][commodity][position_id]
  1336. exit_price = position['exit_target']
  1337. # 使用正确的期货盈亏计算公式(网格交易都是多头)
  1338. profit_loss = self._calculate_futures_pnl(
  1339. position['entry_price'], exit_price, position['quantity'], commodity, is_long=True
  1340. )
  1341. profit_loss_pct = (exit_price - position['entry_price']) / position['entry_price']
  1342. trade_record = {
  1343. 'commodity': commodity,
  1344. 'contract': contract,
  1345. 'strategy': 'grid_trading',
  1346. 'entry_date': position['entry_date'],
  1347. 'exit_date': current_date.strftime('%Y-%m-%d'),
  1348. 'entry_price': position['entry_price'],
  1349. 'exit_price': exit_price,
  1350. 'quantity': position['quantity'],
  1351. 'profit_loss': profit_loss,
  1352. 'profit_loss_pct': profit_loss_pct,
  1353. 'days_held': (current_date - datetime.datetime.strptime(position['entry_date'], '%Y-%m-%d').date()).days,
  1354. 'exit_reason': 'target_reached'
  1355. }
  1356. self.trading_results['grid_trading'].append(trade_record)
  1357. self.active_positions['grid_trading'][commodity][position_id]['status'] = 'closed'
  1358. if self.verbose_logging:
  1359. print(f" {current_date}: {commodity} 网格退出 {position['entry_price']} -> {exit_price:.2f}, 盈亏: {profit_loss:.2f} ({profit_loss_pct:.2%})")
  1360. def _check_grid_hedge_trading(self, commodity, contract, current_date, daily_prices):
  1361. """检查网格对冲交易机会"""
  1362. if not self.grid_hedge_config['enable_hedge_strategy'] or commodity not in self.grid_trading_config:
  1363. return
  1364. config = self.grid_trading_config[commodity]
  1365. start_price = config['start_price']
  1366. grid_size = config['grid_size']
  1367. quantity_per_grid = config['quantity_per_grid']
  1368. stop_loss_pct = self.grid_hedge_config['stop_loss_percentage']
  1369. # 检查入场机会(对冲是在start_price之上)
  1370. current_level = start_price + grid_size
  1371. while current_level < daily_prices['high']:
  1372. position_exists = False
  1373. if commodity in self.active_positions['grid_hedge']:
  1374. for position in self.active_positions['grid_hedge'][commodity].values():
  1375. if (position['contract'] == contract and
  1376. position.get('original_grid_level', position['entry_price']) == current_level and
  1377. position['status'] == 'open'):
  1378. position_exists = True
  1379. break
  1380. if not position_exists and daily_prices['low'] <= current_level <= daily_prices['high']:
  1381. position_id = f"{commodity}_{contract}_{current_date}_hedge_{current_level}"
  1382. if commodity not in self.active_positions['grid_hedge']:
  1383. self.active_positions['grid_hedge'][commodity] = {}
  1384. self.active_positions['grid_hedge'][commodity][position_id] = {
  1385. 'contract': contract,
  1386. 'entry_date': current_date.strftime('%Y-%m-%d'),
  1387. 'entry_price': current_level,
  1388. 'original_grid_level': current_level,
  1389. 'quantity': quantity_per_grid,
  1390. 'status': 'open',
  1391. 'profit_target': current_level - grid_size,
  1392. 'stop_loss': current_level * (1 + stop_loss_pct)
  1393. }
  1394. if self.verbose_logging:
  1395. print(f" {current_date}: {commodity} 对冲入场 @ {current_level},数量:{quantity_per_grid},当天最低价为{daily_prices['low']},最高价为{daily_prices['high']}")
  1396. current_level += grid_size
  1397. # 检查退出机会
  1398. if commodity in self.active_positions['grid_hedge']:
  1399. positions_to_close = []
  1400. for position_id, position in self.active_positions['grid_hedge'][commodity].items():
  1401. if position['contract'] == contract and position['status'] == 'open':
  1402. if daily_prices['low'] <= position['profit_target']:
  1403. positions_to_close.append((position_id, position['profit_target'], 'profit_target'))
  1404. elif daily_prices['high'] >= position['stop_loss']:
  1405. positions_to_close.append((position_id, position['stop_loss'], 'stop_loss'))
  1406. for position_id, exit_price, exit_reason in positions_to_close:
  1407. position = self.active_positions['grid_hedge'][commodity][position_id]
  1408. # 使用正确的期货盈亏计算公式(网格对冲是做空)
  1409. profit_loss = self._calculate_futures_pnl(
  1410. position['entry_price'], exit_price, position['quantity'], commodity, is_long=False
  1411. )
  1412. profit_loss_pct = (position['entry_price'] - exit_price) / position['entry_price']
  1413. trade_record = {
  1414. 'commodity': commodity,
  1415. 'contract': contract,
  1416. 'strategy': 'grid_hedge',
  1417. 'entry_date': position['entry_date'],
  1418. 'exit_date': current_date.strftime('%Y-%m-%d'),
  1419. 'entry_price': position['entry_price'],
  1420. 'exit_price': exit_price,
  1421. 'quantity': position['quantity'],
  1422. 'profit_loss': profit_loss,
  1423. 'profit_loss_pct': profit_loss_pct,
  1424. 'days_held': (current_date - datetime.datetime.strptime(position['entry_date'], '%Y-%m-%d').date()).days,
  1425. 'exit_reason': exit_reason
  1426. }
  1427. self.trading_results['grid_hedge'].append(trade_record)
  1428. self.active_positions['grid_hedge'][commodity][position_id]['status'] = 'closed'
  1429. if self.verbose_logging:
  1430. print(f" {current_date}: {commodity} 对冲退出 {position['entry_price']} -> {exit_price:.2f} ({exit_reason}),数量:{position['quantity']},当天最低价为{daily_prices['low']},最高价为{daily_prices['high']}")
  1431. def simulate_base_position_trading(self):
  1432. """
  1433. 模拟基础头寸交易
  1434. 为每种商品配置价格-数量网格,以指定价格水平和数量开立多头头寸
  1435. 所有头寸使用统一的退出价格(无止损)
  1436. """
  1437. if self.verbose_logging:
  1438. print("\n=== 步骤3: 基础头寸交易模拟 ===")
  1439. base_position_results = []
  1440. for commodity in self.price_data.keys():
  1441. if commodity not in self.base_position_grid:
  1442. continue
  1443. price_grid = self.base_position_grid[commodity]
  1444. exit_price = self.base_position_exit_price[commodity]
  1445. price_data = self.price_data[commodity]['data']
  1446. contract = self.price_data[commodity]['contract']
  1447. if self.verbose_logging:
  1448. print(f"\n分析 {commodity} ({contract}) 基础头寸交易")
  1449. print(f"价格网格: {price_grid}")
  1450. print(f"退出价格: {exit_price}")
  1451. # 遍历每个价格水平
  1452. for entry_price, quantity in price_grid.items():
  1453. # 查找触发入场的日期
  1454. entry_dates = []
  1455. for date, row in price_data.iterrows():
  1456. if row['low'] <= entry_price <= row['high']:
  1457. entry_dates.append(date)
  1458. if not entry_dates:
  1459. continue
  1460. # 使用第一个触发日期作为入场点
  1461. entry_date = entry_dates[0]
  1462. # 查找退出点
  1463. exit_date = None
  1464. exit_price_actual = exit_price
  1465. # 在入场后查找价格达到退出价格的日期
  1466. for date, row in price_data.iterrows():
  1467. if date > entry_date and row['high'] >= exit_price:
  1468. exit_date = date
  1469. exit_price_actual = min(exit_price, row['high'])
  1470. break
  1471. # 如果没有达到退出价格,使用最后一日的收盘价退出
  1472. if exit_date is None:
  1473. exit_date = price_data.index[-1]
  1474. exit_price_actual = price_data.iloc[-1]['close']
  1475. # 计算盈亏
  1476. profit_loss = (exit_price_actual - entry_price) * quantity
  1477. profit_loss_pct = (exit_price_actual - entry_price) / entry_price
  1478. trade_record = {
  1479. 'commodity': commodity,
  1480. 'contract': contract,
  1481. 'strategy': 'base_position',
  1482. 'entry_date': entry_date.strftime('%Y-%m-%d'),
  1483. 'exit_date': exit_date.strftime('%Y-%m-%d'),
  1484. 'entry_price': entry_price,
  1485. 'exit_price': exit_price_actual,
  1486. 'quantity': quantity,
  1487. 'profit_loss': profit_loss,
  1488. 'profit_loss_pct': profit_loss_pct,
  1489. 'days_held': (exit_date - entry_date).days
  1490. }
  1491. base_position_results.append(trade_record)
  1492. if self.verbose_logging:
  1493. print(f" 入场: {entry_date.strftime('%Y-%m-%d')} @ {entry_price}, 数量: {quantity}")
  1494. print(f" 出场: {exit_date.strftime('%Y-%m-%d')} @ {exit_price_actual:.2f}")
  1495. print(f" 盈亏: {profit_loss:.2f} ({profit_loss_pct:.2%})")
  1496. self.trading_results['base_position'] = base_position_results
  1497. if self.verbose_logging:
  1498. print(f"\n基础头寸交易模拟完成,共{len(base_position_results)}笔交易")
  1499. return base_position_results
  1500. def simulate_grid_trading(self):
  1501. """
  1502. 模拟网格交易策略
  1503. 从start_price开始,每次价格下降grid_size时买入quantity_per_grid
  1504. 当价格从入场价格上涨exit_grid_size时退出
  1505. """
  1506. if self.verbose_logging:
  1507. print("\n=== 步骤4: 网格交易策略模拟 ===")
  1508. grid_trading_results = []
  1509. for commodity in self.price_data.keys():
  1510. if commodity not in self.grid_trading_config:
  1511. continue
  1512. config = self.grid_trading_config[commodity]
  1513. start_price = config['start_price']
  1514. grid_size = config['grid_size']
  1515. quantity_per_grid = config['quantity_per_grid']
  1516. exit_grid_size = config['exit_grid_size']
  1517. price_data = self.price_data[commodity]['data']
  1518. contract = self.price_data[commodity]['contract']
  1519. if self.verbose_logging:
  1520. print(f"\n分析 {commodity} ({contract}) 网格交易")
  1521. print(f"起始价格: {start_price}, 网格大小: {grid_size}")
  1522. print(f"每网格数量: {quantity_per_grid}, 退出网格大小: {exit_grid_size}")
  1523. # 生成网格价格水平
  1524. grid_levels = []
  1525. current_level = start_price
  1526. min_price = price_data['low'].min()
  1527. while current_level > min_price:
  1528. grid_levels.append(current_level)
  1529. current_level -= grid_size
  1530. # 模拟每个网格水平的交易
  1531. for entry_price in grid_levels:
  1532. exit_price = entry_price + exit_grid_size
  1533. # 查找入场机会
  1534. entry_date = None
  1535. for date, row in price_data.iterrows():
  1536. if row['low'] <= entry_price <= row['high']:
  1537. entry_date = date
  1538. break
  1539. if entry_date is None:
  1540. continue
  1541. # 查找退出机会
  1542. exit_date = None
  1543. exit_price_actual = exit_price
  1544. for date, row in price_data.iterrows():
  1545. if date > entry_date and row['high'] >= exit_price:
  1546. exit_date = date
  1547. exit_price_actual = exit_price
  1548. break
  1549. # 如果没有达到退出价格,使用最后一日收盘价
  1550. if exit_date is None:
  1551. exit_date = price_data.index[-1]
  1552. exit_price_actual = price_data.iloc[-1]['close']
  1553. # 计算盈亏
  1554. profit_loss = (exit_price_actual - entry_price) * quantity_per_grid
  1555. profit_loss_pct = (exit_price_actual - entry_price) / entry_price
  1556. trade_record = {
  1557. 'commodity': commodity,
  1558. 'contract': contract,
  1559. 'strategy': 'grid_trading',
  1560. 'entry_date': entry_date.strftime('%Y-%m-%d'),
  1561. 'exit_date': exit_date.strftime('%Y-%m-%d'),
  1562. 'entry_price': entry_price,
  1563. 'exit_price': exit_price_actual,
  1564. 'quantity': quantity_per_grid,
  1565. 'profit_loss': profit_loss,
  1566. 'profit_loss_pct': profit_loss_pct,
  1567. 'days_held': (exit_date - entry_date).days
  1568. }
  1569. grid_trading_results.append(trade_record)
  1570. if self.verbose_logging:
  1571. print(f" 网格 {entry_price}: {entry_date.strftime('%Y-%m-%d')} -> {exit_date.strftime('%Y-%m-%d')}")
  1572. print(f" 盈亏: {profit_loss:.2f} ({profit_loss_pct:.2%})")
  1573. self.trading_results['grid_trading'] = grid_trading_results
  1574. if self.verbose_logging:
  1575. print(f"\n网格交易模拟完成,共{len(grid_trading_results)}笔交易")
  1576. return grid_trading_results
  1577. def simulate_grid_hedge_strategy(self):
  1578. """
  1579. 模拟网格对冲策略
  1580. 类似于网格交易,但有额外的止损百分比
  1581. """
  1582. if not self.grid_hedge_config['enable_hedge_strategy']:
  1583. if self.verbose_logging:
  1584. print("\n网格对冲策略已禁用,跳过模拟")
  1585. return []
  1586. if self.verbose_logging:
  1587. print("\n=== 步骤5: 网格对冲策略模拟 ===")
  1588. grid_hedge_results = []
  1589. stop_loss_pct = self.grid_hedge_config['stop_loss_percentage']
  1590. for commodity in self.price_data.keys():
  1591. if commodity not in self.grid_trading_config:
  1592. continue
  1593. config = self.grid_trading_config[commodity]
  1594. start_price = config['start_price']
  1595. grid_size = config['grid_size']
  1596. quantity_per_grid = config['quantity_per_grid']
  1597. exit_grid_size = config['exit_grid_size']
  1598. price_data = self.price_data[commodity]['data']
  1599. contract = self.price_data[commodity]['contract']
  1600. if self.verbose_logging:
  1601. print(f"\n分析 {commodity} ({contract}) 网格对冲策略")
  1602. print(f"起始价格: {start_price}, 网格大小: {grid_size}")
  1603. print(f"止损百分比: {stop_loss_pct:.1%}")
  1604. # 生成网格价格水平(对冲策略:在start_price + grid_size入场)
  1605. grid_levels = []
  1606. current_level = start_price + grid_size
  1607. max_price = price_data['high'].max()
  1608. while current_level < max_price:
  1609. grid_levels.append(current_level)
  1610. current_level += grid_size
  1611. # 模拟每个网格水平的对冲交易
  1612. for entry_price in grid_levels:
  1613. profit_target = entry_price - grid_size # 获利了结
  1614. stop_loss = entry_price * (1 + stop_loss_pct) # 止损
  1615. # 查找入场机会
  1616. entry_date = None
  1617. for date, row in price_data.iterrows():
  1618. if row['low'] <= entry_price <= row['high']:
  1619. entry_date = date
  1620. break
  1621. if entry_date is None:
  1622. continue
  1623. # 查找退出机会(获利或止损)
  1624. exit_date = None
  1625. exit_price_actual = None
  1626. exit_reason = 'hold'
  1627. for date, row in price_data.iterrows():
  1628. if date <= entry_date:
  1629. continue
  1630. # 检查止损
  1631. if row['high'] >= stop_loss:
  1632. exit_date = date
  1633. exit_price_actual = stop_loss
  1634. exit_reason = 'stop_loss'
  1635. break
  1636. # 检查获利了结
  1637. if row['low'] <= profit_target:
  1638. exit_date = date
  1639. exit_price_actual = profit_target
  1640. exit_reason = 'profit_target'
  1641. break
  1642. # 如果没有触发退出条件,使用最后一日收盘价
  1643. if exit_date is None:
  1644. exit_date = price_data.index[-1]
  1645. exit_price_actual = price_data.iloc[-1]['close']
  1646. exit_reason = 'end_of_data'
  1647. # 计算盈亏(做空,所以是entry_price - exit_price)
  1648. profit_loss = (entry_price - exit_price_actual) * quantity_per_grid
  1649. profit_loss_pct = (entry_price - exit_price_actual) / entry_price
  1650. trade_record = {
  1651. 'commodity': commodity,
  1652. 'contract': contract,
  1653. 'strategy': 'grid_hedge',
  1654. 'entry_date': entry_date.strftime('%Y-%m-%d'),
  1655. 'exit_date': exit_date.strftime('%Y-%m-%d'),
  1656. 'entry_price': entry_price,
  1657. 'exit_price': exit_price_actual,
  1658. 'quantity': quantity_per_grid,
  1659. 'profit_loss': profit_loss,
  1660. 'profit_loss_pct': profit_loss_pct,
  1661. 'days_held': (exit_date - entry_date).days,
  1662. 'exit_reason': exit_reason
  1663. }
  1664. grid_hedge_results.append(trade_record)
  1665. if self.verbose_logging:
  1666. print(f" 对冲网格 {entry_price}: {entry_date.strftime('%Y-%m-%d')} -> {exit_date.strftime('%Y-%m-%d')}")
  1667. print(f" 退出原因: {exit_reason}, 盈亏: {profit_loss:.2f} ({profit_loss_pct:.2%})")
  1668. self.trading_results['grid_hedge'] = grid_hedge_results
  1669. if self.verbose_logging:
  1670. print(f"\n网格对冲策略模拟完成,共{len(grid_hedge_results)}笔交易")
  1671. return grid_hedge_results
  1672. def simulate_combined_strategy(self):
  1673. """
  1674. 模拟组合策略:网格交易 + 网格对冲策略
  1675. """
  1676. if self.verbose_logging:
  1677. print("\n=== 步骤5: 组合策略模拟 ===")
  1678. # 简单将网格交易和网格对冲的结果合并
  1679. grid_results = self.trading_results['grid_trading']
  1680. hedge_results = self.trading_results['grid_hedge']
  1681. combined_results = []
  1682. # 添加网格交易结果
  1683. for record in grid_results:
  1684. combined_record = record.copy()
  1685. combined_record['strategy'] = 'combined_grid'
  1686. combined_results.append(combined_record)
  1687. # 添加网格对冲结果
  1688. for record in hedge_results:
  1689. combined_record = record.copy()
  1690. combined_record['strategy'] = 'combined_hedge'
  1691. combined_results.append(combined_record)
  1692. self.trading_results['combined'] = combined_results
  1693. if self.verbose_logging:
  1694. print(f"组合策略模拟完成,共{len(combined_results)}笔交易")
  1695. print(f" 其中网格交易: {len(grid_results)}笔")
  1696. print(f" 其中网格对冲: {len(hedge_results)}笔")
  1697. return combined_results
  1698. def calculate_performance_statistics(self):
  1699. """
  1700. 计算多品种多级聚合的性能统计
  1701. 包括品种-策略级、品种级、策略级和总体级统计
  1702. """
  1703. if self.verbose_logging:
  1704. print("\n=== 步骤6: 多级性能统计分析 ===")
  1705. # 多级统计结构
  1706. performance_stats = {
  1707. 'by_commodity_strategy': {}, # 品种-策略级统计
  1708. 'by_commodity': {}, # 品种级汇总
  1709. 'by_strategy': {}, # 策略级汇总
  1710. 'overall': {} # 总体汇总
  1711. }
  1712. if self.verbose_logging:
  1713. print("\n--- 第一级:品种-策略级统计 ---")
  1714. # 第一步:计算品种-策略级统计
  1715. for strategy_name, results in self.trading_results.items():
  1716. if strategy_name not in performance_stats['by_commodity_strategy']:
  1717. performance_stats['by_commodity_strategy'][strategy_name] = {}
  1718. # 按品种分组交易结果
  1719. commodity_results = {}
  1720. for result in results:
  1721. commodity = result['commodity']
  1722. if commodity not in commodity_results:
  1723. commodity_results[commodity] = []
  1724. commodity_results[commodity].append(result)
  1725. # 为每个品种计算统计
  1726. for commodity in self.core_commodities.keys():
  1727. comm_results = commodity_results.get(commodity, [])
  1728. stats = self._calculate_single_strategy_stats(strategy_name, comm_results, commodity)
  1729. performance_stats['by_commodity_strategy'][strategy_name][commodity] = stats
  1730. if self.verbose_logging:
  1731. print(f"\n{commodity}-{strategy_name} 策略统计:")
  1732. self._print_strategy_stats(stats)
  1733. if self.verbose_logging:
  1734. print("\n--- 第二级:品种级汇总统计 ---")
  1735. # 第二步:计算品种级汇总统计
  1736. for commodity in self.core_commodities.keys():
  1737. commodity_stats = self._calculate_commodity_summary(commodity, performance_stats['by_commodity_strategy'])
  1738. performance_stats['by_commodity'][commodity] = commodity_stats
  1739. if self.verbose_logging:
  1740. print(f"\n{commodity} 品种汇总统计:")
  1741. self._print_strategy_stats(commodity_stats)
  1742. if self.verbose_logging:
  1743. print("\n--- 第三级:策略级汇总统计 ---")
  1744. # 第三步:计算策略级汇总统计
  1745. for strategy_name in self.trading_results.keys():
  1746. strategy_stats = self._calculate_strategy_summary(strategy_name, performance_stats['by_commodity_strategy'])
  1747. performance_stats['by_strategy'][strategy_name] = strategy_stats
  1748. if self.verbose_logging:
  1749. print(f"\n{strategy_name} 策略汇总统计:")
  1750. self._print_strategy_stats(strategy_stats)
  1751. if self.verbose_logging:
  1752. print("\n--- 第四级:整体汇总统计 ---")
  1753. # 第四步:计算总体统计
  1754. overall_stats = self._calculate_overall_summary(performance_stats['by_strategy'])
  1755. performance_stats['overall'] = overall_stats
  1756. if self.verbose_logging:
  1757. print(f"\n整体汇总统计:")
  1758. self._print_strategy_stats(overall_stats)
  1759. return performance_stats
  1760. def _calculate_single_strategy_stats(self, strategy_name, results, commodity):
  1761. """计算单个策略在特定品种下的统计数据"""
  1762. # 计算已平仓交易的盈亏
  1763. closed_profit_loss = sum(r['profit_loss'] for r in results) if results else 0.0
  1764. # 计算未平仓头寸的未实现盈亏(特定品种)
  1765. unrealized_profit_loss = self._calculate_unrealized_pnl_for_commodity(strategy_name, commodity)
  1766. # 总盈亏 = 已实现盈亏 + 未实现盈亏
  1767. total_profit_loss = closed_profit_loss + unrealized_profit_loss
  1768. if not results and unrealized_profit_loss == 0:
  1769. return {
  1770. 'total_trades': 0,
  1771. 'open_positions': 0,
  1772. 'profitable_trades': 0,
  1773. 'losing_trades': 0,
  1774. 'win_rate': 0.0,
  1775. 'closed_profit_loss': 0.0,
  1776. 'unrealized_profit_loss': 0.0,
  1777. 'total_profit_loss': 0.0,
  1778. 'avg_profit_loss': 0.0,
  1779. 'avg_profit_loss_pct': 0.0,
  1780. 'max_profit': 0.0,
  1781. 'max_loss': 0.0,
  1782. 'avg_holding_days': 0.0,
  1783. 'profit_factor': 0.0
  1784. }
  1785. # 基本统计
  1786. total_trades = len(results)
  1787. open_positions = self._count_open_positions_for_commodity(strategy_name, commodity)
  1788. profitable_trades = sum(1 for r in results if r['profit_loss'] > 0)
  1789. losing_trades = sum(1 for r in results if r['profit_loss'] < 0)
  1790. win_rate = profitable_trades / total_trades if total_trades > 0 else 0
  1791. # 平均盈亏(基于已平仓交易)
  1792. avg_profit_loss = closed_profit_loss / total_trades if total_trades > 0 else 0
  1793. avg_profit_loss_pct = sum(r['profit_loss_pct'] for r in results) / total_trades if total_trades > 0 else 0
  1794. # 最大盈亏(基于已平仓交易)
  1795. profit_losses = [r['profit_loss'] for r in results]
  1796. max_profit = max(profit_losses) if profit_losses else 0
  1797. max_loss = min(profit_losses) if profit_losses else 0
  1798. # 平均持有天数
  1799. avg_holding_days = sum(r['days_held'] for r in results) / total_trades if total_trades > 0 else 0
  1800. # 盈亏比(基于已平仓交易)
  1801. total_profits = sum(r['profit_loss'] for r in results if r['profit_loss'] > 0)
  1802. total_losses = abs(sum(r['profit_loss'] for r in results if r['profit_loss'] < 0))
  1803. profit_factor = total_profits / total_losses if total_losses > 0 else float('inf') if total_profits > 0 else 0
  1804. return {
  1805. 'total_trades': total_trades,
  1806. 'open_positions': open_positions,
  1807. 'profitable_trades': profitable_trades,
  1808. 'losing_trades': losing_trades,
  1809. 'win_rate': win_rate,
  1810. 'closed_profit_loss': closed_profit_loss,
  1811. 'unrealized_profit_loss': unrealized_profit_loss,
  1812. 'total_profit_loss': total_profit_loss,
  1813. 'avg_profit_loss': avg_profit_loss,
  1814. 'avg_profit_loss_pct': avg_profit_loss_pct,
  1815. 'max_profit': max_profit,
  1816. 'max_loss': max_loss,
  1817. 'avg_holding_days': avg_holding_days,
  1818. 'profit_factor': profit_factor
  1819. }
  1820. def _print_strategy_stats(self, stats):
  1821. """打印策略统计信息"""
  1822. print(f" 已平仓交易: {stats['total_trades']}")
  1823. print(f" 未平仓头寸: {stats['open_positions']}")
  1824. print(f" 盈利交易: {stats['profitable_trades']}")
  1825. print(f" 亏损交易: {stats['losing_trades']}")
  1826. print(f" 胜率: {stats['win_rate']:.2%}")
  1827. print(f" 已实现盈亏: {stats['closed_profit_loss']:.2f}")
  1828. print(f" 未实现盈亏: {stats['unrealized_profit_loss']:.2f}")
  1829. print(f" 总盈亏: {stats['total_profit_loss']:.2f}")
  1830. print(f" 平均盈亏: {stats['avg_profit_loss']:.2f}")
  1831. print(f" 平均盈亏率: {stats['avg_profit_loss_pct']:.2%}")
  1832. print(f" 最大盈利: {stats['max_profit']:.2f}")
  1833. print(f" 最大亏损: {stats['max_loss']:.2f}")
  1834. print(f" 平均持有天数: {stats['avg_holding_days']:.1f}")
  1835. profit_factor_str = f"{stats['profit_factor']:.2f}" if stats['profit_factor'] != float('inf') else "∞"
  1836. print(f" 盈亏比: {profit_factor_str}")
  1837. def _calculate_commodity_summary(self, commodity, by_commodity_strategy):
  1838. """计算品种级汇总统计"""
  1839. total_stats = {
  1840. 'total_trades': 0,
  1841. 'open_positions': 0,
  1842. 'profitable_trades': 0,
  1843. 'losing_trades': 0,
  1844. 'closed_profit_loss': 0.0,
  1845. 'unrealized_profit_loss': 0.0,
  1846. 'total_profit_loss': 0.0,
  1847. 'max_profit': 0.0,
  1848. 'max_loss': 0.0,
  1849. 'total_holding_days': 0.0,
  1850. 'total_profits': 0.0,
  1851. 'total_losses': 0.0,
  1852. 'all_pct': []
  1853. }
  1854. for strategy_name, commodity_stats in by_commodity_strategy.items():
  1855. if commodity in commodity_stats:
  1856. stats = commodity_stats[commodity]
  1857. total_stats['total_trades'] += stats['total_trades']
  1858. total_stats['open_positions'] += stats['open_positions']
  1859. total_stats['profitable_trades'] += stats['profitable_trades']
  1860. total_stats['losing_trades'] += stats['losing_trades']
  1861. total_stats['closed_profit_loss'] += stats['closed_profit_loss']
  1862. total_stats['unrealized_profit_loss'] += stats['unrealized_profit_loss']
  1863. total_stats['total_profit_loss'] += stats['total_profit_loss']
  1864. total_stats['max_profit'] = max(total_stats['max_profit'], stats['max_profit'])
  1865. total_stats['max_loss'] = min(total_stats['max_loss'], stats['max_loss'])
  1866. total_stats['total_holding_days'] += stats['avg_holding_days'] * stats['total_trades']
  1867. if stats['profit_factor'] != float('inf'):
  1868. profits = stats['closed_profit_loss'] if stats['closed_profit_loss'] > 0 else 0
  1869. losses = abs(stats['closed_profit_loss']) if stats['closed_profit_loss'] < 0 else 0
  1870. total_stats['total_profits'] += profits
  1871. total_stats['total_losses'] += losses
  1872. # 收集所有百分比数据
  1873. if stats['total_trades'] > 0:
  1874. total_stats['all_pct'].extend([stats['avg_profit_loss_pct']] * stats['total_trades'])
  1875. # 计算汇总指标
  1876. win_rate = total_stats['profitable_trades'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1877. avg_profit_loss = total_stats['closed_profit_loss'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1878. avg_profit_loss_pct = sum(total_stats['all_pct']) / len(total_stats['all_pct']) if total_stats['all_pct'] else 0
  1879. avg_holding_days = total_stats['total_holding_days'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1880. profit_factor = total_stats['total_profits'] / total_stats['total_losses'] if total_stats['total_losses'] > 0 else float('inf') if total_stats['total_profits'] > 0 else 0
  1881. return {
  1882. 'total_trades': total_stats['total_trades'],
  1883. 'open_positions': total_stats['open_positions'],
  1884. 'profitable_trades': total_stats['profitable_trades'],
  1885. 'losing_trades': total_stats['losing_trades'],
  1886. 'win_rate': win_rate,
  1887. 'closed_profit_loss': total_stats['closed_profit_loss'],
  1888. 'unrealized_profit_loss': total_stats['unrealized_profit_loss'],
  1889. 'total_profit_loss': total_stats['total_profit_loss'],
  1890. 'avg_profit_loss': avg_profit_loss,
  1891. 'avg_profit_loss_pct': avg_profit_loss_pct,
  1892. 'max_profit': total_stats['max_profit'],
  1893. 'max_loss': total_stats['max_loss'],
  1894. 'avg_holding_days': avg_holding_days,
  1895. 'profit_factor': profit_factor
  1896. }
  1897. def _calculate_strategy_summary(self, strategy_name, by_commodity_strategy):
  1898. """计算策略级汇总统计"""
  1899. if strategy_name not in by_commodity_strategy:
  1900. return self._get_empty_stats()
  1901. strategy_stats = by_commodity_strategy[strategy_name]
  1902. total_stats = {
  1903. 'total_trades': 0,
  1904. 'open_positions': 0,
  1905. 'profitable_trades': 0,
  1906. 'losing_trades': 0,
  1907. 'closed_profit_loss': 0.0,
  1908. 'unrealized_profit_loss': 0.0,
  1909. 'total_profit_loss': 0.0,
  1910. 'max_profit': 0.0,
  1911. 'max_loss': 0.0,
  1912. 'total_holding_days': 0.0,
  1913. 'total_profits': 0.0,
  1914. 'total_losses': 0.0,
  1915. 'all_pct': []
  1916. }
  1917. for commodity, stats in strategy_stats.items():
  1918. total_stats['total_trades'] += stats['total_trades']
  1919. total_stats['open_positions'] += stats['open_positions']
  1920. total_stats['profitable_trades'] += stats['profitable_trades']
  1921. total_stats['losing_trades'] += stats['losing_trades']
  1922. total_stats['closed_profit_loss'] += stats['closed_profit_loss']
  1923. total_stats['unrealized_profit_loss'] += stats['unrealized_profit_loss']
  1924. total_stats['total_profit_loss'] += stats['total_profit_loss']
  1925. total_stats['max_profit'] = max(total_stats['max_profit'], stats['max_profit'])
  1926. total_stats['max_loss'] = min(total_stats['max_loss'], stats['max_loss'])
  1927. total_stats['total_holding_days'] += stats['avg_holding_days'] * stats['total_trades']
  1928. if stats['profit_factor'] != float('inf'):
  1929. profits = stats['closed_profit_loss'] if stats['closed_profit_loss'] > 0 else 0
  1930. losses = abs(stats['closed_profit_loss']) if stats['closed_profit_loss'] < 0 else 0
  1931. total_stats['total_profits'] += profits
  1932. total_stats['total_losses'] += losses
  1933. # 收集所有百分比数据
  1934. if stats['total_trades'] > 0:
  1935. total_stats['all_pct'].extend([stats['avg_profit_loss_pct']] * stats['total_trades'])
  1936. # 计算汇总指标
  1937. win_rate = total_stats['profitable_trades'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1938. avg_profit_loss = total_stats['closed_profit_loss'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1939. avg_profit_loss_pct = sum(total_stats['all_pct']) / len(total_stats['all_pct']) if total_stats['all_pct'] else 0
  1940. avg_holding_days = total_stats['total_holding_days'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1941. profit_factor = total_stats['total_profits'] / total_stats['total_losses'] if total_stats['total_losses'] > 0 else float('inf') if total_stats['total_profits'] > 0 else 0
  1942. return {
  1943. 'total_trades': total_stats['total_trades'],
  1944. 'open_positions': total_stats['open_positions'],
  1945. 'profitable_trades': total_stats['profitable_trades'],
  1946. 'losing_trades': total_stats['losing_trades'],
  1947. 'win_rate': win_rate,
  1948. 'closed_profit_loss': total_stats['closed_profit_loss'],
  1949. 'unrealized_profit_loss': total_stats['unrealized_profit_loss'],
  1950. 'total_profit_loss': total_stats['total_profit_loss'],
  1951. 'avg_profit_loss': avg_profit_loss,
  1952. 'avg_profit_loss_pct': avg_profit_loss_pct,
  1953. 'max_profit': total_stats['max_profit'],
  1954. 'max_loss': total_stats['max_loss'],
  1955. 'avg_holding_days': avg_holding_days,
  1956. 'profit_factor': profit_factor
  1957. }
  1958. def _calculate_overall_summary(self, by_strategy):
  1959. """计算总体汇总统计"""
  1960. total_stats = {
  1961. 'total_trades': 0,
  1962. 'open_positions': 0,
  1963. 'profitable_trades': 0,
  1964. 'losing_trades': 0,
  1965. 'closed_profit_loss': 0.0,
  1966. 'unrealized_profit_loss': 0.0,
  1967. 'total_profit_loss': 0.0,
  1968. 'max_profit': 0.0,
  1969. 'max_loss': 0.0,
  1970. 'total_holding_days': 0.0,
  1971. 'total_profits': 0.0,
  1972. 'total_losses': 0.0,
  1973. 'all_pct': []
  1974. }
  1975. for strategy_name, stats in by_strategy.items():
  1976. total_stats['total_trades'] += stats['total_trades']
  1977. total_stats['open_positions'] += stats['open_positions']
  1978. total_stats['profitable_trades'] += stats['profitable_trades']
  1979. total_stats['losing_trades'] += stats['losing_trades']
  1980. total_stats['closed_profit_loss'] += stats['closed_profit_loss']
  1981. total_stats['unrealized_profit_loss'] += stats['unrealized_profit_loss']
  1982. total_stats['total_profit_loss'] += stats['total_profit_loss']
  1983. total_stats['max_profit'] = max(total_stats['max_profit'], stats['max_profit'])
  1984. total_stats['max_loss'] = min(total_stats['max_loss'], stats['max_loss'])
  1985. total_stats['total_holding_days'] += stats['avg_holding_days'] * stats['total_trades']
  1986. if stats['profit_factor'] != float('inf'):
  1987. profits = stats['closed_profit_loss'] if stats['closed_profit_loss'] > 0 else 0
  1988. losses = abs(stats['closed_profit_loss']) if stats['closed_profit_loss'] < 0 else 0
  1989. total_stats['total_profits'] += profits
  1990. total_stats['total_losses'] += losses
  1991. # 收集所有百分比数据
  1992. if stats['total_trades'] > 0:
  1993. total_stats['all_pct'].extend([stats['avg_profit_loss_pct']] * stats['total_trades'])
  1994. # 计算汇总指标
  1995. win_rate = total_stats['profitable_trades'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1996. avg_profit_loss = total_stats['closed_profit_loss'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1997. avg_profit_loss_pct = sum(total_stats['all_pct']) / len(total_stats['all_pct']) if total_stats['all_pct'] else 0
  1998. avg_holding_days = total_stats['total_holding_days'] / total_stats['total_trades'] if total_stats['total_trades'] > 0 else 0
  1999. profit_factor = total_stats['total_profits'] / total_stats['total_losses'] if total_stats['total_losses'] > 0 else float('inf') if total_stats['total_profits'] > 0 else 0
  2000. return {
  2001. 'total_trades': total_stats['total_trades'],
  2002. 'open_positions': total_stats['open_positions'],
  2003. 'profitable_trades': total_stats['profitable_trades'],
  2004. 'losing_trades': total_stats['losing_trades'],
  2005. 'win_rate': win_rate,
  2006. 'closed_profit_loss': total_stats['closed_profit_loss'],
  2007. 'unrealized_profit_loss': total_stats['unrealized_profit_loss'],
  2008. 'total_profit_loss': total_stats['total_profit_loss'],
  2009. 'avg_profit_loss': avg_profit_loss,
  2010. 'avg_profit_loss_pct': avg_profit_loss_pct,
  2011. 'max_profit': total_stats['max_profit'],
  2012. 'max_loss': total_stats['max_loss'],
  2013. 'avg_holding_days': avg_holding_days,
  2014. 'profit_factor': profit_factor
  2015. }
  2016. def _get_empty_stats(self):
  2017. """返回空的统计数据"""
  2018. return {
  2019. 'total_trades': 0,
  2020. 'open_positions': 0,
  2021. 'profitable_trades': 0,
  2022. 'losing_trades': 0,
  2023. 'win_rate': 0.0,
  2024. 'closed_profit_loss': 0.0,
  2025. 'unrealized_profit_loss': 0.0,
  2026. 'total_profit_loss': 0.0,
  2027. 'avg_profit_loss': 0.0,
  2028. 'avg_profit_loss_pct': 0.0,
  2029. 'max_profit': 0.0,
  2030. 'max_loss': 0.0,
  2031. 'avg_holding_days': 0.0,
  2032. 'profit_factor': 0.0
  2033. }
  2034. def _calculate_unrealized_pnl_for_commodity(self, strategy_name, commodity):
  2035. """计算特定品种未平仓头寸的未实现盈亏"""
  2036. if strategy_name == 'combined':
  2037. return (self._calculate_unrealized_pnl_for_commodity('grid_trading', commodity) +
  2038. self._calculate_unrealized_pnl_for_commodity('grid_hedge', commodity))
  2039. unrealized_pnl = 0.0
  2040. strategy_positions = self.active_positions.get(strategy_name, {})
  2041. if commodity in strategy_positions:
  2042. current_contract = self._get_current_contract(commodity, self.end_date.date())
  2043. if not current_contract:
  2044. return 0.0
  2045. end_price = self._get_price_on_date(commodity, current_contract, self.end_date.date(), 'close')
  2046. if end_price is None:
  2047. return 0.0
  2048. positions = strategy_positions[commodity]
  2049. for position_id, position in positions.items():
  2050. if position['status'] == 'open' and position['contract'] == current_contract:
  2051. if strategy_name == 'grid_hedge':
  2052. pnl = self._calculate_futures_pnl(
  2053. position['entry_price'], end_price, position['quantity'], commodity, is_long=False
  2054. )
  2055. else:
  2056. pnl = self._calculate_futures_pnl(
  2057. position['entry_price'], end_price, position['quantity'], commodity, is_long=True
  2058. )
  2059. unrealized_pnl += pnl
  2060. return unrealized_pnl
  2061. def _count_open_positions_for_commodity(self, strategy_name, commodity):
  2062. """计算特定品种的未平仓头寸数量"""
  2063. if strategy_name == 'combined':
  2064. return (self._count_open_positions_for_commodity('grid_trading', commodity) +
  2065. self._count_open_positions_for_commodity('grid_hedge', commodity))
  2066. count = 0
  2067. strategy_positions = self.active_positions.get(strategy_name, {})
  2068. if commodity in strategy_positions:
  2069. positions = strategy_positions[commodity]
  2070. for position_id, position in positions.items():
  2071. if position['status'] == 'open':
  2072. count += 1
  2073. return count
  2074. def _calculate_unrealized_pnl(self, strategy_name):
  2075. """
  2076. 计算未平仓头寸的未实现盈亏
  2077. """
  2078. unrealized_pnl = 0.0
  2079. if strategy_name == 'combined':
  2080. # 组合策略的未实现盈亏是网格交易和网格对冲的总和
  2081. return (self._calculate_unrealized_pnl('grid_trading') +
  2082. self._calculate_unrealized_pnl('grid_hedge'))
  2083. # 获取策略对应的头寸字典
  2084. strategy_positions = self.active_positions.get(strategy_name, {})
  2085. for commodity, positions in strategy_positions.items():
  2086. # 获取当前合约和最新价格
  2087. current_contract = self._get_current_contract(commodity, self.end_date.date())
  2088. if not current_contract:
  2089. continue
  2090. # 获取结束日期的收盘价
  2091. end_price = self._get_price_on_date(commodity, current_contract, self.end_date.date(), 'close')
  2092. if end_price is None:
  2093. continue
  2094. for position_id, position in positions.items():
  2095. if position['status'] == 'open' and position['contract'] == current_contract:
  2096. # 使用正确的期货盈亏计算公式
  2097. if strategy_name == 'grid_hedge':
  2098. # 网格对冲是做空
  2099. pnl = self._calculate_futures_pnl(
  2100. position['entry_price'], end_price, position['quantity'], commodity, is_long=False
  2101. )
  2102. else:
  2103. # 基础头寸和网格交易都是做多
  2104. pnl = self._calculate_futures_pnl(
  2105. position['entry_price'], end_price, position['quantity'], commodity, is_long=True
  2106. )
  2107. unrealized_pnl += pnl
  2108. return unrealized_pnl
  2109. def _count_open_positions(self, strategy_name):
  2110. """
  2111. 计算未平仓头寸数量
  2112. """
  2113. if strategy_name == 'combined':
  2114. return (self._count_open_positions('grid_trading') +
  2115. self._count_open_positions('grid_hedge'))
  2116. count = 0
  2117. strategy_positions = self.active_positions.get(strategy_name, {})
  2118. for commodity, positions in strategy_positions.items():
  2119. for position_id, position in positions.items():
  2120. if position['status'] == 'open':
  2121. count += 1
  2122. return count
  2123. def generate_comparison_report(self, performance_stats):
  2124. """
  2125. 生成多级聚合的对比报告
  2126. 包括品种-策略级、品种级、策略级和总体级报告
  2127. """
  2128. if self.verbose_logging:
  2129. print("\n=== 多级聚合对比分析报告 ===")
  2130. strategies = ['base_position', 'grid_trading', 'grid_hedge', 'combined']
  2131. strategy_names = {
  2132. 'base_position': '基础头寸交易',
  2133. 'grid_trading': '网格交易',
  2134. 'grid_hedge': '网格对冲策略',
  2135. 'combined': '组合策略'
  2136. }
  2137. all_comparison_data = {
  2138. 'by_commodity_strategy': [],
  2139. 'by_commodity': [],
  2140. 'by_strategy': [],
  2141. 'overall': []
  2142. }
  2143. # 1. 品种-策略级对比报告
  2144. if self.verbose_logging:
  2145. print("\n--- 品种-策略级对比 ---")
  2146. by_comm_strategy_data = []
  2147. for strategy in strategies:
  2148. for commodity in self.core_commodities.keys():
  2149. stats = performance_stats['by_commodity_strategy'].get(strategy, {}).get(commodity, {})
  2150. if stats.get('total_trades', 0) > 0 or stats.get('open_positions', 0) > 0:
  2151. by_comm_strategy_data.append({
  2152. '品种': commodity,
  2153. '策略': strategy_names[strategy],
  2154. '已平仓': stats.get('total_trades', 0),
  2155. '未平仓': stats.get('open_positions', 0),
  2156. '胜率': f"{stats.get('win_rate', 0):.2%}",
  2157. '已实现': f"{stats.get('closed_profit_loss', 0):.2f}",
  2158. '未实现': f"{stats.get('unrealized_profit_loss', 0):.2f}",
  2159. '总盈亏': f"{stats.get('total_profit_loss', 0):.2f}",
  2160. '平均盈亏': f"{stats.get('avg_profit_loss', 0):.2f}",
  2161. '最大盈利': f"{stats.get('max_profit', 0):.2f}",
  2162. '最大亏损': f"{stats.get('max_loss', 0):.2f}",
  2163. '平均天数': f"{stats.get('avg_holding_days', 0):.1f}",
  2164. '盈亏比': f"{stats.get('profit_factor', 0):.2f}" if stats.get('profit_factor', 0) != float('inf') else "∞"
  2165. })
  2166. if by_comm_strategy_data and self.verbose_logging:
  2167. df_comm_strategy = pd.DataFrame(by_comm_strategy_data)
  2168. print(df_comm_strategy.to_string(index=False))
  2169. all_comparison_data['by_commodity_strategy'] = by_comm_strategy_data
  2170. # 2. 品种级汇总对比报告
  2171. if self.verbose_logging:
  2172. print("\n--- 品种级汇总对比 ---")
  2173. by_commodity_data = []
  2174. for commodity in self.core_commodities.keys():
  2175. stats = performance_stats['by_commodity'].get(commodity, {})
  2176. if stats.get('total_trades', 0) > 0 or stats.get('open_positions', 0) > 0:
  2177. by_commodity_data.append({
  2178. '品种': commodity,
  2179. '已平仓': stats.get('total_trades', 0),
  2180. '未平仓': stats.get('open_positions', 0),
  2181. '胜率': f"{stats.get('win_rate', 0):.2%}",
  2182. '已实现': f"{stats.get('closed_profit_loss', 0):.2f}",
  2183. '未实现': f"{stats.get('unrealized_profit_loss', 0):.2f}",
  2184. '总盈亏': f"{stats.get('total_profit_loss', 0):.2f}",
  2185. '平均盈亏': f"{stats.get('avg_profit_loss', 0):.2f}",
  2186. '最大盈利': f"{stats.get('max_profit', 0):.2f}",
  2187. '最大亏损': f"{stats.get('max_loss', 0):.2f}",
  2188. '平均天数': f"{stats.get('avg_holding_days', 0):.1f}",
  2189. '盈亏比': f"{stats.get('profit_factor', 0):.2f}" if stats.get('profit_factor', 0) != float('inf') else "∞"
  2190. })
  2191. if by_commodity_data and self.verbose_logging:
  2192. df_commodity = pd.DataFrame(by_commodity_data)
  2193. print(df_commodity.to_string(index=False))
  2194. all_comparison_data['by_commodity'] = by_commodity_data
  2195. # 3. 策略级汇总对比报告
  2196. if self.verbose_logging:
  2197. print("\n--- 策略级汇总对比 ---")
  2198. by_strategy_data = []
  2199. for strategy in strategies:
  2200. stats = performance_stats['by_strategy'].get(strategy, {})
  2201. by_strategy_data.append({
  2202. '策略': strategy_names[strategy],
  2203. '已平仓': stats.get('total_trades', 0),
  2204. '未平仓': stats.get('open_positions', 0),
  2205. '胜率': f"{stats.get('win_rate', 0):.2%}",
  2206. '已实现': f"{stats.get('closed_profit_loss', 0):.2f}",
  2207. '未实现': f"{stats.get('unrealized_profit_loss', 0):.2f}",
  2208. '总盈亏': f"{stats.get('total_profit_loss', 0):.2f}",
  2209. '平均盈亏': f"{stats.get('avg_profit_loss', 0):.2f}",
  2210. '最大盈利': f"{stats.get('max_profit', 0):.2f}",
  2211. '最大亏损': f"{stats.get('max_loss', 0):.2f}",
  2212. '平均天数': f"{stats.get('avg_holding_days', 0):.1f}",
  2213. '盈亏比': f"{stats.get('profit_factor', 0):.2f}" if stats.get('profit_factor', 0) != float('inf') else "∞"
  2214. })
  2215. if by_strategy_data and self.verbose_logging:
  2216. df_strategy = pd.DataFrame(by_strategy_data)
  2217. print(df_strategy.to_string(index=False))
  2218. all_comparison_data['by_strategy'] = by_strategy_data
  2219. # 4. 总体汇总报告
  2220. if self.verbose_logging:
  2221. print("\n--- 整体汇总 ---")
  2222. overall_data = []
  2223. stats = performance_stats['overall']
  2224. overall_data.append({
  2225. '项目': '整体表现',
  2226. '已平仓': stats.get('total_trades', 0),
  2227. '未平仓': stats.get('open_positions', 0),
  2228. '胜率': f"{stats.get('win_rate', 0):.2%}",
  2229. '已实现': f"{stats.get('closed_profit_loss', 0):.2f}",
  2230. '未实现': f"{stats.get('unrealized_profit_loss', 0):.2f}",
  2231. '总盈亏': f"{stats.get('total_profit_loss', 0):.2f}",
  2232. '平均盈亏': f"{stats.get('avg_profit_loss', 0):.2f}",
  2233. '最大盈利': f"{stats.get('max_profit', 0):.2f}",
  2234. '最大亏损': f"{stats.get('max_loss', 0):.2f}",
  2235. '平均天数': f"{stats.get('avg_holding_days', 0):.1f}",
  2236. '盈亏比': f"{stats.get('profit_factor', 0):.2f}" if stats.get('profit_factor', 0) != float('inf') else "∞"
  2237. })
  2238. if overall_data and self.verbose_logging:
  2239. df_overall = pd.DataFrame(overall_data)
  2240. print(df_overall.to_string(index=False))
  2241. all_comparison_data['overall'] = overall_data
  2242. return all_comparison_data
  2243. def generate_csv_output(self, performance_stats):
  2244. """
  2245. 生成CSV输出文件
  2246. """
  2247. if self.verbose_logging:
  2248. print("\n=== 步骤8: 生成CSV输出文件 ===")
  2249. timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
  2250. # 1. 生成交易记录CSV
  2251. all_trades = []
  2252. for strategy_name, trades in self.trading_results.items():
  2253. all_trades.extend(trades)
  2254. if all_trades:
  2255. df_trades = pd.DataFrame(all_trades)
  2256. # 添加列顺序,确保合约切换相关字段在前面
  2257. column_order = ['commodity', 'contract', 'strategy', 'entry_date', 'exit_date',
  2258. 'entry_price', 'exit_price', 'quantity', 'profit_loss', 'profit_loss_pct',
  2259. 'days_held', 'exit_reason']
  2260. # 重新排列DataFrame的列顺序
  2261. existing_columns = [col for col in column_order if col in df_trades.columns]
  2262. other_columns = [col for col in df_trades.columns if col not in column_order]
  2263. df_trades = df_trades[existing_columns + other_columns]
  2264. trades_filename = f'grid_trading_records_{timestamp}.csv'
  2265. df_trades.to_csv(trades_filename, index=False, encoding=self.output_encoding)
  2266. if self.verbose_logging:
  2267. print(f"交易记录已保存至: {trades_filename}")
  2268. # 2. 生成多级性能统计CSV
  2269. csv_files = []
  2270. # 2.1 品种-策略级统计CSV
  2271. by_comm_strategy_data = []
  2272. for strategy, commodity_data in performance_stats['by_commodity_strategy'].items():
  2273. for commodity, stats in commodity_data.items():
  2274. stats_record = {
  2275. '品种': commodity,
  2276. '策略': strategy,
  2277. **stats
  2278. }
  2279. by_comm_strategy_data.append(stats_record)
  2280. if by_comm_strategy_data:
  2281. df_comm_strategy = pd.DataFrame(by_comm_strategy_data)
  2282. comm_strategy_filename = f'grid_trading_by_commodity_strategy_{timestamp}.csv'
  2283. df_comm_strategy.to_csv(comm_strategy_filename, index=False, encoding=self.output_encoding)
  2284. csv_files.append(comm_strategy_filename)
  2285. if self.verbose_logging:
  2286. print(f"品种-策略级统计已保存至: {comm_strategy_filename}")
  2287. # 2.2 品种级汇总统计CSV
  2288. by_commodity_data = []
  2289. for commodity, stats in performance_stats['by_commodity'].items():
  2290. stats_record = {
  2291. '品种': commodity,
  2292. **stats
  2293. }
  2294. by_commodity_data.append(stats_record)
  2295. if by_commodity_data:
  2296. df_commodity = pd.DataFrame(by_commodity_data)
  2297. commodity_filename = f'grid_trading_by_commodity_{timestamp}.csv'
  2298. df_commodity.to_csv(commodity_filename, index=False, encoding=self.output_encoding)
  2299. csv_files.append(commodity_filename)
  2300. if self.verbose_logging:
  2301. print(f"品种级汇总统计已保存至: {commodity_filename}")
  2302. # 2.3 策略级汇总统计CSV
  2303. by_strategy_data = []
  2304. for strategy, stats in performance_stats['by_strategy'].items():
  2305. stats_record = {
  2306. '策略': strategy,
  2307. **stats
  2308. }
  2309. by_strategy_data.append(stats_record)
  2310. if by_strategy_data:
  2311. df_strategy = pd.DataFrame(by_strategy_data)
  2312. strategy_filename = f'grid_trading_by_strategy_{timestamp}.csv'
  2313. df_strategy.to_csv(strategy_filename, index=False, encoding=self.output_encoding)
  2314. csv_files.append(strategy_filename)
  2315. if self.verbose_logging:
  2316. print(f"策略级汇总统计已保存至: {strategy_filename}")
  2317. # 2.4 整体汇总统计CSV
  2318. overall_data = [{
  2319. '项目': '整体汇总',
  2320. **performance_stats['overall']
  2321. }]
  2322. if overall_data:
  2323. df_overall = pd.DataFrame(overall_data)
  2324. overall_filename = f'grid_trading_overall_{timestamp}.csv'
  2325. df_overall.to_csv(overall_filename, index=False, encoding=self.output_encoding)
  2326. csv_files.append(overall_filename)
  2327. if self.verbose_logging:
  2328. print(f"整体汇总统计已保存至: {overall_filename}")
  2329. return trades_filename if all_trades else None, csv_files
  2330. def run_complete_analysis(self):
  2331. """执行完整的网格交易分析流程(带主力合约切换)"""
  2332. if self.verbose_logging:
  2333. print("开始执行期货网格交易分析(带主力合约切换)")
  2334. print("=" * 60)
  2335. try:
  2336. # 步骤1: 合约选择
  2337. self.select_contracts()
  2338. if not self.selected_contracts:
  2339. if self.verbose_logging:
  2340. print("未选择到有效合约,分析终止")
  2341. return None
  2342. # 步骤2: 构建主力合约历史变化
  2343. self.build_dominant_contract_history()
  2344. if not self.dominant_contract_history:
  2345. if self.verbose_logging:
  2346. print("未获取到主力合约历史,分析终止")
  2347. return None
  2348. # 步骤3: 收集价格数据
  2349. self.collect_price_data()
  2350. if not self.price_data:
  2351. if self.verbose_logging:
  2352. print("未获取到有效价格数据,分析终止")
  2353. return None
  2354. # 步骤4: 带合约切换的交易模拟
  2355. self.simulate_with_contract_switching()
  2356. # 步骤5: 组合策略模拟
  2357. self.simulate_combined_strategy()
  2358. # 步骤6: 性能统计分析
  2359. performance_stats = self.calculate_performance_statistics()
  2360. # 步骤7: 生成对比报告
  2361. comparison_report = self.generate_comparison_report(performance_stats)
  2362. # 步骤8: 生成CSV输出
  2363. # trades_file, stats_files = self.generate_csv_output(performance_stats)
  2364. # 分析汇总
  2365. total_commodities = len(self.selected_contracts)
  2366. total_trades = sum(len(trades) for trades in self.trading_results.values())
  2367. contract_switches = sum(len(history) for history in self.dominant_contract_history.values())
  2368. if self.verbose_logging:
  2369. print("\n" + "=" * 60)
  2370. print("分析完成汇总:")
  2371. print(f"分析商品数: {total_commodities}")
  2372. print(f"合约切换次数: {contract_switches}")
  2373. print(f"总交易笔数: {total_trades}")
  2374. # print(f"交易记录文件: {trades_file}")
  2375. # print(f"性能统计文件: {stats_file}")
  2376. return {
  2377. 'selected_contracts': self.selected_contracts,
  2378. 'dominant_contract_history': self.dominant_contract_history,
  2379. 'price_data': self.price_data,
  2380. 'trading_results': self.trading_results,
  2381. 'performance_stats': performance_stats,
  2382. 'comparison_report': comparison_report,
  2383. # 'output_files': {
  2384. # 'trades_file': trades_file,
  2385. # 'stats_files': stats_files
  2386. # },
  2387. 'summary': {
  2388. 'total_commodities': total_commodities,
  2389. 'contract_switches': contract_switches,
  2390. 'total_trades': total_trades
  2391. }
  2392. }
  2393. except Exception as e:
  2394. if self.verbose_logging:
  2395. print(f"分析过程中出现错误: {str(e)}")
  2396. import traceback
  2397. traceback.print_exc()
  2398. return None
  2399. # =====================================================================================
  2400. # 主程序入口
  2401. # =====================================================================================
  2402. def run_grid_trading_analysis(config=None):
  2403. """运行期货网格交易分析"""
  2404. if config is None:
  2405. config = GridTradingConfig
  2406. # 打印配置信息
  2407. config.print_config()
  2408. # 创建分析器并运行
  2409. analyzer = FutureGridTradingAnalyzer(config)
  2410. results = analyzer.run_complete_analysis()
  2411. return results
  2412. # 执行分析
  2413. if __name__ == "__main__":
  2414. print("期货网格交易研究分析工具(带主力合约切换)")
  2415. print("研究期货网格交易策略在不同配置下的表现")
  2416. print("核心功能包括主力合约自动切换、强制平仓和重新建仓逻辑")
  2417. print("")
  2418. print("支持四种交易场景的对比分析:")
  2419. print(" 1. 基础头寸交易 - 价格-数量网格配置")
  2420. print(" 2. 网格交易策略 - 限价订单网格买入卖出")
  2421. print(" 3. 网格对冲策略 - 带止损的增强网格交易")
  2422. print(" 4. 组合策略 - 网格交易+网格对冲组合")
  2423. print("")
  2424. print("主要特点:")
  2425. print(" - 主力合约自动监控:每日检测主力合约变化")
  2426. print(" - 强制平仓机制:合约切换时立即平掉旧合约所有头寸")
  2427. print(" - 智能重新建仓:根据价格条件在新合约中重新建立头寸")
  2428. print(" - 完整交易记录:记录所有交易包括合约切换引起的强制平仓")
  2429. print("")
  2430. print("适用于聚宽在线研究平台")
  2431. results = run_grid_trading_analysis()
  2432. if results:
  2433. print("\n✅ 分析执行成功!")
  2434. summary = results['summary']
  2435. print(f"📊 结果摘要:")
  2436. print(f" - 分析商品数: {summary['total_commodities']}")
  2437. print(f" - 合约切换次数: {summary['contract_switches']}")
  2438. print(f" - 总交易笔数: {summary['total_trades']}")
  2439. print(f" - 整体总盈亏: {results['performance_stats']['overall']['total_profit_loss']:.2f}")
  2440. print(f" - 整体胜率: {results['performance_stats']['overall']['win_rate']:.2%}")
  2441. print(f" - 未平仓头寸: {results['performance_stats']['overall']['open_positions']}")
  2442. print(f"📂 输出文件:")
  2443. print(f" - 交易记录文件: {results['output_files']['trades_file']}")
  2444. for i, stats_file in enumerate(results['output_files']['stats_files'], 1):
  2445. print(f" - 统计文件{i}: {stats_file}")
  2446. print(f"\n📈 多级汇总:")
  2447. # 品种级汇总简要显示
  2448. print(f" 品种表现:")
  2449. for commodity in ['SA', 'M']:
  2450. comm_stats = results['performance_stats']['by_commodity'].get(commodity, {})
  2451. if comm_stats.get('total_trades', 0) > 0 or comm_stats.get('open_positions', 0) > 0:
  2452. print(f" {commodity}: 交易{comm_stats.get('total_trades', 0)}笔, 盈亏{comm_stats.get('total_profit_loss', 0):.2f}, 胜率{comm_stats.get('win_rate', 0):.2%}")
  2453. # 策略级汇总简要显示
  2454. print(f" 策略表现:")
  2455. strategy_names = {
  2456. 'base_position': '基础头寸',
  2457. 'grid_trading': '网格交易',
  2458. 'grid_hedge': '网格对冲',
  2459. 'combined': '组合策略'
  2460. }
  2461. for strategy, name in strategy_names.items():
  2462. strat_stats = results['performance_stats']['by_strategy'].get(strategy, {})
  2463. print(f" {name}: 交易{strat_stats.get('total_trades', 0)}笔, 盈亏{strat_stats.get('total_profit_loss', 0):.2f}, 胜率{strat_stats.get('win_rate', 0):.2%}")
  2464. else:
  2465. print("\n❌ 分析执行失败,请检查错误信息")