strategy-optimizer
系统性地通过回测优化量化交易策略。当用户需要优化、改进或调优策略性能指标(如夏普比率)时调用此技能。
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
系统性地通过回测优化量化交易策略。当用户需要优化、改进或调优策略性能指标(如夏普比率)时调用此技能。
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
| 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%)比动量加速等复杂模型效果更好 |