backtest-kit 9.8.4 โ†’ 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,13 +71,14 @@ Install the core library and peer dependencies manually. Use this approach when
71
71
 
72
72
  - ๐Ÿš€ **Production-Ready**: Seamless switch between backtest/live modes; identical code across environments.
73
73
  - ๐Ÿ’พ **Crash-Safe**: Atomic persistence recovers states after crashes, preventing duplicates or losses.
74
- - โœ… **Validation**: Checks signals for TP/SL logic, risk/reward ratios, and portfolio limits.
74
+ - โœ… **Validation**: Checks signals for TP/SL logic, risk/reward ratios, whipsaw protection and portfolio limits.
75
75
  - ๐Ÿ”„ **Efficient Execution**: Streaming architecture for large datasets; VWAP pricing for realism.
76
76
  - ๐Ÿค– **AI Integration**: LLM-powered strategy generation (Optimizer) with multi-timeframe analysis.
77
77
  - ๐Ÿ“Š **Reports & Metrics**: Auto Markdown reports with PNL, Sharpe Ratio, win rate, and more.
78
78
  - ๐Ÿ›ก๏ธ **Risk Management**: Custom rules for position limits, time windows, and multi-strategy coordination.
79
79
  - ๐Ÿ”Œ **Pluggable**: Custom data sources (CCXT), persistence (file/Redis), and sizing calculators.
80
80
  - ๐Ÿ—ƒ๏ธ **Transactional Live Orders**: Broker adapter intercepts every trade mutation before internal state changes โ€” exchange rejection rolls back the operation atomically.
81
+ - โฐ **Built-in Crontab**: Register periodic or fire-once jobs that fire on virtual-time boundaries with singleshot coordination across parallel backtests โ€” one handler invocation per boundary, no double-fires.
81
82
  - ๐Ÿงช **Tested**: 520+ unit/integration tests for validation, recovery, and events.
82
83
  - ๐Ÿ”“ **Self hosted**: Zero dependency on third-party node_modules or platforms; run entirely in your own environment.
83
84
 
@@ -1267,6 +1268,80 @@ Broker.enable();
1267
1268
 
1268
1269
  Signal open/close events are routed automatically via an internal event bus once `Broker.enable()` is called. **No manual wiring needed.** All other operations (`partialProfit`, `trailingStop`, `breakeven`, `averageBuy`) are intercepted explicitly before the corresponding state mutation.
1269
1270
 
1271
+ ### ๐Ÿ” How Cron Works
1272
+
1273
+ `Cron` is a periodic / fire-once scheduler that runs in **virtual time** โ€” the same time stream your strategies see in backtest mode. Handlers fire on candle-interval boundaries (`1m`, `5m`, `1h`, `1d`, โ€ฆ) and are coordinated across parallel `Backtest.background(symbol, ...)` runs so the same boundary never produces two concurrent invocations.
1274
+
1275
+ **Public API:**
1276
+ - **`Cron.register({ name, interval?, symbols?, handler })`** โ€” register a job. Returns a disposer. Re-registering the same `name` replaces the previous entry and bumps an internal generation counter (late writes from old handlers are ignored).
1277
+ - **`Cron.enable()`** โ€” subscribe `Cron` to the engine's lifecycle subjects (`beforeStart`, `idlePing`, `activePing`, `schedulePing`). Wrapped in `singleshot`; call once at startup.
1278
+ - **`Cron.disable()`** โ€” tear down the subscriptions installed by `enable()`. Safe to call multiple times and before `enable()`.
1279
+ - **`Cron.unregister(name)`** โ€” remove a registered job.
1280
+ - **`Cron.clear(symbol?)`** โ€” clear fire-once marks. `symbol` provided โ†’ fan-out marks for that symbol only; no argument โ†’ all marks. Does **not** touch in-flight handlers.
1281
+
1282
+ **Two modes per `interval`:**
1283
+ - **Periodic** (`interval: "1h"`) โ€” handler fires once per boundary of that interval.
1284
+ - **Fire-once** (`interval` omitted) โ€” handler fires on the first matching tick and never again until `clear()` / `unregister` / re-`register`.
1285
+
1286
+ **Two scopes per `symbols`:**
1287
+ - **Global** (`symbols` omitted) โ€” handler fires once per boundary across all parallel backtests. First symbol to reach the boundary opens the slot; others await the same promise.
1288
+ - **Fan-out** (`symbols: ["BTC", "ETH"]`) โ€” handler fires once per boundary **per whitelisted symbol**. Each symbol has its own slot.
1289
+
1290
+ <details>
1291
+ <summary>
1292
+ The code
1293
+ </summary>
1294
+
1295
+ ```typescript
1296
+ import { Cron, Backtest } from "backtest-kit";
1297
+
1298
+ // Global hourly job โ€” fires once per virtual hour across all parallel backtests.
1299
+ Cron.register({
1300
+ name: "tg-signal-parser",
1301
+ interval: "1h",
1302
+ handler: async (symbol, when, backtest) => {
1303
+ await parseTelegramSignalsToMongo(when);
1304
+ },
1305
+ });
1306
+
1307
+ // Per-symbol fan-out โ€” fires once per hour per whitelisted symbol.
1308
+ Cron.register({
1309
+ name: "fetch-funding",
1310
+ interval: "1h",
1311
+ symbols: ["BTCUSDT", "ETHUSDT"],
1312
+ handler: async (symbol, when, backtest) => {
1313
+ await fetchFundingRate(symbol, when);
1314
+ },
1315
+ });
1316
+
1317
+ // Fire-once warm-up โ€” runs once globally on the very first tick.
1318
+ Cron.register({
1319
+ name: "warm-cache",
1320
+ handler: async (symbol, when, backtest) => {
1321
+ await warmupCache();
1322
+ },
1323
+ });
1324
+
1325
+ // Wire Cron to the engine once at startup. After this every strategy tick is
1326
+ // forwarded into Cron automatically โ€” no manual listener wiring needed.
1327
+ Cron.enable();
1328
+
1329
+ for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "TRXUSDT"]) {
1330
+ Backtest.background(symbol, { strategyName, exchangeName, frameName });
1331
+ }
1332
+
1333
+ // On shutdown:
1334
+ // Cron.disable();
1335
+ ```
1336
+
1337
+ </details>
1338
+
1339
+ #### Internals
1340
+
1341
+ `Cron.enable()` subscribes a single `singlerun`-wrapped handler to four lifecycle subjects (`beforeStart`, `idlePing`, `activePing`, `schedulePing`). `singlerun` merges all four streams into one serial queue, so concurrent ticks on the same `(symbol, virtual-minute)` cannot race to open the same slot. Each incoming tick is **base-aligned to the 1-minute boundary** before any further processing โ€” lifecycle pings may carry sub-second jitter, but Cron always reasons in whole minutes.
1342
+
1343
+ Coordination keys are built as `${name}:${alignedMs}:${symbol?}:g${generation}`. Parallel backtests that hit the same key share a single in-flight promise (mutex semantics): the first opens the slot and runs the handler, others `await` the same promise and release together. After `.finally()` the slot is removed and the next boundary creates a fresh promise. Fire-once entries additionally record a `_firedOnce` mark on success so subsequent ticks skip them โ€” a failed handler is **not** marked, so it retries on the next tick. The generation suffix isolates re-registrations: a late write from a still-in-flight handler of a previous `register()` carries the old generation and never collides with the new entry.
1344
+
1270
1345
  ### ๐Ÿ” How getCandles Works
