Skip to content

Backtest API: @mathieuc/tradingview/backtest (4.1.0-beta.1) - #352

Merged
Mathieu2301 merged 5 commits into
mainfrom
feat/backtest-module
Oct 23, 2026
Merged

Mathieu2301 merged 5 commits into
mainfrom
feat/backtest-module

Conversation

@Mathieu2301

@Mathieu2301 Mathieu2301 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Turns the honest-backtest example (#351) into a library entry point, @mathieuc/tradingview/backtest. The caller (a person or a coding agent) writes only the entry rule. The module downloads the candles, applies the whole protocol and returns a JSON report. Before this, a real agent spent about 9 of its 10 minutes writing an engine from scratch.

import { backtest, formatReport } from '@mathieuc/tradingview/backtest';

const { report, snapshot } = await backtest({
  symbol: 'BINANCE:BTCUSDT', timeframe: '240', years: 2,
  signal: (candles) => candles.flatMap((c, i) => (i > 0 && c.close > candles[i - 1].high ? [{ bar: i, direction: 'long' }] : [])),
});
console.log(formatReport(report));

No new dependency. Root and /data exports are unchanged; the only shared change is the new error code LOOK_AHEAD.

What is new compared with the example

  • Look-ahead check. The engine reruns the signal function on history truncated right after each signal (up to 100, seeded draw beyond that) and at 8 evenly spaced bars. If a signal only exists when later bars are present (backdated pivot, candles[i + 1]...) or disappears when they arrive (repaint), the run stops with TradingViewError LOOK_AHEAD and details lists every violation. The example relied on the author doing this right; the module enforces it.
  • The entry is always the next bar's open. A signal is { bar, direction, refs? } decided at the close of bar; there is no way to enter on the signal bar.
  • Plain JSON report: snapshot info (SHA-256), segments with dates, signals, rows per exit and split (metrics, baseline, percentile), trades with bar indices and times, causality summary and plain-language warnings. formatReport, formatTable and tradesToCsv render it. Signal refs (e.g. pivots) become <name>_time CSV columns.
  • loadSnapshot checks the SHA-256 on replay. Snapshots saved by the examples: honest backtest protocol on getCandles data聽#351 example still load.
  • Building blocks are exported for custom protocols: rsiDivergence, defaultExits, fixedExit, atrExit, atrTrailingExit (stop-only variants with targetR: null), sma/ema/rsi/atr/findPivots, simulate, randomBaseline, checkCausality...

The protocol rules themselves are unchanged: frozen snapshot with the forming bar dropped, warm-up never traded, chronological 70/30 IS/OOS split with no position across it, fee and adverse slippage on both sides (baseline included), stop and target in the same candle = loss, gap through the stop = open, four interchangeable exits, 1,000 seeded random-entry runs per exit and split (percentile with strictly lower, p5/p50/p95), and metrics per segment (net return, win rate, PF with null, max drawdown, trades).

examples/honest-backtest/index.js now calls the module with the same CLI options; engine.js and engine.d.ts are removed. New examples/backtest-custom-signal.js (an EMA cross in about 15 lines).

Verification

  • Parity with examples: honest backtest protocol on getCandles data聽#351: on the same BINANCE:BTCUSDT 4H snapshot (4,583 bars), the module and the examples: honest backtest protocol on getCandles data聽#351 engine give identical rows and 63 identical trades (every price, stop, target, reason and return). Node and Bun outputs are identical.
  • Tests: tests/unit/backtest.test.ts has 33 deterministic tests that replace the 15 tests of the example. Ten deliberate mutations each make at least one test fail. These are the 7 from examples: honest backtest protocol on getCandles data聽#351 (backdated signal, entry at the signal close, trailing look-ahead, missing exit fee, same-candle rule counted as a win, gap filled at the stop, position across the split) plus entry on the signal bar, look-ahead check bypassed and baseline without costs.
  • npm run check: pass (203 passed, 2 skipped; the packed-package smoke now imports /backtest under Node and Bun, with require() and a TypeScript consumer, and checks that LOOK_AHEAD fires).
  • npm run test:bun: 202 pass, 0 fail.
  • Coverage of src/backtest: 95% statements, 85% branches.
  • Live runs (public data, no account): node examples/honest-backtest/index.js (download and full protocol) and node examples/backtest-custom-signal.js on ETHUSDT 4H in 1.7 s, download included. runBacktest on 4,583 bars with 4 exits 脳 2 splits 脳 1,000 runs takes about 0.2 s.

Docs

docs/backtest.md (signal contract, look-ahead check, options, exits, protocol, report fields, how to read results). Also updated: the backtest section and error table of docs/data-api.md, llms.txt (an agent should write only the rule), README in EN/FR/ES/PT, examples/README.md and docs/release-4.1.0-beta.1.md. The docs make no profitability claim and say that a negative or inconclusive result is valid.

Report format frozen for pages (4.1.0-beta.2)

So that a page template can be built on it, the report now holds identifiers and numbers only. Warnings are codes (formatWarning gives the English sentence), exitLabel is removed from rows, and settings.signal / settings.exits carry id, kind and params. New fields: createdAt, library, snapshot.fetchedAt, buyHold per segment, and refBars on signals and trades. New helpers: chartData(report, snapshot, signal) (candles, indicator series, split times, signals, trades by exit) and backtestMany({ markets, exits, signal }), which takes exits per market and adds a series column to the CSV. Results are unchanged: the same rows and trades as the #351 engine.

Release

4.1.0-beta.1 and then 4.1.0-beta.2 are published under the npm dist-tag beta. latest stays on 4.0.1 for now.

Turn the honest-backtest example (#351) into a library module: the caller
writes only the entry rule, the module applies the protocol and returns a
JSON report.

- backtest() downloads candles once and freezes them; runBacktest() runs
  synchronously on candles or a saved snapshot.
- Signals are { bar, direction, refs? } decided at the close of bar; the
  entry is always the next bar's open.
- New look-ahead check: the signal is rerun on truncated history and the
  run stops with the new LOOK_AHEAD error code when a signal depends on
  later bars or repaints.
- Same rules as the example: costs on both sides, same-candle stop and
  target as a loss, gap fills at the open, warm-up, IS/OOS split without
  open positions across it, four exits, seeded random-entry baseline.
- examples/honest-backtest uses the module (identical report on the same
  snapshot); new examples/backtest-custom-signal.js.
- docs/backtest.md, data API guide, llms.txt, READMEs, release notes.
- Report holds identifiers and numbers only: warnings become codes
  (formatWarning gives the English sentence), rows drop exitLabel,
  settings.signal and settings.exits carry id, kind and params.
- Add createdAt, library, snapshot.fetchedAt, buyHold per segment and
  refBars on signals and trades.
- Add chartData() for price charts and backtestMany() for several
  markets, with exits per market and a series column in the CSV.
- rsiDivergence exposes id, params, indicators and a trendFilter option.
@Mathieu2301
Mathieu2301 merged commit 86dc3c3 into main Oct 23, 2026
12 checks passed
@Mathieu2301 Mathieu2301 mentioned this pull request Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant