name: hfmd-quick-backtest description: Backtest a moving-average crossover (or adapt the template to another signal) on HF Market Data bars with an honest walk-forward (parameters chosen on a training window, evaluated on the next out-of-sample window), transaction costs, and a full metrics report. Use when the user wants to "test a strategy", "backtest", or compare signal parameters on a symbol.
hfmd-quick-backtest
A small, honest backtest template on top of v1 bars. It exists to give a defensible first answer (walk-forward, costs, benchmark, no look-ahead) — not to be a full trading framework.
When to use
- "Backtest a 50/200 MA crossover on SPY", "does a 20/100 cross work on CL?", "which MA pair worked best on BTC — and did it hold out of sample?"
- Any quick strategy sanity check on daily or intraday bars
Method (what the script does)
- Fetch bars (
/v1/bars/{asset}/{ticker}), keepclose, compute daily/bar returns. - Walk-forward: split time into consecutive blocks (
--test-years, default 1). For each test block, select the (fast, slow) pair with the best Sharpe over the preceding--train-years(default 3) of data from the--grid; apply that pair to the test block. Concatenate the out-of-sample test blocks → the reported equity curve. - Signal: long when
MA_fast > MA_slow, flat otherwise (--allow-shortfor ±1). Position is applied to the next bar (no look-ahead). - Costs:
--cost-bpsper side per change of position (default 5 bps). - Report: CAGR, annualised volatility, Sharpe, max drawdown, exposure, number of trades, turnover, vs buy-and-hold on the same out-of-sample span; per-block table of the chosen parameters and the block's Sharpe (this is where you see whether the choice was stable).
- Optional chart: equity vs benchmark + drawdown + the chosen pair over time.
Steps
python3 scripts/ma_crossover.py --asset etf --ticker SPY --start 2010-01-01 \
--grid 10,20,50 --grid-slow 100,150,200 --train-years 3 --test-years 1 --cost-bps 5 --plot spy_wf.pngThen explain: (a) the out-of-sample numbers only, (b) the stability of the selected parameters across blocks, (c) the in-sample vs out-of-sample gap (--show-insample prints the best in-sample pair on the full history for contrast), (d) what is not modelled (slippage beyond bps, borrow, dividends if UNADJUSTED, intraday fills).
Examples
# Crude oil vendor continuous (ratio-adjusted so returns are meaningful across rolls)
python3 scripts/ma_crossover.py --asset futures --ticker CL --adjustment contin_adj_ratio --start 2012-01-01 --allow-short
# Bitcoin, hourly bars, 6-month train / 2-month test
python3 scripts/ma_crossover.py --asset crypto --ticker BTCUSD --timeframe 1hour --start 2024-01-01 --train-years 0.5 --test-years 0.17 --grid 12,24,48 --grid-slow 96,168,336
# Single fixed pair, no optimisation (pure evaluation)
python3 scripts/ma_crossover.py --asset stock --ticker AAPL --fixed 50,200 --start 2015-01-01Adapting the template
signal_ma_cross(close, fast, slow) returns a position series in {0,1} (or {-1,0,1}). Replace it with any function of past data only; keep the .shift(1) when applying positions and keep the walk-forward loop untouched.
Gotchas
- For futures use
contin_adj_ratio(multiplicative back-adjustment):contin_UNadjhas roll jumps that fake returns;contin_adj_absolutecan go negative in long histories. - Intraday bars are US/Eastern and include only the sessions the vendor covers; annualisation uses bars/year inferred from the median bar spacing — check the printed
periods_per_year. - Keyless mode (low hourly limits, 30 req/h) is enough for daily history; hourly since 2010 needs a free API key (
HFMD_API_KEY). - A Sharpe above ~1.5 out-of-sample on a plain MA cross is a red flag for a bug or a tiny sample, not a discovery.