tradelab 1.2.1 → 1.3.1
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 +48 -0
- package/README.md +24 -11
- package/bin/tradelab.js +37 -1
- package/dist/cjs/index.cjs +90 -0
- package/dist/cjs/live.cjs +243 -52
- package/docs/README.md +6 -4
- package/docs/api-reference.md +15 -1
- package/docs/data-reporting-cli.md +9 -0
- package/docs/live-trading.md +130 -0
- package/docs/mcp.md +92 -23
- package/docs/research.md +15 -0
- package/examples/agentResearchLoop.js +188 -0
- package/examples/multiSymbolPortfolio.js +122 -0
- package/package.json +1 -1
- package/src/cli/runPreset.js +42 -0
- package/src/index.js +2 -0
- package/src/live/engine/riskManager.js +38 -0
- package/src/live/index.js +1 -0
- package/src/live/notify.js +42 -0
- package/src/live/session.js +170 -56
- package/src/mcp/liveTools.js +28 -24
- package/src/mcp/researchSession.js +24 -0
- package/src/mcp/schemas.js +5 -1
- package/src/mcp/tools.js +28 -3
- package/src/reporting/summarize.js +43 -0
- package/src/research/store.js +67 -0
- package/types/index.d.ts +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.1] - 2026-06-28
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Documentation refresh: the README now leads with the agent-native MCP story, and the reference docs cover `summarize`, `createResearchStore` and the agent research loop, `attachNotifier`, multi-symbol sessions, exposure caps, and `tradelab run`.
|
|
13
|
+
|
|
14
|
+
## [1.3.0] - 2026-06-27
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Multi-symbol portfolio sessions**
|
|
19
|
+
- `SessionManager.create({ symbols: ["BTC", "ETH"] })` creates a single session that tracks multiple instruments against a shared broker.
|
|
20
|
+
- `pushBar(bar, symbol)` and `placeOrder({ symbol })` target a specific instrument; bracket OCO is managed per-symbol.
|
|
21
|
+
- `closePosition(symbol)` cancels the resting bracket for that symbol before submitting the flattening order.
|
|
22
|
+
- Per-symbol read accessors: `lastPriceFor(sym)` and `candleBufferFor(sym)`.
|
|
23
|
+
- `getStatus()` now includes a `symbols` array alongside the primary `symbol`. Single-symbol usage (`symbol: "AAPL"`) is unchanged.
|
|
24
|
+
|
|
25
|
+
- **Portfolio exposure caps**
|
|
26
|
+
- `maxGrossExposurePct` and `maxNetExposurePct` options on `TradingSession` and `SessionManager.create()`. Both default to `0` (disabled).
|
|
27
|
+
- Enforced in `placeOrder()` before any broker call; throws `risk rejected: <reason>` when the cap is breached.
|
|
28
|
+
- The `RiskManager.checkExposure()` method is the shared gate used by both `canOpenPosition()` and `placeOrder()`.
|
|
29
|
+
|
|
30
|
+
- **Trade attribution on order events**
|
|
31
|
+
- `order:submitted` and `order:filled` events now include a `sizing` block: `{ entry, stop, target, rr, riskFraction, riskAmount, qty, notional }`.
|
|
32
|
+
- Pass `rationale` to `placeOrder()` to attach a free-text note that propagates to all fill events for that order.
|
|
33
|
+
- Bracket legs carry `parentEntryId` (the entry's client order id) and a `leg` field (`"stop"` or `"target"`).
|
|
34
|
+
|
|
35
|
+
- **Agent research loop**
|
|
36
|
+
- `createResearchStore({ dir? })` from `tradelab` returns `{ open, log, recall, close }` for file-backed hypothesis tracking.
|
|
37
|
+
- MCP tools `research_open`, `research_log`, `research_recall`, `research_close` expose the store over stdio.
|
|
38
|
+
- `run_backtest` accepts `researchId` and auto-logs the backtest result plus a Deflated Sharpe verdict (`{ deflatedSharpe, overfit, note }`) without a separate `research_log` call.
|
|
39
|
+
|
|
40
|
+
- **Multi-symbol support in MCP live tools**
|
|
41
|
+
- `create_session` accepts a `symbols` array.
|
|
42
|
+
- `feed_price`, `place_order`, `close_position`, and `attach_strategy` accept an optional `symbol` argument to route operations to a specific instrument.
|
|
43
|
+
|
|
44
|
+
- **`summarize(metrics)`**
|
|
45
|
+
- Exported from `tradelab`. Renders a `buildMetrics` output object as one plain-English paragraph. Accepts an optional `verdict` object to append an overfit caution sentence.
|
|
46
|
+
|
|
47
|
+
- **`tradelab run <preset>` CLI subcommand**
|
|
48
|
+
- Runs a named built-in strategy on Yahoo or CSV data and prints the `summarize()` paragraph to stdout.
|
|
49
|
+
- Accepts `--params '{"fast":5}'` to override strategy defaults.
|
|
50
|
+
|
|
51
|
+
- **`attachNotifier(session, opts)`**
|
|
52
|
+
- Exported from `tradelab/live`. Subscribes a callback and/or webhook URL to a session's event bus.
|
|
53
|
+
- Options: `events` (which events to forward), `onEvent` (async callback), `webhookUrl` (HTTP POST endpoint), `drawdownPct` (fires `drawdown:breach` when equity falls this far from peak).
|
|
54
|
+
- Returns an unsubscribe function.
|
|
55
|
+
|
|
8
56
|
## [1.2.1] - 2026-06-27
|
|
9
57
|
|
|
10
58
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<img src="https://i.imgur.com/HGvvQbq.png" width="420" alt="tradelab logo" />
|
|
3
3
|
|
|
4
|
-
<p><strong>
|
|
4
|
+
<p><strong>An agent-native Node.js trading engine: research, backtest, and trade live through one signal contract.</strong></p>
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/tradelab)
|
|
7
7
|
[](https://github.com/ishsharm0/tradelab)
|
|
@@ -13,17 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
A Node.js toolkit for testing, validating, and operating trading strategies.
|
|
16
|
+
A Node.js toolkit for testing, validating, and operating trading strategies, built so humans and AI agents work from the same primitives.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
One `signal()` contract runs across research and execution:
|
|
19
19
|
|
|
20
20
|
- run candle or tick backtests
|
|
21
21
|
- model slippage, commissions, borrow, carry, and funding
|
|
22
22
|
- validate parameters with walk-forward tests and research statistics
|
|
23
23
|
- combine multiple systems into a shared-capital portfolio
|
|
24
|
-
- move the same strategy into paper or live execution
|
|
24
|
+
- move the same strategy into paper or live execution, single or multi-symbol
|
|
25
25
|
- export reports, metrics, and trade ledgers
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
**Agent-native.** The `tradelab-mcp` server exposes 25 tools over stdio, so an AI agent can run the whole loop itself: pull data, run and score backtests, track hypotheses across runs with built-in overfitting guards, then open a paper or live session and place risk-sized bracket orders behind a kill-switch. Agents get the same depth a quant does, not a thin read-only wrapper. See [docs/mcp.md](docs/mcp.md).
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
30
|
npm install tradelab
|
|
@@ -87,11 +88,16 @@ Start with `result.metrics` for the summary and `result.positions` for completed
|
|
|
87
88
|
| Run a parallel parameter sweep | `optimize({ signalModulePath, parameterSets })` |
|
|
88
89
|
| Use indicators | `import { rsi, macd, vwap } from "tradelab/ta"` |
|
|
89
90
|
| Check overfitting risk | `research.monteCarlo`, `research.deflatedSharpe` |
|
|
90
|
-
| Run in paper or live mode | `LiveEngine`, `LiveOrchestrator`, `tradelab paper`
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
91
|
+
| Run in paper or live mode | `LiveEngine`, `LiveOrchestrator`, `tradelab paper` |
|
|
92
|
+
| Trade multiple symbols in one session | `SessionManager.create({ symbols: ["BTC","ETH"] })` with per-symbol `pushBar` and `placeOrder` |
|
|
93
|
+
| Watch a live run locally | `createDashboardServer({ source })` with equity curve, KPI strip, controls |
|
|
94
|
+
| Get notified on fills or risk halts | `attachNotifier(session, { onEvent, webhookUrl })` from `tradelab/live` |
|
|
95
|
+
| Let MCP clients run research tools | `tradelab-mcp` with `run_backtest`, `walk_forward`, `analyze_robustness`, `optimize_strategy`, `compare_strategies`, `candle_stats` |
|
|
96
|
+
| Let MCP agents trade (paper/live) | `tradelab-mcp` with `create_session`, `feed_price`, `place_order`, bracket orders, `halt_all` kill-switch (see [docs/mcp.md](docs/mcp.md)) |
|
|
97
|
+
| Track strategy research across runs | `tradelab-mcp` with `research_open`, `research_log`, `research_recall`, `research_close` (see [docs/mcp.md](docs/mcp.md)) |
|
|
98
|
+
| Summarize metrics in plain English | `summarize(metrics)` returns one plain-English paragraph |
|
|
99
|
+
| Run a built-in preset from the CLI | `tradelab run ema-cross --source yahoo --symbol SPY --period 1y` |
|
|
100
|
+
| Export reports and machine data | `exportBacktestArtifacts`, `exportMetricsJSON` |
|
|
95
101
|
|
|
96
102
|
## The Signal Contract
|
|
97
103
|
|
|
@@ -265,12 +271,16 @@ Add `--dashboard --dashboardPort 4317` to open a local Server-Sent Events dashbo
|
|
|
265
271
|
|
|
266
272
|
## MCP Server
|
|
267
273
|
|
|
268
|
-
`tradelab-mcp` exposes
|
|
274
|
+
`tradelab-mcp` exposes 25 tools over stdio to any MCP-capable agent (Claude Desktop, Cursor, and similar). They cover the full loop an agent needs to work a strategy end to end: research it, validate it, track the search, then trade it. See [docs/mcp.md](docs/mcp.md) for the full tool reference and agent trading guide.
|
|
269
275
|
|
|
270
276
|
**Research tools:** `list_strategies`, `fetch_candles`, `run_backtest`, `walk_forward`, `analyze_robustness`, `optimize_strategy`, `compare_strategies`, `candle_stats`
|
|
271
277
|
|
|
278
|
+
**Research loop tools:** `research_open`, `research_log`, `research_recall`, `research_close` for persistent file-backed hypothesis tracking. `run_backtest` auto-logs when `researchId` is passed.
|
|
279
|
+
|
|
272
280
|
**Agent trading tools (paper by default; live gated):** `create_session`, `list_sessions`, `session_status`, `feed_price`, `place_order`, `close_position`, `flatten`, `cancel_order`, `account`, `positions`, `recent_events`, `attach_strategy`, `halt_all`
|
|
273
281
|
|
|
282
|
+
`create_session` accepts a `symbols` array for multi-symbol portfolio sessions. Pass `symbol` to `feed_price` and `place_order` to direct bars and orders to a specific instrument.
|
|
283
|
+
|
|
274
284
|
Paper trading needs no credentials. Live trading requires `TRADELAB_ALLOW_LIVE=true` and `confirmLive: true` plus a credentialed broker. `halt_all` is an emergency kill-switch that flattens all positions and stops every session.
|
|
275
285
|
|
|
276
286
|
Use it from any MCP client that can launch a stdio server:
|
|
@@ -292,9 +302,12 @@ Use it from any MCP client that can launch a stdio server:
|
|
|
292
302
|
tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
293
303
|
tradelab portfolio --csvPaths ./spy.csv,./qqq.csv --symbols SPY,QQQ
|
|
294
304
|
tradelab walk-forward --source yahoo --symbol QQQ --interval 1d --period 2y
|
|
305
|
+
tradelab run ema-cross --source yahoo --symbol SPY --period 1y
|
|
295
306
|
tradelab status --dir ./output/live-state
|
|
296
307
|
```
|
|
297
308
|
|
|
309
|
+
`tradelab run <preset>` runs a named built-in strategy on Yahoo or CSV data and prints a plain-English summary. Pass `--params '{"fast":5,"slow":20}'` to override defaults.
|
|
310
|
+
|
|
298
311
|
## Documentation
|
|
299
312
|
|
|
300
313
|
- [Docs home](docs/README.md)
|
package/bin/tradelab.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
saveCandlesToCache,
|
|
15
15
|
walkForwardOptimize,
|
|
16
16
|
} from "../src/index.js";
|
|
17
|
+
import { runPreset } from "../src/cli/runPreset.js";
|
|
17
18
|
import {
|
|
18
19
|
AlpacaBroker,
|
|
19
20
|
BinanceBroker,
|
|
@@ -270,7 +271,7 @@ async function loadWalkForwardStrategy(strategyArg, args) {
|
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
function printHelp() {
|
|
273
|
-
console.log(`tradelab
|
|
274
|
+
console.log(`tradelab: agent-native trading engine for Node.js
|
|
274
275
|
|
|
275
276
|
Usage: tradelab <command> [options]
|
|
276
277
|
|
|
@@ -278,6 +279,7 @@ Commands:
|
|
|
278
279
|
backtest Run a one-off backtest from Yahoo or CSV data
|
|
279
280
|
portfolio Run multiple CSV datasets as an equal-weight portfolio
|
|
280
281
|
walk-forward Run rolling or anchored train/test optimization
|
|
282
|
+
run Run a named built-in preset and print a plain-English summary
|
|
281
283
|
live Run live trading engine (streaming or polling)
|
|
282
284
|
paper Run live engine in paper broker mode
|
|
283
285
|
status Read persisted live state
|
|
@@ -288,6 +290,8 @@ Examples:
|
|
|
288
290
|
tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
289
291
|
tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
290
292
|
tradelab walk-forward --source csv --csvPath ./data/spy.csv --trainBars 120 --testBars 40
|
|
293
|
+
tradelab run ema-cross --source yahoo --symbol SPY --period 1y
|
|
294
|
+
tradelab run ema-cross --source csv --csvPath ./data/spy.csv --params '{"fast":5,"slow":15}'
|
|
291
295
|
tradelab live --strategy ./mySignal.js --symbol AAPL --interval 5m --broker alpaca --paper
|
|
292
296
|
|
|
293
297
|
Options:
|
|
@@ -473,6 +477,37 @@ async function commandPrefetch(args) {
|
|
|
473
477
|
console.log(`Saved ${candles.length} candles to ${outputPath}`);
|
|
474
478
|
}
|
|
475
479
|
|
|
480
|
+
async function commandRun(args) {
|
|
481
|
+
const preset = args._[1];
|
|
482
|
+
if (!preset) {
|
|
483
|
+
throw new Error("run requires a preset name (e.g. tradelab run ema-cross)");
|
|
484
|
+
}
|
|
485
|
+
const symbol = args.symbol || "PRESET";
|
|
486
|
+
const interval = args.interval || "1d";
|
|
487
|
+
const params = parseJsonValue(args.params, {});
|
|
488
|
+
|
|
489
|
+
const source = args.source || (args.csvPath ? "csv" : null);
|
|
490
|
+
let candles;
|
|
491
|
+
if (source === "csv" || args.csvPath) {
|
|
492
|
+
candles = loadCandlesFromCSV(args.csvPath);
|
|
493
|
+
} else if (source === "yahoo" || args.symbol) {
|
|
494
|
+
candles = await getHistoricalCandles({
|
|
495
|
+
source: "yahoo",
|
|
496
|
+
symbol: args.symbol,
|
|
497
|
+
interval,
|
|
498
|
+
period: args.period || "1y",
|
|
499
|
+
cache: args.cache !== "false",
|
|
500
|
+
});
|
|
501
|
+
} else {
|
|
502
|
+
throw new Error(
|
|
503
|
+
"run requires candle data: provide --source yahoo --symbol TICKER or --source csv --csvPath PATH"
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const out = runPreset({ preset, candles, params, symbol, interval });
|
|
508
|
+
console.log(out.summary);
|
|
509
|
+
}
|
|
510
|
+
|
|
476
511
|
async function commandImportCsv(args) {
|
|
477
512
|
const csvPath = args.csvPath || args._[1];
|
|
478
513
|
if (!csvPath) {
|
|
@@ -668,6 +703,7 @@ const commands = {
|
|
|
668
703
|
backtest: commandBacktest,
|
|
669
704
|
portfolio: commandPortfolio,
|
|
670
705
|
"walk-forward": commandWalkForward,
|
|
706
|
+
run: commandRun,
|
|
671
707
|
live: commandLive,
|
|
672
708
|
paper: commandPaper,
|
|
673
709
|
status: commandStatus,
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -45,6 +45,7 @@ __export(index_exports, {
|
|
|
45
45
|
calculatePositionSize: () => calculatePositionSize,
|
|
46
46
|
candleStats: () => candleStats,
|
|
47
47
|
clampFinite: () => clampFinite,
|
|
48
|
+
createResearchStore: () => createResearchStore,
|
|
48
49
|
detectFVG: () => detectFVG,
|
|
49
50
|
ema: () => ema,
|
|
50
51
|
exportBacktestArtifacts: () => exportBacktestArtifacts,
|
|
@@ -75,6 +76,7 @@ __export(index_exports, {
|
|
|
75
76
|
research: () => research_exports,
|
|
76
77
|
saveCandlesToCache: () => saveCandlesToCache,
|
|
77
78
|
structureState: () => structureState,
|
|
79
|
+
summarize: () => summarize,
|
|
78
80
|
swingHigh: () => swingHigh,
|
|
79
81
|
swingLow: () => swingLow,
|
|
80
82
|
walkForwardOptimize: () => walkForwardOptimize
|
|
@@ -4519,6 +4521,66 @@ async function backtestHistorical({ backtestOptions = {}, data, ...legacy } = {}
|
|
|
4519
4521
|
});
|
|
4520
4522
|
}
|
|
4521
4523
|
|
|
4524
|
+
// src/research/store.js
|
|
4525
|
+
var import_promises = require("node:fs/promises");
|
|
4526
|
+
var import_node_path2 = require("node:path");
|
|
4527
|
+
var DEFAULT_DIR = ".tradelab/research";
|
|
4528
|
+
function fileFor(dir, id) {
|
|
4529
|
+
if (!/^[\w.-]+$/.test(String(id))) throw new Error(`invalid research id: ${id}`);
|
|
4530
|
+
return (0, import_node_path2.join)(dir, `${id}.json`);
|
|
4531
|
+
}
|
|
4532
|
+
async function load(dir, id) {
|
|
4533
|
+
try {
|
|
4534
|
+
const raw = await (0, import_promises.readFile)(fileFor(dir, id), "utf8");
|
|
4535
|
+
return JSON.parse(raw);
|
|
4536
|
+
} catch {
|
|
4537
|
+
return null;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
async function save(dir, record) {
|
|
4541
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
4542
|
+
await (0, import_promises.writeFile)(fileFor(dir, record.id), JSON.stringify(record, null, 2));
|
|
4543
|
+
return record;
|
|
4544
|
+
}
|
|
4545
|
+
function bestSharpe(entries) {
|
|
4546
|
+
let best = null;
|
|
4547
|
+
for (const e of entries) {
|
|
4548
|
+
const s = e.metrics?.sharpe;
|
|
4549
|
+
if (Number.isFinite(s) && (best === null || s > best.sharpe)) best = { sharpe: s, params: e.params };
|
|
4550
|
+
}
|
|
4551
|
+
return best;
|
|
4552
|
+
}
|
|
4553
|
+
function createResearchStore({ dir = DEFAULT_DIR } = {}) {
|
|
4554
|
+
return {
|
|
4555
|
+
async open(id, goal = "") {
|
|
4556
|
+
const existing = await load(dir, id);
|
|
4557
|
+
if (existing) return existing;
|
|
4558
|
+
const record = { id, goal, createdAt: (/* @__PURE__ */ new Date()).toISOString(), closedAt: null, entries: [] };
|
|
4559
|
+
return save(dir, record);
|
|
4560
|
+
},
|
|
4561
|
+
async log(id, { hypothesis = "", params = {}, metrics = {}, verdict = null } = {}) {
|
|
4562
|
+
const record = await load(dir, id) || { id, goal: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), closedAt: null, entries: [] };
|
|
4563
|
+
const entry = { at: (/* @__PURE__ */ new Date()).toISOString(), hypothesis, params, metrics, verdict };
|
|
4564
|
+
record.entries.push(entry);
|
|
4565
|
+
await save(dir, record);
|
|
4566
|
+
return entry;
|
|
4567
|
+
},
|
|
4568
|
+
async recall(id, limit = 10) {
|
|
4569
|
+
const record = await load(dir, id) || { goal: "", entries: [] };
|
|
4570
|
+
const entries = record.entries.slice(-limit);
|
|
4571
|
+
const best = bestSharpe(record.entries);
|
|
4572
|
+
const flagged = record.entries.filter((e) => e.verdict?.overfit).length;
|
|
4573
|
+
const summary = record.entries.length ? `Best Sharpe so far: ${best ? best.sharpe.toFixed(2) : "n/a"}${best ? ` via ${JSON.stringify(best.params)}` : ""}. ${flagged} of ${record.entries.length} flagged overfit.` : "No entries logged yet.";
|
|
4574
|
+
return { goal: record.goal, entries, summary };
|
|
4575
|
+
},
|
|
4576
|
+
async close(id) {
|
|
4577
|
+
const record = await load(dir, id) || { id, goal: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), entries: [] };
|
|
4578
|
+
record.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4579
|
+
return save(dir, record);
|
|
4580
|
+
}
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4522
4584
|
// src/reporting/renderHtmlReport.js
|
|
4523
4585
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
4524
4586
|
var import_path3 = __toESM(require("path"), 1);
|
|
@@ -4928,6 +4990,32 @@ function exportBacktestArtifacts({
|
|
|
4928
4990
|
}
|
|
4929
4991
|
return outputs;
|
|
4930
4992
|
}
|
|
4993
|
+
|
|
4994
|
+
// src/reporting/summarize.js
|
|
4995
|
+
function pct2(value, digits = 1) {
|
|
4996
|
+
return Number.isFinite(value) ? `${value.toFixed(digits)}%` : "n/a";
|
|
4997
|
+
}
|
|
4998
|
+
function summarize(metrics = {}, { verdict } = {}) {
|
|
4999
|
+
const trades = Number.isFinite(metrics.trades) ? metrics.trades : 0;
|
|
5000
|
+
const win = Number.isFinite(metrics.winRate) ? Math.round(metrics.winRate * 100) : null;
|
|
5001
|
+
const dd = Number.isFinite(metrics.maxDrawdownPct) ? metrics.maxDrawdownPct : Number.isFinite(metrics.maxDrawdown) ? metrics.maxDrawdown * 100 : null;
|
|
5002
|
+
const ret = Number.isFinite(metrics.totalReturnPct) ? metrics.totalReturnPct : null;
|
|
5003
|
+
const sharpe = Number.isFinite(metrics.sharpe) ? metrics.sharpe : null;
|
|
5004
|
+
if (trades === 0) return "Ran with 0 trades, so there is nothing to evaluate yet.";
|
|
5005
|
+
const parts = [`Made ${trades} trades`];
|
|
5006
|
+
if (win !== null) parts.push(`won ${win}% of them`);
|
|
5007
|
+
if (ret !== null) parts.push(`for a ${pct2(ret)} total return`);
|
|
5008
|
+
if (dd !== null) parts.push(`with a worst drawdown of ${pct2(dd)}`);
|
|
5009
|
+
let text = parts.join(", ");
|
|
5010
|
+
if (sharpe !== null) {
|
|
5011
|
+
text += ` (Sharpe ${sharpe.toFixed(2)})`;
|
|
5012
|
+
}
|
|
5013
|
+
text += ".";
|
|
5014
|
+
if (verdict && verdict.overfit) {
|
|
5015
|
+
text += ` Caution: robustness checks flag this result as likely overfit${verdict.note ? ` (${verdict.note})` : ""}.`;
|
|
5016
|
+
}
|
|
5017
|
+
return text;
|
|
5018
|
+
}
|
|
4931
5019
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4932
5020
|
0 && (module.exports = {
|
|
4933
5021
|
BIG_NUMBER,
|
|
@@ -4945,6 +5033,7 @@ function exportBacktestArtifacts({
|
|
|
4945
5033
|
calculatePositionSize,
|
|
4946
5034
|
candleStats,
|
|
4947
5035
|
clampFinite,
|
|
5036
|
+
createResearchStore,
|
|
4948
5037
|
detectFVG,
|
|
4949
5038
|
ema,
|
|
4950
5039
|
exportBacktestArtifacts,
|
|
@@ -4975,6 +5064,7 @@ function exportBacktestArtifacts({
|
|
|
4975
5064
|
research,
|
|
4976
5065
|
saveCandlesToCache,
|
|
4977
5066
|
structureState,
|
|
5067
|
+
summarize,
|
|
4978
5068
|
swingHigh,
|
|
4979
5069
|
swingLow,
|
|
4980
5070
|
walkForwardOptimize
|