{"id":35162234,"url":"https://github.com/tripolskypetr/backtest-kit","last_synced_at":"2026-03-04T11:08:34.768Z","repository":{"id":325112540,"uuid":"1099752066","full_name":"tripolskypetr/backtest-kit","owner":"tripolskypetr","description":"A powerful TypeScript framework for backtesting trading strategies with clean architecture and real-time execution capabilities.","archived":false,"fork":false,"pushed_at":"2026-02-25T14:22:13.000Z","size":45464,"stargazers_count":14,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-02-25T16:44:02.881Z","etag":null,"topics":["algotrading","backtest","backtesting","backtesting-strategy","backtesting-trading-strategies","crypto","finance","financial-analysis","framework","library","stock-market","trading","trading-strategies"],"latest_commit_sha":null,"homepage":"https://backtest-kit.github.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tripolskypetr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"custom":["http://paypal.me/tripolskypetr"]}},"created_at":"2025-11-19T11:55:02.000Z","updated_at":"2026-02-25T10:09:56.000Z","dependencies_parsed_at":"2025-12-17T19:01:19.141Z","dependency_job_id":"fe9717db-04e5-49c5-bdf5-93a68dfc27b3","html_url":"https://github.com/tripolskypetr/backtest-kit","commit_stats":null,"previous_names":["tripolskypetr/backtest-kit"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/tripolskypetr/backtest-kit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripolskypetr%2Fbacktest-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripolskypetr%2Fbacktest-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripolskypetr%2Fbacktest-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripolskypetr%2Fbacktest-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tripolskypetr","download_url":"https://codeload.github.com/tripolskypetr/backtest-kit/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tripolskypetr%2Fbacktest-kit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29861805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-26T08:51:08.701Z","status":"ssl_error","status_checked_at":"2026-02-26T08:50:19.607Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algotrading","backtest","backtesting","backtesting-strategy","backtesting-trading-strategies","crypto","finance","financial-analysis","framework","library","stock-market","trading","trading-strategies"],"created_at":"2025-12-28T18:42:15.196Z","updated_at":"2026-03-04T11:08:34.757Z","avatar_url":"https://github.com/tripolskypetr.png","language":"TypeScript","funding_links":["http://paypal.me/tripolskypetr"],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"https://github.com/tripolskypetr/backtest-kit/raw/refs/heads/master/assets/consciousness.svg\" height=\"45px\" align=\"right\"\u003e\n\n# 🧿 Backtest Kit\n\n\u003e A TypeScript framework for backtesting and live trading strategies on multi-asset, crypto, forex or [DEX (peer-to-peer marketplace)](https://en.wikipedia.org/wiki/Decentralized_finance#Decentralized_exchanges), spot, futures with crash-safe persistence, signal validation, and AI optimization.\n\n![screenshot](https://raw.githubusercontent.com/tripolskypetr/backtest-kit/HEAD/assets/screenshots/screenshot16.png)\n\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit)\n[![npm](https://img.shields.io/npm/v/backtest-kit.svg?style=flat-square)](https://npmjs.org/package/backtest-kit)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)]()\n[![Build](https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml/badge.svg)](https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml)\n\nBuild reliable trading systems: backtest on historical data, deploy live bots with recovery, and optimize strategies using LLMs like Ollama.\n\n📚 **[API Reference](https://backtest-kit.github.io/documents/example_02_first_backtest.html)** | 🌟 **[Quick Start](https://github.com/tripolskypetr/backtest-kit/tree/master/demo)** | **📰 [Article](https://backtest-kit.github.io/documents/article_02_second_order_chaos.html)**\n\n## 🚀 Quick Start\n\n### 🎯 The Fastest Way: Sidekick CLI\n\n\u003e **Create a production-ready trading bot in seconds:**\n\n```bash\n# Create project with npx (recommended)\nnpx -y @backtest-kit/sidekick my-trading-bot\ncd my-trading-bot\nnpm start\n```\n\n### 📦 Manual Installation\n\n\u003e **Want to see the code?** 👉 [Demo app](https://github.com/tripolskypetr/backtest-kit/tree/master/demo) 👈\n\n```bash\nnpm install backtest-kit ccxt ollama uuid\n```\n\n## ✨ Why Choose Backtest Kit?\n\n- 🚀 **Production-Ready**: Seamless switch between backtest/live modes; identical code across environments.\n- 💾 **Crash-Safe**: Atomic persistence recovers states after crashes, preventing duplicates or losses.\n- ✅ **Validation**: Checks signals for TP/SL logic, risk/reward ratios, and portfolio limits.\n- 🔄 **Efficient Execution**: Streaming architecture for large datasets; VWAP pricing for realism.\n- 🤖 **AI Integration**: LLM-powered strategy generation (Optimizer) with multi-timeframe analysis.\n- 📊 **Reports \u0026 Metrics**: Auto Markdown reports with PNL, Sharpe Ratio, win rate, and more.\n- 🛡️ **Risk Management**: Custom rules for position limits, time windows, and multi-strategy coordination.\n- 🔌 **Pluggable**: Custom data sources (CCXT), persistence (file/Redis), and sizing calculators.\n- 🧪 **Tested**: 350+ unit/integration tests for validation, recovery, and events.\n- 🔓 **Self hosted**: Zero dependency on third-party node_modules or platforms; run entirely in your own environment.\n\n## 📋 Supported Order Types\n\n\u003e With the calculation of PnL\n\n- Market/Limit entries\n- TP/SL/OCO exits\n- Grid with auto-cancel on unmet conditions\n- Partial profit/loss levels\n- Trailing stop-loss\n- Breakeven protection\n- Stop limit entries (before OCO)\n- Dollar cost averaging\n\n## 📚 Code Samples\n\n### ⚙️ Basic Configuration\n```typescript\nimport { setLogger, setConfig } from 'backtest-kit';\n\n// Enable logging\nsetLogger({\n  log: console.log,\n  debug: console.debug,\n  info: console.info,\n  warn: console.warn,\n});\n\n// Global config (optional)\nsetConfig({\n  CC_PERCENT_SLIPPAGE: 0.1,  // % slippage\n  CC_PERCENT_FEE: 0.1,       // % fee\n  CC_SCHEDULE_AWAIT_MINUTES: 120,  // Pending signal timeout\n});\n```\n\n### 🔧 Register Components\n```typescript\nimport ccxt from 'ccxt';\nimport { addExchangeSchema, addStrategySchema, addFrameSchema, addRiskSchema } from 'backtest-kit';\n\n// Exchange (data source)\naddExchangeSchema({\n  exchangeName: 'binance',\n  getCandles: async (symbol, interval, since, limit) =\u003e {\n    const exchange = new ccxt.binance();\n    const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);\n    return ohlcv.map(([timestamp, open, high, low, close, volume]) =\u003e ({ timestamp, open, high, low, close, volume }));\n  },\n  formatPrice: (symbol, price) =\u003e price.toFixed(2),\n  formatQuantity: (symbol, quantity) =\u003e quantity.toFixed(8),\n});\n\n// Risk profile\naddRiskSchema({\n  riskName: 'demo',\n  validations: [\n    // TP at least 1%\n    ({ pendingSignal, currentPrice }) =\u003e {\n      const { priceOpen = currentPrice, priceTakeProfit, position } = pendingSignal;\n      const tpDistance = position === 'long' ? ((priceTakeProfit - priceOpen) / priceOpen) * 100 : ((priceOpen - priceTakeProfit) / priceOpen) * 100;\n      if (tpDistance \u003c 1) throw new Error(`TP too close: ${tpDistance.toFixed(2)}%`);\n    },\n    // R/R at least 2:1\n    ({ pendingSignal, currentPrice }) =\u003e {\n      const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = pendingSignal;\n      const reward = position === 'long' ? priceTakeProfit - priceOpen : priceOpen - priceTakeProfit;\n      const risk = position === 'long' ? priceOpen - priceStopLoss : priceStopLoss - priceOpen;\n      if (reward / risk \u003c 2) throw new Error('Poor R/R ratio');\n    },\n  ],\n});\n\n// Time frame\naddFrameSchema({\n  frameName: '1d-test',\n  interval: '1m',\n  startDate: new Date('2025-12-01'),\n  endDate: new Date('2025-12-02'),\n});\n```\n\n### 💡 Example Strategy (with LLM)\n```typescript\nimport { v4 as uuid } from 'uuid';\nimport { addStrategySchema, dumpSignalData, getCandles } from 'backtest-kit';\nimport { json } from './utils/json.mjs';  // LLM wrapper\nimport { getMessages } from './utils/messages.mjs';  // Market data prep\n\naddStrategySchema({\n  strategyName: 'llm-strategy',\n  interval: '5m',\n  riskName: 'demo',\n  getSignal: async (symbol) =\u003e {\n\n    const candles1h = await getCandles(symbol, \"1h\", 24);\n    const candles15m = await getCandles(symbol, \"15m\", 48);\n    const candles5m = await getCandles(symbol, \"5m\", 60);\n    const candles1m = await getCandles(symbol, \"1m\", 60);\n\n    const messages = await getMessages(symbol, {\n      candles1h,\n      candles15m,\n      candles5m,\n      candles1m,\n    });  // Calculate indicators / Fetch news\n\n    const resultId = uuid();\n    const signal = await json(messages);  // LLM generates signal\n    await dumpSignalData(resultId, messages, signal);  // Log\n\n    return { ...signal, id: resultId };\n  },\n});\n```\n\n### 🧪 Run Backtest\n```typescript\nimport { Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';\n\nBacktest.background('BTCUSDT', {\n  strategyName: 'llm-strategy',\n  exchangeName: 'binance',\n  frameName: '1d-test',\n});\n\nlistenSignalBacktest((event) =\u003e console.log(event));\nlistenDoneBacktest(async (event) =\u003e {\n  await Backtest.dump(event.symbol, event.strategyName);  // Generate report\n});\n```\n\n### 📈 Run Live Trading\n```typescript\nimport { Live, listenSignalLive } from 'backtest-kit';\n\nLive.background('BTCUSDT', {\n  strategyName: 'llm-strategy',\n  exchangeName: 'binance',  // Use API keys in .env\n});\n\nlistenSignalLive((event) =\u003e console.log(event));\n```\n\n### 📡 Monitoring \u0026 Events\n\n- Use `listenRisk`, `listenError`, `listenPartialProfit/Loss` for alerts.\n- Dump reports: `Backtest.dump()`, `Live.dump()`.\n\n## 🌐 Global Configuration\n\nCustomize via `setConfig()`:\n\n- `CC_SCHEDULE_AWAIT_MINUTES`: Pending timeout (default: 120).\n- `CC_AVG_PRICE_CANDLES_COUNT`: VWAP candles (default: 5).\n\n## 💻 Developer Note\n\nBacktest Kit is **not a data-processing library** - it is a **time execution engine**. Think of the engine as an **async stream of time**, where your strategy is evaluated step by step.\n\n### 💰 How PNL Works\n\nThese three functions work together to manage a position dynamically. To reduce position linearity, the framework treats every DCA entry as a fixed **$100 unit** regardless of price — this flattens the effective entry curve and makes PNL weighting independent of position size.\n\n**Public API:**\n- **`commitAverageBuy`** — adds a new DCA entry. For LONG, **only accepted when current price is below a new low**. Silently rejected otherwise. This prevents averaging up. Can be overridden using `setConfig`\n- **`commitPartialProfit`** — closes X% of the position at a profit. Locks in gains while keeping exposure.\n- **`commitPartialLoss`** — closes X% of the position at a loss. Cuts exposure before the stop-loss is hit.\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    The Math\n  \u003c/summary\u003e\n\n  **Scenario:** LONG entry @ 1000, 4 DCA attempts (1 rejected), 3 partials, closed at TP.\n  `totalInvested = $400` (4 × $100, rejected attempt not counted).\n\n  **Entries**\n  ```\n    entry#1 @ 1000  → 0.10000 coins\n      commitPartialProfit(30%) @ 1150          ← cnt=1\n    entry#2 @ 950   → 0.10526 coins\n    entry#3 @ 880   → 0.11364 coins\n      commitPartialLoss(20%)   @ 860           ← cnt=3\n    entry#4 @ 920   → 0.10870 coins\n      commitPartialProfit(40%) @ 1050          ← cnt=4\n    entry#5 @ 980   ✗ REJECTED (980 \u003e ep3≈929.92)\n    totalInvested = $400\n  ```\n\n  **Partial#1 — commitPartialProfit @ 1150, 30%, cnt=1**\n  ```\n    effectivePrice = hm(1000) = 1000\n    costBasis = $100\n    partialDollarValue = 30% × 100 = $30  → weight = 30/400 = 0.075\n    pnl = (1150−1000)/1000 × 100 = +15.00%\n    costBasis → $70\n    coins sold: 0.03000 × 1150 = $34.50\n    remaining:  0.07000\n  ```\n\n  **DCA after Partial#1**\n  ```\n    entry#2 @ 950  (950 \u003c ep1=1000 ✓ accepted)\n    entry#3 @ 880  (880 \u003c ep1=1000 ✓ accepted)\n    coins: 0.07000 + 0.10526 + 0.11364 = 0.28890\n  ```\n\n  **Partial#2 — commitPartialLoss @ 860, 20%, cnt=3**\n  ```\n    costBasis = 70 + 100 + 100 = $270\n    ep2 = 270 / 0.28890 ≈ 934.58\n    partialDollarValue = 20% × 270 = $54  → weight = 54/400 = 0.135\n    pnl = (860−934.58)/934.58 × 100 ≈ −7.98%\n    costBasis → $216\n    coins sold: 0.05778 × 860 = $49.69\n    remaining:  0.23112\n  ```\n\n  **DCA after Partial#2**\n  ```\n    entry#4 @ 920  (920 \u003c ep2=934.58 ✓ accepted)\n    coins: 0.23112 + 0.10870 = 0.33982\n  ```\n\n  **Partial#3 — commitPartialProfit @ 1050, 40%, cnt=4**\n  ```\n    costBasis = 216 + 100 = $316\n    ep3 = 316 / 0.33982 ≈ 929.92\n    partialDollarValue = 40% × 316 = $126.4  → weight = 126.4/400 = 0.316\n    pnl = (1050−929.92)/929.92 × 100 ≈ +12.91%\n    costBasis → $189.6\n    coins sold: 0.13593 × 1050 = $142.72\n    remaining:  0.20389\n  ```\n\n  **DCA after Partial#3 — rejected**\n  ```\n    entry#5 @ 980  (980 \u003e ep3≈929.92 ✗ REJECTED)\n  ```\n\n  **Close at TP @ 1200**\n  ```\n    ep_final = ep3 ≈ 929.92  (no new entries)\n    coins: 0.20389\n\n    remainingDollarValue = 400 − 30 − 54 − 126.4 = $189.6\n    weight = 189.6/400 = 0.474\n    pnl = (1200−929.92)/929.92 × 100 ≈ +29.04%\n    coins sold: 0.20389 × 1200 = $244.67\n  ```\n\n  **Result (toProfitLossDto)**\n  ```\n    0.075 × (+15.00) = +1.125\n    0.135 × (−7.98)  = −1.077\n    0.316 × (+12.91) = +4.080\n    0.474 × (+29.04) = +13.765\n    ─────────────────────────────\n                    ≈ +17.89%\n\n    Cross-check (coins):\n    34.50 + 49.69 + 142.72 + 244.67 = $471.58\n    (471.58 − 400) / 400 × 100      = +17.90%  ✓\n  ```\n\u003c/details\u003e\n\n**`priceOpen`** is the harmonic mean of all accepted DCA entries. After each partial close (`commitPartialProfit` or `commitPartialLoss`), the remaining cost basis is carried forward into the harmonic mean calculation for subsequent entries — so `priceOpen` shifts after every partial, which in turn changes whether the next `commitAverageBuy` call will be accepted.\n\n### 🔍 How getCandles Works\n\nbacktest-kit uses Node.js `AsyncLocalStorage` to automatically provide\ntemporal time context to your strategies.\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    The Math\n  \u003c/summary\u003e\n\n  For a candle with:\n  - `timestamp` = candle open time (openTime)\n  - `stepMs` = interval duration (e.g., 60000ms for \"1m\")\n  - Candle close time = `timestamp + stepMs`\n\n  **Alignment:** All timestamps are aligned down to interval boundary.\n  For example, for 15m interval: 00:17 → 00:15, 00:44 → 00:30\n\n  **Adapter contract:**\n  - First candle.timestamp must equal aligned `since`\n  - Adapter must return exactly `limit` candles\n  - Sequential timestamps: `since + i * stepMs` for i = 0..limit-1\n\n  **How `since` is calculated from `when`:**\n  - `when` = current execution context time (from AsyncLocalStorage)\n  - `alignedWhen` = `Math.floor(when / stepMs) * stepMs` (aligned down to interval boundary)\n  - `since` = `alignedWhen - limit * stepMs` (go back `limit` candles from aligned when)\n\n  **Boundary semantics (inclusive/exclusive):**\n  - `since` is always **inclusive** — first candle has `timestamp === since`\n  - Exactly `limit` candles are returned\n  - Last candle has `timestamp === since + (limit - 1) * stepMs` — **inclusive**\n  - For `getCandles`: `alignedWhen` is **exclusive** — candle at that timestamp is NOT included (it's a pending/incomplete candle)\n  - For `getRawCandles`: `eDate` is **exclusive** — candle at that timestamp is NOT included (it's a pending/incomplete candle)\n  - For `getNextCandles`: `alignedWhen` is **inclusive** — first candle starts at `alignedWhen` (it's the current candle for backtest, already closed in historical data)\n\n  - `getCandles(symbol, interval, limit)` - Returns exactly `limit` candles\n    - Aligns `when` down to interval boundary\n    - Calculates `since = alignedWhen - limit * stepMs`\n    - **since — inclusive**, first candle.timestamp === since\n    - **alignedWhen — exclusive**, candle at alignedWhen is NOT returned\n    - Range: `[since, alignedWhen)` — half-open interval\n    - Example: `getCandles(\"BTCUSDT\", \"1m\", 100)` returns 100 candles ending before aligned when\n\n  - `getNextCandles(symbol, interval, limit)` - Returns exactly `limit` candles (backtest only)\n    - Aligns `when` down to interval boundary\n    - `since = alignedWhen` (starts from aligned when, going forward)\n    - **since — inclusive**, first candle.timestamp === since\n    - Range: `[alignedWhen, alignedWhen + limit * stepMs)` — half-open interval\n    - Throws error in live mode to prevent look-ahead bias\n    - Example: `getNextCandles(\"BTCUSDT\", \"1m\", 10)` returns next 10 candles starting from aligned when\n\n  - `getRawCandles(symbol, interval, limit?, sDate?, eDate?)` - Flexible parameter combinations:\n    - `(limit)` - since = alignedWhen - limit * stepMs, range `[since, alignedWhen)`\n    - `(limit, sDate)` - since = align(sDate), returns `limit` candles forward, range `[since, since + limit * stepMs)`\n    - `(limit, undefined, eDate)` - since = align(eDate) - limit * stepMs, **eDate — exclusive**, range `[since, eDate)`\n    - `(undefined, sDate, eDate)` - since = align(sDate), limit calculated from range, **sDate — inclusive, eDate — exclusive**, range `[sDate, eDate)`\n    - `(limit, sDate, eDate)` - since = align(sDate), returns `limit` candles, **sDate — inclusive**\n    - All combinations respect look-ahead bias protection (eDate/endTime \u003c= when)\n\n  **Persistent Cache:**\n  - Cache lookup calculates expected timestamps: `since + i * stepMs` for i = 0..limit-1\n  - Returns all candles if found, null if any missing (cache miss)\n  - Cache and runtime use identical timestamp calculation logic\n\n\u003c/details\u003e\n\n#### Candle Timestamp Convention:\n\nAccording to this `timestamp` of a candle in backtest-kit is exactly the `openTime`, not ~~`closeTime`~~\n\n**Key principles:**\n- All timestamps are aligned down to interval boundary\n- First candle.timestamp must equal aligned `since`\n- Adapter must return exactly `limit` candles\n- Sequential timestamps: `since + i * stepMs`\n\n\n### 🔍 How getOrderBook Works\n\nOrder book fetching uses the same temporal alignment as candles, but with a configurable time offset window instead of candle intervals.\n\n  \u003cdetails\u003e\n    \u003csummary\u003e\n      The Math\n    \u003c/summary\u003e\n\n    **Time range calculation:**\n    - `when` = current execution context time (from AsyncLocalStorage)\n    - `offsetMinutes` = `CC_ORDER_BOOK_TIME_OFFSET_MINUTES` (configurable)\n    - `alignedTo` = `Math.floor(when / (offsetMinutes * 60000)) * (offsetMinutes * 60000)`\n    - `to` = `alignedTo` (aligned down to offset boundary)\n    - `from` = `alignedTo - offsetMinutes * 60000`\n\n    **Adapter contract:**\n    - `getOrderBook(symbol, depth, from, to, backtest)` is called on the exchange schema\n    - `depth` defaults to `CC_ORDER_BOOK_MAX_DEPTH_LEVELS`\n    - The `from`/`to` range represents a time window of exactly `offsetMinutes` duration\n    - Schema implementation may use the time range (backtest) or ignore it (live trading)\n\n    **Example with CC_ORDER_BOOK_TIME_OFFSET_MINUTES = 10:**\n    ```\n    when = 1704067920000       // 2024-01-01 00:12:00 UTC\n    offsetMinutes = 10\n    offsetMs = 10 * 60000      // 600000ms\n\n    alignedTo = Math.floor(1704067920000 / 600000) * 600000\n              = 1704067800000  // 2024-01-01 00:10:00 UTC\n\n    to   = 1704067800000       // 00:10:00 UTC\n    from = 1704067200000       // 00:00:00 UTC\n    ```\n  \u003c/details\u003e\n\n#### Order Book Timestamp Convention:\n\nUnlike candles, most exchanges (e.g. Binance `GET /api/v3/depth`) only expose the **current** order book with no historical query support — for backtest you must provide your own snapshot storage.\n\n**Key principles:**\n- Time range is aligned down to `CC_ORDER_BOOK_TIME_OFFSET_MINUTES` boundary\n- `to` = aligned timestamp, `from` = `to - offsetMinutes * 60000`\n- `depth` defaults to `CC_ORDER_BOOK_MAX_DEPTH_LEVELS`\n- Adapter receives `(symbol, depth, from, to, backtest)` — may ignore `from`/`to` in live mode\n\n### 🔍 How getAggregatedTrades Works\n\nAggregated trades fetching uses the same look-ahead bias protection as candles - `to` is always aligned down to the nearest minute boundary so future trades are never visible to the strategy.\n\n**Key principles:**\n- `to` is always aligned down to the 1-minute boundary — prevents look-ahead bias\n- Without `limit`: returns one full window (`CC_AGGREGATED_TRADES_MAX_MINUTES`)\n- With `limit`: paginates backwards until collected, then slices to most recent `limit`\n- Adapter receives `(symbol, from, to, backtest)` — may ignore `from`/`to` in live mode\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    The Math\n  \u003c/summary\u003e\n\n  **Time range calculation:**\n  - `when` = current execution context time (from AsyncLocalStorage)\n  - `alignedTo` = `Math.floor(when / 60000) * 60000` (aligned down to 1-minute boundary)\n  - `windowMs` = `CC_AGGREGATED_TRADES_MAX_MINUTES * 60000 − 60000`\n  - `to` = `alignedTo`, `from` = `alignedTo − windowMs`\n\n  **Without `limit`:** fetches a single window and returns it as-is.\n\n  **With `limit`:** paginates backwards in `CC_AGGREGATED_TRADES_MAX_MINUTES` chunks until at least `limit` trades are collected, then slices to the most recent `limit` trades.\n\n  **Example with CC_AGGREGATED_TRADES_MAX_MINUTES = 60, limit = 200:**\n  ```\n  when       = 1704067920000   // 2024-01-01 00:12:00 UTC\n  alignedTo  = 1704067800000   // 2024-01-01 00:12:00 → aligned to 00:12:00\n  windowMs   = 59 * 60000      // 3540000ms = 59 minutes\n\n  Window 1:  from = 00:12:00 − 59m = 23:13:00\n              to   = 00:12:00\n  → got 120 trades — not enough\n\n  Window 2:  from = 23:13:00 − 59m = 22:14:00\n              to   = 23:13:00\n  → got 100 more → total 220 trades\n\n  result = last 200 of 220 (most recent)\n  ```\n\n  **Adapter contract:**\n  - `getAggregatedTrades(symbol, from, to, backtest)` is called on the exchange schema\n  - `from`/`to` are `Date` objects\n  - Schema implementation may use the time range (backtest) or ignore it (live trading)\n\n\u003c/details\u003e\n\n**Compatible with:** [garch](https://www.npmjs.com/package/garch) for volatility modelling and [volume-anomaly](https://www.npmjs.com/package/volume-anomaly) for detecting abnormal trade volume — both accept the same `from`/`to` time range format that `getAggregatedTrades` produces.\n\n### 🔬 Technical Details: Timestamp Alignment\n\n**Why align timestamps to interval boundaries?**\n\nBecause candle APIs return data starting from exact interval boundaries:\n\n```typescript\n// 15-minute interval example:\nwhen = 1704067920000       // 00:12:00\nstep = 15                  // 15 minutes\nstepMs = 15 * 60000        // 900000ms\n\n// Alignment: round down to nearest interval boundary\nalignedWhen = Math.floor(when / stepMs) * stepMs\n// = Math.floor(1704067920000 / 900000) * 900000\n// = 1704067200000 (00:00:00)\n\n// Calculate since for 4 candles backwards:\nsince = alignedWhen - 4 * stepMs\n// = 1704067200000 - 4 * 900000\n// = 1704063600000 (23:00:00 previous day)\n\n// Expected candles:\n// [0] timestamp = 1704063600000 (23:00)\n// [1] timestamp = 1704064500000 (23:15)\n// [2] timestamp = 1704065400000 (23:30)\n// [3] timestamp = 1704066300000 (23:45)\n```\n\n**Pending candle exclusion:** The candle at `00:00:00` (alignedWhen) is NOT included in the result. At `when=00:12:00`, this candle covers the period `[00:00, 00:15)` and is still open (pending). Pending candles have incomplete OHLCV data that would distort technical indicators. Only fully closed candles are returned.\n\n**Validation is applied consistently across:**\n- ✅ `getCandles()` - validates first timestamp and count\n- ✅ `getNextCandles()` - validates first timestamp and count\n- ✅ `getRawCandles()` - validates first timestamp and count\n- ✅ Cache read - calculates exact expected timestamps\n- ✅ Cache write - stores validated candles\n\n**Result:** Deterministic candle retrieval with exact timestamp matching.\n\n### 🕐 Timezone Warning: Candle Boundaries Are UTC-Based\n\nAll candle timestamp alignment uses UTC (Unix epoch). For intervals like `4h`, boundaries are `00:00, 04:00, 08:00, 12:00, 16:00, 20:00 UTC`. If your local timezone offset is not a multiple of the interval, the `since` timestamps will look \"uneven\" in local time.\n\nFor example, in UTC+5 the same 4h candle request logs as:\n\n```\nsince: Sat Sep 20 2025 13:00:00 GMT+0500  ← looks uneven (13:00)\nsince: Sat Sep 20 2025 17:00:00 GMT+0500  ← looks uneven (17:00)\nsince: Sat Sep 20 2025 21:00:00 GMT+0500  ← looks uneven (21:00)\nsince: Sun Sep 21 2025 05:00:00 GMT+0500  ← looks uneven (05:00)\n```\n\nBut in UTC these are perfectly aligned 4h boundaries:\n\n```\nsince: Sat, 20 Sep 2025 08:00:00 GMT  ← 08:00 UTC ✓\nsince: Sat, 20 Sep 2025 12:00:00 GMT  ← 12:00 UTC ✓\nsince: Sat, 20 Sep 2025 16:00:00 GMT  ← 16:00 UTC ✓\nsince: Sun, 21 Sep 2025 00:00:00 GMT  ← 00:00 UTC ✓\n```\n\nUse `toUTCString()` or `toISOString()` in callbacks to see the actual aligned UTC times.\n\n### 💭 What this means:\n- `getCandles()` always returns data UP TO the current backtest timestamp using `async_hooks`\n- Multi-timeframe data is automatically synchronized\n- **Impossible to introduce look-ahead bias** - all time boundaries are enforced\n- Same code works in both backtest and live modes\n- Boundary semantics prevent edge cases in signal generation\n\n\n## 🧠 Two Ways to Run the Engine\n\nBacktest Kit exposes the same runtime in two equivalent forms. Both approaches use **the same engine and guarantees** - only the consumption model differs.\n\n### 1️⃣ Event-driven (background execution)\n\nSuitable for production bots, monitoring, and long-running processes.\n\n```typescript\nBacktest.background('BTCUSDT', config);\n\nlistenSignalBacktest(event =\u003e { /* handle signals */ });\nlistenDoneBacktest(event =\u003e { /* finalize / dump report */ });\n```\n\n### 2️⃣ Async Iterator (pull-based execution)\n\nSuitable for research, scripting, testing, and LLM agents.\n\n```typescript\nfor await (const event of Backtest.run('BTCUSDT', config)) {\n  // signal | trade | progress | done\n}\n```\n\n## ⚔️ Think of it as...\n\n**Open-source QuantConnect/MetaTrader without the vendor lock-in**\n\nUnlike cloud-based platforms, backtest-kit runs entirely in your environment. You own the entire stack from data ingestion to live execution. In addition to Ollama, you can use [neural-trader](https://www.npmjs.com/package/neural-trader) in `getSignal` function or any other third party library\n\n- No C#/C++ required - pure TypeScript/JavaScript\n- Self-hosted - your code, your data, your infrastructure\n- No platform fees or hidden costs\n- Full control over execution and data sources\n- [GUI](https://npmjs.com/package/@backtest-kit/ui) for visualization and monitoring\n\n## 🌍 Ecosystem\n\nThe `backtest-kit` ecosystem extends beyond the core library, offering complementary packages and tools to enhance your trading system development experience:\n\n\n### @backtest-kit/cli\n\n\u003e **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/cli)** 📟\n\nThe **@backtest-kit/cli** package is a zero-boilerplate CLI runner for backtest-kit strategies. Point it at your strategy file and run backtests, paper trading, or live bots — no infrastructure code required.\n\n#### Key Features\n- 🚀 **Zero Config**: Run a backtest with one command — no setup code needed\n- 🔄 **Three Modes**: `--backtest`, `--paper`, `--live` with graceful SIGINT shutdown\n- 💾 **Auto Cache**: Warms OHLCV candle cache for all intervals before the backtest starts\n- 🌐 **Web Dashboard**: Launch `@backtest-kit/ui` with a single `--ui` flag\n- 📬 **Telegram Alerts**: Formatted trade notifications with price charts via `--telegram`\n- 🗂️ **Monorepo Ready**: Each strategy's `dump/`, `modules/`, and `template/` are automatically isolated by entry point directory\n\n#### Use Case\nThe fastest way to run any backtest-kit strategy from the command line. Instead of writing boilerplate for storage, notifications, candle caching, and signal logging, add one dependency and wire up your `package.json` scripts. Works equally well for a single-strategy project or a monorepo with dozens of strategies in separate subdirectories.\n\n#### Get Started\n```bash\nnpx -y @backtest-kit/cli --init\n```\n\n\n### @backtest-kit/pinets\n\n\u003e **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/pinets)** 📜\n\nThe **@backtest-kit/pinets** package lets you run TradingView Pine Script strategies directly in Node.js. Port your existing Pine Script indicators to backtest-kit with zero rewrite using the [PineTS](https://github.com/QuantForgeOrg/PineTS) runtime.\n\n#### Key Features\n- 📜 **Pine Script v5/v6**: Native TradingView syntax with 1:1 compatibility\n- 🎯 **60+ Indicators**: SMA, EMA, RSI, MACD, Bollinger Bands, ATR, Stochastic built-in\n- 📁 **File or Code**: Load `.pine` files or pass code strings directly\n- 🗺️ **Plot Extraction**: Flexible mapping from Pine `plot()` outputs to structured signals\n- ⚡ **Cached Execution**: Memoized file reads for repeated strategy runs\n\n#### Use Case\nPerfect for traders who already have working TradingView strategies. Instead of rewriting your Pine Script logic in JavaScript, simply copy your `.pine` file and use `getSignal()` to extract trading signals. Works seamlessly with backtest-kit's temporal context - no look-ahead bias possible.\n\n#### Get Started\n```bash\nnpm install @backtest-kit/pinets pinets backtest-kit\n```\n\n\n### @backtest-kit/graph\n\n\u003e **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/graph)** 🔗\n\nThe **@backtest-kit/graph** package lets you compose backtest-kit computations as a typed directed acyclic graph (DAG). Define source nodes that fetch market data and output nodes that compute derived values — then resolve the whole graph in topological order with automatic parallelism.\n\n#### Key Features\n- 🔌 **DAG Execution**: Nodes are resolved bottom-up in topological order with `Promise.all` parallelism\n- 🔒 **Type-Safe Values**: TypeScript infers the return type of every node through the graph via generics\n- 🧱 **Two APIs**: Low-level `INode` for runtime/storage, high-level `sourceNode` + `outputNode` builders for authoring\n- 💾 **DB-Ready Serialization**: `serialize` / `deserialize` convert the graph to a flat `IFlatNode[]` list with `id` / `nodeIds`\n- 🌐 **Context-Aware Fetch**: `sourceNode` receives `(symbol, when, exchangeName)` from the execution context automatically\n\n#### Use Case\nPerfect for multi-timeframe strategies where multiple Pine Script or indicator computations must be combined. Instead of manually chaining async calls, define each computation as a node and let the graph resolve dependencies in parallel. Adding a new filter or timeframe requires no changes to the existing wiring.\n\n#### Get Started\n```bash\nnpm install @backtest-kit/graph backtest-kit\n```\n\n\n### @backtest-kit/ui\n\n\u003e **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/ui)** 📊\n\nThe **@backtest-kit/ui** package is a full-stack UI framework for visualizing cryptocurrency trading signals, backtests, and real-time market data. Combines a Node.js backend server with a React dashboard - all in one package.\n\n#### Key Features\n- 📈 **Interactive Charts**: Candlestick visualization with Lightweight Charts (1m, 15m, 1h timeframes)\n- 🎯 **Signal Tracking**: View opened, closed, scheduled, and cancelled signals with full details\n- 📊 **Risk Analysis**: Monitor risk rejections and position management\n- 🔔 **Notifications**: Real-time notification system for all trading events\n- 💹 **Trailing \u0026 Breakeven**: Visualize trailing stop/take and breakeven events\n- 🎨 **Material Design**: Beautiful UI with MUI 5 and Mantine components\n\n#### Use Case\nPerfect for monitoring your trading bots in production. Instead of building custom dashboards, `@backtest-kit/ui` provides a complete visualization layer out of the box. Each signal view includes detailed information forms, multi-timeframe candlestick charts, and JSON export for all data.\n\n#### Get Started\n```bash\nnpm install @backtest-kit/ui backtest-kit ccxt\n```\n\n\n### @backtest-kit/ollama\n\n\u003e **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/ollama)** 🤖\n\nThe **@backtest-kit/ollama** package is a multi-provider LLM inference library that supports 10+ providers including OpenAI, Claude, DeepSeek, Grok, Mistral, Perplexity, Cohere, Alibaba, Hugging Face, and Ollama with unified API and automatic token rotation.\n\n#### Key Features\n- 🔌 **10+ LLM Providers**: OpenAI, Claude, DeepSeek, Grok, Mistral, Perplexity, Cohere, Alibaba, Hugging Face, Ollama\n- 🔄 **Token Rotation**: Automatic API key rotation for Ollama (others throw clear errors)\n- 🎯 **Structured Output**: Enforced JSON schema for trading signals (position, price levels, risk notes)\n- 🔑 **Flexible Auth**: Context-based API keys or environment variables\n- ⚡ **Unified API**: Single interface across all providers\n- 📊 **Trading-First**: Built for backtest-kit with position sizing and risk management\n\n#### Use Case\nIdeal for building multi-provider LLM strategies with fallback chains and ensemble predictions. The package returns structured trading signals with validated TP/SL levels, making it perfect for use in `getSignal` functions. Supports both backtest and live trading modes.\n\n#### Get Started\n```bash\nnpm install @backtest-kit/ollama agent-swarm-kit backtest-kit\n```\n\n\n### @backtest-kit/signals\n\n\u003e **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/signals)** 📊\n\nThe **@backtest-kit/signals** package is a technical analysis and trading signal generation library designed for AI-powered trading systems. It computes 50+ indicators across 4 timeframes and generates markdown reports optimized for LLM consumption.\n\n#### Key Features\n- 📈 **Multi-Timeframe Analysis**: 1m, 15m, 30m, 1h with synchronized indicator computation\n- 🎯 **50+ Technical Indicators**: RSI, MACD, Bollinger Bands, Stochastic, ADX, ATR, CCI, Fibonacci, Support/Resistance\n- 📊 **Order Book Analysis**: Bid/ask depth, spread, liquidity imbalance, top 20 levels\n- 🤖 **AI-Ready Output**: Markdown reports formatted for LLM context injection\n- ⚡ **Performance Optimized**: Intelligent caching with configurable TTL per timeframe\n\n#### Use Case\nPerfect for injecting comprehensive market context into your LLM-powered strategies. Instead of manually calculating indicators, `@backtest-kit/signals` provides a single function call that adds all technical analysis to your message context. Works seamlessly with `getSignal` function in backtest-kit strategies.\n\n#### Get Started\n```bash\nnpm install @backtest-kit/signals backtest-kit\n```\n\n\n\n### @backtest-kit/sidekick\n\n\u003e **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/sidekick)** 🚀\n\nThe **@backtest-kit/sidekick** package is the easiest way to create a new Backtest Kit trading bot project. Like create-react-app, but for algorithmic trading.\n\n#### Key Features\n- 🚀 **Zero Config**: Get started with one command - no setup required\n- 📦 **Complete Template**: Includes backtest strategy, risk management, and LLM integration\n- 🤖 **AI-Powered**: Pre-configured with DeepSeek, Claude, and GPT-5 fallback chain\n- 📊 **Technical Analysis**: Built-in 50+ indicators via @backtest-kit/signals\n- 🔑 **Environment Setup**: Auto-generated .env with all API key placeholders\n- 📝 **Best Practices**: Production-ready code structure with examples\n\n#### Use Case\nThe fastest way to bootstrap a new trading bot project. Instead of manually setting up dependencies, configurations, and boilerplate code, simply run one command and get a working project with LLM-powered strategy, multi-timeframe technical analysis, and risk management validation.\n\n#### Get Started\n```bash\nnpx -y @backtest-kit/sidekick my-trading-bot\ncd my-trading-bot\nnpm start\n```\n\n\n## 🤖 Are you a robot?\n\n**For language models**: Read extended description in [./LLMs.md](./LLMs.md)\n\n## ✅ Tested \u0026 Reliable\n\n450+ tests cover validation, recovery, reports, and events.\n\n## 🤝 Contribute\n\nFork/PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).\n\n## 📜 License\n\nMIT © [tripolskypetr](https://github.com/tripolskypetr)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftripolskypetr%2Fbacktest-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftripolskypetr%2Fbacktest-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftripolskypetr%2Fbacktest-kit/lists"}