feat(strategy): finalize global rotation system with advanced risk controls
Summary of updates: 1. Core Logic (engine.py): Added 'score > 0' filtering to support automatic cash positions during market downturns. 2. Experimental Analysis: Added scripts/analyze_negative_scores.py, scripts/test_select_num.py, and scripts/ab_test_iterations.py. 3. Documentation: Created docs/strategy_evolution_report.md detailing the evolution from benchmark to the final 47% CAGR version. 4. Configuration: Finalized rotation.yaml with 11 core assets and optimal risk parameters.
This commit is contained in:
182
scripts/ab_test_iterations.py
Normal file
182
scripts/ab_test_iterations.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
策略迭代 A/B 对比实验脚本
|
||||
量化三个维度的改进贡献度:
|
||||
1. 标的池: 原始全市场池 vs. 精选11只核心池
|
||||
2. 评分公式: 简单斜率(slope_r2) vs. 年化收益率*R2 (weighted_momentum)
|
||||
3. 观察窗口: 固定25日窗口 vs. 动态ATR窗口 (20-60天)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from strategies.rotation.engine import RotationStrategy
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# ==================== 标的池定义 ====================
|
||||
ORIGINAL_POOL = {
|
||||
"000300.SH": {"name": "沪深300", "market": "A", "etf": "510300.SH"},
|
||||
"000905.SH": {"name": "中证500", "market": "A", "etf": "510500.SH"},
|
||||
"000852.SH": {"name": "中证1000", "market": "A", "etf": "512100.SH"},
|
||||
"399006.SZ": {"name": "创业板指", "market": "A", "etf": "159915.SZ"},
|
||||
"000015.SH": {"name": "上证红利", "market": "A", "etf": "510880.SH"},
|
||||
"399986.SZ": {"name": "中证银行", "market": "A", "etf": "516310.SH"},
|
||||
"399997.SZ": {"name": "中证白酒", "market": "A", "etf": "512690.SH"},
|
||||
"399989.SZ": {"name": "中证医疗", "market": "A", "etf": "512170.SH"},
|
||||
"399395.SZ": {"name": "国证有色", "market": "A", "etf": "159880.SZ"},
|
||||
"399998.SZ": {"name": "中证煤炭", "market": "A", "etf": "515220.SH"},
|
||||
"399967.SZ": {"name": "中证军工", "market": "A", "etf": "512660.SH"},
|
||||
"HSTECH.HK": {"name": "恒生科技", "market": "HK", "etf": "513180.SH"},
|
||||
"NDX": {"name": "纳指100", "market": "US", "etf": "513100.SH"},
|
||||
"AU.SHF": {"name": "黄金", "market": "COMMODITY", "etf": "518880.SH"}
|
||||
}
|
||||
|
||||
FINAL_POOL = {
|
||||
"399006.SZ": {"name": "创业板指", "market": "A", "etf": "159915.SZ"},
|
||||
"H30269.CSI": {"name": "中证红利低波", "market": "A", "etf": "512890.SH"},
|
||||
"000015.SH": {"name": "上证红利", "market": "A", "etf": "510880.SH"},
|
||||
"NDX": {"name": "纳指100", "market": "US", "etf": "513100.SH"},
|
||||
"N225": {"name": "日经225", "market": "JP", "etf": "513520.SH"},
|
||||
"GDAXI": {"name": "德国DAX", "market": "EU", "etf": "513030.SH"},
|
||||
"HSI": {"name": "恒生指数", "market": "HK", "etf": "159920.SZ"},
|
||||
"HSTECH.HK": {"name": "恒生科技", "market": "HK", "etf": "513130.SH"},
|
||||
"AU.SHF": {"name": "黄金", "market": "COMMODITY", "etf": "518880.SH"},
|
||||
"CL.NYM": {"name": "原油", "market": "COMMODITY", "etf": "160723.SZ"},
|
||||
"931862.CSI": {"name": "30年国债", "market": "BOND", "etf": "511090.SH"}
|
||||
}
|
||||
|
||||
# ==================== 实验配置 ====================
|
||||
ITERATIONS = [
|
||||
{
|
||||
"label": "1. 原始基准 (原始池+简单评分+固定窗口)",
|
||||
"config": {
|
||||
"code_list": ORIGINAL_POOL,
|
||||
"factor_type": "slope_r2",
|
||||
"auto_day": False,
|
||||
"n_days": 25,
|
||||
"diversified": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "2. 标的池优化 (精选池+简单评分+固定窗口)",
|
||||
"config": {
|
||||
"code_list": FINAL_POOL,
|
||||
"factor_type": "slope_r2",
|
||||
"auto_day": False,
|
||||
"n_days": 25,
|
||||
"diversified": True # 开启跨大类分散
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "3. 评分公式优化 (精选池+加权评分+固定窗口)",
|
||||
"config": {
|
||||
"code_list": FINAL_POOL,
|
||||
"factor_type": "weighted_momentum",
|
||||
"auto_day": False,
|
||||
"n_days": 25,
|
||||
"diversified": True
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "4. 终极版本 (精选池+加权评分+动态窗口)",
|
||||
"config": {
|
||||
"code_list": FINAL_POOL,
|
||||
"factor_type": "weighted_momentum",
|
||||
"auto_day": True,
|
||||
"n_days": 25, # 提供默认窗口作为 fallback
|
||||
"min_days": 20,
|
||||
"max_days": 60,
|
||||
"diversified": True
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
COMMON_CONFIG = {
|
||||
"start_date": "2019-01-01",
|
||||
"end_date": datetime.now().strftime('%Y-%m-%d'),
|
||||
"select_num": 3,
|
||||
"rebalance_days": 1,
|
||||
"rebalance_threshold": 0.0,
|
||||
"trade_cost": 0.001,
|
||||
"premium_control": {"enabled": True, "default_threshold": 0.10},
|
||||
"use_cache": True,
|
||||
"ssh_tunnel": {"enabled": True, "host": "8.218.167.69", "port": 22, "username": "root", "key_path": "hk_ecs.pem", "local_port": 1080}
|
||||
}
|
||||
|
||||
def run_experiment():
|
||||
results = []
|
||||
|
||||
for i, item in enumerate(ITERATIONS):
|
||||
print(f"\n{'='*80}")
|
||||
print(f"运行实验 {item['label']}")
|
||||
print(f"{'='*80}")
|
||||
|
||||
cfg = COMMON_CONFIG.copy()
|
||||
cfg.update(item['config'])
|
||||
|
||||
strategy = RotationStrategy(cfg)
|
||||
try:
|
||||
res_df = strategy.run()
|
||||
|
||||
# 计算指标
|
||||
nav = res_df['轮动策略净值']
|
||||
total_ret = nav.iloc[-1] - 1
|
||||
days = (nav.index[-1] - nav.index[0]).days
|
||||
cagr = (1 + total_ret)**(365.25/days) - 1
|
||||
|
||||
daily_ret = res_df['轮动策略日收益率']
|
||||
sharpe = daily_ret.mean() / daily_ret.std() * np.sqrt(252) if daily_ret.std() > 0 else 0
|
||||
|
||||
peak = nav.cummax()
|
||||
dd = (nav - peak) / peak
|
||||
max_dd = dd.min()
|
||||
|
||||
results.append({
|
||||
"label": item['label'],
|
||||
"total_ret": total_ret,
|
||||
"cagr": cagr,
|
||||
"max_dd": max_dd,
|
||||
"sharpe": sharpe,
|
||||
"nav": nav
|
||||
})
|
||||
print(f"完成: CAGR={cagr:.2%}, MaxDD={max_dd:.2%}, Sharpe={sharpe:.2f}")
|
||||
except Exception as e:
|
||||
print(f"实验失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# ==================== 汇总报告 ====================
|
||||
print(f"\n\n{'='*100}")
|
||||
print(f"{'策略迭代对比报告':^100}")
|
||||
print(f"{'='*100}")
|
||||
print(f"{'版本':<40} | {'累计收益':>10} | {'年化(CAGR)':>10} | {'最大回撤':>10} | {'夏普比率':>8} | {'贡献增量':>10}")
|
||||
print(f"{'-'*100}")
|
||||
|
||||
prev_cagr = 0
|
||||
for i, r in enumerate(results):
|
||||
delta = f"+{(r['cagr'] - prev_cagr)*100:>.2f}%" if i > 0 else "-"
|
||||
print(f"{r['label']:<40} | {r['total_ret']:>10.2%} | {r['cagr']:>10.2%} | {r['max_dd']:>10.2%} | {r['sharpe']:>8.2f} | {delta:>10}")
|
||||
prev_cagr = r['cagr']
|
||||
print(f"{'='*100}")
|
||||
|
||||
# ==================== 绘图 ====================
|
||||
plt.figure(figsize=(15, 8))
|
||||
for r in results:
|
||||
plt.plot(r['nav'].index, r['nav'], label=r['label'], linewidth=1.5)
|
||||
|
||||
plt.yscale('log')
|
||||
plt.title("策略迭代 A/B 对比 - 净值曲线 (对数坐标)", fontsize=14)
|
||||
plt.legend()
|
||||
plt.grid(True, alpha=0.3)
|
||||
|
||||
output_path = Path(__file__).parent.parent / "results" / "ab_test_iterations.png"
|
||||
plt.savefig(output_path)
|
||||
print(f"\n对比图表已保存至: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_experiment()
|
||||
115
scripts/analyze_negative_scores.py
Normal file
115
scripts/analyze_negative_scores.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
分析历史 Top 3 标的中存在负分的情况 (正式版)
|
||||
"""
|
||||
import sys
|
||||
import yaml
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 添加项目根目录
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from strategies.rotation.engine import RotationStrategy
|
||||
from core.factors.momentum import compute_factors
|
||||
|
||||
def load_config(config_path: str) -> dict:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def analyze_negative_scores():
|
||||
config_path = "config/strategies/rotation.yaml"
|
||||
config = load_config(config_path)
|
||||
|
||||
# 强制不使用过滤,以获取完整数据
|
||||
config['diversified'] = True
|
||||
config['select_num'] = 3
|
||||
|
||||
strategy = RotationStrategy(config)
|
||||
|
||||
# 使用策略内部方法获取数据
|
||||
with strategy.data_source:
|
||||
index_data, etf_data, etf_nav_data, benchmark_data, valid_codes, index_ohlcv_data = strategy.data_source.fetch_all(
|
||||
config['code_list'],
|
||||
config['benchmark']['code'],
|
||||
config["start_date"],
|
||||
datetime.now().strftime('%Y-%m-%d')
|
||||
)
|
||||
|
||||
# 手动计算因子 (不带过滤)
|
||||
# 注意:为了分析原始得分,我们将 compute_factors 内部调用的过滤函数暂时跳过或分析结果
|
||||
factor_data, valid_codes = compute_factors(
|
||||
index_data,
|
||||
valid_codes,
|
||||
n=config["n_days"],
|
||||
factor_type=config["factor_type"],
|
||||
auto_day=config.get("auto_day", False),
|
||||
index_ohlcv_data=index_ohlcv_data
|
||||
)
|
||||
|
||||
score_cols = [c for c in factor_data.columns if c.startswith("得分_")]
|
||||
code_config = config['code_list']
|
||||
|
||||
total_days = len(factor_data)
|
||||
results = []
|
||||
|
||||
last_top_3 = set()
|
||||
rebalance_count = 0
|
||||
|
||||
for date, row in factor_data.iterrows():
|
||||
scores = row[score_cols].dropna()
|
||||
if scores.empty: continue
|
||||
|
||||
# 模拟 diversified 逻辑下的 Top 3 (不带 >0 过滤)
|
||||
cat_best = {}
|
||||
for col_name, s in scores.items():
|
||||
code = col_name.replace("得分_", "")
|
||||
cat = code_config.get(code, {}).get("market", "未知")
|
||||
if cat not in cat_best or s > cat_best[cat][1]:
|
||||
cat_best[cat] = (code, s)
|
||||
|
||||
sorted_cats = sorted(cat_best.values(), key=lambda x: x[1], reverse=True)
|
||||
top_3_raw = sorted_cats[:3]
|
||||
current_top_3_codes = set(code for code, s in top_3_raw)
|
||||
|
||||
# 判断是否发生调仓(目标持仓集合发生变化)
|
||||
if current_top_3_codes != last_top_3:
|
||||
rebalance_count += 1
|
||||
# 统计调仓日这 3 只中得分 <= 0 的数量
|
||||
neg_count = sum(1 for code, s in top_3_raw if s <= 0)
|
||||
|
||||
results.append({
|
||||
"date": date,
|
||||
"neg_count": neg_count,
|
||||
"top_1_score": top_3_raw[0][1],
|
||||
"top_2_score": top_3_raw[1][1] if len(top_3_raw)>1 else np.nan,
|
||||
"top_3_score": top_3_raw[2][1] if len(top_3_raw)>2 else np.nan,
|
||||
"top_1_name": code_config.get(top_3_raw[0][0], {}).get('name')
|
||||
})
|
||||
last_top_3 = current_top_3_codes
|
||||
|
||||
neg_df = pd.DataFrame(results)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"调仓日 (Rebalance Day) Top 3 标的出现负分情况分析")
|
||||
print(f"{'='*60}")
|
||||
print(f"总调仓次数: {rebalance_count}")
|
||||
print(f"涉及负分(<=0)的调仓次数: {len(neg_df[neg_df['neg_count']>0])} ({len(neg_df[neg_df['neg_count']>0])/rebalance_count:.1%})")
|
||||
|
||||
if not neg_df.empty:
|
||||
print(f"\n调仓日负分详细分布:")
|
||||
print(f" - 只有 1 只标的为负: {len(neg_df[neg_df['neg_count']==1])} 次")
|
||||
print(f" - 有 2 只标的为负: {len(neg_df[neg_df['neg_count']==2])} 次")
|
||||
print(f" - 全部 3 只标的均为负: {len(neg_df[neg_df['neg_count']==3])} 次")
|
||||
|
||||
print(f"\n最近 10 次涉及负分的调仓详情:")
|
||||
neg_df['date'] = pd.to_datetime(neg_df['date'])
|
||||
print(neg_df[neg_df['neg_count']>0][['date', 'neg_count', 'top_1_score', 'top_1_name']].tail(10))
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_negative_scores()
|
||||
112
scripts/test_select_num.py
Normal file
112
scripts/test_select_num.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
持仓数量 (select_num) 敏感度测试
|
||||
测试 select_num 分别为 1, 2, 3, 4, 5 时的策略表现
|
||||
基于最终精选的 11 只标的池
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# 添加项目根目录
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from strategies.rotation.engine import RotationStrategy
|
||||
|
||||
# ==================== 基础配置 ====================
|
||||
FINAL_POOL = {
|
||||
"399006.SZ": {"name": "创业板指", "market": "A", "etf": "159915.SZ"},
|
||||
"H30269.CSI": {"name": "中证红利低波", "market": "A", "etf": "512890.SH"},
|
||||
"000015.SH": {"name": "上证红利", "market": "A", "etf": "510880.SH"},
|
||||
"NDX": {"name": "纳指100", "market": "US", "etf": "513100.SH"},
|
||||
"N225": {"name": "日经225", "market": "JP", "etf": "513520.SH"},
|
||||
"GDAXI": {"name": "德国DAX", "market": "EU", "etf": "513030.SH"},
|
||||
"HSI": {"name": "恒生指数", "market": "HK", "etf": "159920.SZ"},
|
||||
"HSTECH.HK": {"name": "恒生科技", "market": "HK", "etf": "513130.SH"},
|
||||
"AU.SHF": {"name": "黄金", "market": "COMMODITY", "etf": "518880.SH"},
|
||||
"CL.NYM": {"name": "原油", "market": "COMMODITY", "etf": "160723.SZ"},
|
||||
"931862.CSI": {"name": "30年国债", "market": "BOND", "etf": "511090.SH"}
|
||||
}
|
||||
|
||||
BASE_CONFIG = {
|
||||
"start_date": "2019-01-01",
|
||||
"end_date": datetime.now().strftime('%Y-%m-%d'),
|
||||
"code_list": FINAL_POOL,
|
||||
"factor_type": "weighted_momentum",
|
||||
"auto_day": False, # 使用当前设定的固定窗口
|
||||
"n_days": 25,
|
||||
"diversified": True,
|
||||
"rebalance_days": 1,
|
||||
"rebalance_threshold": 0.0,
|
||||
"trade_cost": 0.001,
|
||||
"premium_control": {"enabled": True, "default_threshold": 0.10},
|
||||
"use_cache": True,
|
||||
"ssh_tunnel": {"enabled": True, "host": "8.218.167.69", "port": 22, "username": "root", "key_path": "hk_ecs.pem", "local_port": 1080}
|
||||
}
|
||||
|
||||
def run_sensitivity_test():
|
||||
test_values = [1, 2, 3, 4, 5]
|
||||
results = []
|
||||
|
||||
for val in test_values:
|
||||
print(f"\n测试 select_num = {val} ...")
|
||||
cfg = BASE_CONFIG.copy()
|
||||
cfg["select_num"] = val
|
||||
|
||||
strategy = RotationStrategy(cfg)
|
||||
try:
|
||||
res_df = strategy.run()
|
||||
|
||||
nav = res_df['轮动策略净值']
|
||||
total_ret = nav.iloc[-1] - 1
|
||||
days = (nav.index[-1] - nav.index[0]).days
|
||||
cagr = (1 + total_ret)**(365.25/days) - 1
|
||||
|
||||
daily_ret = res_df['轮动策略日收益率']
|
||||
sharpe = daily_ret.mean() / daily_ret.std() * np.sqrt(252) if daily_ret.std() > 0 else 0
|
||||
|
||||
peak = nav.cummax()
|
||||
dd = (nav - peak) / peak
|
||||
max_dd = dd.min()
|
||||
|
||||
results.append({
|
||||
"select_num": val,
|
||||
"total_ret": total_ret,
|
||||
"cagr": cagr,
|
||||
"max_dd": max_dd,
|
||||
"sharpe": sharpe,
|
||||
"nav": nav
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"测试失败 (select_num={val}): {e}")
|
||||
|
||||
# ==================== 汇总报告 ====================
|
||||
print(f"\n\n{'='*90}")
|
||||
print(f"{'持仓数量 (select_num) 敏感度测试报告':^90}")
|
||||
print(f"{'='*90}")
|
||||
print(f"{'持仓数':<10} | {'累计收益':>12} | {'年化(CAGR)':>12} | {'最大回撤':>12} | {'夏普比率':>10}")
|
||||
print(f"{'-'*90}")
|
||||
|
||||
for r in results:
|
||||
print(f"{r['select_num']:<10} | {r['total_ret']:>12.2%} | {r['cagr']:>12.2%} | {r['max_dd']:>12.2%} | {r['sharpe']:>10.2f}")
|
||||
print(f"{'='*90}")
|
||||
|
||||
# ==================== 绘图 ====================
|
||||
plt.figure(figsize=(14, 7))
|
||||
for r in results:
|
||||
plt.plot(r['nav'].index, r['nav'], label=f"select_num = {r['select_num']}")
|
||||
|
||||
plt.yscale('log')
|
||||
plt.title("持仓数量对净值的影响 (select_num 1-5)", fontsize=14)
|
||||
plt.legend()
|
||||
plt.grid(True, alpha=0.3)
|
||||
|
||||
output_path = Path(__file__).parent.parent / "results" / "select_num_test.png"
|
||||
plt.savefig(output_path)
|
||||
print(f"\n对比图表已保存至: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sensitivity_test()
|
||||
Reference in New Issue
Block a user