tradelab 0.4.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 +121 -52
  2. package/bin/tradelab.js +340 -49
  3. package/dist/cjs/data.cjs +210 -155
  4. package/dist/cjs/index.cjs +1782 -274
  5. package/dist/cjs/live.cjs +3350 -0
  6. package/docs/README.md +26 -9
  7. package/docs/api-reference.md +89 -26
  8. package/docs/backtest-engine.md +74 -60
  9. package/docs/data-reporting-cli.md +66 -36
  10. package/docs/examples.md +275 -0
  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 +481 -0
  19. package/src/engine/barSystemRunner.js +1027 -0
  20. package/src/engine/execution.js +11 -39
  21. package/src/engine/portfolio.js +237 -66
  22. package/src/engine/walkForward.js +132 -13
  23. package/src/index.js +3 -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 +103 -100
  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 +98 -4
  54. package/types/live.d.ts +382 -0
@@ -0,0 +1,123 @@
1
+ import { BrokerAdapter } from "./interface.js";
2
+
3
+ /**
4
+ * Interactive Brokers adapter with optional dynamic dependency.
5
+ */
6
+ export class InteractiveBrokersBroker extends BrokerAdapter {
7
+ constructor() {
8
+ super();
9
+ this.connected = false;
10
+ this.config = {};
11
+ this.ibModule = null;
12
+ this.orderCounter = 1;
13
+ this.orders = new Map();
14
+ this.positions = new Map();
15
+ }
16
+
17
+ async connect(config = {}) {
18
+ this.config = { ...config };
19
+ try {
20
+ this.ibModule = await import("@stoqey/ib");
21
+ } catch {
22
+ throw new Error(
23
+ 'InteractiveBrokersBroker requires optional peer dependency "@stoqey/ib". Install it to enable IB support.'
24
+ );
25
+ }
26
+ this.connected = true;
27
+ }
28
+
29
+ async disconnect() {
30
+ this.connected = false;
31
+ }
32
+
33
+ isConnected() {
34
+ return this.connected;
35
+ }
36
+
37
+ supportsPaperNative() {
38
+ return true;
39
+ }
40
+
41
+ async getServerTime() {
42
+ return Date.now();
43
+ }
44
+
45
+ async getAccount() {
46
+ return {
47
+ equity: 0,
48
+ buyingPower: 0,
49
+ cash: 0,
50
+ currency: "USD",
51
+ marginUsed: 0,
52
+ };
53
+ }
54
+
55
+ async getPositions() {
56
+ return [...this.positions.values()];
57
+ }
58
+
59
+ async submitOrder(order) {
60
+ const receipt = {
61
+ orderId: String(this.orderCounter++),
62
+ clientOrderId: order.clientOrderId,
63
+ status: "new",
64
+ filledQty: 0,
65
+ symbol: order.symbol,
66
+ side: order.side,
67
+ type: order.type,
68
+ qty: Number(order.qty || 0),
69
+ avgFillPrice: undefined,
70
+ filledAt: undefined,
71
+ };
72
+ this.orders.set(receipt.orderId, receipt);
73
+ this.emit("order:submitted", receipt);
74
+ return receipt;
75
+ }
76
+
77
+ async cancelOrder(orderId) {
78
+ const order = this.orders.get(String(orderId));
79
+ if (!order) return;
80
+ order.status = "canceled";
81
+ this.emit("order:canceled", { ...order });
82
+ }
83
+
84
+ async modifyOrder(orderId, changes = {}) {
85
+ const order = this.orders.get(String(orderId));
86
+ if (!order) throw new Error(`IB order "${orderId}" not found`);
87
+ if (changes.qty !== undefined) order.qty = Number(changes.qty || order.qty);
88
+ if (changes.limitPrice !== undefined) order.limitPrice = Number(changes.limitPrice);
89
+ if (changes.stopPrice !== undefined) order.stopPrice = Number(changes.stopPrice);
90
+ this.emit("order:modified", { ...order });
91
+ return { ...order };
92
+ }
93
+
94
+ async getOpenOrders() {
95
+ return [...this.orders.values()].filter((order) => order.status === "new");
96
+ }
97
+
98
+ async getOrderStatus(orderId) {
99
+ const order = this.orders.get(String(orderId));
100
+ if (!order) throw new Error(`IB order "${orderId}" not found`);
101
+ return { ...order };
102
+ }
103
+
104
+ async subscribeQuotes(_symbol, _handler) {
105
+ return { unsubscribe: () => {} };
106
+ }
107
+
108
+ async subscribeTrades(_symbol, _handler) {
109
+ return { unsubscribe: () => {} };
110
+ }
111
+
112
+ async subscribeBars(_symbol, _interval, _handler) {
113
+ return { unsubscribe: () => {} };
114
+ }
115
+
116
+ async getHistoricalBars(_symbol, _interval, _limit = 200) {
117
+ return [];
118
+ }
119
+ }
120
+
121
+ export function createInteractiveBrokersBroker(options) {
122
+ return new InteractiveBrokersBroker(options);
123
+ }
@@ -0,0 +1,74 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ function notImplemented(method) {
4
+ throw new Error(`BrokerAdapter.${method}() not implemented`);
5
+ }
6
+
7
+ /**
8
+ * Base class for broker adapters.
9
+ */
10
+ export class BrokerAdapter extends EventEmitter {
11
+ async connect(_config = {}) {
12
+ notImplemented("connect");
13
+ }
14
+
15
+ async disconnect() {
16
+ notImplemented("disconnect");
17
+ }
18
+
19
+ isConnected() {
20
+ notImplemented("isConnected");
21
+ }
22
+
23
+ async getAccount() {
24
+ notImplemented("getAccount");
25
+ }
26
+
27
+ async getPositions() {
28
+ notImplemented("getPositions");
29
+ }
30
+
31
+ async getServerTime() {
32
+ return Date.now();
33
+ }
34
+
35
+ async submitOrder(_order) {
36
+ notImplemented("submitOrder");
37
+ }
38
+
39
+ async cancelOrder(_orderId) {
40
+ notImplemented("cancelOrder");
41
+ }
42
+
43
+ async modifyOrder(_orderId, _changes) {
44
+ notImplemented("modifyOrder");
45
+ }
46
+
47
+ async getOpenOrders() {
48
+ notImplemented("getOpenOrders");
49
+ }
50
+
51
+ async getOrderStatus(_orderId) {
52
+ notImplemented("getOrderStatus");
53
+ }
54
+
55
+ async subscribeQuotes(_symbol, _handler) {
56
+ notImplemented("subscribeQuotes");
57
+ }
58
+
59
+ async subscribeTrades(_symbol, _handler) {
60
+ notImplemented("subscribeTrades");
61
+ }
62
+
63
+ async subscribeBars(_symbol, _interval, _handler) {
64
+ notImplemented("subscribeBars");
65
+ }
66
+
67
+ async getHistoricalBars(_symbol, _interval, _limit = 200) {
68
+ notImplemented("getHistoricalBars");
69
+ }
70
+
71
+ supportsPaperNative() {
72
+ return false;
73
+ }
74
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Broker-synchronized clock used by live engines.
3
+ */
4
+ export class BrokerClock {
5
+ constructor({ warnThresholdMs = 2000 } = {}) {
6
+ this.warnThresholdMs = Math.max(0, warnThresholdMs);
7
+ this.offsetMs = 0;
8
+ this.syncedAt = null;
9
+ }
10
+
11
+ now() {
12
+ return Date.now() + this.offsetMs;
13
+ }
14
+
15
+ getOffsetMs() {
16
+ return this.offsetMs;
17
+ }
18
+
19
+ async syncWithBroker(broker) {
20
+ if (!broker || typeof broker.getServerTime !== "function") {
21
+ this.offsetMs = 0;
22
+ this.syncedAt = Date.now();
23
+ return {
24
+ serverTime: null,
25
+ localTime: this.syncedAt,
26
+ offsetMs: this.offsetMs,
27
+ warning: null,
28
+ };
29
+ }
30
+
31
+ let serverTime = null;
32
+ try {
33
+ serverTime = Number(await broker.getServerTime());
34
+ } catch {
35
+ serverTime = null;
36
+ }
37
+
38
+ const localTime = Date.now();
39
+ this.offsetMs = Number.isFinite(serverTime) ? serverTime - localTime : 0;
40
+ this.syncedAt = localTime;
41
+ const warning =
42
+ Math.abs(this.offsetMs) > this.warnThresholdMs
43
+ ? `clock offset ${this.offsetMs}ms exceeds threshold ${this.warnThresholdMs}ms`
44
+ : null;
45
+ return {
46
+ serverTime,
47
+ localTime,
48
+ offsetMs: this.offsetMs,
49
+ warning,
50
+ };
51
+ }
52
+ }
53
+
54
+ export function createClock(options) {
55
+ return new BrokerClock(options);
56
+ }
@@ -0,0 +1,154 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ import { estimateBarMs } from "../../engine/execution.js";
4
+ import { isSession } from "../../utils/time.js";
5
+
6
+ function intervalToMs(interval) {
7
+ const raw = String(interval || "1m")
8
+ .trim()
9
+ .toLowerCase();
10
+ const match = raw.match(/^(\d+)(m|h|d)$/);
11
+ if (!match) return 60_000;
12
+ const amount = Number(match[1]);
13
+ const unit = match[2];
14
+ if (unit === "m") return amount * 60_000;
15
+ if (unit === "h") return amount * 60 * 60_000;
16
+ return amount * 24 * 60 * 60_000;
17
+ }
18
+
19
+ function normalizeTick(tick) {
20
+ const time = Number(tick?.time);
21
+ const price = Number(tick?.price ?? tick?.last ?? tick?.close ?? tick?.bid ?? tick?.ask);
22
+ const volume = Number(tick?.size ?? tick?.volume ?? 0);
23
+ if (!Number.isFinite(time) || !Number.isFinite(price)) return null;
24
+ return {
25
+ time,
26
+ price,
27
+ volume: Number.isFinite(volume) ? volume : 0,
28
+ };
29
+ }
30
+
31
+ function bucketStart(time, bucketMs) {
32
+ return Math.floor(time / bucketMs) * bucketMs;
33
+ }
34
+
35
+ /**
36
+ * Handles bar-completion detection for streaming bars, ticks, or polling data.
37
+ */
38
+ export class CandleAggregator extends EventEmitter {
39
+ constructor({ mode = "stream", interval = "1m", graceMs = 5000, session = "AUTO" } = {}) {
40
+ super();
41
+ this.mode = mode;
42
+ this.interval = interval;
43
+ this.graceMs = Math.max(0, Number(graceMs) || 5000);
44
+ this.session = session;
45
+ this.intervalMs = intervalToMs(interval);
46
+ this.current = null;
47
+ this.lastEmittedTime = -Infinity;
48
+ }
49
+
50
+ onBar(handler) {
51
+ this.on("bar", handler);
52
+ return () => this.off("bar", handler);
53
+ }
54
+
55
+ emitBar(bar) {
56
+ if (!bar || !Number.isFinite(bar.time)) return;
57
+ if (bar.time <= this.lastEmittedTime) return;
58
+ this.lastEmittedTime = bar.time;
59
+ this.emit("bar", bar);
60
+ }
61
+
62
+ processBar(bar, { isFinal = true } = {}) {
63
+ if (!bar || !Number.isFinite(bar.time)) return;
64
+ if (this.mode === "stream") {
65
+ if (isFinal) this.emitBar(bar);
66
+ return;
67
+ }
68
+ this.emitBar(bar);
69
+ }
70
+
71
+ processPolledBars(bars = []) {
72
+ const ordered = [...bars].sort((left, right) => left.time - right.time);
73
+ for (const bar of ordered) {
74
+ this.emitBar(bar);
75
+ }
76
+ }
77
+
78
+ processTick(rawTick) {
79
+ const tick = normalizeTick(rawTick);
80
+ if (!tick) return;
81
+
82
+ const start = bucketStart(tick.time, this.intervalMs);
83
+ if (!this.current) {
84
+ this.current = {
85
+ time: start,
86
+ open: tick.price,
87
+ high: tick.price,
88
+ low: tick.price,
89
+ close: tick.price,
90
+ volume: tick.volume,
91
+ _lastTickTime: tick.time,
92
+ };
93
+ return;
94
+ }
95
+
96
+ if (start === this.current.time) {
97
+ this.current.high = Math.max(this.current.high, tick.price);
98
+ this.current.low = Math.min(this.current.low, tick.price);
99
+ this.current.close = tick.price;
100
+ this.current.volume += tick.volume;
101
+ this.current._lastTickTime = tick.time;
102
+ return;
103
+ }
104
+
105
+ if (start > this.current.time) {
106
+ this.emitBar({
107
+ time: this.current.time,
108
+ open: this.current.open,
109
+ high: this.current.high,
110
+ low: this.current.low,
111
+ close: this.current.close,
112
+ volume: this.current.volume,
113
+ });
114
+ this.current = {
115
+ time: start,
116
+ open: tick.price,
117
+ high: tick.price,
118
+ low: tick.price,
119
+ close: tick.price,
120
+ volume: tick.volume,
121
+ _lastTickTime: tick.time,
122
+ };
123
+ }
124
+ }
125
+
126
+ forceClose(timeMs = Date.now()) {
127
+ if (!this.current) return;
128
+ const closeDeadline = this.current.time + this.intervalMs + this.graceMs;
129
+ const sessionOpen = isSession(this.current.time + this.intervalMs, this.session);
130
+ if (timeMs >= closeDeadline || !sessionOpen) {
131
+ this.emitBar({
132
+ time: this.current.time,
133
+ open: this.current.open,
134
+ high: this.current.high,
135
+ low: this.current.low,
136
+ close: this.current.close,
137
+ volume: this.current.volume,
138
+ });
139
+ this.current = null;
140
+ }
141
+ }
142
+
143
+ estimateFromSeries(candles) {
144
+ const estimated = estimateBarMs(candles);
145
+ if (Number.isFinite(estimated) && estimated > 0) {
146
+ this.intervalMs = estimated;
147
+ }
148
+ return this.intervalMs;
149
+ }
150
+ }
151
+
152
+ export function createCandleAggregator(options) {
153
+ return new CandleAggregator(options);
154
+ }