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