backtest-kit 1.0.4 → 1.1.1
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 +707 -153
- package/build/index.cjs +2906 -192
- package/build/index.mjs +2901 -188
- package/package.json +2 -9
- package/types.d.ts +2205 -60
package/build/index.mjs
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { createActivator } from 'di-kit';
|
|
2
2
|
import { scoped } from 'di-scoped';
|
|
3
|
-
import { memoize, makeExtendable, singleshot, getErrorMessage, not, trycatch, retry, randomString, ToolRegistry, sleep,
|
|
4
|
-
import fs from 'fs/promises';
|
|
3
|
+
import { memoize, makeExtendable, singleshot, getErrorMessage, not, trycatch, retry, randomString, Subject, ToolRegistry, sleep, str, queued } from 'functools-kit';
|
|
4
|
+
import fs, { mkdir, writeFile } from 'fs/promises';
|
|
5
5
|
import path, { join } from 'path';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import os from 'os';
|
|
8
|
-
import Table from 'cli-table3';
|
|
9
8
|
|
|
10
9
|
const { init, inject, provide } = createActivator("backtest");
|
|
11
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Scoped service for method context propagation.
|
|
13
|
+
*
|
|
14
|
+
* Uses di-scoped for implicit context passing without explicit parameters.
|
|
15
|
+
* Context includes strategyName, exchangeName, and frameName.
|
|
16
|
+
*
|
|
17
|
+
* Used by PublicServices to inject schema names into ConnectionServices.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* MethodContextService.runAsyncIterator(
|
|
22
|
+
* backtestGenerator,
|
|
23
|
+
* {
|
|
24
|
+
* strategyName: "my-strategy",
|
|
25
|
+
* exchangeName: "my-exchange",
|
|
26
|
+
* frameName: "1d-backtest"
|
|
27
|
+
* }
|
|
28
|
+
* );
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
12
31
|
const MethodContextService = scoped(class {
|
|
13
32
|
constructor(context) {
|
|
14
33
|
this.context = context;
|
|
@@ -47,6 +66,15 @@ const logicPublicServices$1 = {
|
|
|
47
66
|
backtestLogicPublicService: Symbol('backtestLogicPublicService'),
|
|
48
67
|
liveLogicPublicService: Symbol('liveLogicPublicService'),
|
|
49
68
|
};
|
|
69
|
+
const markdownServices$1 = {
|
|
70
|
+
backtestMarkdownService: Symbol('backtestMarkdownService'),
|
|
71
|
+
liveMarkdownService: Symbol('liveMarkdownService'),
|
|
72
|
+
};
|
|
73
|
+
const validationServices$1 = {
|
|
74
|
+
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
75
|
+
strategyValidationService: Symbol('strategyValidationService'),
|
|
76
|
+
frameValidationService: Symbol('frameValidationService'),
|
|
77
|
+
};
|
|
50
78
|
const TYPES = {
|
|
51
79
|
...baseServices$1,
|
|
52
80
|
...contextServices$1,
|
|
@@ -55,14 +83,39 @@ const TYPES = {
|
|
|
55
83
|
...globalServices$1,
|
|
56
84
|
...logicPrivateServices$1,
|
|
57
85
|
...logicPublicServices$1,
|
|
86
|
+
...markdownServices$1,
|
|
87
|
+
...validationServices$1,
|
|
58
88
|
};
|
|
59
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Scoped service for execution context propagation.
|
|
92
|
+
*
|
|
93
|
+
* Uses di-scoped for implicit context passing without explicit parameters.
|
|
94
|
+
* Context includes symbol, when (timestamp), and backtest flag.
|
|
95
|
+
*
|
|
96
|
+
* Used by GlobalServices to inject context into operations.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* ExecutionContextService.runInContext(
|
|
101
|
+
* async () => {
|
|
102
|
+
* // Inside this callback, context is automatically available
|
|
103
|
+
* return await someOperation();
|
|
104
|
+
* },
|
|
105
|
+
* { symbol: "BTCUSDT", when: new Date(), backtest: true }
|
|
106
|
+
* );
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
60
109
|
const ExecutionContextService = scoped(class {
|
|
61
110
|
constructor(context) {
|
|
62
111
|
this.context = context;
|
|
63
112
|
}
|
|
64
113
|
});
|
|
65
114
|
|
|
115
|
+
/**
|
|
116
|
+
* No-op logger implementation used as default.
|
|
117
|
+
* Silently discards all log messages.
|
|
118
|
+
*/
|
|
66
119
|
const NOOP_LOGGER = {
|
|
67
120
|
log() {
|
|
68
121
|
},
|
|
@@ -70,31 +123,84 @@ const NOOP_LOGGER = {
|
|
|
70
123
|
},
|
|
71
124
|
info() {
|
|
72
125
|
},
|
|
126
|
+
warn() {
|
|
127
|
+
},
|
|
73
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* Logger service with automatic context injection.
|
|
131
|
+
*
|
|
132
|
+
* Features:
|
|
133
|
+
* - Delegates to user-provided logger via setLogger()
|
|
134
|
+
* - Automatically appends method context (strategyName, exchangeName, frameName)
|
|
135
|
+
* - Automatically appends execution context (symbol, when, backtest)
|
|
136
|
+
* - Defaults to NOOP_LOGGER if no logger configured
|
|
137
|
+
*
|
|
138
|
+
* Used throughout the framework for consistent logging with context.
|
|
139
|
+
*/
|
|
74
140
|
class LoggerService {
|
|
75
141
|
constructor() {
|
|
76
142
|
this.methodContextService = inject(TYPES.methodContextService);
|
|
77
143
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
78
144
|
this._commonLogger = NOOP_LOGGER;
|
|
145
|
+
/**
|
|
146
|
+
* Logs general-purpose message with automatic context injection.
|
|
147
|
+
*
|
|
148
|
+
* @param topic - Log topic/category
|
|
149
|
+
* @param args - Additional log arguments
|
|
150
|
+
*/
|
|
79
151
|
this.log = async (topic, ...args) => {
|
|
80
152
|
await this._commonLogger.log(topic, ...args, this.methodContext, this.executionContext);
|
|
81
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* Logs debug-level message with automatic context injection.
|
|
156
|
+
*
|
|
157
|
+
* @param topic - Log topic/category
|
|
158
|
+
* @param args - Additional log arguments
|
|
159
|
+
*/
|
|
82
160
|
this.debug = async (topic, ...args) => {
|
|
83
161
|
await this._commonLogger.debug(topic, ...args, this.methodContext, this.executionContext);
|
|
84
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* Logs info-level message with automatic context injection.
|
|
165
|
+
*
|
|
166
|
+
* @param topic - Log topic/category
|
|
167
|
+
* @param args - Additional log arguments
|
|
168
|
+
*/
|
|
85
169
|
this.info = async (topic, ...args) => {
|
|
86
170
|
await this._commonLogger.info(topic, ...args, this.methodContext, this.executionContext);
|
|
87
171
|
};
|
|
172
|
+
/**
|
|
173
|
+
* Logs warning-level message with automatic context injection.
|
|
174
|
+
*
|
|
175
|
+
* @param topic - Log topic/category
|
|
176
|
+
* @param args - Additional log arguments
|
|
177
|
+
*/
|
|
178
|
+
this.warn = async (topic, ...args) => {
|
|
179
|
+
await this._commonLogger.warn(topic, ...args, this.methodContext, this.executionContext);
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Sets custom logger implementation.
|
|
183
|
+
*
|
|
184
|
+
* @param logger - Custom logger implementing ILogger interface
|
|
185
|
+
*/
|
|
88
186
|
this.setLogger = (logger) => {
|
|
89
187
|
this._commonLogger = logger;
|
|
90
188
|
};
|
|
91
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Gets current method context if available.
|
|
192
|
+
* Contains strategyName, exchangeName, frameName from MethodContextService.
|
|
193
|
+
*/
|
|
92
194
|
get methodContext() {
|
|
93
195
|
if (MethodContextService.hasContext()) {
|
|
94
196
|
return this.methodContextService.context;
|
|
95
197
|
}
|
|
96
198
|
return {};
|
|
97
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Gets current execution context if available.
|
|
202
|
+
* Contains symbol, when, backtest from ExecutionContextService.
|
|
203
|
+
*/
|
|
98
204
|
get executionContext() {
|
|
99
205
|
if (ExecutionContextService.hasContext()) {
|
|
100
206
|
return this.executionContextService.context;
|
|
@@ -115,10 +221,44 @@ const INTERVAL_MINUTES$2 = {
|
|
|
115
221
|
"6h": 360,
|
|
116
222
|
"8h": 480,
|
|
117
223
|
};
|
|
224
|
+
/**
|
|
225
|
+
* Client implementation for exchange data access.
|
|
226
|
+
*
|
|
227
|
+
* Features:
|
|
228
|
+
* - Historical candle fetching (backwards from execution context)
|
|
229
|
+
* - Future candle fetching (forwards for backtest)
|
|
230
|
+
* - VWAP calculation from last 5 1m candles
|
|
231
|
+
* - Price/quantity formatting for exchange
|
|
232
|
+
*
|
|
233
|
+
* All methods use prototype functions for memory efficiency.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* const exchange = new ClientExchange({
|
|
238
|
+
* exchangeName: "binance",
|
|
239
|
+
* getCandles: async (symbol, interval, since, limit) => [...],
|
|
240
|
+
* formatPrice: async (symbol, price) => price.toFixed(2),
|
|
241
|
+
* formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
|
|
242
|
+
* execution: executionService,
|
|
243
|
+
* logger: loggerService,
|
|
244
|
+
* });
|
|
245
|
+
*
|
|
246
|
+
* const candles = await exchange.getCandles("BTCUSDT", "1m", 100);
|
|
247
|
+
* const vwap = await exchange.getAveragePrice("BTCUSDT");
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
118
250
|
class ClientExchange {
|
|
119
251
|
constructor(params) {
|
|
120
252
|
this.params = params;
|
|
121
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Fetches historical candles backwards from execution context time.
|
|
256
|
+
*
|
|
257
|
+
* @param symbol - Trading pair symbol
|
|
258
|
+
* @param interval - Candle interval
|
|
259
|
+
* @param limit - Number of candles to fetch
|
|
260
|
+
* @returns Promise resolving to array of candles
|
|
261
|
+
*/
|
|
122
262
|
async getCandles(symbol, interval, limit) {
|
|
123
263
|
this.params.logger.debug(`ClientExchange getCandles`, {
|
|
124
264
|
symbol,
|
|
@@ -126,17 +266,34 @@ class ClientExchange {
|
|
|
126
266
|
limit,
|
|
127
267
|
});
|
|
128
268
|
const step = INTERVAL_MINUTES$2[interval];
|
|
129
|
-
const adjust = step * limit -
|
|
269
|
+
const adjust = step * limit - step;
|
|
130
270
|
if (!adjust) {
|
|
131
271
|
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
132
272
|
}
|
|
133
273
|
const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
|
|
134
274
|
const data = await this.params.getCandles(symbol, interval, since, limit);
|
|
275
|
+
// Filter candles to strictly match the requested range
|
|
276
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
277
|
+
const sinceTimestamp = since.getTime();
|
|
278
|
+
const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
|
|
279
|
+
if (filteredData.length < limit) {
|
|
280
|
+
this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${filteredData.length}`);
|
|
281
|
+
}
|
|
135
282
|
if (this.params.callbacks?.onCandleData) {
|
|
136
|
-
this.params.callbacks.onCandleData(symbol, interval, since, limit,
|
|
283
|
+
this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
|
|
137
284
|
}
|
|
138
|
-
return
|
|
285
|
+
return filteredData;
|
|
139
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Fetches future candles forwards from execution context time.
|
|
289
|
+
* Used in backtest mode to get candles for signal duration.
|
|
290
|
+
*
|
|
291
|
+
* @param symbol - Trading pair symbol
|
|
292
|
+
* @param interval - Candle interval
|
|
293
|
+
* @param limit - Number of candles to fetch
|
|
294
|
+
* @returns Promise resolving to array of candles
|
|
295
|
+
* @throws Error if trying to fetch future candles in live mode
|
|
296
|
+
*/
|
|
140
297
|
async getNextCandles(symbol, interval, limit) {
|
|
141
298
|
this.params.logger.debug(`ClientExchange getNextCandles`, {
|
|
142
299
|
symbol,
|
|
@@ -153,11 +310,30 @@ class ClientExchange {
|
|
|
153
310
|
return [];
|
|
154
311
|
}
|
|
155
312
|
const data = await this.params.getCandles(symbol, interval, since, limit);
|
|
313
|
+
// Filter candles to strictly match the requested range
|
|
314
|
+
const sinceTimestamp = since.getTime();
|
|
315
|
+
const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
|
|
316
|
+
if (filteredData.length < limit) {
|
|
317
|
+
this.params.logger.warn(`ClientExchange getNextCandles: Expected ${limit} candles, got ${filteredData.length}`);
|
|
318
|
+
}
|
|
156
319
|
if (this.params.callbacks?.onCandleData) {
|
|
157
|
-
this.params.callbacks.onCandleData(symbol, interval, since, limit,
|
|
320
|
+
this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
|
|
158
321
|
}
|
|
159
|
-
return
|
|
322
|
+
return filteredData;
|
|
160
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Calculates VWAP (Volume Weighted Average Price) from last 5 1m candles.
|
|
326
|
+
*
|
|
327
|
+
* Formula:
|
|
328
|
+
* - Typical Price = (high + low + close) / 3
|
|
329
|
+
* - VWAP = sum(typical_price * volume) / sum(volume)
|
|
330
|
+
*
|
|
331
|
+
* If volume is zero, returns simple average of close prices.
|
|
332
|
+
*
|
|
333
|
+
* @param symbol - Trading pair symbol
|
|
334
|
+
* @returns Promise resolving to VWAP price
|
|
335
|
+
* @throws Error if no candles available
|
|
336
|
+
*/
|
|
161
337
|
async getAveragePrice(symbol) {
|
|
162
338
|
this.params.logger.debug(`ClientExchange getAveragePrice`, {
|
|
163
339
|
symbol,
|
|
@@ -197,13 +373,44 @@ class ClientExchange {
|
|
|
197
373
|
}
|
|
198
374
|
}
|
|
199
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Connection service routing exchange operations to correct ClientExchange instance.
|
|
378
|
+
*
|
|
379
|
+
* Routes all IExchange method calls to the appropriate exchange implementation
|
|
380
|
+
* based on methodContextService.context.exchangeName. Uses memoization to cache
|
|
381
|
+
* ClientExchange instances for performance.
|
|
382
|
+
*
|
|
383
|
+
* Key features:
|
|
384
|
+
* - Automatic exchange routing via method context
|
|
385
|
+
* - Memoized ClientExchange instances by exchangeName
|
|
386
|
+
* - Implements full IExchange interface
|
|
387
|
+
* - Logging for all operations
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* // Used internally by framework
|
|
392
|
+
* const candles = await exchangeConnectionService.getCandles(
|
|
393
|
+
* "BTCUSDT", "1h", 100
|
|
394
|
+
* );
|
|
395
|
+
* // Automatically routes to correct exchange based on methodContext
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
200
398
|
class ExchangeConnectionService {
|
|
201
399
|
constructor() {
|
|
202
400
|
this.loggerService = inject(TYPES.loggerService);
|
|
203
401
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
204
402
|
this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
|
|
205
403
|
this.methodContextService = inject(TYPES.methodContextService);
|
|
206
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Retrieves memoized ClientExchange instance for given exchange name.
|
|
406
|
+
*
|
|
407
|
+
* Creates ClientExchange on first call, returns cached instance on subsequent calls.
|
|
408
|
+
* Cache key is exchangeName string.
|
|
409
|
+
*
|
|
410
|
+
* @param exchangeName - Name of registered exchange schema
|
|
411
|
+
* @returns Configured ClientExchange instance
|
|
412
|
+
*/
|
|
413
|
+
this.getExchange = memoize(([exchangeName]) => `${exchangeName}`, (exchangeName) => {
|
|
207
414
|
const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
|
|
208
415
|
return new ClientExchange({
|
|
209
416
|
execution: this.executionContextService,
|
|
@@ -215,6 +422,16 @@ class ExchangeConnectionService {
|
|
|
215
422
|
callbacks,
|
|
216
423
|
});
|
|
217
424
|
});
|
|
425
|
+
/**
|
|
426
|
+
* Fetches historical candles for symbol using configured exchange.
|
|
427
|
+
*
|
|
428
|
+
* Routes to exchange determined by methodContextService.context.exchangeName.
|
|
429
|
+
*
|
|
430
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
431
|
+
* @param interval - Candle interval (e.g., "1h", "1d")
|
|
432
|
+
* @param limit - Maximum number of candles to fetch
|
|
433
|
+
* @returns Promise resolving to array of candle data
|
|
434
|
+
*/
|
|
218
435
|
this.getCandles = async (symbol, interval, limit) => {
|
|
219
436
|
this.loggerService.log("exchangeConnectionService getCandles", {
|
|
220
437
|
symbol,
|
|
@@ -223,6 +440,17 @@ class ExchangeConnectionService {
|
|
|
223
440
|
});
|
|
224
441
|
return await this.getExchange(this.methodContextService.context.exchangeName).getCandles(symbol, interval, limit);
|
|
225
442
|
};
|
|
443
|
+
/**
|
|
444
|
+
* Fetches next batch of candles relative to executionContext.when.
|
|
445
|
+
*
|
|
446
|
+
* Returns candles that come after the current execution timestamp.
|
|
447
|
+
* Used for backtest progression and live trading updates.
|
|
448
|
+
*
|
|
449
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
450
|
+
* @param interval - Candle interval (e.g., "1h", "1d")
|
|
451
|
+
* @param limit - Maximum number of candles to fetch
|
|
452
|
+
* @returns Promise resolving to array of candle data
|
|
453
|
+
*/
|
|
226
454
|
this.getNextCandles = async (symbol, interval, limit) => {
|
|
227
455
|
this.loggerService.log("exchangeConnectionService getNextCandles", {
|
|
228
456
|
symbol,
|
|
@@ -231,12 +459,30 @@ class ExchangeConnectionService {
|
|
|
231
459
|
});
|
|
232
460
|
return await this.getExchange(this.methodContextService.context.exchangeName).getNextCandles(symbol, interval, limit);
|
|
233
461
|
};
|
|
462
|
+
/**
|
|
463
|
+
* Retrieves current average price for symbol.
|
|
464
|
+
*
|
|
465
|
+
* In live mode: fetches real-time average price from exchange API.
|
|
466
|
+
* In backtest mode: calculates VWAP from candles in current timeframe.
|
|
467
|
+
*
|
|
468
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
469
|
+
* @returns Promise resolving to average price
|
|
470
|
+
*/
|
|
234
471
|
this.getAveragePrice = async (symbol) => {
|
|
235
472
|
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
236
473
|
symbol,
|
|
237
474
|
});
|
|
238
475
|
return await this.getExchange(this.methodContextService.context.exchangeName).getAveragePrice(symbol);
|
|
239
476
|
};
|
|
477
|
+
/**
|
|
478
|
+
* Formats price according to exchange-specific precision rules.
|
|
479
|
+
*
|
|
480
|
+
* Ensures price meets exchange requirements for decimal places and tick size.
|
|
481
|
+
*
|
|
482
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
483
|
+
* @param price - Raw price value to format
|
|
484
|
+
* @returns Promise resolving to formatted price string
|
|
485
|
+
*/
|
|
240
486
|
this.formatPrice = async (symbol, price) => {
|
|
241
487
|
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
242
488
|
symbol,
|
|
@@ -244,6 +490,15 @@ class ExchangeConnectionService {
|
|
|
244
490
|
});
|
|
245
491
|
return await this.getExchange(this.methodContextService.context.exchangeName).formatPrice(symbol, price);
|
|
246
492
|
};
|
|
493
|
+
/**
|
|
494
|
+
* Formats quantity according to exchange-specific precision rules.
|
|
495
|
+
*
|
|
496
|
+
* Ensures quantity meets exchange requirements for decimal places and lot size.
|
|
497
|
+
*
|
|
498
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
499
|
+
* @param quantity - Raw quantity value to format
|
|
500
|
+
* @returns Promise resolving to formatted quantity string
|
|
501
|
+
*/
|
|
247
502
|
this.formatQuantity = async (symbol, quantity) => {
|
|
248
503
|
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
249
504
|
symbol,
|
|
@@ -254,8 +509,45 @@ class ExchangeConnectionService {
|
|
|
254
509
|
}
|
|
255
510
|
}
|
|
256
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Slippage percentage applied to entry and exit prices.
|
|
514
|
+
* Simulates market impact and order book depth.
|
|
515
|
+
*/
|
|
257
516
|
const PERCENT_SLIPPAGE = 0.1;
|
|
517
|
+
/**
|
|
518
|
+
* Fee percentage charged per transaction.
|
|
519
|
+
* Applied twice (entry and exit) for total fee calculation.
|
|
520
|
+
*/
|
|
258
521
|
const PERCENT_FEE = 0.1;
|
|
522
|
+
/**
|
|
523
|
+
* Calculates profit/loss for a closed signal with slippage and fees.
|
|
524
|
+
*
|
|
525
|
+
* Formula breakdown:
|
|
526
|
+
* 1. Apply slippage to open/close prices (worse execution)
|
|
527
|
+
* - LONG: buy higher (+slippage), sell lower (-slippage)
|
|
528
|
+
* - SHORT: sell lower (-slippage), buy higher (+slippage)
|
|
529
|
+
* 2. Calculate raw PNL percentage
|
|
530
|
+
* - LONG: ((closePrice - openPrice) / openPrice) * 100
|
|
531
|
+
* - SHORT: ((openPrice - closePrice) / openPrice) * 100
|
|
532
|
+
* 3. Subtract total fees (0.1% * 2 = 0.2%)
|
|
533
|
+
*
|
|
534
|
+
* @param signal - Closed signal with position details
|
|
535
|
+
* @param priceClose - Actual close price at exit
|
|
536
|
+
* @returns PNL data with percentage and prices
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* ```typescript
|
|
540
|
+
* const pnl = toProfitLossDto(
|
|
541
|
+
* {
|
|
542
|
+
* position: "long",
|
|
543
|
+
* priceOpen: 50000,
|
|
544
|
+
* // ... other signal fields
|
|
545
|
+
* },
|
|
546
|
+
* 51000 // close price
|
|
547
|
+
* );
|
|
548
|
+
* console.log(pnl.pnlPercentage); // e.g., 1.8% (after slippage and fees)
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
259
551
|
const toProfitLossDto = (signal, priceClose) => {
|
|
260
552
|
const priceOpen = signal.priceOpen;
|
|
261
553
|
let priceOpenWithSlippage;
|
|
@@ -431,9 +723,9 @@ const BASE_WAIT_FOR_INIT_FN = async (self) => {
|
|
|
431
723
|
}
|
|
432
724
|
catch {
|
|
433
725
|
const filePath = self._getFilePath(key);
|
|
434
|
-
console.error(`
|
|
726
|
+
console.error(`backtest-kit PersistBase found invalid document for filePath=${filePath} entityName=${self.entityName}`);
|
|
435
727
|
if (await not(BASE_WAIT_FOR_INIT_UNLINK_FN(filePath))) {
|
|
436
|
-
console.error(`
|
|
728
|
+
console.error(`backtest-kit PersistBase failed to remove invalid document for filePath=${filePath} entityName=${self.entityName}`);
|
|
437
729
|
}
|
|
438
730
|
}
|
|
439
731
|
}
|
|
@@ -444,13 +736,36 @@ const BASE_WAIT_FOR_INIT_UNLINK_FN = async (filePath) => trycatch(retry(async ()
|
|
|
444
736
|
return true;
|
|
445
737
|
}
|
|
446
738
|
catch (error) {
|
|
447
|
-
console.error(`
|
|
739
|
+
console.error(`backtest-kit PersistBase unlink failed for filePath=${filePath} error=${getErrorMessage(error)}`);
|
|
448
740
|
throw error;
|
|
449
741
|
}
|
|
450
742
|
}, BASE_UNLINK_RETRY_COUNT, BASE_UNLINK_RETRY_DELAY), {
|
|
451
743
|
defaultValue: false,
|
|
452
744
|
});
|
|
745
|
+
/**
|
|
746
|
+
* Base class for file-based persistence with atomic writes.
|
|
747
|
+
*
|
|
748
|
+
* Features:
|
|
749
|
+
* - Atomic file writes using writeFileAtomic
|
|
750
|
+
* - Auto-validation and cleanup of corrupted files
|
|
751
|
+
* - Async generator support for iteration
|
|
752
|
+
* - Retry logic for file deletion
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* ```typescript
|
|
756
|
+
* const persist = new PersistBase("my-entity", "./data");
|
|
757
|
+
* await persist.waitForInit(true);
|
|
758
|
+
* await persist.writeValue("key1", { data: "value" });
|
|
759
|
+
* const value = await persist.readValue("key1");
|
|
760
|
+
* ```
|
|
761
|
+
*/
|
|
453
762
|
const PersistBase = makeExtendable(class {
|
|
763
|
+
/**
|
|
764
|
+
* Creates new persistence instance.
|
|
765
|
+
*
|
|
766
|
+
* @param entityName - Unique entity type identifier
|
|
767
|
+
* @param baseDir - Base directory for all entities (default: ./logs/data)
|
|
768
|
+
*/
|
|
454
769
|
constructor(entityName, baseDir = join(process.cwd(), "logs/data")) {
|
|
455
770
|
this.entityName = entityName;
|
|
456
771
|
this.baseDir = baseDir;
|
|
@@ -461,6 +776,12 @@ const PersistBase = makeExtendable(class {
|
|
|
461
776
|
});
|
|
462
777
|
this._directory = join(this.baseDir, this.entityName);
|
|
463
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* Computes file path for entity ID.
|
|
781
|
+
*
|
|
782
|
+
* @param entityId - Entity identifier
|
|
783
|
+
* @returns Full file path to entity JSON file
|
|
784
|
+
*/
|
|
464
785
|
_getFilePath(entityId) {
|
|
465
786
|
return join(this.baseDir, this.entityName, `${entityId}.json`);
|
|
466
787
|
}
|
|
@@ -471,6 +792,11 @@ const PersistBase = makeExtendable(class {
|
|
|
471
792
|
});
|
|
472
793
|
await this[BASE_WAIT_FOR_INIT_SYMBOL]();
|
|
473
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* Returns count of persisted entities.
|
|
797
|
+
*
|
|
798
|
+
* @returns Promise resolving to number of .json files in directory
|
|
799
|
+
*/
|
|
474
800
|
async getCount() {
|
|
475
801
|
const files = await fs.readdir(this._directory);
|
|
476
802
|
const { length } = files.filter((file) => file.endsWith(".json"));
|
|
@@ -524,6 +850,13 @@ const PersistBase = makeExtendable(class {
|
|
|
524
850
|
throw new Error(`Failed to write entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
|
|
525
851
|
}
|
|
526
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Removes entity from storage.
|
|
855
|
+
*
|
|
856
|
+
* @param entityId - Entity identifier to remove
|
|
857
|
+
* @returns Promise that resolves when entity is deleted
|
|
858
|
+
* @throws Error if entity not found or deletion fails
|
|
859
|
+
*/
|
|
527
860
|
async removeValue(entityId) {
|
|
528
861
|
backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_VALUE, {
|
|
529
862
|
entityName: this.entityName,
|
|
@@ -540,6 +873,12 @@ const PersistBase = makeExtendable(class {
|
|
|
540
873
|
throw new Error(`Failed to remove entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
|
|
541
874
|
}
|
|
542
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Removes all entities from storage.
|
|
878
|
+
*
|
|
879
|
+
* @returns Promise that resolves when all entities are deleted
|
|
880
|
+
* @throws Error if deletion fails
|
|
881
|
+
*/
|
|
543
882
|
async removeAll() {
|
|
544
883
|
backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_ALL, {
|
|
545
884
|
entityName: this.entityName,
|
|
@@ -555,6 +894,13 @@ const PersistBase = makeExtendable(class {
|
|
|
555
894
|
throw new Error(`Failed to remove values for ${this.entityName}: ${getErrorMessage(error)}`);
|
|
556
895
|
}
|
|
557
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* Async generator yielding all entity values.
|
|
899
|
+
* Sorted alphanumerically by entity ID.
|
|
900
|
+
*
|
|
901
|
+
* @returns AsyncGenerator yielding entities
|
|
902
|
+
* @throws Error if reading fails
|
|
903
|
+
*/
|
|
558
904
|
async *values() {
|
|
559
905
|
backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_VALUES, {
|
|
560
906
|
entityName: this.entityName,
|
|
@@ -577,6 +923,13 @@ const PersistBase = makeExtendable(class {
|
|
|
577
923
|
throw new Error(`Failed to read values for ${this.entityName}: ${getErrorMessage(error)}`);
|
|
578
924
|
}
|
|
579
925
|
}
|
|
926
|
+
/**
|
|
927
|
+
* Async generator yielding all entity IDs.
|
|
928
|
+
* Sorted alphanumerically.
|
|
929
|
+
*
|
|
930
|
+
* @returns AsyncGenerator yielding entity IDs
|
|
931
|
+
* @throws Error if reading fails
|
|
932
|
+
*/
|
|
580
933
|
async *keys() {
|
|
581
934
|
backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
|
|
582
935
|
entityName: this.entityName,
|
|
@@ -598,11 +951,23 @@ const PersistBase = makeExtendable(class {
|
|
|
598
951
|
throw new Error(`Failed to read keys for ${this.entityName}: ${getErrorMessage(error)}`);
|
|
599
952
|
}
|
|
600
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Async iterator implementation.
|
|
956
|
+
* Delegates to values() generator.
|
|
957
|
+
*
|
|
958
|
+
* @returns AsyncIterableIterator yielding entities
|
|
959
|
+
*/
|
|
601
960
|
async *[(_a = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
|
|
602
961
|
for await (const entity of this.values()) {
|
|
603
962
|
yield entity;
|
|
604
963
|
}
|
|
605
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Filters entities by predicate function.
|
|
967
|
+
*
|
|
968
|
+
* @param predicate - Filter function
|
|
969
|
+
* @returns AsyncGenerator yielding filtered entities
|
|
970
|
+
*/
|
|
606
971
|
async *filter(predicate) {
|
|
607
972
|
for await (const entity of this.values()) {
|
|
608
973
|
if (predicate(entity)) {
|
|
@@ -610,6 +975,13 @@ const PersistBase = makeExtendable(class {
|
|
|
610
975
|
}
|
|
611
976
|
}
|
|
612
977
|
}
|
|
978
|
+
/**
|
|
979
|
+
* Takes first N entities, optionally filtered.
|
|
980
|
+
*
|
|
981
|
+
* @param total - Maximum number of entities to yield
|
|
982
|
+
* @param predicate - Optional filter function
|
|
983
|
+
* @returns AsyncGenerator yielding up to total entities
|
|
984
|
+
*/
|
|
613
985
|
async *take(total, predicate) {
|
|
614
986
|
let count = 0;
|
|
615
987
|
if (predicate) {
|
|
@@ -635,6 +1007,17 @@ const PersistBase = makeExtendable(class {
|
|
|
635
1007
|
}
|
|
636
1008
|
}
|
|
637
1009
|
});
|
|
1010
|
+
/**
|
|
1011
|
+
* Utility class for managing signal persistence.
|
|
1012
|
+
*
|
|
1013
|
+
* Features:
|
|
1014
|
+
* - Memoized storage instances per strategy
|
|
1015
|
+
* - Custom adapter support
|
|
1016
|
+
* - Atomic read/write operations
|
|
1017
|
+
* - Crash-safe signal state management
|
|
1018
|
+
*
|
|
1019
|
+
* Used by ClientStrategy for live mode persistence.
|
|
1020
|
+
*/
|
|
638
1021
|
class PersistSignalUtils {
|
|
639
1022
|
constructor() {
|
|
640
1023
|
this.PersistSignalFactory = PersistBase;
|
|
@@ -642,6 +1025,16 @@ class PersistSignalUtils {
|
|
|
642
1025
|
strategyName,
|
|
643
1026
|
`./logs/data/signal/`,
|
|
644
1027
|
]));
|
|
1028
|
+
/**
|
|
1029
|
+
* Reads persisted signal data for a strategy and symbol.
|
|
1030
|
+
*
|
|
1031
|
+
* Called by ClientStrategy.waitForInit() to restore state.
|
|
1032
|
+
* Returns null if no signal exists.
|
|
1033
|
+
*
|
|
1034
|
+
* @param strategyName - Strategy identifier
|
|
1035
|
+
* @param symbol - Trading pair symbol
|
|
1036
|
+
* @returns Promise resolving to signal or null
|
|
1037
|
+
*/
|
|
645
1038
|
this.readSignalData = async (strategyName, symbol) => {
|
|
646
1039
|
backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
|
|
647
1040
|
const isInitial = !this.getSignalStorage.has(strategyName);
|
|
@@ -653,6 +1046,17 @@ class PersistSignalUtils {
|
|
|
653
1046
|
}
|
|
654
1047
|
return null;
|
|
655
1048
|
};
|
|
1049
|
+
/**
|
|
1050
|
+
* Writes signal data to disk with atomic file writes.
|
|
1051
|
+
*
|
|
1052
|
+
* Called by ClientStrategy.setPendingSignal() to persist state.
|
|
1053
|
+
* Uses atomic writes to prevent corruption on crashes.
|
|
1054
|
+
*
|
|
1055
|
+
* @param signalRow - Signal data (null to clear)
|
|
1056
|
+
* @param strategyName - Strategy identifier
|
|
1057
|
+
* @param symbol - Trading pair symbol
|
|
1058
|
+
* @returns Promise that resolves when write is complete
|
|
1059
|
+
*/
|
|
656
1060
|
this.writeSignalData = async (signalRow, strategyName, symbol) => {
|
|
657
1061
|
backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
|
|
658
1062
|
const isInitial = !this.getSignalStorage.has(strategyName);
|
|
@@ -661,11 +1065,41 @@ class PersistSignalUtils {
|
|
|
661
1065
|
await stateStorage.writeValue(symbol, { signalRow });
|
|
662
1066
|
};
|
|
663
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Registers a custom persistence adapter.
|
|
1070
|
+
*
|
|
1071
|
+
* @param Ctor - Custom PersistBase constructor
|
|
1072
|
+
*
|
|
1073
|
+
* @example
|
|
1074
|
+
* ```typescript
|
|
1075
|
+
* class RedisPersist extends PersistBase {
|
|
1076
|
+
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1077
|
+
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1078
|
+
* }
|
|
1079
|
+
* PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
|
|
1080
|
+
* ```
|
|
1081
|
+
*/
|
|
664
1082
|
usePersistSignalAdapter(Ctor) {
|
|
665
1083
|
backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
|
|
666
1084
|
this.PersistSignalFactory = Ctor;
|
|
667
1085
|
}
|
|
668
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Global singleton instance of PersistSignalUtils.
|
|
1089
|
+
* Used by ClientStrategy for signal persistence.
|
|
1090
|
+
*
|
|
1091
|
+
* @example
|
|
1092
|
+
* ```typescript
|
|
1093
|
+
* // Custom adapter
|
|
1094
|
+
* PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
|
|
1095
|
+
*
|
|
1096
|
+
* // Read signal
|
|
1097
|
+
* const signal = await PersistSignalAdaper.readSignalData("my-strategy", "BTCUSDT");
|
|
1098
|
+
*
|
|
1099
|
+
* // Write signal
|
|
1100
|
+
* await PersistSignalAdaper.writeSignalData(signal, "my-strategy", "BTCUSDT");
|
|
1101
|
+
* ```
|
|
1102
|
+
*/
|
|
669
1103
|
const PersistSignalAdaper = new PersistSignalUtils();
|
|
670
1104
|
|
|
671
1105
|
const INTERVAL_MINUTES$1 = {
|
|
@@ -676,6 +1110,48 @@ const INTERVAL_MINUTES$1 = {
|
|
|
676
1110
|
"30m": 30,
|
|
677
1111
|
"1h": 60,
|
|
678
1112
|
};
|
|
1113
|
+
const VALIDATE_SIGNAL_FN = (signal) => {
|
|
1114
|
+
const errors = [];
|
|
1115
|
+
// Валидация цен
|
|
1116
|
+
if (signal.priceOpen <= 0) {
|
|
1117
|
+
errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
|
|
1118
|
+
}
|
|
1119
|
+
if (signal.priceTakeProfit <= 0) {
|
|
1120
|
+
errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
|
|
1121
|
+
}
|
|
1122
|
+
if (signal.priceStopLoss <= 0) {
|
|
1123
|
+
errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
|
|
1124
|
+
}
|
|
1125
|
+
// Валидация для long позиции
|
|
1126
|
+
if (signal.position === "long") {
|
|
1127
|
+
if (signal.priceTakeProfit <= signal.priceOpen) {
|
|
1128
|
+
errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
|
|
1129
|
+
}
|
|
1130
|
+
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1131
|
+
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
// Валидация для short позиции
|
|
1135
|
+
if (signal.position === "short") {
|
|
1136
|
+
if (signal.priceTakeProfit >= signal.priceOpen) {
|
|
1137
|
+
errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
|
|
1138
|
+
}
|
|
1139
|
+
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1140
|
+
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
// Валидация временных параметров
|
|
1144
|
+
if (signal.minuteEstimatedTime <= 0) {
|
|
1145
|
+
errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
|
|
1146
|
+
}
|
|
1147
|
+
if (signal.timestamp <= 0) {
|
|
1148
|
+
errors.push(`timestamp must be positive, got ${signal.timestamp}`);
|
|
1149
|
+
}
|
|
1150
|
+
// Кидаем ошибку если есть проблемы
|
|
1151
|
+
if (errors.length > 0) {
|
|
1152
|
+
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
679
1155
|
const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
680
1156
|
const currentTime = self.params.execution.context.when.getTime();
|
|
681
1157
|
{
|
|
@@ -692,10 +1168,17 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
692
1168
|
if (!signal) {
|
|
693
1169
|
return null;
|
|
694
1170
|
}
|
|
695
|
-
|
|
1171
|
+
const signalRow = {
|
|
696
1172
|
id: randomString(),
|
|
697
1173
|
...signal,
|
|
1174
|
+
symbol: self.params.execution.context.symbol,
|
|
1175
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1176
|
+
strategyName: self.params.method.context.strategyName,
|
|
1177
|
+
timestamp: currentTime,
|
|
698
1178
|
};
|
|
1179
|
+
// Валидируем сигнал перед возвратом
|
|
1180
|
+
VALIDATE_SIGNAL_FN(signalRow);
|
|
1181
|
+
return signalRow;
|
|
699
1182
|
}, {
|
|
700
1183
|
defaultValue: null,
|
|
701
1184
|
});
|
|
@@ -714,15 +1197,70 @@ const WAIT_FOR_INIT_FN = async (self) => {
|
|
|
714
1197
|
if (self.params.execution.context.backtest) {
|
|
715
1198
|
return;
|
|
716
1199
|
}
|
|
717
|
-
|
|
1200
|
+
const pendingSignal = await PersistSignalAdaper.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1201
|
+
if (!pendingSignal) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
self._pendingSignal = pendingSignal;
|
|
718
1211
|
};
|
|
1212
|
+
/**
|
|
1213
|
+
* Client implementation for trading strategy lifecycle management.
|
|
1214
|
+
*
|
|
1215
|
+
* Features:
|
|
1216
|
+
* - Signal generation with interval throttling
|
|
1217
|
+
* - Automatic signal validation (prices, TP/SL logic, timestamps)
|
|
1218
|
+
* - Crash-safe persistence in live mode
|
|
1219
|
+
* - VWAP-based TP/SL monitoring
|
|
1220
|
+
* - Fast backtest with candle array processing
|
|
1221
|
+
*
|
|
1222
|
+
* All methods use prototype functions for memory efficiency.
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```typescript
|
|
1226
|
+
* const strategy = new ClientStrategy({
|
|
1227
|
+
* strategyName: "my-strategy",
|
|
1228
|
+
* interval: "5m",
|
|
1229
|
+
* getSignal: async (symbol) => ({ ... }),
|
|
1230
|
+
* execution: executionService,
|
|
1231
|
+
* exchange: exchangeService,
|
|
1232
|
+
* logger: loggerService,
|
|
1233
|
+
* });
|
|
1234
|
+
*
|
|
1235
|
+
* await strategy.waitForInit(); // Load persisted state
|
|
1236
|
+
* const result = await strategy.tick(); // Monitor signal
|
|
1237
|
+
* ```
|
|
1238
|
+
*/
|
|
719
1239
|
class ClientStrategy {
|
|
720
1240
|
constructor(params) {
|
|
721
1241
|
this.params = params;
|
|
722
1242
|
this._pendingSignal = null;
|
|
723
1243
|
this._lastSignalTimestamp = null;
|
|
1244
|
+
/**
|
|
1245
|
+
* Initializes strategy state by loading persisted signal from disk.
|
|
1246
|
+
*
|
|
1247
|
+
* Uses singleshot pattern to ensure initialization happens exactly once.
|
|
1248
|
+
* In backtest mode: skips persistence, no state to load
|
|
1249
|
+
* In live mode: reads last signal state from disk
|
|
1250
|
+
*
|
|
1251
|
+
* @returns Promise that resolves when initialization is complete
|
|
1252
|
+
*/
|
|
724
1253
|
this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN(this));
|
|
725
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Updates pending signal and persists to disk in live mode.
|
|
1257
|
+
*
|
|
1258
|
+
* Centralized method for all signal state changes.
|
|
1259
|
+
* Uses atomic file writes to prevent corruption.
|
|
1260
|
+
*
|
|
1261
|
+
* @param pendingSignal - New signal state (null to clear)
|
|
1262
|
+
* @returns Promise that resolves when update is complete
|
|
1263
|
+
*/
|
|
726
1264
|
async setPendingSignal(pendingSignal) {
|
|
727
1265
|
this.params.logger.debug("ClientStrategy setPendingSignal", {
|
|
728
1266
|
pendingSignal,
|
|
@@ -733,6 +1271,29 @@ class ClientStrategy {
|
|
|
733
1271
|
}
|
|
734
1272
|
await PersistSignalAdaper.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
735
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Performs a single tick of strategy execution.
|
|
1276
|
+
*
|
|
1277
|
+
* Flow:
|
|
1278
|
+
* 1. If no pending signal: call getSignal with throttling and validation
|
|
1279
|
+
* 2. If signal opened: trigger onOpen callback, persist state
|
|
1280
|
+
* 3. If pending signal exists: check VWAP against TP/SL
|
|
1281
|
+
* 4. If TP/SL/time reached: close signal, trigger onClose, persist state
|
|
1282
|
+
*
|
|
1283
|
+
* @returns Promise resolving to discriminated union result:
|
|
1284
|
+
* - idle: No signal generated
|
|
1285
|
+
* - opened: New signal just created
|
|
1286
|
+
* - active: Signal monitoring in progress
|
|
1287
|
+
* - closed: Signal completed with PNL
|
|
1288
|
+
*
|
|
1289
|
+
* @example
|
|
1290
|
+
* ```typescript
|
|
1291
|
+
* const result = await strategy.tick();
|
|
1292
|
+
* if (result.action === "closed") {
|
|
1293
|
+
* console.log(`PNL: ${result.pnl.pnlPercentage}%`);
|
|
1294
|
+
* }
|
|
1295
|
+
* ```
|
|
1296
|
+
*/
|
|
736
1297
|
async tick() {
|
|
737
1298
|
this.params.logger.debug("ClientStrategy tick");
|
|
738
1299
|
if (!this._pendingSignal) {
|
|
@@ -740,17 +1301,35 @@ class ClientStrategy {
|
|
|
740
1301
|
await this.setPendingSignal(pendingSignal);
|
|
741
1302
|
if (this._pendingSignal) {
|
|
742
1303
|
if (this.params.callbacks?.onOpen) {
|
|
743
|
-
this.params.callbacks.onOpen(this.params.execution.context.
|
|
1304
|
+
this.params.callbacks.onOpen(this.params.execution.context.symbol, this._pendingSignal, this._pendingSignal.priceOpen, this.params.execution.context.backtest);
|
|
744
1305
|
}
|
|
745
|
-
|
|
1306
|
+
const result = {
|
|
746
1307
|
action: "opened",
|
|
747
1308
|
signal: this._pendingSignal,
|
|
1309
|
+
strategyName: this.params.method.context.strategyName,
|
|
1310
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
1311
|
+
currentPrice: this._pendingSignal.priceOpen,
|
|
748
1312
|
};
|
|
1313
|
+
if (this.params.callbacks?.onTick) {
|
|
1314
|
+
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1315
|
+
}
|
|
1316
|
+
return result;
|
|
749
1317
|
}
|
|
750
|
-
|
|
1318
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
1319
|
+
if (this.params.callbacks?.onIdle) {
|
|
1320
|
+
this.params.callbacks.onIdle(this.params.execution.context.symbol, currentPrice, this.params.execution.context.backtest);
|
|
1321
|
+
}
|
|
1322
|
+
const result = {
|
|
751
1323
|
action: "idle",
|
|
752
1324
|
signal: null,
|
|
1325
|
+
strategyName: this.params.method.context.strategyName,
|
|
1326
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
1327
|
+
currentPrice,
|
|
753
1328
|
};
|
|
1329
|
+
if (this.params.callbacks?.onTick) {
|
|
1330
|
+
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1331
|
+
}
|
|
1332
|
+
return result;
|
|
754
1333
|
}
|
|
755
1334
|
const when = this.params.execution.context.when;
|
|
756
1335
|
const signal = this._pendingSignal;
|
|
@@ -796,6 +1375,14 @@ class ClientStrategy {
|
|
|
796
1375
|
if (shouldClose) {
|
|
797
1376
|
const pnl = toProfitLossDto(signal, averagePrice);
|
|
798
1377
|
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
1378
|
+
// Предупреждение о закрытии сигнала в убыток
|
|
1379
|
+
if (closeReason === "stop_loss") {
|
|
1380
|
+
this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1381
|
+
}
|
|
1382
|
+
// Предупреждение о закрытии сигнала в убыток
|
|
1383
|
+
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
1384
|
+
this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1385
|
+
}
|
|
799
1386
|
this.params.logger.debug("ClientStrategy closing", {
|
|
800
1387
|
symbol: this.params.execution.context.symbol,
|
|
801
1388
|
signalId: signal.id,
|
|
@@ -805,24 +1392,58 @@ class ClientStrategy {
|
|
|
805
1392
|
pnlPercentage: pnl.pnlPercentage,
|
|
806
1393
|
});
|
|
807
1394
|
if (this.params.callbacks?.onClose) {
|
|
808
|
-
this.params.callbacks.onClose(this.params.execution.context.
|
|
1395
|
+
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
809
1396
|
}
|
|
810
1397
|
await this.setPendingSignal(null);
|
|
811
|
-
|
|
1398
|
+
const result = {
|
|
812
1399
|
action: "closed",
|
|
813
1400
|
signal: signal,
|
|
814
1401
|
currentPrice: averagePrice,
|
|
815
1402
|
closeReason: closeReason,
|
|
816
1403
|
closeTimestamp: closeTimestamp,
|
|
817
1404
|
pnl: pnl,
|
|
1405
|
+
strategyName: this.params.method.context.strategyName,
|
|
1406
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
818
1407
|
};
|
|
1408
|
+
if (this.params.callbacks?.onTick) {
|
|
1409
|
+
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1410
|
+
}
|
|
1411
|
+
return result;
|
|
1412
|
+
}
|
|
1413
|
+
if (this.params.callbacks?.onActive) {
|
|
1414
|
+
this.params.callbacks.onActive(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
819
1415
|
}
|
|
820
|
-
|
|
1416
|
+
const result = {
|
|
821
1417
|
action: "active",
|
|
822
1418
|
signal: signal,
|
|
823
1419
|
currentPrice: averagePrice,
|
|
1420
|
+
strategyName: this.params.method.context.strategyName,
|
|
1421
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
824
1422
|
};
|
|
1423
|
+
if (this.params.callbacks?.onTick) {
|
|
1424
|
+
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1425
|
+
}
|
|
1426
|
+
return result;
|
|
825
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Fast backtests a pending signal using historical candle data.
|
|
1430
|
+
*
|
|
1431
|
+
* Iterates through candles checking VWAP against TP/SL on each timeframe.
|
|
1432
|
+
* Starts from index 4 (needs 5 candles for VWAP calculation).
|
|
1433
|
+
* Always returns closed result (either TP/SL or time_expired).
|
|
1434
|
+
*
|
|
1435
|
+
* @param candles - Array of candles covering signal's minuteEstimatedTime
|
|
1436
|
+
* @returns Promise resolving to closed signal result with PNL
|
|
1437
|
+
* @throws Error if no pending signal or not in backtest mode
|
|
1438
|
+
*
|
|
1439
|
+
* @example
|
|
1440
|
+
* ```typescript
|
|
1441
|
+
* // After signal opened in backtest
|
|
1442
|
+
* const candles = await exchange.getNextCandles("BTCUSDT", "1m", signal.minuteEstimatedTime);
|
|
1443
|
+
* const result = await strategy.backtest(candles);
|
|
1444
|
+
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired"
|
|
1445
|
+
* ```
|
|
1446
|
+
*/
|
|
826
1447
|
async backtest(candles) {
|
|
827
1448
|
this.params.logger.debug("ClientStrategy backtest", {
|
|
828
1449
|
symbol: this.params.execution.context.symbol,
|
|
@@ -835,6 +1456,10 @@ class ClientStrategy {
|
|
|
835
1456
|
if (!this.params.execution.context.backtest) {
|
|
836
1457
|
throw new Error("ClientStrategy backtest: running in live context");
|
|
837
1458
|
}
|
|
1459
|
+
// Предупреждение если недостаточно свечей для VWAP
|
|
1460
|
+
if (candles.length < 5) {
|
|
1461
|
+
this.params.logger.warn(`ClientStrategy backtest: Expected at least 5 candles for VWAP, got ${candles.length}`);
|
|
1462
|
+
}
|
|
838
1463
|
// Проверяем каждую свечу на достижение TP/SL
|
|
839
1464
|
// Начинаем с индекса 4 (пятая свеча), чтобы было минимум 5 свечей для VWAP
|
|
840
1465
|
for (let i = 4; i < candles.length; i++) {
|
|
@@ -877,18 +1502,28 @@ class ClientStrategy {
|
|
|
877
1502
|
closeTimestamp,
|
|
878
1503
|
pnlPercentage: pnl.pnlPercentage,
|
|
879
1504
|
});
|
|
1505
|
+
// Предупреждение при убытке от stop_loss
|
|
1506
|
+
if (closeReason === "stop_loss") {
|
|
1507
|
+
this.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1508
|
+
}
|
|
880
1509
|
if (this.params.callbacks?.onClose) {
|
|
881
|
-
this.params.callbacks.onClose(this.params.execution.context.
|
|
1510
|
+
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
882
1511
|
}
|
|
883
1512
|
await this.setPendingSignal(null);
|
|
884
|
-
|
|
1513
|
+
const result = {
|
|
885
1514
|
action: "closed",
|
|
886
1515
|
signal: signal,
|
|
887
1516
|
currentPrice: averagePrice,
|
|
888
1517
|
closeReason: closeReason,
|
|
889
1518
|
closeTimestamp: closeTimestamp,
|
|
890
1519
|
pnl: pnl,
|
|
1520
|
+
strategyName: this.params.method.context.strategyName,
|
|
1521
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
891
1522
|
};
|
|
1523
|
+
if (this.params.callbacks?.onTick) {
|
|
1524
|
+
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1525
|
+
}
|
|
1526
|
+
return result;
|
|
892
1527
|
}
|
|
893
1528
|
}
|
|
894
1529
|
// Если TP/SL не достигнут за период, вычисляем VWAP из последних 5 свечей
|
|
@@ -903,21 +1538,73 @@ class ClientStrategy {
|
|
|
903
1538
|
closeTimestamp,
|
|
904
1539
|
pnlPercentage: pnl.pnlPercentage,
|
|
905
1540
|
});
|
|
1541
|
+
// Предупреждение при убытке от time_expired
|
|
1542
|
+
if (pnl.pnlPercentage < 0) {
|
|
1543
|
+
this.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1544
|
+
}
|
|
906
1545
|
if (this.params.callbacks?.onClose) {
|
|
907
|
-
this.params.callbacks.onClose(this.params.execution.context.
|
|
1546
|
+
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, lastPrice, this.params.execution.context.backtest);
|
|
908
1547
|
}
|
|
909
1548
|
await this.setPendingSignal(null);
|
|
910
|
-
|
|
1549
|
+
const result = {
|
|
911
1550
|
action: "closed",
|
|
912
1551
|
signal: signal,
|
|
913
1552
|
currentPrice: lastPrice,
|
|
914
1553
|
closeReason: "time_expired",
|
|
915
1554
|
closeTimestamp: closeTimestamp,
|
|
916
1555
|
pnl: pnl,
|
|
1556
|
+
strategyName: this.params.method.context.strategyName,
|
|
1557
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
917
1558
|
};
|
|
1559
|
+
if (this.params.callbacks?.onTick) {
|
|
1560
|
+
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1561
|
+
}
|
|
1562
|
+
return result;
|
|
918
1563
|
}
|
|
919
1564
|
}
|
|
920
1565
|
|
|
1566
|
+
/**
|
|
1567
|
+
* Global signal emitter for all trading events (live + backtest).
|
|
1568
|
+
* Emits all signal events regardless of execution mode.
|
|
1569
|
+
*/
|
|
1570
|
+
const signalEmitter = new Subject();
|
|
1571
|
+
/**
|
|
1572
|
+
* Live trading signal emitter.
|
|
1573
|
+
* Emits only signals from live trading execution.
|
|
1574
|
+
*/
|
|
1575
|
+
const signalLiveEmitter = new Subject();
|
|
1576
|
+
/**
|
|
1577
|
+
* Backtest signal emitter.
|
|
1578
|
+
* Emits only signals from backtest execution.
|
|
1579
|
+
*/
|
|
1580
|
+
const signalBacktestEmitter = new Subject();
|
|
1581
|
+
/**
|
|
1582
|
+
* Error emitter for background execution errors.
|
|
1583
|
+
* Emits errors caught in background tasks (Live.background, Backtest.background).
|
|
1584
|
+
*/
|
|
1585
|
+
const errorEmitter = new Subject();
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Connection service routing strategy operations to correct ClientStrategy instance.
|
|
1589
|
+
*
|
|
1590
|
+
* Routes all IStrategy method calls to the appropriate strategy implementation
|
|
1591
|
+
* based on methodContextService.context.strategyName. Uses memoization to cache
|
|
1592
|
+
* ClientStrategy instances for performance.
|
|
1593
|
+
*
|
|
1594
|
+
* Key features:
|
|
1595
|
+
* - Automatic strategy routing via method context
|
|
1596
|
+
* - Memoized ClientStrategy instances by strategyName
|
|
1597
|
+
* - Implements IStrategy interface
|
|
1598
|
+
* - Ensures initialization with waitForInit() before operations
|
|
1599
|
+
* - Handles both tick() (live) and backtest() operations
|
|
1600
|
+
*
|
|
1601
|
+
* @example
|
|
1602
|
+
* ```typescript
|
|
1603
|
+
* // Used internally by framework
|
|
1604
|
+
* const result = await strategyConnectionService.tick();
|
|
1605
|
+
* // Automatically routes to correct strategy based on methodContext
|
|
1606
|
+
* ```
|
|
1607
|
+
*/
|
|
921
1608
|
class StrategyConnectionService {
|
|
922
1609
|
constructor() {
|
|
923
1610
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -925,11 +1612,21 @@ class StrategyConnectionService {
|
|
|
925
1612
|
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
926
1613
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
927
1614
|
this.methodContextService = inject(TYPES.methodContextService);
|
|
928
|
-
|
|
1615
|
+
/**
|
|
1616
|
+
* Retrieves memoized ClientStrategy instance for given strategy name.
|
|
1617
|
+
*
|
|
1618
|
+
* Creates ClientStrategy on first call, returns cached instance on subsequent calls.
|
|
1619
|
+
* Cache key is strategyName string.
|
|
1620
|
+
*
|
|
1621
|
+
* @param strategyName - Name of registered strategy schema
|
|
1622
|
+
* @returns Configured ClientStrategy instance
|
|
1623
|
+
*/
|
|
1624
|
+
this.getStrategy = memoize(([strategyName]) => `${strategyName}`, (strategyName) => {
|
|
929
1625
|
const { getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
|
|
930
1626
|
return new ClientStrategy({
|
|
931
1627
|
interval,
|
|
932
1628
|
execution: this.executionContextService,
|
|
1629
|
+
method: this.methodContextService,
|
|
933
1630
|
logger: this.loggerService,
|
|
934
1631
|
exchange: this.exchangeConnectionService,
|
|
935
1632
|
strategyName,
|
|
@@ -937,21 +1634,61 @@ class StrategyConnectionService {
|
|
|
937
1634
|
callbacks,
|
|
938
1635
|
});
|
|
939
1636
|
});
|
|
1637
|
+
/**
|
|
1638
|
+
* Executes live trading tick for current strategy.
|
|
1639
|
+
*
|
|
1640
|
+
* Waits for strategy initialization before processing tick.
|
|
1641
|
+
* Evaluates current market conditions and returns signal state.
|
|
1642
|
+
*
|
|
1643
|
+
* @returns Promise resolving to tick result (idle, opened, active, closed)
|
|
1644
|
+
*/
|
|
940
1645
|
this.tick = async () => {
|
|
941
1646
|
this.loggerService.log("strategyConnectionService tick");
|
|
942
1647
|
const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
|
|
943
1648
|
await strategy.waitForInit();
|
|
944
|
-
|
|
1649
|
+
const tick = await strategy.tick();
|
|
1650
|
+
{
|
|
1651
|
+
if (this.executionContextService.context.backtest) {
|
|
1652
|
+
signalBacktestEmitter.next(tick);
|
|
1653
|
+
}
|
|
1654
|
+
if (!this.executionContextService.context.backtest) {
|
|
1655
|
+
signalLiveEmitter.next(tick);
|
|
1656
|
+
}
|
|
1657
|
+
signalEmitter.next(tick);
|
|
1658
|
+
}
|
|
1659
|
+
return tick;
|
|
945
1660
|
};
|
|
1661
|
+
/**
|
|
1662
|
+
* Executes backtest for current strategy with provided candles.
|
|
1663
|
+
*
|
|
1664
|
+
* Waits for strategy initialization before processing candles.
|
|
1665
|
+
* Evaluates strategy signals against historical data.
|
|
1666
|
+
*
|
|
1667
|
+
* @param candles - Array of historical candle data to backtest
|
|
1668
|
+
* @returns Promise resolving to backtest result (signal or idle)
|
|
1669
|
+
*/
|
|
946
1670
|
this.backtest = async (candles) => {
|
|
947
|
-
this.loggerService.log("strategyConnectionService backtest"
|
|
1671
|
+
this.loggerService.log("strategyConnectionService backtest", {
|
|
1672
|
+
candleCount: candles.length,
|
|
1673
|
+
});
|
|
948
1674
|
const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
|
|
949
1675
|
await strategy.waitForInit();
|
|
950
|
-
|
|
1676
|
+
const tick = await strategy.backtest(candles);
|
|
1677
|
+
{
|
|
1678
|
+
if (this.executionContextService.context.backtest) {
|
|
1679
|
+
signalBacktestEmitter.next(tick);
|
|
1680
|
+
}
|
|
1681
|
+
signalEmitter.next(tick);
|
|
1682
|
+
}
|
|
1683
|
+
return tick;
|
|
951
1684
|
};
|
|
952
1685
|
}
|
|
953
1686
|
}
|
|
954
1687
|
|
|
1688
|
+
/**
|
|
1689
|
+
* Maps FrameInterval to minutes for timestamp calculation.
|
|
1690
|
+
* Used to generate timeframe arrays with proper spacing.
|
|
1691
|
+
*/
|
|
955
1692
|
const INTERVAL_MINUTES = {
|
|
956
1693
|
"1m": 1,
|
|
957
1694
|
"3m": 3,
|
|
@@ -967,6 +1704,15 @@ const INTERVAL_MINUTES = {
|
|
|
967
1704
|
"1d": 1440,
|
|
968
1705
|
"3d": 4320,
|
|
969
1706
|
};
|
|
1707
|
+
/**
|
|
1708
|
+
* Generates timeframe array from startDate to endDate with specified interval.
|
|
1709
|
+
* Uses prototype function pattern for memory efficiency.
|
|
1710
|
+
*
|
|
1711
|
+
* @param symbol - Trading pair symbol (unused, for API consistency)
|
|
1712
|
+
* @param self - ClientFrame instance reference
|
|
1713
|
+
* @returns Array of Date objects representing tick timestamps
|
|
1714
|
+
* @throws Error if interval is unknown
|
|
1715
|
+
*/
|
|
970
1716
|
const GET_TIMEFRAME_FN = async (symbol, self) => {
|
|
971
1717
|
self.params.logger.debug("ClientFrame getTimeframe", {
|
|
972
1718
|
symbol,
|
|
@@ -987,19 +1733,69 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
|
|
|
987
1733
|
}
|
|
988
1734
|
return timeframes;
|
|
989
1735
|
};
|
|
1736
|
+
/**
|
|
1737
|
+
* Client implementation for backtest timeframe generation.
|
|
1738
|
+
*
|
|
1739
|
+
* Features:
|
|
1740
|
+
* - Generates timestamp arrays for backtest iteration
|
|
1741
|
+
* - Singleshot caching prevents redundant generation
|
|
1742
|
+
* - Configurable interval spacing (1m to 3d)
|
|
1743
|
+
* - Callback support for validation and logging
|
|
1744
|
+
*
|
|
1745
|
+
* Used by BacktestLogicPrivateService to iterate through historical periods.
|
|
1746
|
+
*/
|
|
990
1747
|
class ClientFrame {
|
|
991
1748
|
constructor(params) {
|
|
992
1749
|
this.params = params;
|
|
1750
|
+
/**
|
|
1751
|
+
* Generates timeframe array for backtest period.
|
|
1752
|
+
* Results are cached via singleshot pattern.
|
|
1753
|
+
*
|
|
1754
|
+
* @param symbol - Trading pair symbol (unused, for API consistency)
|
|
1755
|
+
* @returns Promise resolving to array of Date objects
|
|
1756
|
+
* @throws Error if interval is invalid
|
|
1757
|
+
*/
|
|
993
1758
|
this.getTimeframe = singleshot(async (symbol) => await GET_TIMEFRAME_FN(symbol, this));
|
|
994
1759
|
}
|
|
995
1760
|
}
|
|
996
1761
|
|
|
1762
|
+
/**
|
|
1763
|
+
* Connection service routing frame operations to correct ClientFrame instance.
|
|
1764
|
+
*
|
|
1765
|
+
* Routes all IFrame method calls to the appropriate frame implementation
|
|
1766
|
+
* based on methodContextService.context.frameName. Uses memoization to cache
|
|
1767
|
+
* ClientFrame instances for performance.
|
|
1768
|
+
*
|
|
1769
|
+
* Key features:
|
|
1770
|
+
* - Automatic frame routing via method context
|
|
1771
|
+
* - Memoized ClientFrame instances by frameName
|
|
1772
|
+
* - Implements IFrame interface
|
|
1773
|
+
* - Backtest timeframe management (startDate, endDate, interval)
|
|
1774
|
+
*
|
|
1775
|
+
* Note: frameName is empty string for live mode (no frame constraints).
|
|
1776
|
+
*
|
|
1777
|
+
* @example
|
|
1778
|
+
* ```typescript
|
|
1779
|
+
* // Used internally by framework
|
|
1780
|
+
* const timeframe = await frameConnectionService.getTimeframe("BTCUSDT");
|
|
1781
|
+
* // Automatically routes to correct frame based on methodContext
|
|
1782
|
+
* ```
|
|
1783
|
+
*/
|
|
997
1784
|
class FrameConnectionService {
|
|
998
1785
|
constructor() {
|
|
999
1786
|
this.loggerService = inject(TYPES.loggerService);
|
|
1000
1787
|
this.frameSchemaService = inject(TYPES.frameSchemaService);
|
|
1001
1788
|
this.methodContextService = inject(TYPES.methodContextService);
|
|
1002
|
-
|
|
1789
|
+
/**
|
|
1790
|
+
* Retrieves memoized ClientFrame instance for given frame name.
|
|
1791
|
+
*
|
|
1792
|
+
* Creates ClientFrame on first call, returns cached instance on subsequent calls.
|
|
1793
|
+
* Cache key is frameName string.
|
|
1794
|
+
*
|
|
1795
|
+
* @param frameName - Name of registered frame schema
|
|
1796
|
+
* @returns Configured ClientFrame instance
|
|
1797
|
+
*/
|
|
1798
|
+
this.getFrame = memoize(([frameName]) => `${frameName}`, (frameName) => {
|
|
1003
1799
|
const { endDate, interval, startDate, callbacks } = this.frameSchemaService.get(frameName);
|
|
1004
1800
|
return new ClientFrame({
|
|
1005
1801
|
frameName,
|
|
@@ -1010,6 +1806,15 @@ class FrameConnectionService {
|
|
|
1010
1806
|
callbacks,
|
|
1011
1807
|
});
|
|
1012
1808
|
});
|
|
1809
|
+
/**
|
|
1810
|
+
* Retrieves backtest timeframe boundaries for symbol.
|
|
1811
|
+
*
|
|
1812
|
+
* Returns startDate and endDate from frame configuration.
|
|
1813
|
+
* Used to limit backtest execution to specific date range.
|
|
1814
|
+
*
|
|
1815
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
1816
|
+
* @returns Promise resolving to { startDate: Date, endDate: Date }
|
|
1817
|
+
*/
|
|
1013
1818
|
this.getTimeframe = async (symbol) => {
|
|
1014
1819
|
this.loggerService.log("frameConnectionService getTimeframe", {
|
|
1015
1820
|
symbol,
|
|
@@ -1019,10 +1824,28 @@ class FrameConnectionService {
|
|
|
1019
1824
|
}
|
|
1020
1825
|
}
|
|
1021
1826
|
|
|
1827
|
+
/**
|
|
1828
|
+
* Global service for exchange operations with execution context injection.
|
|
1829
|
+
*
|
|
1830
|
+
* Wraps ExchangeConnectionService with ExecutionContextService to inject
|
|
1831
|
+
* symbol, when, and backtest parameters into the execution context.
|
|
1832
|
+
*
|
|
1833
|
+
* Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
|
|
1834
|
+
*/
|
|
1022
1835
|
class ExchangeGlobalService {
|
|
1023
1836
|
constructor() {
|
|
1024
1837
|
this.loggerService = inject(TYPES.loggerService);
|
|
1025
1838
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
1839
|
+
/**
|
|
1840
|
+
* Fetches historical candles with execution context.
|
|
1841
|
+
*
|
|
1842
|
+
* @param symbol - Trading pair symbol
|
|
1843
|
+
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
1844
|
+
* @param limit - Maximum number of candles to fetch
|
|
1845
|
+
* @param when - Timestamp for context (used in backtest mode)
|
|
1846
|
+
* @param backtest - Whether running in backtest mode
|
|
1847
|
+
* @returns Promise resolving to array of candles
|
|
1848
|
+
*/
|
|
1026
1849
|
this.getCandles = async (symbol, interval, limit, when, backtest) => {
|
|
1027
1850
|
this.loggerService.log("exchangeGlobalService getCandles", {
|
|
1028
1851
|
symbol,
|
|
@@ -1039,6 +1862,16 @@ class ExchangeGlobalService {
|
|
|
1039
1862
|
backtest,
|
|
1040
1863
|
});
|
|
1041
1864
|
};
|
|
1865
|
+
/**
|
|
1866
|
+
* Fetches future candles (backtest mode only) with execution context.
|
|
1867
|
+
*
|
|
1868
|
+
* @param symbol - Trading pair symbol
|
|
1869
|
+
* @param interval - Candle interval
|
|
1870
|
+
* @param limit - Maximum number of candles to fetch
|
|
1871
|
+
* @param when - Timestamp for context
|
|
1872
|
+
* @param backtest - Whether running in backtest mode (must be true)
|
|
1873
|
+
* @returns Promise resolving to array of future candles
|
|
1874
|
+
*/
|
|
1042
1875
|
this.getNextCandles = async (symbol, interval, limit, when, backtest) => {
|
|
1043
1876
|
this.loggerService.log("exchangeGlobalService getNextCandles", {
|
|
1044
1877
|
symbol,
|
|
@@ -1055,6 +1888,14 @@ class ExchangeGlobalService {
|
|
|
1055
1888
|
backtest,
|
|
1056
1889
|
});
|
|
1057
1890
|
};
|
|
1891
|
+
/**
|
|
1892
|
+
* Calculates VWAP with execution context.
|
|
1893
|
+
*
|
|
1894
|
+
* @param symbol - Trading pair symbol
|
|
1895
|
+
* @param when - Timestamp for context
|
|
1896
|
+
* @param backtest - Whether running in backtest mode
|
|
1897
|
+
* @returns Promise resolving to VWAP price
|
|
1898
|
+
*/
|
|
1058
1899
|
this.getAveragePrice = async (symbol, when, backtest) => {
|
|
1059
1900
|
this.loggerService.log("exchangeGlobalService getAveragePrice", {
|
|
1060
1901
|
symbol,
|
|
@@ -1069,6 +1910,15 @@ class ExchangeGlobalService {
|
|
|
1069
1910
|
backtest,
|
|
1070
1911
|
});
|
|
1071
1912
|
};
|
|
1913
|
+
/**
|
|
1914
|
+
* Formats price with execution context.
|
|
1915
|
+
*
|
|
1916
|
+
* @param symbol - Trading pair symbol
|
|
1917
|
+
* @param price - Price to format
|
|
1918
|
+
* @param when - Timestamp for context
|
|
1919
|
+
* @param backtest - Whether running in backtest mode
|
|
1920
|
+
* @returns Promise resolving to formatted price string
|
|
1921
|
+
*/
|
|
1072
1922
|
this.formatPrice = async (symbol, price, when, backtest) => {
|
|
1073
1923
|
this.loggerService.log("exchangeGlobalService formatPrice", {
|
|
1074
1924
|
symbol,
|
|
@@ -1084,6 +1934,15 @@ class ExchangeGlobalService {
|
|
|
1084
1934
|
backtest,
|
|
1085
1935
|
});
|
|
1086
1936
|
};
|
|
1937
|
+
/**
|
|
1938
|
+
* Formats quantity with execution context.
|
|
1939
|
+
*
|
|
1940
|
+
* @param symbol - Trading pair symbol
|
|
1941
|
+
* @param quantity - Quantity to format
|
|
1942
|
+
* @param when - Timestamp for context
|
|
1943
|
+
* @param backtest - Whether running in backtest mode
|
|
1944
|
+
* @returns Promise resolving to formatted quantity string
|
|
1945
|
+
*/
|
|
1087
1946
|
this.formatQuantity = async (symbol, quantity, when, backtest) => {
|
|
1088
1947
|
this.loggerService.log("exchangeGlobalService formatQuantity", {
|
|
1089
1948
|
symbol,
|
|
@@ -1102,10 +1961,29 @@ class ExchangeGlobalService {
|
|
|
1102
1961
|
}
|
|
1103
1962
|
}
|
|
1104
1963
|
|
|
1964
|
+
/**
|
|
1965
|
+
* Global service for strategy operations with execution context injection.
|
|
1966
|
+
*
|
|
1967
|
+
* Wraps StrategyConnectionService with ExecutionContextService to inject
|
|
1968
|
+
* symbol, when, and backtest parameters into the execution context.
|
|
1969
|
+
*
|
|
1970
|
+
* Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
|
|
1971
|
+
*/
|
|
1105
1972
|
class StrategyGlobalService {
|
|
1106
1973
|
constructor() {
|
|
1107
1974
|
this.loggerService = inject(TYPES.loggerService);
|
|
1108
1975
|
this.strategyConnectionService = inject(TYPES.strategyConnectionService);
|
|
1976
|
+
/**
|
|
1977
|
+
* Checks signal status at a specific timestamp.
|
|
1978
|
+
*
|
|
1979
|
+
* Wraps strategy tick() with execution context containing symbol, timestamp,
|
|
1980
|
+
* and backtest mode flag.
|
|
1981
|
+
*
|
|
1982
|
+
* @param symbol - Trading pair symbol
|
|
1983
|
+
* @param when - Timestamp for tick evaluation
|
|
1984
|
+
* @param backtest - Whether running in backtest mode
|
|
1985
|
+
* @returns Discriminated union of tick result (idle, opened, active, closed)
|
|
1986
|
+
*/
|
|
1109
1987
|
this.tick = async (symbol, when, backtest) => {
|
|
1110
1988
|
this.loggerService.log("strategyGlobalService tick", {
|
|
1111
1989
|
symbol,
|
|
@@ -1120,9 +1998,22 @@ class StrategyGlobalService {
|
|
|
1120
1998
|
backtest,
|
|
1121
1999
|
});
|
|
1122
2000
|
};
|
|
2001
|
+
/**
|
|
2002
|
+
* Runs fast backtest against candle array.
|
|
2003
|
+
*
|
|
2004
|
+
* Wraps strategy backtest() with execution context containing symbol,
|
|
2005
|
+
* timestamp, and backtest mode flag.
|
|
2006
|
+
*
|
|
2007
|
+
* @param symbol - Trading pair symbol
|
|
2008
|
+
* @param candles - Array of historical candles to test against
|
|
2009
|
+
* @param when - Starting timestamp for backtest
|
|
2010
|
+
* @param backtest - Whether running in backtest mode (typically true)
|
|
2011
|
+
* @returns Closed signal result with PNL
|
|
2012
|
+
*/
|
|
1123
2013
|
this.backtest = async (symbol, candles, when, backtest) => {
|
|
1124
2014
|
this.loggerService.log("strategyGlobalService backtest", {
|
|
1125
2015
|
symbol,
|
|
2016
|
+
candleCount: candles.length,
|
|
1126
2017
|
when,
|
|
1127
2018
|
backtest,
|
|
1128
2019
|
});
|
|
@@ -1137,10 +2028,22 @@ class StrategyGlobalService {
|
|
|
1137
2028
|
}
|
|
1138
2029
|
}
|
|
1139
2030
|
|
|
2031
|
+
/**
|
|
2032
|
+
* Global service for frame operations.
|
|
2033
|
+
*
|
|
2034
|
+
* Wraps FrameConnectionService for timeframe generation.
|
|
2035
|
+
* Used internally by BacktestLogicPrivateService.
|
|
2036
|
+
*/
|
|
1140
2037
|
class FrameGlobalService {
|
|
1141
2038
|
constructor() {
|
|
1142
2039
|
this.loggerService = inject(TYPES.loggerService);
|
|
1143
2040
|
this.frameConnectionService = inject(TYPES.frameConnectionService);
|
|
2041
|
+
/**
|
|
2042
|
+
* Generates timeframe array for backtest iteration.
|
|
2043
|
+
*
|
|
2044
|
+
* @param symbol - Trading pair symbol
|
|
2045
|
+
* @returns Promise resolving to array of Date objects
|
|
2046
|
+
*/
|
|
1144
2047
|
this.getTimeframe = async (symbol) => {
|
|
1145
2048
|
this.loggerService.log("frameGlobalService getTimeframe", {
|
|
1146
2049
|
symbol,
|
|
@@ -1150,61 +2053,247 @@ class FrameGlobalService {
|
|
|
1150
2053
|
}
|
|
1151
2054
|
}
|
|
1152
2055
|
|
|
2056
|
+
/**
|
|
2057
|
+
* Service for managing exchange schema registry.
|
|
2058
|
+
*
|
|
2059
|
+
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
2060
|
+
* Exchanges are registered via addExchange() and retrieved by name.
|
|
2061
|
+
*/
|
|
1153
2062
|
class ExchangeSchemaService {
|
|
1154
2063
|
constructor() {
|
|
1155
2064
|
this.loggerService = inject(TYPES.loggerService);
|
|
1156
2065
|
this._registry = new ToolRegistry("exchangeSchema");
|
|
2066
|
+
/**
|
|
2067
|
+
* Registers a new exchange schema.
|
|
2068
|
+
*
|
|
2069
|
+
* @param key - Unique exchange name
|
|
2070
|
+
* @param value - Exchange schema configuration
|
|
2071
|
+
* @throws Error if exchange name already exists
|
|
2072
|
+
*/
|
|
1157
2073
|
this.register = (key, value) => {
|
|
1158
|
-
this.loggerService.
|
|
2074
|
+
this.loggerService.log(`exchangeSchemaService register`, { key });
|
|
2075
|
+
this.validateShallow(value);
|
|
1159
2076
|
this._registry = this._registry.register(key, value);
|
|
1160
2077
|
};
|
|
2078
|
+
/**
|
|
2079
|
+
* Validates exchange schema structure for required properties.
|
|
2080
|
+
*
|
|
2081
|
+
* Performs shallow validation to ensure all required properties exist
|
|
2082
|
+
* and have correct types before registration in the registry.
|
|
2083
|
+
*
|
|
2084
|
+
* @param exchangeSchema - Exchange schema to validate
|
|
2085
|
+
* @throws Error if exchangeName is missing or not a string
|
|
2086
|
+
* @throws Error if getCandles is missing or not a function
|
|
2087
|
+
* @throws Error if formatPrice is missing or not a function
|
|
2088
|
+
* @throws Error if formatQuantity is missing or not a function
|
|
2089
|
+
*/
|
|
2090
|
+
this.validateShallow = (exchangeSchema) => {
|
|
2091
|
+
this.loggerService.log(`exchangeSchemaService validateShallow`, {
|
|
2092
|
+
exchangeSchema,
|
|
2093
|
+
});
|
|
2094
|
+
if (typeof exchangeSchema.exchangeName !== "string") {
|
|
2095
|
+
throw new Error(`exchange schema validation failed: missing exchangeName`);
|
|
2096
|
+
}
|
|
2097
|
+
if (typeof exchangeSchema.getCandles !== "function") {
|
|
2098
|
+
throw new Error(`exchange schema validation failed: missing getCandles for exchangeName=${exchangeSchema.exchangeName}`);
|
|
2099
|
+
}
|
|
2100
|
+
if (typeof exchangeSchema.formatPrice !== "function") {
|
|
2101
|
+
throw new Error(`exchange schema validation failed: missing formatPrice for exchangeName=${exchangeSchema.exchangeName}`);
|
|
2102
|
+
}
|
|
2103
|
+
if (typeof exchangeSchema.formatQuantity !== "function") {
|
|
2104
|
+
throw new Error(`exchange schema validation failed: missing formatQuantity for exchangeName=${exchangeSchema.exchangeName}`);
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
/**
|
|
2108
|
+
* Overrides an existing exchange schema with partial updates.
|
|
2109
|
+
*
|
|
2110
|
+
* @param key - Exchange name to override
|
|
2111
|
+
* @param value - Partial schema updates
|
|
2112
|
+
* @returns Updated exchange schema
|
|
2113
|
+
* @throws Error if exchange name doesn't exist
|
|
2114
|
+
*/
|
|
1161
2115
|
this.override = (key, value) => {
|
|
1162
|
-
this.loggerService.
|
|
2116
|
+
this.loggerService.log(`exchangeSchemaService override`, { key });
|
|
1163
2117
|
this._registry = this._registry.override(key, value);
|
|
1164
2118
|
return this._registry.get(key);
|
|
1165
2119
|
};
|
|
2120
|
+
/**
|
|
2121
|
+
* Retrieves an exchange schema by name.
|
|
2122
|
+
*
|
|
2123
|
+
* @param key - Exchange name
|
|
2124
|
+
* @returns Exchange schema configuration
|
|
2125
|
+
* @throws Error if exchange name doesn't exist
|
|
2126
|
+
*/
|
|
1166
2127
|
this.get = (key) => {
|
|
1167
|
-
this.loggerService.
|
|
2128
|
+
this.loggerService.log(`exchangeSchemaService get`, { key });
|
|
1168
2129
|
return this._registry.get(key);
|
|
1169
2130
|
};
|
|
1170
2131
|
}
|
|
1171
2132
|
}
|
|
1172
2133
|
|
|
2134
|
+
/**
|
|
2135
|
+
* Service for managing strategy schema registry.
|
|
2136
|
+
*
|
|
2137
|
+
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
2138
|
+
* Strategies are registered via addStrategy() and retrieved by name.
|
|
2139
|
+
*/
|
|
1173
2140
|
class StrategySchemaService {
|
|
1174
2141
|
constructor() {
|
|
1175
2142
|
this.loggerService = inject(TYPES.loggerService);
|
|
1176
2143
|
this._registry = new ToolRegistry("strategySchema");
|
|
2144
|
+
/**
|
|
2145
|
+
* Registers a new strategy schema.
|
|
2146
|
+
*
|
|
2147
|
+
* @param key - Unique strategy name
|
|
2148
|
+
* @param value - Strategy schema configuration
|
|
2149
|
+
* @throws Error if strategy name already exists
|
|
2150
|
+
*/
|
|
1177
2151
|
this.register = (key, value) => {
|
|
1178
|
-
this.loggerService.
|
|
2152
|
+
this.loggerService.log(`strategySchemaService register`, { key });
|
|
2153
|
+
this.validateShallow(value);
|
|
1179
2154
|
this._registry = this._registry.register(key, value);
|
|
1180
2155
|
};
|
|
2156
|
+
/**
|
|
2157
|
+
* Validates strategy schema structure for required properties.
|
|
2158
|
+
*
|
|
2159
|
+
* Performs shallow validation to ensure all required properties exist
|
|
2160
|
+
* and have correct types before registration in the registry.
|
|
2161
|
+
*
|
|
2162
|
+
* @param strategySchema - Strategy schema to validate
|
|
2163
|
+
* @throws Error if strategyName is missing or not a string
|
|
2164
|
+
* @throws Error if interval is missing or not a valid SignalInterval
|
|
2165
|
+
* @throws Error if getSignal is missing or not a function
|
|
2166
|
+
*/
|
|
2167
|
+
this.validateShallow = (strategySchema) => {
|
|
2168
|
+
this.loggerService.log(`strategySchemaService validateShallow`, {
|
|
2169
|
+
strategySchema,
|
|
2170
|
+
});
|
|
2171
|
+
if (typeof strategySchema.strategyName !== "string") {
|
|
2172
|
+
throw new Error(`strategy schema validation failed: missing strategyName`);
|
|
2173
|
+
}
|
|
2174
|
+
if (typeof strategySchema.interval !== "string") {
|
|
2175
|
+
throw new Error(`strategy schema validation failed: missing interval for strategyName=${strategySchema.strategyName}`);
|
|
2176
|
+
}
|
|
2177
|
+
if (typeof strategySchema.getSignal !== "function") {
|
|
2178
|
+
throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
/**
|
|
2182
|
+
* Overrides an existing strategy schema with partial updates.
|
|
2183
|
+
*
|
|
2184
|
+
* @param key - Strategy name to override
|
|
2185
|
+
* @param value - Partial schema updates
|
|
2186
|
+
* @returns Updated strategy schema
|
|
2187
|
+
* @throws Error if strategy name doesn't exist
|
|
2188
|
+
*/
|
|
1181
2189
|
this.override = (key, value) => {
|
|
1182
|
-
this.loggerService.
|
|
2190
|
+
this.loggerService.log(`strategySchemaService override`, { key });
|
|
1183
2191
|
this._registry = this._registry.override(key, value);
|
|
1184
2192
|
return this._registry.get(key);
|
|
1185
2193
|
};
|
|
2194
|
+
/**
|
|
2195
|
+
* Retrieves a strategy schema by name.
|
|
2196
|
+
*
|
|
2197
|
+
* @param key - Strategy name
|
|
2198
|
+
* @returns Strategy schema configuration
|
|
2199
|
+
* @throws Error if strategy name doesn't exist
|
|
2200
|
+
*/
|
|
1186
2201
|
this.get = (key) => {
|
|
1187
|
-
this.loggerService.
|
|
2202
|
+
this.loggerService.log(`strategySchemaService get`, { key });
|
|
1188
2203
|
return this._registry.get(key);
|
|
1189
2204
|
};
|
|
1190
2205
|
}
|
|
1191
2206
|
}
|
|
1192
2207
|
|
|
2208
|
+
/**
|
|
2209
|
+
* Service for managing frame schema registry.
|
|
2210
|
+
*
|
|
2211
|
+
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
2212
|
+
* Frames are registered via addFrame() and retrieved by name.
|
|
2213
|
+
*/
|
|
1193
2214
|
class FrameSchemaService {
|
|
1194
2215
|
constructor() {
|
|
2216
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
1195
2217
|
this._registry = new ToolRegistry("frameSchema");
|
|
2218
|
+
/**
|
|
2219
|
+
* Validates frame schema structure for required properties.
|
|
2220
|
+
*
|
|
2221
|
+
* Performs shallow validation to ensure all required properties exist
|
|
2222
|
+
* and have correct types before registration in the registry.
|
|
2223
|
+
*
|
|
2224
|
+
* @param frameSchema - Frame schema to validate
|
|
2225
|
+
* @throws Error if frameName is missing or not a string
|
|
2226
|
+
* @throws Error if interval is missing or not a valid FrameInterval
|
|
2227
|
+
* @throws Error if startDate is missing or not a Date
|
|
2228
|
+
* @throws Error if endDate is missing or not a Date
|
|
2229
|
+
*/
|
|
2230
|
+
this.validateShallow = (frameSchema) => {
|
|
2231
|
+
this.loggerService.log(`frameSchemaService validateShallow`, {
|
|
2232
|
+
frameSchema,
|
|
2233
|
+
});
|
|
2234
|
+
if (typeof frameSchema.frameName !== "string") {
|
|
2235
|
+
throw new Error(`frame schema validation failed: missing frameName`);
|
|
2236
|
+
}
|
|
2237
|
+
if (typeof frameSchema.interval !== "string") {
|
|
2238
|
+
throw new Error(`frame schema validation failed: missing interval for frameName=${frameSchema.frameName}`);
|
|
2239
|
+
}
|
|
2240
|
+
if (!(frameSchema.startDate instanceof Date)) {
|
|
2241
|
+
throw new Error(`frame schema validation failed: missing startDate for frameName=${frameSchema.frameName}`);
|
|
2242
|
+
}
|
|
2243
|
+
if (!(frameSchema.endDate instanceof Date)) {
|
|
2244
|
+
throw new Error(`frame schema validation failed: missing endDate for frameName=${frameSchema.frameName}`);
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
1196
2247
|
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Registers a new frame schema.
|
|
2250
|
+
*
|
|
2251
|
+
* @param key - Unique frame name
|
|
2252
|
+
* @param value - Frame schema configuration
|
|
2253
|
+
* @throws Error if frame name already exists
|
|
2254
|
+
*/
|
|
1197
2255
|
register(key, value) {
|
|
2256
|
+
this.loggerService.log(`frameSchemaService register`, { key });
|
|
2257
|
+
this.validateShallow(value);
|
|
1198
2258
|
this._registry.register(key, value);
|
|
1199
2259
|
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Overrides an existing frame schema with partial updates.
|
|
2262
|
+
*
|
|
2263
|
+
* @param key - Frame name to override
|
|
2264
|
+
* @param value - Partial schema updates
|
|
2265
|
+
* @throws Error if frame name doesn't exist
|
|
2266
|
+
*/
|
|
1200
2267
|
override(key, value) {
|
|
2268
|
+
this.loggerService.log(`frameSchemaService override`, { key });
|
|
1201
2269
|
this._registry.override(key, value);
|
|
1202
2270
|
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Retrieves a frame schema by name.
|
|
2273
|
+
*
|
|
2274
|
+
* @param key - Frame name
|
|
2275
|
+
* @returns Frame schema configuration
|
|
2276
|
+
* @throws Error if frame name doesn't exist
|
|
2277
|
+
*/
|
|
1203
2278
|
get(key) {
|
|
2279
|
+
this.loggerService.log(`frameSchemaService get`, { key });
|
|
1204
2280
|
return this._registry.get(key);
|
|
1205
2281
|
}
|
|
1206
2282
|
}
|
|
1207
2283
|
|
|
2284
|
+
/**
|
|
2285
|
+
* Private service for backtest orchestration using async generators.
|
|
2286
|
+
*
|
|
2287
|
+
* Flow:
|
|
2288
|
+
* 1. Get timeframes from frame service
|
|
2289
|
+
* 2. Iterate through timeframes calling tick()
|
|
2290
|
+
* 3. When signal opens: fetch candles and call backtest()
|
|
2291
|
+
* 4. Skip timeframes until signal closes
|
|
2292
|
+
* 5. Yield closed result and continue
|
|
2293
|
+
*
|
|
2294
|
+
* Memory efficient: streams results without array accumulation.
|
|
2295
|
+
* Supports early termination via break in consumer.
|
|
2296
|
+
*/
|
|
1208
2297
|
class BacktestLogicPrivateService {
|
|
1209
2298
|
constructor() {
|
|
1210
2299
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -1212,6 +2301,20 @@ class BacktestLogicPrivateService {
|
|
|
1212
2301
|
this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
|
|
1213
2302
|
this.frameGlobalService = inject(TYPES.frameGlobalService);
|
|
1214
2303
|
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Runs backtest for a symbol, streaming closed signals as async generator.
|
|
2306
|
+
*
|
|
2307
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2308
|
+
* @yields Closed signal results with PNL
|
|
2309
|
+
*
|
|
2310
|
+
* @example
|
|
2311
|
+
* ```typescript
|
|
2312
|
+
* for await (const result of backtestLogic.run("BTCUSDT")) {
|
|
2313
|
+
* console.log(result.closeReason, result.pnl.pnlPercentage);
|
|
2314
|
+
* if (result.pnl.pnlPercentage < -10) break; // Early termination
|
|
2315
|
+
* }
|
|
2316
|
+
* ```
|
|
2317
|
+
*/
|
|
1215
2318
|
async *run(symbol) {
|
|
1216
2319
|
this.loggerService.log("backtestLogicPrivateService run", {
|
|
1217
2320
|
symbol,
|
|
@@ -1260,11 +2363,49 @@ class BacktestLogicPrivateService {
|
|
|
1260
2363
|
}
|
|
1261
2364
|
|
|
1262
2365
|
const TICK_TTL = 1 * 60 * 1000 + 1;
|
|
2366
|
+
/**
|
|
2367
|
+
* Private service for live trading orchestration using async generators.
|
|
2368
|
+
*
|
|
2369
|
+
* Flow:
|
|
2370
|
+
* 1. Infinite while(true) loop for continuous monitoring
|
|
2371
|
+
* 2. Create real-time date with new Date()
|
|
2372
|
+
* 3. Call tick() to check signal status
|
|
2373
|
+
* 4. Yield opened/closed results (skip idle/active)
|
|
2374
|
+
* 5. Sleep for TICK_TTL between iterations
|
|
2375
|
+
*
|
|
2376
|
+
* Features:
|
|
2377
|
+
* - Crash recovery via ClientStrategy.waitForInit()
|
|
2378
|
+
* - Real-time progression with new Date()
|
|
2379
|
+
* - Memory efficient streaming
|
|
2380
|
+
* - Never completes (infinite generator)
|
|
2381
|
+
*/
|
|
1263
2382
|
class LiveLogicPrivateService {
|
|
1264
2383
|
constructor() {
|
|
1265
2384
|
this.loggerService = inject(TYPES.loggerService);
|
|
1266
2385
|
this.strategyGlobalService = inject(TYPES.strategyGlobalService);
|
|
1267
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Runs live trading for a symbol, streaming results as async generator.
|
|
2389
|
+
*
|
|
2390
|
+
* Infinite generator that yields opened and closed signals.
|
|
2391
|
+
* Process can crash and restart - state will be recovered from disk.
|
|
2392
|
+
*
|
|
2393
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2394
|
+
* @yields Opened and closed signal results
|
|
2395
|
+
*
|
|
2396
|
+
* @example
|
|
2397
|
+
* ```typescript
|
|
2398
|
+
* for await (const result of liveLogic.run("BTCUSDT")) {
|
|
2399
|
+
* if (result.action === "opened") {
|
|
2400
|
+
* console.log("New signal:", result.signal.id);
|
|
2401
|
+
* }
|
|
2402
|
+
* if (result.action === "closed") {
|
|
2403
|
+
* console.log("PNL:", result.pnl.pnlPercentage);
|
|
2404
|
+
* }
|
|
2405
|
+
* // Infinite loop - will never complete
|
|
2406
|
+
* }
|
|
2407
|
+
* ```
|
|
2408
|
+
*/
|
|
1268
2409
|
async *run(symbol) {
|
|
1269
2410
|
this.loggerService.log("liveLogicPrivateService run", {
|
|
1270
2411
|
symbol,
|
|
@@ -1290,10 +2431,44 @@ class LiveLogicPrivateService {
|
|
|
1290
2431
|
}
|
|
1291
2432
|
}
|
|
1292
2433
|
|
|
2434
|
+
/**
|
|
2435
|
+
* Public service for backtest orchestration with context management.
|
|
2436
|
+
*
|
|
2437
|
+
* Wraps BacktestLogicPrivateService with MethodContextService to provide
|
|
2438
|
+
* implicit context propagation for strategyName, exchangeName, and frameName.
|
|
2439
|
+
*
|
|
2440
|
+
* This allows getCandles(), getSignal(), and other functions to work without
|
|
2441
|
+
* explicit context parameters.
|
|
2442
|
+
*
|
|
2443
|
+
* @example
|
|
2444
|
+
* ```typescript
|
|
2445
|
+
* const backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
|
|
2446
|
+
*
|
|
2447
|
+
* for await (const result of backtestLogicPublicService.run("BTCUSDT", {
|
|
2448
|
+
* strategyName: "my-strategy",
|
|
2449
|
+
* exchangeName: "my-exchange",
|
|
2450
|
+
* frameName: "1d-backtest",
|
|
2451
|
+
* })) {
|
|
2452
|
+
* if (result.action === "closed") {
|
|
2453
|
+
* console.log("PNL:", result.pnl.profit);
|
|
2454
|
+
* }
|
|
2455
|
+
* }
|
|
2456
|
+
* ```
|
|
2457
|
+
*/
|
|
1293
2458
|
class BacktestLogicPublicService {
|
|
1294
2459
|
constructor() {
|
|
1295
2460
|
this.loggerService = inject(TYPES.loggerService);
|
|
1296
2461
|
this.backtestLogicPrivateService = inject(TYPES.backtestLogicPrivateService);
|
|
2462
|
+
/**
|
|
2463
|
+
* Runs backtest for a symbol with context propagation.
|
|
2464
|
+
*
|
|
2465
|
+
* Streams closed signals as async generator. Context is automatically
|
|
2466
|
+
* injected into all framework functions called during iteration.
|
|
2467
|
+
*
|
|
2468
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2469
|
+
* @param context - Execution context with strategy, exchange, and frame names
|
|
2470
|
+
* @returns Async generator yielding closed signals with PNL
|
|
2471
|
+
*/
|
|
1297
2472
|
this.run = (symbol, context) => {
|
|
1298
2473
|
this.loggerService.log("backtestLogicPublicService run", {
|
|
1299
2474
|
symbol,
|
|
@@ -1308,10 +2483,52 @@ class BacktestLogicPublicService {
|
|
|
1308
2483
|
}
|
|
1309
2484
|
}
|
|
1310
2485
|
|
|
2486
|
+
/**
|
|
2487
|
+
* Public service for live trading orchestration with context management.
|
|
2488
|
+
*
|
|
2489
|
+
* Wraps LiveLogicPrivateService with MethodContextService to provide
|
|
2490
|
+
* implicit context propagation for strategyName and exchangeName.
|
|
2491
|
+
*
|
|
2492
|
+
* This allows getCandles(), getSignal(), and other functions to work without
|
|
2493
|
+
* explicit context parameters.
|
|
2494
|
+
*
|
|
2495
|
+
* Features:
|
|
2496
|
+
* - Infinite async generator (never completes)
|
|
2497
|
+
* - Crash recovery via persisted state
|
|
2498
|
+
* - Real-time progression with Date.now()
|
|
2499
|
+
*
|
|
2500
|
+
* @example
|
|
2501
|
+
* ```typescript
|
|
2502
|
+
* const liveLogicPublicService = inject(TYPES.liveLogicPublicService);
|
|
2503
|
+
*
|
|
2504
|
+
* // Infinite loop - use Ctrl+C to stop
|
|
2505
|
+
* for await (const result of liveLogicPublicService.run("BTCUSDT", {
|
|
2506
|
+
* strategyName: "my-strategy",
|
|
2507
|
+
* exchangeName: "my-exchange",
|
|
2508
|
+
* })) {
|
|
2509
|
+
* if (result.action === "opened") {
|
|
2510
|
+
* console.log("Signal opened:", result.signal);
|
|
2511
|
+
* } else if (result.action === "closed") {
|
|
2512
|
+
* console.log("PNL:", result.pnl.profit);
|
|
2513
|
+
* }
|
|
2514
|
+
* }
|
|
2515
|
+
* ```
|
|
2516
|
+
*/
|
|
1311
2517
|
class LiveLogicPublicService {
|
|
1312
2518
|
constructor() {
|
|
1313
2519
|
this.loggerService = inject(TYPES.loggerService);
|
|
1314
2520
|
this.liveLogicPrivateService = inject(TYPES.liveLogicPrivateService);
|
|
2521
|
+
/**
|
|
2522
|
+
* Runs live trading for a symbol with context propagation.
|
|
2523
|
+
*
|
|
2524
|
+
* Streams opened and closed signals as infinite async generator.
|
|
2525
|
+
* Context is automatically injected into all framework functions.
|
|
2526
|
+
* Process can crash and restart - state will be recovered from disk.
|
|
2527
|
+
*
|
|
2528
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2529
|
+
* @param context - Execution context with strategy and exchange names
|
|
2530
|
+
* @returns Infinite async generator yielding opened and closed signals
|
|
2531
|
+
*/
|
|
1315
2532
|
this.run = (symbol, context) => {
|
|
1316
2533
|
this.loggerService.log("liveLogicPublicService run", {
|
|
1317
2534
|
symbol,
|
|
@@ -1326,78 +2543,989 @@ class LiveLogicPublicService {
|
|
|
1326
2543
|
}
|
|
1327
2544
|
}
|
|
1328
2545
|
|
|
2546
|
+
const METHOD_NAME_RUN$1 = "liveGlobalService run";
|
|
2547
|
+
/**
|
|
2548
|
+
* Global service providing access to live trading functionality.
|
|
2549
|
+
*
|
|
2550
|
+
* Simple wrapper around LiveLogicPublicService for dependency injection.
|
|
2551
|
+
* Used by public API exports.
|
|
2552
|
+
*/
|
|
1329
2553
|
class LiveGlobalService {
|
|
1330
2554
|
constructor() {
|
|
1331
2555
|
this.loggerService = inject(TYPES.loggerService);
|
|
1332
2556
|
this.liveLogicPublicService = inject(TYPES.liveLogicPublicService);
|
|
2557
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
2558
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
2559
|
+
/**
|
|
2560
|
+
* Runs live trading for a symbol with context propagation.
|
|
2561
|
+
*
|
|
2562
|
+
* Infinite async generator with crash recovery support.
|
|
2563
|
+
*
|
|
2564
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2565
|
+
* @param context - Execution context with strategy and exchange names
|
|
2566
|
+
* @returns Infinite async generator yielding opened and closed signals
|
|
2567
|
+
*/
|
|
1333
2568
|
this.run = (symbol, context) => {
|
|
1334
|
-
this.loggerService.log(
|
|
2569
|
+
this.loggerService.log(METHOD_NAME_RUN$1, {
|
|
1335
2570
|
symbol,
|
|
1336
2571
|
context,
|
|
1337
2572
|
});
|
|
2573
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
|
|
2574
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
|
|
1338
2575
|
return this.liveLogicPublicService.run(symbol, context);
|
|
1339
2576
|
};
|
|
1340
2577
|
}
|
|
1341
2578
|
}
|
|
1342
2579
|
|
|
2580
|
+
const METHOD_NAME_RUN = "backtestGlobalService run";
|
|
2581
|
+
/**
|
|
2582
|
+
* Global service providing access to backtest functionality.
|
|
2583
|
+
*
|
|
2584
|
+
* Simple wrapper around BacktestLogicPublicService for dependency injection.
|
|
2585
|
+
* Used by public API exports.
|
|
2586
|
+
*/
|
|
1343
2587
|
class BacktestGlobalService {
|
|
1344
2588
|
constructor() {
|
|
1345
2589
|
this.loggerService = inject(TYPES.loggerService);
|
|
1346
2590
|
this.backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
|
|
2591
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
2592
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
2593
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
2594
|
+
/**
|
|
2595
|
+
* Runs backtest for a symbol with context propagation.
|
|
2596
|
+
*
|
|
2597
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2598
|
+
* @param context - Execution context with strategy, exchange, and frame names
|
|
2599
|
+
* @returns Async generator yielding closed signals with PNL
|
|
2600
|
+
*/
|
|
1347
2601
|
this.run = (symbol, context) => {
|
|
1348
|
-
this.loggerService.log(
|
|
2602
|
+
this.loggerService.log(METHOD_NAME_RUN, {
|
|
1349
2603
|
symbol,
|
|
1350
2604
|
context,
|
|
1351
2605
|
});
|
|
2606
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN);
|
|
2607
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
|
|
2608
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
|
|
1352
2609
|
return this.backtestLogicPublicService.run(symbol, context);
|
|
1353
2610
|
};
|
|
1354
2611
|
}
|
|
1355
2612
|
}
|
|
1356
2613
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
{
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
2614
|
+
const columns$1 = [
|
|
2615
|
+
{
|
|
2616
|
+
key: "signalId",
|
|
2617
|
+
label: "Signal ID",
|
|
2618
|
+
format: (data) => data.signal.id,
|
|
2619
|
+
},
|
|
2620
|
+
{
|
|
2621
|
+
key: "symbol",
|
|
2622
|
+
label: "Symbol",
|
|
2623
|
+
format: (data) => data.signal.symbol,
|
|
2624
|
+
},
|
|
2625
|
+
{
|
|
2626
|
+
key: "position",
|
|
2627
|
+
label: "Position",
|
|
2628
|
+
format: (data) => data.signal.position.toUpperCase(),
|
|
2629
|
+
},
|
|
2630
|
+
{
|
|
2631
|
+
key: "note",
|
|
2632
|
+
label: "Note",
|
|
2633
|
+
format: (data) => data.signal.note ?? "N/A",
|
|
2634
|
+
},
|
|
2635
|
+
{
|
|
2636
|
+
key: "openPrice",
|
|
2637
|
+
label: "Open Price",
|
|
2638
|
+
format: (data) => `${data.signal.priceOpen.toFixed(8)} USD`,
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
key: "closePrice",
|
|
2642
|
+
label: "Close Price",
|
|
2643
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
2644
|
+
},
|
|
2645
|
+
{
|
|
2646
|
+
key: "takeProfit",
|
|
2647
|
+
label: "Take Profit",
|
|
2648
|
+
format: (data) => `${data.signal.priceTakeProfit.toFixed(8)} USD`,
|
|
2649
|
+
},
|
|
2650
|
+
{
|
|
2651
|
+
key: "stopLoss",
|
|
2652
|
+
label: "Stop Loss",
|
|
2653
|
+
format: (data) => `${data.signal.priceStopLoss.toFixed(8)} USD`,
|
|
2654
|
+
},
|
|
2655
|
+
{
|
|
2656
|
+
key: "pnl",
|
|
2657
|
+
label: "PNL (net)",
|
|
2658
|
+
format: (data) => {
|
|
2659
|
+
const pnlPercentage = data.pnl.pnlPercentage;
|
|
2660
|
+
return `${pnlPercentage > 0 ? "+" : ""}${pnlPercentage.toFixed(2)}%`;
|
|
2661
|
+
},
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
key: "closeReason",
|
|
2665
|
+
label: "Close Reason",
|
|
2666
|
+
format: (data) => data.closeReason,
|
|
2667
|
+
},
|
|
2668
|
+
{
|
|
2669
|
+
key: "duration",
|
|
2670
|
+
label: "Duration (min)",
|
|
2671
|
+
format: (data) => {
|
|
2672
|
+
const durationMs = data.closeTimestamp - data.signal.timestamp;
|
|
2673
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
2674
|
+
return `${durationMin}`;
|
|
2675
|
+
},
|
|
2676
|
+
},
|
|
2677
|
+
{
|
|
2678
|
+
key: "openTimestamp",
|
|
2679
|
+
label: "Open Time",
|
|
2680
|
+
format: (data) => new Date(data.signal.timestamp).toISOString(),
|
|
2681
|
+
},
|
|
2682
|
+
{
|
|
2683
|
+
key: "closeTimestamp",
|
|
2684
|
+
label: "Close Time",
|
|
2685
|
+
format: (data) => new Date(data.closeTimestamp).toISOString(),
|
|
2686
|
+
},
|
|
2687
|
+
];
|
|
2688
|
+
/**
|
|
2689
|
+
* Storage class for accumulating closed signals per strategy.
|
|
2690
|
+
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
2691
|
+
*/
|
|
2692
|
+
let ReportStorage$1 = class ReportStorage {
|
|
2693
|
+
constructor() {
|
|
2694
|
+
/** Internal list of all closed signals for this strategy */
|
|
2695
|
+
this._signalList = [];
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Adds a closed signal to the storage.
|
|
2699
|
+
*
|
|
2700
|
+
* @param data - Closed signal data with PNL and close reason
|
|
2701
|
+
*/
|
|
2702
|
+
addSignal(data) {
|
|
2703
|
+
this._signalList.push(data);
|
|
2704
|
+
}
|
|
2705
|
+
/**
|
|
2706
|
+
* Generates markdown report with all closed signals for a strategy.
|
|
2707
|
+
*
|
|
2708
|
+
* @param strategyName - Strategy name
|
|
2709
|
+
* @returns Markdown formatted report with all signals
|
|
2710
|
+
*/
|
|
2711
|
+
getReport(strategyName) {
|
|
2712
|
+
if (this._signalList.length === 0) {
|
|
2713
|
+
return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
2714
|
+
}
|
|
2715
|
+
const header = columns$1.map((col) => col.label);
|
|
2716
|
+
const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
|
|
2717
|
+
const tableData = [header, ...rows];
|
|
2718
|
+
const table = str.table(tableData);
|
|
2719
|
+
return str.newline(`# Backtest Report: ${strategyName}`, "", `Total signals: ${this._signalList.length}`, "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Saves strategy report to disk.
|
|
2723
|
+
*
|
|
2724
|
+
* @param strategyName - Strategy name
|
|
2725
|
+
* @param path - Directory path to save report (default: "./logs/backtest")
|
|
2726
|
+
*/
|
|
2727
|
+
async dump(strategyName, path = "./logs/backtest") {
|
|
2728
|
+
const markdown = this.getReport(strategyName);
|
|
2729
|
+
try {
|
|
2730
|
+
const dir = join(process.cwd(), path);
|
|
2731
|
+
await mkdir(dir, { recursive: true });
|
|
2732
|
+
const filename = `${strategyName}.md`;
|
|
2733
|
+
const filepath = join(dir, filename);
|
|
2734
|
+
await writeFile(filepath, markdown, "utf-8");
|
|
2735
|
+
console.log(`Backtest report saved: ${filepath}`);
|
|
2736
|
+
}
|
|
2737
|
+
catch (error) {
|
|
2738
|
+
console.error(`Failed to save markdown report:`, error);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
/**
|
|
2743
|
+
* Service for generating and saving backtest markdown reports.
|
|
2744
|
+
*
|
|
2745
|
+
* Features:
|
|
2746
|
+
* - Listens to signal events via onTick callback
|
|
2747
|
+
* - Accumulates closed signals per strategy using memoized storage
|
|
2748
|
+
* - Generates markdown tables with detailed signal information
|
|
2749
|
+
* - Saves reports to disk in logs/backtest/{strategyName}.md
|
|
2750
|
+
*
|
|
2751
|
+
* @example
|
|
2752
|
+
* ```typescript
|
|
2753
|
+
* const service = new BacktestMarkdownService();
|
|
2754
|
+
*
|
|
2755
|
+
* // Add to strategy callbacks
|
|
2756
|
+
* addStrategy({
|
|
2757
|
+
* strategyName: "my-strategy",
|
|
2758
|
+
* callbacks: {
|
|
2759
|
+
* onTick: (symbol, result, backtest) => {
|
|
2760
|
+
* service.tick(result);
|
|
2761
|
+
* }
|
|
2762
|
+
* }
|
|
2763
|
+
* });
|
|
2764
|
+
*
|
|
2765
|
+
* // After backtest, generate and save report
|
|
2766
|
+
* await service.saveReport("my-strategy");
|
|
2767
|
+
* ```
|
|
2768
|
+
*/
|
|
2769
|
+
class BacktestMarkdownService {
|
|
2770
|
+
constructor() {
|
|
2771
|
+
/** Logger service for debug output */
|
|
2772
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
2773
|
+
/**
|
|
2774
|
+
* Memoized function to get or create ReportStorage for a strategy.
|
|
2775
|
+
* Each strategy gets its own isolated storage instance.
|
|
2776
|
+
*/
|
|
2777
|
+
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
|
|
2778
|
+
/**
|
|
2779
|
+
* Processes tick events and accumulates closed signals.
|
|
2780
|
+
* Should be called from IStrategyCallbacks.onTick.
|
|
2781
|
+
*
|
|
2782
|
+
* Only processes closed signals - opened signals are ignored.
|
|
2783
|
+
*
|
|
2784
|
+
* @param data - Tick result from strategy execution (opened or closed)
|
|
2785
|
+
*
|
|
2786
|
+
* @example
|
|
2787
|
+
* ```typescript
|
|
2788
|
+
* const service = new BacktestMarkdownService();
|
|
2789
|
+
*
|
|
2790
|
+
* callbacks: {
|
|
2791
|
+
* onTick: (symbol, result, backtest) => {
|
|
2792
|
+
* service.tick(result);
|
|
2793
|
+
* }
|
|
2794
|
+
* }
|
|
2795
|
+
* ```
|
|
2796
|
+
*/
|
|
2797
|
+
this.tick = async (data) => {
|
|
2798
|
+
this.loggerService.log("backtestMarkdownService tick", {
|
|
2799
|
+
data,
|
|
2800
|
+
});
|
|
2801
|
+
if (data.action !== "closed") {
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const storage = this.getStorage(data.strategyName);
|
|
2805
|
+
storage.addSignal(data);
|
|
2806
|
+
};
|
|
2807
|
+
/**
|
|
2808
|
+
* Generates markdown report with all closed signals for a strategy.
|
|
2809
|
+
* Delegates to ReportStorage.generateReport().
|
|
2810
|
+
*
|
|
2811
|
+
* @param strategyName - Strategy name to generate report for
|
|
2812
|
+
* @returns Markdown formatted report string with table of all closed signals
|
|
2813
|
+
*
|
|
2814
|
+
* @example
|
|
2815
|
+
* ```typescript
|
|
2816
|
+
* const service = new BacktestMarkdownService();
|
|
2817
|
+
* const markdown = service.generateReport("my-strategy");
|
|
2818
|
+
* console.log(markdown);
|
|
2819
|
+
* ```
|
|
2820
|
+
*/
|
|
2821
|
+
this.getReport = async (strategyName) => {
|
|
2822
|
+
this.loggerService.log("backtestMarkdownService getReport", {
|
|
2823
|
+
strategyName,
|
|
2824
|
+
});
|
|
2825
|
+
const storage = this.getStorage(strategyName);
|
|
2826
|
+
return storage.getReport(strategyName);
|
|
2827
|
+
};
|
|
2828
|
+
/**
|
|
2829
|
+
* Saves strategy report to disk.
|
|
2830
|
+
* Creates directory if it doesn't exist.
|
|
2831
|
+
* Delegates to ReportStorage.dump().
|
|
2832
|
+
*
|
|
2833
|
+
* @param strategyName - Strategy name to save report for
|
|
2834
|
+
* @param path - Directory path to save report (default: "./logs/backtest")
|
|
2835
|
+
*
|
|
2836
|
+
* @example
|
|
2837
|
+
* ```typescript
|
|
2838
|
+
* const service = new BacktestMarkdownService();
|
|
2839
|
+
*
|
|
2840
|
+
* // Save to default path: ./logs/backtest/my-strategy.md
|
|
2841
|
+
* await service.dump("my-strategy");
|
|
2842
|
+
*
|
|
2843
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
2844
|
+
* await service.dump("my-strategy", "./custom/path");
|
|
2845
|
+
* ```
|
|
2846
|
+
*/
|
|
2847
|
+
this.dump = async (strategyName, path = "./logs/backtest") => {
|
|
2848
|
+
this.loggerService.log("backtestMarkdownService dump", {
|
|
2849
|
+
strategyName,
|
|
2850
|
+
path,
|
|
2851
|
+
});
|
|
2852
|
+
const storage = this.getStorage(strategyName);
|
|
2853
|
+
await storage.dump(strategyName, path);
|
|
2854
|
+
};
|
|
2855
|
+
/**
|
|
2856
|
+
* Clears accumulated signal data from storage.
|
|
2857
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
2858
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
2859
|
+
*
|
|
2860
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
2861
|
+
*
|
|
2862
|
+
* @example
|
|
2863
|
+
* ```typescript
|
|
2864
|
+
* const service = new BacktestMarkdownService();
|
|
2865
|
+
*
|
|
2866
|
+
* // Clear specific strategy data
|
|
2867
|
+
* await service.clear("my-strategy");
|
|
2868
|
+
*
|
|
2869
|
+
* // Clear all strategies' data
|
|
2870
|
+
* await service.clear();
|
|
2871
|
+
* ```
|
|
2872
|
+
*/
|
|
2873
|
+
this.clear = async (strategyName) => {
|
|
2874
|
+
this.loggerService.log("backtestMarkdownService clear", {
|
|
2875
|
+
strategyName,
|
|
2876
|
+
});
|
|
2877
|
+
this.getStorage.clear(strategyName);
|
|
2878
|
+
};
|
|
2879
|
+
/**
|
|
2880
|
+
* Initializes the service by subscribing to backtest signal events.
|
|
2881
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
2882
|
+
* Automatically called on first use.
|
|
2883
|
+
*
|
|
2884
|
+
* @example
|
|
2885
|
+
* ```typescript
|
|
2886
|
+
* const service = new BacktestMarkdownService();
|
|
2887
|
+
* await service.init(); // Subscribe to backtest events
|
|
2888
|
+
* ```
|
|
2889
|
+
*/
|
|
2890
|
+
this.init = singleshot(async () => {
|
|
2891
|
+
this.loggerService.log("backtestMarkdownService init");
|
|
2892
|
+
signalBacktestEmitter.subscribe(this.tick);
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
const columns = [
|
|
2898
|
+
{
|
|
2899
|
+
key: "timestamp",
|
|
2900
|
+
label: "Timestamp",
|
|
2901
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
key: "action",
|
|
2905
|
+
label: "Action",
|
|
2906
|
+
format: (data) => data.action.toUpperCase(),
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
key: "symbol",
|
|
2910
|
+
label: "Symbol",
|
|
2911
|
+
format: (data) => data.symbol ?? "N/A",
|
|
2912
|
+
},
|
|
2913
|
+
{
|
|
2914
|
+
key: "signalId",
|
|
2915
|
+
label: "Signal ID",
|
|
2916
|
+
format: (data) => data.signalId ?? "N/A",
|
|
2917
|
+
},
|
|
2918
|
+
{
|
|
2919
|
+
key: "position",
|
|
2920
|
+
label: "Position",
|
|
2921
|
+
format: (data) => data.position?.toUpperCase() ?? "N/A",
|
|
2922
|
+
},
|
|
2923
|
+
{
|
|
2924
|
+
key: "note",
|
|
2925
|
+
label: "Note",
|
|
2926
|
+
format: (data) => data.note ?? "N/A",
|
|
2927
|
+
},
|
|
2928
|
+
{
|
|
2929
|
+
key: "currentPrice",
|
|
2930
|
+
label: "Current Price",
|
|
2931
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
2932
|
+
},
|
|
2933
|
+
{
|
|
2934
|
+
key: "openPrice",
|
|
2935
|
+
label: "Open Price",
|
|
2936
|
+
format: (data) => data.openPrice !== undefined ? `${data.openPrice.toFixed(8)} USD` : "N/A",
|
|
2937
|
+
},
|
|
2938
|
+
{
|
|
2939
|
+
key: "takeProfit",
|
|
2940
|
+
label: "Take Profit",
|
|
2941
|
+
format: (data) => data.takeProfit !== undefined
|
|
2942
|
+
? `${data.takeProfit.toFixed(8)} USD`
|
|
2943
|
+
: "N/A",
|
|
2944
|
+
},
|
|
2945
|
+
{
|
|
2946
|
+
key: "stopLoss",
|
|
2947
|
+
label: "Stop Loss",
|
|
2948
|
+
format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
|
|
2949
|
+
},
|
|
2950
|
+
{
|
|
2951
|
+
key: "pnl",
|
|
2952
|
+
label: "PNL (net)",
|
|
2953
|
+
format: (data) => {
|
|
2954
|
+
if (data.pnl === undefined)
|
|
2955
|
+
return "N/A";
|
|
2956
|
+
return `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`;
|
|
2957
|
+
},
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
key: "closeReason",
|
|
2961
|
+
label: "Close Reason",
|
|
2962
|
+
format: (data) => data.closeReason ?? "N/A",
|
|
2963
|
+
},
|
|
2964
|
+
{
|
|
2965
|
+
key: "duration",
|
|
2966
|
+
label: "Duration (min)",
|
|
2967
|
+
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
2968
|
+
},
|
|
2969
|
+
];
|
|
2970
|
+
/** Maximum number of events to store in live trading reports */
|
|
2971
|
+
const MAX_EVENTS = 250;
|
|
2972
|
+
/**
|
|
2973
|
+
* Storage class for accumulating all tick events per strategy.
|
|
2974
|
+
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
2975
|
+
*/
|
|
2976
|
+
class ReportStorage {
|
|
2977
|
+
constructor() {
|
|
2978
|
+
/** Internal list of all tick events for this strategy */
|
|
2979
|
+
this._eventList = [];
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Adds an idle event to the storage.
|
|
2983
|
+
* Replaces the last idle event only if there are no opened/active events after it.
|
|
2984
|
+
*
|
|
2985
|
+
* @param currentPrice - Current market price
|
|
2986
|
+
*/
|
|
2987
|
+
addIdleEvent(currentPrice) {
|
|
2988
|
+
const newEvent = {
|
|
2989
|
+
timestamp: Date.now(),
|
|
2990
|
+
action: "idle",
|
|
2991
|
+
currentPrice,
|
|
2992
|
+
};
|
|
2993
|
+
const lastIdleIndex = this._eventList.findLastIndex((event) => event.action === "idle");
|
|
2994
|
+
const canReplaceLastIdle = lastIdleIndex !== -1 &&
|
|
2995
|
+
!this._eventList
|
|
2996
|
+
.slice(lastIdleIndex + 1)
|
|
2997
|
+
.some((event) => event.action === "opened" || event.action === "active");
|
|
2998
|
+
if (canReplaceLastIdle) {
|
|
2999
|
+
this._eventList[lastIdleIndex] = newEvent;
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
{
|
|
3003
|
+
this._eventList.push(newEvent);
|
|
3004
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
3005
|
+
this._eventList.shift();
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Adds an opened event to the storage.
|
|
3011
|
+
*
|
|
3012
|
+
* @param data - Opened tick result
|
|
3013
|
+
*/
|
|
3014
|
+
addOpenedEvent(data) {
|
|
3015
|
+
this._eventList.push({
|
|
3016
|
+
timestamp: data.signal.timestamp,
|
|
3017
|
+
action: "opened",
|
|
3018
|
+
symbol: data.signal.symbol,
|
|
3019
|
+
signalId: data.signal.id,
|
|
3020
|
+
position: data.signal.position,
|
|
3021
|
+
note: data.signal.note,
|
|
3022
|
+
currentPrice: data.signal.priceOpen,
|
|
3023
|
+
openPrice: data.signal.priceOpen,
|
|
3024
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
3025
|
+
stopLoss: data.signal.priceStopLoss,
|
|
3026
|
+
});
|
|
3027
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
3028
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
3029
|
+
this._eventList.shift();
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Updates or adds an active event to the storage.
|
|
3034
|
+
* Replaces the previous event with the same signalId.
|
|
3035
|
+
*
|
|
3036
|
+
* @param data - Active tick result
|
|
3037
|
+
*/
|
|
3038
|
+
addActiveEvent(data) {
|
|
3039
|
+
// Find existing event with the same signalId
|
|
3040
|
+
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
3041
|
+
const newEvent = {
|
|
3042
|
+
timestamp: Date.now(),
|
|
3043
|
+
action: "active",
|
|
3044
|
+
symbol: data.signal.symbol,
|
|
3045
|
+
signalId: data.signal.id,
|
|
3046
|
+
position: data.signal.position,
|
|
3047
|
+
note: data.signal.note,
|
|
3048
|
+
currentPrice: data.currentPrice,
|
|
3049
|
+
openPrice: data.signal.priceOpen,
|
|
3050
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
3051
|
+
stopLoss: data.signal.priceStopLoss,
|
|
3052
|
+
};
|
|
3053
|
+
// Replace existing event or add new one
|
|
3054
|
+
if (existingIndex !== -1) {
|
|
3055
|
+
this._eventList[existingIndex] = newEvent;
|
|
3056
|
+
}
|
|
3057
|
+
else {
|
|
3058
|
+
this._eventList.push(newEvent);
|
|
3059
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
3060
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
3061
|
+
this._eventList.shift();
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* Updates or adds a closed event to the storage.
|
|
3067
|
+
* Replaces the previous event with the same signalId.
|
|
3068
|
+
*
|
|
3069
|
+
* @param data - Closed tick result
|
|
3070
|
+
*/
|
|
3071
|
+
addClosedEvent(data) {
|
|
3072
|
+
const durationMs = data.closeTimestamp - data.signal.timestamp;
|
|
3073
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
3074
|
+
// Find existing event with the same signalId
|
|
3075
|
+
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
3076
|
+
const newEvent = {
|
|
3077
|
+
timestamp: data.closeTimestamp,
|
|
3078
|
+
action: "closed",
|
|
3079
|
+
symbol: data.signal.symbol,
|
|
3080
|
+
signalId: data.signal.id,
|
|
3081
|
+
position: data.signal.position,
|
|
3082
|
+
note: data.signal.note,
|
|
3083
|
+
currentPrice: data.currentPrice,
|
|
3084
|
+
openPrice: data.signal.priceOpen,
|
|
3085
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
3086
|
+
stopLoss: data.signal.priceStopLoss,
|
|
3087
|
+
pnl: data.pnl.pnlPercentage,
|
|
3088
|
+
closeReason: data.closeReason,
|
|
3089
|
+
duration: durationMin,
|
|
3090
|
+
};
|
|
3091
|
+
// Replace existing event or add new one
|
|
3092
|
+
if (existingIndex !== -1) {
|
|
3093
|
+
this._eventList[existingIndex] = newEvent;
|
|
3094
|
+
}
|
|
3095
|
+
else {
|
|
3096
|
+
this._eventList.push(newEvent);
|
|
3097
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
3098
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
3099
|
+
this._eventList.shift();
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Generates markdown report with all tick events for a strategy.
|
|
3105
|
+
*
|
|
3106
|
+
* @param strategyName - Strategy name
|
|
3107
|
+
* @returns Markdown formatted report with all events
|
|
3108
|
+
*/
|
|
3109
|
+
getReport(strategyName) {
|
|
3110
|
+
if (this._eventList.length === 0) {
|
|
3111
|
+
return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
|
|
3112
|
+
}
|
|
3113
|
+
const header = columns.map((col) => col.label);
|
|
3114
|
+
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
3115
|
+
const tableData = [header, ...rows];
|
|
3116
|
+
const table = str.table(tableData);
|
|
3117
|
+
// Calculate statistics
|
|
3118
|
+
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
3119
|
+
const totalClosed = closedEvents.length;
|
|
3120
|
+
const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
|
|
3121
|
+
const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
|
|
3122
|
+
const avgPnl = totalClosed > 0
|
|
3123
|
+
? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
|
|
3124
|
+
: 0;
|
|
3125
|
+
return str.newline(`# Live Trading Report: ${strategyName}`, "", `Total events: ${this._eventList.length}`, `Closed signals: ${totalClosed}`, totalClosed > 0
|
|
3126
|
+
? `Win rate: ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
|
|
3127
|
+
: "", totalClosed > 0
|
|
3128
|
+
? `Average PNL: ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
|
|
3129
|
+
: "", "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Saves strategy report to disk.
|
|
3133
|
+
*
|
|
3134
|
+
* @param strategyName - Strategy name
|
|
3135
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
3136
|
+
*/
|
|
3137
|
+
async dump(strategyName, path = "./logs/live") {
|
|
3138
|
+
const markdown = this.getReport(strategyName);
|
|
3139
|
+
try {
|
|
3140
|
+
const dir = join(process.cwd(), path);
|
|
3141
|
+
await mkdir(dir, { recursive: true });
|
|
3142
|
+
const filename = `${strategyName}.md`;
|
|
3143
|
+
const filepath = join(dir, filename);
|
|
3144
|
+
await writeFile(filepath, markdown, "utf-8");
|
|
3145
|
+
console.log(`Live trading report saved: ${filepath}`);
|
|
3146
|
+
}
|
|
3147
|
+
catch (error) {
|
|
3148
|
+
console.error(`Failed to save markdown report:`, error);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Service for generating and saving live trading markdown reports.
|
|
3154
|
+
*
|
|
3155
|
+
* Features:
|
|
3156
|
+
* - Listens to all signal events via onTick callback
|
|
3157
|
+
* - Accumulates all events (idle, opened, active, closed) per strategy
|
|
3158
|
+
* - Generates markdown tables with detailed event information
|
|
3159
|
+
* - Provides trading statistics (win rate, average PNL)
|
|
3160
|
+
* - Saves reports to disk in logs/live/{strategyName}.md
|
|
3161
|
+
*
|
|
3162
|
+
* @example
|
|
3163
|
+
* ```typescript
|
|
3164
|
+
* const service = new LiveMarkdownService();
|
|
3165
|
+
*
|
|
3166
|
+
* // Add to strategy callbacks
|
|
3167
|
+
* addStrategy({
|
|
3168
|
+
* strategyName: "my-strategy",
|
|
3169
|
+
* callbacks: {
|
|
3170
|
+
* onTick: (symbol, result, backtest) => {
|
|
3171
|
+
* if (!backtest) {
|
|
3172
|
+
* service.tick(result);
|
|
3173
|
+
* }
|
|
3174
|
+
* }
|
|
3175
|
+
* }
|
|
3176
|
+
* });
|
|
3177
|
+
*
|
|
3178
|
+
* // Later: generate and save report
|
|
3179
|
+
* await service.dump("my-strategy");
|
|
3180
|
+
* ```
|
|
3181
|
+
*/
|
|
3182
|
+
class LiveMarkdownService {
|
|
3183
|
+
constructor() {
|
|
3184
|
+
/** Logger service for debug output */
|
|
3185
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3186
|
+
/**
|
|
3187
|
+
* Memoized function to get or create ReportStorage for a strategy.
|
|
3188
|
+
* Each strategy gets its own isolated storage instance.
|
|
3189
|
+
*/
|
|
3190
|
+
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage());
|
|
3191
|
+
/**
|
|
3192
|
+
* Processes tick events and accumulates all event types.
|
|
3193
|
+
* Should be called from IStrategyCallbacks.onTick.
|
|
3194
|
+
*
|
|
3195
|
+
* Processes all event types: idle, opened, active, closed.
|
|
3196
|
+
*
|
|
3197
|
+
* @param data - Tick result from strategy execution
|
|
3198
|
+
*
|
|
3199
|
+
* @example
|
|
3200
|
+
* ```typescript
|
|
3201
|
+
* const service = new LiveMarkdownService();
|
|
3202
|
+
*
|
|
3203
|
+
* callbacks: {
|
|
3204
|
+
* onTick: (symbol, result, backtest) => {
|
|
3205
|
+
* if (!backtest) {
|
|
3206
|
+
* service.tick(result);
|
|
3207
|
+
* }
|
|
3208
|
+
* }
|
|
3209
|
+
* }
|
|
3210
|
+
* ```
|
|
3211
|
+
*/
|
|
3212
|
+
this.tick = async (data) => {
|
|
3213
|
+
this.loggerService.log("liveMarkdownService tick", {
|
|
3214
|
+
data,
|
|
3215
|
+
});
|
|
3216
|
+
const storage = this.getStorage(data.strategyName);
|
|
3217
|
+
if (data.action === "idle") {
|
|
3218
|
+
storage.addIdleEvent(data.currentPrice);
|
|
3219
|
+
}
|
|
3220
|
+
else if (data.action === "opened") {
|
|
3221
|
+
storage.addOpenedEvent(data);
|
|
3222
|
+
}
|
|
3223
|
+
else if (data.action === "active") {
|
|
3224
|
+
storage.addActiveEvent(data);
|
|
3225
|
+
}
|
|
3226
|
+
else if (data.action === "closed") {
|
|
3227
|
+
storage.addClosedEvent(data);
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
/**
|
|
3231
|
+
* Generates markdown report with all events for a strategy.
|
|
3232
|
+
* Delegates to ReportStorage.getReport().
|
|
3233
|
+
*
|
|
3234
|
+
* @param strategyName - Strategy name to generate report for
|
|
3235
|
+
* @returns Markdown formatted report string with table of all events
|
|
3236
|
+
*
|
|
3237
|
+
* @example
|
|
3238
|
+
* ```typescript
|
|
3239
|
+
* const service = new LiveMarkdownService();
|
|
3240
|
+
* const markdown = await service.getReport("my-strategy");
|
|
3241
|
+
* console.log(markdown);
|
|
3242
|
+
* ```
|
|
3243
|
+
*/
|
|
3244
|
+
this.getReport = async (strategyName) => {
|
|
3245
|
+
this.loggerService.log("liveMarkdownService getReport", {
|
|
3246
|
+
strategyName,
|
|
3247
|
+
});
|
|
3248
|
+
const storage = this.getStorage(strategyName);
|
|
3249
|
+
return storage.getReport(strategyName);
|
|
3250
|
+
};
|
|
3251
|
+
/**
|
|
3252
|
+
* Saves strategy report to disk.
|
|
3253
|
+
* Creates directory if it doesn't exist.
|
|
3254
|
+
* Delegates to ReportStorage.dump().
|
|
3255
|
+
*
|
|
3256
|
+
* @param strategyName - Strategy name to save report for
|
|
3257
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
3258
|
+
*
|
|
3259
|
+
* @example
|
|
3260
|
+
* ```typescript
|
|
3261
|
+
* const service = new LiveMarkdownService();
|
|
3262
|
+
*
|
|
3263
|
+
* // Save to default path: ./logs/live/my-strategy.md
|
|
3264
|
+
* await service.dump("my-strategy");
|
|
3265
|
+
*
|
|
3266
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
3267
|
+
* await service.dump("my-strategy", "./custom/path");
|
|
3268
|
+
* ```
|
|
3269
|
+
*/
|
|
3270
|
+
this.dump = async (strategyName, path = "./logs/live") => {
|
|
3271
|
+
this.loggerService.log("liveMarkdownService dump", {
|
|
3272
|
+
strategyName,
|
|
3273
|
+
path,
|
|
3274
|
+
});
|
|
3275
|
+
const storage = this.getStorage(strategyName);
|
|
3276
|
+
await storage.dump(strategyName, path);
|
|
3277
|
+
};
|
|
3278
|
+
/**
|
|
3279
|
+
* Clears accumulated event data from storage.
|
|
3280
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
3281
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
3282
|
+
*
|
|
3283
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
3284
|
+
*
|
|
3285
|
+
* @example
|
|
3286
|
+
* ```typescript
|
|
3287
|
+
* const service = new LiveMarkdownService();
|
|
3288
|
+
*
|
|
3289
|
+
* // Clear specific strategy data
|
|
3290
|
+
* await service.clear("my-strategy");
|
|
3291
|
+
*
|
|
3292
|
+
* // Clear all strategies' data
|
|
3293
|
+
* await service.clear();
|
|
3294
|
+
* ```
|
|
3295
|
+
*/
|
|
3296
|
+
this.clear = async (strategyName) => {
|
|
3297
|
+
this.loggerService.log("liveMarkdownService clear", {
|
|
3298
|
+
strategyName,
|
|
3299
|
+
});
|
|
3300
|
+
this.getStorage.clear(strategyName);
|
|
3301
|
+
};
|
|
3302
|
+
/**
|
|
3303
|
+
* Initializes the service by subscribing to live signal events.
|
|
3304
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
3305
|
+
* Automatically called on first use.
|
|
3306
|
+
*
|
|
3307
|
+
* @example
|
|
3308
|
+
* ```typescript
|
|
3309
|
+
* const service = new LiveMarkdownService();
|
|
3310
|
+
* await service.init(); // Subscribe to live events
|
|
3311
|
+
* ```
|
|
3312
|
+
*/
|
|
3313
|
+
this.init = singleshot(async () => {
|
|
3314
|
+
this.loggerService.log("liveMarkdownService init");
|
|
3315
|
+
signalLiveEmitter.subscribe(this.tick);
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
/**
|
|
3321
|
+
* @class ExchangeValidationService
|
|
3322
|
+
* Service for managing and validating exchange configurations
|
|
3323
|
+
*/
|
|
3324
|
+
class ExchangeValidationService {
|
|
3325
|
+
constructor() {
|
|
3326
|
+
/**
|
|
3327
|
+
* @private
|
|
3328
|
+
* @readonly
|
|
3329
|
+
* Injected logger service instance
|
|
3330
|
+
*/
|
|
3331
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3332
|
+
/**
|
|
3333
|
+
* @private
|
|
3334
|
+
* Map storing exchange schemas by exchange name
|
|
3335
|
+
*/
|
|
3336
|
+
this._exchangeMap = new Map();
|
|
3337
|
+
/**
|
|
3338
|
+
* Adds an exchange schema to the validation service
|
|
3339
|
+
* @public
|
|
3340
|
+
* @throws {Error} If exchangeName already exists
|
|
3341
|
+
*/
|
|
3342
|
+
this.addExchange = (exchangeName, exchangeSchema) => {
|
|
3343
|
+
this.loggerService.log("exchangeValidationService addExchange", {
|
|
3344
|
+
exchangeName,
|
|
3345
|
+
exchangeSchema,
|
|
3346
|
+
});
|
|
3347
|
+
if (this._exchangeMap.has(exchangeName)) {
|
|
3348
|
+
throw new Error(`exchange ${exchangeName} already exist`);
|
|
3349
|
+
}
|
|
3350
|
+
this._exchangeMap.set(exchangeName, exchangeSchema);
|
|
3351
|
+
};
|
|
3352
|
+
/**
|
|
3353
|
+
* Validates the existence of an exchange
|
|
3354
|
+
* @public
|
|
3355
|
+
* @throws {Error} If exchangeName is not found
|
|
3356
|
+
* Memoized function to cache validation results
|
|
3357
|
+
*/
|
|
3358
|
+
this.validate = memoize(([exchangeName]) => exchangeName, (exchangeName, source) => {
|
|
3359
|
+
this.loggerService.log("exchangeValidationService validate", {
|
|
3360
|
+
exchangeName,
|
|
3361
|
+
source,
|
|
3362
|
+
});
|
|
3363
|
+
const exchange = this._exchangeMap.get(exchangeName);
|
|
3364
|
+
if (!exchange) {
|
|
3365
|
+
throw new Error(`exchange ${exchangeName} not found source=${source}`);
|
|
3366
|
+
}
|
|
3367
|
+
return true;
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
/**
|
|
3373
|
+
* @class StrategyValidationService
|
|
3374
|
+
* Service for managing and validating strategy configurations
|
|
3375
|
+
*/
|
|
3376
|
+
class StrategyValidationService {
|
|
3377
|
+
constructor() {
|
|
3378
|
+
/**
|
|
3379
|
+
* @private
|
|
3380
|
+
* @readonly
|
|
3381
|
+
* Injected logger service instance
|
|
3382
|
+
*/
|
|
3383
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3384
|
+
/**
|
|
3385
|
+
* @private
|
|
3386
|
+
* Map storing strategy schemas by strategy name
|
|
3387
|
+
*/
|
|
3388
|
+
this._strategyMap = new Map();
|
|
3389
|
+
/**
|
|
3390
|
+
* Adds a strategy schema to the validation service
|
|
3391
|
+
* @public
|
|
3392
|
+
* @throws {Error} If strategyName already exists
|
|
3393
|
+
*/
|
|
3394
|
+
this.addStrategy = (strategyName, strategySchema) => {
|
|
3395
|
+
this.loggerService.log("strategyValidationService addStrategy", {
|
|
3396
|
+
strategyName,
|
|
3397
|
+
strategySchema,
|
|
3398
|
+
});
|
|
3399
|
+
if (this._strategyMap.has(strategyName)) {
|
|
3400
|
+
throw new Error(`strategy ${strategyName} already exist`);
|
|
3401
|
+
}
|
|
3402
|
+
this._strategyMap.set(strategyName, strategySchema);
|
|
3403
|
+
};
|
|
3404
|
+
/**
|
|
3405
|
+
* Validates the existence of a strategy
|
|
3406
|
+
* @public
|
|
3407
|
+
* @throws {Error} If strategyName is not found
|
|
3408
|
+
* Memoized function to cache validation results
|
|
3409
|
+
*/
|
|
3410
|
+
this.validate = memoize(([strategyName]) => strategyName, (strategyName, source) => {
|
|
3411
|
+
this.loggerService.log("strategyValidationService validate", {
|
|
3412
|
+
strategyName,
|
|
3413
|
+
source,
|
|
3414
|
+
});
|
|
3415
|
+
const strategy = this._strategyMap.get(strategyName);
|
|
3416
|
+
if (!strategy) {
|
|
3417
|
+
throw new Error(`strategy ${strategyName} not found source=${source}`);
|
|
3418
|
+
}
|
|
3419
|
+
return true;
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
/**
|
|
3425
|
+
* @class FrameValidationService
|
|
3426
|
+
* Service for managing and validating frame configurations
|
|
3427
|
+
*/
|
|
3428
|
+
class FrameValidationService {
|
|
3429
|
+
constructor() {
|
|
3430
|
+
/**
|
|
3431
|
+
* @private
|
|
3432
|
+
* @readonly
|
|
3433
|
+
* Injected logger service instance
|
|
3434
|
+
*/
|
|
3435
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3436
|
+
/**
|
|
3437
|
+
* @private
|
|
3438
|
+
* Map storing frame schemas by frame name
|
|
3439
|
+
*/
|
|
3440
|
+
this._frameMap = new Map();
|
|
3441
|
+
/**
|
|
3442
|
+
* Adds a frame schema to the validation service
|
|
3443
|
+
* @public
|
|
3444
|
+
* @throws {Error} If frameName already exists
|
|
3445
|
+
*/
|
|
3446
|
+
this.addFrame = (frameName, frameSchema) => {
|
|
3447
|
+
this.loggerService.log("frameValidationService addFrame", {
|
|
3448
|
+
frameName,
|
|
3449
|
+
frameSchema,
|
|
3450
|
+
});
|
|
3451
|
+
if (this._frameMap.has(frameName)) {
|
|
3452
|
+
throw new Error(`frame ${frameName} already exist`);
|
|
3453
|
+
}
|
|
3454
|
+
this._frameMap.set(frameName, frameSchema);
|
|
3455
|
+
};
|
|
3456
|
+
/**
|
|
3457
|
+
* Validates the existence of a frame
|
|
3458
|
+
* @public
|
|
3459
|
+
* @throws {Error} If frameName is not found
|
|
3460
|
+
* Memoized function to cache validation results
|
|
3461
|
+
*/
|
|
3462
|
+
this.validate = memoize(([frameName]) => frameName, (frameName, source) => {
|
|
3463
|
+
this.loggerService.log("frameValidationService validate", {
|
|
3464
|
+
frameName,
|
|
3465
|
+
source,
|
|
3466
|
+
});
|
|
3467
|
+
const frame = this._frameMap.get(frameName);
|
|
3468
|
+
if (!frame) {
|
|
3469
|
+
throw new Error(`frame ${frameName} not found source=${source}`);
|
|
3470
|
+
}
|
|
3471
|
+
return true;
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
{
|
|
3477
|
+
provide(TYPES.loggerService, () => new LoggerService());
|
|
3478
|
+
}
|
|
3479
|
+
{
|
|
3480
|
+
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
3481
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
3482
|
+
}
|
|
3483
|
+
{
|
|
3484
|
+
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
3485
|
+
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
3486
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
3487
|
+
}
|
|
3488
|
+
{
|
|
3489
|
+
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
3490
|
+
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
|
|
3491
|
+
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
|
|
3492
|
+
}
|
|
3493
|
+
{
|
|
3494
|
+
provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
|
|
3495
|
+
provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
|
|
3496
|
+
provide(TYPES.frameGlobalService, () => new FrameGlobalService());
|
|
3497
|
+
provide(TYPES.liveGlobalService, () => new LiveGlobalService());
|
|
3498
|
+
provide(TYPES.backtestGlobalService, () => new BacktestGlobalService());
|
|
3499
|
+
}
|
|
3500
|
+
{
|
|
3501
|
+
provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
|
|
3502
|
+
provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
|
|
3503
|
+
}
|
|
3504
|
+
{
|
|
3505
|
+
provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
|
|
3506
|
+
provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
|
|
3507
|
+
}
|
|
3508
|
+
{
|
|
3509
|
+
provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
|
|
3510
|
+
provide(TYPES.liveMarkdownService, () => new LiveMarkdownService());
|
|
3511
|
+
}
|
|
3512
|
+
{
|
|
3513
|
+
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
3514
|
+
provide(TYPES.strategyValidationService, () => new StrategyValidationService());
|
|
3515
|
+
provide(TYPES.frameValidationService, () => new FrameValidationService());
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
const baseServices = {
|
|
3519
|
+
loggerService: inject(TYPES.loggerService),
|
|
3520
|
+
};
|
|
3521
|
+
const contextServices = {
|
|
3522
|
+
executionContextService: inject(TYPES.executionContextService),
|
|
3523
|
+
methodContextService: inject(TYPES.methodContextService),
|
|
3524
|
+
};
|
|
3525
|
+
const connectionServices = {
|
|
3526
|
+
exchangeConnectionService: inject(TYPES.exchangeConnectionService),
|
|
3527
|
+
strategyConnectionService: inject(TYPES.strategyConnectionService),
|
|
3528
|
+
frameConnectionService: inject(TYPES.frameConnectionService),
|
|
1401
3529
|
};
|
|
1402
3530
|
const schemaServices = {
|
|
1403
3531
|
exchangeSchemaService: inject(TYPES.exchangeSchemaService),
|
|
@@ -1419,6 +3547,15 @@ const logicPublicServices = {
|
|
|
1419
3547
|
backtestLogicPublicService: inject(TYPES.backtestLogicPublicService),
|
|
1420
3548
|
liveLogicPublicService: inject(TYPES.liveLogicPublicService),
|
|
1421
3549
|
};
|
|
3550
|
+
const markdownServices = {
|
|
3551
|
+
backtestMarkdownService: inject(TYPES.backtestMarkdownService),
|
|
3552
|
+
liveMarkdownService: inject(TYPES.liveMarkdownService),
|
|
3553
|
+
};
|
|
3554
|
+
const validationServices = {
|
|
3555
|
+
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
3556
|
+
strategyValidationService: inject(TYPES.strategyValidationService),
|
|
3557
|
+
frameValidationService: inject(TYPES.frameValidationService),
|
|
3558
|
+
};
|
|
1422
3559
|
const backtest = {
|
|
1423
3560
|
...baseServices,
|
|
1424
3561
|
...contextServices,
|
|
@@ -1427,10 +3564,29 @@ const backtest = {
|
|
|
1427
3564
|
...globalServices,
|
|
1428
3565
|
...logicPrivateServices,
|
|
1429
3566
|
...logicPublicServices,
|
|
3567
|
+
...markdownServices,
|
|
3568
|
+
...validationServices,
|
|
1430
3569
|
};
|
|
1431
3570
|
init();
|
|
1432
3571
|
var backtest$1 = backtest;
|
|
1433
3572
|
|
|
3573
|
+
/**
|
|
3574
|
+
* Sets custom logger implementation for the framework.
|
|
3575
|
+
*
|
|
3576
|
+
* All log messages from internal services will be forwarded to the provided logger
|
|
3577
|
+
* with automatic context injection (strategyName, exchangeName, symbol, etc.).
|
|
3578
|
+
*
|
|
3579
|
+
* @param logger - Custom logger implementing ILogger interface
|
|
3580
|
+
*
|
|
3581
|
+
* @example
|
|
3582
|
+
* ```typescript
|
|
3583
|
+
* setLogger({
|
|
3584
|
+
* log: (topic, ...args) => console.log(topic, args),
|
|
3585
|
+
* debug: (topic, ...args) => console.debug(topic, args),
|
|
3586
|
+
* info: (topic, ...args) => console.info(topic, args),
|
|
3587
|
+
* });
|
|
3588
|
+
* ```
|
|
3589
|
+
*/
|
|
1434
3590
|
async function setLogger(logger) {
|
|
1435
3591
|
backtest$1.loggerService.setLogger(logger);
|
|
1436
3592
|
}
|
|
@@ -1438,136 +3594,323 @@ async function setLogger(logger) {
|
|
|
1438
3594
|
const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
|
|
1439
3595
|
const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
|
|
1440
3596
|
const ADD_FRAME_METHOD_NAME = "add.addFrame";
|
|
3597
|
+
/**
|
|
3598
|
+
* Registers a trading strategy in the framework.
|
|
3599
|
+
*
|
|
3600
|
+
* The strategy will be validated for:
|
|
3601
|
+
* - Signal validation (prices, TP/SL logic, timestamps)
|
|
3602
|
+
* - Interval throttling (prevents signal spam)
|
|
3603
|
+
* - Crash-safe persistence in live mode
|
|
3604
|
+
*
|
|
3605
|
+
* @param strategySchema - Strategy configuration object
|
|
3606
|
+
* @param strategySchema.strategyName - Unique strategy identifier
|
|
3607
|
+
* @param strategySchema.interval - Signal generation interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h")
|
|
3608
|
+
* @param strategySchema.getSignal - Async function that generates trading signals
|
|
3609
|
+
* @param strategySchema.callbacks - Optional lifecycle callbacks (onOpen, onClose)
|
|
3610
|
+
*
|
|
3611
|
+
* @example
|
|
3612
|
+
* ```typescript
|
|
3613
|
+
* addStrategy({
|
|
3614
|
+
* strategyName: "my-strategy",
|
|
3615
|
+
* interval: "5m",
|
|
3616
|
+
* getSignal: async (symbol) => ({
|
|
3617
|
+
* position: "long",
|
|
3618
|
+
* priceOpen: 50000,
|
|
3619
|
+
* priceTakeProfit: 51000,
|
|
3620
|
+
* priceStopLoss: 49000,
|
|
3621
|
+
* minuteEstimatedTime: 60,
|
|
3622
|
+
* timestamp: Date.now(),
|
|
3623
|
+
* }),
|
|
3624
|
+
* callbacks: {
|
|
3625
|
+
* onOpen: (backtest, symbol, signal) => console.log("Signal opened"),
|
|
3626
|
+
* onClose: (backtest, symbol, priceClose, signal) => console.log("Signal closed"),
|
|
3627
|
+
* },
|
|
3628
|
+
* });
|
|
3629
|
+
* ```
|
|
3630
|
+
*/
|
|
1441
3631
|
function addStrategy(strategySchema) {
|
|
1442
3632
|
backtest$1.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
|
|
1443
3633
|
strategySchema,
|
|
1444
3634
|
});
|
|
3635
|
+
backtest$1.strategyValidationService.addStrategy(strategySchema.strategyName, strategySchema);
|
|
1445
3636
|
backtest$1.strategySchemaService.register(strategySchema.strategyName, strategySchema);
|
|
1446
3637
|
}
|
|
3638
|
+
/**
|
|
3639
|
+
* Registers an exchange data source in the framework.
|
|
3640
|
+
*
|
|
3641
|
+
* The exchange provides:
|
|
3642
|
+
* - Historical candle data via getCandles
|
|
3643
|
+
* - Price/quantity formatting for the exchange
|
|
3644
|
+
* - VWAP calculation from last 5 1m candles
|
|
3645
|
+
*
|
|
3646
|
+
* @param exchangeSchema - Exchange configuration object
|
|
3647
|
+
* @param exchangeSchema.exchangeName - Unique exchange identifier
|
|
3648
|
+
* @param exchangeSchema.getCandles - Async function to fetch candle data
|
|
3649
|
+
* @param exchangeSchema.formatPrice - Async function to format prices
|
|
3650
|
+
* @param exchangeSchema.formatQuantity - Async function to format quantities
|
|
3651
|
+
* @param exchangeSchema.callbacks - Optional callback for candle data events
|
|
3652
|
+
*
|
|
3653
|
+
* @example
|
|
3654
|
+
* ```typescript
|
|
3655
|
+
* addExchange({
|
|
3656
|
+
* exchangeName: "binance",
|
|
3657
|
+
* getCandles: async (symbol, interval, since, limit) => {
|
|
3658
|
+
* // Fetch from Binance API or database
|
|
3659
|
+
* return [{
|
|
3660
|
+
* timestamp: Date.now(),
|
|
3661
|
+
* open: 50000,
|
|
3662
|
+
* high: 51000,
|
|
3663
|
+
* low: 49000,
|
|
3664
|
+
* close: 50500,
|
|
3665
|
+
* volume: 1000,
|
|
3666
|
+
* }];
|
|
3667
|
+
* },
|
|
3668
|
+
* formatPrice: async (symbol, price) => price.toFixed(2),
|
|
3669
|
+
* formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
|
|
3670
|
+
* });
|
|
3671
|
+
* ```
|
|
3672
|
+
*/
|
|
1447
3673
|
function addExchange(exchangeSchema) {
|
|
1448
3674
|
backtest$1.loggerService.info(ADD_EXCHANGE_METHOD_NAME, {
|
|
1449
3675
|
exchangeSchema,
|
|
1450
3676
|
});
|
|
3677
|
+
backtest$1.exchangeValidationService.addExchange(exchangeSchema.exchangeName, exchangeSchema);
|
|
1451
3678
|
backtest$1.exchangeSchemaService.register(exchangeSchema.exchangeName, exchangeSchema);
|
|
1452
3679
|
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Registers a timeframe generator for backtesting.
|
|
3682
|
+
*
|
|
3683
|
+
* The frame defines:
|
|
3684
|
+
* - Start and end dates for backtest period
|
|
3685
|
+
* - Interval for timeframe generation
|
|
3686
|
+
* - Callback for timeframe generation events
|
|
3687
|
+
*
|
|
3688
|
+
* @param frameSchema - Frame configuration object
|
|
3689
|
+
* @param frameSchema.frameName - Unique frame identifier
|
|
3690
|
+
* @param frameSchema.interval - Timeframe interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "12h" | "1d" | "3d")
|
|
3691
|
+
* @param frameSchema.startDate - Start date for timeframe generation
|
|
3692
|
+
* @param frameSchema.endDate - End date for timeframe generation
|
|
3693
|
+
* @param frameSchema.callbacks - Optional callback for timeframe events
|
|
3694
|
+
*
|
|
3695
|
+
* @example
|
|
3696
|
+
* ```typescript
|
|
3697
|
+
* addFrame({
|
|
3698
|
+
* frameName: "1d-backtest",
|
|
3699
|
+
* interval: "1m",
|
|
3700
|
+
* startDate: new Date("2024-01-01T00:00:00Z"),
|
|
3701
|
+
* endDate: new Date("2024-01-02T00:00:00Z"),
|
|
3702
|
+
* callbacks: {
|
|
3703
|
+
* onTimeframe: (timeframe, startDate, endDate, interval) => {
|
|
3704
|
+
* console.log(`Generated ${timeframe.length} timeframes`);
|
|
3705
|
+
* },
|
|
3706
|
+
* },
|
|
3707
|
+
* });
|
|
3708
|
+
* ```
|
|
3709
|
+
*/
|
|
1453
3710
|
function addFrame(frameSchema) {
|
|
1454
3711
|
backtest$1.loggerService.info(ADD_FRAME_METHOD_NAME, {
|
|
1455
3712
|
frameSchema,
|
|
1456
3713
|
});
|
|
3714
|
+
backtest$1.frameValidationService.addFrame(frameSchema.frameName, frameSchema);
|
|
1457
3715
|
backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
|
|
1458
3716
|
}
|
|
1459
3717
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
3718
|
+
const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
|
|
3719
|
+
const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
|
|
3720
|
+
const LISTEN_SIGNAL_LIVE_METHOD_NAME = "event.listenSignalLive";
|
|
3721
|
+
const LISTEN_SIGNAL_LIVE_ONCE_METHOD_NAME = "event.listenSignalLiveOnce";
|
|
3722
|
+
const LISTEN_SIGNAL_BACKTEST_METHOD_NAME = "event.listenSignalBacktest";
|
|
3723
|
+
const LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME = "event.listenSignalBacktestOnce";
|
|
3724
|
+
const LISTEN_ERROR_METHOD_NAME = "event.listenError";
|
|
3725
|
+
/**
|
|
3726
|
+
* Subscribes to all signal events with queued async processing.
|
|
3727
|
+
*
|
|
3728
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
3729
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
3730
|
+
*
|
|
3731
|
+
* @param fn - Callback function to handle signal events (idle, opened, active, closed)
|
|
3732
|
+
* @returns Unsubscribe function to stop listening
|
|
3733
|
+
*
|
|
3734
|
+
* @example
|
|
3735
|
+
* ```typescript
|
|
3736
|
+
* import { listenSignal } from "./function/event";
|
|
3737
|
+
*
|
|
3738
|
+
* const unsubscribe = listenSignal((event) => {
|
|
3739
|
+
* if (event.action === "opened") {
|
|
3740
|
+
* console.log("New signal opened:", event.signal);
|
|
3741
|
+
* } else if (event.action === "closed") {
|
|
3742
|
+
* console.log("Signal closed with PNL:", event.pnl.pnlPercentage);
|
|
3743
|
+
* }
|
|
3744
|
+
* });
|
|
3745
|
+
*
|
|
3746
|
+
* // Later: stop listening
|
|
3747
|
+
* unsubscribe();
|
|
3748
|
+
* ```
|
|
3749
|
+
*/
|
|
3750
|
+
function listenSignal(fn) {
|
|
3751
|
+
backtest$1.loggerService.log(LISTEN_SIGNAL_METHOD_NAME);
|
|
3752
|
+
return signalEmitter.subscribe(queued(async (event) => fn(event)));
|
|
1473
3753
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
`${closedCount} trades`,
|
|
1510
|
-
`Win: ${winCount}`,
|
|
1511
|
-
`Loss: ${lossCount}`,
|
|
1512
|
-
"-",
|
|
1513
|
-
`WR: ${closedCount > 0 ? ((winCount / closedCount) * 100).toFixed(1) : 0}%`,
|
|
1514
|
-
`${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`,
|
|
1515
|
-
]);
|
|
1516
|
-
console.log("\n");
|
|
1517
|
-
console.log(table.toString());
|
|
1518
|
-
console.log("\n");
|
|
3754
|
+
/**
|
|
3755
|
+
* Subscribes to filtered signal events with one-time execution.
|
|
3756
|
+
*
|
|
3757
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
3758
|
+
* and automatically unsubscribes. Useful for waiting for specific signal conditions.
|
|
3759
|
+
*
|
|
3760
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
3761
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
3762
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
3763
|
+
*
|
|
3764
|
+
* @example
|
|
3765
|
+
* ```typescript
|
|
3766
|
+
* import { listenSignalOnce } from "./function/event";
|
|
3767
|
+
*
|
|
3768
|
+
* // Wait for first take profit hit
|
|
3769
|
+
* listenSignalOnce(
|
|
3770
|
+
* (event) => event.action === "closed" && event.closeReason === "take_profit",
|
|
3771
|
+
* (event) => {
|
|
3772
|
+
* console.log("Take profit hit! PNL:", event.pnl.pnlPercentage);
|
|
3773
|
+
* }
|
|
3774
|
+
* );
|
|
3775
|
+
*
|
|
3776
|
+
* // Wait for any signal to close on BTCUSDT
|
|
3777
|
+
* const cancel = listenSignalOnce(
|
|
3778
|
+
* (event) => event.action === "closed" && event.signal.symbol === "BTCUSDT",
|
|
3779
|
+
* (event) => console.log("BTCUSDT signal closed")
|
|
3780
|
+
* );
|
|
3781
|
+
*
|
|
3782
|
+
* // Cancel if needed before event fires
|
|
3783
|
+
* cancel();
|
|
3784
|
+
* ```
|
|
3785
|
+
*/
|
|
3786
|
+
function listenSignalOnce(filterFn, fn) {
|
|
3787
|
+
backtest$1.loggerService.log(LISTEN_SIGNAL_ONCE_METHOD_NAME);
|
|
3788
|
+
return signalEmitter.filter(filterFn).once(fn);
|
|
1519
3789
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
3790
|
+
/**
|
|
3791
|
+
* Subscribes to live trading signal events with queued async processing.
|
|
3792
|
+
*
|
|
3793
|
+
* Only receives events from Live.run() execution.
|
|
3794
|
+
* Events are processed sequentially in order received.
|
|
3795
|
+
*
|
|
3796
|
+
* @param fn - Callback function to handle live signal events
|
|
3797
|
+
* @returns Unsubscribe function to stop listening
|
|
3798
|
+
*
|
|
3799
|
+
* @example
|
|
3800
|
+
* ```typescript
|
|
3801
|
+
* import { listenSignalLive } from "./function/event";
|
|
3802
|
+
*
|
|
3803
|
+
* const unsubscribe = listenSignalLive((event) => {
|
|
3804
|
+
* if (event.action === "closed") {
|
|
3805
|
+
* console.log("Live signal closed:", event.pnl.pnlPercentage);
|
|
3806
|
+
* }
|
|
3807
|
+
* });
|
|
3808
|
+
* ```
|
|
3809
|
+
*/
|
|
3810
|
+
function listenSignalLive(fn) {
|
|
3811
|
+
backtest$1.loggerService.log(LISTEN_SIGNAL_LIVE_METHOD_NAME);
|
|
3812
|
+
return signalLiveEmitter.subscribe(queued(async (event) => fn(event)));
|
|
1532
3813
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
});
|
|
3814
|
+
/**
|
|
3815
|
+
* Subscribes to filtered live signal events with one-time execution.
|
|
3816
|
+
*
|
|
3817
|
+
* Only receives events from Live.run() execution.
|
|
3818
|
+
* Executes callback once and automatically unsubscribes.
|
|
3819
|
+
*
|
|
3820
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
3821
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
3822
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
3823
|
+
*
|
|
3824
|
+
* @example
|
|
3825
|
+
* ```typescript
|
|
3826
|
+
* import { listenSignalLiveOnce } from "./function/event";
|
|
3827
|
+
*
|
|
3828
|
+
* // Wait for first live take profit hit
|
|
3829
|
+
* listenSignalLiveOnce(
|
|
3830
|
+
* (event) => event.action === "closed" && event.closeReason === "take_profit",
|
|
3831
|
+
* (event) => console.log("Live take profit:", event.pnl.pnlPercentage)
|
|
3832
|
+
* );
|
|
3833
|
+
* ```
|
|
3834
|
+
*/
|
|
3835
|
+
function listenSignalLiveOnce(filterFn, fn) {
|
|
3836
|
+
backtest$1.loggerService.log(LISTEN_SIGNAL_LIVE_ONCE_METHOD_NAME);
|
|
3837
|
+
return signalLiveEmitter.filter(filterFn).once(fn);
|
|
1558
3838
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
3839
|
+
/**
|
|
3840
|
+
* Subscribes to backtest signal events with queued async processing.
|
|
3841
|
+
*
|
|
3842
|
+
* Only receives events from Backtest.run() execution.
|
|
3843
|
+
* Events are processed sequentially in order received.
|
|
3844
|
+
*
|
|
3845
|
+
* @param fn - Callback function to handle backtest signal events
|
|
3846
|
+
* @returns Unsubscribe function to stop listening
|
|
3847
|
+
*
|
|
3848
|
+
* @example
|
|
3849
|
+
* ```typescript
|
|
3850
|
+
* import { listenSignalBacktest } from "./function/event";
|
|
3851
|
+
*
|
|
3852
|
+
* const unsubscribe = listenSignalBacktest((event) => {
|
|
3853
|
+
* if (event.action === "closed") {
|
|
3854
|
+
* console.log("Backtest signal closed:", event.pnl.pnlPercentage);
|
|
3855
|
+
* }
|
|
3856
|
+
* });
|
|
3857
|
+
* ```
|
|
3858
|
+
*/
|
|
3859
|
+
function listenSignalBacktest(fn) {
|
|
3860
|
+
backtest$1.loggerService.log(LISTEN_SIGNAL_BACKTEST_METHOD_NAME);
|
|
3861
|
+
return signalBacktestEmitter.subscribe(queued(async (event) => fn(event)));
|
|
1565
3862
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
3863
|
+
/**
|
|
3864
|
+
* Subscribes to filtered backtest signal events with one-time execution.
|
|
3865
|
+
*
|
|
3866
|
+
* Only receives events from Backtest.run() execution.
|
|
3867
|
+
* Executes callback once and automatically unsubscribes.
|
|
3868
|
+
*
|
|
3869
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
3870
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
3871
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
3872
|
+
*
|
|
3873
|
+
* @example
|
|
3874
|
+
* ```typescript
|
|
3875
|
+
* import { listenSignalBacktestOnce } from "./function/event";
|
|
3876
|
+
*
|
|
3877
|
+
* // Wait for first backtest stop loss hit
|
|
3878
|
+
* listenSignalBacktestOnce(
|
|
3879
|
+
* (event) => event.action === "closed" && event.closeReason === "stop_loss",
|
|
3880
|
+
* (event) => console.log("Backtest stop loss:", event.pnl.pnlPercentage)
|
|
3881
|
+
* );
|
|
3882
|
+
* ```
|
|
3883
|
+
*/
|
|
3884
|
+
function listenSignalBacktestOnce(filterFn, fn) {
|
|
3885
|
+
backtest$1.loggerService.log(LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME);
|
|
3886
|
+
return signalBacktestEmitter.filter(filterFn).once(fn);
|
|
3887
|
+
}
|
|
3888
|
+
/**
|
|
3889
|
+
* Subscribes to background execution errors with queued async processing.
|
|
3890
|
+
*
|
|
3891
|
+
* Listens to errors caught in Live.background() and Backtest.background() execution.
|
|
3892
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
3893
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
3894
|
+
*
|
|
3895
|
+
* @param fn - Callback function to handle error events
|
|
3896
|
+
* @returns Unsubscribe function to stop listening
|
|
3897
|
+
*
|
|
3898
|
+
* @example
|
|
3899
|
+
* ```typescript
|
|
3900
|
+
* import { listenError } from "./function/event";
|
|
3901
|
+
*
|
|
3902
|
+
* const unsubscribe = listenError((error) => {
|
|
3903
|
+
* console.error("Background execution error:", error.message);
|
|
3904
|
+
* // Log to monitoring service, send alerts, etc.
|
|
3905
|
+
* });
|
|
3906
|
+
*
|
|
3907
|
+
* // Later: stop listening
|
|
3908
|
+
* unsubscribe();
|
|
3909
|
+
* ```
|
|
3910
|
+
*/
|
|
3911
|
+
function listenError(fn) {
|
|
3912
|
+
backtest$1.loggerService.log(LISTEN_ERROR_METHOD_NAME);
|
|
3913
|
+
return errorEmitter.subscribe(queued(async (error) => fn(error)));
|
|
1571
3914
|
}
|
|
1572
3915
|
|
|
1573
3916
|
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
@@ -1576,6 +3919,23 @@ const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
|
|
|
1576
3919
|
const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
1577
3920
|
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
1578
3921
|
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
3922
|
+
/**
|
|
3923
|
+
* Fetches historical candle data from the registered exchange.
|
|
3924
|
+
*
|
|
3925
|
+
* Candles are fetched backwards from the current execution context time.
|
|
3926
|
+
* Uses the exchange's getCandles implementation.
|
|
3927
|
+
*
|
|
3928
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3929
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
3930
|
+
* @param limit - Number of candles to fetch
|
|
3931
|
+
* @returns Promise resolving to array of candle data
|
|
3932
|
+
*
|
|
3933
|
+
* @example
|
|
3934
|
+
* ```typescript
|
|
3935
|
+
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
3936
|
+
* console.log(candles[0]); // { timestamp, open, high, low, close, volume }
|
|
3937
|
+
* ```
|
|
3938
|
+
*/
|
|
1579
3939
|
async function getCandles(symbol, interval, limit) {
|
|
1580
3940
|
backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
|
|
1581
3941
|
symbol,
|
|
@@ -1584,12 +3944,45 @@ async function getCandles(symbol, interval, limit) {
|
|
|
1584
3944
|
});
|
|
1585
3945
|
return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
1586
3946
|
}
|
|
3947
|
+
/**
|
|
3948
|
+
* Calculates VWAP (Volume Weighted Average Price) for a symbol.
|
|
3949
|
+
*
|
|
3950
|
+
* Uses the last 5 1-minute candles to calculate:
|
|
3951
|
+
* - Typical Price = (high + low + close) / 3
|
|
3952
|
+
* - VWAP = sum(typical_price * volume) / sum(volume)
|
|
3953
|
+
*
|
|
3954
|
+
* If volume is zero, returns simple average of close prices.
|
|
3955
|
+
*
|
|
3956
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3957
|
+
* @returns Promise resolving to VWAP price
|
|
3958
|
+
*
|
|
3959
|
+
* @example
|
|
3960
|
+
* ```typescript
|
|
3961
|
+
* const vwap = await getAveragePrice("BTCUSDT");
|
|
3962
|
+
* console.log(vwap); // 50125.43
|
|
3963
|
+
* ```
|
|
3964
|
+
*/
|
|
1587
3965
|
async function getAveragePrice(symbol) {
|
|
1588
3966
|
backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
|
|
1589
3967
|
symbol,
|
|
1590
3968
|
});
|
|
1591
3969
|
return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
|
|
1592
3970
|
}
|
|
3971
|
+
/**
|
|
3972
|
+
* Formats a price value according to exchange rules.
|
|
3973
|
+
*
|
|
3974
|
+
* Uses the exchange's formatPrice implementation for proper decimal places.
|
|
3975
|
+
*
|
|
3976
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3977
|
+
* @param price - Raw price value
|
|
3978
|
+
* @returns Promise resolving to formatted price string
|
|
3979
|
+
*
|
|
3980
|
+
* @example
|
|
3981
|
+
* ```typescript
|
|
3982
|
+
* const formatted = await formatPrice("BTCUSDT", 50000.123456);
|
|
3983
|
+
* console.log(formatted); // "50000.12"
|
|
3984
|
+
* ```
|
|
3985
|
+
*/
|
|
1593
3986
|
async function formatPrice(symbol, price) {
|
|
1594
3987
|
backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
|
|
1595
3988
|
symbol,
|
|
@@ -1597,6 +3990,21 @@ async function formatPrice(symbol, price) {
|
|
|
1597
3990
|
});
|
|
1598
3991
|
return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
|
|
1599
3992
|
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Formats a quantity value according to exchange rules.
|
|
3995
|
+
*
|
|
3996
|
+
* Uses the exchange's formatQuantity implementation for proper decimal places.
|
|
3997
|
+
*
|
|
3998
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3999
|
+
* @param quantity - Raw quantity value
|
|
4000
|
+
* @returns Promise resolving to formatted quantity string
|
|
4001
|
+
*
|
|
4002
|
+
* @example
|
|
4003
|
+
* ```typescript
|
|
4004
|
+
* const formatted = await formatQuantity("BTCUSDT", 0.123456789);
|
|
4005
|
+
* console.log(formatted); // "0.12345678"
|
|
4006
|
+
* ```
|
|
4007
|
+
*/
|
|
1600
4008
|
async function formatQuantity(symbol, quantity) {
|
|
1601
4009
|
backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
|
|
1602
4010
|
symbol,
|
|
@@ -1604,11 +4012,40 @@ async function formatQuantity(symbol, quantity) {
|
|
|
1604
4012
|
});
|
|
1605
4013
|
return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
1606
4014
|
}
|
|
4015
|
+
/**
|
|
4016
|
+
* Gets the current date from execution context.
|
|
4017
|
+
*
|
|
4018
|
+
* In backtest mode: returns the current timeframe date being processed
|
|
4019
|
+
* In live mode: returns current real-time date
|
|
4020
|
+
*
|
|
4021
|
+
* @returns Promise resolving to current execution context date
|
|
4022
|
+
*
|
|
4023
|
+
* @example
|
|
4024
|
+
* ```typescript
|
|
4025
|
+
* const date = await getDate();
|
|
4026
|
+
* console.log(date); // 2024-01-01T12:00:00.000Z
|
|
4027
|
+
* ```
|
|
4028
|
+
*/
|
|
1607
4029
|
async function getDate() {
|
|
1608
4030
|
backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
|
|
1609
4031
|
const { when } = backtest$1.executionContextService.context;
|
|
1610
4032
|
return new Date(when.getTime());
|
|
1611
4033
|
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Gets the current execution mode.
|
|
4036
|
+
*
|
|
4037
|
+
* @returns Promise resolving to "backtest" or "live"
|
|
4038
|
+
*
|
|
4039
|
+
* @example
|
|
4040
|
+
* ```typescript
|
|
4041
|
+
* const mode = await getMode();
|
|
4042
|
+
* if (mode === "backtest") {
|
|
4043
|
+
* console.log("Running in backtest mode");
|
|
4044
|
+
* } else {
|
|
4045
|
+
* console.log("Running in live mode");
|
|
4046
|
+
* }
|
|
4047
|
+
* ```
|
|
4048
|
+
*/
|
|
1612
4049
|
async function getMode() {
|
|
1613
4050
|
backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
|
|
1614
4051
|
const { backtest: bt } = backtest$1.executionContextService.context;
|
|
@@ -1616,8 +4053,37 @@ async function getMode() {
|
|
|
1616
4053
|
}
|
|
1617
4054
|
|
|
1618
4055
|
const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
|
|
4056
|
+
const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
|
|
4057
|
+
const BACKTEST_METHOD_NAME_GET_REPORT = "BacktestUtils.getReport";
|
|
4058
|
+
const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
|
|
4059
|
+
/**
|
|
4060
|
+
* Utility class for backtest operations.
|
|
4061
|
+
*
|
|
4062
|
+
* Provides simplified access to backtestGlobalService.run() with logging.
|
|
4063
|
+
* Exported as singleton instance for convenient usage.
|
|
4064
|
+
*
|
|
4065
|
+
* @example
|
|
4066
|
+
* ```typescript
|
|
4067
|
+
* import { Backtest } from "./classes/Backtest";
|
|
4068
|
+
*
|
|
4069
|
+
* for await (const result of Backtest.run("BTCUSDT", {
|
|
4070
|
+
* strategyName: "my-strategy",
|
|
4071
|
+
* exchangeName: "my-exchange",
|
|
4072
|
+
* frameName: "1d-backtest"
|
|
4073
|
+
* })) {
|
|
4074
|
+
* console.log("Closed signal PNL:", result.pnl.pnlPercentage);
|
|
4075
|
+
* }
|
|
4076
|
+
* ```
|
|
4077
|
+
*/
|
|
1619
4078
|
class BacktestUtils {
|
|
1620
4079
|
constructor() {
|
|
4080
|
+
/**
|
|
4081
|
+
* Runs backtest for a symbol with context propagation.
|
|
4082
|
+
*
|
|
4083
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
4084
|
+
* @param context - Execution context with strategy, exchange, and frame names
|
|
4085
|
+
* @returns Async generator yielding closed signals with PNL
|
|
4086
|
+
*/
|
|
1621
4087
|
this.run = (symbol, context) => {
|
|
1622
4088
|
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_RUN, {
|
|
1623
4089
|
symbol,
|
|
@@ -1625,13 +4091,157 @@ class BacktestUtils {
|
|
|
1625
4091
|
});
|
|
1626
4092
|
return backtest$1.backtestGlobalService.run(symbol, context);
|
|
1627
4093
|
};
|
|
4094
|
+
/**
|
|
4095
|
+
* Runs backtest in background without yielding results.
|
|
4096
|
+
*
|
|
4097
|
+
* Consumes all backtest results internally without exposing them.
|
|
4098
|
+
* Useful for running backtests for side effects only (callbacks, logging).
|
|
4099
|
+
*
|
|
4100
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
4101
|
+
* @param context - Execution context with strategy, exchange, and frame names
|
|
4102
|
+
* @returns Cancellation closure
|
|
4103
|
+
*
|
|
4104
|
+
* @example
|
|
4105
|
+
* ```typescript
|
|
4106
|
+
* // Run backtest silently, only callbacks will fire
|
|
4107
|
+
* await Backtest.background("BTCUSDT", {
|
|
4108
|
+
* strategyName: "my-strategy",
|
|
4109
|
+
* exchangeName: "my-exchange",
|
|
4110
|
+
* frameName: "1d-backtest"
|
|
4111
|
+
* });
|
|
4112
|
+
* console.log("Backtest completed");
|
|
4113
|
+
* ```
|
|
4114
|
+
*/
|
|
4115
|
+
this.background = (symbol, context) => {
|
|
4116
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_BACKGROUND, {
|
|
4117
|
+
symbol,
|
|
4118
|
+
context,
|
|
4119
|
+
});
|
|
4120
|
+
const iterator = this.run(symbol, context);
|
|
4121
|
+
let isStopped = false;
|
|
4122
|
+
const task = async () => {
|
|
4123
|
+
while (true) {
|
|
4124
|
+
const { done } = await iterator.next();
|
|
4125
|
+
if (done) {
|
|
4126
|
+
break;
|
|
4127
|
+
}
|
|
4128
|
+
if (isStopped) {
|
|
4129
|
+
break;
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
|
|
4134
|
+
return () => {
|
|
4135
|
+
isStopped = true;
|
|
4136
|
+
};
|
|
4137
|
+
};
|
|
4138
|
+
/**
|
|
4139
|
+
* Generates markdown report with all closed signals for a strategy.
|
|
4140
|
+
*
|
|
4141
|
+
* @param strategyName - Strategy name to generate report for
|
|
4142
|
+
* @returns Promise resolving to markdown formatted report string
|
|
4143
|
+
*
|
|
4144
|
+
* @example
|
|
4145
|
+
* ```typescript
|
|
4146
|
+
* const markdown = await Backtest.getReport("my-strategy");
|
|
4147
|
+
* console.log(markdown);
|
|
4148
|
+
* ```
|
|
4149
|
+
*/
|
|
4150
|
+
this.getReport = async (strategyName) => {
|
|
4151
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_REPORT, {
|
|
4152
|
+
strategyName,
|
|
4153
|
+
});
|
|
4154
|
+
return await backtest$1.backtestMarkdownService.getReport(strategyName);
|
|
4155
|
+
};
|
|
4156
|
+
/**
|
|
4157
|
+
* Saves strategy report to disk.
|
|
4158
|
+
*
|
|
4159
|
+
* @param strategyName - Strategy name to save report for
|
|
4160
|
+
* @param path - Optional directory path to save report (default: "./logs/backtest")
|
|
4161
|
+
*
|
|
4162
|
+
* @example
|
|
4163
|
+
* ```typescript
|
|
4164
|
+
* // Save to default path: ./logs/backtest/my-strategy.md
|
|
4165
|
+
* await Backtest.dump("my-strategy");
|
|
4166
|
+
*
|
|
4167
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
4168
|
+
* await Backtest.dump("my-strategy", "./custom/path");
|
|
4169
|
+
* ```
|
|
4170
|
+
*/
|
|
4171
|
+
this.dump = async (strategyName, path) => {
|
|
4172
|
+
backtest$1.loggerService.info(BACKTEST_METHOD_NAME_DUMP, {
|
|
4173
|
+
strategyName,
|
|
4174
|
+
path,
|
|
4175
|
+
});
|
|
4176
|
+
await backtest$1.backtestMarkdownService.dump(strategyName, path);
|
|
4177
|
+
};
|
|
1628
4178
|
}
|
|
1629
4179
|
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Singleton instance of BacktestUtils for convenient backtest operations.
|
|
4182
|
+
*
|
|
4183
|
+
* @example
|
|
4184
|
+
* ```typescript
|
|
4185
|
+
* import { Backtest } from "./classes/Backtest";
|
|
4186
|
+
*
|
|
4187
|
+
* for await (const result of Backtest.run("BTCUSDT", {
|
|
4188
|
+
* strategyName: "my-strategy",
|
|
4189
|
+
* exchangeName: "my-exchange",
|
|
4190
|
+
* frameName: "1d-backtest"
|
|
4191
|
+
* })) {
|
|
4192
|
+
* if (result.action === "closed") {
|
|
4193
|
+
* console.log("PNL:", result.pnl.pnlPercentage);
|
|
4194
|
+
* }
|
|
4195
|
+
* }
|
|
4196
|
+
* ```
|
|
4197
|
+
*/
|
|
1630
4198
|
const Backtest = new BacktestUtils();
|
|
1631
4199
|
|
|
1632
4200
|
const LIVE_METHOD_NAME_RUN = "LiveUtils.run";
|
|
4201
|
+
const LIVE_METHOD_NAME_BACKGROUND = "LiveUtils.background";
|
|
4202
|
+
const LIVE_METHOD_NAME_GET_REPORT = "LiveUtils.getReport";
|
|
4203
|
+
const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
|
|
4204
|
+
/**
|
|
4205
|
+
* Utility class for live trading operations.
|
|
4206
|
+
*
|
|
4207
|
+
* Provides simplified access to liveGlobalService.run() with logging.
|
|
4208
|
+
* Exported as singleton instance for convenient usage.
|
|
4209
|
+
*
|
|
4210
|
+
* Features:
|
|
4211
|
+
* - Infinite async generator (never completes)
|
|
4212
|
+
* - Crash recovery via persisted state
|
|
4213
|
+
* - Real-time progression with Date.now()
|
|
4214
|
+
*
|
|
4215
|
+
* @example
|
|
4216
|
+
* ```typescript
|
|
4217
|
+
* import { Live } from "./classes/Live";
|
|
4218
|
+
*
|
|
4219
|
+
* // Infinite loop - use Ctrl+C to stop
|
|
4220
|
+
* for await (const result of Live.run("BTCUSDT", {
|
|
4221
|
+
* strategyName: "my-strategy",
|
|
4222
|
+
* exchangeName: "my-exchange",
|
|
4223
|
+
* frameName: ""
|
|
4224
|
+
* })) {
|
|
4225
|
+
* if (result.action === "opened") {
|
|
4226
|
+
* console.log("Signal opened:", result.signal);
|
|
4227
|
+
* } else if (result.action === "closed") {
|
|
4228
|
+
* console.log("PNL:", result.pnl.pnlPercentage);
|
|
4229
|
+
* }
|
|
4230
|
+
* }
|
|
4231
|
+
* ```
|
|
4232
|
+
*/
|
|
1633
4233
|
class LiveUtils {
|
|
1634
4234
|
constructor() {
|
|
4235
|
+
/**
|
|
4236
|
+
* Runs live trading for a symbol with context propagation.
|
|
4237
|
+
*
|
|
4238
|
+
* Infinite async generator with crash recovery support.
|
|
4239
|
+
* Process can crash and restart - state will be recovered from disk.
|
|
4240
|
+
*
|
|
4241
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
4242
|
+
* @param context - Execution context with strategy and exchange names
|
|
4243
|
+
* @returns Infinite async generator yielding opened and closed signals
|
|
4244
|
+
*/
|
|
1635
4245
|
this.run = (symbol, context) => {
|
|
1636
4246
|
backtest$1.loggerService.info(LIVE_METHOD_NAME_RUN, {
|
|
1637
4247
|
symbol,
|
|
@@ -1639,8 +4249,111 @@ class LiveUtils {
|
|
|
1639
4249
|
});
|
|
1640
4250
|
return backtest$1.liveGlobalService.run(symbol, context);
|
|
1641
4251
|
};
|
|
4252
|
+
/**
|
|
4253
|
+
* Runs live trading in background without yielding results.
|
|
4254
|
+
*
|
|
4255
|
+
* Consumes all live trading results internally without exposing them.
|
|
4256
|
+
* Infinite loop - will run until process is stopped or crashes.
|
|
4257
|
+
* Useful for running live trading for side effects only (callbacks, persistence).
|
|
4258
|
+
*
|
|
4259
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
4260
|
+
* @param context - Execution context with strategy and exchange names
|
|
4261
|
+
* @returns Cancellation closure
|
|
4262
|
+
*
|
|
4263
|
+
* @example
|
|
4264
|
+
* ```typescript
|
|
4265
|
+
* // Run live trading silently in background, only callbacks will fire
|
|
4266
|
+
* // This will run forever until Ctrl+C
|
|
4267
|
+
* await Live.background("BTCUSDT", {
|
|
4268
|
+
* strategyName: "my-strategy",
|
|
4269
|
+
* exchangeName: "my-exchange"
|
|
4270
|
+
* });
|
|
4271
|
+
* ```
|
|
4272
|
+
*/
|
|
4273
|
+
this.background = (symbol, context) => {
|
|
4274
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_BACKGROUND, {
|
|
4275
|
+
symbol,
|
|
4276
|
+
context,
|
|
4277
|
+
});
|
|
4278
|
+
const iterator = this.run(symbol, context);
|
|
4279
|
+
let isStopped = false;
|
|
4280
|
+
let lastValue = null;
|
|
4281
|
+
const task = async () => {
|
|
4282
|
+
while (true) {
|
|
4283
|
+
const { value, done } = await iterator.next();
|
|
4284
|
+
if (value) {
|
|
4285
|
+
lastValue = value;
|
|
4286
|
+
}
|
|
4287
|
+
if (done) {
|
|
4288
|
+
break;
|
|
4289
|
+
}
|
|
4290
|
+
if (lastValue?.action === "closed" && isStopped) {
|
|
4291
|
+
break;
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
};
|
|
4295
|
+
task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
|
|
4296
|
+
return () => {
|
|
4297
|
+
isStopped = true;
|
|
4298
|
+
};
|
|
4299
|
+
};
|
|
4300
|
+
/**
|
|
4301
|
+
* Generates markdown report with all events for a strategy.
|
|
4302
|
+
*
|
|
4303
|
+
* @param strategyName - Strategy name to generate report for
|
|
4304
|
+
* @returns Promise resolving to markdown formatted report string
|
|
4305
|
+
*
|
|
4306
|
+
* @example
|
|
4307
|
+
* ```typescript
|
|
4308
|
+
* const markdown = await Live.getReport("my-strategy");
|
|
4309
|
+
* console.log(markdown);
|
|
4310
|
+
* ```
|
|
4311
|
+
*/
|
|
4312
|
+
this.getReport = async (strategyName) => {
|
|
4313
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_REPORT, {
|
|
4314
|
+
strategyName,
|
|
4315
|
+
});
|
|
4316
|
+
return await backtest$1.liveMarkdownService.getReport(strategyName);
|
|
4317
|
+
};
|
|
4318
|
+
/**
|
|
4319
|
+
* Saves strategy report to disk.
|
|
4320
|
+
*
|
|
4321
|
+
* @param strategyName - Strategy name to save report for
|
|
4322
|
+
* @param path - Optional directory path to save report (default: "./logs/live")
|
|
4323
|
+
*
|
|
4324
|
+
* @example
|
|
4325
|
+
* ```typescript
|
|
4326
|
+
* // Save to default path: ./logs/live/my-strategy.md
|
|
4327
|
+
* await Live.dump("my-strategy");
|
|
4328
|
+
*
|
|
4329
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
4330
|
+
* await Live.dump("my-strategy", "./custom/path");
|
|
4331
|
+
* ```
|
|
4332
|
+
*/
|
|
4333
|
+
this.dump = async (strategyName, path) => {
|
|
4334
|
+
backtest$1.loggerService.info(LIVE_METHOD_NAME_DUMP, {
|
|
4335
|
+
strategyName,
|
|
4336
|
+
path,
|
|
4337
|
+
});
|
|
4338
|
+
await backtest$1.liveMarkdownService.dump(strategyName, path);
|
|
4339
|
+
};
|
|
1642
4340
|
}
|
|
1643
4341
|
}
|
|
4342
|
+
/**
|
|
4343
|
+
* Singleton instance of LiveUtils for convenient live trading operations.
|
|
4344
|
+
*
|
|
4345
|
+
* @example
|
|
4346
|
+
* ```typescript
|
|
4347
|
+
* import { Live } from "./classes/Live";
|
|
4348
|
+
*
|
|
4349
|
+
* for await (const result of Live.run("BTCUSDT", {
|
|
4350
|
+
* strategyName: "my-strategy",
|
|
4351
|
+
* exchangeName: "my-exchange",
|
|
4352
|
+
* })) {
|
|
4353
|
+
* console.log("Result:", result.action);
|
|
4354
|
+
* }
|
|
4355
|
+
* ```
|
|
4356
|
+
*/
|
|
1644
4357
|
const Live = new LiveUtils();
|
|
1645
4358
|
|
|
1646
|
-
export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy,
|
|
4359
|
+
export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listenError, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, setLogger };
|