tradelab 1.0.1 → 1.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/CHANGELOG.md +66 -0
- package/README.md +75 -12
- package/bin/tradelab-mcp.js +7 -0
- package/bin/tradelab.js +29 -0
- package/dist/cjs/data.cjs +149 -26
- package/dist/cjs/index.cjs +1893 -1003
- package/dist/cjs/live.cjs +134 -25
- package/dist/cjs/ta.cjs +339 -0
- package/docs/api-reference.md +46 -0
- package/docs/backtest-engine.md +112 -0
- package/docs/live-trading.md +51 -0
- package/docs/mcp.md +64 -0
- package/docs/research.md +103 -0
- package/docs/superpowers/plans/2026-00-overview.md +101 -0
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +873 -0
- package/docs/superpowers/plans/2026-02-indicator-library.md +677 -0
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +882 -0
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +981 -0
- package/docs/superpowers/plans/2026-05-mcp-server.md +758 -0
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +508 -0
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +535 -0
- package/docs/superpowers/plans/2026-08-live-dashboard.md +547 -0
- package/docs/superpowers/plans/HANDOFF.md +88 -0
- package/examples/liveDashboard.js +33 -0
- package/examples/llmSignal.js +33 -0
- package/examples/optimize.js +25 -0
- package/package.json +16 -2
- package/src/engine/asyncSignal.js +28 -0
- package/src/engine/backtest.js +13 -1
- package/src/engine/backtestAsync.js +27 -0
- package/src/engine/backtestTicks.js +13 -2
- package/src/engine/barSystemRunner.js +96 -41
- package/src/engine/execution.js +39 -0
- package/src/engine/grid.js +15 -0
- package/src/engine/llmSignal.js +84 -0
- package/src/engine/optimize.js +86 -0
- package/src/engine/optimizeWorker.js +67 -0
- package/src/engine/walkForward.js +1 -0
- package/src/index.js +9 -0
- package/src/live/dashboard/server.js +120 -0
- package/src/live/engine/liveEngine.js +2 -2
- package/src/live/index.js +1 -0
- package/src/mcp/schemas.js +48 -0
- package/src/mcp/server.js +31 -0
- package/src/mcp/tools.js +142 -0
- package/src/metrics/annualize.js +32 -0
- package/src/metrics/benchmark.js +55 -0
- package/src/metrics/buildMetrics.js +34 -13
- package/src/metrics/finite.js +17 -0
- package/src/research/combinations.js +18 -0
- package/src/research/cpcv.js +47 -0
- package/src/research/deflatedSharpe.js +35 -0
- package/src/research/index.js +6 -0
- package/src/research/monteCarlo.js +88 -0
- package/src/research/pbo.js +69 -0
- package/src/research/stats.js +78 -0
- package/src/strategies/builtins.js +96 -0
- package/src/strategies/index.js +30 -0
- package/src/ta/channels.js +67 -0
- package/src/ta/index.js +16 -0
- package/src/ta/oscillators.js +70 -0
- package/src/ta/trend.js +78 -0
- package/src/utils/random.js +33 -0
- package/templates/dashboard.html +174 -0
- package/types/index.d.ts +154 -0
- package/types/live.d.ts +15 -0
- package/types/ta.d.ts +45 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.1.0] - 2026-06-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Metrics correctness and annualization**
|
|
13
|
+
- `sharpeAnnualized` and `sortinoAnnualized` fields in `buildMetrics` output, scaled by `sqrt(periodsPerYear)`.
|
|
14
|
+
- `annualizationPeriods` field exposing the computed periods-per-year used for scaling.
|
|
15
|
+
- All ratio metrics (`profitFactor`, `sharpe`, `calmar`, etc.) now clamped to a finite sentinel via `clampFinite`; metrics JSON never emits `Infinity` or `NaN`.
|
|
16
|
+
- Benchmark statistics (`alpha`, `beta`, `correlation`, `informationRatio`, `trackingError`) available in `metrics.benchmark` when `benchmarkReturns` is passed to `buildMetrics`.
|
|
17
|
+
- New top-level exports: `clampFinite`, `BIG_NUMBER`, `periodsPerYear`, `benchmarkStats`.
|
|
18
|
+
|
|
19
|
+
- **`tradelab/ta` indicator namespace** (new subpath export)
|
|
20
|
+
- Oscillators: `rsi`, `macd`, `stochastic`.
|
|
21
|
+
- Channels: `bollinger`, `donchian`, `keltner`.
|
|
22
|
+
- Trend: `supertrend`, `vwap`.
|
|
23
|
+
- Re-exports from core: `ema`, `atr`, `swingHigh`, `swingLow`, `detectFVG`, `lastSwing`, `structureState`.
|
|
24
|
+
- All indicators return full-length arrays aligned to input (warmup positions are `undefined`).
|
|
25
|
+
|
|
26
|
+
- **Async and agent signals**
|
|
27
|
+
- `backtestAsync(options)` — async sibling of `backtest()` where `signal()` may return a `Promise`; accepts `signalBudgetMs` to race each bar's signal against a timeout.
|
|
28
|
+
- `LlmSignal` class — wraps an async `resolve(context)` function with per-bar caching, a configurable `budgetMs` timeout, a no-lookahead candle proxy, and a `log` array of every bar decision.
|
|
29
|
+
- `backtestTicks()` now accepts a `seed` option for reproducible probabilistic limit fills.
|
|
30
|
+
- `LiveEngine` awaits async signals, so `LlmSignal` can be used directly in live/paper execution.
|
|
31
|
+
|
|
32
|
+
- **`tradelab/mcp` MCP server** (new subpath export)
|
|
33
|
+
- `tradelab-mcp` binary — starts an MCP stdio server exposing tradelab tools to any MCP-capable agent (Claude Desktop, Cursor, etc.).
|
|
34
|
+
- Server exposes tools: `list_strategies`, `fetch_candles`, `run_backtest`, `walk_forward`.
|
|
35
|
+
- Name-addressable strategy registry: `listStrategies()`, `getStrategy(name)`, `registerStrategy(name, def)` (exported from `tradelab`).
|
|
36
|
+
|
|
37
|
+
- **Research and overfitting toolkit** (`research` namespace, re-exported from `tradelab`)
|
|
38
|
+
- `research.monteCarlo(returns, options)` — equity curve Monte Carlo simulation.
|
|
39
|
+
- `research.deflatedSharpe(sharpe, options)` — deflated Sharpe ratio (DSR) adjustment.
|
|
40
|
+
- `research.sweepHaircut(results, options)` — apply DSR haircut across a parameter sweep.
|
|
41
|
+
- `research.probabilityOfBacktestOverfitting(folds, options)` — CSCV/PBO overfitting probability.
|
|
42
|
+
- `research.combinatorialPurgedSplits(candles, options)` — combinatorial purged cross-validation (CPCV) split generator.
|
|
43
|
+
|
|
44
|
+
- **Parallel parameter optimization**
|
|
45
|
+
- `optimize(options)` — worker-thread pool that runs a parameter sweep in parallel; accepts `candles`, `signalModulePath`, `parameterSets`, `concurrency`, and `scoreBy`; returns `{ results, leaderboard, best }`.
|
|
46
|
+
- `grid(spec)` — helper that expands a `{ param: [v1, v2] }` spec into an array of parameter-set objects.
|
|
47
|
+
|
|
48
|
+
- **Carry and funding cost model**
|
|
49
|
+
- `costs.carry` — annualized borrow/margin cost (`longAnnualBps`, `shortAnnualBps`); deducted proportionally over hold time.
|
|
50
|
+
- `costs.funding` — perpetual futures funding (`rateBps`, `intervalMs`, `anchorMs`); positive rates charge longs and credit shorts.
|
|
51
|
+
- Closed positions include `exit.financing` (total financing cost already deducted from `exit.pnl`).
|
|
52
|
+
|
|
53
|
+
- **Live dashboard**
|
|
54
|
+
- `createDashboardServer(options)` exported from `tradelab/live` — zero-dependency SSE server using Node `node:http`.
|
|
55
|
+
- `--dashboard` and `--dashboardPort` CLI flags for `tradelab paper` and `tradelab live`.
|
|
56
|
+
|
|
57
|
+
- **Dependencies**: `@modelcontextprotocol/sdk` and `zod` added as runtime dependencies.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Metrics JSON no longer emits `Infinity` or `NaN`; all ratio metrics are clamped to `BIG_NUMBER` (1e9) or zero.
|
|
62
|
+
- Non-annualized Sharpe was not directly comparable across different timeframes; `sharpeAnnualized` provides a consistent cross-timeframe measure.
|
|
63
|
+
|
|
64
|
+
## [1.0.1]
|
|
65
|
+
|
|
66
|
+
Prior release. See git history for details.
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
Use it for backtests, portfolio and walk-forward validation, and live or paper execution through broker adapters while keeping the same `signal()` contract.
|
|
19
19
|
|
|
20
|
+
AI-agent users: see [docs/mcp.md](docs/mcp.md) to run the research loop via MCP.
|
|
21
|
+
|
|
20
22
|
```bash
|
|
21
23
|
npm install tradelab
|
|
22
24
|
```
|
|
@@ -29,6 +31,7 @@ npm install tradelab
|
|
|
29
31
|
- [Quick start](#quick-start)
|
|
30
32
|
- [Loading historical data](#loading-historical-data)
|
|
31
33
|
- [Core concepts](#core-concepts)
|
|
34
|
+
- [AI agents / MCP](#ai-agents--mcp)
|
|
32
35
|
- [Portfolio mode](#portfolio-mode)
|
|
33
36
|
- [Walk-forward optimization](#walk-forward-optimization)
|
|
34
37
|
- [Tick backtests](#tick-backtests)
|
|
@@ -43,16 +46,21 @@ npm install tradelab
|
|
|
43
46
|
|
|
44
47
|
## What it includes
|
|
45
48
|
|
|
46
|
-
| Area
|
|
47
|
-
|
|
|
48
|
-
| **Engine**
|
|
49
|
-
| **
|
|
50
|
-
| **
|
|
51
|
-
| **
|
|
52
|
-
| **
|
|
53
|
-
| **
|
|
54
|
-
| **
|
|
55
|
-
| **
|
|
49
|
+
| Area | What you get |
|
|
50
|
+
| -------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| **Engine** | Candle and tick backtests with position sizing, exits, replay capture, and cost models |
|
|
52
|
+
| **Async / AI signals** | Promise-returning signals, `LlmSignal` caching/budgets, and live async signal support |
|
|
53
|
+
| **Indicators (`ta`)** | RSI, MACD, stochastic, Bollinger, Donchian, Keltner, Supertrend, VWAP, EMA, ATR, swing/FVG helpers |
|
|
54
|
+
| **Optimization** | `optimize()` worker-pool parameter sweep and `grid()` spec helper |
|
|
55
|
+
| **Portfolio** | Multi-system shared-capital simulation with live capital locking and daily loss halts |
|
|
56
|
+
| **Walk-forward** | Rolling and anchored train/test validation with parameter search and stability summaries |
|
|
57
|
+
| **Research / overfitting** | Monte Carlo, deflated Sharpe, sweep haircut, PBO (CSCV), and CPCV combinatorial purging |
|
|
58
|
+
| **AI / MCP server** | `tradelab-mcp` stdio server — run the research loop from Claude Desktop, Cursor, or any MCP agent |
|
|
59
|
+
| **Live execution** | Live and paper engines with broker adapters, state persistence, orchestration, and SSE dashboard |
|
|
60
|
+
| **Data** | Yahoo Finance downloads, CSV import, and local cache helpers |
|
|
61
|
+
| **Costs** | Slippage, spread, commission, annualized carry/borrow, and perpetual futures funding |
|
|
62
|
+
| **Exports** | HTML reports, metrics JSON, and trade CSV |
|
|
63
|
+
| **Dev experience** | TypeScript definitions, ESM/CJS support, CLI for quick runs |
|
|
56
64
|
|
|
57
65
|
---
|
|
58
66
|
|
|
@@ -98,6 +106,14 @@ After the run, check `result.metrics` for the headline numbers and `result.posit
|
|
|
98
106
|
|
|
99
107
|
---
|
|
100
108
|
|
|
109
|
+
## AI agents / MCP
|
|
110
|
+
|
|
111
|
+
`tradelab-mcp` lets MCP-capable agents run the research loop through tools: list strategies, fetch candles, run backtests, and walk-forward validate parameter grids.
|
|
112
|
+
|
|
113
|
+
See [docs/mcp.md](docs/mcp.md) for setup and the Claude Desktop config.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
101
117
|
## Loading historical data
|
|
102
118
|
|
|
103
119
|
Most users can start with `getHistoricalCandles()`. It abstracts over Yahoo Finance and CSV, handles caching, and normalizes the output so it feeds straight into `backtest()`.
|
|
@@ -254,12 +270,35 @@ import { backtestTicks } from "tradelab";
|
|
|
254
270
|
const result = backtestTicks({
|
|
255
271
|
ticks,
|
|
256
272
|
queueFillProbability: 0.35,
|
|
273
|
+
seed: "research-run-1",
|
|
257
274
|
signal,
|
|
258
275
|
});
|
|
259
276
|
```
|
|
260
277
|
|
|
261
278
|
Market entries fill on the next tick, limit orders can fill at the touch with configurable queue probability, and stop exits use the existing cost model with stop-specific slippage if you provide it in `costs.slippageByKind.stop`.
|
|
262
279
|
|
|
280
|
+
Use `seed` to make probabilistic limit fills reproducible across repeated runs with the same data and options.
|
|
281
|
+
|
|
282
|
+
## Async and LLM signals
|
|
283
|
+
|
|
284
|
+
Use `backtestAsync()` when your signal returns a promise. `LlmSignal` wraps async model or agent calls with a per-bar budget, one-decision-per-bar cache, no-lookahead candle view, and a decision log.
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
import { backtestAsync, LlmSignal } from "tradelab";
|
|
288
|
+
|
|
289
|
+
const llm = new LlmSignal({
|
|
290
|
+
budgetMs: 2000,
|
|
291
|
+
async resolve({ candles, bar }) {
|
|
292
|
+
const closes = candles.map((c) => c.close);
|
|
293
|
+
return closes.at(-1) > closes.at(-5) ? { side: "long", stop: bar.close * 0.98, rr: 2 } : null;
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const result = await backtestAsync({ candles, signal: llm.signal, signalBudgetMs: 3000 });
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`LiveEngine` awaits async signals too, so the same `llm.signal` can move from research into paper or live execution.
|
|
301
|
+
|
|
263
302
|
---
|
|
264
303
|
|
|
265
304
|
## Live trading
|
|
@@ -314,6 +353,15 @@ const result = backtest({
|
|
|
314
353
|
commissionPerUnit: 0,
|
|
315
354
|
commissionPerOrder: 1,
|
|
316
355
|
minCommission: 1,
|
|
356
|
+
carry: {
|
|
357
|
+
longAnnualBps: 500,
|
|
358
|
+
shortAnnualBps: 800,
|
|
359
|
+
},
|
|
360
|
+
funding: {
|
|
361
|
+
rateBps: 10,
|
|
362
|
+
intervalMs: 8 * 60 * 60 * 1000,
|
|
363
|
+
anchorMs: 0,
|
|
364
|
+
},
|
|
317
365
|
},
|
|
318
366
|
});
|
|
319
367
|
```
|
|
@@ -322,6 +370,9 @@ const result = backtest({
|
|
|
322
370
|
- Spread is modeled as half-spread paid on entry and exit
|
|
323
371
|
- Commission can be percentage-based, per-unit, per-order, or mixed
|
|
324
372
|
- `minCommission` floors the fee per fill
|
|
373
|
+
- `carry` models annualized overnight financing or borrow costs
|
|
374
|
+
- `funding` models per-interval perpetual futures funding; positive rates charge longs and credit shorts
|
|
375
|
+
- Closed trades include `exit.financing`, already deducted from `exit.pnl`
|
|
325
376
|
|
|
326
377
|
> Leaving costs at zero is the most common cause of inflated backtests. Set them from the start.
|
|
327
378
|
|
|
@@ -396,6 +447,7 @@ You can also point `--strategy` at a local module that exports `default(args)`,
|
|
|
396
447
|
```bash
|
|
397
448
|
node examples/emaCross.js
|
|
398
449
|
node examples/yahooEmaCross.js SPY 1d 1y
|
|
450
|
+
node examples/llmSignal.js
|
|
399
451
|
```
|
|
400
452
|
|
|
401
453
|
The examples are a good place to start if you want something runnable before wiring the package into your own strategy code.
|
|
@@ -408,18 +460,27 @@ The examples are a good place to start if you want something runnable before wir
|
|
|
408
460
|
|
|
409
461
|
```js
|
|
410
462
|
import { backtest, getHistoricalCandles, ema } from "tradelab";
|
|
463
|
+
import { backtestAsync, LlmSignal, optimize, grid } from "tradelab";
|
|
464
|
+
import { research } from "tradelab"; // monteCarlo, deflatedSharpe, probabilityOfBacktestOverfitting, ...
|
|
411
465
|
import { fetchHistorical } from "tradelab/data";
|
|
412
|
-
import { LiveEngine, PaperEngine } from "tradelab/live";
|
|
466
|
+
import { LiveEngine, PaperEngine, createDashboardServer } from "tradelab/live";
|
|
467
|
+
import { rsi, macd, bollinger, vwap, supertrend } from "tradelab/ta";
|
|
468
|
+
import { createServer, startStdioServer } from "tradelab/mcp";
|
|
413
469
|
```
|
|
414
470
|
|
|
415
471
|
### CommonJS
|
|
416
472
|
|
|
417
473
|
```js
|
|
418
474
|
const { backtest, getHistoricalCandles, ema } = require("tradelab");
|
|
475
|
+
const { backtestAsync, LlmSignal, optimize, grid } = require("tradelab");
|
|
476
|
+
const { research } = require("tradelab");
|
|
419
477
|
const { fetchHistorical } = require("tradelab/data");
|
|
420
|
-
const { LiveEngine, PaperEngine } = require("tradelab/live");
|
|
478
|
+
const { LiveEngine, PaperEngine, createDashboardServer } = require("tradelab/live");
|
|
479
|
+
const { rsi, macd, bollinger, vwap, supertrend } = require("tradelab/ta");
|
|
421
480
|
```
|
|
422
481
|
|
|
482
|
+
> `tradelab/mcp` is ESM-only (the MCP SDK is ESM-only). Use the `tradelab-mcp` binary or import it from an ESM context.
|
|
483
|
+
|
|
423
484
|
---
|
|
424
485
|
|
|
425
486
|
## Documentation
|
|
@@ -429,6 +490,8 @@ const { LiveEngine, PaperEngine } = require("tradelab/live");
|
|
|
429
490
|
| [Backtest engine](docs/backtest-engine.md) | Signal contract, all options, result shape, portfolio mode, walk-forward |
|
|
430
491
|
| [Data, reporting, and CLI](docs/data-reporting-cli.md) | Data loading, cache behavior, exports, CLI reference |
|
|
431
492
|
| [Live trading](docs/live-trading.md) | Live engine, broker adapters, paper mode, orchestration, and state persistence |
|
|
493
|
+
| [MCP server](docs/mcp.md) | Run the research loop from any MCP-capable agent |
|
|
494
|
+
| [Research & overfitting](docs/research.md) | Monte Carlo, deflated Sharpe, PBO, CPCV, sweep haircut |
|
|
432
495
|
| [Strategy examples](docs/examples.md) | Mean reversion, breakout, sentiment, LLM, and portfolio strategy patterns |
|
|
433
496
|
| [API reference](docs/api-reference.md) | Compact index of every public export |
|
|
434
497
|
|
package/bin/tradelab.js
CHANGED
|
@@ -116,6 +116,22 @@ function createBrokerAdapter(args, overrides = {}) {
|
|
|
116
116
|
throw new Error(`Unsupported broker "${brokerName}"`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
async function maybeStartDashboard(args, source) {
|
|
120
|
+
if (!toBoolean(args.dashboard, false)) return null;
|
|
121
|
+
const { createDashboardServer } = await import("../src/live/dashboard/server.js");
|
|
122
|
+
const dashboard = createDashboardServer({
|
|
123
|
+
source,
|
|
124
|
+
port: toNumber(args.dashboardPort, 4317),
|
|
125
|
+
});
|
|
126
|
+
const url = await dashboard.start();
|
|
127
|
+
console.log(`dashboard: ${url}`);
|
|
128
|
+
return dashboard;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function closeDashboard(dashboard) {
|
|
132
|
+
if (dashboard) await dashboard.close();
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
function brokerConfigFromArgs(args, overrides = {}) {
|
|
120
136
|
return {
|
|
121
137
|
apiKey: overrides.apiKey ?? args.apiKey,
|
|
@@ -514,6 +530,7 @@ async function commandLive(args, overrides = {}) {
|
|
|
514
530
|
maxDailyLossPct: toNumber(fileConfig.maxDailyLossPct ?? args.maxDailyLossPct, 0),
|
|
515
531
|
equity: toNumber(fileConfig.equity ?? args.equity, 10_000),
|
|
516
532
|
});
|
|
533
|
+
const dashboard = await maybeStartDashboard(args, orchestrator);
|
|
517
534
|
await broker.connect(brokerConfig);
|
|
518
535
|
await orchestrator.start();
|
|
519
536
|
|
|
@@ -526,6 +543,15 @@ async function commandLive(args, overrides = {}) {
|
|
|
526
543
|
|
|
527
544
|
if (!watch) {
|
|
528
545
|
await orchestrator.stop();
|
|
546
|
+
await closeDashboard(dashboard);
|
|
547
|
+
} else if (dashboard) {
|
|
548
|
+
const shutdown = async () => {
|
|
549
|
+
await orchestrator.stop();
|
|
550
|
+
await closeDashboard(dashboard);
|
|
551
|
+
process.exit(0);
|
|
552
|
+
};
|
|
553
|
+
process.once("SIGINT", shutdown);
|
|
554
|
+
process.once("SIGTERM", shutdown);
|
|
529
555
|
}
|
|
530
556
|
return;
|
|
531
557
|
}
|
|
@@ -551,6 +577,7 @@ async function commandLive(args, overrides = {}) {
|
|
|
551
577
|
storage,
|
|
552
578
|
brokerConfig,
|
|
553
579
|
});
|
|
580
|
+
const dashboard = await maybeStartDashboard(args, engine);
|
|
554
581
|
|
|
555
582
|
await engine.start();
|
|
556
583
|
|
|
@@ -563,11 +590,13 @@ async function commandLive(args, overrides = {}) {
|
|
|
563
590
|
|
|
564
591
|
if (!watch) {
|
|
565
592
|
await engine.stop();
|
|
593
|
+
await closeDashboard(dashboard);
|
|
566
594
|
return;
|
|
567
595
|
}
|
|
568
596
|
|
|
569
597
|
const shutdown = async () => {
|
|
570
598
|
await engine.stop();
|
|
599
|
+
await closeDashboard(dashboard);
|
|
571
600
|
process.exit(0);
|
|
572
601
|
};
|
|
573
602
|
process.once("SIGINT", shutdown);
|
package/dist/cjs/data.cjs
CHANGED
|
@@ -119,22 +119,93 @@ function calculatePositionSize({
|
|
|
119
119
|
return quantity >= minQty ? quantity : 0;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// src/metrics/finite.js
|
|
123
|
+
var BIG_NUMBER = 1e9;
|
|
124
|
+
function clampFinite(value, fallback = 0) {
|
|
125
|
+
if (value === Infinity) return BIG_NUMBER;
|
|
126
|
+
if (value === -Infinity) return -BIG_NUMBER;
|
|
127
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/metrics/annualize.js
|
|
132
|
+
var TRADING_DAYS = 252;
|
|
133
|
+
var RTH_HOURS = 6.5;
|
|
134
|
+
var MS_PER_YEAR = 365 * 24 * 60 * 60 * 1e3;
|
|
135
|
+
var INTERVAL_PERIODS = {
|
|
136
|
+
"1m": TRADING_DAYS * RTH_HOURS * 60,
|
|
137
|
+
"2m": TRADING_DAYS * RTH_HOURS * 30,
|
|
138
|
+
"5m": TRADING_DAYS * RTH_HOURS * 12,
|
|
139
|
+
"15m": TRADING_DAYS * RTH_HOURS * 4,
|
|
140
|
+
"30m": TRADING_DAYS * RTH_HOURS * 2,
|
|
141
|
+
"1h": TRADING_DAYS * RTH_HOURS,
|
|
142
|
+
"60m": TRADING_DAYS * RTH_HOURS,
|
|
143
|
+
"1d": TRADING_DAYS,
|
|
144
|
+
"1wk": 52,
|
|
145
|
+
"1mo": 12
|
|
146
|
+
};
|
|
147
|
+
function periodsPerYear(interval, estBarMs) {
|
|
148
|
+
if (interval && INTERVAL_PERIODS[interval]) return INTERVAL_PERIODS[interval];
|
|
149
|
+
if (Number.isFinite(estBarMs) && estBarMs > 0) {
|
|
150
|
+
return Math.round(MS_PER_YEAR / estBarMs);
|
|
151
|
+
}
|
|
152
|
+
return TRADING_DAYS;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/metrics/benchmark.js
|
|
156
|
+
function mean(xs) {
|
|
157
|
+
return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
158
|
+
}
|
|
159
|
+
function benchmarkStats(strategyReturns, benchmarkReturns) {
|
|
160
|
+
const nullStats = {
|
|
161
|
+
alpha: null,
|
|
162
|
+
beta: null,
|
|
163
|
+
correlation: null,
|
|
164
|
+
informationRatio: null,
|
|
165
|
+
trackingError: null
|
|
166
|
+
};
|
|
167
|
+
if (!Array.isArray(strategyReturns) || !Array.isArray(benchmarkReturns) || strategyReturns.length === 0 || strategyReturns.length !== benchmarkReturns.length) {
|
|
168
|
+
return nullStats;
|
|
169
|
+
}
|
|
170
|
+
const meanStrat = mean(strategyReturns);
|
|
171
|
+
const meanBench = mean(benchmarkReturns);
|
|
172
|
+
let covar = 0;
|
|
173
|
+
let varBench = 0;
|
|
174
|
+
let varStrat = 0;
|
|
175
|
+
for (let i = 0; i < strategyReturns.length; i += 1) {
|
|
176
|
+
const ds = strategyReturns[i] - meanStrat;
|
|
177
|
+
const db = benchmarkReturns[i] - meanBench;
|
|
178
|
+
covar += ds * db;
|
|
179
|
+
varBench += db * db;
|
|
180
|
+
varStrat += ds * ds;
|
|
181
|
+
}
|
|
182
|
+
const beta = varBench === 0 ? 0 : covar / varBench;
|
|
183
|
+
const alpha = meanStrat - beta * meanBench;
|
|
184
|
+
const denom = Math.sqrt(varStrat * varBench);
|
|
185
|
+
const correlation = denom === 0 ? 0 : covar / denom;
|
|
186
|
+
const active = strategyReturns.map((r, i) => r - benchmarkReturns[i]);
|
|
187
|
+
const meanActive = mean(active);
|
|
188
|
+
const trackingError = Math.sqrt(mean(active.map((a) => (a - meanActive) ** 2)));
|
|
189
|
+
const informationRatio = trackingError === 0 ? 0 : meanActive / trackingError;
|
|
190
|
+
return { alpha, beta, correlation, informationRatio, trackingError };
|
|
191
|
+
}
|
|
192
|
+
|
|
122
193
|
// src/metrics/buildMetrics.js
|
|
123
194
|
function sum(values) {
|
|
124
195
|
return values.reduce((total, value) => total + value, 0);
|
|
125
196
|
}
|
|
126
|
-
function
|
|
197
|
+
function mean2(values) {
|
|
127
198
|
return values.length ? sum(values) / values.length : 0;
|
|
128
199
|
}
|
|
129
200
|
function stddev(values) {
|
|
130
201
|
if (values.length <= 1) return 0;
|
|
131
|
-
const avg =
|
|
132
|
-
return Math.sqrt(
|
|
202
|
+
const avg = mean2(values);
|
|
203
|
+
return Math.sqrt(mean2(values.map((value) => (value - avg) ** 2)));
|
|
133
204
|
}
|
|
134
205
|
function sortino(values) {
|
|
135
206
|
const losses = values.filter((value) => value < 0);
|
|
136
207
|
const downsideDeviation = stddev(losses.length ? losses : [0]);
|
|
137
|
-
const avg =
|
|
208
|
+
const avg = mean2(values);
|
|
138
209
|
return downsideDeviation === 0 ? avg > 0 ? Infinity : 0 : avg / downsideDeviation;
|
|
139
210
|
}
|
|
140
211
|
function dayKeyUTC(timeMs) {
|
|
@@ -220,14 +291,22 @@ function percentile(values, percentileRank) {
|
|
|
220
291
|
const index = Math.floor((sorted.length - 1) * percentileRank);
|
|
221
292
|
return sorted[index];
|
|
222
293
|
}
|
|
223
|
-
var PROFIT_FACTOR_CAP = 1e6;
|
|
224
294
|
function finiteProfitFactor(grossProfit, grossLoss) {
|
|
225
295
|
if (grossLoss === 0) {
|
|
226
|
-
return grossProfit > 0 ?
|
|
296
|
+
return grossProfit > 0 ? BIG_NUMBER : 0;
|
|
227
297
|
}
|
|
228
298
|
return grossProfit / grossLoss;
|
|
229
299
|
}
|
|
230
|
-
function buildMetrics({
|
|
300
|
+
function buildMetrics({
|
|
301
|
+
closed,
|
|
302
|
+
equityStart,
|
|
303
|
+
equityFinal,
|
|
304
|
+
candles,
|
|
305
|
+
estBarMs,
|
|
306
|
+
eqSeries,
|
|
307
|
+
interval,
|
|
308
|
+
benchmarkReturns
|
|
309
|
+
}) {
|
|
231
310
|
const legs = [...closed].sort((left, right) => left.exit.time - right.exit.time);
|
|
232
311
|
const completedTrades = [];
|
|
233
312
|
const tradeRs = [];
|
|
@@ -297,11 +376,11 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
297
376
|
if (pnl > 0) shortTradeWins += 1;
|
|
298
377
|
}
|
|
299
378
|
}
|
|
300
|
-
const avgR =
|
|
379
|
+
const avgR = mean2(tradeRs);
|
|
301
380
|
const { maxWin, maxLoss } = streaks(labels);
|
|
302
|
-
const expectancy =
|
|
381
|
+
const expectancy = mean2(tradePnls);
|
|
303
382
|
const tradeReturnStd = stddev(tradeReturns);
|
|
304
|
-
const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 :
|
|
383
|
+
const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 : mean2(tradeReturns) / tradeReturnStd;
|
|
305
384
|
const sortinoPerTrade = sortino(tradeReturns);
|
|
306
385
|
const profitFactorPositions = finiteProfitFactor(grossProfitPositions, grossLossPositions);
|
|
307
386
|
const profitFactorLegs = finiteProfitFactor(grossProfitLegs, grossLossLegs);
|
|
@@ -309,11 +388,11 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
309
388
|
const calmar = maxDrawdown === 0 ? returnPct > 0 ? Infinity : 0 : returnPct / maxDrawdown;
|
|
310
389
|
const totalBars = Math.max(1, candles.length);
|
|
311
390
|
const exposurePct = openBars / totalBars;
|
|
312
|
-
const avgHoldMin =
|
|
391
|
+
const avgHoldMin = mean2(holdDurationsMinutes);
|
|
313
392
|
const equitySeries = eqSeries && eqSeries.length ? eqSeries : buildEquitySeriesFromLegs({ legs, equityStart });
|
|
314
393
|
const dailyReturnsSeries = dailyReturns(equitySeries);
|
|
315
394
|
const dailyStd = stddev(dailyReturnsSeries);
|
|
316
|
-
const sharpeDaily = dailyStd === 0 ? dailyReturnsSeries.length ? Infinity : 0 :
|
|
395
|
+
const sharpeDaily = dailyStd === 0 ? dailyReturnsSeries.length ? Infinity : 0 : mean2(dailyReturnsSeries) / dailyStd;
|
|
317
396
|
const sortinoDaily = sortino(dailyReturnsSeries);
|
|
318
397
|
const dailyWinRate = dailyReturnsSeries.length ? dailyReturnsSeries.filter((value) => value > 0).length / dailyReturnsSeries.length : 0;
|
|
319
398
|
const rDistribution = {
|
|
@@ -335,28 +414,36 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
335
414
|
trades: longTradesCount,
|
|
336
415
|
winRate: longTradesCount ? longTradeWins / longTradesCount : 0,
|
|
337
416
|
avgPnL: longTradesCount ? longPnLSum / longTradesCount : 0,
|
|
338
|
-
avgR:
|
|
417
|
+
avgR: mean2(longRs)
|
|
339
418
|
},
|
|
340
419
|
short: {
|
|
341
420
|
trades: shortTradesCount,
|
|
342
421
|
winRate: shortTradesCount ? shortTradeWins / shortTradesCount : 0,
|
|
343
422
|
avgPnL: shortTradesCount ? shortPnLSum / shortTradesCount : 0,
|
|
344
|
-
avgR:
|
|
423
|
+
avgR: mean2(shortRs)
|
|
345
424
|
}
|
|
346
425
|
};
|
|
426
|
+
const periods = periodsPerYear(interval, estBarMs);
|
|
427
|
+
const sqrtPeriods = Math.sqrt(periods);
|
|
428
|
+
const sharpeAnnualized = clampFinite(clampFinite(sharpeDaily) * sqrtPeriods);
|
|
429
|
+
const sortinoAnnualized = clampFinite(clampFinite(sortinoDaily) * sqrtPeriods);
|
|
430
|
+
const benchmark = benchmarkStats(dailyReturnsSeries, benchmarkReturns ?? []);
|
|
347
431
|
return {
|
|
348
432
|
trades: completedTrades.length,
|
|
349
433
|
winRate: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
|
|
350
|
-
profitFactor: profitFactorPositions,
|
|
434
|
+
profitFactor: clampFinite(profitFactorPositions),
|
|
351
435
|
expectancy,
|
|
352
436
|
totalR,
|
|
353
437
|
avgR,
|
|
354
|
-
sharpe: sharpeDaily,
|
|
355
|
-
|
|
356
|
-
|
|
438
|
+
sharpe: clampFinite(sharpeDaily),
|
|
439
|
+
sharpeAnnualized,
|
|
440
|
+
sortinoAnnualized,
|
|
441
|
+
sharpePerTrade: clampFinite(sharpePerTrade),
|
|
442
|
+
sortinoPerTrade: clampFinite(sortinoPerTrade),
|
|
443
|
+
annualizationPeriods: periods,
|
|
357
444
|
maxDrawdown,
|
|
358
445
|
maxDrawdownPct: maxDrawdown,
|
|
359
|
-
calmar,
|
|
446
|
+
calmar: clampFinite(calmar),
|
|
360
447
|
maxConsecWins: maxWin,
|
|
361
448
|
maxConsecLosses: maxLoss,
|
|
362
449
|
avgHold: avgHoldMin,
|
|
@@ -366,12 +453,13 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
366
453
|
returnPct,
|
|
367
454
|
finalEquity: equityFinal,
|
|
368
455
|
startEquity: equityStart,
|
|
369
|
-
profitFactor_pos: profitFactorPositions,
|
|
370
|
-
profitFactor_leg: profitFactorLegs,
|
|
456
|
+
profitFactor_pos: clampFinite(profitFactorPositions),
|
|
457
|
+
profitFactor_leg: clampFinite(profitFactorLegs),
|
|
371
458
|
winRate_pos: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
|
|
372
459
|
winRate_leg: legs.length ? winningLegCount / legs.length : 0,
|
|
373
|
-
sharpeDaily,
|
|
374
|
-
sortinoDaily,
|
|
460
|
+
sharpeDaily: clampFinite(sharpeDaily),
|
|
461
|
+
sortinoDaily: clampFinite(sortinoDaily),
|
|
462
|
+
benchmark,
|
|
375
463
|
sideBreakdown,
|
|
376
464
|
long: sideBreakdown.long,
|
|
377
465
|
short: sideBreakdown.short,
|
|
@@ -380,7 +468,7 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
|
|
|
380
468
|
daily: {
|
|
381
469
|
count: dailyReturnsSeries.length,
|
|
382
470
|
winRate: dailyWinRate,
|
|
383
|
-
avgReturn:
|
|
471
|
+
avgReturn: mean2(dailyReturnsSeries)
|
|
384
472
|
}
|
|
385
473
|
};
|
|
386
474
|
}
|
|
@@ -800,6 +888,30 @@ function dayKeyET(timeMs) {
|
|
|
800
888
|
const pseudoEtTime = anchor.getTime() + hoursET * 60 * 60 * 1e3 + minutesETDay * 60 * 1e3;
|
|
801
889
|
return dayKeyUTC2(pseudoEtTime);
|
|
802
890
|
}
|
|
891
|
+
var MS_PER_YEAR2 = 365 * 24 * 60 * 60 * 1e3;
|
|
892
|
+
function fundingEvents(fromMs, toMs, intervalMs, anchorMs = 0) {
|
|
893
|
+
if (!(intervalMs > 0) || toMs <= fromMs) return 0;
|
|
894
|
+
const firstK = Math.floor((fromMs - anchorMs) / intervalMs) + 1;
|
|
895
|
+
const lastK = Math.floor((toMs - anchorMs) / intervalMs);
|
|
896
|
+
return Math.max(0, lastK - firstK + 1);
|
|
897
|
+
}
|
|
898
|
+
function financingCost({ side, notional, fromMs, toMs, costs }) {
|
|
899
|
+
const model = costs || {};
|
|
900
|
+
const absNotional = Math.abs(notional);
|
|
901
|
+
let cost = 0;
|
|
902
|
+
if (model.carry) {
|
|
903
|
+
const annualBps = side === "long" ? model.carry.longAnnualBps ?? 0 : model.carry.shortAnnualBps ?? 0;
|
|
904
|
+
const years = Math.max(0, toMs - fromMs) / MS_PER_YEAR2;
|
|
905
|
+
cost += absNotional * (annualBps / 1e4) * years;
|
|
906
|
+
}
|
|
907
|
+
const funding = model.funding;
|
|
908
|
+
if (funding && funding.intervalMs > 0 && Number.isFinite(funding.rateBps)) {
|
|
909
|
+
const count = fundingEvents(fromMs, toMs, funding.intervalMs, funding.anchorMs ?? 0);
|
|
910
|
+
const perEvent = absNotional * (funding.rateBps / 1e4);
|
|
911
|
+
cost += (side === "long" ? 1 : -1) * perEvent * count;
|
|
912
|
+
}
|
|
913
|
+
return cost;
|
|
914
|
+
}
|
|
803
915
|
|
|
804
916
|
// src/engine/backtest.js
|
|
805
917
|
function asNumber(value) {
|
|
@@ -931,6 +1043,7 @@ function mergeOptions(options) {
|
|
|
931
1043
|
maxSlipROnFill: options.maxSlipROnFill ?? 0.4,
|
|
932
1044
|
collectEqSeries: options.collectEqSeries ?? true,
|
|
933
1045
|
collectReplay: options.collectReplay ?? true,
|
|
1046
|
+
benchmarkReturns: Array.isArray(options.benchmarkReturns) ? options.benchmarkReturns : null,
|
|
934
1047
|
strict: options.strict ?? false
|
|
935
1048
|
};
|
|
936
1049
|
}
|
|
@@ -1053,7 +1166,14 @@ function backtest(rawOptions) {
|
|
|
1053
1166
|
const entryFill = openPos.entryFill;
|
|
1054
1167
|
const grossPnl = (exitPx - entryFill) * direction * qty;
|
|
1055
1168
|
const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
|
|
1056
|
-
const
|
|
1169
|
+
const financing = financingCost({
|
|
1170
|
+
side: openPos.side,
|
|
1171
|
+
notional: entryFill * qty,
|
|
1172
|
+
fromMs: openPos.openTime,
|
|
1173
|
+
toMs: time,
|
|
1174
|
+
costs
|
|
1175
|
+
});
|
|
1176
|
+
const pnl = grossPnl - entryFeePortion - exitFeeTotal - financing;
|
|
1057
1177
|
currentEquity += pnl;
|
|
1058
1178
|
dayPnl += pnl;
|
|
1059
1179
|
if (wantEqSeries) {
|
|
@@ -1081,6 +1201,7 @@ function backtest(rawOptions) {
|
|
|
1081
1201
|
time,
|
|
1082
1202
|
reason,
|
|
1083
1203
|
pnl,
|
|
1204
|
+
financing,
|
|
1084
1205
|
exitATR: openPos._lastATR ?? void 0
|
|
1085
1206
|
},
|
|
1086
1207
|
mfeR: openPos._mfeR ?? 0,
|
|
@@ -1483,7 +1604,9 @@ function backtest(rawOptions) {
|
|
|
1483
1604
|
equityFinal: currentEquity,
|
|
1484
1605
|
candles,
|
|
1485
1606
|
estBarMs: estimatedBarMs,
|
|
1486
|
-
eqSeries
|
|
1607
|
+
eqSeries,
|
|
1608
|
+
interval: options.interval,
|
|
1609
|
+
benchmarkReturns: options.benchmarkReturns
|
|
1487
1610
|
});
|
|
1488
1611
|
const positions = closed.filter((trade) => trade.exit.reason !== "SCALE");
|
|
1489
1612
|
const lastPrice = asNumber(candles[candles.length - 1]?.close);
|