-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
643 lines (602 loc) · 165 KB
/
Copy pathindex.html
File metadata and controls
643 lines (602 loc) · 165 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
<!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>plumbing</em> was.</p>
<p><code>backtest-kit</code> is that plumbing, 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 "discouraged," not "documented," 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"> && </span><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-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"> && </span><span class="hl-1">cd</span><span class="hl-2"> </span><span class="hl-3">my-trading-bot</span><span class="hl-2"> && </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"> && </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">'ccxt'</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">'backtest-kit'</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">'binance'</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">=></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">=></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">=></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">=></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">'feb-2026'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'1m'</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">'2026-02-01'</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">'2026-02-28'</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">'my-strategy'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'15m'</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">=></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">'long'</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">'long'</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">'BTCUSDT'</span><span class="hl-2">, { </span><span class="hl-5">strategyName:</span><span class="hl-2"> </span><span class="hl-3">'my-strategy'</span><span class="hl-2">, </span><span class="hl-5">exchangeName:</span><span class="hl-2"> </span><span class="hl-3">'binance'</span><span class="hl-2">, </span><span class="hl-5">frameName:</span><span class="hl-2"> </span><span class="hl-3">'feb-2026'</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">=></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><=</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 "be careful." 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 "now." 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 "now" 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 "now" 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 "uneven" 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">=></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">'1h'</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">'15m'</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">'5m'</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">'1m'</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. "It worked in the backtest" 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">'BTCUSDT'</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">'BTCUSDT'</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">=></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">'closed'</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">'BTCUSDT'</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">=></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">'BTCUSDT'</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">=></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">'closed'</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">'scheduled'</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">'cancelled'</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>"Is this position closed?" 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">=></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">'idle'</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">'scheduled'</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">'opened'</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">'active'</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">'closed'</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 > entry > 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 "fix" 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">'limit'</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"> < </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">'closed'</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"> > </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">'market'</span><span class="hl-2">, </span><span class="hl-5">side</span><span class="hl-2"> === </span><span class="hl-3">'buy'</span><span class="hl-2"> ? </span><span class="hl-3">'sell'</span><span class="hl-2"> : </span><span class="hl-3">'buy'</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">'not filled in time — partial fill rolled back, backtest-kit will retry'</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">'buy'</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">'limit'</span><span class="hl-2">, </span><span class="hl-3">'sell'</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">'market'</span><span class="hl-2">, </span><span class="hl-3">'sell'</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"> > </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">'backtest-kit'</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">'apr_2026_strategy'</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">=></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">'long'</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">'long'</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">=></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"> >= </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">=></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">) < </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">'unknown'</span><span class="hl-2">, </span><span class="hl-5">note:</span><span class="hl-2"> </span><span class="hl-3">'# closed by target pnl'</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 "risking 10%" 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 "is this allowed?" and "the order went out."</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">'demo'</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">=></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">'long'</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"> < </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">=></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">'long'</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">'long'</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"> < </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">'Poor R/R ratio'</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">=></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">'backtest-kit'</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">'BTCUSDT'</span><span class="hl-2">,</span><span class="hl-3">'ETHUSDT'</span><span class="hl-2">,</span><span class="hl-3">'SOLUSDT'</span><span class="hl-2">,</span><span class="hl-3">'BNBUSDT'</span><span class="hl-2">,</span><span class="hl-3">'XRPUSDT'</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">'binance'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'1m'</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">'2026-02-01T00:00:00Z'</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">'2026-02-28T23:59:59Z'</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">'binance'</span><span class="hl-2">, </span><span class="hl-5">frameName:</span><span class="hl-2"> </span><span class="hl-3">'feb-2026'</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">'@backtest-kit/mongo'</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">'backtest-kit'</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">'tg-parser'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'1h'</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">=></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">'funding'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'1h'</span><span class="hl-2">, </span><span class="hl-5">symbols:</span><span class="hl-2"> [</span><span class="hl-3">'BTCUSDT'</span><span class="hl-2">,</span><span class="hl-3">'ETHUSDT'</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">=></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">'warm-cache'</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">=></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 "but my indicators live in Pine Script and TA-Lib." 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">'@backtest-kit/pinets'</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">'strategy.pine'</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">'BTCUSDT'</span><span class="hl-2">, </span><span class="hl-5">timeframe:</span><span class="hl-2"> </span><span class="hl-3">'5m'</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">'@backtest-kit/signals'</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">'BTCUSDT'</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">'@backtest-kit/graph'</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">=></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">'timeframe_4h.pine'</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">'4h'</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">'AllowLong'</span><span class="hl-2">, </span><span class="hl-5">allowShort:</span><span class="hl-2"> </span><span class="hl-3">'AllowShort'</span><span class="hl-2">, </span><span class="hl-5">noTrades:</span><span class="hl-2"> </span><span class="hl-3">'NoTrades'</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">=></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">'timeframe_15m.pine'</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">'15m'</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">'Signal'</span><span class="hl-2">, </span><span class="hl-5">priceOpen:</span><span class="hl-2"> </span><span class="hl-3">'Close'</span><span class="hl-2">, </span><span class="hl-5">priceTakeProfit:</span><span class="hl-2"> </span><span class="hl-3">'TakeProfit'</span><span class="hl-2">, </span><span class="hl-5">priceStopLoss:</span><span class="hl-2"> </span><span class="hl-3">'StopLoss'</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">=></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"> && </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"> && </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">'mtf'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'5m'</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">=></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">'@backtest-kit/ollama'</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">'llm-signal'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'5m'</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">'deepseek-chat'</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 "SL must be below entry for LONG"); 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">'uuid'</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">'backtest-kit'</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">'./utils/json.mjs'</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">'./utils/messages.mjs'</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">'llm-strategy'</span><span class="hl-2">, </span><span class="hl-5">interval:</span><span class="hl-2"> </span><span class="hl-3">'5m'</span><span class="hl-2">, </span><span class="hl-5">riskName:</span><span class="hl-2"> </span><span class="hl-3">'demo'</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">=></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">'1h'</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">'15m'</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">'5m'</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">'1m'</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">'position-context'</span><span class="hl-2">, </span><span class="hl-5">bucketName:</span><span class="hl-2"> </span><span class="hl-3">'mtf'</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">'agent reasoning'</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">'position-entry'</span><span class="hl-2">, </span><span class="hl-5">bucketName:</span><span class="hl-2"> </span><span class="hl-3">'mtf'</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">'signal params'</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 "pit of success": 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">"backtest-kit"</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">"functools-kit"</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 "now", 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">"apr_2026_strategy"</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">=></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">"long"</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">"long"</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">=></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"> >= </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">=></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">) < </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">"position closed due to the target pnl reached"</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">"unknown"</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">"# Closed by target pnl"</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">=></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">"error"</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 "now" 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 "no-try-catch" 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 "same code, both modes" 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 eight 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>
</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>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 "dependency zoo": 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">'@backtest-kit/ui'</span><span class="hl-2">;</span><br/><span class="hl-1">serve</span><span class="hl-2">(</span><span class="hl-3">'0.0.0.0'</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-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 "eject" 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"> && </span><span class="hl-1">cd</span><span class="hl-2"> </span><span class="hl-3">my-trading-bot</span><span class="hl-2"> && </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/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>
</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 > 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 "duplicate": 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 > 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> — "temporary failure, retry me" (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>): "the exchange definitively said no — retrying is pointless" (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>): "the exchange reports no order under this id" (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>970+ 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 "closed"</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'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. "<wbr/>It worked in the backtest" 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't be corrupted because it can'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'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-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>