tradelab 0.5.0 → 1.0.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.
Files changed (54) hide show
  1. package/README.md +89 -41
  2. package/bin/tradelab.js +276 -30
  3. package/dist/cjs/data.cjs +134 -104
  4. package/dist/cjs/index.cjs +378 -177
  5. package/dist/cjs/live.cjs +3350 -0
  6. package/docs/README.md +21 -9
  7. package/docs/api-reference.md +87 -29
  8. package/docs/backtest-engine.md +37 -53
  9. package/docs/data-reporting-cli.md +60 -34
  10. package/docs/examples.md +6 -12
  11. package/docs/live-trading.md +186 -0
  12. package/examples/yahooEmaCross.js +1 -6
  13. package/package.json +18 -3
  14. package/src/data/csv.js +24 -14
  15. package/src/data/index.js +1 -5
  16. package/src/data/yahoo.js +6 -19
  17. package/src/engine/backtest.js +137 -144
  18. package/src/engine/backtestTicks.js +89 -37
  19. package/src/engine/barSystemRunner.js +182 -118
  20. package/src/engine/execution.js +11 -39
  21. package/src/engine/portfolio.js +54 -6
  22. package/src/engine/walkForward.js +37 -14
  23. package/src/index.js +2 -11
  24. package/src/live/broker/alpaca.js +254 -0
  25. package/src/live/broker/binance.js +351 -0
  26. package/src/live/broker/coinbase.js +339 -0
  27. package/src/live/broker/interactiveBrokers.js +123 -0
  28. package/src/live/broker/interface.js +74 -0
  29. package/src/live/clock.js +56 -0
  30. package/src/live/engine/candleAggregator.js +154 -0
  31. package/src/live/engine/liveEngine.js +694 -0
  32. package/src/live/engine/paperEngine.js +453 -0
  33. package/src/live/engine/riskManager.js +185 -0
  34. package/src/live/engine/stateManager.js +112 -0
  35. package/src/live/events.js +48 -0
  36. package/src/live/feed/brokerFeed.js +35 -0
  37. package/src/live/feed/interface.js +28 -0
  38. package/src/live/feed/pollingFeed.js +105 -0
  39. package/src/live/index.js +27 -0
  40. package/src/live/logger.js +82 -0
  41. package/src/live/orchestrator.js +133 -0
  42. package/src/live/storage/interface.js +36 -0
  43. package/src/live/storage/jsonFileStorage.js +112 -0
  44. package/src/metrics/buildMetrics.js +18 -41
  45. package/src/reporting/exportBacktestArtifacts.js +1 -4
  46. package/src/reporting/exportTradesCsv.js +2 -7
  47. package/src/reporting/renderHtmlReport.js +8 -13
  48. package/src/utils/indicators.js +1 -2
  49. package/src/utils/positionSizing.js +16 -2
  50. package/src/utils/time.js +4 -12
  51. package/templates/report.html +23 -9
  52. package/templates/report.js +83 -69
  53. package/types/index.d.ts +21 -3
  54. package/types/live.d.ts +382 -0
@@ -1,17 +1,33 @@
1
1
  import { buildMetrics } from "../metrics/buildMetrics.js";
2
2
  import { calculatePositionSize } from "../utils/positionSizing.js";
3
- import {
4
- applyFill,
5
- dayKeyUTC,
6
- ocoExitCheck,
7
- roundStep,
8
- } from "./execution.js";
3
+ import { applyFill, dayKeyUTC, ocoExitCheck, roundStep } from "./execution.js";
9
4
 
10
5
  function asNumber(value) {
11
6
  const numeric = Number(value);
12
7
  return Number.isFinite(numeric) ? numeric : null;
13
8
  }
14
9
 
