strategy-optimizer
系统性地通过回测优化量化交易策略。当用户需要优化、改进或调优策略性能指标(如夏普比率)时调用此技能。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
系统性地通过回测优化量化交易策略。当用户需要优化、改进或调优策略性能指标(如夏普比率)时调用此技能。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | strategy-optimizer |
| description | 系统性地通过回测优化量化交易策略。当用户需要优化、改进或调优策略性能指标(如夏普比率)时调用此技能。 |
在 qmt_backtrader 框架中系统性优化量化交易策略。本技能提供结构化的工作流程:提出优化建议、独立实施、逐一回测、硬逻辑与过度拟合审查、仅保留有效且稳健的改进。
Python 运行环境(通过 PowerShell 动态获取系统 Python 环境):
$pythonPath = "$env:USERPROFILE\AppData\Local\Programs\Python\Python312\python.exe"
if (-not (Test-Path $pythonPath)) {
$pythonPath = (Get-ChildItem "$env:USERPROFILE\AppData\Local\Programs\Python\Python*" -Directory |
Sort-Object Name -Descending | Select-Object -First 1).FullName + "\python.exe"
}
& "$pythonPath" main.py --mode backtest ...sys.executable 或上述脚本获取后调用e:\jupyter notebook\automatic\qmt_backtrader当用户出现以下情况时调用此技能:
所有优化回测必须严格遵循以下时间区间划分:
| 区间 | 起止日期 | 用途 | 说明 |
|---|---|---|---|
| 测试集(样本内) | 2020-04-28 ~ 2024-04-28 | 参数优化、策略开发 | 在此区间上搜索最优参数、验证优化效果 |
| 验证集(样本外) | 2024-04-28 ~ 2026-04-28 | 过度拟合检测、最终验证 | 仅用于验证优化结果的稳健性,禁止在此区间上调参 |
关键规则:
2020-04-28 ~ 2024-04-282024-04-28 ~ 2026-04-28start_date 早于 2020-04-28,仍以 2020-04-28 作为测试集起点回测模板中的默认参数更新:
# StockSelectionStrategy 模板
def run_backtest_with_params(strategy_name=STRATEGY_NAME, extra_params=None, label='test',
pool='中证1000', start_date='2020-04-28', end_date='2024-04-28'):
# StrategyLogic 模板
def run_backtest_with_params(strategy_name=STRATEGY_NAME, extra_params=None, label='test',
start_date='2020-04-28', end_date='2024-04-28'):
样本外验证函数更新(使用固定验证集区间,不再使用 split_ratio):
# 固定的测试集/验证集时间边界
TRAIN_START = '2020-04-28'
TRAIN_END = '2024-04-28' # 测试集结束 = 验证集开始
VALID_START = '2024-04-28' # 验证集开始
VALID_END = '2026-04-28' # 验证集结束
def run_out_of_sample_test(strategy_name, extra_params, label):
"""使用固定验证集进行样本外测试"""
# 测试集(样本内)回测
in_sample = run_backtest_with_params(
strategy_name=strategy_name, extra_params=extra_params,
label=f'{label}_is', start_date=TRAIN_START, end_date=TRAIN_END)
# 验证集(样本外)回测
out_sample = run_backtest_with_params(
strategy_name=strategy_name, extra_params=extra_params,
label=f'{label}_oos', start_date=VALID_START, end_date=VALID_END)
# 基线对比
baseline_is = run_backtest_with_params(
strategy_name=strategy_name, extra_params=None,
label='baseline_is', start_date=TRAIN_START, end_date=TRAIN_END)
baseline_oos = run_backtest_with_params(
strategy_name=strategy_name, extra_params=None,
label='baseline_oos', start_date=VALID_START, end_date=VALID_END)
is_improvement = (in_sample.get('sharpe_ratio', 0) - baseline_is.get('sharpe_ratio', 0)) / abs(baseline_is.get('sharpe_ratio', 1)) * 100
oos_improvement = (out_sample.get('sharpe_ratio', 0) - baseline_oos.get('sharpe_ratio', 0)) / abs(baseline_oos.get('sharpe_ratio', 1)) * 100
return {
'train_period': f'{TRAIN_START} ~ {TRAIN_END}',
'valid_period': f'{VALID_START} ~ {VALID_END}',
'in_sample_sharpe': in_sample.get('sharpe_ratio', 0),
'out_sample_sharpe': out_sample.get('sharpe_ratio', 0),
'baseline_is_sharpe': baseline_is.get('sharpe_ratio', 0),
'baseline_oos_sharpe': baseline_oos.get('sharpe_ratio', 0),
'is_improvement_pct': is_improvement,
'oos_improvement_pct': oos_improvement,
'decay_ratio': oos_improvement / is_improvement if is_improvement != 0 else 0,
}
readme.md 文档StockSelectionStrategy 还是 StrategyLogic,两者回测模板不同提出10项具体优化建议,每项必须包含:
每项优化的实施步骤:
params 元组中添加参数,默认值设为禁用状态(如 None、False、0)$pythonPath = "$env:USERPROFILE\AppData\Local\Programs\Python\Python312\python.exe"
& "$pythonPath" main.py --mode backtest --strategy <策略名> --period 1d --pool <股票池> --start <起始日期> --end <结束日期> --debug
在策略目录下的 optimization/ 目录中创建 run_optimization.py 脚本(确保脚本和结果文件都在策略目录内):
import os
import sys
import json
import datetime
import traceback
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, PROJECT_ROOT)
os.environ['QMT_LOG_LEVEL'] = 'WARNING'
os.environ['QMT_CACHE_DIR'] = os.path.join(PROJECT_ROOT, '.cache')
from api.backtest_api import BacktestAPI
from core.stock_selection import StockSelectionStrategy
from strategies import get_strategy, get_strategy_default_kwargs, get_strategy_backtest_config, get_strategy_dir
from core.data.index_constituent import IndexConstituentManager
STRATEGY_NAME = '<策略注册名>'
STRATEGY_DIR = get_strategy_dir(STRATEGY_NAME)
# 所有优化相关文件都放在策略目录下的 optimization/ 子目录中
OPTIMIZATION_DIR = os.path.join(STRATEGY_DIR, 'optimization')
RESULTS_DIR = os.path.join(OPTIMIZATION_DIR, 'optimization_results')
os.makedirs(RESULTS_DIR, exist_ok=True)
def run_backtest_with_params(strategy_name=STRATEGY_NAME, extra_params=None, label='test',
pool='中证1000', start_date='2020-04-28', end_date='2024-04-28'):
strategy_class = get_strategy(strategy_name)
default_kwargs = get_strategy_default_kwargs(strategy_name)
backtest_config = get_strategy_backtest_config(strategy_name)
config = dict(backtest_config)
config['period'] = '1d'
config['start_date'] = start_date
config['end_date'] = end_date
benchmark = IndexConstituentManager.SECTOR_TO_INDEX.get(pool, '000300.SH')
config.setdefault('benchmark', benchmark)
merged_kwargs = dict(default_kwargs)
if extra_params:
merged_kwargs.update(extra_params)
api = BacktestAPI()
api.set_ai_mode(True)
api.set_no_record(True)
api.configure(**config)
api.load_financial_data(sector=pool)
api.add_stock_selection_strategy(strategy_class, **merged_kwargs)
results = api.run()
result = api.get_result()
metrics = {}
if result:
sr = result.sharpe_ratio()
dd = result.max_drawdown()
acc = result.account
metrics['initial_capital'] = acc.initial_capital
metrics['final_value'] = acc.dynamic_rights
metrics['total_return_pct'] = acc.rate * 100
metrics['sharpe_ratio'] = sr
metrics['max_drawdown_pct'] = dd * 100
if result.df is not None and len(result.df) > 0:
days = len(result.df)
years = days / 252
annual_ret = (1 + acc.rate) ** (1 / years) - 1 if years > 0 else 0
if isinstance(annual_ret, complex):
annual_ret = annual_ret.real
metrics['annual_return_pct'] = float(annual_ret) * 100
metrics['trading_days'] = days
metrics['fee'] = getattr(result, 'total_fee', 0)
metrics['turnover'] = getattr(result, 'turnover', 0)
metrics['label'] = label
metrics['extra_params'] = extra_params
metrics['timestamp'] = datetime.datetime.now().isoformat()
else:
metrics['label'] = label
metrics['error'] = 'No result'
result_file = os.path.join(RESULTS_DIR, f'{label}.json')
with open(result_file, 'w', encoding='utf-8') as f:
json.dump(metrics, f, indent=2, ensure_ascii=False)
return metrics
StrategyLogic 基类策略(如 ETF 轮动)不使用 load_financial_data 和 add_stock_selection_strategy,改用 add_strategy:
import os
import sys
import json
import datetime
import traceback
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
sys.path.insert(0, PROJECT_ROOT)
os.environ['QMT_LOG_LEVEL'] = 'WARNING'
os.environ['QMT_CACHE_DIR'] = os.path.join(PROJECT_ROOT, '.cache')
from api.backtest_api import BacktestAPI
from strategies import get_strategy, get_strategy_default_kwargs, get_strategy_backtest_config, get_strategy_dir
STRATEGY_NAME = '<策略注册名>'
STRATEGY_DIR = get_strategy_dir(STRATEGY_NAME)
# 所有优化相关文件都放在策略目录下的 optimization/ 子目录中
OPTIMIZATION_DIR = os.path.join(STRATEGY_DIR, 'optimization')
RESULTS_DIR = os.path.join(OPTIMIZATION_DIR, 'optimization_results')
os.makedirs(RESULTS_DIR, exist_ok=True)
def run_backtest_with_params(strategy_name=STRATEGY_NAME, extra_params=None, label='test',
start_date='2020-04-28', end_date='2024-04-28'):
strategy_class = get_strategy(strategy_name)
default_kwargs = get_strategy_default_kwargs(strategy_name)
backtest_config = get_strategy_backtest_config(strategy_name)
config = dict(backtest_config)
config['period'] = '1d'
config['start_date'] = start_date
config['end_date'] = end_date
config.setdefault('benchmark', '000300.SH')
merged_kwargs = dict(default_kwargs)
if extra_params:
merged_kwargs.update(extra_params)
api = BacktestAPI()
api.set_ai_mode(True)
api.set_no_record(True)
api.configure(**config)
api.add_strategy(strategy_class, **merged_kwargs)
results = api.run()
result = api.get_result()
metrics = {}
if result:
sr = result.sharpe_ratio()
dd = result.max_drawdown()
acc = result.account
metrics['initial_capital'] = acc.initial_capital
metrics['final_value'] = acc.dynamic_rights
metrics['total_return_pct'] = acc.rate * 100
metrics['sharpe_ratio'] = sr
metrics['max_drawdown_pct'] = dd * 100
if result.df is not None and len(result.df) > 0:
days = len(result.df)
years = days / 252
annual_ret = (1 + acc.rate) ** (1 / years) - 1 if years > 0 else 0
if isinstance(annual_ret, complex):
annual_ret = annual_ret.real
metrics['annual_return_pct'] = float(annual_ret) * 100
metrics['trading_days'] = days
metrics['fee'] = getattr(result, 'total_fee', 0)
metrics['turnover'] = getattr(result, 'turnover', 0)
metrics['label'] = label
metrics['extra_params'] = extra_params
metrics['timestamp'] = datetime.datetime.now().isoformat()
else:
metrics['label'] = label
metrics['error'] = 'No result'
result_file = os.path.join(RESULTS_DIR, f'{label}.json')
with open(result_file, 'w', encoding='utf-8') as f:
json.dump(metrics, f, indent=2, ensure_ascii=False)
return metrics
关键差异:
api.load_financial_data(sector=pool) + api.add_stock_selection_strategy()api.add_strategy() (无 load_financial_data)dirname(因策略在 strategies_for_vip/ 子目录下)annual_ret 可能为 complex 类型的问题(if isinstance(annual_ret, complex): annual_ret = annual_ret.real)api.set_no_record(True) 避免优化回测写入正式结果目录保留标准:夏普比率相对基线提升 >= 5%
| 结果 | 处理方式 |
|---|---|
| 夏普提升 >= 5% | 保留并整合到策略中 |
| 0% <= 提升 < 5% | 放弃(收益不足) |
| 负向提升 | 放弃并记录失败原因 |
对阶段四中夏普提升 >= 5% 的有效优化,逐一进行硬逻辑强度和过度拟合风险审查。任何一项审查不通过的优化,必须降级为"有条件有效"或直接放弃。
评估优化的逻辑是否具有坚实的因果基础,而非仅仅是统计巧合。
| 检查维度 | 评估标准 | 不通过判定 |
|---|---|---|
| 逻辑因果链 | 优化是否有清晰的"条件→动作→结果"因果链?能否用一句话解释为什么这个优化应该有效? | 无法给出合理解释,或解释依赖"历史数据就是这样" |
| 经济合理性 | 优化效果能否被金融理论解释(风险溢价、市场微观结构、行为金融等)? | 改进仅在统计上显著,但无经济学逻辑支撑 |
| 逻辑独立性 | 优化是否提供了新的信息维度,而非已有逻辑的变相重复? | 与已有参数高度相关(如同时有"5日动量"和"10日动量") |
| 极端场景稳健性 | 逻辑在极端市场(暴涨暴跌、流动性枯竭)下是否仍然成立? | 仅在常态市场有效,极端场景下逻辑失效或反向 |
| 可解释的交易行为 | 优化导致的交易行为变化是否可解释、可预期? | 优化后产生了无法解释的频繁交易或异常持仓 |
硬逻辑强度评级:
| 评级 | 条件 | 处理方式 |
|---|---|---|
| A(强) | 全部5项通过 | 正常进入组合优化阶段 |
| B(中) | 通过4项,仅极端场景或可解释性稍弱 | 进入组合优化,但在报告中标注风险 |
| C(弱) | 通过3项及以下 | 降级为"有条件有效",需补充样本外验证才能进入组合 |
通过实证方法检验优化是否对历史数据过度拟合。
检测方法一:样本外验证(必做)
使用固定的验证集区间(2024-04-28 ~ 2026-04-28)检验优化在样本外是否仍然有效。测试集用于参数优化,验证集仅用于最终检验,禁止在验证集上调参:
# 固定的测试集/验证集时间边界
TRAIN_START = '2020-04-28'
TRAIN_END = '2024-04-28' # 测试集结束 = 验证集开始
VALID_START = '2024-04-28' # 验证集开始
VALID_END = '2026-04-28' # 验证集结束
def run_out_of_sample_test(strategy_name, extra_params, label):
"""使用固定验证集进行样本外测试"""
# 测试集(样本内)回测
in_sample = run_backtest_with_params(
strategy_name=strategy_name, extra_params=extra_params,
label=f'{label}_is', start_date=TRAIN_START, end_date=TRAIN_END)
# 验证集(样本外)回测
out_sample = run_backtest_with_params(
strategy_name=strategy_name, extra_params=extra_params,
label=f'{label}_oos', start_date=VALID_START, end_date=VALID_END)
# 基线对比
baseline_is = run_backtest_with_params(
strategy_name=strategy_name, extra_params=None,
label='baseline_is', start_date=TRAIN_START, end_date=TRAIN_END)
baseline_oos = run_backtest_with_params(
strategy_name=strategy_name, extra_params=None,
label='baseline_oos', start_date=VALID_START, end_date=VALID_END)
is_improvement = (in_sample.get('sharpe_ratio', 0) - baseline_is.get('sharpe_ratio', 0)) / abs(baseline_is.get('sharpe_ratio', 1)) * 100
oos_improvement = (out_sample.get('sharpe_ratio', 0) - baseline_oos.get('sharpe_ratio', 0)) / abs(baseline_oos.get('sharpe_ratio', 1)) * 100
return {
'train_period': f'{TRAIN_START} ~ {TRAIN_END}',
'valid_period': f'{VALID_START} ~ {VALID_END}',
'in_sample_sharpe': in_sample.get('sharpe_ratio', 0),
'out_sample_sharpe': out_sample.get('sharpe_ratio', 0),
'baseline_is_sharpe': baseline_is.get('sharpe_ratio', 0),
'baseline_oos_sharpe': baseline_oos.get('sharpe_ratio', 0),
'is_improvement_pct': is_improvement,
'oos_improvement_pct': oos_improvement,
'decay_ratio': oos_improvement / is_improvement if is_improvement != 0 else 0,
}
样本外判定标准:
| 衰减比(OOS/IS) | 判定 | 处理方式 |
|---|---|---|
| >= 0.5 | 样本外仍保持至少50%的改进 | 通过,优化稳健 |
| 0.2 ~ 0.5 | 样本外效果大幅衰减 | 有条件通过,需降低参数权重或收紧阈值 |
| < 0.2 或为负 | 样本外几乎无效或反向 | 不通过,高度疑似过度拟合,必须放弃 |
检测方法二:参数敏感性分析(必做)
对优化的核心参数进行 ±10%、±20% 的扰动测试,检验优化效果是否对参数值高度敏感:
def run_parameter_sensitivity_test(strategy_name, param_name, param_value,
label, perturbations=[-0.2, -0.1, 0.1, 0.2]):
results = {}
base_result = run_backtest_with_params(
strategy_name=strategy_name,
extra_params={param_name: param_value},
label=f'{label}_base')
for delta in perturbations:
perturbed_value = param_value * (1 + delta)
if isinstance(param_value, int):
perturbed_value = int(round(perturbed_value))
if perturbed_value == 0:
perturbed_value = 1
result = run_backtest_with_params(
strategy_name=strategy_name,
extra_params={param_name: perturbed_value},
label=f'{label}_perturb_{delta:+.0%}')
results[f'perturb_{delta:+.0%}'] = {
'param_value': perturbed_value,
'sharpe_ratio': result.get('sharpe_ratio', 0),
}
base_sharpe = base_result.get('sharpe_ratio', 0)
sharpe_values = [r['sharpe_ratio'] for r in results.values()]
sharpe_std = (sum((s - base_sharpe) ** 2 for s in sharpe_values) / len(sharpe_values)) ** 0.5
sharpe_range = max(sharpe_values) - min(sharpe_values)
return {
'base_sharpe': base_sharpe,
'base_param': param_value,
'perturbation_results': results,
'sharpe_std': sharpe_std,
'sharpe_range': sharpe_range,
'sensitivity_ratio': sharpe_range / abs(base_sharpe) if base_sharpe != 0 else float('inf'),
}
参数敏感性判定标准:
| 敏感度比率(range/|base|) | 判定 | 处理方式 | |--------------------------|------|---------| | < 0.3 | 参数鲁棒,优化可信 | 通过 | | 0.3 ~ 0.6 | 参数较敏感 | 有条件通过,建议选择参数区间的中间值而非最优值 | | > 0.6 | 参数高度敏感,优化可能仅对特定值有效 | 不通过,高度疑似过度拟合 |
检测方法三:时间分段稳定性(必做)
将回测区间按年分段,检验优化在各年度是否一致有效:
def run_temporal_stability_test(strategy_name, extra_params, label,
full_start, full_end):
from datetime import datetime
start_year = datetime.strptime(full_start, '%Y-%m-%d').year
end_year = datetime.strptime(full_end, '%Y-%m-%d').year
yearly_results = []
for year in range(start_year, end_year + 1):
year_start = f'{year}-01-01'
year_end = f'{year}-12-31'
if year_start < full_start:
year_start = full_start
if year_end > full_end:
year_end = full_end
opt_result = run_backtest_with_params(
strategy_name=strategy_name, extra_params=extra_params,
label=f'{label}_{year}', start_date=year_start, end_date=year_end)
base_result = run_backtest_with_params(
strategy_name=strategy_name, extra_params=None,
label=f'baseline_{year}', start_date=year_start, end_date=year_end)
opt_sharpe = opt_result.get('sharpe_ratio', 0)
base_sharpe = base_result.get('sharpe_ratio', 0)
improvement = opt_sharpe - base_sharpe
yearly_results.append({
'year': year,
'opt_sharpe': opt_sharpe,
'base_sharpe': base_sharpe,
'improvement': improvement,
'is_positive': improvement > 0,
})
positive_years = sum(1 for r in yearly_results if r['is_positive'])
total_years = len(yearly_results)
consistency_ratio = positive_years / total_years if total_years > 0 else 0
return {
'yearly_results': yearly_results,
'positive_years': positive_years,
'total_years': total_years,
'consistency_ratio': consistency_ratio,
}
时间稳定性判定标准:
| 一致性比率(正改进年数/总年数) | 判定 | 处理方式 |
|---|---|---|
| >= 0.7 | 优化效果时间稳定 | 通过 |
| 0.5 ~ 0.7 | 部分年份无效 | 有条件通过,需分析无效年份的市场特征 |
| < 0.5 | 多数年份无效,改进集中在少数年份 | 不通过,优化可能仅对特定行情有效 |
对每项有效优化,汇总三项检测结果,给出最终审查结论:
| 硬逻辑评级 | 样本外衰减比 | 参数敏感度 | 时间稳定性 | 最终结论 |
|---|---|---|---|---|
| A | >= 0.5 | < 0.3 | >= 0.7 | ✅ 强力通过,优先组合 |
| A/B | >= 0.2 | < 0.6 | >= 0.5 | ⚠️ 有条件通过,组合时降低权重 |
| 任意 | < 0.2 或 敏感度>0.6 或 稳定性<0.5 | - | - | ❌ 不通过,放弃该优化 |
重要:即使优化在阶段四中夏普提升 >= 5%,如果在本阶段审查不通过,也必须放弃。硬逻辑和过度拟合审查是比夏普比率更根本的质量门槛。
在组合优化前,必须分析有效优化之间是否存在逻辑冲突:
| 冲突类型 | 示例 | 原因 |
|---|---|---|
| 换仓限制冲突 | 最小持仓天数 + 波动率过滤 | 波动率过滤需及时调仓,最小持仓天数阻止调仓 |
| 止损与空仓冲突 | 止损 + 轮动空仓机制 | 轮动策略已有空仓机制,止损造成双重卖出 |
| 过滤叠加冲突 | 多重动量确认 | 过多过滤条件消除有效信号 |
冲突检测方法:分析两个优化的触发条件和执行动作是否存在互斥场景。如果优化A要求"延迟行动"而优化B要求"立即行动",则存在冲突。
readme.md:仅反映保留的优化内容optimization_history.json(详见下方"优化历史记录")每次优化完成后,必须将完整的优化过程和结果追加到 optimization/optimization_history.json 文件中。该文件记录了策略的所有优化历史,便于追溯和对比不同轮次的优化效果。
记录格式:
import json
import datetime
def save_optimization_history(strategy_dir, optimization_record):
"""将本次优化记录追加到历史文件中"""
history_file = os.path.join(strategy_dir, 'optimization', 'optimization_history.json')
# 读取已有历史
if os.path.exists(history_file):
with open(history_file, 'r', encoding='utf-8') as f:
history = json.load(f)
else:
history = {'optimizations': []}
# 追加新记录
history['optimizations'].append(optimization_record)
# 写回文件
with open(history_file, 'w', encoding='utf-8') as f:
json.dump(history, f, indent=2, ensure_ascii=False)
每条优化记录必须包含以下字段:
optimization_record = {
# 基本信息
'round_id': 'round_1', # 优化轮次标识
'timestamp': datetime.datetime.now().isoformat(), # 优化完成时间
'strategy_name': 'etf_rotation', # 策略注册名
'train_period': '2020-04-28 ~ 2024-04-28', # 测试集区间
'valid_period': '2024-04-28 ~ 2026-04-28', # 验证集区间
# 基线指标
'baseline': {
'sharpe_ratio': 1.38,
'total_return_pct': 499.27,
'max_drawdown_pct': -18.84,
'annual_return_pct': 35.0,
},
# 最终优化后指标(测试集)
'optimized': {
'sharpe_ratio': 2.04,
'total_return_pct': 589.6,
'max_drawdown_pct': -10.12,
'annual_return_pct': 39.7,
},
# 验证集指标
'validation': {
'sharpe_ratio': 2.31,
'total_return_pct': 120.5,
'max_drawdown_pct': -8.5,
'decay_ratio': 0.95, # 验证集改进/测试集改进
},
# 所有优化方案结果摘要
'optimization_results': [
{
'label': 'opt03_volatility_filter',
'name': '波动率过滤 (vol=0.03)',
'params': {'max_volatility': 0.03},
'sharpe_ratio': 1.65,
'sharpe_improvement_pct': 19.6,
'verdict': '有效',
},
{
'label': 'opt21_expand_hk_tech',
'name': '扩展: 恒生科技ETF',
'params': {'extra_etf_codes': {'恒生科技': '513130.SH'}},
'sharpe_ratio': 1.15,
'sharpe_improvement_pct': -16.7,
'verdict': '无效',
},
# ... 所有优化方案
],
# 硬逻辑与过度拟合审查结果
'review_results': [
{
'label': 'opt03_volatility_filter',
'logic_rating': 'A',
'decay_ratio': 0.85,
'sensitivity_ratio': 0.15,
'temporal_consistency': 0.80,
'conclusion': '通过',
},
# ... 所有有效优化的审查结果
],
# 最终采纳的参数变更
'adopted_changes': {
'max_volatility': {'old': 0.03, 'new': 0.015, 'reason': '降低波动率阈值,过滤更多高波动标的'},
},
# 被放弃的优化及原因
'rejected_changes': [
{'label': 'opt21_expand_hk_tech', 'reason': '扩展标的池引入高波动ETF,夏普下降16.7%'},
{'label': 'opt24_abs_momentum_benchmark', 'reason': '绝对动量基准过于严格,错过跨市场机会'},
],
# 报告文件路径
'report_path': 'optimization/optimization_report.html',
# 经验总结
'lessons_learned': [
'波动率过滤是最有效的优化方向',
'扩展标的池反而有害,高波动ETF拖累策略',
'简单阈值优于复杂机制',
],
}
优化历史文件的使用场景:
在策略目录下的 optimization/ 目录中创建 generate_report.py 脚本,生成包含 Chart.js 交互式图表的 HTML 报告(确保报告文件也在策略目录内)。
报告应包含以下8个章节:
import json
import os
from strategies import get_strategy_dir
STRATEGY_NAME = '<策略注册名>'
STRATEGY_DIR = get_strategy_dir(STRATEGY_NAME)
# 所有优化相关文件都放在策略目录下的 optimization/ 子目录中
OPTIMIZATION_DIR = os.path.join(STRATEGY_DIR, 'optimization')
RESULTS_DIR = os.path.join(OPTIMIZATION_DIR, 'optimization_results')
OUTPUT_FILE = os.path.join(OPTIMIZATION_DIR, 'optimization_report.html')
ALL_RESULTS = [
('baseline', '基线策略', None),
('opt01_xxx', '优化1名称', {'param1': value1}),
# ... 按实际优化方案填写
]
def load_result(label):
filepath = os.path.join(RESULTS_DIR, f'{label}.json')
if os.path.exists(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
return None
def generate_report():
results = []
for label, name, params in ALL_RESULTS:
data = load_result(label)
if data:
results.append({
'label': label,
'name': name,
'params': params,
'sharpe': data.get('sharpe_ratio', 0),
'total_return': data.get('total_return_pct', 0),
'annual_return': data.get('annual_return_pct', 0),
'max_drawdown': data.get('max_drawdown_pct', 0),
'turnover': data.get('turnover', 0),
'final_value': data.get('final_value', 0),
})
baseline = results[0] if results else None
if not baseline:
print("No baseline data found!")
return
single_opts = results[1:N] # N = 单项优化数量 + 1
combined_opts = results[N:]
# 预计算所有图表数据(避免在 HTML 模板中做复杂计算)
labels_single = [r['name'] for r in single_opts]
sharpe_single = [r['sharpe'] for r in single_opts]
return_single = [r['total_return'] for r in single_opts]
sharpe_improvement_single = [(s - baseline['sharpe']) / baseline['sharpe'] * 100 for s in sharpe_single]
# 颜色编码:绿色=有效(夏普提升>=5%)、红色=无效
single_sharpe_colors = ['rgba(52,211,153,0.7)' if s >= baseline['sharpe'] * 1.05 else 'rgba(248,113,113,0.5)' for s in sharpe_single]
single_improve_colors = ['rgba(52,211,153,0.7)' if x >= 5 else 'rgba(248,113,113,0.5)' for x in sharpe_improvement_single]
# 生成数据表行
rows_single = ""
for r in single_opts:
sharpe_delta = (r['sharpe'] - baseline['sharpe']) / baseline['sharpe'] * 100
is_effective = sharpe_delta >= 5
badge = '<span class="badge effective-badge">有效</span>' if is_effective else '<span class="badge ineffective-badge">无效</span>'
param_str = ", ".join([f"{k}={v}" for k, v in (r['params'] or {}).items()]) if r['params'] else "-"
rows_single += f'<tr class="{"effective" if is_effective else ""}">'
rows_single += f'<td>{badge} {r["name"]}</td><td>{param_str}</td>'
rows_single += f'<td>{r["sharpe"]:.4f}</td><td class="{"up" if sharpe_delta > 0 else "down"}">{sharpe_delta:+.1f}%</td>'
rows_single += f'<td>{r["total_return"]:.2f}%</td><td>{r["max_drawdown"]:.2f}%</td></tr>'
# HTML 模板 - 使用 Chart.js CDN
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>策略优化分析报告</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; }}
.container {{ max-width: 1200px; margin: 0 auto; padding: 40px 20px; }}
h1 {{ font-size: 2.2em; text-align: center; background: linear-gradient(135deg, #60a5fa, #a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
h2 {{ font-size: 1.4em; color: #60a5fa; margin: 40px 0 20px; border-bottom: 1px solid #1e293b; padding-bottom: 10px; }}
.summary-cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin: 30px 0; }}
.card {{ background: #1e293b; border-radius: 12px; padding: 24px; border: 1px solid #334155; }}
.card-label {{ font-size: 0.85em; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }}
.card-value {{ font-size: 2em; font-weight: 700; }}
.up {{ color: #34d399; }}
.down {{ color: #f87171; }}
.chart-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin: 24px 0; }}
.chart-box {{ background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }}
.chart-box.full {{ grid-column: 1 / -1; }}
table {{ width: 100%; border-collapse: collapse; margin: 16px 0; background: #1e293b; border-radius: 12px; overflow: hidden; }}
th {{ background: #0f172a; color: #94a3b8; font-weight: 600; font-size: 0.85em; text-transform: uppercase; padding: 14px 16px; text-align: left; }}
td {{ padding: 12px 16px; border-top: 1px solid #334155; font-size: 0.92em; }}
tr.effective {{ background: rgba(52, 211, 153, 0.05); }}
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; font-weight: 600; margin-right: 6px; }}
.effective-badge {{ background: rgba(52, 211, 153, 0.2); color: #34d399; }}
.ineffective-badge {{ background: rgba(248, 113, 113, 0.15); color: #f87171; }}
.insight-box {{ background: #1e293b; border-radius: 12px; padding: 24px; border: 1px solid #334155; margin: 16px 0; }}
.insight-box ul {{ padding-left: 20px; }}
.insight-box li {{ margin: 8px 0; }}
.insight-box code {{ background: #334155; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; color: #60a5fa; }}
</style>
</head>
<body>
<div class="container">
<h1>策略优化分析报告</h1>
<p style="text-align:center;color:#94a3b8;margin-bottom:40px;">测试集: 2020-04-28 ~ 2024-04-28 | 验证集: 2024-04-28 ~ 2026-04-28</p>
<!-- 一、优化总览 -->
<h2>一、优化总览</h2>
<div class="summary-cards">
<div class="card"><div class="card-label">夏普比率</div><div class="card-value up">基线 → 优化后</div></div>
<div class="card"><div class="card-label">总收益率</div><div class="card-value up">基线 → 优化后</div></div>
<div class="card"><div class="card-label">最大回撤</div><div class="card-value up">基线 → 优化后</div></div>
<div class="card"><div class="card-label">年化收益率</div><div class="card-value up">基线 → 优化后</div></div>
</div>
<!-- 二、策略说明 -->
<h2>二、策略说明</h2>
<div class="insight-box">
<h3>原始策略逻辑</h3><ul><li>...</li></ul>
<h3>优化后新增逻辑</h3><ul><li>...</li></ul>
</div>
<!-- 三、单项优化回测结果 -->
<h2>三、单项优化回测结果</h2>
<table>
<thead><tr><th>优化方向</th><th>参数</th><th>夏普比率</th><th>夏普变化</th><th>总收益</th><th>最大回撤</th></tr></thead>
<tbody>{rows_single}</tbody>
</table>
<div class="chart-grid">
<div class="chart-box"><canvas id="chartSingleSharpe"></canvas></div>
<div class="chart-box"><canvas id="chartSingleReturn"></canvas></div>
</div>
<div class="chart-box full"><canvas id="chartSingleImprovement"></canvas></div>
<!-- 四、硬逻辑与过度拟合审查 -->
<h2>四、硬逻辑与过度拟合审查</h2>
<table>
<thead><tr><th>优化方向</th><th>硬逻辑评级</th><th>样本外衰减比</th><th>参数敏感度</th><th>时间稳定性</th><th>审查结论</th></tr></thead>
<tbody><!-- 按实际审查结果填写 --></tbody>
</table>
<div class="chart-grid">
<div class="chart-box"><canvas id="chartOOSDecay"></canvas></div>
<div class="chart-box"><canvas id="chartParamSensitivity"></canvas></div>
</div>
<div class="chart-box full"><canvas id="chartTemporalStability"></canvas></div>
<div class="insight-box">
<h3>过度拟合风险警示</h3><ul><!-- 标注高风险优化及原因 --></ul>
</div>
<!-- 五、组合优化回测结果 -->
<h2>五、组合优化回测结果</h2>
<!-- 类似结构 -->
<!-- 六、核心发现 -->
<h2>六、核心发现</h2>
<div class="insight-box"><!-- 深度洞察 --></div>
<!-- 七、优化前后对比 -->
<h2>七、优化前后对比</h2>
<!-- 雷达图 + 参数清单 -->
<!-- 八、验证集检验 -->
<h2>八、验证集检验 (2024-04-28 ~ 2026-04-28)</h2>
<!-- 验证集上的策略表现,与测试集对比,衰减比分析 -->
</div>
<script>
Chart.defaults.color = '#94a3b8';
const gridColor = 'rgba(148, 163, 184, 0.1)';
// 夏普比率柱状图(含基线参考线)
new Chart(document.getElementById('chartSingleSharpe'), {{
type: 'bar',
data: {{
labels: {json.dumps(labels_single, ensure_ascii=False)},
datasets: [
{{ label: '夏普比率', data: {json.dumps(sharpe_single)}, backgroundColor: {json.dumps(single_sharpe_colors)}, borderWidth: 1 }},
{{ label: '基线', data: Array({len(labels_single)}).fill({baseline['sharpe']:.4f}), type: 'line', borderColor: '#fbbf24', borderDash: [5, 5], borderWidth: 2, pointRadius: 0, fill: false }}
]
}},
options: {{
responsive: true,
plugins: {{ title: {{ display: true, text: '单项优化 - 夏普比率' }} }},
scales: {{ y: {{ grid: {{ color: gridColor }} }}, x: {{ grid: {{ display: false }}, ticks: {{ maxRotation: 45 }} }} }}
}}
}});
// 夏普提升幅度水平柱状图
new Chart(document.getElementById('chartSingleImprovement'), {{
type: 'bar',
data: {{
labels: {json.dumps(labels_single, ensure_ascii=False)},
datasets: [{{ label: '夏普变化(%)', data: {json.dumps([round(x, 1) for x in sharpe_improvement_single])}, backgroundColor: {json.dumps(single_improve_colors)}, borderWidth: 1 }}]
}},
options: {{ indexAxis: 'y', responsive: true }}
}});
</script>
</body>
</html>"""
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
f.write(html)
print(f'Report generated: {OUTPUT_FILE}')
if __name__ == '__main__':
generate_report()
HTML 报告生成注意事项:
json.dumps() 将 Python 列表转为 JavaScript 数组,需设置 ensure_ascii=False 以支持中文标签https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.jsoptimization/optimization_report.html(与优化脚本同目录),便于统一管理rgba(52,211,153,...) = 有效,红色 rgba(248,113,113,...) = 无效,蓝色 rgba(96,165,250,...) = 最优组合,黄色 #fbbf24 = 基线参考线optimization_history.jsonimport 框架模块之前设置 os.environ['QMT_CACHE_DIR'] = os.path.join(PROJECT_ROOT, '.cache'),确保缓存数据写入项目根目录的 .cache/ 而非脚本运行目录下的 .cache/本项目中的策略遵循以下模式:
from collections import defaultdict
from typing import Dict, List
from core.stock_selection import StockSelectionStrategy
from strategies import register_strategy
@register_strategy('strategy_name', default_kwargs={'max_stocks': 10},
backtest_config={'cash': 1000000, 'commission': 0.0001,
'start_date': '2016-01-01', 'end_date': '2026-04-17'})
class MyStrategy(StockSelectionStrategy):
params = (
('rebalance_freq', 'monthly'),
('max_stocks', 10),
('position_ratio', 0.95),
)
def select_stocks(self) -> List[str]:
pool = self.get_stock_pool()
# ... 过滤步骤
return selected
from core.strategy_logic import StrategyLogic, BarData, OrderInfo, TradeInfo
from strategies import register_strategy
from .config import ETF_CODES
@register_strategy('etf_rotation',
backtest_config={'cash': 200000, 'commission': 0.0005,
'start_date': '2020-04-28', 'end_date': '2026-04-28'})
class ETFRotationStrategy(StrategyLogic):
params = (
('return_period', 20),
('max_volatility', 0.03),
('switch_threshold', 0.05),
)
def on_bar(self, bar: BarData):
# K线到达时执行轮动逻辑
pass
def on_order(self, order: OrderInfo):
# 委托状态变化处理
pass
def on_trade(self, trade: TradeInfo):
# 成交回报处理
pass
| 方法 | 说明 |
|---|---|
self.get_stock_pool() | 获取股票池列表 |
self.get_financial_field(stock, table, field) | 获取股票财务数据 |
self.get_current_price(stock) | 获取当前价格 |
self.get_close_prices(stock, count) | 获取最近N日收盘价 |
self.get_industry(stock) | 获取行业分类 |
self.get_symbols() | 获取所有可用标的 |
self.log(msg) | 记录日志 |
| 方法 | 说明 |
|---|---|
self.get_close_prices(symbol) | 获取标的收盘价序列 |
self.get_current_price(symbol) | 获取标的当前价格 |
self.get_position_size(symbol) | 获取标的持仓数量 |
self.get_cash() | 获取可用现金 |
self.get_current_date() | 获取当前日期 |
self.buy(symbol, price, size) | 买入 |
self.sell(symbol, price, size) | 卖出 |
self.log(msg) | 记录日志 |
所有优化相关文件严格限制在被优化策略所在的目录内,优化结果保存在策略目录下的 optimization/ 子目录中:
strategies/<策略目录>/ # ← 被优化策略的根目录
├── <策略名>.py # 策略主文件
├── __init__.py # 模块初始化文件
├── readme.md # 策略文档(优化后更新)
├── backtest_results/ # 回测结果记录(原有)
│ └── YYYYMMDD_HHMMSS_<策略名>.json
└── optimization/ # ← 新增的优化目录(全部在策略目录内)
├── run_optimization.py # 自动化回测运行脚本
├── generate_report.py # HTML报告生成脚本
├── optimization_report.html # HTML优化报告
├── optimization_history.json # 优化历史记录(每次优化追加写入)
└── optimization_results/ # 优化回测结果子目录
├── baseline.json # 基线结果
├── opt01_xxx.json # 优化1结果
├── opt02_xxx.json # 优化2结果
├── ...
├── optNN_combined.json # 组合优化结果
├── opt01_xxx_oos.json # 优化1样本外验证结果
├── opt01_xxx_is.json # 优化1样本内验证结果
├── opt01_xxx_perturb_*.json # 优化1参数敏感性测试结果
├── opt01_xxx_YYYY.json # 优化1年度稳定性测试结果
└── review_summary.json # 硬逻辑与过度拟合审查汇总
目录隔离规则:
strategies/<策略目录>/optimization/ 目录内| 策略注册名 | 策略目录 | 基类 |
|---|---|---|
small_cap | strategies/small_cap_strategy/ | StockSelectionStrategy |
high_dividend | strategies/high_dividend_strategy/ | StockSelectionStrategy |
double_ma | strategies/example_strategy/ | StrategyLogic |
fundamental_roe | strategies/fundamental_strategy/ | StockSelectionStrategy |
fundamental_growth | strategies/fundamental_strategy/ | StockSelectionStrategy |
etf_rotation | strategies_for_vip/etf_rotation_strategy/ | StrategyLogic |
通过 get_strategy_dir() 函数获取策略的绝对路径:
from strategies import get_strategy_dir
strategy_dir = get_strategy_dir('small_cap')
# => 'E:\...\strategies\small_cap_strategy'
报告应包含以下内容:
基于小市值策略优化经验,以下方案已证明具有一致的有效性:
| 优化方案 | 类型 | 典型影响 | 实现方式 |
|---|---|---|---|
| 波动率过滤 | 事前风控 | 夏普+10~15% | 过滤日波动率超过阈值的股票 |
| 止损机制 | 事后风控 | 夏普+10~15% | 调仓时剔除亏损超过阈值的股票 |
| 波动率+止损组合 | 双层风控 | 夏普+20~25% | 以上两项组合使用 |
以下方案对小市值策略已验证无效:
基于 ETF 轮动策略优化经验,以下方案已证明有效:
| 优化方案 | 类型 | 典型影响 | 实现方式 |
|---|---|---|---|
| 波动率过滤 | 事前风控 | 夏普+15% | 过滤日波动率超过3%的标的 |
| 换仓阈值 | 成本控制 | 夏普+28% | 新标的需比当前持仓收益高出阈值才换仓 |
| 波动率+换仓阈值组合 | 风控+成本 | 夏普+48% | 以上两项组合,存在协同效应 |
以下方案对 ETF 轮动策略已验证无效:
| 经验 | 说明 |
|---|---|
| 交易成本是最大敌人 | 所有能有效减少换仓的优化都显著提升夏普比率 |
| 波动率过滤普适有效 | 对选股和轮动策略均有效,是首选优化方向 |
| 止损效果因策略而异 | 对选股策略有效,对已有空仓机制的轮动策略无效甚至有害 |
| 组合优化需检测冲突 | 单项有效的优化组合后可能冲突,必须验证 |
| 简单阈值优于复杂模型 | 换仓阈值(5%)比动量加速等复杂模型效果更好 |