backtest-kit 1.0.2 → 1.0.4

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/build/index.mjs CHANGED
@@ -1,8 +1,68 @@
1
1
  import { createActivator } from 'di-kit';
2
- import { memoize, singlerun, sleep } from 'functools-kit';
3
2
  import { scoped } from 'di-scoped';
3
+ import { memoize, makeExtendable, singleshot, getErrorMessage, not, trycatch, retry, randomString, ToolRegistry, sleep, singlerun } from 'functools-kit';
4
+ import fs from 'fs/promises';
5
+ import path, { join } from 'path';
6
+ import crypto from 'crypto';
7
+ import os from 'os';
4
8
  import Table from 'cli-table3';
5
9
 
10
+ const { init, inject, provide } = createActivator("backtest");
11
+
12
+ const MethodContextService = scoped(class {
13
+ constructor(context) {
14
+ this.context = context;
15
+ }
16
+ });
17
+
18
+ const baseServices$1 = {
19
+ loggerService: Symbol('loggerService'),
20
+ };
21
+ const contextServices$1 = {
22
+ executionContextService: Symbol('executionContextService'),
23
+ methodContextService: Symbol('methodContextService'),
24
+ };
25
+ const connectionServices$1 = {
26
+ exchangeConnectionService: Symbol('exchangeConnectionService'),
27
+ strategyConnectionService: Symbol('strategyConnectionService'),
28
+ frameConnectionService: Symbol('frameConnectionService'),
29
+ };
30
+ const schemaServices$1 = {
31
+ exchangeSchemaService: Symbol('exchangeSchemaService'),
32
+ strategySchemaService: Symbol('strategySchemaService'),
33
+ frameSchemaService: Symbol('frameSchemaService'),
34
+ };
35
+ const globalServices$1 = {
36
+ exchangeGlobalService: Symbol('exchangeGlobalService'),
37
+ strategyGlobalService: Symbol('strategyGlobalService'),
38
+ frameGlobalService: Symbol('frameGlobalService'),
39
+ liveGlobalService: Symbol('liveGlobalService'),
40
+ backtestGlobalService: Symbol('backtestGlobalService'),
41
+ };
42
+ const logicPrivateServices$1 = {
43
+ backtestLogicPrivateService: Symbol('backtestLogicPrivateService'),
44
+ liveLogicPrivateService: Symbol('liveLogicPrivateService'),
45
+ };
46
+ const logicPublicServices$1 = {
47
+ backtestLogicPublicService: Symbol('backtestLogicPublicService'),
48
+ liveLogicPublicService: Symbol('liveLogicPublicService'),
49
+ };
50
+ const TYPES = {
51
+ ...baseServices$1,
52
+ ...contextServices$1,
53
+ ...connectionServices$1,
54
+ ...schemaServices$1,
55
+ ...globalServices$1,
56
+ ...logicPrivateServices$1,
57
+ ...logicPublicServices$1,
58
+ };
59
+
60
+ const ExecutionContextService = scoped(class {
61
+ constructor(context) {
62
+ this.context = context;
63
+ }
64
+ });
65
+
6
66
  const NOOP_LOGGER = {
7
67
  log() {
8
68
  },
@@ -13,51 +73,37 @@ const NOOP_LOGGER = {
13
73
  };
14
74
  class LoggerService {
15
75
  constructor() {
76
+ this.methodContextService = inject(TYPES.methodContextService);
77
+ this.executionContextService = inject(TYPES.executionContextService);
16
78
  this._commonLogger = NOOP_LOGGER;
17
79
  this.log = async (topic, ...args) => {
18
- await this._commonLogger.log(topic, ...args);
80
+ await this._commonLogger.log(topic, ...args, this.methodContext, this.executionContext);
19
81
  };
20
82
  this.debug = async (topic, ...args) => {
21
- await this._commonLogger.debug(topic, ...args);
83
+ await this._commonLogger.debug(topic, ...args, this.methodContext, this.executionContext);
22
84
  };
23
85
  this.info = async (topic, ...args) => {
24
- await this._commonLogger.info(topic, ...args);
86
+ await this._commonLogger.info(topic, ...args, this.methodContext, this.executionContext);
25
87
  };
26
88
  this.setLogger = (logger) => {
27
89
  this._commonLogger = logger;
28
90
  };
29
91
  }
92
+ get methodContext() {
93
+ if (MethodContextService.hasContext()) {
94
+ return this.methodContextService.context;
95
+ }
96
+ return {};
97
+ }
98
+ get executionContext() {
99
+ if (ExecutionContextService.hasContext()) {
100
+ return this.executionContextService.context;
101
+ }
102
+ return {};
103
+ }
30
104
  }
31
105
 
32
- const { init, inject, provide } = createActivator("backtest");
33
-
34
- const baseServices$1 = {
35
- loggerService: Symbol('loggerService'),
36
- };
37
- const contextServices$1 = {
38
- executionContextService: Symbol('executionContextService'),
39
- };
40
- const connectionServices$1 = {
41
- candleConnectionService: Symbol('candleConnectionService'),
42
- strategyConnectionService: Symbol('strategyConnectionService'),
43
- };
44
- const schemaServices$1 = {
45
- candleSchemaService: Symbol('candleSchemaService'),
46
- strategySchemaService: Symbol('strategySchemaService'),
47
- };
48
- const publicServices$1 = {
49
- candlePublicService: Symbol('candlePublicService'),
50
- strategyPublicService: Symbol('strategyPublicService'),
51
- };
52
- const TYPES = {
53
- ...baseServices$1,
54
- ...contextServices$1,
55
- ...connectionServices$1,
56
- ...schemaServices$1,
57
- ...publicServices$1,
58
- };
59
-
60
- const INTERVAL_MINUTES = {
106
+ const INTERVAL_MINUTES$2 = {
61
107
  "1m": 1,
62
108
  "3m": 3,
63
109
  "5m": 5,
@@ -69,87 +115,148 @@ const INTERVAL_MINUTES = {
69
115
  "6h": 360,
70
116
  "8h": 480,
71
117
  };
72
- class ClientCandle {
118
+ class ClientExchange {
73
119
  constructor(params) {
74
120
  this.params = params;
75
- this.getCandles = async (symbol, interval, limit) => {
76
- this.params.logger.debug(`ClientCandle getCandles`, {
77
- symbol,
78
- interval,
79
- limit,
80
- });
81
- const step = INTERVAL_MINUTES[interval];
82
- const adjust = step * limit;
83
- if (!adjust) {
84
- throw new Error(`ClientCandle unknown time adjust for interval=${interval}`);
85
- }
86
- const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
87
- const data = await this.params.getCandles(symbol, interval, since, limit);
88
- if (this.params.callbacks?.onCandleData) {
89
- this.params.callbacks.onCandleData(symbol, interval, since, limit, data);
90
- }
91
- return data;
92
- };
93
- this.getAveragePrice = async (symbol) => {
94
- this.params.logger.debug(`ClientCandle getAveragePrice`, {
95
- symbol,
96
- });
97
- const candles = await this.getCandles(symbol, "1m", 5);
98
- if (candles.length === 0) {
99
- throw new Error(`ClientCandle getAveragePrice: no candles data for symbol=${symbol}`);
100
- }
101
- // VWAP (Volume Weighted Average Price)
102
- // Используем типичную цену (typical price) = (high + low + close) / 3
103
- const sumPriceVolume = candles.reduce((acc, candle) => {
104
- const typicalPrice = (candle.high + candle.low + candle.close) / 3;
105
- return acc + typicalPrice * candle.volume;
106
- }, 0);
107
- const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
108
- if (totalVolume === 0) {
109
- // Если объем нулевой, возвращаем простое среднее close цен
110
- const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
111
- return sum / candles.length;
112
- }
113
- const vwap = sumPriceVolume / totalVolume;
114
- return vwap;
115
- };
121
+ }
122
+ async getCandles(symbol, interval, limit) {
123
+ this.params.logger.debug(`ClientExchange getCandles`, {
124
+ symbol,
125
+ interval,
126
+ limit,
127
+ });
128
+ const step = INTERVAL_MINUTES$2[interval];
129
+ const adjust = step * limit - 1;
130
+ if (!adjust) {
131
+ throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
132
+ }
133
+ const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
134
+ const data = await this.params.getCandles(symbol, interval, since, limit);
135
+ if (this.params.callbacks?.onCandleData) {
136
+ this.params.callbacks.onCandleData(symbol, interval, since, limit, data);
137
+ }
138
+ return data;
139
+ }
140
+ async getNextCandles(symbol, interval, limit) {
141
+ this.params.logger.debug(`ClientExchange getNextCandles`, {
142
+ symbol,
143
+ interval,
144
+ limit,
145
+ });
146
+ const since = new Date(this.params.execution.context.when.getTime());
147
+ const now = Date.now();
148
+ // Вычисляем конечное время запроса
149
+ const step = INTERVAL_MINUTES$2[interval];
150
+ const endTime = since.getTime() + limit * step * 60 * 1000;
151
+ // Проверяем что запрошенный период не заходит за Date.now()
152
+ if (endTime > now) {
153
+ return [];
154
+ }
155
+ const data = await this.params.getCandles(symbol, interval, since, limit);
156
+ if (this.params.callbacks?.onCandleData) {
157
+ this.params.callbacks.onCandleData(symbol, interval, since, limit, data);
158
+ }
159
+ return data;
160
+ }
161
+ async getAveragePrice(symbol) {
162
+ this.params.logger.debug(`ClientExchange getAveragePrice`, {
163
+ symbol,
164
+ });
165
+ const candles = await this.getCandles(symbol, "1m", 5);
166
+ if (candles.length === 0) {
167
+ throw new Error(`ClientExchange getAveragePrice: no candles data for symbol=${symbol}`);
168
+ }
169
+ // VWAP (Volume Weighted Average Price)
170
+ // Используем типичную цену (typical price) = (high + low + close) / 3
171
+ const sumPriceVolume = candles.reduce((acc, candle) => {
172
+ const typicalPrice = (candle.high + candle.low + candle.close) / 3;
173
+ return acc + typicalPrice * candle.volume;
174
+ }, 0);
175
+ const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
176
+ if (totalVolume === 0) {
177
+ // Если объем нулевой, возвращаем простое среднее close цен
178
+ const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
179
+ return sum / candles.length;
180
+ }
181
+ const vwap = sumPriceVolume / totalVolume;
182
+ return vwap;
183
+ }
184
+ async formatQuantity(symbol, quantity) {
185
+ this.params.logger.debug("binanceService formatQuantity", {
186
+ symbol,
187
+ quantity,
188
+ });
189
+ return await this.params.formatQuantity(symbol, quantity);
190
+ }
191
+ async formatPrice(symbol, price) {
192
+ this.params.logger.debug("binanceService formatPrice", {
193
+ symbol,
194
+ price,
195
+ });
196
+ return await this.params.formatPrice(symbol, price);
116
197
  }
117
198
  }
118
199
 
119
- class CandleConnectionService {
200
+ class ExchangeConnectionService {
120
201
  constructor() {
121
202
  this.loggerService = inject(TYPES.loggerService);
122
203
  this.executionContextService = inject(TYPES.executionContextService);
123
- this.candleSchemaService = inject(TYPES.candleSchemaService);
124
- this.getCandle = memoize((symbol) => `${symbol}`, () => {
125
- const { getCandles, callbacks } = this.candleSchemaService.getSchema();
126
- return new ClientCandle({
204
+ this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
205
+ this.methodContextService = inject(TYPES.methodContextService);
206
+ this.getExchange = memoize((exchangeName) => `${exchangeName}`, (exchangeName) => {
207
+ const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
208
+ return new ClientExchange({
127
209
  execution: this.executionContextService,
128
210
  logger: this.loggerService,
211
+ exchangeName,
129
212
  getCandles,
213
+ formatPrice,
214
+ formatQuantity,
130
215
  callbacks,
131
216
  });
132
217
  });
133
218
  this.getCandles = async (symbol, interval, limit) => {
134
- this.loggerService.log("candleConnectionService getCandles", {
219
+ this.loggerService.log("exchangeConnectionService getCandles", {
220
+ symbol,
221
+ interval,
222
+ limit,
223
+ });
224
+ return await this.getExchange(this.methodContextService.context.exchangeName).getCandles(symbol, interval, limit);
225
+ };
226
+ this.getNextCandles = async (symbol, interval, limit) => {
227
+ this.loggerService.log("exchangeConnectionService getNextCandles", {
135
228
  symbol,
136
229
  interval,
137
230
  limit,
138
231
  });
139
- return await this.getCandle(symbol).getCandles(symbol, interval, limit);
232
+ return await this.getExchange(this.methodContextService.context.exchangeName).getNextCandles(symbol, interval, limit);
140
233
  };
141
234
  this.getAveragePrice = async (symbol) => {
142
- this.loggerService.log("candleConnectionService getAveragePrice", {
235
+ this.loggerService.log("exchangeConnectionService getAveragePrice", {
236
+ symbol,
237
+ });
238
+ return await this.getExchange(this.methodContextService.context.exchangeName).getAveragePrice(symbol);
239
+ };
240
+ this.formatPrice = async (symbol, price) => {
241
+ this.loggerService.log("exchangeConnectionService getAveragePrice", {
143
242
  symbol,
243
+ price,
144
244
  });
145
- return await this.getCandle(symbol).getAveragePrice(symbol);
245
+ return await this.getExchange(this.methodContextService.context.exchangeName).formatPrice(symbol, price);
246
+ };
247
+ this.formatQuantity = async (symbol, quantity) => {
248
+ this.loggerService.log("exchangeConnectionService getAveragePrice", {
249
+ symbol,
250
+ quantity,
251
+ });
252
+ return await this.getExchange(this.methodContextService.context.exchangeName).formatQuantity(symbol, quantity);
146
253
  };
147
254
  }
148
255
  }
149
256
 
150
257
  const PERCENT_SLIPPAGE = 0.1;
151
258
  const PERCENT_FEE = 0.1;
152
- const GET_PNL_FN = (signal, priceClose) => {
259
+ const toProfitLossDto = (signal, priceClose) => {
153
260
  const priceOpen = signal.priceOpen;
154
261
  let priceOpenWithSlippage;
155
262
  let priceCloseWithSlippage;
@@ -188,48 +295,554 @@ const GET_PNL_FN = (signal, priceClose) => {
188
295
  priceClose,
189
296
  };
190
297
  };
298
+
299
+ const IS_WINDOWS = os.platform() === "win32";
300
+ /**
301
+ * Atomically writes data to a file, ensuring the operation either fully completes or leaves the original file unchanged.
302
+ * Uses a temporary file with a rename strategy on POSIX systems for atomicity, or direct writing with sync on Windows (or when POSIX rename is skipped).
303
+ *
304
+ *
305
+ * @param {string} file - The file parameter.
306
+ * @param {string | Buffer} data - The data to be processed or validated.
307
+ * @param {Options | BufferEncoding} options - The options parameter (optional).
308
+ * @throws {Error} Throws an error if the write, sync, or rename operation fails, after attempting cleanup of temporary files.
309
+ *
310
+ * @example
311
+ * // Basic usage with default options
312
+ * await writeFileAtomic("output.txt", "Hello, world!");
313
+ * // Writes "Hello, world!" to "output.txt" atomically
314
+ *
315
+ * @example
316
+ * // Custom options and Buffer data
317
+ * const buffer = Buffer.from("Binary data");
318
+ * await writeFileAtomic("data.bin", buffer, { encoding: "binary", mode: 0o644, tmpPrefix: "temp-" });
319
+ * // Writes binary data to "data.bin" with custom permissions and temp prefix
320
+ *
321
+ * @example
322
+ * // Using encoding shorthand
323
+ * await writeFileAtomic("log.txt", "Log entry", "utf16le");
324
+ * // Writes "Log entry" to "log.txt" in UTF-16LE encoding
325
+ *
326
+ * @remarks
327
+ * This function ensures atomicity to prevent partial writes:
328
+ * - On POSIX systems (non-Windows, unless `GLOBAL_CONFIG.CC_SKIP_POSIX_RENAME` is true):
329
+ * - Writes data to a temporary file (e.g., `.tmp-<random>-filename`) in the same directory.
330
+ * - Uses `crypto.randomBytes` to generate a unique temporary name, reducing collision risk.
331
+ * - Syncs the data to disk and renames the temporary file to the target file atomically with `fs.rename`.
332
+ * - Cleans up the temporary file on failure, swallowing cleanup errors to prioritize throwing the original error.
333
+ * - On Windows (or when POSIX rename is skipped):
334
+ * - Writes directly to the target file, syncing data to disk to minimize corruption risk (though not fully atomic).
335
+ * - Closes the file handle on failure without additional cleanup.
336
+ * - Accepts `options` as an object or a string (interpreted as `encoding`), defaulting to `{ encoding: "utf8", mode: 0o666, tmpPrefix: ".tmp-" }`.
337
+ * Useful in the agent swarm system for safely writing configuration files, logs, or state data where partial writes could cause corruption.
338
+ *
339
+ * @see {@link https://nodejs.org/api/fs.html#fspromiseswritefilefile-data-options|fs.promises.writeFile} for file writing details.
340
+ * @see {@link https://nodejs.org/api/crypto.html#cryptorandombytessize-callback|crypto.randomBytes} for temporary file naming.
341
+ * @see {@link ../config/params|GLOBAL_CONFIG} for configuration impacting POSIX behavior.
342
+ */
343
+ async function writeFileAtomic(file, data, options = {}) {
344
+ if (typeof options === "string") {
345
+ options = { encoding: options };
346
+ }
347
+ else if (!options) {
348
+ options = {};
349
+ }
350
+ const { encoding = "utf8", mode = 0o666, tmpPrefix = ".tmp-" } = options;
351
+ let fileHandle = null;
352
+ if (IS_WINDOWS) {
353
+ try {
354
+ // Create and write to temporary file
355
+ fileHandle = await fs.open(file, "w", mode);
356
+ // Write data to the temp file
357
+ await fileHandle.writeFile(data, { encoding });
358
+ // Ensure data is flushed to disk
359
+ await fileHandle.sync();
360
+ // Close the file before rename
361
+ await fileHandle.close();
362
+ }
363
+ catch (error) {
364
+ // Clean up if something went wrong
365
+ if (fileHandle) {
366
+ await fileHandle.close().catch(() => { });
367
+ }
368
+ throw error; // Re-throw the original error
369
+ }
370
+ return;
371
+ }
372
+ // Create a temporary filename in the same directory
373
+ const dir = path.dirname(file);
374
+ const filename = path.basename(file);
375
+ const tmpFile = path.join(dir, `${tmpPrefix}${crypto.randomBytes(6).toString("hex")}-${filename}`);
376
+ try {
377
+ // Create and write to temporary file
378
+ fileHandle = await fs.open(tmpFile, "w", mode);
379
+ // Write data to the temp file
380
+ await fileHandle.writeFile(data, { encoding });
381
+ // Ensure data is flushed to disk
382
+ await fileHandle.sync();
383
+ // Close the file before rename
384
+ await fileHandle.close();
385
+ fileHandle = null;
386
+ // Atomically replace the target file with our temp file
387
+ await fs.rename(tmpFile, file);
388
+ }
389
+ catch (error) {
390
+ // Clean up if something went wrong
391
+ if (fileHandle) {
392
+ await fileHandle.close().catch(() => { });
393
+ }
394
+ // Try to remove the temporary file
395
+ try {
396
+ await fs.unlink(tmpFile).catch(() => { });
397
+ }
398
+ catch (_) {
399
+ // Ignore errors during cleanup
400
+ }
401
+ throw error;
402
+ }
403
+ }
404
+
405
+ var _a;
406
+ const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
407
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
408
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
409
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
410
+ const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
411
+ const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
412
+ const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
413
+ const PERSIST_BASE_METHOD_NAME_WRITE_VALUE = "PersistBase.writeValue";
414
+ const PERSIST_BASE_METHOD_NAME_HAS_VALUE = "PersistBase.hasValue";
415
+ const PERSIST_BASE_METHOD_NAME_REMOVE_VALUE = "PersistBase.removeValue";
416
+ const PERSIST_BASE_METHOD_NAME_REMOVE_ALL = "PersistBase.removeAll";
417
+ const PERSIST_BASE_METHOD_NAME_VALUES = "PersistBase.values";
418
+ const PERSIST_BASE_METHOD_NAME_KEYS = "PersistBase.keys";
419
+ const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
420
+ const BASE_UNLINK_RETRY_COUNT = 5;
421
+ const BASE_UNLINK_RETRY_DELAY = 1000;
422
+ const BASE_WAIT_FOR_INIT_FN = async (self) => {
423
+ backtest$1.loggerService.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
424
+ entityName: self.entityName,
425
+ directory: self._directory,
426
+ });
427
+ await fs.mkdir(self._directory, { recursive: true });
428
+ for await (const key of self.keys()) {
429
+ try {
430
+ await self.readValue(key);
431
+ }
432
+ catch {
433
+ const filePath = self._getFilePath(key);
434
+ console.error(`agent-swarm PersistBase found invalid document for filePath=${filePath} entityName=${self.entityName}`);
435
+ if (await not(BASE_WAIT_FOR_INIT_UNLINK_FN(filePath))) {
436
+ console.error(`agent-swarm PersistBase failed to remove invalid document for filePath=${filePath} entityName=${self.entityName}`);
437
+ }
438
+ }
439
+ }
440
+ };
441
+ const BASE_WAIT_FOR_INIT_UNLINK_FN = async (filePath) => trycatch(retry(async () => {
442
+ try {
443
+ await fs.unlink(filePath);
444
+ return true;
445
+ }
446
+ catch (error) {
447
+ console.error(`agent-swarm PersistBase unlink failed for filePath=${filePath} error=${getErrorMessage(error)}`);
448
+ throw error;
449
+ }
450
+ }, BASE_UNLINK_RETRY_COUNT, BASE_UNLINK_RETRY_DELAY), {
451
+ defaultValue: false,
452
+ });
453
+ const PersistBase = makeExtendable(class {
454
+ constructor(entityName, baseDir = join(process.cwd(), "logs/data")) {
455
+ this.entityName = entityName;
456
+ this.baseDir = baseDir;
457
+ this[_a] = singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
458
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
459
+ entityName: this.entityName,
460
+ baseDir,
461
+ });
462
+ this._directory = join(this.baseDir, this.entityName);
463
+ }
464
+ _getFilePath(entityId) {
465
+ return join(this.baseDir, this.entityName, `${entityId}.json`);
466
+ }
467
+ async waitForInit(initial) {
468
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
469
+ entityName: this.entityName,
470
+ initial,
471
+ });
472
+ await this[BASE_WAIT_FOR_INIT_SYMBOL]();
473
+ }
474
+ async getCount() {
475
+ const files = await fs.readdir(this._directory);
476
+ const { length } = files.filter((file) => file.endsWith(".json"));
477
+ return length;
478
+ }
479
+ async readValue(entityId) {
480
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
481
+ entityName: this.entityName,
482
+ entityId,
483
+ });
484
+ try {
485
+ const filePath = this._getFilePath(entityId);
486
+ const fileContent = await fs.readFile(filePath, "utf-8");
487
+ return JSON.parse(fileContent);
488
+ }
489
+ catch (error) {
490
+ if (error?.code === "ENOENT") {
491
+ throw new Error(`Entity ${this.entityName}:${entityId} not found`);
492
+ }
493
+ throw new Error(`Failed to read entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
494
+ }
495
+ }
496
+ async hasValue(entityId) {
497
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
498
+ entityName: this.entityName,
499
+ entityId,
500
+ });
501
+ try {
502
+ const filePath = this._getFilePath(entityId);
503
+ await fs.access(filePath);
504
+ return true;
505
+ }
506
+ catch (error) {
507
+ if (error?.code === "ENOENT") {
508
+ return false;
509
+ }
510
+ throw new Error(`Failed to check existence of entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
511
+ }
512
+ }
513
+ async writeValue(entityId, entity) {
514
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
515
+ entityName: this.entityName,
516
+ entityId,
517
+ });
518
+ try {
519
+ const filePath = this._getFilePath(entityId);
520
+ const serializedData = JSON.stringify(entity);
521
+ await writeFileAtomic(filePath, serializedData, "utf-8");
522
+ }
523
+ catch (error) {
524
+ throw new Error(`Failed to write entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
525
+ }
526
+ }
527
+ async removeValue(entityId) {
528
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_VALUE, {
529
+ entityName: this.entityName,
530
+ entityId,
531
+ });
532
+ try {
533
+ const filePath = this._getFilePath(entityId);
534
+ await fs.unlink(filePath);
535
+ }
536
+ catch (error) {
537
+ if (error?.code === "ENOENT") {
538
+ throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
539
+ }
540
+ throw new Error(`Failed to remove entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
541
+ }
542
+ }
543
+ async removeAll() {
544
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_ALL, {
545
+ entityName: this.entityName,
546
+ });
547
+ try {
548
+ const files = await fs.readdir(this._directory);
549
+ const entityFiles = files.filter((file) => file.endsWith(".json"));
550
+ for (const file of entityFiles) {
551
+ await fs.unlink(join(this._directory, file));
552
+ }
553
+ }
554
+ catch (error) {
555
+ throw new Error(`Failed to remove values for ${this.entityName}: ${getErrorMessage(error)}`);
556
+ }
557
+ }
558
+ async *values() {
559
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_VALUES, {
560
+ entityName: this.entityName,
561
+ });
562
+ try {
563
+ const files = await fs.readdir(this._directory);
564
+ const entityIds = files
565
+ .filter((file) => file.endsWith(".json"))
566
+ .map((file) => file.slice(0, -5))
567
+ .sort((a, b) => a.localeCompare(b, undefined, {
568
+ numeric: true,
569
+ sensitivity: "base",
570
+ }));
571
+ for (const entityId of entityIds) {
572
+ const entity = await this.readValue(entityId);
573
+ yield entity;
574
+ }
575
+ }
576
+ catch (error) {
577
+ throw new Error(`Failed to read values for ${this.entityName}: ${getErrorMessage(error)}`);
578
+ }
579
+ }
580
+ async *keys() {
581
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
582
+ entityName: this.entityName,
583
+ });
584
+ try {
585
+ const files = await fs.readdir(this._directory);
586
+ const entityIds = files
587
+ .filter((file) => file.endsWith(".json"))
588
+ .map((file) => file.slice(0, -5))
589
+ .sort((a, b) => a.localeCompare(b, undefined, {
590
+ numeric: true,
591
+ sensitivity: "base",
592
+ }));
593
+ for (const entityId of entityIds) {
594
+ yield entityId;
595
+ }
596
+ }
597
+ catch (error) {
598
+ throw new Error(`Failed to read keys for ${this.entityName}: ${getErrorMessage(error)}`);
599
+ }
600
+ }
601
+ async *[(_a = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
602
+ for await (const entity of this.values()) {
603
+ yield entity;
604
+ }
605
+ }
606
+ async *filter(predicate) {
607
+ for await (const entity of this.values()) {
608
+ if (predicate(entity)) {
609
+ yield entity;
610
+ }
611
+ }
612
+ }
613
+ async *take(total, predicate) {
614
+ let count = 0;
615
+ if (predicate) {
616
+ for await (const entity of this.values()) {
617
+ if (!predicate(entity)) {
618
+ continue;
619
+ }
620
+ count += 1;
621
+ yield entity;
622
+ if (count >= total) {
623
+ break;
624
+ }
625
+ }
626
+ }
627
+ else {
628
+ for await (const entity of this.values()) {
629
+ count += 1;
630
+ yield entity;
631
+ if (count >= total) {
632
+ break;
633
+ }
634
+ }
635
+ }
636
+ }
637
+ });
638
+ class PersistSignalUtils {
639
+ constructor() {
640
+ this.PersistSignalFactory = PersistBase;
641
+ this.getSignalStorage = memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistSignalFactory, [
642
+ strategyName,
643
+ `./logs/data/signal/`,
644
+ ]));
645
+ this.readSignalData = async (strategyName, symbol) => {
646
+ backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
647
+ const isInitial = !this.getSignalStorage.has(strategyName);
648
+ const stateStorage = this.getSignalStorage(strategyName);
649
+ await stateStorage.waitForInit(isInitial);
650
+ if (await stateStorage.hasValue(symbol)) {
651
+ const { signalRow } = await stateStorage.readValue(symbol);
652
+ return signalRow;
653
+ }
654
+ return null;
655
+ };
656
+ this.writeSignalData = async (signalRow, strategyName, symbol) => {
657
+ backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
658
+ const isInitial = !this.getSignalStorage.has(strategyName);
659
+ const stateStorage = this.getSignalStorage(strategyName);
660
+ await stateStorage.waitForInit(isInitial);
661
+ await stateStorage.writeValue(symbol, { signalRow });
662
+ };
663
+ }
664
+ usePersistSignalAdapter(Ctor) {
665
+ backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
666
+ this.PersistSignalFactory = Ctor;
667
+ }
668
+ }
669
+ const PersistSignalAdaper = new PersistSignalUtils();
670
+
671
+ const INTERVAL_MINUTES$1 = {
672
+ "1m": 1,
673
+ "3m": 3,
674
+ "5m": 5,
675
+ "15m": 15,
676
+ "30m": 30,
677
+ "1h": 60,
678
+ };
679
+ const GET_SIGNAL_FN = trycatch(async (self) => {
680
+ const currentTime = self.params.execution.context.when.getTime();
681
+ {
682
+ const intervalMinutes = INTERVAL_MINUTES$1[self.params.interval];
683
+ const intervalMs = intervalMinutes * 60 * 1000;
684
+ // Проверяем что прошел нужный интервал с последнего getSignal
685
+ if (self._lastSignalTimestamp !== null &&
686
+ currentTime - self._lastSignalTimestamp < intervalMs) {
687
+ return null;
688
+ }
689
+ self._lastSignalTimestamp = currentTime;
690
+ }
691
+ const signal = await self.params.getSignal(self.params.execution.context.symbol);
692
+ if (!signal) {
693
+ return null;
694
+ }
695
+ return {
696
+ id: randomString(),
697
+ ...signal,
698
+ };
699
+ }, {
700
+ defaultValue: null,
701
+ });
702
+ const GET_AVG_PRICE_FN = (candles) => {
703
+ const sumPriceVolume = candles.reduce((acc, c) => {
704
+ const typicalPrice = (c.high + c.low + c.close) / 3;
705
+ return acc + typicalPrice * c.volume;
706
+ }, 0);
707
+ const totalVolume = candles.reduce((acc, c) => acc + c.volume, 0);
708
+ return totalVolume === 0
709
+ ? candles.reduce((acc, c) => acc + c.close, 0) / candles.length
710
+ : sumPriceVolume / totalVolume;
711
+ };
712
+ const WAIT_FOR_INIT_FN = async (self) => {
713
+ self.params.logger.debug("ClientStrategy waitForInit");
714
+ if (self.params.execution.context.backtest) {
715
+ return;
716
+ }
717
+ self._pendingSignal = await PersistSignalAdaper.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
718
+ };
191
719
  class ClientStrategy {
192
720
  constructor(params) {
193
721
  this.params = params;
194
722
  this._pendingSignal = null;
195
- this.tick = async (symbol) => {
196
- this.params.logger.debug("ClientStrategy tick", {
197
- symbol,
198
- });
199
- if (!this._pendingSignal) {
200
- this._pendingSignal = await this.params.getSignal(this.params.symbol);
201
- if (this._pendingSignal) {
202
- if (this.params.callbacks?.onOpen) {
203
- this.params.callbacks.onOpen(this.params.execution.context.backtest, symbol, this._pendingSignal);
204
- }
205
- return {
206
- action: "opened",
207
- signal: this._pendingSignal,
208
- };
723
+ this._lastSignalTimestamp = null;
724
+ this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN(this));
725
+ }
726
+ async setPendingSignal(pendingSignal) {
727
+ this.params.logger.debug("ClientStrategy setPendingSignal", {
728
+ pendingSignal,
729
+ });
730
+ this._pendingSignal = pendingSignal;
731
+ if (this.params.execution.context.backtest) {
732
+ return;
733
+ }
734
+ await PersistSignalAdaper.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
735
+ }
736
+ async tick() {
737
+ this.params.logger.debug("ClientStrategy tick");
738
+ if (!this._pendingSignal) {
739
+ const pendingSignal = await GET_SIGNAL_FN(this);
740
+ await this.setPendingSignal(pendingSignal);
741
+ if (this._pendingSignal) {
742
+ if (this.params.callbacks?.onOpen) {
743
+ this.params.callbacks.onOpen(this.params.execution.context.backtest, this.params.execution.context.symbol, this._pendingSignal);
209
744
  }
210
745
  return {
211
- action: "idle",
212
- signal: null,
746
+ action: "opened",
747
+ signal: this._pendingSignal,
213
748
  };
214
749
  }
215
- const when = this.params.execution.context.when;
216
- const signal = this._pendingSignal;
217
- // Получаем среднюю цену
218
- const averagePrice = await this.params.candle.getAveragePrice(symbol);
219
- this.params.logger.debug("ClientStrategy tick check", {
220
- symbol,
221
- averagePrice,
750
+ return {
751
+ action: "idle",
752
+ signal: null,
753
+ };
754
+ }
755
+ const when = this.params.execution.context.when;
756
+ const signal = this._pendingSignal;
757
+ // Получаем среднюю цену
758
+ const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
759
+ this.params.logger.debug("ClientStrategy tick check", {
760
+ symbol: this.params.execution.context.symbol,
761
+ averagePrice,
762
+ signalId: signal.id,
763
+ position: signal.position,
764
+ });
765
+ let shouldClose = false;
766
+ let closeReason;
767
+ // Проверяем истечение времени
768
+ const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
769
+ if (when.getTime() >= signalEndTime) {
770
+ shouldClose = true;
771
+ closeReason = "time_expired";
772
+ }
773
+ // Проверяем достижение TP/SL для long позиции
774
+ if (signal.position === "long") {
775
+ if (averagePrice >= signal.priceTakeProfit) {
776
+ shouldClose = true;
777
+ closeReason = "take_profit";
778
+ }
779
+ else if (averagePrice <= signal.priceStopLoss) {
780
+ shouldClose = true;
781
+ closeReason = "stop_loss";
782
+ }
783
+ }
784
+ // Проверяем достижение TP/SL для short позиции
785
+ if (signal.position === "short") {
786
+ if (averagePrice <= signal.priceTakeProfit) {
787
+ shouldClose = true;
788
+ closeReason = "take_profit";
789
+ }
790
+ else if (averagePrice >= signal.priceStopLoss) {
791
+ shouldClose = true;
792
+ closeReason = "stop_loss";
793
+ }
794
+ }
795
+ // Закрываем сигнал если выполнены условия
796
+ if (shouldClose) {
797
+ const pnl = toProfitLossDto(signal, averagePrice);
798
+ const closeTimestamp = this.params.execution.context.when.getTime();
799
+ this.params.logger.debug("ClientStrategy closing", {
800
+ symbol: this.params.execution.context.symbol,
222
801
  signalId: signal.id,
223
- position: signal.position,
802
+ reason: closeReason,
803
+ priceClose: averagePrice,
804
+ closeTimestamp,
805
+ pnlPercentage: pnl.pnlPercentage,
224
806
  });
807
+ if (this.params.callbacks?.onClose) {
808
+ this.params.callbacks.onClose(this.params.execution.context.backtest, this.params.execution.context.symbol, averagePrice, signal);
809
+ }
810
+ await this.setPendingSignal(null);
811
+ return {
812
+ action: "closed",
813
+ signal: signal,
814
+ currentPrice: averagePrice,
815
+ closeReason: closeReason,
816
+ closeTimestamp: closeTimestamp,
817
+ pnl: pnl,
818
+ };
819
+ }
820
+ return {
821
+ action: "active",
822
+ signal: signal,
823
+ currentPrice: averagePrice,
824
+ };
825
+ }
826
+ async backtest(candles) {
827
+ this.params.logger.debug("ClientStrategy backtest", {
828
+ symbol: this.params.execution.context.symbol,
829
+ candlesCount: candles.length,
830
+ });
831
+ const signal = this._pendingSignal;
832
+ if (!signal) {
833
+ throw new Error("ClientStrategy backtest: no pending signal");
834
+ }
835
+ if (!this.params.execution.context.backtest) {
836
+ throw new Error("ClientStrategy backtest: running in live context");
837
+ }
838
+ // Проверяем каждую свечу на достижение TP/SL
839
+ // Начинаем с индекса 4 (пятая свеча), чтобы было минимум 5 свечей для VWAP
840
+ for (let i = 4; i < candles.length; i++) {
841
+ // Вычисляем VWAP из последних 5 свечей для текущего момента
842
+ const recentCandles = candles.slice(i - 4, i + 1);
843
+ const averagePrice = GET_AVG_PRICE_FN(recentCandles);
225
844
  let shouldClose = false;
226
845
  let closeReason;
227
- // Проверяем истечение времени
228
- const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
229
- if (when.getTime() >= signalEndTime) {
230
- shouldClose = true;
231
- closeReason = "time_expired";
232
- }
233
846
  // Проверяем достижение TP/SL для long позиции
234
847
  if (signal.position === "long") {
235
848
  if (averagePrice >= signal.priceTakeProfit) {
@@ -252,33 +865,55 @@ class ClientStrategy {
252
865
  closeReason = "stop_loss";
253
866
  }
254
867
  }
255
- // Закрываем сигнал если выполнены условия
868
+ // Если достигнут TP/SL, закрываем сигнал
256
869
  if (shouldClose) {
257
- const pnl = GET_PNL_FN(signal, averagePrice);
258
- this.params.logger.debug("ClientStrategy closing", {
259
- symbol,
870
+ const pnl = toProfitLossDto(signal, averagePrice);
871
+ const closeTimestamp = recentCandles[recentCandles.length - 1].timestamp;
872
+ this.params.logger.debug("ClientStrategy backtest closing", {
873
+ symbol: this.params.execution.context.symbol,
260
874
  signalId: signal.id,
261
875
  reason: closeReason,
262
876
  priceClose: averagePrice,
877
+ closeTimestamp,
263
878
  pnlPercentage: pnl.pnlPercentage,
264
879
  });
265
880
  if (this.params.callbacks?.onClose) {
266
- this.params.callbacks.onClose(this.params.execution.context.backtest, symbol, averagePrice, signal);
881
+ this.params.callbacks.onClose(this.params.execution.context.backtest, this.params.execution.context.symbol, averagePrice, signal);
267
882
  }
268
- this._pendingSignal = null;
883
+ await this.setPendingSignal(null);
269
884
  return {
270
885
  action: "closed",
271
886
  signal: signal,
272
887
  currentPrice: averagePrice,
273
888
  closeReason: closeReason,
889
+ closeTimestamp: closeTimestamp,
274
890
  pnl: pnl,
275
891
  };
276
892
  }
277
- return {
278
- action: "active",
279
- signal: signal,
280
- currentPrice: averagePrice,
281
- };
893
+ }
894
+ // Если TP/SL не достигнут за период, вычисляем VWAP из последних 5 свечей
895
+ const lastFiveCandles = candles.slice(-5);
896
+ const lastPrice = GET_AVG_PRICE_FN(lastFiveCandles);
897
+ const closeTimestamp = lastFiveCandles[lastFiveCandles.length - 1].timestamp;
898
+ const pnl = toProfitLossDto(signal, lastPrice);
899
+ this.params.logger.debug("ClientStrategy backtest time_expired", {
900
+ symbol: this.params.execution.context.symbol,
901
+ signalId: signal.id,
902
+ priceClose: lastPrice,
903
+ closeTimestamp,
904
+ pnlPercentage: pnl.pnlPercentage,
905
+ });
906
+ if (this.params.callbacks?.onClose) {
907
+ this.params.callbacks.onClose(this.params.execution.context.backtest, this.params.execution.context.symbol, lastPrice, signal);
908
+ }
909
+ await this.setPendingSignal(null);
910
+ return {
911
+ action: "closed",
912
+ signal: signal,
913
+ currentPrice: lastPrice,
914
+ closeReason: "time_expired",
915
+ closeTimestamp: closeTimestamp,
916
+ pnl: pnl,
282
917
  };
283
918
  }
284
919
  }
@@ -288,39 +923,108 @@ class StrategyConnectionService {
288
923
  this.loggerService = inject(TYPES.loggerService);
289
924
  this.executionContextService = inject(TYPES.executionContextService);
290
925
  this.strategySchemaService = inject(TYPES.strategySchemaService);
291
- this.candleConnectionService = inject(TYPES.candleConnectionService);
292
- this.getStrategy = memoize((symbol) => `${symbol}`, (symbol) => {
293
- const { getSignal, callbacks } = this.strategySchemaService.getSchema();
926
+ this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
927
+ this.methodContextService = inject(TYPES.methodContextService);
928
+ this.getStrategy = memoize((strategyName) => `${strategyName}`, (strategyName) => {
929
+ const { getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
294
930
  return new ClientStrategy({
295
- symbol,
931
+ interval,
296
932
  execution: this.executionContextService,
297
933
  logger: this.loggerService,
298
- candle: this.candleConnectionService,
934
+ exchange: this.exchangeConnectionService,
935
+ strategyName,
299
936
  getSignal,
300
937
  callbacks,
301
938
  });
302
939
  });
303
- this.tick = async (symbol) => {
304
- this.loggerService.log("strategyConnectionService tick", {
305
- symbol,
306
- });
307
- return await this.getStrategy(symbol).tick(symbol);
940
+ this.tick = async () => {
941
+ this.loggerService.log("strategyConnectionService tick");
942
+ const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
943
+ await strategy.waitForInit();
944
+ return await strategy.tick();
945
+ };
946
+ this.backtest = async (candles) => {
947
+ this.loggerService.log("strategyConnectionService backtest");
948
+ const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
949
+ await strategy.waitForInit();
950
+ return await strategy.backtest(candles);
308
951
  };
309
952
  }
310
953
  }
311
954
 
312
- const ExecutionContextService = scoped(class {
313
- constructor(context) {
314
- this.context = context;
955
+ const INTERVAL_MINUTES = {
956
+ "1m": 1,
957
+ "3m": 3,
958
+ "5m": 5,
959
+ "15m": 15,
960
+ "30m": 30,
961
+ "1h": 60,
962
+ "2h": 120,
963
+ "4h": 240,
964
+ "6h": 360,
965
+ "8h": 480,
966
+ "12h": 720,
967
+ "1d": 1440,
968
+ "3d": 4320,
969
+ };
970
+ const GET_TIMEFRAME_FN = async (symbol, self) => {
971
+ self.params.logger.debug("ClientFrame getTimeframe", {
972
+ symbol,
973
+ });
974
+ const { interval, startDate, endDate } = self.params;
975
+ const intervalMinutes = INTERVAL_MINUTES[interval];
976
+ if (!intervalMinutes) {
977
+ throw new Error(`ClientFrame unknown interval: ${interval}`);
315
978
  }
316
- });
979
+ const timeframes = [];
980
+ let currentDate = new Date(startDate);
981
+ while (currentDate <= endDate) {
982
+ timeframes.push(new Date(currentDate));
983
+ currentDate = new Date(currentDate.getTime() + intervalMinutes * 60 * 1000);
984
+ }
985
+ if (self.params.callbacks?.onTimeframe) {
986
+ self.params.callbacks.onTimeframe(timeframes, startDate, endDate, interval);
987
+ }
988
+ return timeframes;
989
+ };
990
+ class ClientFrame {
991
+ constructor(params) {
992
+ this.params = params;
993
+ this.getTimeframe = singleshot(async (symbol) => await GET_TIMEFRAME_FN(symbol, this));
994
+ }
995
+ }
317
996
 
318
- class CandlePublicService {
997
+ class FrameConnectionService {
319
998
  constructor() {
320
999
  this.loggerService = inject(TYPES.loggerService);
321
- this.candleConnectionService = inject(TYPES.candleConnectionService);
1000
+ this.frameSchemaService = inject(TYPES.frameSchemaService);
1001
+ this.methodContextService = inject(TYPES.methodContextService);
1002
+ this.getFrame = memoize((frameName) => `${frameName}`, (frameName) => {
1003
+ const { endDate, interval, startDate, callbacks } = this.frameSchemaService.get(frameName);
1004
+ return new ClientFrame({
1005
+ frameName,
1006
+ logger: this.loggerService,
1007
+ startDate,
1008
+ endDate,
1009
+ interval,
1010
+ callbacks,
1011
+ });
1012
+ });
1013
+ this.getTimeframe = async (symbol) => {
1014
+ this.loggerService.log("frameConnectionService getTimeframe", {
1015
+ symbol,
1016
+ });
1017
+ return await this.getFrame(this.methodContextService.context.frameName).getTimeframe(symbol);
1018
+ };
1019
+ }
1020
+ }
1021
+
1022
+ class ExchangeGlobalService {
1023
+ constructor() {
1024
+ this.loggerService = inject(TYPES.loggerService);
1025
+ this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
322
1026
  this.getCandles = async (symbol, interval, limit, when, backtest) => {
323
- this.loggerService.log("candlePublicService getCandles", {
1027
+ this.loggerService.log("exchangeGlobalService getCandles", {
324
1028
  symbol,
325
1029
  interval,
326
1030
  limit,
@@ -328,21 +1032,69 @@ class CandlePublicService {
328
1032
  backtest,
329
1033
  });
330
1034
  return await ExecutionContextService.runInContext(async () => {
331
- return await this.candleConnectionService.getCandles(symbol, interval, limit);
1035
+ return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
332
1036
  }, {
1037
+ symbol,
1038
+ when,
1039
+ backtest,
1040
+ });
1041
+ };
1042
+ this.getNextCandles = async (symbol, interval, limit, when, backtest) => {
1043
+ this.loggerService.log("exchangeGlobalService getNextCandles", {
1044
+ symbol,
1045
+ interval,
1046
+ limit,
1047
+ when,
1048
+ backtest,
1049
+ });
1050
+ return await ExecutionContextService.runInContext(async () => {
1051
+ return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
1052
+ }, {
1053
+ symbol,
333
1054
  when,
334
1055
  backtest,
335
1056
  });
336
1057
  };
337
1058
  this.getAveragePrice = async (symbol, when, backtest) => {
338
- this.loggerService.log("candlePublicService getAveragePrice", {
1059
+ this.loggerService.log("exchangeGlobalService getAveragePrice", {
1060
+ symbol,
1061
+ when,
1062
+ backtest,
1063
+ });
1064
+ return await ExecutionContextService.runInContext(async () => {
1065
+ return await this.exchangeConnectionService.getAveragePrice(symbol);
1066
+ }, {
339
1067
  symbol,
340
1068
  when,
341
1069
  backtest,
342
1070
  });
1071
+ };
1072
+ this.formatPrice = async (symbol, price, when, backtest) => {
1073
+ this.loggerService.log("exchangeGlobalService formatPrice", {
1074
+ symbol,
1075
+ price,
1076
+ when,
1077
+ backtest,
1078
+ });
343
1079
  return await ExecutionContextService.runInContext(async () => {
344
- return await this.candleConnectionService.getAveragePrice(symbol);
1080
+ return await this.exchangeConnectionService.formatPrice(symbol, price);
345
1081
  }, {
1082
+ symbol,
1083
+ when,
1084
+ backtest,
1085
+ });
1086
+ };
1087
+ this.formatQuantity = async (symbol, quantity, when, backtest) => {
1088
+ this.loggerService.log("exchangeGlobalService formatQuantity", {
1089
+ symbol,
1090
+ quantity,
1091
+ when,
1092
+ backtest,
1093
+ });
1094
+ return await ExecutionContextService.runInContext(async () => {
1095
+ return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
1096
+ }, {
1097
+ symbol,
346
1098
  when,
347
1099
  backtest,
348
1100
  });
@@ -350,19 +1102,34 @@ class CandlePublicService {
350
1102
  }
351
1103
  }
352
1104
 
353
- class StrategyPublicService {
1105
+ class StrategyGlobalService {
354
1106
  constructor() {
355
1107
  this.loggerService = inject(TYPES.loggerService);
356
1108
  this.strategyConnectionService = inject(TYPES.strategyConnectionService);
357
1109
  this.tick = async (symbol, when, backtest) => {
358
- this.loggerService.log("strategyPublicService tick", {
1110
+ this.loggerService.log("strategyGlobalService tick", {
1111
+ symbol,
1112
+ when,
1113
+ backtest,
1114
+ });
1115
+ return await ExecutionContextService.runInContext(async () => {
1116
+ return await this.strategyConnectionService.tick();
1117
+ }, {
1118
+ symbol,
1119
+ when,
1120
+ backtest,
1121
+ });
1122
+ };
1123
+ this.backtest = async (symbol, candles, when, backtest) => {
1124
+ this.loggerService.log("strategyGlobalService backtest", {
359
1125
  symbol,
360
1126
  when,
361
1127
  backtest,
362
1128
  });
363
1129
  return await ExecutionContextService.runInContext(async () => {
364
- return await this.strategyConnectionService.tick(symbol);
1130
+ return await this.strategyConnectionService.backtest(candles);
365
1131
  }, {
1132
+ symbol,
366
1133
  when,
367
1134
  backtest,
368
1135
  });
@@ -370,19 +1137,35 @@ class StrategyPublicService {
370
1137
  }
371
1138
  }
372
1139
 
373
- class CandleSchemaService {
1140
+ class FrameGlobalService {
374
1141
  constructor() {
375
1142
  this.loggerService = inject(TYPES.loggerService);
376
- this.getSchema = () => {
377
- this.loggerService.log("candleSchemaService getSchema");
378
- if (!this._candleSchema) {
379
- throw new Error("CandleSchemaService no candle source provided");
380
- }
381
- return this._candleSchema;
1143
+ this.frameConnectionService = inject(TYPES.frameConnectionService);
1144
+ this.getTimeframe = async (symbol) => {
1145
+ this.loggerService.log("frameGlobalService getTimeframe", {
1146
+ symbol,
1147
+ });
1148
+ return await this.frameConnectionService.getTimeframe(symbol);
1149
+ };
1150
+ }
1151
+ }
1152
+
1153
+ class ExchangeSchemaService {
1154
+ constructor() {
1155
+ this.loggerService = inject(TYPES.loggerService);
1156
+ this._registry = new ToolRegistry("exchangeSchema");
1157
+ this.register = (key, value) => {
1158
+ this.loggerService.info(`exchangeSchemaService register`, { key });
1159
+ this._registry = this._registry.register(key, value);
382
1160
  };
383
- this.addSchema = (candleSchema) => {
384
- this.loggerService.log("candleSchemaService addSchema");
385
- this._candleSchema = candleSchema;
1161
+ this.override = (key, value) => {
1162
+ this.loggerService.info(`exchangeSchemaService override`, { key });
1163
+ this._registry = this._registry.override(key, value);
1164
+ return this._registry.get(key);
1165
+ };
1166
+ this.get = (key) => {
1167
+ this.loggerService.info(`exchangeSchemaService get`, { key });
1168
+ return this._registry.get(key);
386
1169
  };
387
1170
  }
388
1171
  }
@@ -390,16 +1173,183 @@ class CandleSchemaService {
390
1173
  class StrategySchemaService {
391
1174
  constructor() {
392
1175
  this.loggerService = inject(TYPES.loggerService);
393
- this.getSchema = () => {
394
- this.loggerService.log("strategySchemaService getSchema");
395
- if (!this._strategySchema) {
396
- throw new Error("StrategySchemaService no strategy provided");
1176
+ this._registry = new ToolRegistry("strategySchema");
1177
+ this.register = (key, value) => {
1178
+ this.loggerService.info(`strategySchemaService register`, { key });
1179
+ this._registry = this._registry.register(key, value);
1180
+ };
1181
+ this.override = (key, value) => {
1182
+ this.loggerService.info(`strategySchemaService override`, { key });
1183
+ this._registry = this._registry.override(key, value);
1184
+ return this._registry.get(key);
1185
+ };
1186
+ this.get = (key) => {
1187
+ this.loggerService.info(`strategySchemaService get`, { key });
1188
+ return this._registry.get(key);
1189
+ };
1190
+ }
1191
+ }
1192
+
1193
+ class FrameSchemaService {
1194
+ constructor() {
1195
+ this._registry = new ToolRegistry("frameSchema");
1196
+ }
1197
+ register(key, value) {
1198
+ this._registry.register(key, value);
1199
+ }
1200
+ override(key, value) {
1201
+ this._registry.override(key, value);
1202
+ }
1203
+ get(key) {
1204
+ return this._registry.get(key);
1205
+ }
1206
+ }
1207
+
1208
+ class BacktestLogicPrivateService {
1209
+ constructor() {
1210
+ this.loggerService = inject(TYPES.loggerService);
1211
+ this.strategyGlobalService = inject(TYPES.strategyGlobalService);
1212
+ this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
1213
+ this.frameGlobalService = inject(TYPES.frameGlobalService);
1214
+ }
1215
+ async *run(symbol) {
1216
+ this.loggerService.log("backtestLogicPrivateService run", {
1217
+ symbol,
1218
+ });
1219
+ const timeframes = await this.frameGlobalService.getTimeframe(symbol);
1220
+ let i = 0;
1221
+ while (i < timeframes.length) {
1222
+ const when = timeframes[i];
1223
+ const result = await this.strategyGlobalService.tick(symbol, when, true);
1224
+ // Если сигнал открыт, вызываем backtest
1225
+ if (result.action === "opened") {
1226
+ const signal = result.signal;
1227
+ this.loggerService.info("backtestLogicPrivateService signal opened", {
1228
+ symbol,
1229
+ signalId: signal.id,
1230
+ minuteEstimatedTime: signal.minuteEstimatedTime,
1231
+ });
1232
+ // Получаем свечи для бектеста
1233
+ const candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", signal.minuteEstimatedTime, when, true);
1234
+ if (!candles.length) {
1235
+ return;
1236
+ }
1237
+ this.loggerService.info("backtestLogicPrivateService candles fetched", {
1238
+ symbol,
1239
+ signalId: signal.id,
1240
+ candlesCount: candles.length,
1241
+ });
1242
+ // Вызываем backtest - всегда возвращает closed
1243
+ const backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
1244
+ this.loggerService.info("backtestLogicPrivateService signal closed", {
1245
+ symbol,
1246
+ signalId: backtestResult.signal.id,
1247
+ closeTimestamp: backtestResult.closeTimestamp,
1248
+ closeReason: backtestResult.closeReason,
1249
+ });
1250
+ // Пропускаем timeframes до closeTimestamp
1251
+ while (i < timeframes.length &&
1252
+ timeframes[i].getTime() < backtestResult.closeTimestamp) {
1253
+ i++;
1254
+ }
1255
+ yield backtestResult;
1256
+ }
1257
+ i++;
1258
+ }
1259
+ }
1260
+ }
1261
+
1262
+ const TICK_TTL = 1 * 60 * 1000 + 1;
1263
+ class LiveLogicPrivateService {
1264
+ constructor() {
1265
+ this.loggerService = inject(TYPES.loggerService);
1266
+ this.strategyGlobalService = inject(TYPES.strategyGlobalService);
1267
+ }
1268
+ async *run(symbol) {
1269
+ this.loggerService.log("liveLogicPrivateService run", {
1270
+ symbol,
1271
+ });
1272
+ while (true) {
1273
+ const when = new Date();
1274
+ const result = await this.strategyGlobalService.tick(symbol, when, false);
1275
+ this.loggerService.info("liveLogicPrivateService tick result", {
1276
+ symbol,
1277
+ action: result.action,
1278
+ });
1279
+ if (result.action === "active") {
1280
+ await sleep(TICK_TTL);
1281
+ continue;
1282
+ }
1283
+ if (result.action === "idle") {
1284
+ await sleep(TICK_TTL);
1285
+ continue;
397
1286
  }
398
- return this._strategySchema;
1287
+ yield result;
1288
+ await sleep(TICK_TTL);
1289
+ }
1290
+ }
1291
+ }
1292
+
1293
+ class BacktestLogicPublicService {
1294
+ constructor() {
1295
+ this.loggerService = inject(TYPES.loggerService);
1296
+ this.backtestLogicPrivateService = inject(TYPES.backtestLogicPrivateService);
1297
+ this.run = (symbol, context) => {
1298
+ this.loggerService.log("backtestLogicPublicService run", {
1299
+ symbol,
1300
+ context,
1301
+ });
1302
+ return MethodContextService.runAsyncIterator(this.backtestLogicPrivateService.run(symbol), {
1303
+ exchangeName: context.exchangeName,
1304
+ strategyName: context.strategyName,
1305
+ frameName: context.frameName,
1306
+ });
399
1307
  };
400
- this.addSchema = (strategySchema) => {
401
- this.loggerService.log("strategySchemaService addSchema");
402
- this._strategySchema = strategySchema;
1308
+ }
1309
+ }
1310
+
1311
+ class LiveLogicPublicService {
1312
+ constructor() {
1313
+ this.loggerService = inject(TYPES.loggerService);
1314
+ this.liveLogicPrivateService = inject(TYPES.liveLogicPrivateService);
1315
+ this.run = (symbol, context) => {
1316
+ this.loggerService.log("liveLogicPublicService run", {
1317
+ symbol,
1318
+ context,
1319
+ });
1320
+ return MethodContextService.runAsyncIterator(this.liveLogicPrivateService.run(symbol), {
1321
+ exchangeName: context.exchangeName,
1322
+ strategyName: context.strategyName,
1323
+ frameName: "",
1324
+ });
1325
+ };
1326
+ }
1327
+ }
1328
+
1329
+ class LiveGlobalService {
1330
+ constructor() {
1331
+ this.loggerService = inject(TYPES.loggerService);
1332
+ this.liveLogicPublicService = inject(TYPES.liveLogicPublicService);
1333
+ this.run = (symbol, context) => {
1334
+ this.loggerService.log("liveGlobalService run", {
1335
+ symbol,
1336
+ context,
1337
+ });
1338
+ return this.liveLogicPublicService.run(symbol, context);
1339
+ };
1340
+ }
1341
+ }
1342
+
1343
+ class BacktestGlobalService {
1344
+ constructor() {
1345
+ this.loggerService = inject(TYPES.loggerService);
1346
+ this.backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
1347
+ this.run = (symbol, context) => {
1348
+ this.loggerService.log("backtestGlobalService run", {
1349
+ symbol,
1350
+ context,
1351
+ });
1352
+ return this.backtestLogicPublicService.run(symbol, context);
403
1353
  };
404
1354
  }
405
1355
  }
@@ -409,18 +1359,32 @@ class StrategySchemaService {
409
1359
  }
410
1360
  {
411
1361
  provide(TYPES.executionContextService, () => new ExecutionContextService());
1362
+ provide(TYPES.methodContextService, () => new MethodContextService());
412
1363
  }
413
1364
  {
414
- provide(TYPES.candleConnectionService, () => new CandleConnectionService());
1365
+ provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
415
1366
  provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
1367
+ provide(TYPES.frameConnectionService, () => new FrameConnectionService());
416
1368
  }
417
1369
  {
418
- provide(TYPES.candleSchemaService, () => new CandleSchemaService());
1370
+ provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
419
1371
  provide(TYPES.strategySchemaService, () => new StrategySchemaService());
1372
+ provide(TYPES.frameSchemaService, () => new FrameSchemaService());
1373
+ }
1374
+ {
1375
+ provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
1376
+ provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
1377
+ provide(TYPES.frameGlobalService, () => new FrameGlobalService());
1378
+ provide(TYPES.liveGlobalService, () => new LiveGlobalService());
1379
+ provide(TYPES.backtestGlobalService, () => new BacktestGlobalService());
1380
+ }
1381
+ {
1382
+ provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
1383
+ provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
420
1384
  }
421
1385
  {
422
- provide(TYPES.candlePublicService, () => new CandlePublicService());
423
- provide(TYPES.strategyPublicService, () => new StrategyPublicService());
1386
+ provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
1387
+ provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
424
1388
  }
425
1389
 
426
1390
  const baseServices = {
@@ -428,39 +1392,75 @@ const baseServices = {
428
1392
  };
429
1393
  const contextServices = {
430
1394
  executionContextService: inject(TYPES.executionContextService),
1395
+ methodContextService: inject(TYPES.methodContextService),
431
1396
  };
432
1397
  const connectionServices = {
433
- candleConnectionService: inject(TYPES.candleConnectionService),
1398
+ exchangeConnectionService: inject(TYPES.exchangeConnectionService),
434
1399
  strategyConnectionService: inject(TYPES.strategyConnectionService),
1400
+ frameConnectionService: inject(TYPES.frameConnectionService),
435
1401
  };
436
1402
  const schemaServices = {
437
- candleSchemaService: inject(TYPES.candleSchemaService),
1403
+ exchangeSchemaService: inject(TYPES.exchangeSchemaService),
438
1404
  strategySchemaService: inject(TYPES.strategySchemaService),
1405
+ frameSchemaService: inject(TYPES.frameSchemaService),
1406
+ };
1407
+ const globalServices = {
1408
+ exchangeGlobalService: inject(TYPES.exchangeGlobalService),
1409
+ strategyGlobalService: inject(TYPES.strategyGlobalService),
1410
+ frameGlobalService: inject(TYPES.frameGlobalService),
1411
+ liveGlobalService: inject(TYPES.liveGlobalService),
1412
+ backtestGlobalService: inject(TYPES.backtestGlobalService),
439
1413
  };
440
- const publicServices = {
441
- candlePublicService: inject(TYPES.candlePublicService),
442
- strategyPublicService: inject(TYPES.strategyPublicService),
1414
+ const logicPrivateServices = {
1415
+ backtestLogicPrivateService: inject(TYPES.backtestLogicPrivateService),
1416
+ liveLogicPrivateService: inject(TYPES.liveLogicPrivateService),
1417
+ };
1418
+ const logicPublicServices = {
1419
+ backtestLogicPublicService: inject(TYPES.backtestLogicPublicService),
1420
+ liveLogicPublicService: inject(TYPES.liveLogicPublicService),
443
1421
  };
444
1422
  const backtest = {
445
1423
  ...baseServices,
446
1424
  ...contextServices,
447
1425
  ...connectionServices,
448
1426
  ...schemaServices,
449
- ...publicServices,
1427
+ ...globalServices,
1428
+ ...logicPrivateServices,
1429
+ ...logicPublicServices,
450
1430
  };
451
1431
  init();
1432
+ var backtest$1 = backtest;
452
1433
 
1434
+ async function setLogger(logger) {
1435
+ backtest$1.loggerService.setLogger(logger);
1436
+ }
1437
+
1438
+ const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
1439
+ const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
1440
+ const ADD_FRAME_METHOD_NAME = "add.addFrame";
453
1441
  function addStrategy(strategySchema) {
454
- backtest.strategySchemaService.addSchema(strategySchema);
1442
+ backtest$1.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
1443
+ strategySchema,
1444
+ });
1445
+ backtest$1.strategySchemaService.register(strategySchema.strategyName, strategySchema);
455
1446
  }
456
- function addCandle(candleSchema) {
457
- backtest.candleSchemaService.addSchema(candleSchema);
1447
+ function addExchange(exchangeSchema) {
1448
+ backtest$1.loggerService.info(ADD_EXCHANGE_METHOD_NAME, {
1449
+ exchangeSchema,
1450
+ });
1451
+ backtest$1.exchangeSchemaService.register(exchangeSchema.exchangeName, exchangeSchema);
1452
+ }
1453
+ function addFrame(frameSchema) {
1454
+ backtest$1.loggerService.info(ADD_FRAME_METHOD_NAME, {
1455
+ frameSchema,
1456
+ });
1457
+ backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
458
1458
  }
459
1459
 
460
1460
  async function runBacktest(symbol, timeframes) {
461
1461
  const results = [];
462
1462
  for (const when of timeframes) {
463
- const result = await backtest.strategyPublicService.tick(symbol, when, true);
1463
+ const result = await backtest$1.strategyGlobalService.tick(symbol, when, true);
464
1464
  // Сохраняем только результаты closed
465
1465
  if (result.action === "closed") {
466
1466
  results.push(result);
@@ -540,7 +1540,7 @@ function startRun(config) {
540
1540
  }
541
1541
  const doWork = singlerun(async () => {
542
1542
  const now = new Date();
543
- const result = await backtest.strategyPublicService.tick(symbol, now, false);
1543
+ const result = await backtest$1.strategyGlobalService.tick(symbol, now, false);
544
1544
  const instance = instances.get(symbol);
545
1545
  if (instance) {
546
1546
  instance.tickCount++;
@@ -570,11 +1570,77 @@ function stopAll() {
570
1570
  instances.clear();
571
1571
  }
572
1572
 
1573
+ const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
1574
+ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
1575
+ const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
1576
+ const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
1577
+ const GET_DATE_METHOD_NAME = "exchange.getDate";
1578
+ const GET_MODE_METHOD_NAME = "exchange.getMode";
573
1579
  async function getCandles(symbol, interval, limit) {
574
- return await backtest.candleConnectionService.getCandles(symbol, interval, limit);
1580
+ backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
1581
+ symbol,
1582
+ interval,
1583
+ limit,
1584
+ });
1585
+ return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
575
1586
  }
576
1587
  async function getAveragePrice(symbol) {
577
- return await backtest.candleConnectionService.getAveragePrice(symbol);
1588
+ backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
1589
+ symbol,
1590
+ });
1591
+ return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
1592
+ }
1593
+ async function formatPrice(symbol, price) {
1594
+ backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
1595
+ symbol,
1596
+ price,
1597
+ });
1598
+ return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
1599
+ }
1600
+ async function formatQuantity(symbol, quantity) {
1601
+ backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
1602
+ symbol,
1603
+ quantity,
1604
+ });
1605
+ return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
1606
+ }
1607
+ async function getDate() {
1608
+ backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
1609
+ const { when } = backtest$1.executionContextService.context;
1610
+ return new Date(when.getTime());
1611
+ }
1612
+ async function getMode() {
1613
+ backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
1614
+ const { backtest: bt } = backtest$1.executionContextService.context;
1615
+ return bt ? "backtest" : "live";
1616
+ }
1617
+
1618
+ const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
1619
+ class BacktestUtils {
1620
+ constructor() {
1621
+ this.run = (symbol, context) => {
1622
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_RUN, {
1623
+ symbol,
1624
+ context,
1625
+ });
1626
+ return backtest$1.backtestGlobalService.run(symbol, context);
1627
+ };
1628
+ }
1629
+ }
1630
+ const Backtest = new BacktestUtils();
1631
+
1632
+ const LIVE_METHOD_NAME_RUN = "LiveUtils.run";
1633
+ class LiveUtils {
1634
+ constructor() {
1635
+ this.run = (symbol, context) => {
1636
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_RUN, {
1637
+ symbol,
1638
+ context,
1639
+ });
1640
+ return backtest$1.liveGlobalService.run(symbol, context);
1641
+ };
1642
+ }
578
1643
  }
1644
+ const Live = new LiveUtils();
579
1645
 
580
- export { ExecutionContextService, addCandle, addStrategy, backtest, getAveragePrice, getCandles, reduce, runBacktest, runBacktestGUI, startRun, stopAll, stopRun };
1646
+ export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy, backtest, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, reduce, runBacktest, runBacktestGUI, setLogger, startRun, stopAll, stopRun };