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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/reporting/summarize.js
|
|
2
|
+
|
|
3
|
+
function pct(value, digits = 1) {
|
|
4
|
+
return Number.isFinite(value) ? `${value.toFixed(digits)}%` : "n/a";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render a metrics object into one plain-English paragraph. Optionally append an
|
|
9
|
+
* overfitting caveat from a research verdict. No em-dashes (house style).
|
|
10
|
+
*
|
|
11
|
+
* @param {object} metrics - Fields: trades, winRate, maxDrawdownPct, totalReturnPct, sharpe.
|
|
12
|
+
* @param {{ verdict?: { overfit?: boolean, note?: string } }} [options]
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function summarize(metrics = {}, { verdict } = {}) {
|
|
16
|
+
const trades = Number.isFinite(metrics.trades) ? metrics.trades : 0;
|
|
17
|
+
const win = Number.isFinite(metrics.winRate) ? Math.round(metrics.winRate * 100) : null;
|
|
18
|
+
const dd = Number.isFinite(metrics.maxDrawdownPct)
|
|
19
|
+
? metrics.maxDrawdownPct
|
|
20
|
+
: Number.isFinite(metrics.maxDrawdown)
|
|
21
|
+
? metrics.maxDrawdown * 100
|
|
22
|
+
: null;
|
|
23
|
+
const ret = Number.isFinite(metrics.totalReturnPct) ? metrics.totalReturnPct : null;
|
|
24
|
+
const sharpe = Number.isFinite(metrics.sharpe) ? metrics.sharpe : null;
|
|
25
|
+
|
|
26
|
+
if (trades === 0) return "Ran with 0 trades, so there is nothing to evaluate yet.";
|
|
27
|
+
|
|
28
|
+
const parts = [`Made ${trades} trades`];
|
|
29
|
+
if (win !== null) parts.push(`won ${win}% of them`);
|
|
30
|
+
if (ret !== null) parts.push(`for a ${pct(ret)} total return`);
|
|
31
|
+
if (dd !== null) parts.push(`with a worst drawdown of ${pct(dd)}`);
|
|
32
|
+
|
|
33
|
+
let text = parts.join(", ");
|
|
34
|
+
if (sharpe !== null) {
|
|
35
|
+
text += ` (Sharpe ${sharpe.toFixed(2)})`;
|
|
36
|
+
}
|
|
37
|
+
text += ".";
|
|
38
|
+
|
|
39
|
+
if (verdict && verdict.overfit) {
|
|
40
|
+
text += ` Caution: robustness checks flag this result as likely overfit${verdict.note ? ` (${verdict.note})` : ""}.`;
|
|
41
|
+
}
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
@@ -35,6 +35,10 @@ export function monteCarlo({
|
|
|
35
35
|
if (!Array.isArray(tradePnls) || tradePnls.length === 0) {
|
|
36
36
|
throw new Error("monteCarlo() requires a non-empty tradePnls array");
|
|
37
37
|
}
|
|
38
|
+
const runCount = Math.floor(Number(iterations));
|
|
39
|
+
if (!Number.isFinite(runCount) || runCount < 1) {
|
|
40
|
+
throw new Error("monteCarlo() requires positive iterations");
|
|
41
|
+
}
|
|
38
42
|
const rng = makeRng(seed);
|
|
39
43
|
const n = tradePnls.length;
|
|
40
44
|
const block = Math.max(1, Math.floor(blockSize));
|
|
@@ -43,7 +47,7 @@ export function monteCarlo({
|
|
|
43
47
|
const drawdowns = [];
|
|
44
48
|
const pathSamples = Array.from({ length: n + 1 }, () => []);
|
|
45
49
|
|
|
46
|
-
for (let it = 0; it <
|
|
50
|
+
for (let it = 0; it < runCount; it += 1) {
|
|
47
51
|
const path = [equityStart];
|
|
48
52
|
let equity = equityStart;
|
|
49
53
|
let filled = 0;
|
|
@@ -78,7 +82,7 @@ export function monteCarlo({
|
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
return {
|
|
81
|
-
iterations,
|
|
85
|
+
iterations: runCount,
|
|
82
86
|
blockSize: block,
|
|
83
87
|
finalEquity: bands(sortedFinals),
|
|
84
88
|
maxDrawdown: bands(sortedDd),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/research/store.js
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DIR = ".tradelab/research";
|
|
6
|
+
|
|
7
|
+
function fileFor(dir, id) {
|
|
8
|
+
if (!/^[\w.-]+$/.test(String(id))) throw new Error(`invalid research id: ${id}`);
|
|
9
|
+
return join(dir, `${id}.json`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function load(dir, id) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(fileFor(dir, id), "utf8");
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function save(dir, record) {
|
|
22
|
+
await mkdir(dir, { recursive: true });
|
|
23
|
+
await writeFile(fileFor(dir, record.id), JSON.stringify(record, null, 2));
|
|
24
|
+
return record;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bestSharpe(entries) {
|
|
28
|
+
let best = null;
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
const s = e.metrics?.sharpe;
|
|
31
|
+
if (Number.isFinite(s) && (best === null || s > best.sharpe)) best = { sharpe: s, params: e.params };
|
|
32
|
+
}
|
|
33
|
+
return best;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createResearchStore({ dir = DEFAULT_DIR } = {}) {
|
|
37
|
+
return {
|
|
38
|
+
async open(id, goal = "") {
|
|
39
|
+
const existing = await load(dir, id);
|
|
40
|
+
if (existing) return existing;
|
|
41
|
+
const record = { id, goal, createdAt: new Date().toISOString(), closedAt: null, entries: [] };
|
|
42
|
+
return save(dir, record);
|
|
43
|
+
},
|
|
44
|
+
async log(id, { hypothesis = "", params = {}, metrics = {}, verdict = null } = {}) {
|
|
45
|
+
const record = (await load(dir, id)) || { id, goal: "", createdAt: new Date().toISOString(), closedAt: null, entries: [] };
|
|
46
|
+
const entry = { at: new Date().toISOString(), hypothesis, params, metrics, verdict };
|
|
47
|
+
record.entries.push(entry);
|
|
48
|
+
await save(dir, record);
|
|
49
|
+
return entry;
|
|
50
|
+
},
|
|
51
|
+
async recall(id, limit = 10) {
|
|
52
|
+
const record = (await load(dir, id)) || { goal: "", entries: [] };
|
|
53
|
+
const entries = record.entries.slice(-limit);
|
|
54
|
+
const best = bestSharpe(record.entries);
|
|
55
|
+
const flagged = record.entries.filter((e) => e.verdict?.overfit).length;
|
|
56
|
+
const summary = record.entries.length
|
|
57
|
+
? `Best Sharpe so far: ${best ? best.sharpe.toFixed(2) : "n/a"}${best ? ` via ${JSON.stringify(best.params)}` : ""}. ${flagged} of ${record.entries.length} flagged overfit.`
|
|
58
|
+
: "No entries logged yet.";
|
|
59
|
+
return { goal: record.goal, entries, summary };
|
|
60
|
+
},
|
|
61
|
+
async close(id) {
|
|
62
|
+
const record = (await load(dir, id)) || { id, goal: "", createdAt: new Date().toISOString(), entries: [] };
|
|
63
|
+
record.closedAt = new Date().toISOString();
|
|
64
|
+
return save(dir, record);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -724,6 +724,36 @@ export function registerStrategy(name: string, def: StrategyDefinition): void;
|
|
|
724
724
|
export function listStrategies(): StrategySummary[];
|
|
725
725
|
export function getStrategy(name: string): StrategyDefinition["factory"];
|
|
726
726
|
|
|
727
|
+
export interface ResearchEntry {
|
|
728
|
+
at: string;
|
|
729
|
+
hypothesis?: string;
|
|
730
|
+
params?: Record<string, unknown>;
|
|
731
|
+
metrics?: Record<string, unknown>;
|
|
732
|
+
verdict?: Record<string, unknown> | null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export interface ResearchRecord {
|
|
736
|
+
id: string;
|
|
737
|
+
goal: string;
|
|
738
|
+
createdAt: string;
|
|
739
|
+
closedAt: string | null;
|
|
740
|
+
entries: ResearchEntry[];
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export interface ResearchStore {
|
|
744
|
+
open(id: string, goal?: string): Promise<ResearchRecord>;
|
|
745
|
+
log(id: string, options?: {
|
|
746
|
+
hypothesis?: string;
|
|
747
|
+
params?: Record<string, unknown>;
|
|
748
|
+
metrics?: Record<string, unknown>;
|
|
749
|
+
verdict?: Record<string, unknown> | null;
|
|
750
|
+
}): Promise<ResearchEntry>;
|
|
751
|
+
recall(id: string, limit?: number): Promise<{ goal: string; entries: ResearchEntry[]; summary: string }>;
|
|
752
|
+
close(id: string): Promise<ResearchRecord>;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function createResearchStore(options?: { dir?: string }): ResearchStore;
|
|
756
|
+
|
|
727
757
|
export namespace research {
|
|
728
758
|
function monteCarlo(options: {
|
|
729
759
|
tradePnls: number[];
|
package/types/live.d.ts
CHANGED
|
@@ -341,7 +341,8 @@ export interface DashboardServer {
|
|
|
341
341
|
export function createDashboardServer(options: {
|
|
342
342
|
source: {
|
|
343
343
|
eventBus: EventBus;
|
|
344
|
-
getStatus?: () =>
|
|
344
|
+
getStatus?: () => unknown;
|
|
345
|
+
refresh?: () => Promise<unknown>;
|
|
345
346
|
};
|
|
346
347
|
port?: number;
|
|
347
348
|
maxBuffer?: number;
|
|
@@ -414,6 +415,7 @@ export interface TradingSessionOptions {
|
|
|
414
415
|
minQty?: number;
|
|
415
416
|
maxLeverage?: number;
|
|
416
417
|
eventBus?: EventBus;
|
|
418
|
+
confirmLive?: boolean;
|
|
417
419
|
}
|
|
418
420
|
|
|
419
421
|
export interface SessionPlaceOrderOptions {
|