Skip to content

Commit 9894881

Browse files
yingwangclaude
andcommitted
feat(backtest): honest ETF control universe + read-only Actions runner
Adds config_etf.yaml: the exact same multi-factor momentum engine (same signals/portfolio/risk/leverage params) pointed at a clean, survivorship-free basket of ~31 category-defined ETFs (sectors, size, intl, bonds, commodities). Purpose: measure how much real alpha survives once the hand-picked-winners stock universe is removed. Plus .github/workflows/honest-backtest.yml: manual-dispatch, read-only (no Alpaca, no secrets, no commits) job that runs the ETF backtest and prints strategy-vs-SPY to the job summary. Fully additive — does not change any existing strategy, config, state, account, dashboard, or workflow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 95b5f28 commit 9894881

2 files changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Honest Backtest (ETF control)
2+
3+
# Runs the EXACT same multi-factor momentum strategy on a clean, survivorship-free
4+
# ETF universe (config_etf.yaml) to measure how much real alpha survives once the
5+
# stock-universe cheating is removed. Manual trigger only. No Alpaca, no secrets,
6+
# no commits — pure read-only backtest. Result is printed to the job summary.
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
start:
12+
description: 'Backtest start date (YYYY-MM-DD)'
13+
required: false
14+
default: '2007-01-01'
15+
end:
16+
description: 'Backtest end date (YYYY-MM-DD, blank = today)'
17+
required: false
18+
default: ''
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
backtest:
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 20
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- uses: actions/setup-python@v5
31+
with:
32+
python-version: '3.11'
33+
cache: 'pip'
34+
35+
- run: pip install -r requirements.txt
36+
37+
- name: Run honest ETF backtest
38+
id: bt
39+
env:
40+
PYTHONPATH: .
41+
run: |
42+
END_ARG=""
43+
if [ -n "${{ github.event.inputs.end }}" ]; then
44+
END_ARG="--end ${{ github.event.inputs.end }}"
45+
fi
46+
set -o pipefail
47+
python run.py -c config_etf.yaml backtest \
48+
--start "${{ github.event.inputs.start }}" $END_ARG \
49+
| tee backtest_output.txt
50+
51+
- name: Publish result to job summary
52+
if: always()
53+
run: |
54+
{
55+
echo "# Honest Backtest — ETF control universe"
56+
echo ""
57+
echo "Same momentum engine as the main strategy, run on a clean ETF universe"
58+
echo "(\`config_etf.yaml\`). Window: **${{ github.event.inputs.start }} → ${{ github.event.inputs.end || 'today' }}**."
59+
echo ""
60+
echo '```'
61+
cat backtest_output.txt 2>/dev/null || echo "(no output captured)"
62+
echo '```'
63+
echo ""
64+
echo "> Read 'Total Return' vs 'Benchmark Return' — the gap is the honest alpha"
65+
echo "> (no survivorship bias, no look-ahead). Compare against the README's"
66+
echo "> stock-universe numbers, which ride a hand-picked winners list."
67+
} >> "$GITHUB_STEP_SUMMARY"

