Initial commit: cc-cursor 全链路量化研究平台

7 Sprints 全部完成:
  Sprint 0: 基础设施 (DataManager + MariaDB)
  Sprint 1: 因子引擎 (34因子/12分类)
  Sprint 2: VectorBT 回测 (5策略+截面)
  Sprint 3: Optuna 优化 (+Walk-Forward)
  Sprint 4: ML 模型 (LightGBM+CatBoost)
  Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐)
  Sprint 6: Agent 系统 (4Agent+日报.md/.html)

生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping,
  save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复,
  RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4,
  CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化,
  indexDatas API修正

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 15:59:05 +08:00
co-authored by Claude Opus 4.7
commit 271a9343a5
293 changed files with 59598 additions and 0 deletions
View File
+242
View File
@@ -0,0 +1,242 @@
"""
Optuna 优化引擎。
统一接口:optimizer.optimize(strategy_class, space, price_df, factor_df) → OptimizationResult
"""
import copy
import time
import numpy as np
import optuna
import pandas as pd
from backtest.base import BaseStrategy
from backtest.report import BacktestReport
from backtest.vectorbt.engine import VectorBTEngine
from optimizer.objectives import Objective
from optimizer.result import OptimizationResult, WalkForwardResult
from optimizer.space import SearchSpace
# 抑制 Optuna 日志
optuna.logging.set_verbosity(optuna.logging.WARNING)
class OptunaEngine:
"""
Optuna 优化引擎。
"""
def __init__(self, bt_engine: VectorBTEngine | None = None):
self.bt_engine = bt_engine or VectorBTEngine()
def optimize(
self,
strategy_class: type[BaseStrategy],
search_space: SearchSpace,
price_df: pd.DataFrame,
factor_df: pd.DataFrame | None = None,
metric: str = "sharpe",
n_trials: int = 100,
direction: str = "maximize",
sampler: optuna.samplers.BaseSampler | None = None,
) -> OptimizationResult:
"""
参数寻优。
参数:
strategy_class: 策略类
search_space: 搜索空间
price_df: 价格数据
factor_df: 因子数据
metric: 优化目标
n_trials: 试验次数
direction: 'maximize' | 'minimize'
sampler: Optuna 采样器,默认 TPESampler
"""
if sampler is None:
sampler = optuna.samplers.TPESampler(seed=42)
study = optuna.create_study(
direction=direction,
sampler=sampler,
)
objective = Objective(
strategy_class=strategy_class,
search_space=search_space,
price_df=price_df,
factor_df=factor_df,
bt_engine=self.bt_engine,
metric=metric,
)
t0 = time.time()
study.optimize(objective, n_trials=n_trials, show_progress_bar=True)
elapsed = time.time() - t0
# 用最优参数跑一次完整回测
best_params = study.best_params
try:
best_strategy = strategy_class(**best_params)
except TypeError:
valid = {k: v for k, v in best_params.items()
if k in strategy_class.__init__.__code__.co_varnames}
best_strategy = strategy_class(**valid)
best_report = self.bt_engine.run(
best_strategy,
price_df,
price_df if factor_df is None else factor_df,
)
# 参数重要性
try:
importance = optuna.importance.get_param_importances(study)
except Exception:
importance = {}
# 试验记录
trials_df = study.trials_dataframe()
return OptimizationResult(
best_params=study.best_params,
best_value=study.best_value,
metric=metric,
best_report=best_report,
trials_df=trials_df,
param_importance=importance,
)
def optimize_walk_forward(
self,
strategy_class: type[BaseStrategy],
search_space: SearchSpace,
price_df: pd.DataFrame,
factor_df: pd.DataFrame | None = None,
metric: str = "sharpe",
n_trials: int = 100,
train_window: int = 252 * 3,
test_window: int = 252,
) -> WalkForwardResult:
"""
滚动窗口优化(Walk-Forward Analysis)。
每一步:train_window 训练 → test_window 验证 → 滑动。
"""
if factor_df is None:
factor_df = price_df
n_total = len(price_df)
windows = []
test_equities = []
param_history = []
start = 0
while start + train_window + test_window <= n_total:
train_slice = slice(start, start + train_window)
test_slice = slice(start + train_window, start + train_window + test_window)
train_price = price_df.iloc[train_slice]
train_factor = factor_df.iloc[train_slice]
test_price = price_df.iloc[test_slice]
test_factor = factor_df.iloc[test_slice]
# 训练集上优化
opt_result = self.optimize(
strategy_class=strategy_class,
search_space=search_space,
price_df=train_price,
factor_df=train_factor,
metric=metric,
n_trials=n_trials,
)
# 测试集上验证
try:
test_strategy = strategy_class(**opt_result.best_params)
except TypeError:
valid = {k: v for k, v in opt_result.best_params.items()
if k in strategy_class.__init__.__code__.co_varnames}
test_strategy = strategy_class(**valid)
test_report = self.bt_engine.run(test_strategy, test_price, test_factor)
if len(test_report.equity_curve) > 0:
test_equities.append(test_report.equity_curve)
train_idx = train_price.index
test_idx = test_price.index
windows.append({
"train_start": train_idx[0] if len(train_idx) > 0 else "",
"train_end": train_idx[-1] if len(train_idx) > 0 else "",
"test_start": test_idx[0] if len(test_idx) > 0 else "",
"test_end": test_idx[-1] if len(test_idx) > 0 else "",
"best_params": opt_result.best_params,
"best_value": opt_result.best_value,
"test_return": test_report.total_return,
"test_sharpe": test_report.sharpe_ratio,
"test_mdd": test_report.max_drawdown,
})
param_history.append(opt_result.best_params)
start += test_window
# 合并测试期权益曲线
consolidated = _merge_test_periods(test_equities, self.bt_engine.initial_capital)
# 参数稳定性
param_df = pd.DataFrame(param_history) if param_history else pd.DataFrame()
if not param_df.empty:
param_df.index.name = "window"
return WalkForwardResult(
windows=windows,
consolidated_report=consolidated,
param_stability=param_df,
)
def _merge_test_periods(
equity_list: list[pd.Series],
initial_capital: float = 100_000,
) -> BacktestReport | None:
"""拼接各窗口测试期权益曲线为一个连续序列。"""
if not equity_list:
return None
merged = pd.concat(equity_list)
merged = merged.sort_index()
merged = merged[~merged.index.duplicated()]
# 确保 DatetimeIndex
if not isinstance(merged.index, pd.DatetimeIndex):
merged.index = pd.to_datetime(merged.index, format="%Y%m%d")
dd = merged / merged.cummax() - 1
daily_ret = merged.pct_change().dropna()
years = max(len(daily_ret) / 252, 0.02)
total_ret = (merged.iloc[-1] / merged.iloc[0] - 1) * 100
cagr = ((total_ret / 100 + 1) ** (1 / years) - 1) * 100
mdd = dd.min() * 100
std_ret = daily_ret.std() * np.sqrt(252)
sharpe = (daily_ret.mean() * 252) / std_ret if std_ret > 0 else 0
calmar = cagr / abs(mdd) if abs(mdd) > 0 else 0
try:
monthly = merged.resample("ME").last().pct_change()
except Exception:
monthly = pd.Series(dtype=float)
return BacktestReport(
total_return=round(total_ret, 2),
cagr=round(cagr, 2),
max_drawdown=round(mdd, 2),
sharpe_ratio=round(sharpe, 2),
calmar_ratio=round(calmar, 2),
annual_volatility=round(std_ret * 100 if std_ret != 0 else 0, 2),
equity_curve=merged,
drawdown_curve=dd,
monthly_returns=monthly,
)
+74
View File
@@ -0,0 +1,74 @@
"""
Optuna 目标函数。
将策略实例化 → 回测 → 提取指标,包装为 Optuna objective。
"""
import optuna
import pandas as pd
from backtest.base import BaseStrategy
from backtest.vectorbt.engine import VectorBTEngine
from optimizer.space import SearchSpace
# 指标提取器:从 BacktestReport 取对应字段
_METRIC_EXTRACTORS = {
"sharpe": lambda r: r.sharpe_ratio,
"cagr": lambda r: r.cagr,
"calmar": lambda r: r.calmar_ratio,
"total_return": lambda r: r.total_return,
"return_over_dd": lambda r: abs(r.total_return / r.max_drawdown) if r.max_drawdown != 0 else 0.0,
"win_rate": lambda r: r.win_rate,
"profit_factor": lambda r: r.profit_factor,
}
class Objective:
"""
Optuna 目标函数(可调用)。
用法:
obj = Objective(SMACrossStrategy, sma_cross_space, price_df, factor_df, metric="sharpe")
study = optuna.create_study(direction="maximize")
study.optimize(obj, n_trials=100)
"""
def __init__(
self,
strategy_class: type[BaseStrategy],
search_space: SearchSpace,
price_df: pd.DataFrame,
factor_df: pd.DataFrame | None = None,
bt_engine: VectorBTEngine | None = None,
metric: str = "sharpe",
):
self.strategy_class = strategy_class
self.search_space = search_space
self.price_df = price_df
self.factor_df = factor_df if factor_df is not None else price_df
self.bt_engine = bt_engine or VectorBTEngine()
self.metric = metric
self._extractor = _METRIC_EXTRACTORS.get(metric)
if self._extractor is None:
raise ValueError(f"不支持的指标: '{metric}'。可选: {list(_METRIC_EXTRACTORS)}")
def __call__(self, trial: optuna.Trial) -> float:
params = self.search_space.suggest(trial)
try:
strategy = self.strategy_class(**params)
except TypeError:
# 过滤不匹配的参数
valid = {k: v for k, v in params.items()
if k in self.strategy_class.__init__.__code__.co_varnames}
strategy = self.strategy_class(**valid)
report = self.bt_engine.run(strategy, self.price_df, self.factor_df)
value = self._extractor(report) # type: ignore
# 无效值处理
if value is None or (isinstance(value, float) and (pd.isna(value) or value == float("inf"))):
return float("-inf")
return float(value)
+86
View File
@@ -0,0 +1,86 @@
"""
策略优化快捷函数。
为常用策略提供一键优化入口。
"""
import pandas as pd
from backtest.vectorbt.engine import VectorBTEngine
from optimizer.engine import OptunaEngine
from optimizer.result import OptimizationResult, WalkForwardResult
from optimizer.space import (
sma_cross_space,
rsi_revert_space,
momentum_breakout_space,
factor_cross_space,
)
_DEFAULT_TRIALS = 100
def optimize_sma_cross(
price_df: pd.DataFrame,
factor_df: pd.DataFrame | None = None,
bt_engine: VectorBTEngine | None = None,
n_trials: int = _DEFAULT_TRIALS,
metric: str = "sharpe",
) -> OptimizationResult:
"""均线交叉策略参数寻优。"""
from backtest.strategies.sma_cross import SMACrossStrategy
return OptunaEngine(bt_engine).optimize(
SMACrossStrategy, sma_cross_space, price_df, factor_df, metric, n_trials,
)
def optimize_rsi_revert(
price_df: pd.DataFrame,
factor_df: pd.DataFrame | None = None,
bt_engine: VectorBTEngine | None = None,
n_trials: int = _DEFAULT_TRIALS,
metric: str = "sharpe",
) -> OptimizationResult:
"""RSI 反转策略参数寻优。"""
from backtest.strategies.rsi_mean_revert import RSIMeanRevertStrategy
return OptunaEngine(bt_engine).optimize(
RSIMeanRevertStrategy, rsi_revert_space, price_df, factor_df, metric, n_trials,
)
def optimize_momentum_breakout(
price_df: pd.DataFrame,
factor_df: pd.DataFrame | None = None,
bt_engine: VectorBTEngine | None = None,
n_trials: int = _DEFAULT_TRIALS,
metric: str = "sharpe",
) -> OptimizationResult:
"""动量突破策略参数寻优。"""
from backtest.strategies.momentum_breakout import MomentumBreakoutStrategy
return OptunaEngine(bt_engine).optimize(
MomentumBreakoutStrategy, momentum_breakout_space, price_df, factor_df, metric, n_trials,
)
def optimize_factor_cross(
price_df: pd.DataFrame,
factor_column: str,
factor_df: pd.DataFrame | None = None,
bt_engine: VectorBTEngine | None = None,
n_trials: int = _DEFAULT_TRIALS,
metric: str = "sharpe",
) -> OptimizationResult:
"""因子阈值交叉策略参数寻优。
参数:
factor_column: 因子列名(如 'momentum_20'
其余同 optimize_* 系列。
"""
from backtest.strategies.factor_cross import FactorCrossStrategy
class _FCS(FactorCrossStrategy):
def __init__(self, buy_threshold=0, sell_threshold=None):
super().__init__(factor_column, buy_threshold, sell_threshold)
return OptunaEngine(bt_engine).optimize(
_FCS, factor_cross_space, price_df, factor_df, metric, n_trials,
)
+57
View File
@@ -0,0 +1,57 @@
"""
优化结果数据结构。
"""
from dataclasses import dataclass, field
import pandas as pd
from backtest.report import BacktestReport
@dataclass
class OptimizationResult:
"""单次参数优化结果。"""
best_params: dict = field(default_factory=dict)
best_value: float = 0.0
metric: str = "sharpe"
best_report: BacktestReport | None = None
trials_df: pd.DataFrame = field(default_factory=pd.DataFrame)
param_importance: dict = field(default_factory=dict)
def summary(self) -> str:
lines = [
f"最优参数: {self.best_params}",
f"最优目标 ({self.metric}): {self.best_value:.4f}",
]
if self.best_report is not None:
lines.append(f"回测: {self.best_report.summary()}")
return "\n".join(lines)
@dataclass
class WalkForwardResult:
"""滚动窗口优化结果。"""
windows: list[dict] = field(default_factory=list)
consolidated_report: BacktestReport | None = None
param_stability: pd.DataFrame = field(default_factory=pd.DataFrame)
def summary(self) -> str:
n = len(self.windows)
lines = [f"Walk-Forward: {n} 个窗口"]
for w in self.windows:
lines.append(
f" {w['train_start']}~{w['train_end']}"
f"{w['test_start']}~{w['test_end']}"
f" | 参数={w.get('best_params', {})}"
f" | 收益={w.get('test_return', 0):.1f}%"
)
if self.consolidated_report is not None:
lines.append(f"整体: {self.consolidated_report.summary()}")
if not self.param_stability.empty:
stds = self.param_stability.std()
lines.append(f"参数稳定性(std): {dict(stds.round(2))}")
return "\n".join(lines)
+67
View File
@@ -0,0 +1,67 @@
"""
参数搜索空间定义。
"""
from dataclasses import dataclass, field
import optuna
@dataclass
class SearchSpace:
"""参数搜索空间。"""
params: list[dict] = field(default_factory=list)
# 每个元素: {"name": str, "type": "int"|"float"|"categorical",
# "low": float, "high": float, "step": float, "choices": list}
def suggest(self, trial: optuna.Trial) -> dict:
"""从 trial 中采样一组参数。"""
result = {}
for p in self.params:
name = p["name"]
kind = p["type"]
if kind == "int":
low = p.get("low", 0)
high = p.get("high", 100)
step = p.get("step", 1)
result[name] = trial.suggest_int(name, int(low), int(high), step=int(step))
elif kind == "float":
low = p.get("low", 0.0)
high = p.get("high", 1.0)
result[name] = trial.suggest_float(name, float(low), float(high))
elif kind == "categorical":
choices = p.get("choices", [])
result[name] = trial.suggest_categorical(name, choices)
return result
# ── 预置搜索空间 ──────────────────────────────────────────
sma_cross_space = SearchSpace(params=[
{"name": "fast", "type": "int", "low": 2, "high": 30, "step": 1},
{"name": "slow", "type": "int", "low": 15, "high": 120, "step": 5},
])
rsi_revert_space = SearchSpace(params=[
{"name": "oversold", "type": "int", "low": 10, "high": 45, "step": 1},
{"name": "overbought", "type": "int", "low": 55, "high": 90, "step": 1},
])
momentum_breakout_space = SearchSpace(params=[
{"name": "lookback", "type": "int", "low": 10, "high": 60, "step": 5},
{"name": "exit_period", "type": "int", "low": 5, "high": 30, "step": 1},
])
factor_cross_space = SearchSpace(params=[
{"name": "buy_threshold", "type": "float", "low": -10.0, "high": 10.0},
{"name": "sell_threshold", "type": "float", "low": -10.0, "high": 10.0},
])
# 名称 → 空间映射
SPACES = {
"sma_cross": sma_cross_space,
"rsi_revert": rsi_revert_space,
"momentum_breakout": momentum_breakout_space,
"factor_cross": factor_cross_space,
}