backtest-kit-docs / index.html
tripolskypetr's picture
patch
76d5628
Raw
History Blame Contribute Delete
172 kB
<!DOCTYPE html><html class="default" lang="en" data-base="."><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>backtest-kit</title><meta name="description" content="Documentation for backtest-kit"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><script defer src="assets/main.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script><script async src="assets/hierarchy.js" id="tsd-hierarchy-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search"><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="index.html" class="title">backtest-kit</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><h1>backtest-kit</h1></div><div class="tsd-panel tsd-typography"><img src="https://github.com/tripolskypetr/backtest-kit/raw/refs/heads/master/assets/consciousness.svg" height="45px" align="right">
<a id="🧿-backtest-kit" class="tsd-anchor"></a><h1 class="tsd-anchor-link">🧿 Backtest Kit<a href="#🧿-backtest-kit" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h1><blockquote>
<p>A TypeScript engine for backtesting <strong>and</strong> live-trading strategies — crypto, forex, DEX, spot or futures — where the code you test is the code you ship. See <a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example">reference implementation</a></p>
</blockquote>
<p><img src="https://raw.githubusercontent.com/tripolskypetr/backtest-kit/HEAD/assets/screenshots/screenshot16.png" alt="screenshot"></p>
<p><a href="https://deepwiki.com/tripolskypetr/backtest-kit"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
<a href="https://npmjs.org/package/backtest-kit"><img src="https://img.shields.io/npm/v/backtest-kit.svg?style=flat-square" alt="npm"></a>
<a href=""><img src="https://img.shields.io/badge/TypeScript-5.0+-blue" alt="TypeScript"></a>
<a href="https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml"><img src="https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml/badge.svg" alt="Build"></a></p>
<p>Most trading bots don't die because the strategy was wrong. They die because the backtest quietly read tomorrow's candle, because the process crashed mid-fill and opened the position twice, because the exchange rejected an order and the bot kept trading a ghost. The strategy was never the hard part — the <em>infrastructure</em> was.</p>
<p><code>backtest-kit</code> is that infrastructure, closed off one failure at a time over a year of live trading and running real money in production at <a href="https://theonetrade.github.io">TheOneTrade</a>. This page walks the failures that kill bots and shows how each one is designed out of the default path — not &quot;discouraged,&quot; not &quot;documented,&quot; but structurally unavailable unless you go out of your way to defeat the engine. Every claim opens into <strong>The Code / The Math / The Proof</strong> so you (or the model reading this for you) can check the work instead of trusting the pitch.</p>
<p>📚 <strong><a href="https://backtest-kit.github.io/documents/example_02_first_backtest.html">API Reference</a></strong> · 🌟 <strong><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example">Reference implementation</a></strong> · 📰 <strong><a href="https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html">Article series</a></strong></p>
<hr>
<a id="start-here" class="tsd-anchor"></a><h2 class="tsd-anchor-link">Start here<a href="#start-here" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>Three on-ramps, one engine. Casual keeps the boilerplate inside the CLI; Sidekick ejects every wire into your repo; Docker gives you a restart-safe box.</p>
<details>
<summary>The Code</summary>
<pre><code class="bash"><span class="hl-0"># Casual — your repo holds only strategy files; docs auto-fetched into docs/lib/</span><br/><span class="hl-1">npx</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/cli</span><span class="hl-2"> </span><span class="hl-4">--init</span><span class="hl-2"> </span><span class="hl-4">--output</span><span class="hl-2"> </span><span class="hl-3">backtest-kit-project</span><br/><span class="hl-1">cd</span><span class="hl-2"> </span><span class="hl-3">backtest-kit-project</span><span class="hl-2"> &amp;&amp; </span><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> &amp;&amp; </span><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">start</span><br/><br/><span class="hl-0"># Full control — exchange/frames/risk/runner all editable in your project</span><br/><span class="hl-1">npx</span><span class="hl-2"> </span><span class="hl-4">-y</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/sidekick</span><span class="hl-2"> </span><span class="hl-3">my-trading-bot</span><span class="hl-2"> &amp;&amp; </span><span class="hl-1">cd</span><span class="hl-2"> </span><span class="hl-3">my-trading-bot</span><span class="hl-2"> &amp;&amp; </span><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">start</span><br/><br/><span class="hl-0"># Docker — zero-downtime live trading</span><br/><span class="hl-1">npx</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/cli</span><span class="hl-2"> </span><span class="hl-4">--docker</span><span class="hl-2"> &amp;&amp; </span><span class="hl-1">cd</span><span class="hl-2"> </span><span class="hl-3">backtest-kit-docker</span><br/><span class="hl-5">MODE</span><span class="hl-2">=</span><span class="hl-3">live</span><span class="hl-2"> </span><span class="hl-5">SYMBOL</span><span class="hl-2">=</span><span class="hl-3">TRXUSDT</span><span class="hl-2"> </span><span class="hl-5">STRATEGY_FILE</span><span class="hl-2">=</span><span class="hl-3">./content/feb_2026/feb_2026.strategy.ts</span><span class="hl-2"> </span><span class="hl-1">docker-compose</span><span class="hl-2"> </span><span class="hl-3">up</span><span class="hl-2"> </span><span class="hl-4">-d</span>
</code><button type="button">Copy</button></pre>
<p>A whole strategy is three registrations and a run call. No bootstrap, no DI container to learn:</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> </span><span class="hl-5">ccxt</span><span class="hl-2"> </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;ccxt&#39;</span><span class="hl-2">;</span><br/><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">addExchangeSchema</span><span class="hl-2">, </span><span class="hl-5">addStrategySchema</span><span class="hl-2">, </span><span class="hl-5">addFrameSchema</span><span class="hl-2">, </span><span class="hl-5">Position</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">Backtest</span><span class="hl-2">, </span><span class="hl-5">listenSignalBacktest</span><span class="hl-2">, </span><span class="hl-5">listenDoneBacktest</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;backtest-kit&#39;</span><span class="hl-2">;</span><br/><br/><span class="hl-1">addExchangeSchema</span><span class="hl-2">({</span><br/><span class="hl-2"> </span><span class="hl-5">exchangeName:</span><span class="hl-2"> </span><span class="hl-3">&#39;binance&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">interval</span><span class="hl-2">, </span><span class="hl-5">since</span><span class="hl-2">, </span><span class="hl-5">limit</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">ex</span><span class="hl-2"> = </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-5">ccxt</span><span class="hl-2">.</span><span class="hl-1">binance</span><span class="hl-2">();</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">ohlcv</span><span class="hl-2"> = </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">ex</span><span class="hl-2">.</span><span class="hl-1">fetchOHLCV</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">interval</span><span class="hl-2">, </span><span class="hl-5">since</span><span class="hl-2">.</span><span class="hl-1">getTime</span><span class="hl-2">(), </span><span class="hl-5">limit</span><span class="hl-2">);</span><br/><span class="hl-2"> </span><span class="hl-6">return</span><span class="hl-2"> </span><span class="hl-5">ohlcv</span><span class="hl-2">.</span><span class="hl-1">map</span><span class="hl-2">(([</span><span class="hl-5">timestamp</span><span class="hl-2">, </span><span class="hl-5">open</span><span class="hl-2">, </span><span class="hl-5">high</span><span class="hl-2">, </span><span class="hl-5">low</span><span class="hl-2">, </span><span class="hl-5">close</span><span class="hl-2">, </span><span class="hl-5">volume</span><span class="hl-2">]) </span><span class="hl-4">=&gt;</span><br/><span class="hl-2"> ({ </span><span class="hl-5">timestamp</span><span class="hl-2">, </span><span class="hl-5">open</span><span class="hl-2">, </span><span class="hl-5">high</span><span class="hl-2">, </span><span class="hl-5">low</span><span class="hl-2">, </span><span class="hl-5">close</span><span class="hl-2">, </span><span class="hl-5">volume</span><span class="hl-2"> }));</span><br/><span class="hl-2"> },</span><br/><span class="hl-2"> </span><span class="hl-1">formatPrice</span><span class="hl-5">:</span><span class="hl-2"> (</span><span class="hl-5">s</span><span class="hl-2">, </span><span class="hl-5">p</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> </span><span class="hl-5">p</span><span class="hl-2">.</span><span class="hl-1">toFixed</span><span class="hl-2">(</span><span class="hl-8">2</span><span class="hl-2">), </span><span class="hl-1">formatQuantity</span><span class="hl-5">:</span><span class="hl-2"> (</span><span class="hl-5">s</span><span class="hl-2">, </span><span class="hl-5">q</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> </span><span class="hl-5">q</span><span class="hl-2">.</span><span class="hl-1">toFixed</span><span class="hl-2">(</span><span class="hl-8">8</span><span class="hl-2">),</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-1">addFrameSchema</span><span class="hl-2">({ </span><span class="hl-5">frameName:</span><span class="hl-2"> </span><span class="hl-3">&#39;feb-2026&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;1m&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">startDate:</span><span class="hl-2"> </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-1">Date</span><span class="hl-2">(</span><span class="hl-3">&#39;2026-02-01&#39;</span><span class="hl-2">), </span><span class="hl-5">endDate:</span><span class="hl-2"> </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-1">Date</span><span class="hl-2">(</span><span class="hl-3">&#39;2026-02-28&#39;</span><span class="hl-2">) });</span><br/><br/><span class="hl-1">addStrategySchema</span><span class="hl-2">({</span><br/><span class="hl-2"> </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">&#39;my-strategy&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;15m&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-1">getSignal</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">when</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> ({</span><br/><span class="hl-2"> </span><span class="hl-5">position:</span><span class="hl-2"> </span><span class="hl-3">&#39;long&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> ...</span><span class="hl-5">Position</span><span class="hl-2">.</span><span class="hl-1">bracket</span><span class="hl-2">({ </span><span class="hl-5">position:</span><span class="hl-2"> </span><span class="hl-3">&#39;long&#39;</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">, </span><span class="hl-5">percentTakeProfit:</span><span class="hl-2"> </span><span class="hl-8">2</span><span class="hl-2">, </span><span class="hl-5">percentStopLoss:</span><span class="hl-2"> </span><span class="hl-8">1</span><span class="hl-2"> }),</span><br/><span class="hl-2"> </span><span class="hl-5">minuteEstimatedTime:</span><span class="hl-2"> </span><span class="hl-8">60</span><span class="hl-2"> * </span><span class="hl-8">24</span><span class="hl-2">, </span><span class="hl-5">cost:</span><span class="hl-2"> </span><span class="hl-8">100</span><span class="hl-2">,</span><br/><span class="hl-2"> }),</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-5">Backtest</span><span class="hl-2">.</span><span class="hl-1">background</span><span class="hl-2">(</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">, { </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">&#39;my-strategy&#39;</span><span class="hl-2">, </span><span class="hl-5">exchangeName:</span><span class="hl-2"> </span><span class="hl-3">&#39;binance&#39;</span><span class="hl-2">, </span><span class="hl-5">frameName:</span><span class="hl-2"> </span><span class="hl-3">&#39;feb-2026&#39;</span><span class="hl-2"> });</span><br/><span class="hl-1">listenSignalBacktest</span><span class="hl-2">(</span><span class="hl-5">console</span><span class="hl-2">.</span><span class="hl-5">log</span><span class="hl-2">);</span><br/><span class="hl-1">listenDoneBacktest</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">e</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">Backtest</span><span class="hl-2">.</span><span class="hl-1">dump</span><span class="hl-2">(</span><span class="hl-5">e</span><span class="hl-2">.</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">e</span><span class="hl-2">.</span><span class="hl-5">strategyName</span><span class="hl-2">); });</span>
</code><button type="button">Copy</button></pre>
</details>
<hr>
<a id="the-rakes--and-where-they-went" class="tsd-anchor"></a><h2 class="tsd-anchor-link">The rakes — and where they went<a href="#the-rakes--and-where-they-went" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>What follows isn't a feature list. It's the set of mistakes that quietly drain accounts, each one paired with the design decision that took it off the table. If you've shipped a bot before, you've stepped on at least three of these.</p>
<a id="1-your-backtest-lied-to-you-and-youll-only-find-out-with-real-money" class="tsd-anchor"></a><h3 class="tsd-anchor-link">1. Your backtest lied to you, and you'll only find out with real money<a href="#1-your-backtest-lied-to-you-and-youll-only-find-out-with-real-money" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Look-ahead bias is the assassin of algo trading: a single line that touches a future candle, an indicator loaded without a timestamp filter, one forgotten <code>&lt;=</code>. The backtest prints a beautiful equity curve that can <em>never</em> be reproduced live, and you deploy straight into a drawdown.</p>
<p>The usual defense is &quot;be careful.&quot; Careful doesn't survive a 2,000-line strategy or a refactor at 1 a.m. So the cure here isn't discipline — it's removal of the failure surface. There is no timestamp parameter to forget. An ambient temporal context flows through every async call via Node's <code>AsyncLocalStorage</code>, and the data layer physically refuses to hand you a candle past &quot;now.&quot; The pending (still-forming) candle is never returned, because its half-finished OHLC would poison every indicator.</p>
<p>The one rule this rests on: that context is live for the whole <code>await</code> chain of your <code>getSignal</code> and every <code>listen*</code> callback — including across <code>Promise.all</code>, which is where strategy code actually runs. It is not sorcery over execution you deliberately detach from that chain. A bare timer, an <code>EventEmitter</code>, a forked process, or the web dashboard reads engine state by <strong>identifier</strong> (signal id / symbol), not by inheriting the ambient clock — that explicit, id-based interop is exactly how the frontend talks to a running backtest. Inside the hooks the guarantee holds; step outside them on purpose and you address the engine deliberately rather than by accident.</p>
<details>
<summary>The Math</summary>
<p>Every request resolves &quot;now&quot; from the ambient context, aligns down to the interval boundary, and treats the pending candle as exclusive:</p>
<pre><code><span class="hl-5">when</span><span class="hl-2"> = </span><span class="hl-5">current</span><span class="hl-2"> </span><span class="hl-5">execution</span><span class="hl-2">-</span><span class="hl-5">context</span><span class="hl-2"> </span><span class="hl-1">time</span><span class="hl-2"> (</span><span class="hl-5">AsyncLocalStorage</span><span class="hl-2">)</span><br/><span class="hl-5">stepMs</span><span class="hl-2"> = </span><span class="hl-5">interval</span><span class="hl-2"> </span><span class="hl-1">duration</span><span class="hl-2"> (1</span><span class="hl-5">m</span><span class="hl-2"></span><span class="hl-8">60000</span><span class="hl-2">)</span><br/><span class="hl-5">alignedWhen</span><span class="hl-2"> = </span><span class="hl-5">Math</span><span class="hl-2">.</span><span class="hl-1">floor</span><span class="hl-2">(</span><span class="hl-5">when</span><span class="hl-2"> / </span><span class="hl-5">stepMs</span><span class="hl-2">) * </span><span class="hl-5">stepMs</span><span class="hl-2"> </span><span class="hl-0">// round down to boundary</span><br/><span class="hl-5">since</span><span class="hl-2"> = </span><span class="hl-5">alignedWhen</span><span class="hl-2"></span><span class="hl-5">limit</span><span class="hl-2"> * </span><span class="hl-5">stepMs</span><span class="hl-2"> </span><span class="hl-0">// go back `limit` candles</span>
</code><button>Copy</button></pre>
<ul>
<li><code>since</code> is <strong>inclusive</strong> — first candle has <code>timestamp === since</code>.</li>
<li><code>alignedWhen</code> is <strong>exclusive</strong> — the candle covering <code>[alignedWhen, alignedWhen+stepMs)</code> is still open and is never returned.</li>
<li>Range is the half-open <code>[since, alignedWhen)</code>; exactly <code>limit</code> candles return; timestamps are <code>since + i·stepMs</code>.</li>
</ul>
<p><code>getNextCandles()</code> is backtest-only and <strong>throws in live mode</strong> — there is no future to look at when &quot;now&quot; is wall-clock. <code>getRawCandles(limit?, sDate?, eDate?)</code> supports flexible windows, all clamped to <code>eDate ≤ when</code>. Order books and aggregated trades use the same alignment (trades always to a 1-minute boundary). All boundaries are <strong>UTC</strong>: a 4h candle aligns to <code>00/04/08/12/16/20 UTC</code> regardless of your local offset — so <code>since</code> values that look &quot;uneven&quot; in local time are exact in UTC. Because <code>since</code> is derived from the ambient <code>when</code>, multi-timeframe pulls inside one <code>getSignal</code> are automatically synchronized, and runtime and the persistent cache compute identical keys — deterministic, exact-timestamp retrieval.</p>
</details>
<details>
<summary>The Code</summary>
<pre><code class="typescript"><span class="hl-9">getSignal</span><span class="hl-2">: </span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-0">// No timestamps anywhere. Context flows even through Promise.all —</span><br/><span class="hl-2"> </span><span class="hl-0">// all four timeframes are pinned to the same tick automatically.</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> [</span><span class="hl-7">c1h</span><span class="hl-2">, </span><span class="hl-7">c15m</span><span class="hl-2">, </span><span class="hl-7">c5m</span><span class="hl-2">, </span><span class="hl-7">c1m</span><span class="hl-2">] = </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-10">Promise</span><span class="hl-2">.</span><span class="hl-1">all</span><span class="hl-2">([</span><br/><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;1h&#39;</span><span class="hl-2">, </span><span class="hl-8">24</span><span class="hl-2">),</span><br/><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;15m&#39;</span><span class="hl-2">, </span><span class="hl-8">48</span><span class="hl-2">),</span><br/><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;5m&#39;</span><span class="hl-2">, </span><span class="hl-8">60</span><span class="hl-2">),</span><br/><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;1m&#39;</span><span class="hl-2">, </span><span class="hl-8">60</span><span class="hl-2">),</span><br/><span class="hl-2"> ]);</span><br/><span class="hl-2">}</span>
</code><button type="button">Copy</button></pre>
<p>The bias you can't introduce by hand is the bias you'll never debug in production.</p>
</details>
<a id="2-it-worked-in-the-backtest-means-nothing-if-live-runs-different-code" class="tsd-anchor"></a><h3 class="tsd-anchor-link">2. &quot;It worked in the backtest&quot; means nothing if live runs different code<a href="#2-it-worked-in-the-backtest-means-nothing-if-live-runs-different-code" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>The standard path productionizes a strategy by rewriting it: the research notebook becomes a second, hand-built live system with its own order logic, its own bugs, its own divergence. Now you have two strategies that <em>look</em> identical and behave differently exactly when it matters.</p>
<p>Here there is one code path. The <code>getSignal</code> you backtested is the <code>getSignal</code> that trades. Backtest mode feeds it historical timestamps; live mode feeds it <code>Date.now()</code>. The business logic — entries, validation, scheduled activation, TP/SL/timeout, partial closes — is byte-for-byte the same in both. The only differences are infrastructural: where the data comes from, not what you do with it.</p>
<details>
<summary>The Code</summary>
<pre><code class="typescript"><span class="hl-0">// Backtest — a historical frame drives the clock</span><br/><span class="hl-5">Backtest</span><span class="hl-2">.</span><span class="hl-1">background</span><span class="hl-2">(</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">, { </span><span class="hl-5">strategyName</span><span class="hl-2">, </span><span class="hl-5">exchangeName</span><span class="hl-2">, </span><span class="hl-5">frameName</span><span class="hl-2"> });</span><br/><br/><span class="hl-0">// Live — wall-clock drives the clock; the strategy file is untouched</span><br/><span class="hl-5">Live</span><span class="hl-2">.</span><span class="hl-1">background</span><span class="hl-2">(</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">, { </span><span class="hl-5">strategyName</span><span class="hl-2">, </span><span class="hl-5">exchangeName</span><span class="hl-2"> }); </span><span class="hl-0">// keys via .env</span><br/><span class="hl-1">listenSignalLive</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">e</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">e</span><span class="hl-2">.</span><span class="hl-5">action</span><span class="hl-2"> === </span><span class="hl-3">&#39;closed&#39;</span><span class="hl-2">) </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">Live</span><span class="hl-2">.</span><span class="hl-1">dump</span><span class="hl-2">(</span><span class="hl-5">e</span><span class="hl-2">.</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">e</span><span class="hl-2">.</span><span class="hl-5">strategyName</span><span class="hl-2">); });</span><br/><br/><span class="hl-0">// Paper — live prices, no real orders, identical path. Validate here before risking capital.</span>
</code><button type="button">Copy</button></pre>
<p>And one engine, two ways to consume it — pick by use case, not by capability:</p>
<pre><code class="typescript"><span class="hl-0">// Event-driven (production bots, monitoring)</span><br/><span class="hl-5">Backtest</span><span class="hl-2">.</span><span class="hl-1">background</span><span class="hl-2">(</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">, </span><span class="hl-5">config</span><span class="hl-2">);</span><br/><span class="hl-1">listenSignalBacktest</span><span class="hl-2">(</span><span class="hl-5">e</span><span class="hl-2"> </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><span class="hl-0">/* … */</span><span class="hl-2">});</span><br/><br/><span class="hl-0">// Async iterator (research, scripts, LLM agents)</span><br/><span class="hl-6">for</span><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> (</span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">event</span><span class="hl-2"> </span><span class="hl-4">of</span><span class="hl-2"> </span><span class="hl-5">Backtest</span><span class="hl-2">.</span><span class="hl-1">run</span><span class="hl-2">(</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">, </span><span class="hl-5">config</span><span class="hl-2">)) { </span><span class="hl-0">/* signal | progress | done */</span><span class="hl-2"> }</span>
</code><button type="button">Copy</button></pre>
</details>
<details>
<summary>The Proof</summary>
<p>This is the property the test suite exists to defend, and the line in the sand for the whole project: <strong>business logic is 100% synchronous across backtest and live.</strong> Signal validation is identical in both modes; immediate activation behaves identically; scheduled-signal logic is fully synchronized; TP / SL / timeout checks do not differ. The only divergence is infrastructural — how candles, order books, and time are sourced. <code>validation.test.mjs</code>, <code>backtest.test.mjs</code>, and <code>callbacks.test.mjs</code> pin this behavior; <code>event.test.mjs</code> pins the live path against the same expectations. If the two ever drift, a test goes red before you do.</p>
</details>
<a id="3-the-crash-that-opens-your-position-twice" class="tsd-anchor"></a><h3 class="tsd-anchor-link">3. The crash that opens your position twice<a href="#3-the-crash-that-opens-your-position-twice" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>A bot updating a position when the process dies — OOM, deploy, power blip — usually wakes up to corrupted state: a half-opened position, a cost basis that's wrong, an exit that never registered. Recovery by hand is where money leaks.</p>
<p>Every state mutation is written atomically to disk <em>before</em> it counts as done (write-temp-then-rename), and on restart the engine reloads to the last consistent state. Live runs reload persisted signal state on every start, and <code>Live.background()</code> shuts down gracefully — it waits for open positions to reach <code>closed</code> before stopping, so a deploy never severs a live trade mid-flight.</p>
<details>
<summary>The Proof</summary>
<p>Recovery is structural, not a feature you remember to enable. <code>PersistBase</code> does atomic write-to-temp + rename, repairs corrupted files, and verifies integrity in <code>waitForInit()</code>. Fifteen per-domain <code>Persist*Instance</code> classes cover everything that can change: Signal, State, Session, Candle, Risk, Partial, Breakeven, Schedule, Recent, Notification, Log, Measure, Interval, Memory. Concrete scenarios that resolve cleanly:</p>
<ul>
<li>Process killed during order placement → internal state unchanged, retried next tick.</li>
<li>Network failure during an exchange call → automatic retry on the next tick.</li>
<li>Power loss during a save → recovery from the last atomic write.</li>
<li>OOM → graceful shutdown with state preserved.</li>
</ul>
<pre><code class="typescript"><span class="hl-1">listenSignalLive</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">event</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">action</span><span class="hl-2"> === </span><span class="hl-3">&#39;closed&#39;</span><span class="hl-2">) {</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">Live</span><span class="hl-2">.</span><span class="hl-1">dump</span><span class="hl-2">(</span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">strategyName</span><span class="hl-2">); </span><span class="hl-0">// atomic snapshot to disk</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">Partial</span><span class="hl-2">.</span><span class="hl-1">dump</span><span class="hl-2">(</span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">strategyName</span><span class="hl-2">);</span><br/><span class="hl-2"> }</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">action</span><span class="hl-2"> === </span><span class="hl-3">&#39;scheduled&#39;</span><span class="hl-2"> || </span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">action</span><span class="hl-2"> === </span><span class="hl-3">&#39;cancelled&#39;</span><span class="hl-2">) {</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">Schedule</span><span class="hl-2">.</span><span class="hl-1">dump</span><span class="hl-2">(</span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">strategyName</span><span class="hl-2">);</span><br/><span class="hl-2"> }</span><br/><span class="hl-2">});</span>
</code><button type="button">Copy</button></pre>
</details>
<a id="4-the-state-that-cant-be-corrupted-because-it-cant-be-expressed" class="tsd-anchor"></a><h3 class="tsd-anchor-link">4. The state that can't be corrupted because it can't be expressed<a href="#4-the-state-that-cant-be-corrupted-because-it-cant-be-expressed" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>&quot;Is this position closed?&quot; is a question you should never have to ask at runtime. A signal here moves through a strict lifecycle — <strong>idle → scheduled → opened → active → closed</strong> — modeled with TypeScript discriminated unions. Reading a closed position's live PnL, or mutating an active trade as if it were idle, isn't a bug you catch in QA; it's a line that won't compile.</p>
<details>
<summary>The Code</summary>
<p>Each state exposes only the data that is meaningful in that state, so the wrong access never type-checks:</p>
<pre><code class="typescript"><span class="hl-1">listenSignal</span><span class="hl-2">((</span><span class="hl-5">event</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-6">switch</span><span class="hl-2"> (</span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">action</span><span class="hl-2">) {</span><br/><span class="hl-2"> </span><span class="hl-6">case</span><span class="hl-2"> </span><span class="hl-3">&#39;idle&#39;</span><span class="hl-2">: </span><span class="hl-0">/* no signal — only monitoring fields exist */</span><span class="hl-2"> </span><span class="hl-6">break</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">case</span><span class="hl-2"> </span><span class="hl-3">&#39;scheduled&#39;</span><span class="hl-2">: </span><span class="hl-0">/* waiting for entry price — has priceOpen, scheduledAt */</span><span class="hl-2"> </span><span class="hl-6">break</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">case</span><span class="hl-2"> </span><span class="hl-3">&#39;opened&#39;</span><span class="hl-2">: </span><span class="hl-0">/* just filled — entry data, no closeReason yet */</span><span class="hl-2"> </span><span class="hl-6">break</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">case</span><span class="hl-2"> </span><span class="hl-3">&#39;active&#39;</span><span class="hl-2">: </span><span class="hl-0">/* live position — pnl, peakProfit, maxDrawdown */</span><span class="hl-2"> </span><span class="hl-6">break</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">case</span><span class="hl-2"> </span><span class="hl-3">&#39;closed&#39;</span><span class="hl-2">: </span><span class="hl-0">/* exited — closeReason, final pnl; live fields gone */</span><span class="hl-2"> </span><span class="hl-6">break</span><span class="hl-2">;</span><br/><span class="hl-2"> }</span><br/><span class="hl-2">});</span>
</code><button type="button">Copy</button></pre>
<p>Before any signal reaches the engine it passes a validation pipeline: TP/SL prices positive, relationship correct (<code>TP &gt; entry &gt; SL</code> long, inverse short), risk/reward ≥ your minimum, timestamps not in the future, interval-throttling respected. Invalid signals are rejected or logged — never executed.</p>
</details>
<details>
<summary>The Proof</summary>
<p>The discriminated-union result types (<code>IStrategyTickResultWaiting / …Opened / …Closed / …Scheduled / …Cancelled</code>) are enforced end-to-end: <code>ClientStrategy.tick()/backtest()</code>, <code>StrategyCoreService</code>, the persistence layer, and every notification contract (<code>SignalOpenedNotification</code>, <code>SignalClosedNotification</code>, <code>SignalCancelledNotification</code>, <code>SignalScheduledNotification</code>) carry the lifecycle state explicitly. <code>validation.test.mjs</code> exercises valid long/short, inverted TP/SL, negative prices, and future timestamps; <code>backtest.test.mjs</code> walks every close reason (<code>take_profit</code>, <code>stop_loss</code>, <code>time_expired</code>).</p>
</details>
<a id="5-the-order-the-exchange-silently-rejected" class="tsd-anchor"></a><h3 class="tsd-anchor-link">5. The order the exchange silently rejected<a href="#5-the-order-the-exchange-silently-rejected" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Live trading's quiet killer: the exchange rejects, times out, or fills partially, and your bot's internal state no longer matches reality. The textbook &quot;fix&quot; is hand-written <code>try/catch</code> rollback around every order — which is exactly the code that breaks on the edge case you didn't think of.</p>
<p>Here, every state-mutating action fires through the broker adapter <em>before</em> the internal state changes. If the adapter throws — rejection, timeout, network failure — the mutation is skipped, the state stays exactly as it was, and the engine retries on the next tick. You never write rollback logic, and there is no half-applied state to reconcile. In backtest mode no adapter is called at all, so historical replays never touch exchange code.</p>
<details>
<summary>The Code</summary>
<p>The reusable core: place → poll to fill → on timeout cancel, market-out any partial fill, restore TP/SL so the position is never left naked, then throw so the engine retries.</p>
<pre><code class="typescript"><span class="hl-4">async</span><span class="hl-2"> </span><span class="hl-4">function</span><span class="hl-2"> </span><span class="hl-1">createLimitOrderAndWait</span><span class="hl-2">(</span><span class="hl-5">exchange</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">side</span><span class="hl-2">, </span><span class="hl-5">qty</span><span class="hl-2">, </span><span class="hl-5">price</span><span class="hl-2">, </span><span class="hl-5">restore</span><span class="hl-2">?) {</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">order</span><span class="hl-2"> = </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">exchange</span><span class="hl-2">.</span><span class="hl-1">createOrder</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;limit&#39;</span><span class="hl-2">, </span><span class="hl-5">side</span><span class="hl-2">, </span><span class="hl-5">qty</span><span class="hl-2">, </span><span class="hl-5">price</span><span class="hl-2">);</span><br/><br/><span class="hl-2"> </span><span class="hl-6">for</span><span class="hl-2"> (</span><span class="hl-4">let</span><span class="hl-2"> </span><span class="hl-5">i</span><span class="hl-2"> = </span><span class="hl-8">0</span><span class="hl-2">; </span><span class="hl-5">i</span><span class="hl-2"> &lt; </span><span class="hl-7">FILL_POLL_ATTEMPTS</span><span class="hl-2">; </span><span class="hl-5">i</span><span class="hl-2">++) {</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">sleep</span><span class="hl-2">(</span><span class="hl-7">FILL_POLL_INTERVAL_MS</span><span class="hl-2">);</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> ((</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">exchange</span><span class="hl-2">.</span><span class="hl-1">fetchOrder</span><span class="hl-2">(</span><span class="hl-5">order</span><span class="hl-2">.</span><span class="hl-5">id</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">)).</span><span class="hl-5">status</span><span class="hl-2"> === </span><span class="hl-3">&#39;closed&#39;</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2">; </span><span class="hl-0">// filled</span><br/><span class="hl-2"> }</span><br/><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">exchange</span><span class="hl-2">.</span><span class="hl-1">cancelOrder</span><span class="hl-2">(</span><span class="hl-5">order</span><span class="hl-2">.</span><span class="hl-5">id</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">);</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">sleep</span><span class="hl-2">(</span><span class="hl-7">CANCEL_SETTLE_MS</span><span class="hl-2">); </span><span class="hl-0">// let the exchange settle before reading</span><br/><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">filledQty</span><span class="hl-2"> = (</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">exchange</span><span class="hl-2">.</span><span class="hl-1">fetchOrder</span><span class="hl-2">(</span><span class="hl-5">order</span><span class="hl-2">.</span><span class="hl-5">id</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">)).</span><span class="hl-5">filled</span><span class="hl-2"> ?? </span><span class="hl-8">0</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">filledQty</span><span class="hl-2"> &gt; </span><span class="hl-8">0</span><span class="hl-2">) { </span><span class="hl-0">// roll the partial fill back to clean state</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">exchange</span><span class="hl-2">.</span><span class="hl-1">createOrder</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;market&#39;</span><span class="hl-2">, </span><span class="hl-5">side</span><span class="hl-2"> === </span><span class="hl-3">&#39;buy&#39;</span><span class="hl-2"> ? </span><span class="hl-3">&#39;sell&#39;</span><span class="hl-2"> : </span><span class="hl-3">&#39;buy&#39;</span><span class="hl-2">, </span><span class="hl-5">filledQty</span><span class="hl-2">);</span><br/><span class="hl-2"> }</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">restore</span><span class="hl-2">) { </span><span class="hl-0">/* re-place TP + stop-loss on the remaining position so it is never unprotected */</span><span class="hl-2"> }</span><br/><br/><span class="hl-2"> </span><span class="hl-6">throw</span><span class="hl-2"> </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-1">Error</span><span class="hl-2">(</span><span class="hl-3">&#39;not filled in time — partial fill rolled back, backtest-kit will retry&#39;</span><span class="hl-2">);</span><br/><span class="hl-2">}</span>
</code><button type="button">Copy</button></pre>
<p>A hook wires it to position open. Signal open/close are routed automatically by an internal event bus the moment <code>Broker.enable()</code> is called — no manual wiring. The other mutations are intercepted explicitly before their state change:</p>
<pre><code class="typescript"><span class="hl-5">Broker</span><span class="hl-2">.</span><span class="hl-1">useBrokerAdapter</span><span class="hl-2">(</span><span class="hl-4">class</span><span class="hl-2"> </span><span class="hl-4">implements</span><span class="hl-2"> </span><span class="hl-10">IBroker</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> </span><span class="hl-1">waitForInit</span><span class="hl-2">() { </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getExchange</span><span class="hl-2">(); }</span><br/><br/><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> </span><span class="hl-1">onOrderOpenCommit</span><span class="hl-2">({ </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">cost</span><span class="hl-2">, </span><span class="hl-5">priceOpen</span><span class="hl-2">, </span><span class="hl-5">priceTakeProfit</span><span class="hl-2">, </span><span class="hl-5">priceStopLoss</span><span class="hl-2"> }) {</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">ex</span><span class="hl-2"> = </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getExchange</span><span class="hl-2">();</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">qty</span><span class="hl-2"> = </span><span class="hl-1">truncateQty</span><span class="hl-2">(</span><span class="hl-5">ex</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">cost</span><span class="hl-2"> / </span><span class="hl-5">priceOpen</span><span class="hl-2">);</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">createLimitOrderAndWait</span><span class="hl-2">(</span><span class="hl-5">ex</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;buy&#39;</span><span class="hl-2">, </span><span class="hl-5">qty</span><span class="hl-2">, </span><span class="hl-5">priceOpen</span><span class="hl-2">); </span><span class="hl-0">// entry</span><br/><span class="hl-2"> </span><span class="hl-6">try</span><span class="hl-2"> { </span><span class="hl-0">// protect immediately</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">ex</span><span class="hl-2">.</span><span class="hl-1">createOrder</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;limit&#39;</span><span class="hl-2">, </span><span class="hl-3">&#39;sell&#39;</span><span class="hl-2">, </span><span class="hl-5">qty</span><span class="hl-2">, </span><span class="hl-5">priceTakeProfit</span><span class="hl-2">);</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">createStopLossOrder</span><span class="hl-2">(</span><span class="hl-5">ex</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">qty</span><span class="hl-2">, </span><span class="hl-5">priceStopLoss</span><span class="hl-2">);</span><br/><span class="hl-2"> } </span><span class="hl-6">catch</span><span class="hl-2"> (</span><span class="hl-5">err</span><span class="hl-2">) { </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">ex</span><span class="hl-2">.</span><span class="hl-1">createOrder</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;market&#39;</span><span class="hl-2">, </span><span class="hl-3">&#39;sell&#39;</span><span class="hl-2">, </span><span class="hl-5">qty</span><span class="hl-2">); </span><span class="hl-6">throw</span><span class="hl-2"> </span><span class="hl-5">err</span><span class="hl-2">; }</span><br/><span class="hl-2"> }</span><br/><span class="hl-2"> </span><span class="hl-0">// onOrderCloseCommit · onPartialProfitCommit · onPartialLossCommit</span><br/><span class="hl-2"> </span><span class="hl-0">// onTrailingStopCommit · onTrailingTakeCommit · onBreakevenCommit · onAverageBuyCommit</span><br/><span class="hl-2">});</span><br/><span class="hl-5">Broker</span><span class="hl-2">.</span><span class="hl-1">enable</span><span class="hl-2">();</span>
</code><button type="button">Copy</button></pre>
<p>Complete, production-grade <strong>Spot</strong> (<code>stop_loss_limit</code>, balance truncation, dust/notional guards) and <strong>Futures</strong> (<code>reduceOnly</code>, hedge-mode <code>positionSide</code>, <code>setLeverage</code>, ghost-position guards) adapters — every hook, every edge case — ship verbatim in the docs. The CLI can also dry-fire any single hook against your live adapter for verification before you wait hours for a real signal:</p>
<pre><code class="bash"><span class="hl-1">npx</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/cli</span><span class="hl-2"> </span><span class="hl-4">--brokerdebug</span><span class="hl-2"> </span><span class="hl-4">--commit</span><span class="hl-2"> </span><span class="hl-3">signal-open</span><span class="hl-2"> </span><span class="hl-4">--symbol</span><span class="hl-2"> </span><span class="hl-3">BTCUSDT</span>
</code><button type="button">Copy</button></pre>
</details>
<a id="6-averaging-up-is-how-a-dip-becomes-a-margin-call" class="tsd-anchor"></a><h3 class="tsd-anchor-link">6. Averaging up is how a dip becomes a margin call<a href="#6-averaging-up-is-how-a-dip-becomes-a-margin-call" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Dollar-cost averaging is where hand-rolled position math quietly bankrupts people. Average into a <em>rising</em> price by accident and you've raised your cost basis on a losing-direction trade — the opposite of the intent. And once you add partial closes on top, the cost-basis bookkeeping becomes a second strategy you have to get right.</p>
<p><code>commitAverageBuy</code> is, by default, <em>only</em> accepted when price is below the running effective entry — averaging up is silently rejected, structurally. The effective price is a cost-weighted harmonic mean (correct for fixed-dollar entries, where $100 buys different quantities at different prices), and every partial close snapshots its cost basis so PnL replays exactly without re-walking history. No math required from you — the guardrail is in the engine.</p>
<details>
<summary>The Math</summary>
<pre><code><span class="hl-5">effectivePrice</span><span class="hl-2"> = </span><span class="hl-5">Σcost</span><span class="hl-2"> / </span><span class="hl-1">Σ</span><span class="hl-2">(</span><span class="hl-5">cost</span><span class="hl-2"> / </span><span class="hl-5">price</span><span class="hl-2">) </span><span class="hl-0">// cost-weighted harmonic mean</span>
</code><button>Copy</button></pre>
<p>Each partial stores <code>costBasisAtClose</code> (the running dollar basis <em>before</em> it fired); a partial sell does not change the effective price of the coins still held. Final PnL is a dollar-weighted sum across every partial (each at its own effective price) plus the remainder, with slippage and per-leg fees:</p>
<pre><code><span class="hl-5">weight</span><span class="hl-2">[</span><span class="hl-5">i</span><span class="hl-2">] = (</span><span class="hl-5">percent</span><span class="hl-2">[</span><span class="hl-5">i</span><span class="hl-2">]/</span><span class="hl-8">100</span><span class="hl-2"> × </span><span class="hl-5">costBasisAtClose</span><span class="hl-2">[</span><span class="hl-5">i</span><span class="hl-2">]) / </span><span class="hl-5">totalInvested</span><br/><span class="hl-5">totalWeightedPnl</span><span class="hl-2"> = </span><span class="hl-7">Σ</span><span class="hl-2"> </span><span class="hl-5">weight</span><span class="hl-2">[</span><span class="hl-5">i</span><span class="hl-2"></span><span class="hl-5">pnl</span><span class="hl-2">[</span><span class="hl-5">i</span><span class="hl-2">] + </span><span class="hl-5">remainingWeight</span><span class="hl-2">·</span><span class="hl-5">pnlRemaining</span><br/><span class="hl-5">pnlPercentage</span><span class="hl-2"> = </span><span class="hl-5">totalWeightedPnl</span><span class="hl-2"></span><span class="hl-5">fees</span><span class="hl-2"> </span><span class="hl-0">// open fee once + per-partial + final close</span><br/><span class="hl-5">pnlCost</span><span class="hl-2"> = </span><span class="hl-5">pnlPercentage</span><span class="hl-2"> / </span><span class="hl-8">100</span><span class="hl-2"> × </span><span class="hl-5">totalInvested</span>
</code><button>Copy</button></pre>
<p>Worked example — LONG @1000, 4 accepted DCA + 1 rejected, 3 partials, close @1200 — reconciles two independent ways to <strong>+17.9%</strong>:</p>
<pre><code><span class="hl-8">0.075</span><span class="hl-2">·(+</span><span class="hl-8">15.00</span><span class="hl-2">) + </span><span class="hl-8">0.135</span><span class="hl-2">·(−</span><span class="hl-8">7.98</span><span class="hl-2">) + </span><span class="hl-8">0.316</span><span class="hl-2">·(+</span><span class="hl-8">12.91</span><span class="hl-2">) + </span><span class="hl-8">0.474</span><span class="hl-2">·(+</span><span class="hl-8">29.04</span><span class="hl-2">) ≈ +</span><span class="hl-8">17.89</span><span class="hl-2">%</span><br/><span class="hl-5">coin</span><span class="hl-2"> </span><span class="hl-5">cross</span><span class="hl-2">-</span><span class="hl-9">check</span><span class="hl-2">: (</span><span class="hl-8">34.50</span><span class="hl-2"> + </span><span class="hl-8">49.69</span><span class="hl-2"> + </span><span class="hl-8">142.72</span><span class="hl-2"> + </span><span class="hl-8">244.67</span><span class="hl-2"></span><span class="hl-8">400</span><span class="hl-2">) / </span><span class="hl-8">400</span><span class="hl-2"> ≈ +</span><span class="hl-8">17.90</span><span class="hl-2">% ✓</span><br/><span class="hl-5">entry</span><span class="hl-2"> #</span><span class="hl-8">5</span><span class="hl-2"> @</span><span class="hl-8">980</span><span class="hl-2"> </span><span class="hl-7">REJECTED</span><span class="hl-2"></span><span class="hl-8">980</span><span class="hl-2"> &gt; </span><span class="hl-5">effective</span><span class="hl-2"> </span><span class="hl-5">entry</span><span class="hl-2"></span><span class="hl-8">929.92</span><span class="hl-2"> (</span><span class="hl-5">the</span><span class="hl-2"> </span><span class="hl-5">guard</span><span class="hl-2"> </span><span class="hl-5">firing</span><span class="hl-2">)</span>
</code><button>Copy</button></pre>
</details>
<details>
<summary>The Code</summary>
<p>A complete DCA-ladder strategy — open once, average on overlap-free dips up to 10 rungs, close at target — is about thirty lines, and the dangerous math is all inside the engine:</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">addStrategySchema</span><span class="hl-2">, </span><span class="hl-5">listenActivePing</span><span class="hl-2">, </span><span class="hl-5">Position</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">commitAverageBuy</span><span class="hl-2">, </span><span class="hl-5">commitClosePending</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">getPositionEntries</span><span class="hl-2">, </span><span class="hl-5">getPositionEntryOverlap</span><span class="hl-2">, </span><span class="hl-5">getPositionPnlPercent</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;backtest-kit&#39;</span><span class="hl-2">;</span><br/><br/><span class="hl-1">addStrategySchema</span><span class="hl-2">({</span><br/><span class="hl-2"> </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">&#39;apr_2026_strategy&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-1">getSignal</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">when</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> ({</span><br/><span class="hl-2"> </span><span class="hl-5">position:</span><span class="hl-2"> </span><span class="hl-3">&#39;long&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> ...</span><span class="hl-5">Position</span><span class="hl-2">.</span><span class="hl-1">moonbag</span><span class="hl-2">({ </span><span class="hl-5">position:</span><span class="hl-2"> </span><span class="hl-3">&#39;long&#39;</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">, </span><span class="hl-5">percentStopLoss:</span><span class="hl-2"> </span><span class="hl-8">25</span><span class="hl-2"> }),</span><br/><span class="hl-2"> </span><span class="hl-5">minuteEstimatedTime:</span><span class="hl-2"> </span><span class="hl-4">Infinity</span><span class="hl-2">, </span><span class="hl-5">cost:</span><span class="hl-2"> </span><span class="hl-8">100</span><span class="hl-2">,</span><br/><span class="hl-2"> }),</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-1">listenActivePing</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> ({ </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-0">// the ladder</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> ((</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getPositionEntries</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">)).</span><span class="hl-5">length</span><span class="hl-2"> &gt;= </span><span class="hl-8">10</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getPositionEntryOverlap</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">, { </span><span class="hl-5">upperPercent:</span><span class="hl-2"> </span><span class="hl-8">5</span><span class="hl-2">, </span><span class="hl-5">lowerPercent:</span><span class="hl-2"> </span><span class="hl-8">1</span><span class="hl-2"> })) </span><span class="hl-6">return</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">commitAverageBuy</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-8">100</span><span class="hl-2">); </span><span class="hl-0">// rejected if it averages up</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-1">listenActivePing</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> ({ </span><span class="hl-5">symbol</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-0">// exit on blended target</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getPositionPnlPercent</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">) &lt; </span><span class="hl-8">3</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">commitClosePending</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, { </span><span class="hl-5">id:</span><span class="hl-2"> </span><span class="hl-3">&#39;unknown&#39;</span><span class="hl-2">, </span><span class="hl-5">note:</span><span class="hl-2"> </span><span class="hl-3">&#39;# closed by target pnl&#39;</span><span class="hl-2"> });</span><br/><span class="hl-2">});</span>
</code><button type="button">Copy</button></pre>
<p>Every order primitive is here, each with per-entry PnL, peak-profit and max-drawdown tracking: market/limit entries, TP/SL/OCO exits, grid with auto-cancel, partial profit/loss levels, trailing take/stop (absorbed only when they tighten in your favour, computed from the <em>original</em> distance to avoid drift), breakeven (moves the stop to entry once profit clears fees+slippage), stop-limit entries, DCA, and time-attack / infinite-hold.</p>
</details>
<a id="7-ten-strategies-one-account-100-exposure" class="tsd-anchor"></a><h3 class="tsd-anchor-link">7. Ten strategies, one account, 100% exposure<a href="#7-ten-strategies-one-account-100-exposure" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Per-strategy risk checks miss the obvious portfolio truth: ten strategies each &quot;risking 10%&quot; is one account risking everything. Risk validation here runs across <em>all</em> strategies and symbols at once, with an atomic check-and-reserve that closes the race between &quot;is this allowed?&quot; and &quot;the order went out.&quot;</p>
<details>
<summary>The Code</summary>
<pre><code class="typescript"><span class="hl-1">addRiskSchema</span><span class="hl-2">({</span><br/><span class="hl-2"> </span><span class="hl-5">riskName:</span><span class="hl-2"> </span><span class="hl-3">&#39;demo&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">validations:</span><span class="hl-2"> [</span><br/><span class="hl-2"> ({ </span><span class="hl-5">pendingSignal</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-0">// TP ≥ 1%</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> { </span><span class="hl-7">priceOpen</span><span class="hl-2"> = </span><span class="hl-5">currentPrice</span><span class="hl-2">, </span><span class="hl-7">priceTakeProfit</span><span class="hl-2">, </span><span class="hl-7">position</span><span class="hl-2"> } = </span><span class="hl-5">pendingSignal</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">tp</span><span class="hl-2"> = </span><span class="hl-5">position</span><span class="hl-2"> === </span><span class="hl-3">&#39;long&#39;</span><br/><span class="hl-2"> ? ((</span><span class="hl-5">priceTakeProfit</span><span class="hl-2"> - </span><span class="hl-5">priceOpen</span><span class="hl-2">) / </span><span class="hl-5">priceOpen</span><span class="hl-2">) * </span><span class="hl-8">100</span><br/><span class="hl-2"> : ((</span><span class="hl-5">priceOpen</span><span class="hl-2"> - </span><span class="hl-5">priceTakeProfit</span><span class="hl-2">) / </span><span class="hl-5">priceOpen</span><span class="hl-2">) * </span><span class="hl-8">100</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">tp</span><span class="hl-2"> &lt; </span><span class="hl-8">1</span><span class="hl-2">) </span><span class="hl-6">throw</span><span class="hl-2"> </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-1">Error</span><span class="hl-2">(</span><span class="hl-3">`TP too close: </span><span class="hl-4">${</span><span class="hl-5">tp</span><span class="hl-11">.</span><span class="hl-1">toFixed</span><span class="hl-11">(</span><span class="hl-8">2</span><span class="hl-11">)</span><span class="hl-4">}</span><span class="hl-3">%`</span><span class="hl-2">);</span><br/><span class="hl-2"> },</span><br/><span class="hl-2"> ({ </span><span class="hl-5">pendingSignal</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-0">// R/R ≥ 2:1</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> { </span><span class="hl-7">priceOpen</span><span class="hl-2"> = </span><span class="hl-5">currentPrice</span><span class="hl-2">, </span><span class="hl-7">priceTakeProfit</span><span class="hl-2">, </span><span class="hl-7">priceStopLoss</span><span class="hl-2">, </span><span class="hl-7">position</span><span class="hl-2"> } = </span><span class="hl-5">pendingSignal</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">reward</span><span class="hl-2"> = </span><span class="hl-5">position</span><span class="hl-2"> === </span><span class="hl-3">&#39;long&#39;</span><span class="hl-2"> ? </span><span class="hl-5">priceTakeProfit</span><span class="hl-2"> - </span><span class="hl-5">priceOpen</span><span class="hl-2"> : </span><span class="hl-5">priceOpen</span><span class="hl-2"> - </span><span class="hl-5">priceTakeProfit</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">risk</span><span class="hl-2"> = </span><span class="hl-5">position</span><span class="hl-2"> === </span><span class="hl-3">&#39;long&#39;</span><span class="hl-2"> ? </span><span class="hl-5">priceOpen</span><span class="hl-2"> - </span><span class="hl-5">priceStopLoss</span><span class="hl-2"> : </span><span class="hl-5">priceStopLoss</span><span class="hl-2"> - </span><span class="hl-5">priceOpen</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">reward</span><span class="hl-2"> / </span><span class="hl-5">risk</span><span class="hl-2"> &lt; </span><span class="hl-8">2</span><span class="hl-2">) </span><span class="hl-6">throw</span><span class="hl-2"> </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-1">Error</span><span class="hl-2">(</span><span class="hl-3">&#39;Poor R/R ratio&#39;</span><span class="hl-2">);</span><br/><span class="hl-2"> },</span><br/><span class="hl-2"> ],</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-1">listenRisk</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">event</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-5">Risk</span><span class="hl-2">.</span><span class="hl-1">dump</span><span class="hl-2">(</span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">event</span><span class="hl-2">.</span><span class="hl-5">strategyName</span><span class="hl-2">); }); </span><span class="hl-0">// every rejection, logged</span>
</code><button type="button">Copy</button></pre>
<p><code>ClientRisk</code> tracks every open position across the portfolio; multiple strategies can share one profile for holistic exposure. <code>checkSignalAndReserve</code> is the thread-safe variant — after a successful reserve you <strong>must</strong> <code>addSignal</code> (finalize) or <code>removeSignal</code> (cancel) so reservations never go stale. A real LLM-gated portfolio improved from <strong>+52.22% → +68.90%</strong> PNL, Sharpe <strong>+0.309 → +0.512</strong>, win-rate <strong>68% → 82%</strong> simply by letting a local model veto 6 signals — 4 of them losers.</p>
</details>
<a id="8-one-process-can-trade-the-whole-market" class="tsd-anchor"></a><h3 class="tsd-anchor-link">8. One process can trade the whole market<a href="#8-one-process-can-trade-the-whole-market" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Spawning a process per symbol burns CPU on IPC and turns shared state — global risk, candle cache — into a distributed-systems problem you didn't sign up for. Dozens of symbols run concurrently here inside a <strong>single Node process</strong>, sharing one event loop, one Mongo pool, one Redis cache, with strict per-symbol state isolation.</p>
<details>
<summary>The Proof</summary>
<p>Measured on a commodity laptop (HP Victus, i5-13420H, 16 GB DDR4, NVMe SSD), 9 symbols in parallel, one Node process:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Wall-clock span (first → last event)</td>
<td><strong>2,893 ms</strong></td>
</tr>
<tr>
<td>Events captured</td>
<td><strong>297</strong></td>
</tr>
<tr>
<td>Historical time advanced / symbol</td>
<td><strong>34 minutes</strong></td>
</tr>
<tr>
<td>Per-symbol replay speed</td>
<td><strong>≈703×</strong> real-time</td>
</tr>
<tr>
<td>Aggregate (9 symbols)</td>
<td><strong>≈6,326×</strong> real-time</td>
</tr>
<tr>
<td>Hot-loop throughput</td>
<td><strong>≈103 events/sec</strong></td>
</tr>
</tbody>
</table>
<p>Why it's fast: single-process concurrency (no IPC, no fork), an in-memory activity registry (<code>Lookup</code>) tracking every in-flight workload, a cooperative event-loop hand-off (<code>Candle.spinLock</code>) so parallel symbols advance round-robin instead of one hogging the CPU, Redis O(1) candle lookups, atomic <code>findOneAndUpdate</code> upserts (no read-modify-write), and <code>--cache</code> pre-warming so the inner loop never blocks on HTTP.</p>
<p>In live mode the bottleneck moves from CPU to the exchange — and that is where the shared cache earns its keep. Every symbol pulls candles, order books, and trades through one <strong>deduplicated</strong> layer, so nine strategies asking for the same <code>BTCUSDT 1m</code> candle issue <em>one</em> request, not nine. Hand-written per-bot code with no cache hammers the REST endpoint until the exchange rate-limits it; here the dedup + Redis O(1) layer keeps request volume flat as you add symbols, so rate limits stay off your back instead of throttling the desk. The ×700 / ×6,300 figures are CPU-bound backtest replay; live throughput is paced by the exchange, but the request layer is built so that pacing is the exchange's published limit, not self-inflicted spam.</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">Backtest</span><span class="hl-2">, </span><span class="hl-5">warmCandles</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;backtest-kit&#39;</span><span class="hl-2">;</span><br/><br/><span class="hl-6">for</span><span class="hl-2"> (</span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">symbol</span><span class="hl-2"> </span><span class="hl-4">of</span><span class="hl-2"> [</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">,</span><span class="hl-3">&#39;ETHUSDT&#39;</span><span class="hl-2">,</span><span class="hl-3">&#39;SOLUSDT&#39;</span><span class="hl-2">,</span><span class="hl-3">&#39;BNBUSDT&#39;</span><span class="hl-2">,</span><span class="hl-3">&#39;XRPUSDT&#39;</span><span class="hl-2">]) {</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">warmCandles</span><span class="hl-2">({ </span><span class="hl-5">exchangeName:</span><span class="hl-2"> </span><span class="hl-3">&#39;binance&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;1m&#39;</span><span class="hl-2">, </span><span class="hl-5">symbol</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">from:</span><span class="hl-2"> </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-1">Date</span><span class="hl-2">(</span><span class="hl-3">&#39;2026-02-01T00:00:00Z&#39;</span><span class="hl-2">), </span><span class="hl-5">to:</span><span class="hl-2"> </span><span class="hl-4">new</span><span class="hl-2"> </span><span class="hl-1">Date</span><span class="hl-2">(</span><span class="hl-3">&#39;2026-02-28T23:59:59Z&#39;</span><span class="hl-2">) });</span><br/><span class="hl-2"> </span><span class="hl-5">Backtest</span><span class="hl-2">.</span><span class="hl-1">background</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, { </span><span class="hl-5">strategyName</span><span class="hl-2">, </span><span class="hl-5">exchangeName:</span><span class="hl-2"> </span><span class="hl-3">&#39;binance&#39;</span><span class="hl-2">, </span><span class="hl-5">frameName:</span><span class="hl-2"> </span><span class="hl-3">&#39;feb-2026&#39;</span><span class="hl-2"> });</span><br/><span class="hl-2">}</span>
</code><button type="button">Copy</button></pre>
<pre><code class="bash"><span class="hl-1">npx</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/cli</span><span class="hl-2"> </span><span class="hl-4">--backtest</span><span class="hl-2"> </span><span class="hl-4">--entry</span><span class="hl-2"> </span><span class="hl-3">./content/multi-symbol.ts</span><span class="hl-2"> </span><span class="hl-0"># CLI defers symbol selection to your file</span>
</code><button type="button">Copy</button></pre>
</details>
<a id="9-when-dump-stops-being-enough" class="tsd-anchor"></a><h3 class="tsd-anchor-link">9. When <code>./dump/</code> stops being enough<a href="#9-when-dump-stops-being-enough" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>File storage is perfect on day one and a bottleneck the day you're doing thousands of context-keyed reads per second. Swap to MongoDB (durable, queryable, atomic) with a Redis O(1) cache via a single <code>setup()</code> — all 15 persistence contracts reimplemented, and <strong>not one line of strategy code changes.</strong></p>
<details>
<summary>The Code</summary>
<pre><code class="typescript"><span class="hl-0">// config/setup.config.ts — loaded once before any persistence call</span><br/><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">setup</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;@backtest-kit/mongo&#39;</span><span class="hl-2">;</span><br/><span class="hl-1">setup</span><span class="hl-2">(); </span><span class="hl-0">// reads CC_MONGO_CONNECTION_STRING / CC_REDIS_* from env, or pass explicitly</span>
</code><button type="button">Copy</button></pre>
<p>Fifteen adapters, each with a unique compound index (<code>Signal → symbol+strategyName+exchangeName</code>, <code>Candle → symbol+interval+timestamp</code>, <code>Memory → signalId+bucketName+memoryId</code>, …). Candle records are immutable (<code>$setOnInsert</code>, first write wins); Measure/Interval/Memory use soft delete (<code>removed</code> flag) for an audit trail. Reads go Redis-first for the Mongo <code>_id</code>, then <code>findById</code> — two O(1) ops; a miss falls back to an indexed <code>findOne</code> and backfills. Writes are one <code>findOneAndUpdate({ upsert:true, new:true })</code> round-trip, so the unique index rejects concurrent duplicates at the storage engine and a write-then-read always sees fresh data. Signal-affecting adapters store the simulation <code>when</code>, so look-ahead protection is enforceable even inside the database.</p>
<pre><code><span class="hl-5">read</span><span class="hl-2"> </span><span class="hl-1">signal</span><span class="hl-2"> (</span><span class="hl-7">BTCUSDT</span><span class="hl-2">, </span><span class="hl-5">my_strategy</span><span class="hl-2">, </span><span class="hl-5">binance</span><span class="hl-2">)</span><br/><span class="hl-2"> ├─ </span><span class="hl-5">Redis</span><span class="hl-2"> </span><span class="hl-7">GET</span><span class="hl-2"></span><span class="hl-5">hit</span><span class="hl-2"></span><span class="hl-5">Mongo</span><span class="hl-2"> </span><span class="hl-1">findById</span><span class="hl-2">(</span><span class="hl-5">_id</span><span class="hl-2">) ← </span><span class="hl-1">O</span><span class="hl-2">(</span><span class="hl-8">1</span><span class="hl-2">) + </span><span class="hl-1">O</span><span class="hl-2">(</span><span class="hl-8">1</span><span class="hl-2">)</span><br/><span class="hl-2"> └─ </span><span class="hl-5">Redis</span><span class="hl-2"> </span><span class="hl-7">GET</span><span class="hl-2"></span><span class="hl-5">miss</span><span class="hl-2"></span><span class="hl-5">Mongo</span><span class="hl-2"> </span><span class="hl-1">findOne</span><span class="hl-2">(</span><span class="hl-5">filter</span><span class="hl-2">) → </span><span class="hl-5">Redis</span><span class="hl-2"> </span><span class="hl-7">SET</span><span class="hl-2"></span><span class="hl-6">return</span>
</code><button>Copy</button></pre>
<p>The default file adapter is already crash-safe (atomic temp+rename, repair on restart) — you get durability before you ever add a database.</p>
</details>
<a id="10-a-sharpe-of-10000000-is-a-bug-not-an-edge" class="tsd-anchor"></a><h3 class="tsd-anchor-link">10. A Sharpe of 10,000,000 is a bug, not an edge<a href="#10-a-sharpe-of-10000000-is-a-bug-not-an-edge" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Metrics that a tiny sample can't support are worse than no metrics — they're false confidence you bet money on. The analytics engine was rebuilt against canonical definitions and an independent 84-file reference testbed, and it prints <strong><code>N/A</code></strong> rather than a number it can't stand behind.</p>
<details>
<summary>The Math</summary>
<ul>
<li><strong>Pooled Sharpe</strong> (v10.2.0+): per-trade returns are pooled across all symbols into one sample, then Sharpe is computed on that distribution — replacing the trade-count-weighted <em>average of ratios</em>, which inflates when one symbol is great and another negative. The header reads <code>Pooled Sharpe</code>, not <code>Portfolio Sharpe</code>, with a Markowitz disclaimer so it's never mistaken for covariance-based optimization.</li>
<li><strong>Bessel's correction (N−1)</strong> for unbiased variance — no risk underestimation on small samples.</li>
<li><strong>Compounded equity curve</strong> for Max Drawdown / Calmar / Recovery Factor — no double-counting of percentage returns.</li>
<li><strong>Geometric annualization</strong> for expected yearly returns — accounts for volatility drag (a 50% loss needs a 100% gain to recover).</li>
<li><strong>Canonical Sortino (1991)</strong> with downside deviation over <code>N_total</code>.</li>
<li><strong>Float-artifact guard:</strong> identical-return series produce stddev ≈1e-17; an <code>STDDEV_EPSILON</code> guard returns <code>N/A</code> instead of a fake Sharpe of 10,000,000. Gates of ≥10 signals and ≥14 calendar days gate publication.</li>
</ul>
<p>Dashboard revenue is dollar-true: <code>pnlCost = pnlPercentage/100 × pnlEntries</code>, summed across closed signals per window (Today / Yesterday / 7d / 31d), anchored to the run end in backtest and <code>Date.now()</code> live.</p>
</details>
<a id="11-the-jobs-that-fire-on-virtual-time" class="tsd-anchor"></a><h3 class="tsd-anchor-link">11. The jobs that fire on virtual time<a href="#11-the-jobs-that-fire-on-virtual-time" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Most schedulers run on wall-clock — useless in a backtest that replays a month in three seconds. <code>Cron</code> runs on the <em>same</em> time stream your strategies see, firing on candle boundaries, coordinated across parallel backtests so one boundary never double-fires. The identical API drives live re-polling and one-shot backtest prep.</p>
<details>
<summary>The Code</summary>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">Cron</span><span class="hl-2">, </span><span class="hl-5">Backtest</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;backtest-kit&#39;</span><span class="hl-2">;</span><br/><br/><span class="hl-5">Cron</span><span class="hl-2">.</span><span class="hl-1">register</span><span class="hl-2">({ </span><span class="hl-5">name:</span><span class="hl-2"> </span><span class="hl-3">&#39;tg-parser&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;1h&#39;</span><span class="hl-2">, </span><span class="hl-0">// global, hourly</span><br/><span class="hl-2"> </span><span class="hl-1">handler</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> ({ </span><span class="hl-5">when</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">parseTelegramSignals</span><span class="hl-2">(</span><span class="hl-5">when</span><span class="hl-2">); } });</span><br/><br/><span class="hl-5">Cron</span><span class="hl-2">.</span><span class="hl-1">register</span><span class="hl-2">({ </span><span class="hl-5">name:</span><span class="hl-2"> </span><span class="hl-3">&#39;funding&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;1h&#39;</span><span class="hl-2">, </span><span class="hl-5">symbols:</span><span class="hl-2"> [</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">,</span><span class="hl-3">&#39;ETHUSDT&#39;</span><span class="hl-2">], </span><span class="hl-0">// per-symbol fan-out</span><br/><span class="hl-2"> </span><span class="hl-1">handler</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> ({ </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">when</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">fetchFundingRate</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">when</span><span class="hl-2">); } });</span><br/><br/><span class="hl-5">Cron</span><span class="hl-2">.</span><span class="hl-1">register</span><span class="hl-2">({ </span><span class="hl-5">name:</span><span class="hl-2"> </span><span class="hl-3">&#39;warm-cache&#39;</span><span class="hl-2">, </span><span class="hl-0">// fire-once, global</span><br/><span class="hl-2"> </span><span class="hl-1">handler</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> () </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">warmupCache</span><span class="hl-2">(); } });</span><br/><br/><span class="hl-5">Cron</span><span class="hl-2">.</span><span class="hl-1">enable</span><span class="hl-2">(); </span><span class="hl-0">// wire to engine lifecycle once; every tick is forwarded automatically</span>
</code><button type="button">Copy</button></pre>
<p><code>enable()</code> merges four lifecycle subjects (<code>beforeStart</code>, <code>idlePing</code>, <code>activePing</code>, <code>schedulePing</code>) into one serial queue via <code>singlerun</code>; each tick is base-aligned to the minute. Coordination keys <code>${name}:${alignedMs}:${symbol?}:g${generation}</code> give mutex semantics — parallel backtests on the same boundary share one in-flight promise (first opens the slot, others await). Fire-once marks record only on success, so a failed handler retries; the generation suffix isolates re-registrations from late writes.</p>
</details>
<a id="12-you-shouldnt-have-to-abandon-tradingview-or-python-to-use-typescript" class="tsd-anchor"></a><h3 class="tsd-anchor-link">12. You shouldn't have to abandon TradingView or Python to use TypeScript<a href="#12-you-shouldnt-have-to-abandon-tradingview-or-python-to-use-typescript" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>The honest objection to a TS trading engine is &quot;but my indicators live in Pine Script and TA-Lib.&quot; So they don't have to move. Run native Pine Script, run Python via WASM, use 50+ built-in indicators, or drop in zero-dependency quant ports — all under the same temporal guarantees.</p>
<details>
<summary>The Code</summary>
<p><strong>Pine Script</strong> — v5/v6, 60+ indicators, 1:1 syntax, look-ahead-safe (<a href="https://www.npmjs.com/package/@backtest-kit/pinets"><code>@backtest-kit/pinets</code></a>):</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">File</span><span class="hl-2">, </span><span class="hl-5">getSignal</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;@backtest-kit/pinets&#39;</span><span class="hl-2">;</span><br/><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">signal</span><span class="hl-2"> = </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getSignal</span><span class="hl-2">(</span><span class="hl-5">File</span><span class="hl-2">.</span><span class="hl-1">fromPath</span><span class="hl-2">(</span><span class="hl-3">&#39;strategy.pine&#39;</span><span class="hl-2">),</span><br/><span class="hl-2"> { </span><span class="hl-5">symbol:</span><span class="hl-2"> </span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">, </span><span class="hl-5">timeframe:</span><span class="hl-2"> </span><span class="hl-3">&#39;5m&#39;</span><span class="hl-2">, </span><span class="hl-5">limit:</span><span class="hl-2"> </span><span class="hl-8">100</span><span class="hl-2"> }); </span><span class="hl-0">// plots: Signal/Close/StopLoss/TakeProfit/EstimatedTime</span>
</code><button type="button">Copy</button></pre>
<p><strong>50+ indicators across 1m/15m/30m/1h + order book, as LLM-ready Markdown, in one call</strong> (<a href="https://www.npmjs.com/package/@backtest-kit/signals"><code>@backtest-kit/signals</code></a>):</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">commitHistorySetup</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;@backtest-kit/signals&#39;</span><span class="hl-2">;</span><br/><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">commitHistorySetup</span><span class="hl-2">(</span><span class="hl-3">&#39;BTCUSDT&#39;</span><span class="hl-2">, </span><span class="hl-5">messages</span><span class="hl-2">); </span><span class="hl-0">// order book + candles + indicators, cached per TTL</span>
</code><button type="button">Copy</button></pre>
<p><strong>Typed DAG</strong> of computations, resolved in topological order with <code>Promise.all</code> parallelism, serializable to a DB (<a href="https://www.npmjs.com/package/@backtest-kit/graph"><code>@backtest-kit/graph</code></a>):</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">sourceNode</span><span class="hl-2">, </span><span class="hl-5">outputNode</span><span class="hl-2">, </span><span class="hl-5">resolve</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;@backtest-kit/graph&#39;</span><span class="hl-2">;</span><br/><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">higher</span><span class="hl-2"> = </span><span class="hl-1">sourceNode</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> </span><span class="hl-1">extract</span><span class="hl-2">(</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">run</span><span class="hl-2">(</span><span class="hl-5">File</span><span class="hl-2">.</span><span class="hl-1">fromPath</span><span class="hl-2">(</span><span class="hl-3">&#39;timeframe_4h.pine&#39;</span><span class="hl-2">), { </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">timeframe:</span><span class="hl-2"> </span><span class="hl-3">&#39;4h&#39;</span><span class="hl-2">, </span><span class="hl-5">limit:</span><span class="hl-2"> </span><span class="hl-8">100</span><span class="hl-2"> }), { </span><span class="hl-5">allowLong:</span><span class="hl-2"> </span><span class="hl-3">&#39;AllowLong&#39;</span><span class="hl-2">, </span><span class="hl-5">allowShort:</span><span class="hl-2"> </span><span class="hl-3">&#39;AllowShort&#39;</span><span class="hl-2">, </span><span class="hl-5">noTrades:</span><span class="hl-2"> </span><span class="hl-3">&#39;NoTrades&#39;</span><span class="hl-2"> }));</span><br/><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">lower</span><span class="hl-2"> = </span><span class="hl-1">sourceNode</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> </span><span class="hl-1">extract</span><span class="hl-2">(</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">run</span><span class="hl-2">(</span><span class="hl-5">File</span><span class="hl-2">.</span><span class="hl-1">fromPath</span><span class="hl-2">(</span><span class="hl-3">&#39;timeframe_15m.pine&#39;</span><span class="hl-2">), { </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">timeframe:</span><span class="hl-2"> </span><span class="hl-3">&#39;15m&#39;</span><span class="hl-2">, </span><span class="hl-5">limit:</span><span class="hl-2"> </span><span class="hl-8">100</span><span class="hl-2"> }), { </span><span class="hl-5">position:</span><span class="hl-2"> </span><span class="hl-3">&#39;Signal&#39;</span><span class="hl-2">, </span><span class="hl-5">priceOpen:</span><span class="hl-2"> </span><span class="hl-3">&#39;Close&#39;</span><span class="hl-2">, </span><span class="hl-5">priceTakeProfit:</span><span class="hl-2"> </span><span class="hl-3">&#39;TakeProfit&#39;</span><span class="hl-2">, </span><span class="hl-5">priceStopLoss:</span><span class="hl-2"> </span><span class="hl-3">&#39;StopLoss&#39;</span><span class="hl-2"> }));</span><br/><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">mtf</span><span class="hl-2"> = </span><span class="hl-1">outputNode</span><span class="hl-2">(([</span><span class="hl-5">h</span><span class="hl-2">, </span><span class="hl-5">l</span><span class="hl-2">]) </span><span class="hl-4">=&gt;</span><span class="hl-2"> { </span><span class="hl-0">// combine; null when timeframes disagree</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">h</span><span class="hl-2">.</span><span class="hl-5">noTrades</span><span class="hl-2"> || </span><span class="hl-5">l</span><span class="hl-2">.</span><span class="hl-5">position</span><span class="hl-2"> === </span><span class="hl-8">0</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2"> </span><span class="hl-4">null</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">h</span><span class="hl-2">.</span><span class="hl-5">allowShort</span><span class="hl-2"> &amp;&amp; </span><span class="hl-5">l</span><span class="hl-2">.</span><span class="hl-5">position</span><span class="hl-2"> === </span><span class="hl-8">1</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2"> </span><span class="hl-4">null</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-5">h</span><span class="hl-2">.</span><span class="hl-5">allowLong</span><span class="hl-2"> &amp;&amp; </span><span class="hl-5">l</span><span class="hl-2">.</span><span class="hl-5">position</span><span class="hl-2"> === -</span><span class="hl-8">1</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2"> </span><span class="hl-4">null</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">return</span><span class="hl-2"> </span><span class="hl-1">toSignalDto</span><span class="hl-2">(</span><span class="hl-1">randomString</span><span class="hl-2">(), </span><span class="hl-5">l</span><span class="hl-2">, </span><span class="hl-4">null</span><span class="hl-2">);</span><br/><span class="hl-2">}, </span><span class="hl-5">higher</span><span class="hl-2">, </span><span class="hl-5">lower</span><span class="hl-2">);</span><br/><span class="hl-1">addStrategySchema</span><span class="hl-2">({ </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">&#39;mtf&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;5m&#39;</span><span class="hl-2">, </span><span class="hl-1">getSignal</span><span class="hl-5">:</span><span class="hl-2"> () </span><span class="hl-4">=&gt;</span><span class="hl-2"> </span><span class="hl-1">resolve</span><span class="hl-2">(</span><span class="hl-5">mtf</span><span class="hl-2">) });</span>
</code><button type="button">Copy</button></pre>
<p><strong>Python via WASM (WASI)</strong> runs <code>ta-lib</code>/<code>pandas</code>/<code>scikit-learn</code> indicators in the Node event loop with no IPC. And zero-dependency TS ports of the math behind vectorbt — see <a href="#-see-also">See also</a>.</p>
</details>
<a id="13-ai-strategies-without-ten-provider-sdks" class="tsd-anchor"></a><h3 class="tsd-anchor-link">13. AI strategies without ten provider SDKs<a href="#13-ai-strategies-without-ten-provider-sdks" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>LLM-driven signals normally mean per-provider boilerplate and JSON you can't trust. One HOF API spans 10+ providers; structured output is schema-enforced; trading context is injected automatically.</p>
<details>
<summary>The Code</summary>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">deepseek</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;@backtest-kit/ollama&#39;</span><span class="hl-2">;</span><br/><span class="hl-1">addStrategy</span><span class="hl-2">({</span><br/><span class="hl-2"> </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">&#39;llm-signal&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;5m&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-0">// swap deepseek() → claude() / gpt5() / ollama() with no other change</span><br/><span class="hl-2"> </span><span class="hl-5">getSignal:</span><span class="hl-2"> </span><span class="hl-1">deepseek</span><span class="hl-2">(</span><span class="hl-5">getSignal</span><span class="hl-2">, </span><span class="hl-3">&#39;deepseek-chat&#39;</span><span class="hl-2">, </span><span class="hl-5">process</span><span class="hl-2">.</span><span class="hl-5">env</span><span class="hl-2">.</span><span class="hl-7">DEEPSEEK_API_KEY</span><span class="hl-2">),</span><br/><span class="hl-2">});</span>
</code><button type="button">Copy</button></pre>
<p>Providers: OpenAI, Claude, DeepSeek, Grok, Mistral, Perplexity, Cohere, Alibaba, Hugging Face, Ollama (local), GLM-4. Structured output is enforced with Zod / JSON schema via <code>addOutline</code> (auto-retry on malformed output, custom rules like &quot;SL must be below entry for LONG&quot;); token rotation accepts a key array; prompts live in <code>config/prompt/*.cjs</code> and are memoized to kill redundant backtest API calls. The full LLM strategy — fetch multi-timeframe candles, ask the model, dump the reasoning, return a validated signal:</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">v4</span><span class="hl-2"> </span><span class="hl-6">as</span><span class="hl-2"> </span><span class="hl-5">uuid</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;uuid&#39;</span><span class="hl-2">;</span><br/><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">addStrategySchema</span><span class="hl-2">, </span><span class="hl-5">getCandles</span><span class="hl-2">, </span><span class="hl-5">dumpAgentAnswer</span><span class="hl-2">, </span><span class="hl-5">dumpRecord</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;backtest-kit&#39;</span><span class="hl-2">;</span><br/><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">json</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;./utils/json.mjs&#39;</span><span class="hl-2">;</span><br/><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">getMessages</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;./utils/messages.mjs&#39;</span><span class="hl-2">;</span><br/><br/><span class="hl-1">addStrategySchema</span><span class="hl-2">({</span><br/><span class="hl-2"> </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">&#39;llm-strategy&#39;</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">&#39;5m&#39;</span><span class="hl-2">, </span><span class="hl-5">riskName:</span><span class="hl-2"> </span><span class="hl-3">&#39;demo&#39;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-1">getSignal</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">messages</span><span class="hl-2"> = </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getMessages</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, {</span><br/><span class="hl-2"> </span><span class="hl-5">candles1h:</span><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;1h&#39;</span><span class="hl-2">, </span><span class="hl-8">24</span><span class="hl-2">),</span><br/><span class="hl-2"> </span><span class="hl-5">candles15m:</span><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;15m&#39;</span><span class="hl-2">, </span><span class="hl-8">48</span><span class="hl-2">),</span><br/><span class="hl-2"> </span><span class="hl-5">candles5m:</span><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;5m&#39;</span><span class="hl-2">, </span><span class="hl-8">60</span><span class="hl-2">),</span><br/><span class="hl-2"> </span><span class="hl-5">candles1m:</span><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getCandles</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-3">&#39;1m&#39;</span><span class="hl-2">, </span><span class="hl-8">60</span><span class="hl-2">),</span><br/><span class="hl-2"> });</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">resultId</span><span class="hl-2"> = </span><span class="hl-1">uuid</span><span class="hl-2">();</span><br/><span class="hl-2"> </span><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">signal</span><span class="hl-2"> = </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">json</span><span class="hl-2">(</span><span class="hl-5">messages</span><span class="hl-2">); </span><span class="hl-0">// LLM → structured signal</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">dumpAgentAnswer</span><span class="hl-2">({ </span><span class="hl-5">dumpId:</span><span class="hl-2"> </span><span class="hl-3">&#39;position-context&#39;</span><span class="hl-2">, </span><span class="hl-5">bucketName:</span><span class="hl-2"> </span><span class="hl-3">&#39;mtf&#39;</span><span class="hl-2">, </span><span class="hl-5">messages</span><span class="hl-2">, </span><span class="hl-5">description:</span><span class="hl-2"> </span><span class="hl-3">&#39;agent reasoning&#39;</span><span class="hl-2"> });</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">dumpRecord</span><span class="hl-2">({ </span><span class="hl-5">dumpId:</span><span class="hl-2"> </span><span class="hl-3">&#39;position-entry&#39;</span><span class="hl-2">, </span><span class="hl-5">bucketName:</span><span class="hl-2"> </span><span class="hl-3">&#39;mtf&#39;</span><span class="hl-2">, </span><span class="hl-5">record:</span><span class="hl-2"> </span><span class="hl-5">signal</span><span class="hl-2">, </span><span class="hl-5">description:</span><span class="hl-2"> </span><span class="hl-3">&#39;signal params&#39;</span><span class="hl-2"> });</span><br/><span class="hl-2"> </span><span class="hl-6">return</span><span class="hl-2"> { ...</span><span class="hl-5">signal</span><span class="hl-2">, </span><span class="hl-5">id:</span><span class="hl-2"> </span><span class="hl-5">resultId</span><span class="hl-2"> };</span><br/><span class="hl-2"> },</span><br/><span class="hl-2">});</span>
</code><button type="button">Copy</button></pre>
<p>Memory adapters persist LLM reasoning per signal (BM25 search, soft delete); <code>dumpAgentAnswer</code> archives the full conversation — roles, reasoning, tool calls — attached to the signal, so an opaque model decision becomes a debuggable record.</p>
</details>
<hr>
<a id="the-api-assumes-you-will-make-every-mistake" class="tsd-anchor"></a><h2 class="tsd-anchor-link">The API assumes you will make every mistake<a href="#the-api-assumes-you-will-make-every-mistake" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>Read back through the rakes and a pattern shows: none of them are solved by <em>telling you to be careful</em>. Look-ahead bias isn't prevented by a lint rule — there's simply no timestamp to pass. Averaging up isn't discouraged in the docs — the call is rejected. A closed position's live PnL isn't a runtime guard — it doesn't compile. The whole surface is built on the assumption that you, or the model writing your strategy, will eventually do the wrong thing at 3 a.m. — so the wrong thing is made unreachable. This is the &quot;pit of success&quot;: the easy path and the correct path are the same path.</p>
<p>And the shape of that surface is <strong>reactive — React for traders.</strong> You never write the time loop. You don't iterate candles, advance a clock, or poll for fills. You <em>declare reactions</em> to lifecycle events, and the engine owns the loop in both backtest and live. <code>getSignal</code> is your pure render function — given the current state of the world, return a signal or <code>null</code>. The <code>listen*</code> family is your effects layer — small handlers that fire when the position's state changes, exactly like subscribing to state in a component. Composition is additive: stack independent listeners and each one minds its own concern, the same way you'd split hooks.</p>
<details>
<summary>The Code</summary>
<p><code>getSignal</code> declares <em>what</em> to open; the listeners declare <em>how the position behaves once alive</em> — a DCA ladder, a profit target, and an error sink, three independent reactions to the same event stream, no shared loop, no manual bookkeeping:</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-5">addStrategySchema</span><span class="hl-2">, </span><span class="hl-5">listenActivePing</span><span class="hl-2">, </span><span class="hl-5">listenError</span><span class="hl-2">, </span><span class="hl-5">Log</span><span class="hl-2">, </span><span class="hl-5">Position</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">commitAverageBuy</span><span class="hl-2">, </span><span class="hl-5">commitClosePending</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-5">getPositionEntries</span><span class="hl-2">, </span><span class="hl-5">getPositionEntryOverlap</span><span class="hl-2">, </span><span class="hl-5">getPositionPnlPercent</span><span class="hl-2">,</span><br/><span class="hl-2">} </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&quot;backtest-kit&quot;</span><span class="hl-2">;</span><br/><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">errorData</span><span class="hl-2">, </span><span class="hl-5">getErrorMessage</span><span class="hl-2">, </span><span class="hl-5">str</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&quot;functools-kit&quot;</span><span class="hl-2">;</span><br/><br/><span class="hl-4">const</span><span class="hl-2"> </span><span class="hl-7">HARD_STOP</span><span class="hl-2"> = </span><span class="hl-8">25</span><span class="hl-2">, </span><span class="hl-7">TARGET_PROFIT</span><span class="hl-2"> = </span><span class="hl-8">3</span><span class="hl-2">, </span><span class="hl-7">STEP</span><span class="hl-2"> = </span><span class="hl-8">100</span><span class="hl-2">, </span><span class="hl-7">MAX_STEPS</span><span class="hl-2"> = </span><span class="hl-8">10</span><span class="hl-2">;</span><br/><br/><span class="hl-0">// render: given &quot;now&quot;, declare the position to open (or null to stay flat)</span><br/><span class="hl-1">addStrategySchema</span><span class="hl-2">({</span><br/><span class="hl-2"> </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">&quot;apr_2026_strategy&quot;</span><span class="hl-2">,</span><br/><span class="hl-2"> </span><span class="hl-1">getSignal</span><span class="hl-5">:</span><span class="hl-2"> </span><span class="hl-4">async</span><span class="hl-2"> (</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">when</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> ({</span><br/><span class="hl-2"> </span><span class="hl-5">position:</span><span class="hl-2"> </span><span class="hl-3">&quot;long&quot;</span><span class="hl-2">,</span><br/><span class="hl-2"> ...</span><span class="hl-5">Position</span><span class="hl-2">.</span><span class="hl-1">moonbag</span><span class="hl-2">({ </span><span class="hl-5">position:</span><span class="hl-2"> </span><span class="hl-3">&quot;long&quot;</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">, </span><span class="hl-5">percentStopLoss:</span><span class="hl-2"> </span><span class="hl-7">HARD_STOP</span><span class="hl-2"> }),</span><br/><span class="hl-2"> </span><span class="hl-5">minuteEstimatedTime:</span><span class="hl-2"> </span><span class="hl-4">Infinity</span><span class="hl-2">, </span><span class="hl-5">cost:</span><span class="hl-2"> </span><span class="hl-7">STEP</span><span class="hl-2">,</span><br/><span class="hl-2"> }),</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-0">// effect: average into dips, up to 10 overlap-free rungs (averaging up is rejected for you)</span><br/><span class="hl-1">listenActivePing</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> ({ </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> ((</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getPositionEntries</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">)).</span><span class="hl-5">length</span><span class="hl-2"> &gt;= </span><span class="hl-7">MAX_STEPS</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getPositionEntryOverlap</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">currentPrice</span><span class="hl-2">, { </span><span class="hl-5">upperPercent:</span><span class="hl-2"> </span><span class="hl-8">5</span><span class="hl-2">, </span><span class="hl-5">lowerPercent:</span><span class="hl-2"> </span><span class="hl-8">1</span><span class="hl-2"> })) </span><span class="hl-6">return</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">commitAverageBuy</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-7">STEP</span><span class="hl-2">);</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-0">// effect: close the whole position once blended PnL clears the target</span><br/><span class="hl-1">listenActivePing</span><span class="hl-2">(</span><span class="hl-4">async</span><span class="hl-2"> ({ </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">data</span><span class="hl-2"> }) </span><span class="hl-4">=&gt;</span><span class="hl-2"> {</span><br/><span class="hl-2"> </span><span class="hl-6">if</span><span class="hl-2"> (</span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">getPositionPnlPercent</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">) &lt; </span><span class="hl-7">TARGET_PROFIT</span><span class="hl-2">) </span><span class="hl-6">return</span><span class="hl-2">;</span><br/><span class="hl-2"> </span><span class="hl-5">Log</span><span class="hl-2">.</span><span class="hl-1">info</span><span class="hl-2">(</span><span class="hl-3">&quot;position closed due to the target pnl reached&quot;</span><span class="hl-2">, { </span><span class="hl-5">symbol</span><span class="hl-2">, </span><span class="hl-5">data</span><span class="hl-2"> });</span><br/><span class="hl-2"> </span><span class="hl-6">await</span><span class="hl-2"> </span><span class="hl-1">commitClosePending</span><span class="hl-2">(</span><span class="hl-5">symbol</span><span class="hl-2">, { </span><span class="hl-5">id:</span><span class="hl-2"> </span><span class="hl-3">&quot;unknown&quot;</span><span class="hl-2">, </span><span class="hl-5">note:</span><span class="hl-2"> </span><span class="hl-5">str</span><span class="hl-2">.</span><span class="hl-1">newline</span><span class="hl-2">(</span><span class="hl-3">&quot;# Closed by target pnl&quot;</span><span class="hl-2">) });</span><br/><span class="hl-2">});</span><br/><br/><span class="hl-0">// effect: a single place for anything that goes wrong</span><br/><span class="hl-1">listenError</span><span class="hl-2">((</span><span class="hl-5">error</span><span class="hl-2">) </span><span class="hl-4">=&gt;</span><span class="hl-2"> </span><span class="hl-5">Log</span><span class="hl-2">.</span><span class="hl-1">debug</span><span class="hl-2">(</span><span class="hl-3">&quot;error&quot;</span><span class="hl-2">, { </span><span class="hl-5">error:</span><span class="hl-2"> </span><span class="hl-1">errorData</span><span class="hl-2">(</span><span class="hl-5">error</span><span class="hl-2">), </span><span class="hl-5">message:</span><span class="hl-2"> </span><span class="hl-1">getErrorMessage</span><span class="hl-2">(</span><span class="hl-5">error</span><span class="hl-2">) }));</span>
</code><button type="button">Copy</button></pre>
<p>The full reactive surface — subscribe to any point in a position's life and the engine fires it in order, queued, never overlapping: <code>listenSignal</code> / <code>listenSignalBacktest</code> / <code>listenSignalLive</code> (lifecycle), <code>listenActivePing</code> (per-minute while a position is live), <code>listenSchedulePing</code> / <code>listenIdlePing</code>, <code>listenPartialProfit</code> / <code>listenPartialLoss</code>, <code>listenBreakevenAvailable</code>, <code>listenHighestProfit</code>, <code>listenMaxDrawdown</code>, <code>listenRisk</code> (rejections), <code>listenError</code> / <code>listenExit</code>, <code>listenDone*</code>, plus <code>*Once</code> filtered variants for one-shot reactions. You compose behavior by adding handlers, not by editing a loop.</p>
</details>
<details>
<summary>The Proof</summary>
<p>The five guarantees that make the surface fool-proof, each enforced by the engine rather than by convention:</p>
<ol>
<li><strong>Ambient temporal context</strong> — no <code>currentDate</code>/<code>timestamp</code> parameter exists to forget; the engine resolves &quot;now&quot; from <code>AsyncLocalStorage</code> and blocks future data at the adapter level.</li>
<li><strong>Type-safe state machine</strong><code>idle → scheduled → pending → opened → active → closed</code> as discriminated unions; calling a close on an already-closed signal, or editing an active trade's entry, is a compile error.</li>
<li><strong>Guarded DCA</strong><code>commitAverageBuy</code> rejects any call that would worsen the harmonic-mean effective entry; you cannot accidentally average up.</li>
<li><strong>Transactional broker commits (the &quot;no-try-catch&quot; rule)</strong> — the adapter intercepts every mutation before internal state changes; an exchange throw rolls back and retries on the next tick, so you never hand-write rollback.</li>
<li><strong>Automatic signal validation</strong> — TP/SL soundness, R/R minimum, and interval throttling are checked before a signal reaches execution; invalid signals are logged or rejected, never run.</li>
</ol>
<p>Because the loop belongs to the engine, the <em>same</em> declarations run identically in backtest and live — the reactive model is the reason &quot;same code, both modes&quot; is structurally true, not just aspirational.</p>
</details>
<hr>
<a id="receipts" class="tsd-anchor"></a><h2 class="tsd-anchor-link">Receipts<a href="#receipts" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>Toy READMEs prove a moving-average crossover on daily candles. These are nine production-quality strategies, each a <em>different</em> signal source, each backtested on real history with the numbers written down. They live in <a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example"><code>/example</code></a> — clone it, run it, get the same prints.</p>
<table>
<thead>
<tr>
<th>Strategy</th>
<th>Ticker · Period</th>
<th>Signal source</th>
<th style="text-align:right">Net PNL</th>
<th style="text-align:right">Sharpe</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/oct_2021.strategy">Neural Network</a></td>
<td>BTC · Oct 2021</td>
<td>TensorFlow NN (8→6→4→1) predicting next-candle close</td>
<td style="text-align:right"><strong>+18.26%</strong></td>
<td style="text-align:right">0.31</td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/feb_2021.strategy">Python EMA Crossover</a></td>
<td>DOT · Feb 2021</td>
<td>EMA(9)/EMA(21) via WebAssembly (WASI)</td>
<td style="text-align:right"><strong>+5.52%</strong></td>
<td style="text-align:right">0.09</td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/apr_2024.strategy">Polymarket Δprob</a></td>
<td>BTC · Apr 2024</td>
<td>Prediction-market probability shifts</td>
<td style="text-align:right"><strong>+0.63%</strong></td>
<td style="text-align:right">0.065</td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/dec_2025.strategy">Pine Script Range Breakout</a></td>
<td>BTC · Dec 2025</td>
<td>Bollinger + range + volume spike (Pine)</td>
<td style="text-align:right"><strong>+2.40%</strong></td>
<td style="text-align:right">0.06</td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/jan_2026.strategy">Liquidity Harvesting</a></td>
<td>TRX · Jan 2026</td>
<td>Telegram channel signals, <strong>inverted</strong></td>
<td style="text-align:right"><strong>+8.58%</strong></td>
<td style="text-align:right"><strong>1.14</strong></td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/feb_2026.strategy">AI News Sentiment</a></td>
<td>BTC · Feb 2026</td>
<td>LLM on live news (Tavily + Ollama)</td>
<td style="text-align:right"><strong>+16.99%</strong></td>
<td style="text-align:right">0.25</td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/mar_2026.strategy">SHORT DCA Ladder</a></td>
<td>BTC · Mar 2026</td>
<td>Fixed SHORT + ladder up (≤10 rungs)</td>
<td style="text-align:right"><strong>+37.83%</strong></td>
<td style="text-align:right">0.35</td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/apr_2026.strategy">LONG DCA Ladder</a></td>
<td>BTC · Apr 2026</td>
<td>Fixed LONG + ladder down (≤10 rungs)</td>
<td style="text-align:right"><strong>+67.85%</strong></td>
<td style="text-align:right">0.12</td>
</tr>
<tr>
<td><a href="https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/jun_2026.strategy">Crowd Liquidity</a></td>
<td>BTC · Jun 2026</td>
<td>TradingView ideas, <code>Simulator</code>-trained author whitelist</td>
<td style="text-align:right"><strong>+19.80%</strong></td>
<td style="text-align:right"><strong>0.64</strong></td>
</tr>
</tbody>
</table>
<details>
<summary>The Proof</summary>
<ul>
<li><strong>Liquidity Harvesting (Sharpe 1.14)</strong> — a Telegram channel published SHORT signals with ~0.375:1 R/R and 106% deposit at risk at 25× leverage, mathematically guaranteed to lose; a volume spike appeared 15 min before every post and the TP step multipliers were identical across signals — an algorithm. Inverting it turned <strong>−5.05% → +8.58%</strong>, profit factor <strong>0.56 → 7.31</strong>. The edge was the bot crowd, not the indicators.</li>
<li><strong>Crowd Liquidity</strong> — 462 TradingView ideas, 167 authors, and a falling knife of a month (BTC −20.4%). Following the raw crowd lost −24.23%; the framework's <code>Simulator</code> entity grid-searched the author ban rule itself (minimum track × minimum hit rate among 3,456 points) and kept <strong>5 authors of 167</strong>. Trading any post of those five: 10 trades, 90% WR, <strong>+19.80%</strong> — the whitelist artifact is generated by <code>scripts/simulator.mjs</code>, not curated by hand. Stated caveat: trained and traded on the same month; the July out-of-sample is the open question.</li>
<li><strong>AI News Sentiment</strong> held SHORT through nearly all of a −16.4% month, flipped to LONG on the recovery bounce, and flipped back on geopolitical news — <strong>+16.99%</strong> where buy-and-hold lost 16%.</li>
<li><strong>DCA Ladders</strong> show the trade-off honestly: high % return on deployed capital, but absolute fiat risk grows with rungs (Mar: −$104.93 on a 10-rung position; theoretical max −$2,500 if a non-reverting trend hits the 25% hard stop with all rungs filled). The README states the downside, not just the upside.</li>
</ul>
<p>Every example documents price context, trade log, equity curve, and risk analysis — and several ship a <code>--noDCA</code> / single-entry variant so you can see exactly what the position management bought you.</p>
</details>
<hr>
<a id="how-it-sits-next-to-the-alternatives" class="tsd-anchor"></a><h2 class="tsd-anchor-link">How it sits next to the alternatives<a href="#how-it-sits-next-to-the-alternatives" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>The honest version: for a quick research prototype or a single MA crossover, VectorBT or Backtrader are hard to beat on raw speed. The moment you need to <em>deploy</em> — complex position sizing, AI agents, a network outage that mustn't desync your bot — is where the guardrails below start to matter.</p>
<table>
<thead>
<tr>
<th></th>
<th>Backtest Kit</th>
<th>Backtrader</th>
<th>VectorBT</th>
<th>MetaTrader/MQL5</th>
<th>QuantConnect</th>
<th>Freqtrade</th>
</tr>
</thead>
<tbody>
<tr>
<td>Language</td>
<td>TypeScript</td>
<td>Python</td>
<td>Python</td>
<td>MQL5</td>
<td>C#/Python</td>
<td>Python</td>
</tr>
<tr>
<td>Live trading</td>
<td>✅ built-in</td>
<td>⚠️ manual</td>
<td>❌ research</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Look-ahead prevention</td>
<td>✅ engine-enforced</td>
<td>⚠️ discipline</td>
<td>⚠️ discipline</td>
<td>⚠️ discipline</td>
<td>⚠️ partial</td>
<td>⚠️ partial</td>
</tr>
<tr>
<td>Crash-safe persistence</td>
<td>✅ atomic + Mongo</td>
<td></td>
<td></td>
<td></td>
<td>⚠️ cloud</td>
<td>⚠️ basic</td>
</tr>
<tr>
<td>Transactional broker</td>
<td>✅ auto rollback</td>
<td></td>
<td></td>
<td></td>
<td>⚠️ partial</td>
<td>⚠️ basic</td>
</tr>
<tr>
<td>Type-safe state machine</td>
<td>✅ compile-time</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>DCA / partial closes</td>
<td>✅ first-class</td>
<td>⚠️ manual</td>
<td>⚠️ manual</td>
<td>⚠️ manual</td>
<td>⚠️ manual</td>
<td>⚠️ limited</td>
</tr>
<tr>
<td>AI / LLM integration</td>
<td>✅ built-in</td>
<td></td>
<td></td>
<td></td>
<td>⚠️ custom</td>
<td></td>
</tr>
<tr>
<td>Pine Script</td>
<td>✅ native</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Self-hosted</td>
<td>✅ 100%</td>
<td></td>
<td></td>
<td>⚠️ desktop</td>
<td>❌ cloud</td>
<td></td>
</tr>
</tbody>
</table>
<p>Open-source QuantConnect/MetaTrader without the lock-in: pure TypeScript, your code, your data, your machines, no platform fees, no proprietary GUI. Drop any library into <code>getSignal</code> — Ollama, <a href="https://www.npmjs.com/package/neural-trader"><code>neural-trader</code></a>, your own.</p>
<hr>
<a id="🌍-ecosystem" class="tsd-anchor"></a><h2 class="tsd-anchor-link">🌍 Ecosystem<a href="#🌍-ecosystem" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p><strong>The core is a library; the CLI is the framework on top — and the framework is optional.</strong> Think React vs Next.js. <code>backtest-kit</code> (the reactive engine — <code>getSignal</code> + the <code>listen*</code>/<code>commit*</code> API) is the library you build against directly. <code>@backtest-kit/cli</code> is the Next.js: it wires the runner, candle cache, dashboard, Telegram, and graceful shutdown so you don't have to — but you can ignore it entirely and call <code>Backtest.run()</code> / <code>Live.background()</code> yourself. <code>@backtest-kit/sidekick</code> is the explicit middle ground — it scaffolds a project where every wire (exchange adapter, frames, risk rules, strategy, runner) lives as plain, editable source in <strong>your</strong> userspace, with no CLI in the loop and nothing hidden. You pick how much magic you want.</p>
<p>On the &quot;dependency zoo&quot;: every package below is authored by one team and shipped by the commercial vendor <a href="https://theonetrade.github.io">TheOneTrade</a> — versioned together, released together. Treat it like the .NET base class library: a single coherent contract where the userspace surface (<code>getSignal</code>, <code>commit*</code>, <code>listen*</code>, <code>get*</code>) does not churn under you between releases. You install only what a given strategy needs, and the heavy or platform-specific pieces (Python-via-WASM, TensorFlow builds) sit behind their own optional packages so the core install stays clean and conflict-free.</p>
<a id="backtest-kitcli--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/cli</code><a href="https://www.npmjs.com/package/@backtest-kit/cli">npm</a><a href="#backtest-kitcli--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Zero-boilerplate runner. Modes: <code>--backtest / --paper / --live / --walker / --main / --pine / --editor / --dump / --pnldebug / --brokerdebug / --flush / --init / --docker</code>. Auto candle caching, monorepo cwd-resolution with per-strategy <code>.env</code> override, folder-based import aliases, broker module hooks, <code>setup.config</code> / <code>loader.config</code> / <code>alias.config</code>, graceful SIGINT.</p>
<pre><code class="bash"><span class="hl-1">npx</span><span class="hl-2"> </span><span class="hl-4">-y</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/cli</span><span class="hl-2"> </span><span class="hl-4">--init</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitpinets--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/pinets</code><a href="https://www.npmjs.com/package/@backtest-kit/pinets">npm</a><a href="#backtest-kitpinets--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Run TradingView Pine Script v5/v6 in Node, 60+ indicators, 1:1 syntax, <code>getSignal</code> / <code>run</code> / <code>extract</code> / <code>extractRows</code>.</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/pinets</span><span class="hl-2"> </span><span class="hl-3">pinets</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitgraph--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/graph</code><a href="https://www.npmjs.com/package/@backtest-kit/graph">npm</a><a href="#backtest-kitgraph--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Compose computations as a typed DAG; resolved in topological order with <code>Promise.all</code>, serializable to a DB for storage.</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/graph</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitui--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/ui</code><a href="https://www.npmjs.com/package/@backtest-kit/ui">npm</a><a href="#backtest-kitui--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>React/MUI dashboard with Lightweight Charts: live signal-lifecycle state-machine view, per-signal inspection, risk/partial/trailing/breakeven views, manual control, Pine editor.</p>
<pre><code class="typescript"><span class="hl-6">import</span><span class="hl-2"> { </span><span class="hl-5">serve</span><span class="hl-2"> } </span><span class="hl-6">from</span><span class="hl-2"> </span><span class="hl-3">&#39;@backtest-kit/ui&#39;</span><span class="hl-2">;</span><br/><span class="hl-1">serve</span><span class="hl-2">(</span><span class="hl-3">&#39;0.0.0.0&#39;</span><span class="hl-2">, </span><span class="hl-8">60050</span><span class="hl-2">); </span><span class="hl-0">// http://localhost:60050</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitmongo--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/mongo</code><a href="https://www.npmjs.com/package/@backtest-kit/mongo">npm</a><a href="#backtest-kitmongo--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>MongoDB source-of-truth + Redis O(1) cache. All 15 persistence contracts, atomic upserts, soft delete, look-ahead-safe <code>when</code>. Zero strategy changes.</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/mongo</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span><span class="hl-2"> </span><span class="hl-3">mongoose</span><span class="hl-2"> </span><span class="hl-3">ioredis</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitpg--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/pg</code><a href="https://www.npmjs.com/package/@backtest-kit/pg">npm</a><a href="#backtest-kitpg--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>PostgreSQL + Redis O(1) cache via TypeORM. All 15 persistence contracts, atomic upserts, soft delete, look-ahead-safe <code>when</code>. Tuned for Pgpool-II so read fan-out scales across replicas: up to ~4× faster</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/pg</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span><span class="hl-2"> </span><span class="hl-3">typeorm</span><span class="hl-2"> </span><span class="hl-3">pg</span><span class="hl-2"> </span><span class="hl-3">ioredis</span><span class="hl-2"> </span><span class="hl-3">reflect-metadata</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitminio--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/minio</code><a href="https://www.npmjs.com/package/@backtest-kit/minio">npm</a><a href="#backtest-kitminio--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>MinIO (S3) source-of-truth + Redis time-ordered index. Listings in O(limit), zero schema management. Zero strategy changes.</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/minio</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span><span class="hl-2"> </span><span class="hl-3">minio</span><span class="hl-2"> </span><span class="hl-3">ioredis</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitollama--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/ollama</code><a href="https://www.npmjs.com/package/@backtest-kit/ollama">npm</a><a href="#backtest-kitollama--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Universal LLM adapter: 10+ providers, structured output, token rotation, fallback chains, trading-context injection.</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/ollama</span><span class="hl-2"> </span><span class="hl-3">agent-swarm-kit</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitsignals--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/signals</code><a href="https://www.npmjs.com/package/@backtest-kit/signals">npm</a><a href="#backtest-kitsignals--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>50+ indicators across 4 timeframes + order book, multi-timeframe synchronized, LLM-ready Markdown reports.</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/signals</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitmcp--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/mcp</code><a href="https://www.npmjs.com/package/@backtest-kit/mcp">npm</a><a href="#backtest-kitmcp--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>Model Context Protocol server: an LLM agent (Claude, any MCP client) watches the live portfolio and opens/closes positions through 3 guarded tools — TP/SL/cost stay engine-owned, stdio server talks to the trading process over HTTP.</p>
<pre><code class="bash"><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">install</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/mcp</span><span class="hl-2"> </span><span class="hl-3">backtest-kit</span><span class="hl-2"> </span><span class="hl-3">@modelcontextprotocol/sdk</span>
</code><button type="button">Copy</button></pre>
<a id="backtest-kitsidekick--npm" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><code>@backtest-kit/sidekick</code><a href="https://www.npmjs.com/package/@backtest-kit/sidekick">npm</a><a href="#backtest-kitsidekick--npm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><p>The &quot;eject&quot; of <code>--init</code>: scaffolds a project where exchange adapter, frames, risk rules, strategy, and runner are all editable source. 4H-trend + 15m-signal Pine template, partial profit taking, breakeven trailing.</p>
<pre><code class="bash"><span class="hl-1">npx</span><span class="hl-2"> </span><span class="hl-4">-y</span><span class="hl-2"> </span><span class="hl-3">@backtest-kit/sidekick</span><span class="hl-2"> </span><span class="hl-3">my-trading-bot</span><span class="hl-2"> &amp;&amp; </span><span class="hl-1">cd</span><span class="hl-2"> </span><span class="hl-3">my-trading-bot</span><span class="hl-2"> &amp;&amp; </span><span class="hl-1">npm</span><span class="hl-2"> </span><span class="hl-3">start</span>
</code><button type="button">Copy</button></pre>
<hr>
<a id="👨👩👦-community" class="tsd-anchor"></a><h2 class="tsd-anchor-link">👨‍👩‍👦 Community<a href="#👨👩👦-community" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>Real, runnable templates — not slideware. And worth naming the concern directly: yes, this is one author's ecosystem, which is exactly what makes it <em>coherent</em> — but coherent is not captive. Everything is <strong>MIT and open-source</strong>, the core engine has <strong>zero hard dependency</strong> on any <code>@backtest-kit/*</code> add-on (you can run <code>getSignal</code> + <code>listen*</code> against a bare <code>addExchangeSchema</code> and nothing else), and each repo below is an independent reference you're meant to <strong>fork and own</strong>. The lock-in you'd normally fear — a closed runtime, a proprietary data format, a cloud you can't leave — none of it applies; the persistence is plain files or your own Mongo, the signals are your code, and the exit cost is a <code>git clone</code>.</p>
<ul>
<li><strong><a href="https://github.com/backtest-kit/ai-trading-mcp">ai-trading-mcp</a></strong> — MCP-driven crypto trading rig. Claude (or any MCP client) trades a live portfolio through three guarded tools: get_status, open_position, close_position. Capable to read news and charts. Paper and live.</li>
<li><strong><a href="https://github.com/backtest-kit/backtest-monorepo-parallel">backtest-monorepo-parallel</a></strong> — 9 symbols in parallel in one Node process on shared Mongo+Redis, ~6,300× real-time, self-enforcement runtime exposing the workspace DI container to <code>./content/</code> strategy files. The scaling recipe: +1 service = +1 file, +1 provider, +1 ioc entry.</li>
<li><strong><a href="https://github.com/backtest-kit/backtest-ollama-crontab">backtest-ollama-crontab</a></strong> — a local Ollama (<code>gpt-oss</code> quantized) as a per-signal risk gate plus a 15-minute crontab ingesting any public Telegram channel; the <em>same code</em> re-polls live and bulk-prepares in backtest. Documented result: <strong>+52.22% → +68.90%</strong> with the LLM gate on.</li>
<li><strong><a href="https://github.com/backtest-kit/backtest-kit-redis-mongo-docker">backtest-kit-redis-mongo-docker</a></strong> — production persistence: all 15 adapters on Mongo+Redis, atomic read-after-write, <code>docker-compose</code> one-command deploy.</li>
<li><strong><a href="https://github.com/backtest-kit/backtest-kit-redis-postgres-pgpool-docker">backtest-kit-redis-postgres-pgpool-docker</a></strong> — backtest-kit persistence on PostgreSQL (Pgpool-II) + Redis cache, with atomic upserts and a replica cluster.</li>
<li><strong><a href="https://github.com/backtest-kit/backtest-kit-minio-s3-docker">backtest-kit-minio-s3-docker</a></strong> — persistence on MinIO (S3) with deterministic keys, S3-grade durability</li>
<li><strong><a href="https://github.com/backtest-kit/backtest-kit-skills">backtest-kit-skills</a></strong> — a Claude Code skill + Mintlify docs: describe a strategy in plain language, get working TypeScript with every schema registration wired. <code>npx skills add https://github.com/backtest-kit/backtest-kit-skills</code></li>
<li><strong><a href="https://github.com/backtest-kit/uzse-backtest-app">uzse-backtest-app</a></strong> — Pine Script on regional exchanges that aren't on TradingView (UZSE, MSE, DSE…): download raw trades, build candles, feed them through a custom Mongo exchange adapter.</li>
<li><strong><a href="https://github.com/backtest-kit/backtest-kit-docs">backtest-kit-docs</a></strong> — Architecture handbook and knowledge base: explains the engine's design, AI workflows, production patterns, and quantitative trading concepts beyond the API.</li>
<li><strong><a href="https://github.com/tripolskypetr/wallet-manager">wallet-manager</a></strong> — Binance spot wallet toolkit with an interactive REPL and a reference broker adapter. Encodes the typical adapter mistake most implementations trip over: trying to sell an asset while its funds are still frozen in a pending order — the correct sequence is to cancel the pending orders first, verify the book is clean, and only then sell with a new order. lets you vibe-code an adapter for any exchange on top of it.</li>
</ul>
<hr>
<a id="🪐-see-also" class="tsd-anchor"></a><h2 class="tsd-anchor-link">🪐 See also<a href="#🪐-see-also" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>Zero-dependency TypeScript ports of the quant math behind <a href="https://github.com/polakowo/vectorbt">vectorbt</a> — same models, native to the <code>Exchange</code> schema, no Python runtime. Each estimates a different dimension of speculative pressure and plugs in independently:</p>
<ul>
<li><strong><a href="https://www.npmjs.com/package/garch">garch</a></strong> — conditional variance of log-returns (GARCH / EGARCH / GJR-GARCH / HAR-RV / NoVaS, auto-selected by QLIKE) to bound how far flow can push price next candle; fitted <code>σ</code> → log-normal corridor <code>P·exp(±z·σ)</code> for TP/SL. Via <code>Exchange.getCandles</code>.</li>
<li><strong><a href="https://www.npmjs.com/package/pump-anomaly">pump-anomaly</a></strong> — coordinated-speculation detection: cross-correlation + union-find author clustering separates real multi-actor inflow from one actor on many channels; volume z-scores score cascade pressure (pump vs stop-hunt). Returns an entry/exit plan, exits fitted by OHLC replay and screened against winner's-curse (DSR / PBO / SPA). Via <code>Exchange.getRawCandles</code>.</li>
<li><strong><a href="https://www.npmjs.com/package/volume-anomaly">volume-anomaly</a></strong> — order-flow intensity: Hawkes branching ratio (arrival clustering), CUSUM (imbalance shift), BOCPD (regime break) → composite outlier score as an entry-timing gate. Via <code>Exchange.getAggregatedTrades</code>.</li>
</ul>
<hr>
<a id="🌐-internationalization" class="tsd-anchor"></a><h2 class="tsd-anchor-link">🌐 Internationalization<a href="#🌐-internationalization" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>The <code>@backtest-kit/ui</code> dashboard ships in <strong>7 languages</strong>: English, Русский, Türkçe, 中文, हिन्दी, Español, Português. Switch via the language picker in the header</p>
<details>
<summary>Locales</summary>
<ul>
<li>
<p>🇬🇧 <strong>English</strong> — ~1.5B speakers. Backtest Kit is a TypeScript engine where the strategy you test on history is byte-for-byte the one that trades live — only the clock changes. It removes the failure modes that kill bots (look-ahead bias, crash corruption, silent order rejects, averaging up) at the API level, then adds first-class DCA, partial closes, portfolio risk, and AI/Pine signals on top.</p>
</li>
<li>
<p>🇨🇳 <strong>中文</strong> — ~1.1B speakers. Backtest Kit 是一个 TypeScript 引擎:在历史数据上回测的策略代码,与实盘运行的代码逐字节一致,唯一区别只是时钟来源。它在 API 层面消除了让交易机器人崩溃的隐患(未来函数、崩溃损坏、静默拒单、越买越亏),并内置分批建仓、部分平仓、组合风控以及 AI/Pine 信号。</p>
</li>
<li>
<p>🇮🇳 <strong>हिन्दी</strong> — ~600M speakers. Backtest Kit एक TypeScript इंजन है जिसमें इतिहास पर परखा गया कोड ही बिना बदलाव के लाइव ट्रेड करता है — केवल घड़ी बदलती है। यह बॉट को बर्बाद करने वाली गलतियाँ (लुक-अहेड बायस, क्रैश करप्शन, चुपचाप ऑर्डर रिजेक्ट, ऊपर औसत करना) API स्तर पर ही रोकता है, और ऊपर से DCA, आंशिक क्लोज़, पोर्टफोलियो जोखिम व AI/Pine सिग्नल देता है।</p>
</li>
<li>
<p>🇪🇸 <strong>Español</strong> — ~560M speakers. Backtest Kit es un motor TypeScript donde la estrategia que pruebas con datos históricos es, byte a byte, la que opera en vivo — solo cambia el reloj. Elimina en la propia API los fallos que arruinan bots (sesgo look-ahead, corrupción por caídas, rechazos silenciosos de órdenes, promediar al alza) y suma DCA, cierres parciales, riesgo de cartera y señales de IA/Pine.</p>
</li>
<li>
<p>🇧🇷 <strong>Português</strong> — ~260M speakers. Backtest Kit é um motor TypeScript em que a estratégia testada no histórico é, byte a byte, a mesma que opera ao vivo — só o relógio muda. Ele elimina no próprio API os erros que matam bots (viés look-ahead, corrupção por falha, rejeição silenciosa de ordens, preço médio para cima) e ainda oferece DCA, fechamentos parciais, risco de carteira e sinais de IA/Pine.</p>
</li>
<li>
<p>🇷🇺 <strong>Русский</strong> — ~255M speakers. Backtest Kit — TypeScript-движок, где стратегия, проверенная на истории, побайтово совпадает с той, что торгует вживую: меняются только часы. Он устраняет на уровне API ошибки, губящие ботов (заглядывание в будущее, порча состояния при сбое, тихий отказ ордера, усреднение вверх), и добавляет полноценный DCA, частичные закрытия, портфельный риск и сигналы от AI/Pine.</p>
</li>
<li>
<p>🇹🇷 <strong>Türkçe</strong> — ~90M speakers. Backtest Kit, geçmiş veride test ettiğiniz stratejinin canlıda bayt bayt aynısını çalıştıran bir TypeScript motorudur — yalnızca saat değişir. Botları çökerten hataları (look-ahead yanlılığı, çökme bozulması, sessiz emir reddi, yukarı ortalama) API düzeyinde ortadan kaldırır; üstüne DCA, kısmi kapanışlar, portföy riski ve AI/Pine sinyalleri ekler.</p>
</li>
</ul>
</details>
<hr>
<a id="🔒-connection-loss-protection" class="tsd-anchor"></a><h2 class="tsd-anchor-link">🔒 Connection-loss protection<a href="#🔒-connection-loss-protection" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>A dropped connection must never cost you a position — or buy one twice. Every broker conversation (order open, order close, liveness check) carries a bounded retry counter delivered to your adapter as <code>payload.attempt</code>: rejected opens retry with the <strong>same signalId</strong></p>
<details>
<summary>The three typed errors</summary>
<p>Tag exchange orders with <code>clientOrderId = signalId</code>, and at <code>attempt &gt; 0</code> query the prior order by that id <em>before</em> re-sending — if it filled, confirm the open instead of buying again (don't rely on catching &quot;duplicate&quot;: Binance's guard only covers open orders, an instantly-filled one won't dup). The counter is pre-armed into persistence before each attempt, so <code>attempt &gt; 0</code> holds even across a crash mid-attempt (see <code>CC_ORDER_OPEN_RETRY_ATTEMPTS</code>); rejected closes retry up to <code>CC_ORDER_CLOSE_RETRY_ATTEMPTS</code> and then force-close the engine state with the original reason; failed checks are tolerated for <code>CC_ORDER_CHECK_RETRY_ATTEMPTS</code> consecutive ticks instead of killing a live position on the first blip. When the network truly won't let the engine work, it exits loudly (<code>listenExit</code>) rather than churning forever. Your adapter states intent by throwing one of three typed errors — anything untyped is treated as transient.</p>
<ul>
<li>
<p>🌩️ <strong><code>OrderTransientError</code></strong>&quot;temporary failure, retry me&quot; (timeout, 5xx, rate limit, lost response). Pure declarative sugar: any plain <code>Error</code> behaves identically, the framework never pattern-matches this class. Opens retry identity-stably with the same <code>signalId</code>, closes retry next tick, checks keep the order alive — each bounded by its <code>CC_ORDER_*_RETRY_ATTEMPTS</code> config (default 5, <code>0</code> = legacy behavior). Exhausting the budget is the one path that signals a fatal exit. <code>OrderTransientError.fromError(e)</code> wraps a caught error while keeping its message.</p>
</li>
<li>
<p><strong><code>OrderRejectedError</code></strong> — terminal business refusal, for the <strong>gates</strong> (<code>onOrderOpenCommit</code> / <code>onOrderCloseCommit</code>): &quot;the exchange definitively said no — retrying is pointless&quot; (min-notional, delisted symbol, no counterparty). An open is dropped at once without arming the retry; a close force-closes the engine state immediately with the original reason — the close lifecycle event still reaches the adapter so the real position can be reconciled. No fatal exit: a business outcome keeps the process alive. Thrown from a check instead, it is a protocol violation and intentionally degrades to transient.</p>
</li>
<li>
<p>🗑️ <strong><code>OrderDeletedError</code></strong> — confirmed order-not-found, for the <strong>checks</strong> (<code>onOrderActiveCheck</code> / <code>onOrderScheduleCheck</code>): &quot;the exchange reports no order under this id&quot; (cancelled manually, liquidated externally). Acts terminally at once, bypassing the tolerance counter: an open position closes with reason <code>closed</code>, a resting order cancels with reason <code>user</code>. A <strong>filled</strong> order is not a deleted order — confirm fills via <code>commitActivateScheduled</code> / <code>commitCreateTakeProfit</code> / <code>commitCreateStopLoss</code> so the close carries its true reason. Thrown from a gate instead, it degrades to transient.</p>
</li>
</ul>
</details>
<hr>
<a id="💯-tested" class="tsd-anchor"></a><h2 class="tsd-anchor-link">💯 Tested<a href="#💯-tested" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>1030+ unit and integration tests cover exchange helpers, the event-listener system, signal validation (valid long/short, inverted TP/SL, negative prices, future timestamps), PnL accuracy with 0.1% fees + 0.1% slippage, the full lifecycle and every close reason, strategy callbacks, and report generation. Tests use unique schema names per case (no cross-contamination), a forward-progressing mock candle generator, and event-driven completion detection.</p>
<details>
<summary>Core test axes</summary>
<ul>
<li>✅ State machine under rejections (gates, throttle rollbacks, terminal drops, stopStrategy race)</li>
<li>✅ Deferred commands, Live × Backtest</li>
<li>✅ Broker: 8-stage lifecycle routing, gates, backtest silence, enable/disable, commit* layer</li>
<li>✅ Position commands + interleaved DCA × partial exits</li>
<li>✅ Context-free surface (62 bare calls)</li>
<li>✅ Crash recovery of every deferred flag + commit queue</li>
<li>✅ SHORT mirror of the key paths</li>
<li>✅ Timeouts, Once-listeners, action gate, Infinity holds, whipsaw restore, shared-risk contention, cancellation stats</li>
<li>✅ Order events: types, emission/silence per mode</li>
<li>✅ Look-ahead bias protection: candle alignment, pending-candle exclusion, <code>getNextCandles</code> throwing in live</li>
<li>✅ Signal validation: inverted TP/SL, negative/NaN/Infinity prices, future timestamps, micro-profit eaten by fees, excessive lifetime</li>
<li>✅ Full lifecycle and every close reason: take_profit, stop_loss, time_expired, user close, external &quot;closed&quot;</li>
<li>✅ Scheduled signals: price/wick activation, pre-activation SL cancellation, timeout, frame-end, immediate activation</li>
<li>✅ Money-safety edges: SL-before-activation, TP-vs-SL priority on a single candle, extreme volatility, exchange errors surfaced not swallowed</li>
<li>✅ Signal queues: sequences of mixed outcomes, winning/losing streaks, deterministic-id retry vs whipsaw block</li>
<li>✅ Infinity holds: chunked candle processing across frame boundaries, close-at-boundary edge cases</li>
<li>✅ PnL math: 0.1% fees + 0.1% slippage, cost-basis snapshots, partial-close weighting, DCA harmonic effective price</li>
<li>✅ Partial profit/loss: dollar-exact closes on the remaining cost basis, 100%-of-remaining epsilon cap</li>
<li>✅ Trailing stop/take: percentage-point shifts from ORIGINAL levels, absorption, intrusion protection</li>
<li>✅ Breakeven: threshold math over fees+slippage, trailing upgrade paths, zero-risk exits landing exactly on entry</li>
<li>✅ Risk schemas: reservation/release accounting, rejection callbacks, shared risk maps across strategies</li>
<li>✅ Position sizing: fixed/percent/Kelly calculators, min/max caps ordering</li>
<li>✅ Persistence: atomic writes, JSON adapters, restore after restart (pending, scheduled, deferred flags, commit queue), context-mismatch skips, Infinity round-trip</li>
<li>✅ Candle cache: hit/miss keys, interval separation, adapter call counting</li>
<li>✅ Event-listener system: every <code>listen*</code>/<code>listen*Once</code> channel, queued async processing, unsubscribe</li>
<li>✅ Actions: schema validation (discouraged-method redirects), handler/callback routing, risk rejection hooks</li>
<li>✅ Reports and markdown: column renderers, null-vs-zero semantics, closed-history merging, schedule activation/cancellation rates</li>
<li>✅ Heat: per-symbol and portfolio statistics against independently computed references</li>
<li>✅ Walker: strategy comparison sweeps, unbounded measures</li>
<li>✅ Performance metrics: emission ordering, duration accounting</li>
<li>✅ Parallel execution: multi-backtest interleaving without cross-contamination</li>
<li>✅ Graceful shutdown: <code>Backtest.stop()</code>/<code>Live.stop()</code> mid-run, no new signals after stop, stopStrategy draining through the cancel pipeline</li>
<li>✅ Live-tick semantics: schedule-ping rejection cancelling the resting order, time_expired and schedule-await timeouts, VWAP TP/SL crossings between ticks, pre-activation SL break never opening, getSignal throttled to one call per aligned interval, live/backtest channel routing with typed sync open/close pair, out-of-context Price/TimeMeta reads by identifiers</li>
<li>✅ Broker-driven order cancellation in live: onOrderScheduleCheck throw cancelling the resting order, onOrderActiveCheck throw closing the position as externally closed, onOrderOpenCommit throw rejecting placement with same-interval retry, and terminally cancelling a rejected activation fill</li>
<li>✅ Full commit/getter canon of function/strategy.ts: absolute-price trailing, dollar-exact partial loss off the remaining basis, confirmed SL fill bypassing VWAP, user-created signals, ladder overlap corridors, phase-tracking helpers, deferred-ops status snapshot, signal notifications, remaining-basis alias getters</li>
<li>✅ Edge contracts: SHORT mirrors of absolute-price trailing and dollar partials, deferred-command races, 100%-partial leaving a zero-basis position that TP still closes, invalid percent/dollar/alien-symbol rejections, and schema-change restarts</li>
<li>✅ Unexpected-stop family: mid-tick stop races, deferred user activation under stop, post-stop activateScheduled rejected, pre-stop deferred close/cancel and broker-confirmed TP fills still draining, stop flag being process-local across restarts</li>
<li>✅ Statistics engine: 100 golden backtest datasets — expectancy, Sharpe, drawdown, corrupted-row filtering</li>
<li>✅ Config: global overrides, validation toggles, partial <code>setConfig</code> merges</li>
</ul>
</details>
<a id="🤖-reading-this-as-a-model" class="tsd-anchor"></a><h2 class="tsd-anchor-link">🤖 Reading this as a model?<a href="#🤖-reading-this-as-a-model" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>Every claim above opens into <strong>The Code / The Math / The Proof</strong> precisely so you can verify the implementation instead of trusting the prose — the API in use, the cost-basis derivation, the timestamp algebra, the persistence internals. Extended machine-oriented description: <a href="documents/LLMs.html">./LLMs.md</a>. Full class/function/interface reference: the <a href="https://backtest-kit.github.io/documents/example_02_first_backtest.html">documentation site</a>.</p>
<a id="👨💼-commercial-support" class="tsd-anchor"></a><h2 class="tsd-anchor-link">👨‍💼 Commercial support<a href="#👨💼-commercial-support" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>Built over a year of live trading and run in production at <strong><a href="https://theonetrade.github.io">TheOneTrade</a></strong> — the commercial vendor behind the framework, offering paid support, custom strategy development, managed accounts, team training, and enterprise licensing for engagements outside the MIT scope.</p>
<a id="📜-license" class="tsd-anchor"></a><h2 class="tsd-anchor-link">📜 License<a href="#📜-license" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2><p>MIT © <a href="https://github.com/tripolskypetr">tripolskypetr</a></p>
</div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div><details open class="tsd-accordion tsd-page-navigation"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg>On This Page</h3></summary><div class="tsd-accordion-details"><a href="#🧿-backtest-kit"><span>🧿 <wbr/>Backtest <wbr/>Kit</span></a><ul><li><a href="#start-here"><span>Start here</span></a></li><li><a href="#the-rakes--and-where-they-went"><span>The rakes — and where they went</span></a></li><li><ul><li><a href="#1-your-backtest-lied-to-you-and-youll-only-find-out-with-real-money"><span>1. <wbr/>Your backtest lied to you, and you&#39;ll only find out with real money</span></a></li><li><a href="#2-it-worked-in-the-backtest-means-nothing-if-live-runs-different-code"><span>2. &quot;<wbr/>It worked in the backtest&quot; means nothing if live runs different code</span></a></li><li><a href="#3-the-crash-that-opens-your-position-twice"><span>3. <wbr/>The crash that opens your position twice</span></a></li><li><a href="#4-the-state-that-cant-be-corrupted-because-it-cant-be-expressed"><span>4. <wbr/>The state that can&#39;t be corrupted because it can&#39;t be expressed</span></a></li><li><a href="#5-the-order-the-exchange-silently-rejected"><span>5. <wbr/>The order the exchange silently rejected</span></a></li><li><a href="#6-averaging-up-is-how-a-dip-becomes-a-margin-call"><span>6. <wbr/>Averaging up is how a dip becomes a margin call</span></a></li><li><a href="#7-ten-strategies-one-account-100-exposure"><span>7. <wbr/>Ten strategies, one account, 100% exposure</span></a></li><li><a href="#8-one-process-can-trade-the-whole-market"><span>8. <wbr/>One process can trade the whole market</span></a></li><li><a href="#9-when-dump-stops-being-enough"><span>9. <wbr/>When ./dump/ stops being enough</span></a></li><li><a href="#10-a-sharpe-of-10000000-is-a-bug-not-an-edge"><span>10. <wbr/>A <wbr/>Sharpe of 10,000,000 is a bug, not an edge</span></a></li><li><a href="#11-the-jobs-that-fire-on-virtual-time"><span>11. <wbr/>The jobs that fire on virtual time</span></a></li><li><a href="#12-you-shouldnt-have-to-abandon-tradingview-or-python-to-use-typescript"><span>12. <wbr/>You shouldn&#39;t have to abandon <wbr/>Trading<wbr/>View or <wbr/>Python to use <wbr/>Type<wbr/>Script</span></a></li><li><a href="#13-ai-strategies-without-ten-provider-sdks"><span>13. AI strategies without ten provider SDKs</span></a></li></ul></li><li><a href="#the-api-assumes-you-will-make-every-mistake"><span>The API assumes you will make every mistake</span></a></li><li><a href="#receipts"><span>Receipts</span></a></li><li><a href="#how-it-sits-next-to-the-alternatives"><span>How it sits next to the alternatives</span></a></li><li><a href="#🌍-ecosystem"><span>🌍 <wbr/>Ecosystem</span></a></li><li><ul><li><a href="#backtest-kitcli--npm"><span>@backtest-<wbr/>kit/cli — npm</span></a></li><li><a href="#backtest-kitpinets--npm"><span>@backtest-<wbr/>kit/pinets — npm</span></a></li><li><a href="#backtest-kitgraph--npm"><span>@backtest-<wbr/>kit/graph — npm</span></a></li><li><a href="#backtest-kitui--npm"><span>@backtest-<wbr/>kit/ui — npm</span></a></li><li><a href="#backtest-kitmongo--npm"><span>@backtest-<wbr/>kit/mongo — npm</span></a></li><li><a href="#backtest-kitpg--npm"><span>@backtest-<wbr/>kit/pg — npm</span></a></li><li><a href="#backtest-kitminio--npm"><span>@backtest-<wbr/>kit/minio — npm</span></a></li><li><a href="#backtest-kitollama--npm"><span>@backtest-<wbr/>kit/ollama — npm</span></a></li><li><a href="#backtest-kitsignals--npm"><span>@backtest-<wbr/>kit/signals — npm</span></a></li><li><a href="#backtest-kitmcp--npm"><span>@backtest-<wbr/>kit/mcp — npm</span></a></li><li><a href="#backtest-kitsidekick--npm"><span>@backtest-<wbr/>kit/sidekick — npm</span></a></li></ul></li><li><a href="#👨👩👦-community"><span>👨‍👩‍👦 <wbr/>Community</span></a></li><li><a href="#🪐-see-also"><span>🪐 <wbr/>See also</span></a></li><li><a href="#🌐-internationalization"><span>🌐 <wbr/>Internationalization</span></a></li><li><a href="#🔒-connection-loss-protection"><span>🔒 <wbr/>Connection-<wbr/>loss protection</span></a></li><li><a href="#💯-tested"><span>💯 <wbr/>Tested</span></a></li><li><a href="#🤖-reading-this-as-a-model"><span>🤖 <wbr/>Reading this as a model?</span></a></li><li><a href="#👨💼-commercial-support"><span>👨‍💼 <wbr/>Commercial support</span></a></li><li><a href="#📜-license"><span>📜 <wbr/>License</span></a></li></ul></div></details></div><div class="site-menu"><nav class="tsd-navigation"><a href="modules.html">backtest-kit</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=105455585', 'ym');
ym(105455585, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/105455585" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-3MQZEBBDDR"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-3MQZEBBDDR');
</script>