comfort_zone_research.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. CYB50 T+1 策略舒适区深度研究
  5. 客观量化哪些指标组合预测策略进入高胜率模式
  6. """
  7. import sys
  8. import io
  9. if sys.platform == 'win32':
  10. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
  11. sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
  12. import pandas as pd
  13. import numpy as np
  14. from scipy import stats
  15. import warnings
  16. warnings.filterwarnings('ignore')
  17. OUTPUT_FILE = 'D:/work/project/cyb50-quant/cat-fly/t1/comfort_zone_research_result.txt'
  18. def log(msg=''):
  19. print(msg)
  20. def load_trades():
  21. df = pd.read_csv(
  22. 'D:/work/project/cyb50-quant/cat-fly/t1/t1_trades_with_environment_20260327_141655.csv',
  23. encoding='utf-8-sig'
  24. )
  25. # 重命名列(按位置)
  26. cols = [
  27. '交易方向','开仓时间','平仓时间','开仓价格','平仓价格','仓位',
  28. '盈亏金额','盈亏百分比','退出原因','持仓周期数','持仓小时数',
  29. 'T1调整','原平仓时间','原平仓价格','原盈亏','盈亏变化',
  30. '入场信号','开仓市值','平仓时资金','市场状态',
  31. '趋势短期','趋势中期','趋势强度','波动率分位','波动率水平',
  32. '成交量分位','布林带位置','布林带区域','RSI分位','RSI区域',
  33. '1日动量','入场价格'
  34. ]
  35. df.columns = cols
  36. df['开仓时间'] = pd.to_datetime(df['开仓时间'])
  37. df['年份'] = df['开仓时间'].dt.year
  38. df['月份'] = df['开仓时间'].dt.month
  39. df['年月'] = df['开仓时间'].dt.to_period('M')
  40. df['盈利'] = df['盈亏金额'] > 0
  41. df['盈亏百分比'] = pd.to_numeric(df['盈亏百分比'], errors='coerce')
  42. df['波动率分位'] = pd.to_numeric(df['波动率分位'], errors='coerce')
  43. df['RSI分位'] = pd.to_numeric(df['RSI分位'], errors='coerce')
  44. df['布林带位置'] = pd.to_numeric(df['布林带位置'], errors='coerce')
  45. df['成交量分位'] = pd.to_numeric(df['成交量分位'], errors='coerce')
  46. df['趋势强度'] = pd.to_numeric(df['趋势强度'], errors='coerce')
  47. df['1日动量'] = pd.to_numeric(df['1日动量'], errors='coerce')
  48. return df
  49. def section(title, f):
  50. line = '=' * 72
  51. log(line)
  52. log(f' {title}')
  53. log(line)
  54. def yearly_summary(df, f):
  55. section('第一部分:年度绩效对比', f)
  56. log()
  57. for year in sorted(df['年份'].unique()):
  58. sub = df[df['年份'] == year]
  59. wr = sub['盈利'].mean()
  60. total_pnl = sub['盈亏金额'].sum()
  61. avg_win = sub[sub['盈利']]['盈亏金额'].mean() if sub['盈利'].any() else 0
  62. avg_loss = sub[~sub['盈利']]['盈亏金额'].mean() if (~sub['盈利']).any() else 0
  63. plr = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf')
  64. log(f' {year}年: {len(sub)}笔 | 胜率{wr:.1%} | 盈亏比{plr:.2f} | 总盈亏{total_pnl:+,.0f}元')
  65. log()
  66. # 好年份 vs 差年份
  67. good = df[df['年份'].isin([2025, 2026])]
  68. bad = df[df['年份'].isin([2023, 2024])]
  69. log(f' 好年份(2025-2026): {len(good)}笔, 胜率{good["盈利"].mean():.1%}, 平均盈亏{good["盈亏金额"].mean():+,.0f}元')
  70. log(f' 差年份(2023-2024): {len(bad)}笔, 胜率{bad["盈利"].mean():.1%}, 平均盈亏{bad["盈亏金额"].mean():+,.0f}元')
  71. log()
  72. def analyze_categorical(df, col, label, f, min_count=5):
  73. section(f'第二部分:类别指标分析 — {label}', f)
  74. log()
  75. results = []
  76. for val in df[col].dropna().unique():
  77. sub = df[df[col] == val]
  78. if len(sub) < min_count:
  79. continue
  80. wr = sub['盈利'].mean()
  81. avg_pnl = sub['盈亏金额'].mean()
  82. total_pnl = sub['盈亏金额'].sum()
  83. n = len(sub)
  84. results.append((val, n, wr, avg_pnl, total_pnl))
  85. results.sort(key=lambda x: x[2], reverse=True)
  86. log(f' {"类别":<20} {"笔数":>5} {"胜率":>7} {"均盈亏":>10} {"总盈亏":>12}')
  87. log(f' {"-"*20} {"-"*5} {"-"*7} {"-"*10} {"-"*12}')
  88. for val, n, wr, avg, total in results:
  89. log(f' {str(val):<20} {n:>5} {wr:>7.1%} {avg:>+10,.0f} {total:>+12,.0f}')
  90. log()
  91. return results
  92. def analyze_continuous_bins(df, col, label, f, bins=5):
  93. section(f'第三部分:连续指标分位分析 — {label}', f)
  94. log()
  95. valid = df[df[col].notna()].copy()
  96. if len(valid) < 20:
  97. log(f' 数据不足,跳过')
  98. return
  99. valid['_bin'] = pd.qcut(valid[col], q=bins, duplicates='drop')
  100. results = []
  101. for bin_val in valid['_bin'].cat.categories:
  102. sub = valid[valid['_bin'] == bin_val]
  103. wr = sub['盈利'].mean()
  104. avg_pnl = sub['盈亏金额'].mean()
  105. n = len(sub)
  106. lo = bin_val.left
  107. hi = bin_val.right
  108. results.append((lo, hi, n, wr, avg_pnl))
  109. log(f' {"区间":<25} {"笔数":>5} {"胜率":>7} {"均盈亏":>10}')
  110. log(f' {"-"*25} {"-"*5} {"-"*7} {"-"*10}')
  111. for lo, hi, n, wr, avg in results:
  112. log(f' [{lo:>8.3f}, {hi:>8.3f}] {n:>5} {wr:>7.1%} {avg:>+10,.0f}')
  113. log()
  114. # 相关性
  115. corr, pval = stats.pointbiserialr(valid[col], valid['盈利'].astype(int))
  116. log(f' 与胜率相关系数: r={corr:.3f}, p={pval:.3f}{" ★显著" if pval < 0.05 else ""}')
  117. log()
  118. return results
  119. def good_vs_bad_distributions(df, f):
  120. section('第四部分:好/差年份 各指标分布对比', f)
  121. log()
  122. good = df[df['年份'].isin([2025, 2026])]
  123. bad = df[df['年份'].isin([2023, 2024])]
  124. # 连续指标
  125. num_cols = [
  126. ('波动率分位', '波动率分位数'),
  127. ('RSI分位', 'RSI分位数'),
  128. ('布林带位置', '布林带位置'),
  129. ('趋势强度', '趋势强度'),
  130. ('1日动量', '1日动量'),
  131. ('成交量分位', '成交量分位'),
  132. ]
  133. log(f' {"指标":<15} {"差年份均值":>12} {"好年份均值":>12} {"差值":>10} {"t检验p值":>10} {"显著?":>6}')
  134. log(f' {"-"*15} {"-"*12} {"-"*12} {"-"*10} {"-"*10} {"-"*6}')
  135. for col, label in num_cols:
  136. g_vals = good[col].dropna()
  137. b_vals = bad[col].dropna()
  138. if len(g_vals) < 5 or len(b_vals) < 5:
  139. continue
  140. t, p = stats.ttest_ind(g_vals, b_vals)
  141. diff = g_vals.mean() - b_vals.mean()
  142. sig = '★' if p < 0.05 else ''
  143. log(f' {label:<15} {b_vals.mean():>12.3f} {g_vals.mean():>12.3f} {diff:>+10.3f} {p:>10.3f} {sig:>6}')
  144. log()
  145. # 类别指标
  146. cat_cols = [
  147. ('市场状态', '市场状态'),
  148. ('趋势短期', '趋势短期'),
  149. ('趋势中期', '趋势中期'),
  150. ('波动率水平', '波动率水平'),
  151. ('布林带区域', '布林带区域'),
  152. ('RSI区域', 'RSI区域'),
  153. ]
  154. for col, label in cat_cols:
  155. log(f' [{label}] 好年份分布 vs 差年份分布:')
  156. all_vals = df[col].dropna().unique()
  157. log(f' {"类别":<18} {"差年份占比":>10} {"好年份占比":>10} {"差值":>8}')
  158. for val in sorted(all_vals):
  159. b_pct = (bad[col] == val).mean()
  160. g_pct = (good[col] == val).mean()
  161. diff = g_pct - b_pct
  162. marker = ' ←' if abs(diff) > 0.05 else ''
  163. log(f' {str(val):<18} {b_pct:>10.1%} {g_pct:>10.1%} {diff:>+8.1%}{marker}')
  164. log()
  165. def winning_condition_scan(df, f):
  166. section('第五部分:胜率 > 60% 的单指标条件扫描', f)
  167. log()
  168. log(' 过滤条件:笔数≥8, 胜率≥60%')
  169. log()
  170. cat_cols = ['市场状态', '趋势短期', '趋势中期', '波动率水平', '布林带区域', 'RSI区域', 'T1调整']
  171. findings = []
  172. for col in cat_cols:
  173. for val in df[col].dropna().unique():
  174. sub = df[df[col] == val]
  175. if len(sub) < 8:
  176. continue
  177. wr = sub['盈利'].mean()
  178. avg = sub['盈亏金额'].mean()
  179. if wr >= 0.60:
  180. findings.append((f'{col}={val}', len(sub), wr, avg))
  181. findings.sort(key=lambda x: x[2], reverse=True)
  182. log(f' {"条件":<30} {"笔数":>5} {"胜率":>7} {"均盈亏":>10}')
  183. log(f' {"-"*30} {"-"*5} {"-"*7} {"-"*10}')
  184. for cond, n, wr, avg in findings:
  185. log(f' {cond:<30} {n:>5} {wr:>7.1%} {avg:>+10,.0f}')
  186. log()
  187. def combo_scan(df, f):
  188. section('第六部分:双指标组合扫描 (笔数≥8, 胜率≥60%)', f)
  189. log()
  190. cat_cols = ['市场状态', '趋势中期', '波动率水平', '布林带区域', 'RSI区域']
  191. combos = []
  192. for i, c1 in enumerate(cat_cols):
  193. for c2 in cat_cols[i+1:]:
  194. for v1 in df[c1].dropna().unique():
  195. for v2 in df[c2].dropna().unique():
  196. sub = df[(df[c1] == v1) & (df[c2] == v2)]
  197. if len(sub) < 8:
  198. continue
  199. wr = sub['盈利'].mean()
  200. avg = sub['盈亏金额'].mean()
  201. if wr >= 0.60:
  202. combos.append((f'{c1}={v1} & {c2}={v2}', len(sub), wr, avg))
  203. combos.sort(key=lambda x: x[2], reverse=True)
  204. log(f' {"组合条件":<45} {"笔数":>5} {"胜率":>7} {"均盈亏":>10}')
  205. log(f' {"-"*45} {"-"*5} {"-"*7} {"-"*10}')
  206. for cond, n, wr, avg in combos[:20]:
  207. log(f' {cond:<45} {n:>5} {wr:>7.1%} {avg:>+10,.0f}')
  208. log()
  209. def volatility_threshold(df, f):
  210. section('第七部分:波动率阈值精确定位', f)
  211. log()
  212. valid = df[df['波动率分位'].notna()].copy()
  213. thresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
  214. log(f' 波动率分位 < X 时的胜率:')
  215. log(f' {"阈值":>8} {"笔数":>6} {"胜率":>8} {"均盈亏":>10}')
  216. log(f' {"-"*8} {"-"*6} {"-"*8} {"-"*10}')
  217. for thr in thresholds:
  218. sub = valid[valid['波动率分位'] < thr]
  219. if len(sub) < 5:
  220. continue
  221. wr = sub['盈利'].mean()
  222. avg = sub['盈亏金额'].mean()
  223. log(f' {thr:>8.1f} {len(sub):>6} {wr:>8.1%} {avg:>+10,.0f}')
  224. log()
  225. log(f' 波动率分位 >= X 时的胜率:')
  226. log(f' {"阈值":>8} {"笔数":>6} {"胜率":>8} {"均盈亏":>10}')
  227. log(f' {"-"*8} {"-"*6} {"-"*8} {"-"*10}')
  228. for thr in thresholds:
  229. sub = valid[valid['波动率分位'] >= thr]
  230. if len(sub) < 5:
  231. continue
  232. wr = sub['盈利'].mean()
  233. avg = sub['盈亏金额'].mean()
  234. log(f' {thr:>8.1f} {len(sub):>6} {wr:>8.1%} {avg:>+10,.0f}')
  235. log()
  236. def rsi_threshold(df, f):
  237. section('第八部分:RSI分位阈值精确定位', f)
  238. log()
  239. valid = df[df['RSI分位'].notna()].copy()
  240. thresholds = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
  241. log(f' RSI分位 < X 时的胜率(低RSI/超卖区间做多):')
  242. log(f' {"阈值":>8} {"笔数":>6} {"胜率":>8} {"均盈亏":>10}')
  243. log(f' {"-"*8} {"-"*6} {"-"*8} {"-"*10}')
  244. for thr in thresholds:
  245. sub = valid[valid['RSI分位'] < thr]
  246. if len(sub) < 5:
  247. continue
  248. wr = sub['盈利'].mean()
  249. avg = sub['盈亏金额'].mean()
  250. log(f' {thr:>8.1f} {len(sub):>6} {wr:>8.1%} {avg:>+10,.0f}')
  251. log()
  252. def monthly_rolling(df, f):
  253. section('第九部分:滚动月度胜率 — 识别策略周期规律', f)
  254. log()
  255. monthly = df.groupby('年月').agg(
  256. 笔数=('盈利', 'count'),
  257. 胜率=('盈利', 'mean'),
  258. 总盈亏=('盈亏金额', 'sum')
  259. ).reset_index()
  260. monthly['年月_str'] = monthly['年月'].astype(str)
  261. log(f' {"年月":<10} {"笔数":>5} {"胜率":>7} {"总盈亏":>12} {"状态":>6}')
  262. log(f' {"-"*10} {"-"*5} {"-"*7} {"-"*12} {"-"*6}')
  263. for _, row in monthly.iterrows():
  264. state = '★好' if row['胜率'] >= 0.6 else ('△差' if row['胜率'] < 0.35 else ' ')
  265. log(f' {row["年月_str"]:<10} {row["笔数"]:>5} {row["胜率"]:>7.1%} {row["总盈亏"]:>+12,.0f} {state:>6}')
  266. log()
  267. # 统计好月份的特征
  268. good_months = monthly[monthly['胜率'] >= 0.6]
  269. bad_months = monthly[monthly['胜率'] < 0.35]
  270. log(f' 胜率≥60%的月份: {len(good_months)}个, 总计{good_months["笔数"].sum()}笔')
  271. log(f' 胜率<35%的月份: {len(bad_months)}个, 总计{bad_months["笔数"].sum()}笔')
  272. log()
  273. def comfort_zone_score(df, f):
  274. section('第十部分:多维舒适区评分模型', f)
  275. log()
  276. log(' 基于以上分析,建立量化评分规则(每项满足得分累加):')
  277. log()
  278. df2 = df.copy()
  279. # 规则定义:(描述, 条件函数, 分值)
  280. rules = []
  281. # 波动率
  282. if df2['波动率分位'].notna().any():
  283. rules.append(('波动率分位 < 0.4', lambda r: r['波动率分位'] < 0.4 if pd.notna(r['波动率分位']) else False, 2))
  284. rules.append(('波动率分位 < 0.2', lambda r: r['波动率分位'] < 0.2 if pd.notna(r['波动率分位']) else False, 1))
  285. # RSI
  286. if df2['RSI分位'].notna().any():
  287. rules.append(('RSI分位 < 0.4', lambda r: r['RSI分位'] < 0.4 if pd.notna(r['RSI分位']) else False, 1))
  288. rules.append(('RSI分位 < 0.5 (偏低)', lambda r: r['RSI分位'] < 0.5 if pd.notna(r['RSI分位']) else False, 1))
  289. # 趋势
  290. rules.append(('趋势中期=上涨', lambda r: r['趋势中期'] == '上涨', 2))
  291. rules.append(('趋势短期=上涨', lambda r: r['趋势短期'] == '上涨', 1))
  292. # 布林带
  293. rules.append(('布林带区域=下轨中位', lambda r: '下轨' in str(r['布林带区域']), 1))
  294. rules.append(('布林带区域=中轨', lambda r: r['布林带区域'] == '中轨', 1))
  295. # 市场状态
  296. rules.append(('市场状态=强趋势低波', lambda r: r['市场状态'] == '强趋势低波', 2))
  297. rules.append(('市场状态=趋势上涨', lambda r: '趋势' in str(r['市场状态']) and '上' in str(r['市场状态']), 1))
  298. # 计算每笔交易的舒适区评分
  299. def score_trade(row):
  300. s = 0
  301. for _, cond, pts in rules:
  302. try:
  303. if cond(row):
  304. s += pts
  305. except:
  306. pass
  307. return s
  308. df2['舒适区评分'] = df2.apply(score_trade, axis=1)
  309. # 按评分分组
  310. log(f' {"评分":>6} {"笔数":>5} {"胜率":>7} {"均盈亏":>10} {"总盈亏":>12}')
  311. log(f' {"-"*6} {"-"*5} {"-"*7} {"-"*10} {"-"*12}')
  312. for score in sorted(df2['舒适区评分'].unique()):
  313. sub = df2[df2['舒适区评分'] == score]
  314. wr = sub['盈利'].mean()
  315. avg = sub['盈亏金额'].mean()
  316. total = sub['盈亏金额'].sum()
  317. log(f' {score:>6} {len(sub):>5} {wr:>7.1%} {avg:>+10,.0f} {total:>+12,.0f}')
  318. log()
  319. # 阈值建议
  320. for thr in [4, 5, 6, 7, 8]:
  321. sub = df2[df2['舒适区评分'] >= thr]
  322. if len(sub) < 5:
  323. continue
  324. wr = sub['盈利'].mean()
  325. avg = sub['盈亏金额'].mean()
  326. cov = len(sub) / len(df2)
  327. log(f' 评分≥{thr}: {len(sub)}笔 ({cov:.1%}覆盖) | 胜率{wr:.1%} | 均盈亏{avg:+,.0f}元')
  328. log()
  329. # 输出规则列表
  330. log(' 评分规则明细:')
  331. for desc, _, pts in rules:
  332. log(f' +{pts}分 {desc}')
  333. log()
  334. return df2
  335. def momentum_analysis(df, f):
  336. section('第十一部分:1日动量与胜率关系', f)
  337. log()
  338. valid = df[df['1日动量'].notna()].copy()
  339. log(f' 1日动量 > 0 (上涨动量): {len(valid[valid["1日动量"]>0])}笔, 胜率{valid[valid["1日动量"]>0]["盈利"].mean():.1%}')
  340. log(f' 1日动量 < 0 (下跌动量): {len(valid[valid["1日动量"]<0])}笔, 胜率{valid[valid["1日动量"]<0]["盈利"].mean():.1%}')
  341. log()
  342. corr, pval = stats.pointbiserialr(valid['1日动量'], valid['盈利'].astype(int))
  343. log(f' 动量与胜率相关系数: r={corr:.3f}, p={pval:.3f}{" ★显著" if pval < 0.05 else ""}')
  344. log()
  345. def t1_effect(df, f):
  346. section('第十二部分:T+1调整对胜率的影响', f)
  347. log()
  348. t1 = df[df['T1调整'].str.contains('T1|T0', na=False)]
  349. not_t1 = df[~df['T1调整'].str.contains('T1|T0', na=False)]
  350. log(f' T+1调整交易: {len(t1)}笔, 胜率{t1["盈利"].mean():.1%}, 均盈亏{t1["盈亏金额"].mean():+,.0f}元')
  351. log(f' 非T+1交易: {len(not_t1)}笔, 胜率{not_t1["盈利"].mean():.1%}, 均盈亏{not_t1["盈亏金额"].mean():+,.0f}元')
  352. # 原盈亏 vs 新盈亏
  353. t1_valid = t1[t1['原盈亏'].notna() & t1['盈亏金额'].notna()]
  354. if len(t1_valid) > 0:
  355. orig_wins = (t1_valid['原盈亏'] > 0).mean()
  356. new_wins = (t1_valid['盈亏金额'] > 0).mean()
  357. log(f' T+1调整笔中,原始胜率{orig_wins:.1%} → T+1后胜率{new_wins:.1%}')
  358. log()
  359. def final_summary(f):
  360. section('第十三部分:综合结论与舒适区定义', f)
  361. log()
  362. log(' 基于以上分析,策略舒适区的量化定义:')
  363. log()
  364. log(' 【核心必要条件】(缺一不可)')
  365. log(' 1. 波动率水平 ≠ "极高" (高波动是最大杀手)')
  366. log(' 2. 波动率分位 < 0.5 (处于历史波动率中位以下)')
  367. log()
  368. log(' 【加分条件】(满足越多越好)')
  369. log(' +2分 趋势中期 = "上涨" (中期趋势配合做多方向)')
  370. log(' +2分 市场状态 = "强趋势低波" (最优市场环境)')
  371. log(' +2分 波动率分位 < 0.4 (低波动率环境)')
  372. log(' +1分 趋势短期 = "上涨" (短期趋势配合)')
  373. log(' +1分 RSI分位 < 0.5 (RSI未过热)')
  374. log(' +1分 布林带区域含 "下轨" (价格位于布林带下方回调位)')
  375. log(' +1分 波动率分位 < 0.2 (极低波动率加成)')
  376. log()
  377. log(' 【评分建议】')
  378. log(' 总分 ≥ 5分: 进入舒适区,正常交易')
  379. log(' 总分 3-4分: 半舒适区,减半仓位')
  380. log(' 总分 < 3分: 非舒适区,建议观望或跳过')
  381. log()
  382. log(' 【时间规律】')
  383. log(' 策略在创业板行情趋势明确、波动率收敛后表现最好')
  384. log(' 2025-2026年市场结构更适合此策略(可能与流动性环境有关)')
  385. log()
  386. def main():
  387. # 重定向输出到文件
  388. orig_stdout = sys.stdout
  389. with open(OUTPUT_FILE, 'w', encoding='utf-8') as f_out:
  390. # 双重输出:控制台 + 文件
  391. class Tee:
  392. def __init__(self, *files):
  393. self.files = files
  394. def write(self, data):
  395. for fh in self.files:
  396. fh.write(data)
  397. def flush(self):
  398. for fh in self.files:
  399. fh.flush()
  400. sys.stdout = Tee(orig_stdout, f_out)
  401. log('CYB50 T+1 策略舒适区深度研究报告')
  402. log(f'数据覆盖: 2023-03-27 ~ 2026-03-25')
  403. log()
  404. df = load_trades()
  405. log(f'加载交易记录: {len(df)}笔')
  406. log()
  407. f = None # 文件句柄占位,实际通过Tee输出
  408. yearly_summary(df, f)
  409. analyze_categorical(df, '市场状态', '市场状态', f)
  410. analyze_categorical(df, '趋势中期', '趋势中期', f)
  411. analyze_categorical(df, '波动率水平', '波动率水平', f)
  412. analyze_categorical(df, '布林带区域', '布林带区域', f)
  413. analyze_categorical(df, 'RSI区域', 'RSI区域', f)
  414. good_vs_bad_distributions(df, f)
  415. analyze_continuous_bins(df, '波动率分位', '波动率分位', f)
  416. analyze_continuous_bins(df, 'RSI分位', 'RSI分位', f)
  417. analyze_continuous_bins(df, '布林带位置', '布林带位置', f)
  418. analyze_continuous_bins(df, '趋势强度', '趋势强度', f)
  419. volatility_threshold(df, f)
  420. rsi_threshold(df, f)
  421. monthly_rolling(df, f)
  422. winning_condition_scan(df, f)
  423. combo_scan(df, f)
  424. momentum_analysis(df, f)
  425. t1_effect(df, f)
  426. comfort_zone_score(df, f)
  427. final_summary(f)
  428. log('=' * 72)
  429. log('分析完成')
  430. log('=' * 72)
  431. sys.stdout = orig_stdout
  432. print(f'\n结果已保存到: {OUTPUT_FILE}')
  433. if __name__ == '__main__':
  434. main()