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