config_etf.yaml

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Quant Trading System Configuration — HONEST CONTROL (ETF universe)
2+
#
3+
# 目的:把现有多因子动量策略原样跑在一个"天然无幸存者偏差"的池子上,
4+
# 用来回答"去掉作弊后到底还剩多少真 alpha"。
5+
#
6+
# 与 config.yaml 的唯一区别:universe.symbols 换成一篮子流动性好、历史深、
7+
# 几乎不退市的 ETF(行业 + 规模 + 国际 + 债券 + 商品)。
8+
# signals / portfolio / risk / leverage / safety / backtest 全部与主策略保持一致,
9+
# 这样对比才公平 —— 同一套逻辑,只换池子。
10+
#
11+
# 为什么 ETF 能消除幸存者偏差:
12+
# - 这些是"按类别"发行的 ETF(科技/能源/债券/黄金…),不是按未来表现挑出来的赢家;
13+
# - ETF 极少退市,不存在"今天名单里全是赢家"的问题;
14+
# - 个别较新的 ETF(XLRE 2015、XLC 2018)上市前价格为 NaN,引擎会按 0 权重处理,
15+
# 不构成前视(上市本身与未来表现无关)。
16+
17+
universe:
18+
# ~31 liquid, deep-history, category-defined ETFs. No winner-picking.
19+
symbols:
20+
# Sector SPDRs (since 1998-12; XLRE 2015, XLC 2018)
21+
- XLK # technology
22+
- XLF # financials
23+
- XLE # energy
24+
- XLV # health care
25+
- XLY # consumer discretionary
26+
- XLP # consumer staples
27+
- XLI # industrials
28+
- XLB # materials
29+
- XLU # utilities
30+
- XLRE # real estate
31+
- XLC # communication services
32+
# Broad / size / style
33+
- QQQ # nasdaq 100
34+
- IWM # russell 2000 (small cap)
35+
- MDY # s&p midcap 400
36+
- DIA # dow 30
37+
# International
38+
- EFA # developed ex-US
39+
- EEM # emerging markets
40+
- VGK # europe
41+
- EWJ # japan
42+
# Fixed income
43+
- TLT # 20y+ treasuries
44+
- IEF # 7-10y treasuries
45+
- SHY # 1-3y treasuries
46+
- LQD # investment-grade credit
47+
- HYG # high-yield credit
48+
# Commodities / real assets
49+
- GLD # gold
50+
- SLV # silver
51+
- DBC # broad commodities
52+
- USO # crude oil
53+
# Other equity sleeves
54+
- VNQ # us reits
55+
- XBI # biotech
56+
- SMH # semiconductors
57+
benchmark: SPY
58+
59+
data:
60+
lookback_years: 5
61+
frequency: 1d # daily bars
62+
63+
signals:
64+
# IDENTICAL to config.yaml — same momentum logic, no changes.
65+
momentum_windows: [42, 126, 252]
66+
mean_reversion_window: 20
67+
mean_reversion_zscore_threshold: 4.0
68+
sma_short: 50
69+
sma_long: 200
70+
volatility_window: 63
71+
industry_neutral: true # no ETF sector map → falls back to cross-sectional z-score
72+
winsorize_clip: 3.0
73+
factor_weights:
74+
momentum: 0.50
75+
high_proximity: 0.20
76+
short_term_reversal: 0.10
77+
vol_contraction: 0.10
78+
volume_momentum: 0.10
79+
quality: 0.00
80+
volatility: 0.00
81+
value: 0.00
82+
mean_reversion: 0.00
83+
trend: 0.00
84+
85+
portfolio:
86+
# IDENTICAL to config.yaml — same concentration / vol target / rebalance / turnover.
87+
max_positions: 12
88+
max_position_weight: 0.12
89+
min_position_weight: 0.03
90+
target_volatility: 0.22
91+
rebalance_frequency_days: 21
92+
transaction_cost_bps: 10
93+
max_turnover_per_rebalance: 0.40
94+
95+
risk:
96+
# IDENTICAL to config.yaml.
97+
max_drawdown_limit: 0.25
98+
max_sector_weight: 0.50
99+
stop_loss_pct: 0.15
100+
101+
leverage:
102+
# IDENTICAL to config.yaml — same dynamic leverage so the comparison is fair.
103+
max_leverage: 2.0
104+
regime_spy_vol_window: 21
105+
regime_thresholds:
106+
low: 0.12
107+
high: 0.25
108+
regime_leverage_caps:
109+
low_vol: 1.8
110+
normal: 1.5
111+
high_vol: 0.8
112+
margin_annual_rate: 0.06
113+
114+
safety:
115+
# IDENTICAL to config.yaml.
116+
max_single_order_value: 50000
117+
max_single_order_shares: 10000
118+
max_daily_trade_value: 500000
119+
max_daily_loss: 25000
120+
max_adv_fraction: 0.01
121+
min_price: 1.0
122+
max_position_pct_of_portfolio: 0.20
123+
require_paper_mode: true
124+
125+
backtest:
126+
# Longer clean window than the stock config — includes the 2008 GFC and the
127+
# 2009 momentum crash, which are the honest stress tests for a momentum book.
128+
start_date: "2007-01-01"
129+
end_date: null # null = today
130+
initial_capital: 1000000
131+
slippage_bps: 5
132+
market_impact_coeff: 2.5

0 commit comments

Comments
 (0)