10
+ function describeValue(value) {
11
+ if (Array.isArray(value)) return `array(length=${value.length})`;
12
+ if (value === null) return "null";
13
+ return typeof value;
14
+ }
15
+
16
+ function formatIsoTime(time) {
17
+ return Number.isFinite(time) ? new Date(time).toISOString() : "invalid-time";
18
+ }
19
+
20
+ function callSignalWithContext({ signal, context, index, bar, symbol }) {
21
+ try {
22
+ return signal(context);
23
+ } catch (error) {
24
+ const cause = error instanceof Error ? error.message : String(error);
25
+ throw new Error(
26
+ `signal() threw at index=${index}, time=${formatIsoTime(bar?.time)}, symbol=${symbol}: ${cause}`
27
+ );
28
+ }
29
+ }
30
+
15
31
  function normalizeSide(value) {
16
32
  if (value === "long" || value === "buy") return "long";
17
33
  if (value === "short" || value === "sell") return "short";
@@ -23,10 +39,7 @@ function normalizeTick(tick) {
23
39
  const bid = asNumber(tick?.bid);
24
40
  const ask = asNumber(tick?.ask);
25
41
  const last = asNumber(tick?.price ?? tick?.last ?? tick?.close);
26
- const mid =
27
- bid !== null && ask !== null
28
- ? (bid + ask) / 2
29
- : last ?? bid ?? ask;
42
+ const mid = bid !== null && ask !== null ? (bid + ask) / 2 : (last ?? bid ?? ask);
30
43
  if (!Number.isFinite(time) || !Number.isFinite(mid)) return null;
31
44
 
32
45
  const prices = [asNumber(tick?.low), asNumber(tick?.high), bid, ask, last, mid].filter(
@@ -53,8 +66,7 @@ function normalizeSignal(signal, bar, fallbackR) {
53
66
 
54
67
  const hasExplicitEntry =
55
68
  signal.entry !== undefined || signal.limit !== undefined || signal.price !== undefined;
56
- const entry =
57
- asNumber(signal.entry ?? signal.limit ?? signal.price) ?? asNumber(bar?.close);
69
+ const entry = asNumber(signal.entry ?? signal.limit ?? signal.price) ?? asNumber(bar?.close);
58
70
  const stop = asNumber(signal.stop ?? signal.stopLoss ?? signal.sl);
59
71
  if (entry === null || stop === null) return null;
60
72
 
@@ -66,8 +78,7 @@ function normalizeSignal(signal, bar, fallbackR) {
66
78
  const targetR = rrHint ?? fallbackR;
67
79
 
68
80
  if (takeProfit === null && Number.isFinite(targetR) && targetR > 0) {
69
- takeProfit =
70
- side === "long" ? entry + risk * targetR : entry - risk * targetR;
81
+ takeProfit = side === "long" ? entry + risk * targetR : entry - risk * targetR;
71
82
  }
72
83
  if (takeProfit === null) return null;
73
84
 
@@ -88,19 +99,50 @@ function equityPoint(time, equity) {
88
99
  return { time, timestamp: time, equity };
89
100
  }
90
101
 
102
+ function xmur3(seed) {
103
+ let hash = 1779033703 ^ seed.length;
104
+ for (let index = 0; index < seed.length; index += 1) {
105
+ hash = Math.imul(hash ^ seed.charCodeAt(index), 3432918353);
106
+ hash = (hash << 13) | (hash >>> 19);
107
+ }
108
+ return () => {
109
+ hash = Math.imul(hash ^ (hash >>> 16), 2246822507);
110
+ hash = Math.imul(hash ^ (hash >>> 13), 3266489909);
111
+ return (hash ^= hash >>> 16) >>> 0;
112
+ };
113
+ }
114
+
115
+ function mulberry32(seed) {
116
+ let state = seed >>> 0;
117
+ return () => {
118
+ state = (state + 0x6d2b79f5) >>> 0;
119
+ let value = Math.imul(state ^ (state >>> 15), state | 1);
120
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
121
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
122
+ };
123
+ }
124
+
125
+ function seededUnitInterval(seedParts) {
126
+ const seed = seedParts.map((part) => String(part)).join("|");
127
+ const seedFn = xmur3(seed);
128
+ return mulberry32(seedFn())();
129
+ }
130
+
91
131
  function deterministicFill(probability, seedParts) {
92
132
  if (probability >= 1) return true;
93
133
  if (probability <= 0) return false;
94
- let hash = 2166136261;
95
- const seed = seedParts.join("|");
96
- for (let index = 0; index < seed.length; index += 1) {
97
- hash ^= seed.charCodeAt(index);
98
- hash = Math.imul(hash, 16777619);
99
- }
100
- const normalized = (hash >>> 0) / 0xffffffff;
134
+ const normalized = seededUnitInterval(seedParts);
101
135
  return normalized <= probability;
102
136
  }
103
137
 
138
+ /**
139
+ * Run a tick-level backtest with event-driven fills.
140
+ *
141
+ * Tick data is normalized into `{ time, open, high, low, close }` shape, then
142
+ * the strategy `signal()` callback is evaluated sequentially. Returned payload
143
+ * matches the candle engine: `trades`, `positions`, `metrics`, `eqSeries`, and
144
+ * chart-friendly `replay` frames/events.
145
+ */
104
146
  export function backtestTicks({
105
147
  ticks = [],
106
148
  symbol = "UNKNOWN",
@@ -124,15 +166,19 @@ export function backtestTicks({
124
166
  oco = {},
125
167
  } = {}) {
126
168
  if (!Array.isArray(ticks) || ticks.length === 0) {
127
- throw new Error("backtestTicks() requires a non-empty ticks array");
169
+ throw new Error(
170
+ `backtestTicks() requires a non-empty ticks array, got ${describeValue(ticks)}`
171
+ );
128
172
  }
129
173
  if (typeof signal !== "function") {
130
- throw new Error("backtestTicks() requires a signal function");
174
+ throw new Error(`backtestTicks() requires a signal function, got ${describeValue(signal)}`);
131
175
  }
132
176
 
133
177
  const normalizedTicks = ticks.map(normalizeTick).filter(Boolean);
134
178
  if (!normalizedTicks.length) {
135
- throw new Error("backtestTicks() could not normalize any ticks");
179
+ throw new Error(
180
+ `backtestTicks() could not normalize any ticks from ${ticks.length} input rows`
181
+ );
136
182
  }
137
183
 
138
184
  const ocoOptions = {
@@ -293,9 +339,7 @@ export function backtestTicks({
293
339
  pending = null;
294
340
  } else {
295
341
  const touched =
296
- pending.side === "long"
297
- ? tick.low <= pending.entry
298
- : tick.high >= pending.entry;
342
+ pending.side === "long" ? tick.low <= pending.entry : tick.high >= pending.entry;
299
343
  if (
300
344
  touched &&
301
345
  deterministicFill(queueFillProbability, [
@@ -362,13 +406,19 @@ export function backtestTicks({
362
406
 
363
407
  if (!open && !pending && !dailyLossHit && !dailyTradeCapHit) {
364
408
  const nextSignal = normalizeSignal(
365
- signal({
366
- candles: history,
409
+ callSignalWithContext({
410
+ signal,
411
+ context: {
412
+ candles: history,
413
+ index,
414
+ bar: tick,
415
+ equity: markedEquity(tick),
416
+ openPosition: open,
417
+ pendingOrder: pending,
418
+ },
367
419
  index,
368
420
  bar: tick,
369
- equity: markedEquity(tick),
370
- openPosition: open,
371
- pendingOrder: pending,
421
+ symbol,
372
422
  }),
373
423
  tick,
374
424
  finalTP_R
@@ -384,8 +434,8 @@ export function backtestTicks({
384
434
  riskFrac: Number.isFinite(nextSignal.riskFraction)
385
435
  ? nextSignal.riskFraction
386
436
  : Number.isFinite(nextSignal.riskPct)
387
- ? nextSignal.riskPct / 100
388
- : riskPct / 100,
437
+ ? nextSignal.riskPct / 100
438
+ : riskPct / 100,
389
439
  orderType: nextSignal.orderType,
390
440
  createdAtIndex: index,
391
441
  };
@@ -407,9 +457,10 @@ export function backtestTicks({
407
457
  equityStart: equity,
408
458
  equityFinal: currentEquity,
409
459
  candles: normalizedTicks,
410
- estBarMs: normalizedTicks.length > 1
411
- ? Math.max(1, normalizedTicks[1].time - normalizedTicks[0].time)
412
- : 1,
460
+ estBarMs:
461
+ normalizedTicks.length > 1
462
+ ? Math.max(1, normalizedTicks[1].time - normalizedTicks[0].time)
463
+ : 1,
413
464
  eqSeries,
414
465
  });
415
466
 
@@ -419,6 +470,7 @@ export function backtestTicks({
419
470
  range,
420
471
  trades,
421
472
  positions,
473
+ openPositions: [],
422
474
  metrics,
423
475
  eqSeries,
424
476
  replay: {