backtest-kit 1.0.3 → 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.cjs +1197 -186
- package/build/index.mjs +1189 -187
- package/package.json +1 -1
- package/types.d.ts +264 -46
package/build/index.mjs
CHANGED
|
@@ -1,63 +1,109 @@
|
|
|
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
|
|
|
6
|
-
const NOOP_LOGGER = {
|
|
7
|
-
log() {
|
|
8
|
-
},
|
|
9
|
-
debug() {
|
|
10
|
-
},
|
|
11
|
-
info() {
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
class LoggerService {
|
|
15
|
-
constructor() {
|
|
16
|
-
this._commonLogger = NOOP_LOGGER;
|
|
17
|
-
this.log = async (topic, ...args) => {
|
|
18
|
-
await this._commonLogger.log(topic, ...args);
|
|
19
|
-
};
|
|
20
|
-
this.debug = async (topic, ...args) => {
|
|
21
|
-
await this._commonLogger.debug(topic, ...args);
|
|
22
|
-
};
|
|
23
|
-
this.info = async (topic, ...args) => {
|
|
24
|
-
await this._commonLogger.info(topic, ...args);
|
|
25
|
-
};
|
|
26
|
-
this.setLogger = (logger) => {
|
|
27
|
-
this._commonLogger = logger;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
10
|
const { init, inject, provide } = createActivator("backtest");
|
|
33
11
|
|
|
12
|
+
const MethodContextService = scoped(class {
|
|
13
|
+
constructor(context) {
|
|
14
|
+
this.context = context;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
34
18
|
const baseServices$1 = {
|
|
35
19
|
loggerService: Symbol('loggerService'),
|
|
36
20
|
};
|
|
37
21
|
const contextServices$1 = {
|
|
38
22
|
executionContextService: Symbol('executionContextService'),
|
|
23
|
+
methodContextService: Symbol('methodContextService'),
|
|
39
24
|
};
|
|
40
25
|
const connectionServices$1 = {
|
|
41
26
|
exchangeConnectionService: Symbol('exchangeConnectionService'),
|
|
42
27
|
strategyConnectionService: Symbol('strategyConnectionService'),
|
|
28
|
+
frameConnectionService: Symbol('frameConnectionService'),
|
|
43
29
|
};
|
|
44
30
|
const schemaServices$1 = {
|
|
45
31
|
exchangeSchemaService: Symbol('exchangeSchemaService'),
|
|
46
32
|
strategySchemaService: Symbol('strategySchemaService'),
|
|
33
|
+
frameSchemaService: Symbol('frameSchemaService'),
|
|
47
34
|
};
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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'),
|
|
51
49
|
};
|
|
52
50
|
const TYPES = {
|
|
53
51
|
...baseServices$1,
|
|
54
52
|
...contextServices$1,
|
|
55
53
|
...connectionServices$1,
|
|
56
54
|
...schemaServices$1,
|
|
57
|
-
...
|
|
55
|
+
...globalServices$1,
|
|
56
|
+
...logicPrivateServices$1,
|
|
57
|
+
...logicPublicServices$1,
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const ExecutionContextService = scoped(class {
|
|
61
|
+
constructor(context) {
|
|
62
|
+
this.context = context;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const NOOP_LOGGER = {
|
|
67
|
+
log() {
|
|
68
|
+
},
|
|
69
|
+
debug() {
|
|
70
|
+
},
|
|
71
|
+
info() {
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
class LoggerService {
|
|
75
|
+
constructor() {
|
|
76
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
77
|
+
this.executionContextService = inject(TYPES.executionContextService);
|
|
78
|
+
this._commonLogger = NOOP_LOGGER;
|
|
79
|
+
this.log = async (topic, ...args) => {
|
|
80
|
+
await this._commonLogger.log(topic, ...args, this.methodContext, this.executionContext);
|
|
81
|
+
};
|
|
82
|
+
this.debug = async (topic, ...args) => {
|
|
83
|
+
await this._commonLogger.debug(topic, ...args, this.methodContext, this.executionContext);
|
|
84
|
+
};
|
|
85
|
+
this.info = async (topic, ...args) => {
|
|
86
|
+
await this._commonLogger.info(topic, ...args, this.methodContext, this.executionContext);
|
|
87
|
+
};
|
|
88
|
+
this.setLogger = (logger) => {
|
|
89
|
+
this._commonLogger = logger;
|
|
90
|
+
};
|
|
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
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const INTERVAL_MINUTES$2 = {
|
|
61
107
|
"1m": 1,
|
|
62
108
|
"3m": 3,
|
|
63
109
|
"5m": 5,
|
|
@@ -72,61 +118,82 @@ const INTERVAL_MINUTES = {
|
|
|
72
118
|
class ClientExchange {
|
|
73
119
|
constructor(params) {
|
|
74
120
|
this.params = params;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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);
|
|
130
197
|
}
|
|
131
198
|
}
|
|
132
199
|
|
|
@@ -135,11 +202,13 @@ class ExchangeConnectionService {
|
|
|
135
202
|
this.loggerService = inject(TYPES.loggerService);
|
|
136
203
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
137
204
|
this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
|
|
138
|
-
this.
|
|
139
|
-
|
|
205
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
206
|
+
this.getExchange = memoize((exchangeName) => `${exchangeName}`, (exchangeName) => {
|
|
207
|
+
const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
|
|
140
208
|
return new ClientExchange({
|
|
141
209
|
execution: this.executionContextService,
|
|
142
210
|
logger: this.loggerService,
|
|
211
|
+
exchangeName,
|
|
143
212
|
getCandles,
|
|
144
213
|
formatPrice,
|
|
145
214
|
formatQuantity,
|
|
@@ -152,34 +221,42 @@ class ExchangeConnectionService {
|
|
|
152
221
|
interval,
|
|
153
222
|
limit,
|
|
154
223
|
});
|
|
155
|
-
return await this.getExchange(
|
|
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", {
|
|
228
|
+
symbol,
|
|
229
|
+
interval,
|
|
230
|
+
limit,
|
|
231
|
+
});
|
|
232
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).getNextCandles(symbol, interval, limit);
|
|
156
233
|
};
|
|
157
234
|
this.getAveragePrice = async (symbol) => {
|
|
158
235
|
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
159
236
|
symbol,
|
|
160
237
|
});
|
|
161
|
-
return await this.getExchange(
|
|
238
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).getAveragePrice(symbol);
|
|
162
239
|
};
|
|
163
240
|
this.formatPrice = async (symbol, price) => {
|
|
164
241
|
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
165
242
|
symbol,
|
|
166
243
|
price,
|
|
167
244
|
});
|
|
168
|
-
return await this.getExchange(
|
|
245
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).formatPrice(symbol, price);
|
|
169
246
|
};
|
|
170
247
|
this.formatQuantity = async (symbol, quantity) => {
|
|
171
248
|
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
172
249
|
symbol,
|
|
173
250
|
quantity,
|
|
174
251
|
});
|
|
175
|
-
return await this.getExchange(
|
|
252
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).formatQuantity(symbol, quantity);
|
|
176
253
|
};
|
|
177
254
|
}
|
|
178
255
|
}
|
|
179
256
|
|
|
180
257
|
const PERCENT_SLIPPAGE = 0.1;
|
|
181
258
|
const PERCENT_FEE = 0.1;
|
|
182
|
-
const
|
|
259
|
+
const toProfitLossDto = (signal, priceClose) => {
|
|
183
260
|
const priceOpen = signal.priceOpen;
|
|
184
261
|
let priceOpenWithSlippage;
|
|
185
262
|
let priceCloseWithSlippage;
|
|
@@ -218,48 +295,554 @@ const GET_PNL_FN = (signal, priceClose) => {
|
|
|
218
295
|
priceClose,
|
|
219
296
|
};
|
|
220
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
|
+
};
|
|
221
719
|
class ClientStrategy {
|
|
222
720
|
constructor(params) {
|
|
223
721
|
this.params = params;
|
|
224
722
|
this._pendingSignal = null;
|
|
225
|
-
this.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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);
|
|
239
744
|
}
|
|
240
745
|
return {
|
|
241
|
-
action: "
|
|
242
|
-
signal:
|
|
746
|
+
action: "opened",
|
|
747
|
+
signal: this._pendingSignal,
|
|
243
748
|
};
|
|
244
749
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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,
|
|
252
801
|
signalId: signal.id,
|
|
253
|
-
|
|
802
|
+
reason: closeReason,
|
|
803
|
+
priceClose: averagePrice,
|
|
804
|
+
closeTimestamp,
|
|
805
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
254
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);
|
|
255
844
|
let shouldClose = false;
|
|
256
845
|
let closeReason;
|
|
257
|
-
// Проверяем истечение времени
|
|
258
|
-
const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
|
|
259
|
-
if (when.getTime() >= signalEndTime) {
|
|
260
|
-
shouldClose = true;
|
|
261
|
-
closeReason = "time_expired";
|
|
262
|
-
}
|
|
263
846
|
// Проверяем достижение TP/SL для long позиции
|
|
264
847
|
if (signal.position === "long") {
|
|
265
848
|
if (averagePrice >= signal.priceTakeProfit) {
|
|
@@ -282,33 +865,55 @@ class ClientStrategy {
|
|
|
282
865
|
closeReason = "stop_loss";
|
|
283
866
|
}
|
|
284
867
|
}
|
|
285
|
-
//
|
|
868
|
+
// Если достигнут TP/SL, закрываем сигнал
|
|
286
869
|
if (shouldClose) {
|
|
287
|
-
const pnl =
|
|
288
|
-
|
|
289
|
-
|
|
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,
|
|
290
874
|
signalId: signal.id,
|
|
291
875
|
reason: closeReason,
|
|
292
876
|
priceClose: averagePrice,
|
|
877
|
+
closeTimestamp,
|
|
293
878
|
pnlPercentage: pnl.pnlPercentage,
|
|
294
879
|
});
|
|
295
880
|
if (this.params.callbacks?.onClose) {
|
|
296
|
-
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);
|
|
297
882
|
}
|
|
298
|
-
this.
|
|
883
|
+
await this.setPendingSignal(null);
|
|
299
884
|
return {
|
|
300
885
|
action: "closed",
|
|
301
886
|
signal: signal,
|
|
302
887
|
currentPrice: averagePrice,
|
|
303
888
|
closeReason: closeReason,
|
|
889
|
+
closeTimestamp: closeTimestamp,
|
|
304
890
|
pnl: pnl,
|
|
305
891
|
};
|
|
306
892
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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,
|
|
312
917
|
};
|
|
313
918
|
}
|
|
314
919
|
}
|
|
@@ -319,38 +924,107 @@ class StrategyConnectionService {
|
|
|
319
924
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
320
925
|
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
321
926
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
322
|
-
this.
|
|
323
|
-
|
|
927
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
928
|
+
this.getStrategy = memoize((strategyName) => `${strategyName}`, (strategyName) => {
|
|
929
|
+
const { getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
|
|
324
930
|
return new ClientStrategy({
|
|
325
|
-
|
|
931
|
+
interval,
|
|
326
932
|
execution: this.executionContextService,
|
|
327
933
|
logger: this.loggerService,
|
|
328
934
|
exchange: this.exchangeConnectionService,
|
|
935
|
+
strategyName,
|
|
329
936
|
getSignal,
|
|
330
937
|
callbacks,
|
|
331
938
|
});
|
|
332
939
|
});
|
|
333
|
-
this.tick = async (
|
|
334
|
-
this.loggerService.log("strategyConnectionService tick"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return await
|
|
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);
|
|
338
951
|
};
|
|
339
952
|
}
|
|
340
953
|
}
|
|
341
954
|
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
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}`);
|
|
345
978
|
}
|
|
346
|
-
|
|
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
|
+
}
|
|
996
|
+
|
|
997
|
+
class FrameConnectionService {
|
|
998
|
+
constructor() {
|
|
999
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
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
|
+
}
|
|
347
1021
|
|
|
348
|
-
class
|
|
1022
|
+
class ExchangeGlobalService {
|
|
349
1023
|
constructor() {
|
|
350
1024
|
this.loggerService = inject(TYPES.loggerService);
|
|
351
1025
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
352
1026
|
this.getCandles = async (symbol, interval, limit, when, backtest) => {
|
|
353
|
-
this.loggerService.log("
|
|
1027
|
+
this.loggerService.log("exchangeGlobalService getCandles", {
|
|
354
1028
|
symbol,
|
|
355
1029
|
interval,
|
|
356
1030
|
limit,
|
|
@@ -360,12 +1034,29 @@ class ExchangePublicService {
|
|
|
360
1034
|
return await ExecutionContextService.runInContext(async () => {
|
|
361
1035
|
return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
362
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,
|
|
363
1054
|
when,
|
|
364
1055
|
backtest,
|
|
365
1056
|
});
|
|
366
1057
|
};
|
|
367
1058
|
this.getAveragePrice = async (symbol, when, backtest) => {
|
|
368
|
-
this.loggerService.log("
|
|
1059
|
+
this.loggerService.log("exchangeGlobalService getAveragePrice", {
|
|
369
1060
|
symbol,
|
|
370
1061
|
when,
|
|
371
1062
|
backtest,
|
|
@@ -373,12 +1064,13 @@ class ExchangePublicService {
|
|
|
373
1064
|
return await ExecutionContextService.runInContext(async () => {
|
|
374
1065
|
return await this.exchangeConnectionService.getAveragePrice(symbol);
|
|
375
1066
|
}, {
|
|
1067
|
+
symbol,
|
|
376
1068
|
when,
|
|
377
1069
|
backtest,
|
|
378
1070
|
});
|
|
379
1071
|
};
|
|
380
1072
|
this.formatPrice = async (symbol, price, when, backtest) => {
|
|
381
|
-
this.loggerService.log("
|
|
1073
|
+
this.loggerService.log("exchangeGlobalService formatPrice", {
|
|
382
1074
|
symbol,
|
|
383
1075
|
price,
|
|
384
1076
|
when,
|
|
@@ -387,12 +1079,13 @@ class ExchangePublicService {
|
|
|
387
1079
|
return await ExecutionContextService.runInContext(async () => {
|
|
388
1080
|
return await this.exchangeConnectionService.formatPrice(symbol, price);
|
|
389
1081
|
}, {
|
|
1082
|
+
symbol,
|
|
390
1083
|
when,
|
|
391
1084
|
backtest,
|
|
392
1085
|
});
|
|
393
1086
|
};
|
|
394
1087
|
this.formatQuantity = async (symbol, quantity, when, backtest) => {
|
|
395
|
-
this.loggerService.log("
|
|
1088
|
+
this.loggerService.log("exchangeGlobalService formatQuantity", {
|
|
396
1089
|
symbol,
|
|
397
1090
|
quantity,
|
|
398
1091
|
when,
|
|
@@ -401,6 +1094,7 @@ class ExchangePublicService {
|
|
|
401
1094
|
return await ExecutionContextService.runInContext(async () => {
|
|
402
1095
|
return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
403
1096
|
}, {
|
|
1097
|
+
symbol,
|
|
404
1098
|
when,
|
|
405
1099
|
backtest,
|
|
406
1100
|
});
|
|
@@ -408,22 +1102,50 @@ class ExchangePublicService {
|
|
|
408
1102
|
}
|
|
409
1103
|
}
|
|
410
1104
|
|
|
411
|
-
class
|
|
1105
|
+
class StrategyGlobalService {
|
|
412
1106
|
constructor() {
|
|
413
1107
|
this.loggerService = inject(TYPES.loggerService);
|
|
414
1108
|
this.strategyConnectionService = inject(TYPES.strategyConnectionService);
|
|
415
1109
|
this.tick = async (symbol, when, backtest) => {
|
|
416
|
-
this.loggerService.log("
|
|
1110
|
+
this.loggerService.log("strategyGlobalService tick", {
|
|
417
1111
|
symbol,
|
|
418
1112
|
when,
|
|
419
1113
|
backtest,
|
|
420
1114
|
});
|
|
421
1115
|
return await ExecutionContextService.runInContext(async () => {
|
|
422
|
-
return await this.strategyConnectionService.tick(
|
|
1116
|
+
return await this.strategyConnectionService.tick();
|
|
423
1117
|
}, {
|
|
1118
|
+
symbol,
|
|
1119
|
+
when,
|
|
1120
|
+
backtest,
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
this.backtest = async (symbol, candles, when, backtest) => {
|
|
1124
|
+
this.loggerService.log("strategyGlobalService backtest", {
|
|
1125
|
+
symbol,
|
|
424
1126
|
when,
|
|
425
1127
|
backtest,
|
|
426
1128
|
});
|
|
1129
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
1130
|
+
return await this.strategyConnectionService.backtest(candles);
|
|
1131
|
+
}, {
|
|
1132
|
+
symbol,
|
|
1133
|
+
when,
|
|
1134
|
+
backtest,
|
|
1135
|
+
});
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
class FrameGlobalService {
|
|
1141
|
+
constructor() {
|
|
1142
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
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);
|
|
427
1149
|
};
|
|
428
1150
|
}
|
|
429
1151
|
}
|
|
@@ -431,16 +1153,19 @@ class StrategyPublicService {
|
|
|
431
1153
|
class ExchangeSchemaService {
|
|
432
1154
|
constructor() {
|
|
433
1155
|
this.loggerService = inject(TYPES.loggerService);
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
return this._exchangeSchema;
|
|
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);
|
|
440
1160
|
};
|
|
441
|
-
this.
|
|
442
|
-
this.loggerService.
|
|
443
|
-
this.
|
|
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);
|
|
444
1169
|
};
|
|
445
1170
|
}
|
|
446
1171
|
}
|
|
@@ -448,16 +1173,183 @@ class ExchangeSchemaService {
|
|
|
448
1173
|
class StrategySchemaService {
|
|
449
1174
|
constructor() {
|
|
450
1175
|
this.loggerService = inject(TYPES.loggerService);
|
|
451
|
-
this.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
455
1286
|
}
|
|
456
|
-
|
|
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
|
+
});
|
|
457
1307
|
};
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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);
|
|
461
1353
|
};
|
|
462
1354
|
}
|
|
463
1355
|
}
|
|
@@ -467,18 +1359,32 @@ class StrategySchemaService {
|
|
|
467
1359
|
}
|
|
468
1360
|
{
|
|
469
1361
|
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
1362
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
470
1363
|
}
|
|
471
1364
|
{
|
|
472
1365
|
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
473
1366
|
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
1367
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
474
1368
|
}
|
|
475
1369
|
{
|
|
476
1370
|
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
477
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());
|
|
478
1380
|
}
|
|
479
1381
|
{
|
|
480
|
-
provide(TYPES.
|
|
481
|
-
provide(TYPES.
|
|
1382
|
+
provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
|
|
1383
|
+
provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
|
|
1384
|
+
}
|
|
1385
|
+
{
|
|
1386
|
+
provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
|
|
1387
|
+
provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
|
|
482
1388
|
}
|
|
483
1389
|
|
|
484
1390
|
const baseServices = {
|
|
@@ -486,39 +1392,75 @@ const baseServices = {
|
|
|
486
1392
|
};
|
|
487
1393
|
const contextServices = {
|
|
488
1394
|
executionContextService: inject(TYPES.executionContextService),
|
|
1395
|
+
methodContextService: inject(TYPES.methodContextService),
|
|
489
1396
|
};
|
|
490
1397
|
const connectionServices = {
|
|
491
1398
|
exchangeConnectionService: inject(TYPES.exchangeConnectionService),
|
|
492
1399
|
strategyConnectionService: inject(TYPES.strategyConnectionService),
|
|
1400
|
+
frameConnectionService: inject(TYPES.frameConnectionService),
|
|
493
1401
|
};
|
|
494
1402
|
const schemaServices = {
|
|
495
1403
|
exchangeSchemaService: inject(TYPES.exchangeSchemaService),
|
|
496
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),
|
|
1413
|
+
};
|
|
1414
|
+
const logicPrivateServices = {
|
|
1415
|
+
backtestLogicPrivateService: inject(TYPES.backtestLogicPrivateService),
|
|
1416
|
+
liveLogicPrivateService: inject(TYPES.liveLogicPrivateService),
|
|
497
1417
|
};
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
1418
|
+
const logicPublicServices = {
|
|
1419
|
+
backtestLogicPublicService: inject(TYPES.backtestLogicPublicService),
|
|
1420
|
+
liveLogicPublicService: inject(TYPES.liveLogicPublicService),
|
|
501
1421
|
};
|
|
502
1422
|
const backtest = {
|
|
503
1423
|
...baseServices,
|
|
504
1424
|
...contextServices,
|
|
505
1425
|
...connectionServices,
|
|
506
1426
|
...schemaServices,
|
|
507
|
-
...
|
|
1427
|
+
...globalServices,
|
|
1428
|
+
...logicPrivateServices,
|
|
1429
|
+
...logicPublicServices,
|
|
508
1430
|
};
|
|
509
1431
|
init();
|
|
1432
|
+
var backtest$1 = backtest;
|
|
510
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";
|
|
511
1441
|
function addStrategy(strategySchema) {
|
|
512
|
-
backtest.
|
|
1442
|
+
backtest$1.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
|
|
1443
|
+
strategySchema,
|
|
1444
|
+
});
|
|
1445
|
+
backtest$1.strategySchemaService.register(strategySchema.strategyName, strategySchema);
|
|
513
1446
|
}
|
|
514
1447
|
function addExchange(exchangeSchema) {
|
|
515
|
-
backtest.
|
|
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);
|
|
516
1458
|
}
|
|
517
1459
|
|
|
518
1460
|
async function runBacktest(symbol, timeframes) {
|
|
519
1461
|
const results = [];
|
|
520
1462
|
for (const when of timeframes) {
|
|
521
|
-
const result = await backtest.
|
|
1463
|
+
const result = await backtest$1.strategyGlobalService.tick(symbol, when, true);
|
|
522
1464
|
// Сохраняем только результаты closed
|
|
523
1465
|
if (result.action === "closed") {
|
|
524
1466
|
results.push(result);
|
|
@@ -598,7 +1540,7 @@ function startRun(config) {
|
|
|
598
1540
|
}
|
|
599
1541
|
const doWork = singlerun(async () => {
|
|
600
1542
|
const now = new Date();
|
|
601
|
-
const result = await backtest.
|
|
1543
|
+
const result = await backtest$1.strategyGlobalService.tick(symbol, now, false);
|
|
602
1544
|
const instance = instances.get(symbol);
|
|
603
1545
|
if (instance) {
|
|
604
1546
|
instance.tickCount++;
|
|
@@ -628,17 +1570,77 @@ function stopAll() {
|
|
|
628
1570
|
instances.clear();
|
|
629
1571
|
}
|
|
630
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";
|
|
631
1579
|
async function getCandles(symbol, interval, limit) {
|
|
632
|
-
|
|
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);
|
|
633
1586
|
}
|
|
634
1587
|
async function getAveragePrice(symbol) {
|
|
635
|
-
|
|
1588
|
+
backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
|
|
1589
|
+
symbol,
|
|
1590
|
+
});
|
|
1591
|
+
return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
|
|
636
1592
|
}
|
|
637
1593
|
async function formatPrice(symbol, price) {
|
|
638
|
-
|
|
1594
|
+
backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
|
|
1595
|
+
symbol,
|
|
1596
|
+
price,
|
|
1597
|
+
});
|
|
1598
|
+
return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
|
|
639
1599
|
}
|
|
640
1600
|
async function formatQuantity(symbol, quantity) {
|
|
641
|
-
|
|
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
|
+
}
|
|
642
1643
|
}
|
|
1644
|
+
const Live = new LiveUtils();
|
|
643
1645
|
|
|
644
|
-
export { ExecutionContextService, addExchange, addStrategy, backtest, formatPrice, formatQuantity, 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 };
|