Backtest runner
@qtrader/backtest-runner
Backtest a trading idea on an OHLCV CSV with honest assumptions: next-bar fills, slippage, commission, no look-ahead. Built-in SMA crossover, RSI mean reversion, and buy-and-hold, plus a one-file plug-in format for your own strategies. Writes equity curve and trades CSVs and metrics (CAGR, volatility, Sharpe, Sortino, max drawdown, win rate, exposure). Not financial advice.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
What it makes
Real output from this kit
- Sample file16 KB, download to openSma equityFrom the kit's tests
- Sample file917 bytes, download to openSma metricsFrom the kit's tests
- Sample file536 bytes, download to openSma tradesFrom the kit's tests
How this works
- It is a skill: written instructions Claude reads when your request matches what the skill is for. You do not call it by name; just ask for the task.
- While it works, it reads and changes files in the places it lists, nowhere else.
- It runs these commands on your computer: node scripts/backtest.mjs, node scripts/selftest.mjs.
- Nothing runs until you choose to use it, and you can remove it any time.
SKILL.md
Backtest runner
An event-driven backtester in one Node script. For each bar: fill yesterday's order at today's open (plus slippage and commission), mark equity at today's close, then ask the strategy for a new target using only bars up to today. That ordering is what keeps it honest.
Backtests describe the past under simplified assumptions. Say so every time you report results, and never present a backtest as a prediction or as financial advice.
Inputs
- Prices: a CSV with
date(ortimestamp),open,high,low,close, optionalvolume, one row per bar, one symbol. Thealpaca-connectorkit writes this format (data/SPY-1Day.csv). Use split and dividend adjusted prices, or splits will look like crashes. - Strategy: a built-in name or a plug-in file (below).
- Costs: ask if the user has not said. Defaults: slippage 5 bps per side, commission 0, integer shares, long only, starting capital 100,000.
Steps
- Check the data. Row count, date range, gaps, and that prices look adjusted. The script rejects bad rows (missing columns, high below close, duplicate dates) with the line number.
Run the baseline first so every result has a comparison:
node scripts/backtest.mjs --data data/SPY-1Day.csv --strategy buy-hold --out out/buy-holdRun the strategy.
node scripts/backtest.mjs --data data/SPY-1Day.csv --strategy sma-cross --param fast=20 --param slow=50 --slippage-bps 5 --commission-per-share 0.005 --out out/sma-20-50 node scripts/backtest.mjs --data data/SPY-1Day.csv --strategy rsi-revert --param period=14 --param entry=30 --param exit=50 --out out/rsi node scripts/backtest.mjs --data data/SPY-1Day.csv --strategy-file strategies/my-idea.mjs --out out/my-ideaOther flags:
--capital,--commission-bps,--fractional,--allow-short,--periods-per-year(252 daily, 52 weekly, about 98280 for 1-minute regular hours),--rf(annual risk-free rate for Sharpe and Sortino, default 0).- Read the outputs:
<out>-metrics.json,<out>-trades.csv,<out>-equity.csv. - Report with the template below, then point out the caveats that apply (next section).
- If the user wants to tune parameters, run a small grid and show the whole table, not just the best row. Warn that picking the best of many runs overfits; suggest checking the winner on a later, untouched date range.
Built-in strategies
| Name | Params (defaults) | Rule |
|---|---|---|
buy-hold | none | Long from the first possible fill to the end. |
sma-cross | fast=20, slow=50 | Long when SMA(fast) > SMA(slow), else flat. |
rsi-revert | period=14, entry=30, exit=50 | Wilder RSI. Buy when RSI < entry; sell when RSI > exit; otherwise hold. |
Writing a plug-in strategy
Save to strategies/<name>.mjs. See examples/strategies/donchian-breakout.mjs for a full example.
export default {
name: "my-idea",
params: { lookback: 20 }, // defaults; override with --param lookback=50
onBar({ i, bar, bars, closes, params, state, position, ind }) {
const avg = ind.sma(closes, params.lookback); // uses bars 0..i only
if (avg === null) return 0; // not enough history yet
return bar.close > avg ? 1 : 0; // 1 long, 0 flat, -1 short, null keep
},
};bars and closes end at today; reading bars[i + 1] gives undefined. Unknown --param names are rejected, so typos fail loudly.
Report template
# Backtest: <strategy> <params> on <symbol>, <start> to <end>
| Metric | Strategy | Buy and hold |
| --- | ---: | ---: |
| Total return | 0.37% | 9.36% |
| CAGR | 0.29% | 7.32% |
| Volatility (ann.) | 11.18% | 17.05% |
| Sharpe | 0.08 | 0.50 |
| Sortino | 0.11 | 0.70 |
| Max drawdown | -16.60% | -22.40% |
| Trades / win rate | 5 / 40% | 0 |
| Exposure | 42.5% | 99.7% |
Assumptions: next-open fills, 5 bps slippage, $0 commission, integer shares, long only.
What stands out: <2 or 3 plain sentences>
Not financial advice. Past results do not predict future returns.Caveats to call out
- Fewer than about 30 trades: the win rate and Sharpe are noisy. Say so.
- One symbol and one period: no survivorship check, no regime variety.
- No dividends unless prices are adjusted. No borrow costs for shorts. No partial fills, gaps through stops, or market impact beyond the slippage setting.
- Signals are computed on close and filled next open, so overnight gaps hit the fill price. That is realistic, not a bug.
Metric definitions
Per-bar returns r from the close-to-close equity curve. Volatility = sample stdev(r) x sqrt(periods per year). Sharpe = (mean(r) - rf per bar) / stdev(r) x sqrt(ppy). Sortino uses downside deviation sqrt(sum(min(0, r - rf per bar)^2) / N) over all N bars. CAGR = (1 + total)^(ppy / N) - 1. Max drawdown is the worst fall from a running equity peak. Win rate counts closed trades with pnl > 0 after costs. Exposure is the share of bars ending with a position.
Guardrails
- Never fetch prices yourself in this kit; use the user's CSV or the
alpaca-connectorkit. - Never claim a strategy "works" or recommend trading it. Report numbers and caveats.
- Plug-in files are code the user owns. Only run strategy files the user wrote or asked you to write in
strategies/.
README.md
backtest-runner
Test a trading idea on historical prices before you risk anything. Give it an OHLCV CSV and a strategy (SMA crossover, RSI mean reversion, buy-and-hold, or your own one-file plug-in) and it writes an equity curve, a trade list, and the metrics people actually compare: CAGR, volatility, Sharpe, Sortino, max drawdown, win rate, and exposure.
Who it is for: people learning systematic trading who want honest numbers, not a curve that only works because it peeked at tomorrow.
Not financial advice. Backtests describe the past under simplified assumptions.
How it stays honest
- Decisions use bars up to today only. Strategies get a read-only view where tomorrow reads as
undefined. - Orders fill at the next bar's open, never at the close that produced the signal.
- Slippage (default 5 bps per side) and commission (per share and/or bps) are charged on every fill.
- Integer shares by default, long only unless you pass
--allow-short. - A signal on the last bar is never filled. Open trades at the end are reported as open, not counted as wins.
Example
node scripts/backtest.mjs --data examples/prices.csv --strategy sma-cross --param fast=10 --param slow=30sma-cross {"fast":10,"slow":30} on examples/prices.csv, 2024-01-02 to 2025-03-24 (320 bars)
Total 0.37% | CAGR 0.29% | Vol 11.18% | Sharpe 0.08 | Sortino 0.11 | Max DD -16.60%
Trades 5 (+0 open) | Win rate 40.00% | Exposure 42.50% | Buy and hold 9.38%examples/prices.csv is a made-up series (a trend plus cycles plus seeded noise), not real market data. Files written: out/backtest-equity.csv, out/backtest-trades.csv, out/backtest-metrics.json.
Your own strategy is one file. See examples/strategies/donchian-breakout.mjs:
node scripts/backtest.mjs --data data/SPY-1Day.csv --strategy-file strategies/my-idea.mjs --param lookback=50Hand check (verify it yourself)
Costs and fills. Bars (open, close): d0 (100, 100), d1 (101, 104), d2 (105, 103), d3 (102, 106), d4 (107, 108). Capital 10,000, slippage 10 bps, commission $0.01 per share. Signal long on d0's close, flat on d2's close.
- Buy on d1 open: 101 x 1.001 = 101.101. Shares = floor(10,000 / (101.101 + 0.01)) = 98. Cash = 10,000 - 98 x 101.101 - 0.98 = 91.122.
- Equity d1 = 91.122 + 98 x 104 = 10,283.122. Equity d2 = 91.122 + 98 x 103 = 10,185.122.
- Sell on d3 open: 102 x 0.999 = 101.898. Cash = 91.122 + 98 x 101.898 - 0.98 = 10,076.146.
- Trade pnl = +76.146 after costs. Exposure = 2 of 5 bars = 40%.
Metrics. Buy and hold on closes 100, 100, 110, 121, 108.9, 119.79 (open = close), 10,000 capital, fractional, no costs. The signal on bar 0 fills at bar 1's open (100), so equity is 10,000, 10,000, 11,000, 12,100, 10,890, 11,979.
- Returns: 0, 0.10, 0.10, -0.10, 0.10. Mean 0.04. Sample variance 0.032 / 4 = 0.008.
- Sharpe = 0.04 / sqrt(0.008) x sqrt(252) = 7.0993. Sortino = 0.04 / sqrt(0.01 / 5) x sqrt(252) = 14.1986.
- Max drawdown = 10,890 / 12,100 - 1 = -10%. Total return 19.79%. Exposure 5 of 6 bars.
SMA cross. fast 2, slow 3 on closes 10, 9, 8, 9, 10, 11, 10, 9, 8. On bar 4, SMA2 = 9.5 > SMA3 = 9, so buy on bar 5's open (11). On bar 7, SMA2 = 9.5 < SMA3 = 10, so sell on bar 8's open (8).
node scripts/selftest.mjs checks all of these, plus Wilder RSI by hand, a short sale, a long-to-short flip, and that strategies cannot see or change future bars.
What it can access
- Reads your price CSV from
data/and strategy files fromstrategies/. Writes results toout/and new strategy files tostrategies/. - Runs only its own scripts with Node. No network, no API keys.
Setup
Node 22 or newer. Nothing to install. Get prices from any source as CSV, or use the alpaca-connector kit: node scripts/alpaca.mjs bars --symbol SPY --start 2021-01-01 --out data/SPY-1Day.csv.
Files
- README.md
- SKILL.md
- examples/prices.csv
- examples/strategies/donchian-breakout.mjs
- kit.json
- scripts/backtest.mjs
- scripts/selftest.mjs
Did it work?
0worked for them0did not work
Sign in to say whether it worked for you.
Nobody has said yet. Tried it? Tell the next person.
