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/src/mcp/tools.js
CHANGED
|
@@ -5,6 +5,10 @@ import { getStrategy, listStrategies } from "../strategies/index.js";
|
|
|
5
5
|
import { grid } from "../engine/grid.js";
|
|
6
6
|
import { monteCarlo, deflatedSharpe } from "../research/index.js";
|
|
7
7
|
import { liveTools } from "./liveTools.js";
|
|
8
|
+
import { researchTools as researchSessionTools } from "./researchSession.js";
|
|
9
|
+
import { createResearchStore } from "../research/store.js";
|
|
10
|
+
|
|
11
|
+
const researchStore = createResearchStore();
|
|
8
12
|
|
|
9
13
|
function summarizeMetrics(metrics) {
|
|
10
14
|
const {
|
|
@@ -93,10 +97,31 @@ export const researchTools = {
|
|
|
93
97
|
collectReplay: false,
|
|
94
98
|
...(args.backtestOptions || {}),
|
|
95
99
|
});
|
|
100
|
+
const metrics = summarizeMetrics(result.metrics);
|
|
101
|
+
if (args.researchId) {
|
|
102
|
+
let verdict = null;
|
|
103
|
+
try {
|
|
104
|
+
const psr = deflatedSharpe({
|
|
105
|
+
sharpe: result.metrics.sharpe,
|
|
106
|
+
sampleSize: result.metrics.trades,
|
|
107
|
+
numTrials: args.numTrials ?? 1,
|
|
108
|
+
});
|
|
109
|
+
verdict = {
|
|
110
|
+
deflatedSharpe: psr,
|
|
111
|
+
overfit: Number.isFinite(psr) ? psr < 0.9 : false,
|
|
112
|
+
note: Number.isFinite(psr) ? `PSR ${(psr * 100).toFixed(1)}%` : "insufficient data",
|
|
113
|
+
};
|
|
114
|
+
} catch {
|
|
115
|
+
verdict = { deflatedSharpe: null, overfit: false, note: "verdict unavailable" };
|
|
116
|
+
}
|
|
117
|
+
await researchStore.log(args.researchId, {
|
|
118
|
+
hypothesis: args.strategy, params: args.params || {}, metrics, verdict,
|
|
119
|
+
}).catch(() => {});
|
|
120
|
+
}
|
|
96
121
|
return {
|
|
97
122
|
symbol: result.symbol,
|
|
98
123
|
interval: result.interval,
|
|
99
|
-
metrics
|
|
124
|
+
metrics,
|
|
100
125
|
tradesPreview: result.positions.slice(0, 10).map((p) => ({
|
|
101
126
|
side: p.side,
|
|
102
127
|
entry: p.entryFill ?? p.entry,
|
|
@@ -168,7 +193,7 @@ export const researchPlusTools = {
|
|
|
168
193
|
metrics,
|
|
169
194
|
monteCarlo: null,
|
|
170
195
|
deflatedSharpe: null,
|
|
171
|
-
note: `Only ${tradePnls.length} trade(s)
|
|
196
|
+
note: `Only ${tradePnls.length} trade(s), need at least 2 for statistical analysis.`,
|
|
172
197
|
};
|
|
173
198
|
}
|
|
174
199
|
const mc = monteCarlo({
|
|
@@ -262,4 +287,4 @@ export const researchPlusTools = {
|
|
|
262
287
|
},
|
|
263
288
|
};
|
|
264
289
|
|
|
265
|
-
export const mcpTools = { ...researchTools, ...researchPlusTools, ...liveTools };
|
|
290
|
+
export const mcpTools = { ...researchTools, ...researchPlusTools, ...liveTools, ...researchSessionTools() };
|
|
@@ -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
|
+
}
|
|
@@ -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[];
|