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
package/types/index.d.ts CHANGED
@@ -108,6 +108,22 @@ export interface BacktestTrade {
108
108
  [key: string]: unknown;
109
109
  }
110
110
 
111
+ export interface OpenPosition {
112
+ id?: number;
113
+ symbol?: string;
114
+ side: Side;
115
+ entry: number;
116
+ entryFill?: number;
117
+ stop: number;
118
+ takeProfit: number;
119
+ size: number;
120
+ openTime: number;
121
+ markPrice: number;
122
+ unrealizedPnl: number;
123
+ _initRisk?: number;
124
+ [key: string]: unknown;
125
+ }
126
+
111
127
  export interface SideBreakdownEntry {
112
128
  trades: number;
113
129
  winRate: number;
@@ -353,6 +369,8 @@ export interface BacktestResult {
353
369
  trades: BacktestTrade[];
354
370
  /** Completed positions only, without intermediate realized legs. */
355
371
  positions: BacktestTrade[];
372
+ /** Open positions still active at end-of-data (if any). */
373
+ openPositions: OpenPosition[];
356
374
  /** Aggregate performance statistics. */
357
375
  metrics: BacktestMetrics;
358
376
  /** Realized equity points suitable for charts and exports. */
@@ -540,6 +558,8 @@ export function backtestPortfolio(options: {
540
558
  collectEqSeries?: boolean;
541
559
  collectReplay?: boolean;
542
560
  maxDailyLossPct?: number;
561
+ processingOrder?: "sequential" | "shuffle";
562
+ shuffleSeed?: number;
543
563
  }): PortfolioBacktestResult;
544
564
  export function walkForwardOptimize(options: {
545
565
  candles: Candle[];
@@ -637,9 +657,7 @@ export function calculatePositionSize(input: {
637
657
  export function offsetET(timeMs: number): number;
638
658
  export function minutesET(timeMs: number): number;
639
659
  export function isSession(timeMs: number, session?: "NYSE" | "FUT" | "AUTO"): boolean;
640
- export function parseWindowsCSV(
641
- csv: string
642
- ): Array<{ aMin: number; bMin: number }> | null;
660
+ export function parseWindowsCSV(csv: string): Array<{ aMin: number; bMin: number }> | null;
643
661
  export function inWindowsET(
644
662
  timeMs: number,
645
663
  windows: Array<{ aMin: number; bMin: number }>
@@ -0,0 +1,382 @@
1
+ import type {
2
+ BacktestTrade,
3
+ Candle,
4
+ EquityPoint,
5
+ ExecutionCostOptions,
6
+ OpenPosition,
7
+ PendingOrder,
8
+ SignalFunction,
9
+ SignalResult,
10
+ } from "./index.d.ts";
11
+
12
+ export interface BrokerConfig {
13
+ apiKey?: string;
14
+ apiSecret?: string;
15
+ passphrase?: string;
16
+ paper?: boolean;
17
+ baseUrl?: string;
18
+ wsUrl?: string;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ export interface AccountInfo {
23
+ equity: number;
24
+ buyingPower: number;
25
+ cash: number;
26
+ currency: string;
27
+ marginUsed?: number;
28
+ }
29
+
30
+ export interface LiveOrder {
31
+ symbol: string;
32
+ side: "buy" | "sell";
33
+ type: "market" | "limit" | "stop" | "stop_limit";
34
+ qty: number;
35
+ limitPrice?: number;
36
+ stopPrice?: number;
37
+ timeInForce?: "day" | "gtc" | "ioc" | "fok";
38
+ clientOrderId?: string;
39
+ }
40
+
41
+ export interface OrderModification {
42
+ qty?: number;
43
+ limitPrice?: number;
44
+ stopPrice?: number;
45
+ }
46
+
47
+ export interface OrderReceipt {
48
+ orderId: string;
49
+ clientOrderId?: string;
50
+ status: "new" | "partially_filled" | "filled" | "canceled" | "rejected" | "expired";
51
+ filledQty: number;
52
+ avgFillPrice?: number;
53
+ filledAt?: number;
54
+ symbol: string;
55
+ side: "buy" | "sell";
56
+ type: string;
57
+ qty: number;
58
+ rejectReason?: string;
59
+ }
60
+
61
+ export interface BrokerPosition {
62
+ symbol: string;
63
+ side: "long" | "short";
64
+ qty: number;
65
+ avgEntry: number;
66
+ marketValue: number;
67
+ unrealizedPnl: number;
68
+ }
69
+
70
+ export interface Subscription {
71
+ unsubscribe(): void;
72
+ }
73
+
74
+ export interface StoredState {
75
+ openPosition: OpenPosition | null;
76
+ pendingOrder: PendingOrder | null;
77
+ equity: number;
78
+ candleBuffer: Candle[];
79
+ strategyState: Record<string, unknown>;
80
+ lastBarTime: number | null;
81
+ dayPnl: number;
82
+ dayTrades: number;
83
+ tradeIdCounter: number;
84
+ savedAt: number;
85
+ }
86
+
87
+ export class EventBus extends import("node:events").EventEmitter {
88
+ emitEvent(event: string, payload?: Record<string, unknown>): true;
89
+ onAny(handler: (input: { event: string; payload: Record<string, unknown> }) => void): () => void;
90
+ }
91
+
92
+ export class LiveLogger {
93
+ constructor(options?: {
94
+ level?: "debug" | "info" | "warn" | "error" | "silent";
95
+ stream?: NodeJS.WritableStream;
96
+ });
97
+ attach(eventBus: EventBus): () => void;
98
+ detach(): void;
99
+ }
100
+
101
+ export class BrokerClock {
102
+ constructor(options?: { warnThresholdMs?: number });
103
+ syncWithBroker(broker: BrokerAdapter): Promise<{
104
+ serverTime: number | null;
105
+ localTime: number;
106
+ offsetMs: number;
107
+ warning: string | null;
108
+ }>;
109
+ now(): number;
110
+ }
111
+
112
+ export class BrokerAdapter extends import("node:events").EventEmitter {
113
+ connect(config?: BrokerConfig): Promise<void>;
114
+ disconnect(): Promise<void>;
115
+ isConnected(): boolean;
116
+ getAccount(): Promise<AccountInfo>;
117
+ getPositions(): Promise<BrokerPosition[]>;
118
+ getServerTime(): Promise<number>;
119
+ submitOrder(order: LiveOrder): Promise<OrderReceipt>;
120
+ cancelOrder(orderId: string): Promise<void>;
121
+ modifyOrder(orderId: string, changes: OrderModification): Promise<OrderReceipt>;
122
+ getOpenOrders(): Promise<OrderReceipt[]>;
123
+ getOrderStatus(orderId: string): Promise<OrderReceipt>;
124
+ subscribeQuotes(symbol: string, handler: (quote: unknown) => void): Promise<Subscription>;
125
+ subscribeTrades(symbol: string, handler: (trade: unknown) => void): Promise<Subscription>;
126
+ subscribeBars(
127
+ symbol: string,
128
+ interval: string,
129
+ handler: (bar: Candle) => void
130
+ ): Promise<Subscription>;
131
+ getHistoricalBars(symbol: string, interval: string, limit: number): Promise<Candle[]>;
132
+ supportsPaperNative(): boolean;
133
+ }
134
+
135
+ export class AlpacaBroker extends BrokerAdapter {}
136
+ export class BinanceBroker extends BrokerAdapter {}
137
+ export class CoinbaseBroker extends BrokerAdapter {}
138
+ export class InteractiveBrokersBroker extends BrokerAdapter {}
139
+
140
+ export class FeedProvider {
141
+ connect(): Promise<void>;
142
+ disconnect(): Promise<void>;
143
+ subscribeBars(
144
+ symbol: string,
145
+ interval: string,
146
+ handler: (bar: Candle) => void
147
+ ): Subscription | Promise<Subscription>;
148
+ subscribeTicks(
149
+ symbol: string,
150
+ handler: (tick: unknown) => void
151
+ ): Subscription | Promise<Subscription>;
152
+ getHistoricalBars(symbol: string, interval: string, count: number): Promise<Candle[]>;
153
+ }
154
+
155
+ export class BrokerFeed extends FeedProvider {
156
+ constructor(options: { broker: BrokerAdapter });
157
+ }
158
+
159
+ export class PollingFeed extends FeedProvider {
160
+ constructor(options: {
161
+ broker: BrokerAdapter;
162
+ pollIntervalMs?: number;
163
+ defaultBarsPerPoll?: number;
164
+ });
165
+ pollOnce(): Promise<void>;
166
+ startPolling(): void;
167
+ stopPolling(): void;
168
+ }
169
+
170
+ export class StorageProvider {
171
+ load(namespace: string): Promise<StoredState | null>;
172
+ save(namespace: string, state: StoredState): Promise<void>;
173
+ appendTrade(namespace: string, trade: BacktestTrade): Promise<void>;
174
+ appendEquityPoint(namespace: string, point: EquityPoint): Promise<void>;
175
+ loadTrades(namespace: string): Promise<BacktestTrade[]>;
176
+ loadEquityCurve(namespace: string): Promise<EquityPoint[]>;
177
+ clear(namespace: string): Promise<void>;
178
+ }
179
+
180
+ export class JsonFileStorage extends StorageProvider {
181
+ constructor(options?: { baseDir?: string });
182
+ }
183
+
184
+ export interface RiskManagerOptions {
185
+ maxDailyLossPct?: number;
186
+ maxDailyLossDollars?: number;
187
+ maxDrawdownPct?: number;
188
+ maxPositions?: number;
189
+ maxPositionPct?: number;
190
+ maxDailyTrades?: number;
191
+ cooldownAfterLossMs?: number;
192
+ allowedSessions?: string;
193
+ allowedWindows?: string;
194
+ }
195
+
196
+ export class RiskManager {
197
+ constructor(options?: RiskManagerOptions);
198
+ initialize(equity: number, timeMs?: number): void;
199
+ update(input: { timeMs: number; equity: number }): void;
200
+ canTrade(input?: { timeMs?: number }): { ok: boolean; reason: string | null };
201
+ canOpenPosition(input?: {
202
+ timeMs?: number;
203
+ positionCount?: number;
204
+ positionValue?: number;
205
+ equity?: number | null;
206
+ }): { ok: boolean; reason: string | null };
207
+ recordTrade(input?: { pnl?: number; timeMs?: number; equity?: number | null }): void;
208
+ halt(reason?: string): void;
209
+ clearHalt(): void;
210
+ getState(): Record<string, unknown>;
211
+ }
212
+
213
+ export class StateManager {
214
+ constructor(options: { storage: StorageProvider });
215
+ load(namespace: string): Promise<StoredState | null>;
216
+ save(namespace: string, state: StoredState): Promise<void>;
217
+ appendTrade(namespace: string, trade: BacktestTrade): Promise<void>;
218
+ appendEquityPoint(namespace: string, point: EquityPoint): Promise<void>;
219
+ loadTrades(namespace: string): Promise<BacktestTrade[]>;
220
+ loadEquityCurve(namespace: string): Promise<EquityPoint[]>;
221
+ clear(namespace: string): Promise<void>;
222
+ reconcile(input: {
223
+ persistedState: StoredState | null;
224
+ brokerPositions?: BrokerPosition[];
225
+ symbol: string;
226
+ }): {
227
+ status: "ok" | "warn" | "error";
228
+ action: "none" | "adopt-broker" | "closed-externally" | "external-position" | "mismatch";
229
+ message: string;
230
+ adoptedPosition: OpenPosition | null;
231
+ mismatch: { persisted: OpenPosition; broker: BrokerPosition } | null;
232
+ };
233
+ }
234
+
235
+ export class CandleAggregator extends import("node:events").EventEmitter {
236
+ constructor(options?: {
237
+ mode?: "stream" | "tick" | "poll";
238
+ interval?: string;
239
+ graceMs?: number;
240
+ session?: string;
241
+ });
242
+ processBar(bar: Candle, options?: { isFinal?: boolean }): void;
243
+ processTick(tick: unknown): void;
244
+ processPolledBars(bars: Candle[]): void;
245
+ forceClose(timeMs?: number): void;
246
+ }
247
+
248
+ export class PaperEngine extends BrokerAdapter {
249
+ constructor(options?: {
250
+ equity?: number;
251
+ currency?: string;
252
+ slippageBps?: number;
253
+ feeBps?: number;
254
+ costs?: ExecutionCostOptions | null;
255
+ qtyStep?: number;
256
+ });
257
+ setHistoricalBars(symbol: string, interval: string, bars: Candle[]): void;
258
+ simulateBar(symbol: string, interval: string, bar: Candle): Promise<void>;
259
+ }
260
+
261
+ export interface LiveEngineOptions {
262
+ id?: string;
263
+ signal: SignalFunction;
264
+ symbol: string;
265
+ interval: string;
266
+ broker: BrokerAdapter;
267
+ brokerConfig?: BrokerConfig;
268
+ feed?: FeedProvider;
269
+ storage?: StorageProvider;
270
+ eventBus?: EventBus;
271
+ equity?: number;
272
+ useBrokerAccountEquity?: boolean;
273
+ mode?: "streaming" | "polling";
274
+ pollIntervalMs?: number;
275
+ paper?: boolean;
276
+ warmupBars?: number;
277
+ riskPct?: number;
278
+ costs?: ExecutionCostOptions | null;
279
+ finalTP_R?: number;
280
+ maxDailyLossPct?: number;
281
+ flattenAtClose?: boolean;
282
+ qtyStep?: number;
283
+ minQty?: number;
284
+ maxLeverage?: number;
285
+ dailyMaxTrades?: number;
286
+ entryChase?: {
287
+ enabled?: boolean;
288
+ afterBars?: number;
289
+ maxSlipR?: number;
290
+ convertOnExpiry?: boolean;
291
+ };
292
+ risk?: RiskManagerOptions;
293
+ logLevel?: "debug" | "info" | "warn" | "error" | "silent";
294
+ }
295
+
296
+ export class LiveEngine {
297
+ constructor(options: LiveEngineOptions);
298
+ readonly eventBus: EventBus;
299
+ start(): Promise<void>;
300
+ stop(options?: { flattenOnShutdown?: boolean }): Promise<void>;
301
+ handleBar(bar: Candle): Promise<void>;
302
+ pollOnce(): Promise<void>;
303
+ getStatus(): Record<string, unknown>;
304
+ }
305
+
306
+ export interface LiveSystemConfig extends Omit<
307
+ LiveEngineOptions,
308
+ "broker" | "feed" | "storage" | "eventBus"
309
+ > {
310
+ id?: string;
311
+ weight?: number;
312
+ }
313
+
314
+ export class LiveOrchestrator {
315
+ constructor(options: {
316
+ systems: LiveSystemConfig[];
317
+ broker: BrokerAdapter;
318
+ brokerConfig?: BrokerConfig;
319
+ feed?: FeedProvider;
320
+ storage?: StorageProvider;
321
+ eventBus?: EventBus;
322
+ equity?: number;
323
+ allocation?: "equal" | "weight";
324
+ maxDailyLossPct?: number;
325
+ risk?: RiskManagerOptions;
326
+ });
327
+ readonly eventBus: EventBus;
328
+ start(): Promise<void>;
329
+ stop(): Promise<void>;
330
+ getStatus(): Record<string, unknown>;
331
+ }
332
+
333
+ export function createEventBus(): EventBus;
334
+ export function createLogger(options?: {
335
+ level?: "debug" | "info" | "warn" | "error" | "silent";
336
+ }): LiveLogger;
337
+ export function createClock(options?: { warnThresholdMs?: number }): BrokerClock;
338
+
339
+ export function createAlpacaBroker(options?: { fetchImpl?: typeof fetch }): AlpacaBroker;
340
+ export function createBinanceBroker(options?: { fetchImpl?: typeof fetch }): BinanceBroker;
341
+ export function createCoinbaseBroker(options?: { fetchImpl?: typeof fetch }): CoinbaseBroker;
342
+ export function createInteractiveBrokersBroker(
343
+ options?: Record<string, unknown>
344
+ ): InteractiveBrokersBroker;
345
+ export function createBrokerFeed(options: { broker: BrokerAdapter }): BrokerFeed;
346
+ export function createPollingFeed(options: {
347
+ broker: BrokerAdapter;
348
+ pollIntervalMs?: number;
349
+ defaultBarsPerPoll?: number;
350
+ }): PollingFeed;
351
+ export function createJsonFileStorage(options?: { baseDir?: string }): JsonFileStorage;
352
+ export function createRiskManager(options?: RiskManagerOptions): RiskManager;
353
+ export function createStateManager(options: { storage: StorageProvider }): StateManager;
354
+ export function createCandleAggregator(options?: {
355
+ mode?: "stream" | "tick" | "poll";
356
+ interval?: string;
357
+ graceMs?: number;
358
+ session?: string;
359
+ }): CandleAggregator;
360
+ export function createPaperEngine(options?: {
361
+ equity?: number;
362
+ currency?: string;
363
+ slippageBps?: number;
364
+ feeBps?: number;
365
+ costs?: ExecutionCostOptions | null;
366
+ qtyStep?: number;
367
+ }): PaperEngine;
368
+ export function createLiveEngine(options: LiveEngineOptions): LiveEngine;
369
+ export function createLiveOrchestrator(options: {
370
+ systems: LiveSystemConfig[];
371
+ broker: BrokerAdapter;
372
+ brokerConfig?: BrokerConfig;
373
+ feed?: FeedProvider;
374
+ storage?: StorageProvider;
375
+ eventBus?: EventBus;
376
+ equity?: number;
377
+ allocation?: "equal" | "weight";
378
+ maxDailyLossPct?: number;
379
+ risk?: RiskManagerOptions;
380
+ }): LiveOrchestrator;
381
+
382
+ export type { SignalResult };