基于 vectorbt 的月度定投回测:可插拔的权重分配器、多信号系统(drawdown/MA/VIX/年收益)、 多数据源行情容错(本地缓存/CSV、yfinance、Stooq、可选 IBKR)、与多条 baseline 在同一日历与资金约束下对比、 量化指标汇总,基于 Plotly 的多图报告、agent 中立报告包、Codex 交互式 artifact JSON 与 Jupyter 探索。
pip install -e .
pip install -e ".[ibkr]" # 可选:使用 IBKR/TWS 拉历史日线
# 跑一个策略
trading run QQQ,TQQQ --allocator smart --vix ^VIX --start 2021 --end 2025
# 批量跑所有预设策略
trading experiment --presets
# 生成交互式 HTML 报告
trading report QQQ,TQQQ --allocator smart --vix ^VIX
# 同时生成 HTML、通用报告包、Codex artifact JSON
trading report QQQ,TQQQ --allocator smart --vix ^VIX --format all --package-dir reports/latest_package
# 看看有哪些可用
trading list
trading show smart_signal_fusion常用参数:--start 2021(自动补全为 2021-01-01)、--budget 5000、--weights QQQ=0.7,TQQQ=0.3、
--signals ^IXIC,^GSPC、--allocator fixed|nasdaq_rule|equal_weight|smart|trend_follow|momentum_rotation、
--data-source auto|auto_ibkr|local|ibkr|yfinance|stooq
Python API 仍然可用,见下方第 11 节代码示例。
| 能力 | 说明 | 主要入口 |
|---|---|---|
| 定投策略制定 | 标的、区间、月预算、权重;按「年」调用分配器得到当月目标权重,再折算为股数订单 | DCAParams、build_order_sizes、run_dca_portfolio |
| 多信号系统 | drawdown、MA偏离、VIX恐惧指数、多指数年收益,每个定投日自动注入分配器 | SignalSnapshot、smart_allocator |
| 数据源韧性 | 先用缓存/本地文件,再按配置降级到 yfinance、Stooq 或 IBKR,缓解公开接口限流 | trading.data.fetch_close_prices |
| 策略家族扩展 | 固定权重、纳指规则、趋势过滤、动量轮动、现金仓位、sell/tilt 再平衡 | trading.strategies.dca、StrategySpec |
| Baseline 对比 | 与主策略共用对齐后的行情、相同 monthly_budget 与 total_invested |
run_scenarios |
| 量化分析 | CAGR、回撤、年化波动、夏普/索提诺、Calmar;可选合并 vectorbt stats;相对基准终值比 |
portfolio_metrics_row、compare_portfolios |
| 多维度可视化 | 净值/回撤/超额、年度权重柱状堆叠、月度收益热力图、滚动夏普、总览子图 | trading/viz.py、trading report |
| Agent 报告包 | 先输出通用 manifest + CSV 数据集,再按需转成 Codex artifact;后续可增加 Hermes adapter | trading/reporting/package.py、trading/reporting/codex_adapter.py |
| CLI | trading run/experiment/report/list/show 子命令 |
trading/cli.py |
| 策略规格抽象 | 用统一 StrategySpec 描述策略,便于批量实验与后续 LLM 生成 |
trading/specs.py |
| 批量实验与排名 | 多策略一键回测、导出汇总与排名表,支持 preset/spec-file/CLI 参数输入 | trading/experiment.py、trading experiment |
Trading/
├── pyproject.toml # 发行名 trading-backtest;可编辑安装后 import 仍为 trading
├── requirements.txt
├── trading/ # 核心库(Python 包名 trading)
│ ├── __init__.py # 对外 re-export 常用符号
│ ├── data.py # 行情下载、年度收益、定投日、Parquet 缓存
│ ├── engine.py # BacktestResult、portfolio_from_orders、run_dca_portfolio、run_scenarios
│ ├── scenario_context.py # ScenarioContext、BaselineBuilder 协议
│ ├── baseline_builders.py # monthly_full_invest、lump_sum_first_day 等工厂与 default_baseline_builders_v1
│ ├── metrics.py # 净值序列、指标行/表、超额序列
│ ├── specs.py # StrategySpec、preset 模板、JSON spec 解析
│ ├── experiment.py # run_experiment(s)、分配器注册、策略排名
│ ├── reporting/ # 通用报告包与 Codex artifact adapter
│ ├── viz.py # Plotly 图形与 HTML/PNG 写出
│ └── strategies/
│ └── dca.py # DCAParams、WeightAllocator、订单构造、内置分配器
├── examples/
│ └── dynamic_dca_vectorbt.py
├── notebooks/
│ └── backtest_explore.ipynb
├── data/
│ ├── cache/ # 收盘价 Parquet(gitignore,运行期生成)
│ └── pulling.py # 历史草稿,与引擎无耦合;新逻辑请用 trading.data
└── reports/ # trading report/experiment 输出目录(HTML/CSV/PNG 默认被 gitignore)
注意: PyPI/安装元数据中的项目名为 trading-backtest(见 pyproject.toml),避免与常见词冲突;代码中包目录仍为 trading/。
数据从 trading.data 的多数据源加载器进入后,在引擎内对齐索引,再生成 order_sizes 并交给 vectorbt.Portfolio.from_orders。
flowchart TB
subgraph data_layer [trading.data]
YF[yfinance / Stooq / IBKR / local]
Cache[Parquet cache]
YF --> Cache
Cache --> Close[对齐后的 close DataFrame]
Close --> Ann[年度收益 Series]
end
subgraph strat [trading.strategies.dca]
Alloc[WeightAllocator]
Build[build_order_sizes]
Alloc --> Build
Close --> Build
Ann --> Build
Build --> Orders[order_sizes DataFrame]
end
subgraph eng [trading.engine]
Orders --> VBT[Portfolio.from_orders]
Close --> VBT
VBT --> BR[BacktestResult]
end
subgraph post [metrics / viz]
BR --> Met[指标表]
BR --> Fig[Plotly]
BR --> Pack[报告包]
Pack --> Codex[Codex artifact JSON]
end
对齐规则(run_scenarios): 对 params.symbols ∪ params.extra_symbols 对应列做 dropna(how="any") 得 aligned_close;主策略取 strategy_close = aligned_close[symbols]。各 baseline 由传入的 BaselineBuilder 从 ScenarioContext 中自行选取列子集(如 monthly_full_invest("SPY") 用 aligned_close[["SPY"]])。调用方须把所有 baseline 用到的 ticker 放进 symbols 或 extra_symbols,否则拉不到列会报错。total_invested 与定投次数与主策略一致以保证可比性。
| 字段 | 含义 |
|---|---|
symbols |
主策略交易标的元组;权重字典应覆盖或子集于这些列 |
start / end |
字符串日期,传给数据源下载或从本地文件过滤 |
monthly_budget |
每个定投日投入现金总额(在各标的间按权重分配) |
default_weights |
归一化后的默认权重;__post_init__ 会再次 normalize_weights |
signal_symbols |
用于 fetch_annual_returns 的标的集合(如 ^IXIC,^GSPC),供动态分配器使用 |
benchmark_symbol |
仅作快捷参数;与 default_baseline_builders_v1(benchmark_symbol) 搭配使用,run_scenarios 不会自动读取 |
extra_symbols |
仅用于 baseline / 对比、不在主策略 symbols 中的 ticker;参与下载与 aligned_close |
use_cache |
是否读写 data/cache/close_<hash>.parquet |
data_source |
auto、auto_ibkr、local、ibkr、yfinance、stooq |
local_data_dir |
本地 CSV/parquet 行情目录;默认 data/local |
fee_rate / slippage_rate |
回测成交成本参数,透传到 Portfolio.from_orders 的 fees 与 slippage(默认 0) |
max_weight_per_asset |
单资产权重上限(可选),超过后会触发权重截断与再分配 |
max_gross_exposure |
每次定投预算使用上限(可选,0.8 表示最多投入月预算 80%) |
rebalance_max_weight / rebalance_mode |
持仓超阈值后的处理:sell 卖出超重资产,tilt 只倾斜新资金 |
cash_symbol |
虚拟现金标的(如 CASH),价格恒为 1,可用于趋势过滤/动量避险 |
risk_observe_only |
风控观测模式:只记录触发,不实际改变下单 |
可调用对象,签名:
(signal: SignalSnapshot, default_weights: dict[str, float]) -> dict[str, float]
signal.invest_year:该笔定投日所在自然年。signal.annual_returns:index 为年份、columns 为signal_symbols,值为相对上一自然年末收盘的涨跌幅。signal.drawdown/signal.ma_deviation/signal.vix:定投日级别的风控与环境信号。- 返回值需能映射到
asset_prices.columns;build_order_sizes内会合并缺失列为 0 再归一化。
内置实现:
fixed_weight_allocator:始终返回default_weights的归一化。nasdaq_rule_allocator:当default_weights的键集包含QQQ与TQQQ时,按上一年涨跌幅切换三档配比,否则退回固定权重。equal_weight_allocator:在default_weights的键上等权(用于 baseline C)。trend_follow_allocator:当主信号跌破均线时,把当月新资金转入现金类袖珍仓位(如CASH)。momentum_rotation_allocator:每年按上一年度表现把当月新资金轮动到最强资产;若全为负且有现金仓位,则转入现金。
对每个自然月,取该月第一个可用交易日作为定投日;订单仅在对应行非零。
| 字段 | 说明 |
|---|---|
name |
场景名;compare_portfolios 的表索引使用此字段(如 strategy、monthly_full_QQQ) |
portfolio |
vectorbt.Portfolio |
order_sizes |
与 close 对齐的日频订单股数 |
yearly_weights |
每年记录一次的目标权重列(部分 baseline 为 None) |
annual_returns |
信号标的年度收益序列(各场景共用同一份引用) |
total_invested |
近似总投入:monthly_budget × 定投次数;Portfolio.from_orders 的 init_cash 与此相同 |
签名: run_scenarios(params, allocator=..., *, baseline_builders=())。
baseline_builders 为 Sequence[BaselineBuilder];每个 builder 接收只读 ScenarioContext,返回 BacktestResult,其 name 用作返回字典的键(不得为 "strategy",且同次调用内不可重复)。
内置工厂(trading/baseline_builders.py):
| 工厂 | 生成的 BacktestResult.name(示例) |
含义 |
|---|---|---|
monthly_full_invest("QQQ") |
monthly_full_QQQ |
每月 monthly_budget 全买该标的 |
lump_sum_first_day() |
lump_sum_first_day |
首定投日一次性投入 total_invested,权重为 allocator 对应首月权重 |
equal_weight_monthly_on_strategy_universe() |
equal_weight_monthly |
在 symbols 宇宙上每月等权 DCA |
一键复刻旧版三条 baseline: baseline_builders=default_baseline_builders_v1(params.benchmark_symbol)。
引擎使用:
# trading.engine.portfolio_from_orders 封装为:
vbt.Portfolio.from_orders(
close=...,
size=order_sizes,
init_cash=total_invested,
fees=params.fee_rate,
slippage=params.slippage_rate,
cash_sharing=True,
group_by=True,
freq="1D",
)含义简述:
- 一次性
init_cash:等于「若每月都足额定投」的现金总和;未下单前的现金留在账户,用于近似「每月到账再买入」,而非逐日外部入金 API。 cash_sharing=True:多标的共享同一现金池。- 运行兜底: 正常环境优先使用
vectorbt.Portfolio.from_orders;若本地vectorbt/numba导入或运行失败,会退回到项目内置的简化日线组合估值器,覆盖value()与空stats(),保证报告和指标仍可生成。 - 成本与风控诊断:策略结果包含
risk_trigger_count与decision_snapshot(定投日级别的信号、权重截断、预算利用率),便于复盘规则影响。 - 收益与风险指标:
metrics中夏普/索提诺等基于组合净值日收益率;无风险利率默认年化2%,按 252 个交易日摊到日(见portfolio_metrics_row的risk_free_annual、trading_days_per_year)。
二次开发若需更贴近真实入金: 可改为 vectorbt 的现金流/自定义记录,或分段 from_orders;当前模块边界是「先统一生成 order_sizes + 单次 init_cash」。
- 数据源:
trading.data.fetch_close_prices支持auto、auto_ibkr、local、ibkr、yfinance、stooq。默认auto顺序为本地文件 -> yfinance 重试 -> Stooq;auto_ibkr顺序为本地文件 -> IBKR -> yfinance -> Stooq。 - yfinance 限流缓解: 先读完整区间缓存,再读单标的缓存;在线源失败后可回退到旧的单标的缓存,避免一次失败导致整组策略无法跑。
- 本地行情: 将
QQQ.csv、TQQQ.parquet等放入data/local/,至少包含日期列和Close列;也可用--local-data-dir指向其他目录。 - IBKR: 需要本机运行 TWS 或 IB Gateway,并安装
pip install -e ".[ibkr]"。默认连接127.0.0.1:7497、clientId=19;可用环境变量TRADING_IBKR_HOST、TRADING_IBKR_PORT、TRADING_IBKR_CLIENT_ID、TRADING_IBKR_REQUEST_SLEEP调整。IBKR 历史数据也有 pacing limit,项目默认按年度分片并在分片之间等待,适合日线 ETF/指数回测,不适合高频大股票池。 - 时区: 索引统一
tz_localize(None),避免按年月分组错位。 - 缓存键: 多标的路径
data/cache/close_<key>.parquet;单标的路径data/cache/close_symbol_<symbol>_<key>.parquet。 - 失效: 改日期或标的即新文件;手动删
data/cache/可强制重拉。 - 依赖: Parquet 需
pyarrow(已在pyproject.toml/requirements.txt)。
常用命令:
# 只使用本地文件
trading run QQQ,TQQQ --data-source local --local-data-dir data/local
# 通过 IBKR 拉日线并写入缓存
trading run QQQ,TQQQ --data-source ibkr
# 本地优先,然后 IBKR,再 fallback 到公开源
TRADING_IBKR_PORT=7497 trading run SPY,TLT,GLD --allocator momentum_rotation --signals SPY,TLT,GLD --data-source auto_ibkr --cash CASHequity_curve(result):portfolio.value(),Series。portfolio_metrics_row:输出含final_value、total_invested、total_return、CAGR、max_drawdown、volatility_annual、sharpe、sortino、calmar、起止日期与years;若portfolio.stats()可用,会附加Win Rate [%]等键(存在则写入)。portfolio_metrics_table:多场景纵向合并,index为scenario。compare_portfolios:baseline_key为None(默认)时,用infer_monthly_full_baseline_key选第一条monthly_full_*场景作为基准;若指定字符串且存在于表索引,则增加列vs_baseline_final_ratio(终值相对比减 1)。excess_equity_vs_baseline:两组合净值内连接日期后,strategy/baseline - 1。
扩展指标时建议:在 portfolio_metrics_row 中追加键,或新建函数接受 BacktestResult 保持与 viz 一致。
| 函数 | 作用 |
|---|---|
fig_equity_comparison |
多场景净值曲线 |
fig_drawdown |
相对历史新高的回撤 |
fig_excess_vs_baseline |
策略相对某一 baseline 净值比减 1 |
fig_yearly_weights_stacked |
年度权重堆叠柱状图 |
fig_monthly_returns_heatmap |
由日净值 resample M 得月收益再 pivot |
fig_rolling_sharpe |
默认 252 日滚动夏普 |
fig_summary_dashboard |
三行子图:净值、回撤、策略相对「默认 monthly_full_* baseline」超额(baseline_key=None 时同 infer_monthly_full_baseline_key) |
write_report_html |
多图拼单页 HTML,首图嵌入 Plotly CDN |
write_figure_image |
PNG,需安装 kaleido |
trading report 组装上述图表并写出 HTML、指标 CSV、成本影响 CSV 与可选 decision_snapshot.csv;--allocator ...、--data-source ... 控制行情来源,--png 尝试导出各图 PNG。
trading.reporting.package 会从 BacktestResult 生成 agent 中立报告包:manifest.json、agent_report_index.json、metrics.csv、equity_curve.csv、drawdown.csv、monthly_returns.csv,以及可选的 decision_snapshot.csv、yearly_weights.csv。其中 agent_report_index.json 是给自动 agent 的入口索引,声明推荐读取顺序、每个文件的角色和报告用途。trading.reporting.codex_adapter 再把这份报告包转成 Codex Data Analytics artifact 所需的 codex_manifest.json 与 codex_snapshot.json。这个分层让后续 Hermes agent 只需要新增自己的 adapter,而不必重写回测和指标逻辑。
CLI 可用 --format html|package|codex|all 控制输出:
trading report QQQ,TQQQ --allocator smart --vix ^VIX --format codex --package-dir reports/latest_package
trading report QQQ,TQQQ --allocator smart --vix ^VIX --format all当前对外导出(节选):BacktestResult、ScenarioContext、BaselineBuilder、DCAParams、run_dca_portfolio、run_scenarios、portfolio_from_orders、default_baseline_builders_v1、monthly_full_invest、lump_sum_first_day、equal_weight_monthly_on_strategy_universe、compare_portfolios、infer_monthly_full_baseline_key、portfolio_metrics_table、各分配器与 normalize_weights。
未在 __init__ 中导出但仍常用的:trading.metrics.excess_equity_vs_baseline、trading.viz 各图函数、trading.data.fetch_close_prices 等。
- 在
trading/strategies/dca.py(或新模块)实现符合WeightAllocator的函数或__call__方法。 - 传入
run_dca_portfolio(params, allocator=...)或run_scenarios(..., baseline_builders=[...]);新 baseline 实现BaselineBuilder协议,在trading/baseline_builders.py中可参考现有工厂。 - 若规则依赖月频或非自然年,需改
build_order_sizes的循环(当前按invest_date.year调用分配器),或在该分配器内自行用invest_date扩展协议(属 API 变更,建议同步改Protocol与所有内置分配器)。
实现新的 BaselineBuilder:闭包或类 __call__(ctx: ScenarioContext) -> BacktestResult,内部用 ctx.aligned_close / ctx.strategy_close、ctx.total_invested、ctx.monthly_budget 等构造 order_sizes,再调用 portfolio_from_orders;BacktestResult.name 需全局唯一(单次 run_scenarios 内)。若需新 ticker,提醒调用方加入 params.extra_symbols。
多个月度全仓基准时,用 compare_portfolios(..., baseline_key="monthly_full_SPY") 等显式指定超额对比基准。
实现与 fetch_close_prices 相同签名的函数,返回 DatetimeIndex + 多列 close 的 DataFrame,并在引擎中替换调用点;缓存逻辑可复制 _cache_key 策略或改为按文件路径读取(CSV/Parquet)。
当前仅 get_monthly_invest_dates;可新增 get_period_invest_dates(..., rule="W-MON") 等,并在 build_order_sizes 中注入周期参数(建议将周期提升为 DCAParams 字段,避免魔法常量)。
- 用短区间、已知缓存或 mock
close,对比「仅改 allocator」前后order_sizes非零行数与total_invested是否一致。 - 跑通
examples/dynamic_dca_vectorbt.py与trading report ... --format all作为冒烟测试。
以下示例假设已 pip install -e .,且在项目根目录启动 Python(或已将包安装到当前环境)。首次拉取行情需要网络。
from trading import DCAParams, fixed_weight_allocator, run_dca_portfolio
params = DCAParams(
symbols=("QQQ", "TQQQ"),
start="2018-01-01",
end="2024-12-31",
monthly_budget=3000.0,
default_weights={"QQQ": 0.7, "TQQQ": 0.3},
signal_symbols=("^IXIC",),
benchmark_symbol="QQQ",
use_cache=True,
)
result = run_dca_portfolio(params, allocator=fixed_weight_allocator)
equity = result.portfolio.value()
print("期末净值:", float(equity.iloc[-1]))
print("累计投入:", result.total_invested)
print(result.yearly_weights.tail())from trading import (
DCAParams,
compare_portfolios,
default_baseline_builders_v1,
fixed_weight_allocator,
nasdaq_rule_allocator,
run_scenarios,
)
params = DCAParams(
symbols=("QQQ", "TQQQ"),
start="2016-01-01",
end="2026-01-01",
monthly_budget=5000.0,
default_weights={"QQQ": 0.7, "TQQQ": 0.3},
benchmark_symbol="QQQ",
)
bl = default_baseline_builders_v1(params.benchmark_symbol)
results = run_scenarios(params, allocator=fixed_weight_allocator, baseline_builders=bl)
table = compare_portfolios(results)
print(table[["final_value", "CAGR", "max_drawdown", "sharpe", "vs_baseline_final_ratio"]])
# 换用纳指规则分配器再跑一轮
results_rule = run_scenarios(params, allocator=nasdaq_rule_allocator, baseline_builders=bl)
print(compare_portfolios(results_rule).loc["strategy"])from trading import DCAParams, fixed_weight_allocator, run_dca_portfolio
from trading.metrics import portfolio_metrics_row
params = DCAParams(
symbols=("QQQ", "TQQQ"),
start="2020-01-01",
end="2024-12-31",
monthly_budget=1000.0,
default_weights={"QQQ": 0.5, "TQQQ": 0.5},
)
result = run_dca_portfolio(params)
row = portfolio_metrics_row(result, risk_free_annual=0.03, trading_days_per_year=252)
print(row.to_string())from trading import DCAParams, default_baseline_builders_v1, fixed_weight_allocator, run_scenarios
from trading.metrics import excess_equity_vs_baseline
params = DCAParams(
symbols=("QQQ", "TQQQ"),
start="2018-01-01",
end="2024-12-31",
monthly_budget=2000.0,
default_weights={"QQQ": 0.6, "TQQQ": 0.4},
benchmark_symbol="QQQ",
)
results = run_scenarios(
params,
allocator=fixed_weight_allocator,
baseline_builders=default_baseline_builders_v1(params.benchmark_symbol),
)
ex = excess_equity_vs_baseline(results["strategy"], results["lump_sum_first_day"])
print(ex.tail())from trading.data import fetch_annual_returns, fetch_close_prices
close = fetch_close_prices(["QQQ", "SPY"], start="2020-01-01", end="2024-12-31", use_cache=True)
print(close.head())
ann = fetch_annual_returns("^IXIC", start="2015-01-01", end="2024-12-31")
print(ann)from trading import normalize_weights
w = normalize_weights({"QQQ": 7, "TQQQ": 3}) # 任意正比例
# {'QQQ': 0.7, 'TQQQ': 0.3}from trading import DCAParams, run_dca_portfolio
from trading.strategies.dca import normalize_weights
def conservative_allocator(invest_year, annual_returns, default_weights):
# 示例:无论信号如何,略提高 QQQ 权重(需与标的列名一致)
base = dict(default_weights)
if "QQQ" in base and "TQQQ" in base:
base["QQQ"] = base.get("QQQ", 0) + 0.1
base["TQQQ"] = max(base.get("TQQQ", 0) - 0.1, 0.01)
return normalize_weights(base)
params = DCAParams(
symbols=("QQQ", "TQQQ"),
start="2020-01-01",
end="2024-12-31",
monthly_budget=1000.0,
default_weights={"QQQ": 0.5, "TQQQ": 0.5},
)
result = run_dca_portfolio(params, allocator=conservative_allocator)from trading import DCAParams, default_baseline_builders_v1, fixed_weight_allocator, run_scenarios
from trading.viz import fig_drawdown, fig_equity_comparison, fig_summary_dashboard
params = DCAParams(
symbols=("QQQ", "TQQQ"),
start="2019-01-01",
end="2024-12-31",
monthly_budget=1000.0,
default_weights={"QQQ": 0.7, "TQQQ": 0.3},
benchmark_symbol="QQQ",
)
results = run_scenarios(
params,
allocator=fixed_weight_allocator,
baseline_builders=default_baseline_builders_v1(params.benchmark_symbol),
)
fig_equity_comparison(results).show()
fig_drawdown(results).show()
fig_summary_dashboard(results).show()from pathlib import Path
from trading import DCAParams, default_baseline_builders_v1, fixed_weight_allocator, run_scenarios
from trading.viz import fig_equity_comparison, fig_summary_dashboard, write_report_html
params = DCAParams(
symbols=("QQQ", "TQQQ"),
start="2020-01-01",
end="2024-12-31",
monthly_budget=500.0,
default_weights={"QQQ": 0.7, "TQQQ": 0.3},
benchmark_symbol="QQQ",
)
results = run_scenarios(
params,
allocator=fixed_weight_allocator,
baseline_builders=default_baseline_builders_v1(params.benchmark_symbol),
)
write_report_html(
[
("总览", fig_summary_dashboard(results)),
("净值对比", fig_equity_comparison(results)),
],
Path("reports/my_report.html"),
)from trading import DCAParams, run_dca_portfolio
result = run_dca_portfolio(DCAParams(
symbols=("QQQ",),
start="2021-01-01",
end="2024-12-31",
monthly_budget=500.0,
default_weights={"QQQ": 1.0},
))
stats = result.portfolio.stats()
print(stats)from trading import DCAParams, fixed_weight_allocator, run_scenarios
from trading.baseline_builders import (
default_baseline_builders_v1,
monthly_full_invest,
)
params = DCAParams(
symbols=("QQQ", "TQQQ", "VOO"),
extra_symbols=("SPY",),
start="2018-01-01",
end="2024-12-31",
monthly_budget=1000.0,
default_weights={"QQQ": 0.4, "TQQQ": 0.2, "VOO": 0.4},
benchmark_symbol="QQQ",
)
results = run_scenarios(
params,
allocator=fixed_weight_allocator,
baseline_builders=(
*default_baseline_builders_v1(params.benchmark_symbol),
monthly_full_invest("SPY"),
),
)
print(sorted(results.keys()))python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
pip install -e ".[dev]" # Jupyter、kaleido(PNG)、pytest
pip install -e ".[ibkr]" # 可选:IBKR/TWS 数据源不安装 editable 时,可将项目根加入 PYTHONPATH 后运行脚本。
python examples/dynamic_dca_vectorbt.py
python -m trading.cli report QQQ,TQQQ
python -m trading.cli report QQQ,TQQQ --allocator nasdaq_rule
python -m trading.cli report QQQ,TQQQ --fee-rate 0.001 --slippage-rate 0.0005
python -m trading.cli report QQQ,TQQQ --max-weight-per-asset 0.65 --max-gross-exposure 0.9
python -m trading.cli report QQQ,TQQQ --risk-observe-only
python -m trading.cli report QQQ,TQQQ --format codex
python -m trading.cli report QQQ,TQQQ --format all
python -m trading.cli report QQQ,TQQQ --png
python -m trading.cli experiment --presets
python -m trading.cli experiment --presets
python -m trading.cli experiment QQQ,TQQQ --allocator nasdaq_rule --weights QQQ=0.6,TQQQ=0.4Jupyter:打开 notebooks/backtest_explore.ipynb(工作目录建议为项目根,以便缓存与 reports 路径一致)。
面向个人研究建议使用以下流程:
- 先用
StrategySpec表达策略(手写 JSON、preset 或显式 CLI 参数)。 - 用
run_experiments批量跑同一区间同预算策略,避免单策略偏见。 - 用
rank_experiments先看策略行 (scenario == "strategy") 的 CAGR/Sharpe 排名。 - 再回看
trading report的图表和decision_snapshot.csv做可解释复盘。
这套流程的关键是把“策略描述”和“执行引擎”解耦,后续接入 Claude/Cursor 时,LLM 只负责生成 StrategySpec,而不是直接改回测代码。
当前项目已能复刻或近似追踪:
- 定投型组合: QQQ/TQQQ、60/40、全天候 ETF、单标的月投、等权月投。
- 规则型权重切换: 纳指上一年收益规则、VIX/回撤恐慌加仓、单资产权重上限。
- 现金过滤:
trend_follow_allocator可近似“低于 200 日均线则买现金/短债”的趋势过滤策略。 - 资产轮动:
momentum_rotation_allocator可近似年度动量轮动、双动量、ETF 相对强弱轮动。 - 再平衡差异:
rebalance_mode=sell支持卖出超重资产;tilt支持不卖出、只把新钱倾斜给低占比资产。
仍不能严格复刻或需要后续扩展:
- 需要真实外部现金流的策略: 当前仍用一次性
init_cash近似月度入金,严格 IRR/现金流统计需扩展。 - 日内/分钟级/高频策略: 数据层和订单模型按日线设计,IBKR pacing 也不适合大规模高频历史抓取。
- 期权、期货、保证金与借贷成本:
vectorbt.from_orders当前封装未覆盖期权 Greeks、期货展期、融资利率、强平等。 - 复杂税务与分红复投: 尚未建模税后收益、股息现金流、不同账户类型。
- 成分股历史与幸存者偏差: 本项目按给定 ticker 回测,尚不能自动复刻历史指数成分变动。
- 多周期信号组合: 已有日线信号快照,但周/月/季混合调仓、滚动窗口优化、walk-forward 参数学习仍需单独模块。
- 行情质量:公开源、IBKR 与本地文件的复权口径可能不同;生产研究请自行校验数据。
- 费用与滑点:当前支持固定费率/滑点;尚未覆盖阶梯费率、最小佣金、冲击成本等复杂成交模型。
- 基准在
symbols外:将此类 ticker 写入extra_symbols,_symbols_to_fetch为symbols ∪ extra_symbols ∪ signal_symbols;否则monthly_full_invest("SPY")等会缺列。 - IBKR 权限与限流:IBKR 需要 TWS/IB Gateway 在线和对应市场数据权限;历史数据有 pacing limit,适合作为缓存填充源,而不是每次无缓存全量重拉。
- gitignore:
data/cache/、reports/*.{html,csv,png}默认忽略;reports/.gitkeep用于保留空目录。
- Python:
>=3.10(见pyproject.toml)。 - 核心依赖: vectorbt、pandas、numpy、plotly、pyarrow;开发可选 jupyter、kaleido、pytest;IBKR 可选
ib_insync。
如有架构级变更(例如拆分 engine 或改变 BacktestResult 字段),建议在本节追加「变更日志」小节或单独维护 CHANGELOG.md(按需)。