optimize_parameters.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 趋势质量评估器 - 参数优化测试
  5. 测试不同参数组合的回测表现,寻找最优配置
  6. """
  7. import sys
  8. sys.path.insert(0, '/root/.openclaw/workspace/trend-quality-evaluator')
  9. import numpy as np
  10. import pandas as pd
  11. from trend_quality_evaluator import fetch_stock_data
  12. from dataclasses import dataclass
  13. from typing import List, Tuple
  14. import itertools
  15. import warnings
  16. warnings.filterwarnings('ignore')
  17. print("="*70)
  18. print("趋势质量评估器 - 参数优化测试")
  19. print("="*70)
  20. # 获取数据
  21. df = fetch_stock_data("399673", "2017-01-01", "2026-03-06", "d")
  22. if df is None:
  23. print("数据获取失败")
  24. exit(1)
  25. print(f"\n✓ 数据获取成功: {len(df)}条")
  26. print(f" 日期范围: {df.index[0].date()} ~ {df.index[-1].date()}")
  27. # 定义参数搜索空间
  28. param_grid = {
  29. 'adx_threshold': [20, 25, 30], # ADX阈值
  30. 'adx_weight': [25, 30, 35], # ADX权重
  31. 'ma_slope_threshold': [1.001, 1.002, 1.003], # 均线斜率阈值
  32. 'ma_slope_weight': [20, 25, 30], # 均线斜率权重
  33. 'volatility_threshold': [0.7, 0.8, 0.9], # 波动率比率阈值
  34. 'volatility_weight': [15, 20, 25], # 波动率权重
  35. 'volume_threshold': [1.3, 1.5, 1.8], # 成交量阈值
  36. 'volume_weight': [8, 10, 12], # 成交量权重
  37. }
  38. # 总权重必须是100
  39. @dataclass
  40. class StrategyConfig:
  41. """策略配置"""
  42. adx_threshold: float
  43. adx_weight: int
  44. ma_slope_threshold: float
  45. ma_slope_weight: int
  46. volatility_threshold: float
  47. volatility_weight: int
  48. volume_threshold: float
  49. volume_weight: int
  50. timeframe_weight: int = 15 # 固定15分
  51. @property
  52. def total_weight(self) -> int:
  53. return self.adx_weight + self.ma_slope_weight + self.volatility_weight + self.volume_weight + self.timeframe_weight
  54. @property
  55. def trade_threshold(self) -> int:
  56. return 60 # 固定60分阈值
  57. def calculate_indicators(df: pd.DataFrame) -> pd.DataFrame:
  58. """预计算所有技术指标"""
  59. data = df.copy()
  60. # ADX
  61. high, low, close = data['high'], data['low'], data['close']
  62. plus_dm = high.diff()
  63. minus_dm = low.diff().abs()
  64. plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0)
  65. minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0)
  66. tr1 = high - low
  67. tr2 = (high - close.shift()).abs()
  68. tr3 = (low - close.shift()).abs()
  69. tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
  70. atr = tr.rolling(14).mean()
  71. plus_di = 100 * (plus_dm.rolling(14).mean() / atr)
  72. minus_di = 100 * (minus_dm.rolling(14).mean() / atr)
  73. dx = (abs(plus_di - minus_di) / (plus_di + minus_di + 1e-10)) * 100
  74. data['adx'] = dx.rolling(14).mean()
  75. # MA20斜率
  76. data['ma20'] = data['close'].rolling(20).mean()
  77. data['ma20_slope'] = data['ma20'] / data['ma20'].shift(5)
  78. # ATR比率
  79. data['atr14'] = tr.rolling(14).mean()
  80. data['atr50'] = tr.rolling(50).mean()
  81. data['atr_ratio'] = data['atr14'] / data['atr50']
  82. # 成交量
  83. data['volume_ma20'] = data['volume'].rolling(20).mean()
  84. data['volume_ratio'] = data['volume'] / data['volume_ma20']
  85. # 日线突破
  86. data['price_above_ma20'] = data['close'] > data['ma20']
  87. return data
  88. def evaluate_strategy(data: pd.DataFrame, config: StrategyConfig) -> dict:
  89. """
  90. 评估策略配置
  91. 返回各项绩效指标
  92. """
  93. scores = []
  94. # 从第60天开始(需要足够数据计算MA60)
  95. for i in range(60, len(data)):
  96. row = data.iloc[i]
  97. # 计算各因子得分
  98. # 1. ADX得分
  99. if row['adx'] >= config.adx_threshold:
  100. adx_score = config.adx_weight
  101. else:
  102. adx_score = config.adx_weight * (row['adx'] / config.adx_threshold)
  103. # 2. 均线斜率得分
  104. if row['ma20_slope'] >= config.ma_slope_threshold * 1.5:
  105. ma_score = config.ma_slope_weight
  106. elif row['ma20_slope'] >= config.ma_slope_threshold:
  107. ma_score = config.ma_slope_weight * 0.7
  108. elif row['ma20_slope'] >= 1.0:
  109. ma_score = config.ma_slope_weight * 0.3
  110. else:
  111. ma_score = 0
  112. # 3. 波动率得分
  113. if row['atr_ratio'] <= config.volatility_threshold * 0.75:
  114. vol_score = config.volatility_weight
  115. elif row['atr_ratio'] <= config.volatility_threshold:
  116. vol_score = config.volatility_weight * 0.7
  117. elif row['atr_ratio'] <= 1.0:
  118. vol_score = config.volatility_weight * 0.3
  119. else:
  120. vol_score = 0
  121. # 4. 成交量得分
  122. if row['volume_ratio'] >= config.volume_threshold * 1.3:
  123. vol_score_trade = config.volume_weight
  124. elif row['volume_ratio'] >= config.volume_threshold:
  125. vol_score_trade = config.volume_weight * 0.7
  126. elif row['volume_ratio'] >= 1.0:
  127. vol_score_trade = config.volume_weight * 0.3
  128. else:
  129. vol_score_trade = 0
  130. # 5. 时间框架得分(简化版:价格突破MA20)
  131. tf_score = config.timeframe_weight if row['price_above_ma20'] else 0
  132. total_score = adx_score + ma_score + vol_score + vol_score_trade + tf_score
  133. is_tradeable = total_score >= config.trade_threshold
  134. scores.append({
  135. 'date': data.index[i],
  136. 'close': row['close'],
  137. 'total_score': total_score,
  138. 'is_tradeable': is_tradeable,
  139. 'adx': row['adx'],
  140. 'ma20_slope': row['ma20_slope'],
  141. 'atr_ratio': row['atr_ratio'],
  142. 'volume_ratio': row['volume_ratio']
  143. })
  144. scores_df = pd.DataFrame(scores)
  145. scores_df = scores_df.set_index('date')
  146. # 计算未来收益
  147. scores_df['future_5d_return'] = scores_df['close'].pct_change(5).shift(-5) * 100
  148. scores_df['future_10d_return'] = scores_df['close'].pct_change(10).shift(-10) * 100
  149. scores_df['future_20d_return'] = scores_df['close'].pct_change(20).shift(-20) * 100
  150. # 计算绩效指标
  151. t_mask = scores_df['is_tradeable']
  152. total_days = len(scores_df)
  153. tradeable_days = t_mask.sum()
  154. if tradeable_days == 0:
  155. return None
  156. returns_5d = scores_df[t_mask]['future_5d_return'].dropna()
  157. returns_10d = scores_df[t_mask]['future_10d_return'].dropna()
  158. returns_20d = scores_df[t_mask]['future_20d_return'].dropna()
  159. results = {
  160. 'config': config,
  161. 'total_days': total_days,
  162. 'tradeable_days': tradeable_days,
  163. 'tradeable_pct': tradeable_days / total_days * 100,
  164. 'avg_score': scores_df['total_score'].mean(),
  165. 'return_5d': returns_5d.mean() if len(returns_5d) > 0 else 0,
  166. 'return_10d': returns_10d.mean() if len(returns_10d) > 0 else 0,
  167. 'return_20d': returns_20d.mean() if len(returns_20d) > 0 else 0,
  168. 'win_rate_20d': (returns_20d > 0).mean() * 100 if len(returns_20d) > 0 else 0,
  169. 'sharpe_20d': returns_20d.mean() / returns_20d.std() if len(returns_20d) > 0 and returns_20d.std() > 0 else 0,
  170. 'excess_return_20d': returns_20d.mean() - scores_df[~t_mask]['future_20d_return'].mean() if len(returns_20d) > 0 else 0,
  171. 'scores_df': scores_df
  172. }
  173. return results
  174. # 预计算指标
  175. print("\n预计算技术指标...")
  176. data = calculate_indicators(df)
  177. print("✓ 指标计算完成")
  178. # 生成参数组合(只测试权重和为100的组合)
  179. print("\n生成参数组合...")
  180. valid_configs = []
  181. for adx_t, adx_w, ma_t, ma_w, vol_t, vol_w, volu_t, volu_w in itertools.product(
  182. param_grid['adx_threshold'],
  183. param_grid['adx_weight'],
  184. param_grid['ma_slope_threshold'],
  185. param_grid['ma_slope_weight'],
  186. param_grid['volatility_threshold'],
  187. param_grid['volatility_weight'],
  188. param_grid['volume_threshold'],
  189. param_grid['volume_weight']
  190. ):
  191. config = StrategyConfig(
  192. adx_threshold=adx_t,
  193. adx_weight=adx_w,
  194. ma_slope_threshold=ma_t,
  195. ma_slope_weight=ma_w,
  196. volatility_threshold=vol_t,
  197. volatility_weight=vol_w,
  198. volume_threshold=volu_t,
  199. volume_weight=volu_w
  200. )
  201. if config.total_weight == 100: # 只保留权重和为100的组合
  202. valid_configs.append(config)
  203. print(f"✓ 有效参数组合: {len(valid_configs)}个")
  204. # 运行回测
  205. print(f"\n开始回测 ({len(valid_configs)}个组合)...")
  206. print("-"*70)
  207. all_results = []
  208. for i, config in enumerate(valid_configs):
  209. result = evaluate_strategy(data, config)
  210. if result:
  211. all_results.append(result)
  212. if (i + 1) % 50 == 0:
  213. print(f" 进度: {i+1}/{len(valid_configs)} ({(i+1)/len(valid_configs)*100:.1f}%)")
  214. print(f"\n✓ 回测完成: {len(all_results)}个有效结果")
  215. # 排序并选择最佳配置
  216. print("\n" + "="*70)
  217. print("参数优化结果")
  218. print("="*70)
  219. # 按不同目标排序
  220. sort_by = [
  221. ('20日收益', lambda r: r['return_20d'], True),
  222. ('超额收益', lambda r: r['excess_return_20d'], True),
  223. ('胜率', lambda r: r['win_rate_20d'], True),
  224. ('夏普比率', lambda r: r['sharpe_20d'], True),
  225. ]
  226. best_configs = {}
  227. for name, key_func, reverse in sort_by:
  228. sorted_results = sorted(all_results, key=key_func, reverse=reverse)
  229. best_configs[name] = sorted_results[0]
  230. print(f"\n【最佳配置 - {name}】")
  231. best = sorted_results[0]
  232. print(f" ADX阈值: {best['config'].adx_threshold}, 权重: {best['config'].adx_weight}")
  233. print(f" MA斜率阈值: {best['config'].ma_slope_threshold}, 权重: {best['config'].ma_slope_weight}")
  234. print(f" 波动率阈值: {best['config'].volatility_threshold}, 权重: {best['config'].volatility_weight}")
  235. print(f" 成交量阈值: {best['config'].volume_threshold}, 权重: {best['config'].volume_weight}")
  236. print(f" 时间框架权重: {best['config'].timeframe_weight}")
  237. print(f"\n 绩效指标:")
  238. print(f" 可交易比例: {best['tradeable_pct']:.1f}%")
  239. print(f" 5日收益: {best['return_5d']:+.2f}%")
  240. print(f" 10日收益: {best['return_10d']:+.2f}%")
  241. print(f" 20日收益: {best['return_20d']:+.2f}%")
  242. print(f" 超额收益: {best['excess_return_20d']:+.2f}%")
  243. print(f" 胜率(20日): {best['win_rate_20d']:.1f}%")
  244. print(f" 夏普比率: {best['sharpe_20d']:.2f}")
  245. # 综合评分最佳(平衡收益、胜率、夏普)
  246. print(f"\n【综合最佳配置】")
  247. print("-"*50)
  248. # 计算综合得分(标准化后加权)
  249. max_return = max(r['return_20d'] for r in all_results)
  250. max_winrate = max(r['win_rate_20d'] for r in all_results)
  251. max_sharpe = max(r['sharpe_20d'] for r in all_results)
  252. max_excess = max(r['excess_return_20d'] for r in all_results)
  253. for r in all_results:
  254. # 综合得分 = 收益*0.3 + 超额*0.3 + 胜率*0.2 + 夏普*0.2
  255. r['composite_score'] = (
  256. (r['return_20d'] / max_return if max_return > 0 else 0) * 0.3 +
  257. (r['excess_return_20d'] / max_excess if max_excess > 0 else 0) * 0.3 +
  258. (r['win_rate_20d'] / max_winrate if max_winrate > 0 else 0) * 0.2 +
  259. (r['sharpe_20d'] / max_sharpe if max_sharpe > 0 else 0) * 0.2
  260. ) * 100
  261. best_composite = max(all_results, key=lambda r: r['composite_score'])
  262. print(f" ADX阈值: {best_composite['config'].adx_threshold}, 权重: {best_composite['config'].adx_weight}")
  263. print(f" MA斜率阈值: {best_composite['config'].ma_slope_threshold}, 权重: {best_composite['config'].ma_slope_weight}")
  264. print(f" 波动率阈值: {best_composite['config'].volatility_threshold}, 权重: {best_composite['config'].volatility_weight}")
  265. print(f" 成交量阈值: {best_composite['config'].volume_threshold}, 权重: {best_composite['config'].volume_weight}")
  266. print(f"\n 绩效指标:")
  267. print(f" 可交易比例: {best_composite['tradeable_pct']:.1f}%")
  268. print(f" 5日收益: {best_composite['return_5d']:+.2f}%")
  269. print(f" 10日收益: {best_composite['return_10d']:+.2f}%")
  270. print(f" 20日收益: {best_composite['return_20d']:+.2f}%")
  271. print(f" 超额收益: {best_composite['excess_return_20d']:+.2f}%")
  272. print(f" 胜率(20日): {best_composite['win_rate_20d']:.1f}%")
  273. print(f" 夏普比率: {best_composite['sharpe_20d']:.2f}")
  274. print(f" 综合得分: {best_composite['composite_score']:.1f}")
  275. # 保存结果
  276. print("\n" + "="*70)
  277. print("保存结果...")
  278. results_summary = []
  279. for r in all_results:
  280. results_summary.append({
  281. 'adx_threshold': r['config'].adx_threshold,
  282. 'adx_weight': r['config'].adx_weight,
  283. 'ma_slope_threshold': r['config'].ma_slope_threshold,
  284. 'ma_slope_weight': r['config'].ma_slope_weight,
  285. 'volatility_threshold': r['config'].volatility_threshold,
  286. 'volatility_weight': r['config'].volatility_weight,
  287. 'volume_threshold': r['config'].volume_threshold,
  288. 'volume_weight': r['config'].volume_weight,
  289. 'tradeable_pct': r['tradeable_pct'],
  290. 'return_5d': r['return_5d'],
  291. 'return_10d': r['return_10d'],
  292. 'return_20d': r['return_20d'],
  293. 'excess_return_20d': r['excess_return_20d'],
  294. 'win_rate_20d': r['win_rate_20d'],
  295. 'sharpe_20d': r['sharpe_20d'],
  296. 'composite_score': r['composite_score']
  297. })
  298. results_df = pd.DataFrame(results_summary)
  299. results_df = results_df.sort_values('composite_score', ascending=False)
  300. results_df.to_csv('/root/.openclaw/workspace/trend-quality-evaluator/optimization_results.csv', index=False)
  301. # 保存最佳配置详情
  302. best_composite['scores_df'].to_csv('/root/.openclaw/workspace/trend-quality-evaluator/best_config_backtest.csv')
  303. print("✓ 优化结果已保存: optimization_results.csv")
  304. print("✓ 最佳配置回测详情: best_config_backtest.csv")
  305. print("\n" + "="*70)
  306. print("参数优化测试完成!")
  307. print("="*70)