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.
- package/README.md +89 -41
- package/bin/tradelab.js +276 -30
- package/dist/cjs/data.cjs +134 -104
- package/dist/cjs/index.cjs +378 -177
- package/dist/cjs/live.cjs +3350 -0
- package/docs/README.md +21 -9
- package/docs/api-reference.md +87 -29
- package/docs/backtest-engine.md +37 -53
- package/docs/data-reporting-cli.md +60 -34
- package/docs/examples.md +6 -12
- package/docs/live-trading.md +186 -0
- package/examples/yahooEmaCross.js +1 -6
- package/package.json +18 -3
- package/src/data/csv.js +24 -14
- package/src/data/index.js +1 -5
- package/src/data/yahoo.js +6 -19
- package/src/engine/backtest.js +137 -144
- package/src/engine/backtestTicks.js +89 -37
- package/src/engine/barSystemRunner.js +182 -118
- package/src/engine/execution.js +11 -39
- package/src/engine/portfolio.js +54 -6
- package/src/engine/walkForward.js +37 -14
- package/src/index.js +2 -11
- package/src/live/broker/alpaca.js +254 -0
- package/src/live/broker/binance.js +351 -0
- package/src/live/broker/coinbase.js +339 -0
- package/src/live/broker/interactiveBrokers.js +123 -0
- package/src/live/broker/interface.js +74 -0
- package/src/live/clock.js +56 -0
- package/src/live/engine/candleAggregator.js +154 -0
- package/src/live/engine/liveEngine.js +694 -0
- package/src/live/engine/paperEngine.js +453 -0
- package/src/live/engine/riskManager.js +185 -0
- package/src/live/engine/stateManager.js +112 -0
- package/src/live/events.js +48 -0
- package/src/live/feed/brokerFeed.js +35 -0
- package/src/live/feed/interface.js +28 -0
- package/src/live/feed/pollingFeed.js +105 -0
- package/src/live/index.js +27 -0
- package/src/live/logger.js +82 -0
- package/src/live/orchestrator.js +133 -0
- package/src/live/storage/interface.js +36 -0
- package/src/live/storage/jsonFileStorage.js +112 -0
- package/src/metrics/buildMetrics.js +18 -41
- package/src/reporting/exportBacktestArtifacts.js +1 -4
- package/src/reporting/exportTradesCsv.js +2 -7
- package/src/reporting/renderHtmlReport.js +8 -13
- package/src/utils/indicators.js +1 -2
- package/src/utils/positionSizing.js +16 -2
- package/src/utils/time.js +4 -12
- package/templates/report.html +23 -9
- package/templates/report.js +83 -69
- package/types/index.d.ts +21 -3
- 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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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:
|
|
411
|
-
|
|
412
|
-
|
|
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: {
|