SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

# 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)

  1. Fetch bars (/v1/bars/{asset}/{ticker}), keep close, compute daily/bar returns.
  2. 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.
  3. Signal: long when MA_fast > MA_slow, flat otherwise (--allow-short for ±1). Position is applied to the next bar (no look-ahead).
  4. Costs: --cost-bps per side per change of position (default 5 bps).
  5. 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).
  6. Optional chart: equity vs benchmark + drawdown + the chosen pair over time.

# Steps

bash
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.png

Then 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

bash
# 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-01

# Adapting 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_UNadj has roll jumps that fake returns; contin_adj_absolute can 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.