tradelab 1.2.0 → 1.3.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 +53 -0
- package/README.md +31 -6
- package/bin/tradelab.js +36 -0
- package/dist/cjs/index.cjs +98 -3
- package/dist/cjs/live.cjs +286 -58
- package/docs/live-trading.md +131 -1
- package/docs/mcp.md +90 -21
- 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/engine/portfolio.js +2 -1
- package/src/index.js +2 -0
- package/src/live/engine/paperEngine.js +16 -11
- 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 +200 -49
- package/src/mcp/liveTools.js +42 -15
- package/src/mcp/researchSession.js +24 -0
- package/src/mcp/schemas.js +5 -1
- package/src/mcp/tools.js +27 -2
- package/src/reporting/summarize.js +43 -0
- package/src/research/monteCarlo.js +6 -2
- package/src/research/store.js +67 -0
- package/types/index.d.ts +30 -0
- package/types/live.d.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,59 @@ 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.0] - 2026-06-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Multi-symbol portfolio sessions**
|
|
13
|
+
- `SessionManager.create({ symbols: ["BTC", "ETH"] })` creates a single session that tracks multiple instruments against a shared broker.
|
|
14
|
+
- `pushBar(bar, symbol)` and `placeOrder({ symbol })` target a specific instrument; bracket OCO is managed per-symbol.
|
|
15
|
+
- `closePosition(symbol)` cancels the resting bracket for that symbol before submitting the flattening order.
|
|
16
|
+
- Per-symbol read accessors: `lastPriceFor(sym)` and `candleBufferFor(sym)`.
|
|
17
|
+
- `getStatus()` now includes a `symbols` array alongside the primary `symbol`. Single-symbol usage (`symbol: "AAPL"`) is unchanged.
|
|
18
|
+
|
|
19
|
+
- **Portfolio exposure caps**
|
|
20
|
+
- `maxGrossExposurePct` and `maxNetExposurePct` options on `TradingSession` and `SessionManager.create()`. Both default to `0` (disabled).
|
|
21
|
+
- Enforced in `placeOrder()` before any broker call; throws `risk rejected: <reason>` when the cap is breached.
|
|
22
|
+
- The `RiskManager.checkExposure()` method is the shared gate used by both `canOpenPosition()` and `placeOrder()`.
|
|
23
|
+
|
|
24
|
+
- **Trade attribution on order events**
|
|
25
|
+
- `order:submitted` and `order:filled` events now include a `sizing` block: `{ entry, stop, target, rr, riskFraction, riskAmount, qty, notional }`.
|
|
26
|
+
- Pass `rationale` to `placeOrder()` to attach a free-text note that propagates to all fill events for that order.
|
|
27
|
+
- Bracket legs carry `parentEntryId` (the entry's client order id) and a `leg` field (`"stop"` or `"target"`).
|
|
28
|
+
|
|
29
|
+
- **Agent research loop**
|
|
30
|
+
- `createResearchStore({ dir? })` from `tradelab` returns `{ open, log, recall, close }` for file-backed hypothesis tracking.
|
|
31
|
+
- MCP tools `research_open`, `research_log`, `research_recall`, `research_close` expose the store over stdio.
|
|
32
|
+
- `run_backtest` accepts `researchId` and auto-logs the backtest result plus a Deflated Sharpe verdict (`{ deflatedSharpe, overfit, note }`) without a separate `research_log` call.
|
|
33
|
+
|
|
34
|
+
- **Multi-symbol support in MCP live tools**
|
|
35
|
+
- `create_session` accepts a `symbols` array.
|
|
36
|
+
- `feed_price`, `place_order`, `close_position`, and `attach_strategy` accept an optional `symbol` argument to route operations to a specific instrument.
|
|
37
|
+
|
|
38
|
+
- **`summarize(metrics)`**
|
|
39
|
+
- Exported from `tradelab`. Renders a `buildMetrics` output object as one plain-English paragraph. Accepts an optional `verdict` object to append an overfit caution sentence.
|
|
40
|
+
|
|
41
|
+
- **`tradelab run <preset>` CLI subcommand**
|
|
42
|
+
- Runs a named built-in strategy on Yahoo or CSV data and prints the `summarize()` paragraph to stdout.
|
|
43
|
+
- Accepts `--params '{"fast":5}'` to override strategy defaults.
|
|
44
|
+
|
|
45
|
+
- **`attachNotifier(session, opts)`**
|
|
46
|
+
- Exported from `tradelab/live`. Subscribes a callback and/or webhook URL to a session's event bus.
|
|
47
|
+
- Options: `events` (which events to forward), `onEvent` (async callback), `webhookUrl` (HTTP POST endpoint), `drawdownPct` (fires `drawdown:breach` when equity falls this far from peak).
|
|
48
|
+
- Returns an unsubscribe function.
|
|
49
|
+
|
|
50
|
+
## [1.2.1] - 2026-06-27
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `backtestPortfolio()` now reports aggregate `metrics.finalEquity` correctly when `collectEqSeries: false`.
|
|
55
|
+
- `PaperEngine` now rejects market orders that have no price reference instead of filling them at zero.
|
|
56
|
+
- `TradingSession` now clears staged brackets when an async entry is rejected or canceled, and staged brackets are matched by order id/client order id instead of a loose string check.
|
|
57
|
+
- MCP `attach_strategy` now evaluates built-in strategies with the normal backtest signal context and auto-places returned order intents from `feed_price`.
|
|
58
|
+
- `research.monteCarlo()` now rejects non-positive iteration counts instead of returning empty bands and `NaN` probability fields.
|
|
59
|
+
- Public live types now include `TradingSessionOptions.confirmLive` and optional dashboard `source.refresh()`.
|
|
60
|
+
|
|
8
61
|
## [1.2.0] - 2026-06-26
|
|
9
62
|
|
|
10
63
|
### Added
|
package/README.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://i.imgur.com/HGvvQbq.png" width="420" alt="tradelab logo" />
|
|
3
|
+
|
|
4
|
+
<p><strong>A Node.js backtesting toolkit for serious trading strategy research.</strong></p>
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/tradelab)
|
|
7
|
+
[](https://github.com/ishsharm0/tradelab)
|
|
8
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/LICENSE)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/types/index.d.ts)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
2
15
|
|
|
3
16
|
A Node.js toolkit for testing, validating, and operating trading strategies.
|
|
4
17
|
|
|
@@ -74,11 +87,16 @@ Start with `result.metrics` for the summary and `result.positions` for completed
|
|
|
74
87
|
| Run a parallel parameter sweep | `optimize({ signalModulePath, parameterSets })` |
|
|
75
88
|
| Use indicators | `import { rsi, macd, vwap } from "tradelab/ta"` |
|
|
76
89
|
| Check overfitting risk | `research.monteCarlo`, `research.deflatedSharpe` |
|
|
77
|
-
| Run in paper or live mode | `LiveEngine`, `LiveOrchestrator`, `tradelab paper`
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
90
|
+
| Run in paper or live mode | `LiveEngine`, `LiveOrchestrator`, `tradelab paper` |
|
|
91
|
+
| Trade multiple symbols in one session | `SessionManager.create({ symbols: ["BTC","ETH"] })` with per-symbol `pushBar` and `placeOrder` |
|
|
92
|
+
| Watch a live run locally | `createDashboardServer({ source })` with equity curve, KPI strip, controls |
|
|
93
|
+
| Get notified on fills or risk halts | `attachNotifier(session, { onEvent, webhookUrl })` from `tradelab/live` |
|
|
94
|
+
| Let MCP clients run research tools | `tradelab-mcp` with `run_backtest`, `walk_forward`, `analyze_robustness`, `optimize_strategy`, `compare_strategies`, `candle_stats` |
|
|
95
|
+
| 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)) |
|
|
96
|
+
| Track strategy research across runs | `tradelab-mcp` with `research_open`, `research_log`, `research_recall`, `research_close` (see [docs/mcp.md](docs/mcp.md)) |
|
|
97
|
+
| Summarize metrics in plain English | `summarize(metrics)` returns one plain-English paragraph |
|
|
98
|
+
| Run a built-in preset from the CLI | `tradelab run ema-cross --source yahoo --symbol SPY --period 1y` |
|
|
99
|
+
| Export reports and machine data | `exportBacktestArtifacts`, `exportMetricsJSON` |
|
|
82
100
|
|
|
83
101
|
## The Signal Contract
|
|
84
102
|
|
|
@@ -256,8 +274,12 @@ Add `--dashboard --dashboardPort 4317` to open a local Server-Sent Events dashbo
|
|
|
256
274
|
|
|
257
275
|
**Research tools:** `list_strategies`, `fetch_candles`, `run_backtest`, `walk_forward`, `analyze_robustness`, `optimize_strategy`, `compare_strategies`, `candle_stats`
|
|
258
276
|
|
|
277
|
+
**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.
|
|
278
|
+
|
|
259
279
|
**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`
|
|
260
280
|
|
|
281
|
+
`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.
|
|
282
|
+
|
|
261
283
|
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.
|
|
262
284
|
|
|
263
285
|
Use it from any MCP client that can launch a stdio server:
|
|
@@ -279,9 +301,12 @@ Use it from any MCP client that can launch a stdio server:
|
|
|
279
301
|
tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
280
302
|
tradelab portfolio --csvPaths ./spy.csv,./qqq.csv --symbols SPY,QQQ
|
|
281
303
|
tradelab walk-forward --source yahoo --symbol QQQ --interval 1d --period 2y
|
|
304
|
+
tradelab run ema-cross --source yahoo --symbol SPY --period 1y
|
|
282
305
|
tradelab status --dir ./output/live-state
|
|
283
306
|
```
|
|
284
307
|
|
|
308
|
+
`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.
|
|
309
|
+
|
|
285
310
|
## Documentation
|
|
286
311
|
|
|
287
312
|
- [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,
|
|
@@ -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
|
|
@@ -3329,10 +3331,11 @@ function backtestPortfolio({
|
|
|
3329
3331
|
const allCandles = systems.flatMap((system) => system.candles || []);
|
|
3330
3332
|
const orderedCandles = [...allCandles].sort((left, right) => left.time - right.time);
|
|
3331
3333
|
const metricsInterval = interval ?? systems[0]?.interval;
|
|
3334
|
+
const finalState = portfolioState(runners, equity);
|
|
3332
3335
|
const metrics = buildMetrics({
|
|
3333
3336
|
closed: trades,
|
|
3334
3337
|
equityStart: equity,
|
|
3335
|
-
equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity :
|
|
3338
|
+
equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : finalState.markedEquity,
|
|
3336
3339
|
candles: orderedCandles,
|
|
3337
3340
|
estBarMs: estimateBarMs(orderedCandles),
|
|
3338
3341
|
eqSeries,
|
|
@@ -3967,13 +3970,17 @@ function monteCarlo({
|
|
|
3967
3970
|
if (!Array.isArray(tradePnls) || tradePnls.length === 0) {
|
|
3968
3971
|
throw new Error("monteCarlo() requires a non-empty tradePnls array");
|
|
3969
3972
|
}
|
|
3973
|
+
const runCount = Math.floor(Number(iterations));
|
|
3974
|
+
if (!Number.isFinite(runCount) || runCount < 1) {
|
|
3975
|
+
throw new Error("monteCarlo() requires positive iterations");
|
|
3976
|
+
}
|
|
3970
3977
|
const rng = makeRng(seed);
|
|
3971
3978
|
const n = tradePnls.length;
|
|
3972
3979
|
const block = Math.max(1, Math.floor(blockSize));
|
|
3973
3980
|
const finals = [];
|
|
3974
3981
|
const drawdowns = [];
|
|
3975
3982
|
const pathSamples = Array.from({ length: n + 1 }, () => []);
|
|
3976
|
-
for (let it = 0; it <
|
|
3983
|
+
for (let it = 0; it < runCount; it += 1) {
|
|
3977
3984
|
const path7 = [equityStart];
|
|
3978
3985
|
let equity = equityStart;
|
|
3979
3986
|
let filled = 0;
|
|
@@ -4005,7 +4012,7 @@ function monteCarlo({
|
|
|
4005
4012
|
p95: percentile2(sorted, 0.95)
|
|
4006
4013
|
});
|
|
4007
4014
|
return {
|
|
4008
|
-
iterations,
|
|
4015
|
+
iterations: runCount,
|
|
4009
4016
|
blockSize: block,
|
|
4010
4017
|
finalEquity: bands(sortedFinals),
|
|
4011
4018
|
maxDrawdown: bands(sortedDd),
|
|
@@ -4514,6 +4521,66 @@ async function backtestHistorical({ backtestOptions = {}, data, ...legacy } = {}
|
|
|
4514
4521
|
});
|
|
4515
4522
|
}
|
|
4516
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
|
+
|
|
4517
4584
|
// src/reporting/renderHtmlReport.js
|
|
4518
4585
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
4519
4586
|
var import_path3 = __toESM(require("path"), 1);
|
|
@@ -4923,6 +4990,32 @@ function exportBacktestArtifacts({
|
|
|
4923
4990
|
}
|
|
4924
4991
|
return outputs;
|
|
4925
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
|
+
}
|
|
4926
5019
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4927
5020
|
0 && (module.exports = {
|
|
4928
5021
|
BIG_NUMBER,
|
|
@@ -4940,6 +5033,7 @@ function exportBacktestArtifacts({
|
|
|
4940
5033
|
calculatePositionSize,
|
|
4941
5034
|
candleStats,
|
|
4942
5035
|
clampFinite,
|
|
5036
|
+
createResearchStore,
|
|
4943
5037
|
detectFVG,
|
|
4944
5038
|
ema,
|
|
4945
5039
|
exportBacktestArtifacts,
|
|
@@ -4970,6 +5064,7 @@ function exportBacktestArtifacts({
|
|
|
4970
5064
|
research,
|
|
4971
5065
|
saveCandlesToCache,
|
|
4972
5066
|
structureState,
|
|
5067
|
+
summarize,
|
|
4973
5068
|
swingHigh,
|
|
4974
5069
|
swingLow,
|
|
4975
5070
|
walkForwardOptimize
|