1271
1346
 
1272
1347
  backtest-kit uses Node.js `AsyncLocalStorage` to automatically provide
@@ -1765,6 +1840,28 @@ git clone https://github.com/backtest-kit/backtest-monorepo-parallel.git
1765
1840
  ```
1766
1841
 
1767
1842
 
1843
+ ### backtest-ollama-crontab
1844
+
1845
+ > **[Explore on GitHub](https://github.com/backtest-kit/backtest-ollama-crontab)** ๐ŸŸ
1846
+
1847
+ The **backtest-ollama-crontab** repository is a TypeScript monorepo template that wires a cloud/local **Ollama** into a trading-signal pipeline as a risk filter, with a **15-minute crontab** ingesting signals from any public Telegram channel. The **same code runs in both live and backtest modes** โ€” the crontab re-polls live and pulls the entire frame at startup in backtest.
1848
+
1849
+ #### Key Features
1850
+ - ๐Ÿค– **Local/Cloud LLM Risk Filter**: Per-signal verdict from local Ollama (`gpt-oss` quantized) returning `riskAction: "skip" | "follow"`, with empirical rules embedded in the system prompt and tunable without recompiling packages
1851
+ - โฐ **Crontab-Driven Ingestion**: `Cron.register(..., interval: "15m")` for live re-polling of the Telegram channel, plus a fire-once `Cron.register(...)` (no `interval`) for backtest-time bulk prepare โ€” same code path in both modes
1852
+ - ๐Ÿ“ก **Telegram MTProto Crawler**: QR-code session auth, `iterMessages` pull from any public channel into a `parser-items` Mongo collection, regex extraction of `direction / entry / targets / stoploss` into `screen-items`
1853
+ - ๐Ÿง  **Outline-Based Risk Logic**: Risk outline ingests 1m/15m candles + a pre-computed metrics packet (`avgRangePct`, `momentum24hPct`) and produces a zod-validated verdict consumed by the strategy
1854
+ - ๐Ÿ“ˆ **Reproducible Backtest Comparison**: same parsed-signal set, two backtests side-by-side โ€” **+52.22% โ†’ +68.90%** total PNL, Sharpe **+0.309 โ†’ +0.512**, winrate **68% โ†’ 82%**, profit factor **2.73 โ†’ 6.37** with the LLM gate enabled
1855
+
1856
+ #### Use Case
1857
+ Reference for integrating any local LLM into a backtest-kit pipeline as a signal filter, and for combining periodic crontab pulls (live) with one-shot bulk prepare (backtest) via the same `Cron.register` API.
1858
+
1859
+ #### Get Started
1860
+ ```bash
1861
+ git clone https://github.com/backtest-kit/backtest-ollama-crontab.git
1862
+ ```
1863
+
1864
+
1768
1865
  ### backtest-kit-redis-mongo-docker
1769
1866
 
1770
1867
  > **[Explore on GitHub](https://github.com/backtest-kit/backtest-kit-redis-mongo-docker)** ๐Ÿณ