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.
@@ -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 < iterations; it += 1) {
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?: () => Record<string, unknown>;
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 {