kline_zone_analysis.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. CYB50 K线 + 日线指标 舒适区 / 黑暗区 深度分析
  5. ──────────────────────────────────────────────────────────────────────
  6. 数据管道:
  7. cyb50_30min_2023_to_20260325.csv
  8. → resample 1D → 日线指标(RSI/KDJ/MACD/MA/BB/动量)
  9. backtest_vB_fee_*.csv(195笔,全部非死亡区,含30分钟K线指标)
  10. → attach_daily_indicators → 完整 30min + 日线 指标集
  11. 分析:
  12. 单变量:每个指标分箱 → count / WR / avg_PnL / total_PnL
  13. 双变量:关键指标对的 2D 组合矩阵
  14. 区域识别:WR≥60% → 舒适区;WR≤35% → 黑暗区
  15. """
  16. import sys, io, os, glob
  17. if sys.platform == 'win32':
  18. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
  19. sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
  20. import pandas as pd
  21. import numpy as np
  22. from datetime import datetime
  23. import warnings
  24. warnings.filterwarnings('ignore')
  25. # ──────────────────────────────────────────────────────────────────
  26. # 配置
  27. # ──────────────────────────────────────────────────────────────────
  28. DATA_CSV = os.path.join(os.path.dirname(__file__), 'cyb50_30min_2023_to_20260325.csv')
  29. MIN_TRADES = 4 # 最少笔数(统计置信阈值)
  30. WR_COMFORT = 0.60 # 舒适区胜率阈值
  31. WR_DARK = 0.35 # 黑暗区胜率阈值
  32. TOP_N = 15 # 展示前 N 个区域
  33. SEP = '=' * 76
  34. # ──────────────────────────────────────────────────────────────────
  35. # 1. 数据加载
  36. # ──────────────────────────────────────────────────────────────────
  37. def load_backtest_vB() -> pd.DataFrame:
  38. """加载最新 backtest_vB_fee_*.csv(195笔非死亡区全量)"""
  39. pattern = os.path.join(os.path.dirname(__file__), 'backtest_vB_fee_*.csv')
  40. files = sorted(glob.glob(pattern))
  41. if not files:
  42. raise FileNotFoundError(
  43. "未找到 backtest_vB_fee_*.csv,请先运行 final_strategy.py")
  44. latest = files[-1]
  45. print(f" 回测数据: {os.path.basename(latest)}")
  46. df = pd.read_csv(latest, encoding='utf-8-sig')
  47. df['开仓时间'] = pd.to_datetime(df['开仓时间'])
  48. df['平仓时间'] = pd.to_datetime(df['平仓时间'])
  49. df['盈利'] = df['盈利标记'] == '盈'
  50. for c in ['实际盈亏', 'RSI', 'K', 'D', 'J', 'MACD_hist',
  51. 'BB_width', 'ATR_Pct', 'Momentum', 'Volume_Ratio']:
  52. if c in df.columns:
  53. df[c] = pd.to_numeric(df[c], errors='coerce')
  54. return df
  55. def build_daily_indicators() -> pd.DataFrame:
  56. """从30分钟K线聚合日线,计算日线技术指标"""
  57. raw = pd.read_csv(DATA_CSV, encoding='utf-8-sig')
  58. raw['DateTime'] = pd.to_datetime(raw['DateTime'])
  59. raw.set_index('DateTime', inplace=True)
  60. raw.sort_index(inplace=True)
  61. # 聚合为日线(只保留有数据的交易日)
  62. d = raw.resample('D').agg(
  63. Open=('Open', 'first'), High=('High', 'max'),
  64. Low=('Low', 'min'), Close=('Close', 'last'),
  65. Volume=('Volume', 'sum')
  66. ).dropna(subset=['Close'])
  67. # ── RSI(14) ──
  68. delta = d['Close'].diff()
  69. gain = delta.where(delta > 0, 0).rolling(14).mean()
  70. loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
  71. d['RSI_D'] = 100 - 100 / (1 + gain / (loss + 1e-9))
  72. # ── KDJ(9,3,3) ──
  73. low9 = d['Low'].rolling(9).min()
  74. high9 = d['High'].rolling(9).max()
  75. rsv = (d['Close'] - low9) / (high9 - low9 + 1e-9) * 100
  76. d['K_D'] = rsv.ewm(com=2, adjust=False).mean()
  77. d['D_D'] = d['K_D'].ewm(com=2, adjust=False).mean()
  78. d['J_D'] = 3 * d['K_D'] - 2 * d['D_D']
  79. # ── MACD(12,26,9) ──
  80. ema12 = d['Close'].ewm(span=12, adjust=False).mean()
  81. ema26 = d['Close'].ewm(span=26, adjust=False).mean()
  82. d['MACD_D'] = ema12 - ema26
  83. d['Signal_D'] = d['MACD_D'].ewm(span=9, adjust=False).mean()
  84. d['MACDhist_D'] = d['MACD_D'] - d['Signal_D']
  85. # ── 均线 & 偏离度 ──
  86. d['MA5_D'] = d['Close'].rolling(5).mean()
  87. d['MA10_D'] = d['Close'].rolling(10).mean()
  88. d['MA20_D'] = d['Close'].rolling(20).mean()
  89. d['MA60_D'] = d['Close'].rolling(60).mean()
  90. d['pct_MA20'] = (d['Close'] - d['MA20_D']) / d['MA20_D'] * 100
  91. d['pct_MA60'] = (d['Close'] - d['MA60_D']) / d['MA60_D'] * 100
  92. d['MA5_slope'] = d['MA5_D'].diff() / d['MA5_D'].shift() * 100 # MA5日斜率%
  93. # ── 布林带位置(20,2) ──
  94. bm = d['Close'].rolling(20).mean()
  95. bstd = d['Close'].rolling(20).std()
  96. bl, bu = bm - 2 * bstd, bm + 2 * bstd
  97. d['BB_pos_D'] = (d['Close'] - bl) / (bu - bl + 1e-9) # 0~1
  98. # ── 动量 ──
  99. d['Mom5_D'] = d['Close'].pct_change(5) * 100
  100. d['Mom10_D'] = d['Close'].pct_change(10) * 100
  101. return d
  102. def attach_daily(trades: pd.DataFrame, daily: pd.DataFrame) -> pd.DataFrame:
  103. """为每笔交易附加开仓日的日线指标"""
  104. daily_cols = ['RSI_D', 'K_D', 'D_D', 'J_D',
  105. 'MACD_D', 'MACDhist_D',
  106. 'pct_MA20', 'pct_MA60', 'MA5_slope',
  107. 'BB_pos_D', 'Mom5_D', 'Mom10_D']
  108. result = trades.copy()
  109. for c in daily_cols:
  110. result[c] = float('nan')
  111. for i, row in trades.iterrows():
  112. entry_day = row['开仓时间'].normalize()
  113. if entry_day in daily.index:
  114. for c in daily_cols:
  115. if c in daily.columns:
  116. result.at[i, c] = daily.loc[entry_day, c]
  117. return result
  118. # ──────────────────────────────────────────────────────────────────
  119. # 2. 指标分箱定义
  120. # ──────────────────────────────────────────────────────────────────
  121. IND_BINS = {
  122. # ── 30分钟指标 ──
  123. 'RSI': (
  124. [-np.inf, 25, 35, 45, 55, 65, np.inf],
  125. ['超卖<25', '弱25-35', '偏弱35-45', '中45-55', '偏强55-65', '超买>65']
  126. ),
  127. 'K': (
  128. [-np.inf, 20, 40, 60, 80, np.inf],
  129. ['超卖<20', '低20-40', '中40-60', '高60-80', '超买>80']
  130. ),
  131. 'J': (
  132. [-np.inf, 0, 20, 50, 80, 100, np.inf],
  133. ['极卖<0', '超卖0-20', '低20-50', '高50-80', '超买80-100', '极买>100']
  134. ),
  135. 'MACD_hist': (
  136. [-np.inf, -0.8, -0.2, 0, 0.2, 0.8, np.inf],
  137. ['强空<-0.8', '空-0.8~-0.2', '弱空~0', '弱多0~0.2', '多0.2~0.8', '强多>0.8']
  138. ),
  139. 'Momentum': (
  140. [-np.inf, -0.03, -0.01, 0, 0.01, 0.03, np.inf],
  141. ['大跌>3%', '跌1-3%', '小跌<1%', '小涨<1%', '涨1-3%', '大涨>3%']
  142. ),
  143. 'Volume_Ratio': (
  144. [-np.inf, 0.5, 0.8, 1.2, 1.8, np.inf],
  145. ['缩量<0.5', '偏低0.5-0.8', '平量0.8-1.2', '放量1.2-1.8', '大放>1.8']
  146. ),
  147. 'BB_width': (
  148. [-np.inf, 0.025, 0.04, 0.06, 0.08, np.inf],
  149. ['极窄<2.5%', '窄2.5-4%', '中4-6%', '宽6-8%', '极宽>8%']
  150. ),
  151. 'ATR_Pct': (
  152. [-np.inf, 0.006, 0.009, 0.013, 0.017, np.inf],
  153. ['极低<0.6%', '低0.6-0.9%', '中0.9-1.3%', '高1.3-1.7%', '极高>1.7%']
  154. ),
  155. # ── 日线指标 ──
  156. 'RSI_D': (
  157. [-np.inf, 30, 40, 50, 60, 70, np.inf],
  158. ['超卖<30', '弱30-40', '偏弱40-50', '中50-60', '偏强60-70', '超买>70']
  159. ),
  160. 'K_D': (
  161. [-np.inf, 20, 40, 60, 80, np.inf],
  162. ['超卖<20', '低20-40', '中40-60', '高60-80', '超买>80']
  163. ),
  164. 'J_D': (
  165. [-np.inf, 0, 20, 50, 80, 100, np.inf],
  166. ['极卖<0', '超卖0-20', '低20-50', '高50-80', '超买80-100', '极买>100']
  167. ),
  168. 'MACDhist_D': (
  169. [-np.inf, -2, -0.5, 0, 0.5, 2, np.inf],
  170. ['强空<-2', '空-2~-0.5', '弱空~0', '弱多0~0.5', '多0.5~2', '强多>2']
  171. ),
  172. 'pct_MA20': (
  173. [-np.inf, -5, -2, 0, 2, 5, np.inf],
  174. ['大幅低<-5%', '低-5~-2%', '略低-2~0%', '略高0~2%', '高2~5%', '大幅高>5%']
  175. ),
  176. 'pct_MA60': (
  177. [-np.inf, -8, -3, 0, 3, 8, np.inf],
  178. ['大幅低<-8%', '低-8~-3%', '略低-3~0%', '略高0~3%', '高3~8%', '大幅高>8%']
  179. ),
  180. 'BB_pos_D': (
  181. [-np.inf, 0.1, 0.3, 0.5, 0.7, 0.9, np.inf],
  182. ['超下轨<0.1', '下轨0.1-0.3', '中下0.3-0.5', '中上0.5-0.7', '上轨0.7-0.9', '超上轨>0.9']
  183. ),
  184. 'MA5_slope': (
  185. [-np.inf, -0.5, -0.1, 0.1, 0.5, np.inf],
  186. ['急降<-0.5%', '下降-0.5~-0.1%', '平-0.1~0.1%', '上升0.1~0.5%', '急升>0.5%']
  187. ),
  188. 'Mom5_D': (
  189. [-np.inf, -4, -2, 0, 2, 4, np.inf],
  190. ['大跌>4%', '跌2-4%', '小跌<2%', '小涨<2%', '涨2-4%', '大涨>4%']
  191. ),
  192. 'Mom10_D': (
  193. [-np.inf, -6, -3, 0, 3, 6, np.inf],
  194. ['大跌>6%', '跌3-6%', '小跌<3%', '小涨<3%', '涨3-6%', '大涨>6%']
  195. ),
  196. }
  197. def do_bin(series: pd.Series, col: str) -> pd.Series:
  198. """对指标列分箱,返回带标签的Categorical"""
  199. bins, labels = IND_BINS[col]
  200. return pd.cut(pd.to_numeric(series, errors='coerce'),
  201. bins=bins, labels=labels, include_lowest=True)
  202. # ──────────────────────────────────────────────────────────────────
  203. # 3. 分析函数
  204. # ──────────────────────────────────────────────────────────────────
  205. def analyze_univariate(df: pd.DataFrame, min_n=MIN_TRADES) -> dict:
  206. """单变量分析:返回 {col: DataFrame(bin, n, wr, avg_pnl, total_pnl)}"""
  207. results = {}
  208. for col in IND_BINS:
  209. if col not in df.columns:
  210. continue
  211. binned = do_bin(df[col], col)
  212. tmp = df.copy()
  213. tmp['_b'] = binned
  214. grp = (tmp.groupby('_b', observed=True)
  215. .agg(n=('盈利', 'count'),
  216. wr=('盈利', 'mean'),
  217. avg_pnl=('实际盈亏', 'mean'),
  218. total_pnl=('实际盈亏', 'sum'))
  219. .reset_index()
  220. .rename(columns={'_b': 'bin'}))
  221. grp = grp[grp['n'] >= min_n].copy()
  222. grp['col'] = col
  223. results[col] = grp
  224. return results
  225. # 双变量分析的关键指标对
  226. BIVARIATE_PAIRS = [
  227. ('RSI', 'RSI_D'), # 30min超卖 × 日线超卖共振
  228. ('J', 'J_D'), # KDJ J值跨周期
  229. ('Momentum', 'Mom5_D'), # 动量跨周期
  230. ('Volume_Ratio', 'ATR_Pct'), # 放量 × 波动率
  231. ('MACD_hist', 'MACDhist_D'), # MACD多空力道跨周期
  232. ('RSI', 'pct_MA20'), # 超卖 × 均线偏离
  233. ('BB_pos_D', 'J_D'), # 日线布林位置 × 日线KDJ
  234. ('RSI_D', 'Mom5_D'), # 日线超卖 × 日线动量
  235. ('RSI', 'MA5_slope'), # 30min超卖 × 日线趋势方向
  236. ('K', 'K_D'), # KDJ K跨周期
  237. ]
  238. def analyze_bivariate(df: pd.DataFrame,
  239. pairs=BIVARIATE_PAIRS,
  240. min_n=MIN_TRADES) -> dict:
  241. """双变量分析:返回 {c1×c2: DataFrame(c1_bin, c2_bin, n, wr, avg_pnl, total_pnl)}"""
  242. results = {}
  243. for c1, c2 in pairs:
  244. if c1 not in df.columns or c2 not in df.columns:
  245. continue
  246. tmp = df.copy()
  247. tmp['_b1'] = do_bin(df[c1], c1)
  248. tmp['_b2'] = do_bin(df[c2], c2)
  249. grp = (tmp.groupby(['_b1', '_b2'], observed=True)
  250. .agg(n=('盈利', 'count'),
  251. wr=('盈利', 'mean'),
  252. avg_pnl=('实际盈亏', 'mean'),
  253. total_pnl=('实际盈亏', 'sum'))
  254. .reset_index())
  255. grp.columns = [c1, c2, 'n', 'wr', 'avg_pnl', 'total_pnl']
  256. grp = grp[grp['n'] >= min_n].copy()
  257. grp['pair'] = f'{c1}×{c2}'
  258. results[f'{c1}×{c2}'] = grp
  259. return results
  260. # ──────────────────────────────────────────────────────────────────
  261. # 4. 区域识别
  262. # ──────────────────────────────────────────────────────────────────
  263. def identify_zones(uni: dict, biv: dict):
  264. """
  265. 从单变量+双变量结果中提炼舒适区(WR≥60%)和黑暗区(WR≤35%)。
  266. 综合评分 = WR × avg_PnL(正向 → 舒适;负向 → 黑暗)
  267. """
  268. rows = []
  269. # 单变量
  270. for col, grp in uni.items():
  271. for _, r in grp.iterrows():
  272. rows.append({
  273. 'type': '单变量',
  274. 'dim1': col,
  275. 'bin1': str(r['bin']),
  276. 'dim2': '',
  277. 'bin2': '',
  278. 'n': int(r['n']),
  279. 'wr': r['wr'],
  280. 'avg_pnl': r['avg_pnl'],
  281. 'total_pnl': r['total_pnl'],
  282. 'score': r['wr'] * r['avg_pnl'],
  283. })
  284. # 双变量
  285. for key, grp in biv.items():
  286. c1, c2 = key.split('×')
  287. for _, r in grp.iterrows():
  288. rows.append({
  289. 'type': '双变量',
  290. 'dim1': c1,
  291. 'bin1': str(r[c1]),
  292. 'dim2': c2,
  293. 'bin2': str(r[c2]),
  294. 'n': int(r['n']),
  295. 'wr': r['wr'],
  296. 'avg_pnl': r['avg_pnl'],
  297. 'total_pnl': r['total_pnl'],
  298. 'score': r['wr'] * r['avg_pnl'],
  299. })
  300. all_df = pd.DataFrame(rows)
  301. comfort = (all_df[all_df['wr'] >= WR_COMFORT]
  302. .sort_values('score', ascending=False)
  303. .reset_index(drop=True))
  304. dark = (all_df[all_df['wr'] <= WR_DARK]
  305. .sort_values('score', ascending=True)
  306. .reset_index(drop=True))
  307. return all_df, comfort, dark
  308. # ──────────────────────────────────────────────────────────────────
  309. # 5. 报告输出
  310. # ──────────────────────────────────────────────────────────────────
  311. def _wr_bar(wr: float, width=20) -> str:
  312. """ASCII 胜率条"""
  313. filled = int(wr * width)
  314. bar = '█' * filled + '░' * (width - filled)
  315. return f'[{bar}] {wr:.1%}'
  316. def print_univariate_summary(uni: dict):
  317. """打印每个指标的分箱胜率摘要(以WR排序)"""
  318. print(f'\n{"指标":<15} {"分箱":<20} {"笔数":>5} {"胜率":>28} {"均盈亏":>10} {"累计盈亏":>12}')
  319. print('-' * 92)
  320. for col, grp in uni.items():
  321. if grp.empty:
  322. continue
  323. grp_s = grp.sort_values('wr', ascending=False)
  324. first = True
  325. for _, r in grp_s.iterrows():
  326. col_disp = col if first else ''
  327. first = False
  328. bar = _wr_bar(r['wr'])
  329. pnl_sign = '+' if r['avg_pnl'] >= 0 else ''
  330. print(f'{col_disp:<15} {str(r["bin"]):<20} {int(r["n"]):>5} '
  331. f'{bar} {pnl_sign}{r["avg_pnl"]:>9,.0f} {r["total_pnl"]:>+12,.0f}')
  332. print()
  333. def print_bivariate_heatmap(biv: dict, top_each=6):
  334. """打印每个双变量组合的 Top 舒适+黑暗区"""
  335. for pair_key, grp in biv.items():
  336. if grp.empty:
  337. continue
  338. c1, c2 = pair_key.split('×')
  339. print(f'\n ◆ {pair_key} (共{len(grp)}个有效组合,最少{MIN_TRADES}笔)')
  340. print(f' {"":1} {c1:<22} {c2:<22} {"笔数":>4} {"胜率":>7} {"均盈亏":>10} {"累计盈亏":>12}')
  341. print(f' {"-"} {"-"*22} {"-"*22} {"-"*4} {"-"*7} {"-"*10} {"-"*12}')
  342. # 舒适
  343. top_comfort = grp.nlargest(top_each, 'wr')
  344. for _, r in top_comfort.iterrows():
  345. flag = '★' if r['wr'] >= WR_COMFORT else ' '
  346. print(f' {flag} {str(r[c1]):<22} {str(r[c2]):<22} {int(r["n"]):>4} '
  347. f'{r["wr"]:>7.1%} {r["avg_pnl"]:>+10,.0f} {r["total_pnl"]:>+12,.0f}')
  348. # 黑暗(如果有)
  349. dark_rows = grp[grp['wr'] <= WR_DARK].nsmallest(min(3, top_each), 'wr')
  350. if not dark_rows.empty:
  351. print(f' --- 黑暗 ---')
  352. for _, r in dark_rows.iterrows():
  353. print(f' ✗ {str(r[c1]):<22} {str(r[c2]):<22} {int(r["n"]):>4} '
  354. f'{r["wr"]:>7.1%} {r["avg_pnl"]:>+10,.0f} {r["total_pnl"]:>+12,.0f}')
  355. def print_zones(comfort: pd.DataFrame, dark: pd.DataFrame, top_n=TOP_N):
  356. """打印最终舒适区/黑暗区汇总"""
  357. def _fmt_zone(row):
  358. if row['dim2']:
  359. return f"{row['dim1']}={row['bin1']} & {row['dim2']}={row['bin2']}"
  360. return f"{row['dim1']}={row['bin1']}"
  361. print(f'\n{"★ 舒适区 TOP":=<76}')
  362. print(f' {"#":>3} {"类型":<5} {"条件":<52} {"笔数":>4} {"胜率":>7} {"均盈亏":>10}')
  363. print(f' {"---":>3} {"-----":<5} {"-"*52} {"-"*4} {"-"*7} {"-"*10}')
  364. for i, (_, r) in enumerate(comfort.head(top_n).iterrows(), 1):
  365. cond = _fmt_zone(r)
  366. print(f' {i:>3} {r["type"]:<5} {cond:<52} {int(r["n"]):>4} '
  367. f'{r["wr"]:>7.1%} {r["avg_pnl"]:>+10,.0f}')
  368. print(f'\n{"✗ 黑暗区 TOP":=<76}')
  369. print(f' {"#":>3} {"类型":<5} {"条件":<52} {"笔数":>4} {"胜率":>7} {"均盈亏":>10}')
  370. print(f' {"---":>3} {"-----":<5} {"-"*52} {"-"*4} {"-"*7} {"-"*10}')
  371. for i, (_, r) in enumerate(dark.head(top_n).iterrows(), 1):
  372. cond = _fmt_zone(r)
  373. print(f' {i:>3} {r["type"]:<5} {cond:<52} {int(r["n"]):>4} '
  374. f'{r["wr"]:>7.1%} {r["avg_pnl"]:>+10,.0f}')
  375. # ──────────────────────────────────────────────────────────────────
  376. # 6. CSV 导出
  377. # ──────────────────────────────────────────────────────────────────
  378. def export_csv(trades: pd.DataFrame, all_zones: pd.DataFrame,
  379. comfort: pd.DataFrame, dark: pd.DataFrame):
  380. ts = datetime.now().strftime('%Y%m%d_%H%M%S')
  381. out = os.path.dirname(__file__)
  382. # 每笔交易完整指标(含日线)
  383. fname_trades = os.path.join(out, f'zone_trades_{ts}.csv')
  384. trades.to_csv(fname_trades, index=False, encoding='utf-8-sig')
  385. print(f' 交易明细(含日线指标): {os.path.basename(fname_trades)} ({len(trades)}笔)')
  386. # 全量分析结果
  387. fname_zones = os.path.join(out, f'zone_analysis_{ts}.csv')
  388. all_zones.to_csv(fname_zones, index=False, encoding='utf-8-sig')
  389. print(f' 全量区域分析: {os.path.basename(fname_zones)} ({len(all_zones)}行)')
  390. # 舒适区
  391. fname_c = os.path.join(out, f'zone_comfort_{ts}.csv')
  392. comfort.to_csv(fname_c, index=False, encoding='utf-8-sig')
  393. print(f' 舒适区: {os.path.basename(fname_c)} ({len(comfort)}个区域)')
  394. # 黑暗区
  395. fname_d = os.path.join(out, f'zone_dark_{ts}.csv')
  396. dark.to_csv(fname_d, index=False, encoding='utf-8-sig')
  397. print(f' 黑暗区: {os.path.basename(fname_d)} ({len(dark)}个区域)')
  398. # ──────────────────────────────────────────────────────────────────
  399. # 7. 主流程
  400. # ──────────────────────────────────────────────────────────────────
  401. def main():
  402. print(SEP)
  403. print(' CYB50 K线 + 日线指标 舒适区 / 黑暗区 深度分析')
  404. print(f' Version B(非死亡区全量)| 最少笔数≥{MIN_TRADES} | '
  405. f'舒适≥{WR_COMFORT:.0%} | 黑暗≤{WR_DARK:.0%}')
  406. print(SEP)
  407. # ── 加载数据 ──
  408. print(f'\n📂 加载数据...')
  409. trades = load_backtest_vB()
  410. print(f' 回测交易: {len(trades)}笔 '
  411. f'{trades["开仓时间"].min().date()} ~ {trades["开仓时间"].max().date()}')
  412. print(f'\n📈 计算日线指标...')
  413. daily = build_daily_indicators()
  414. print(f' 日线: {len(daily)}条 {daily.index[0].date()} ~ {daily.index[-1].date()}')
  415. print(f'\n🔗 关联日线指标...')
  416. trades = attach_daily(trades, daily)
  417. daily_hits = trades['RSI_D'].notna().sum()
  418. print(f' 成功关联日线指标: {daily_hits}/{len(trades)} 笔')
  419. # ── 分析 ──
  420. print(f'\n🔬 单变量分析({len(IND_BINS)}个指标)...')
  421. uni = analyze_univariate(trades)
  422. print(f' 完成: {len(uni)}个指标')
  423. print(f'\n🔬 双变量分析({len(BIVARIATE_PAIRS)}个组合对)...')
  424. biv = analyze_bivariate(trades)
  425. print(f' 完成: {len(biv)}个组合对')
  426. print(f'\n🎯 识别舒适区 / 黑暗区...')
  427. all_zones, comfort, dark = identify_zones(uni, biv)
  428. print(f' 舒适区(WR≥{WR_COMFORT:.0%}): {len(comfort)}个')
  429. print(f' 黑暗区(WR≤{WR_DARK:.0%}): {len(dark)}个')
  430. # ── 报告 ──
  431. print(f'\n{SEP}')
  432. print(' 单变量分析 — 各指标分箱胜率')
  433. print(SEP)
  434. print_univariate_summary(uni)
  435. print(f'\n{SEP}')
  436. print(' 双变量分析 — 关键指标对组合')
  437. print(SEP)
  438. print_bivariate_heatmap(biv)
  439. print(f'\n{SEP}')
  440. print(' 综合区域汇总')
  441. print(SEP)
  442. print_zones(comfort, dark)
  443. # ── 导出 ──
  444. print(f'\n📁 导出CSV...')
  445. export_csv(trades, all_zones, comfort, dark)
  446. print()
  447. print(SEP)
  448. print(' 分析完成')
  449. print(SEP)
  450. if __name__ == '__main__':
  451. main()