backtest-kit 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -9
- package/build/index.cjs +1262 -185
- package/build/index.mjs +1251 -185
- package/package.json +1 -1
- package/types.d.ts +288 -58
package/build/index.cjs
CHANGED
|
@@ -1,10 +1,70 @@
|
|
|
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
|
|
|
12
|
+
const { init, inject, provide } = diKit.createActivator("backtest");
|
|
13
|
+
|
|
14
|
+
const MethodContextService = diScoped.scoped(class {
|
|
15
|
+
constructor(context) {
|
|
16
|
+
this.context = context;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const baseServices$1 = {
|
|
21
|
+
loggerService: Symbol('loggerService'),
|
|
22
|
+
};
|
|
23
|
+
const contextServices$1 = {
|
|
24
|
+
executionContextService: Symbol('executionContextService'),
|
|
25
|
+
methodContextService: Symbol('methodContextService'),
|
|
26
|
+
};
|
|
27
|
+
const connectionServices$1 = {
|
|
28
|
+
exchangeConnectionService: Symbol('exchangeConnectionService'),
|
|
29
|
+
strategyConnectionService: Symbol('strategyConnectionService'),
|
|
30
|
+
frameConnectionService: Symbol('frameConnectionService'),
|
|
31
|
+
};
|
|
32
|
+
const schemaServices$1 = {
|
|
33
|
+
exchangeSchemaService: Symbol('exchangeSchemaService'),
|
|
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'),
|
|
47
|
+
};
|
|
48
|
+
const logicPublicServices$1 = {
|
|
49
|
+
backtestLogicPublicService: Symbol('backtestLogicPublicService'),
|
|
50
|
+
liveLogicPublicService: Symbol('liveLogicPublicService'),
|
|
51
|
+
};
|
|
52
|
+
const TYPES = {
|
|
53
|
+
...baseServices$1,
|
|
54
|
+
...contextServices$1,
|
|
55
|
+
...connectionServices$1,
|
|
56
|
+
...schemaServices$1,
|
|
57
|
+
...globalServices$1,
|
|
58
|
+
...logicPrivateServices$1,
|
|
59
|
+
...logicPublicServices$1,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const ExecutionContextService = diScoped.scoped(class {
|
|
63
|
+
constructor(context) {
|
|
64
|
+
this.context = context;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
8
68
|
const NOOP_LOGGER = {
|
|
9
69
|
log() {
|
|
10
70
|
},
|
|
@@ -15,51 +75,37 @@ const NOOP_LOGGER = {
|
|
|
15
75
|
};
|
|
16
76
|
class LoggerService {
|
|
17
77
|
constructor() {
|
|
78
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
79
|
+
this.executionContextService = inject(TYPES.executionContextService);
|
|
18
80
|
this._commonLogger = NOOP_LOGGER;
|
|
19
81
|
this.log = async (topic, ...args) => {
|
|
20
|
-
await this._commonLogger.log(topic, ...args);
|
|
82
|
+
await this._commonLogger.log(topic, ...args, this.methodContext, this.executionContext);
|
|
21
83
|
};
|
|
22
84
|
this.debug = async (topic, ...args) => {
|
|
23
|
-
await this._commonLogger.debug(topic, ...args);
|
|
85
|
+
await this._commonLogger.debug(topic, ...args, this.methodContext, this.executionContext);
|
|
24
86
|
};
|
|
25
87
|
this.info = async (topic, ...args) => {
|
|
26
|
-
await this._commonLogger.info(topic, ...args);
|
|
88
|
+
await this._commonLogger.info(topic, ...args, this.methodContext, this.executionContext);
|
|
27
89
|
};
|
|
28
90
|
this.setLogger = (logger) => {
|
|
29
91
|
this._commonLogger = logger;
|
|
30
92
|
};
|
|
31
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
|
+
}
|
|
32
106
|
}
|
|
33
107
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const baseServices$1 = {
|
|
37
|
-
loggerService: Symbol('loggerService'),
|
|
38
|
-
};
|
|
39
|
-
const contextServices$1 = {
|
|
40
|
-
executionContextService: Symbol('executionContextService'),
|
|
41
|
-
};
|
|
42
|
-
const connectionServices$1 = {
|
|
43
|
-
candleConnectionService: Symbol('candleConnectionService'),
|
|
44
|
-
strategyConnectionService: Symbol('strategyConnectionService'),
|
|
45
|
-
};
|
|
46
|
-
const schemaServices$1 = {
|
|
47
|
-
candleSchemaService: Symbol('candleSchemaService'),
|
|
48
|
-
strategySchemaService: Symbol('strategySchemaService'),
|
|
49
|
-
};
|
|
50
|
-
const publicServices$1 = {
|
|
51
|
-
candlePublicService: Symbol('candlePublicService'),
|
|
52
|
-
strategyPublicService: Symbol('strategyPublicService'),
|
|
53
|
-
};
|
|
54
|
-
const TYPES = {
|
|
55
|
-
...baseServices$1,
|
|
56
|
-
...contextServices$1,
|
|
57
|
-
...connectionServices$1,
|
|
58
|
-
...schemaServices$1,
|
|
59
|
-
...publicServices$1,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const INTERVAL_MINUTES = {
|
|
108
|
+
const INTERVAL_MINUTES$2 = {
|
|
63
109
|
"1m": 1,
|
|
64
110
|
"3m": 3,
|
|
65
111
|
"5m": 5,
|
|
@@ -71,87 +117,148 @@ const INTERVAL_MINUTES = {
|
|
|
71
117
|
"6h": 360,
|
|
72
118
|
"8h": 480,
|
|
73
119
|
};
|
|
74
|
-
class
|
|
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
|
-
|
|
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);
|
|
118
199
|
}
|
|
119
200
|
}
|
|
120
201
|
|
|
121
|
-
class
|
|
202
|
+
class ExchangeConnectionService {
|
|
122
203
|
constructor() {
|
|
123
204
|
this.loggerService = inject(TYPES.loggerService);
|
|
124
205
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
206
|
+
this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
|
|
207
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
208
|
+
this.getExchange = functoolsKit.memoize((exchangeName) => `${exchangeName}`, (exchangeName) => {
|
|
209
|
+
const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
|
|
210
|
+
return new ClientExchange({
|
|
129
211
|
execution: this.executionContextService,
|
|
130
212
|
logger: this.loggerService,
|
|
213
|
+
exchangeName,
|
|
131
214
|
getCandles,
|
|
215
|
+
formatPrice,
|
|
216
|
+
formatQuantity,
|
|
132
217
|
callbacks,
|
|
133
218
|
});
|
|
134
219
|
});
|
|
135
220
|
this.getCandles = async (symbol, interval, limit) => {
|
|
136
|
-
this.loggerService.log("
|
|
221
|
+
this.loggerService.log("exchangeConnectionService getCandles", {
|
|
137
222
|
symbol,
|
|
138
223
|
interval,
|
|
139
224
|
limit,
|
|
140
225
|
});
|
|
141
|
-
return await this.
|
|
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);
|
|
142
235
|
};
|
|
143
236
|
this.getAveragePrice = async (symbol) => {
|
|
144
|
-
this.loggerService.log("
|
|
237
|
+
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
145
238
|
symbol,
|
|
146
239
|
});
|
|
147
|
-
return await this.
|
|
240
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).getAveragePrice(symbol);
|
|
241
|
+
};
|
|
242
|
+
this.formatPrice = async (symbol, price) => {
|
|
243
|
+
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
244
|
+
symbol,
|
|
245
|
+
price,
|
|
246
|
+
});
|
|
247
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).formatPrice(symbol, price);
|
|
248
|
+
};
|
|
249
|
+
this.formatQuantity = async (symbol, quantity) => {
|
|
250
|
+
this.loggerService.log("exchangeConnectionService getAveragePrice", {
|
|
251
|
+
symbol,
|
|
252
|
+
quantity,
|
|
253
|
+
});
|
|
254
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).formatQuantity(symbol, quantity);
|
|
148
255
|
};
|
|
149
256
|
}
|
|
150
257
|
}
|
|
151
258
|
|
|
152
259
|
const PERCENT_SLIPPAGE = 0.1;
|
|
153
260
|
const PERCENT_FEE = 0.1;
|
|
154
|
-
const
|
|
261
|
+
const toProfitLossDto = (signal, priceClose) => {
|
|
155
262
|
const priceOpen = signal.priceOpen;
|
|
156
263
|
let priceOpenWithSlippage;
|
|
157
264
|
let priceCloseWithSlippage;
|
|
@@ -190,48 +297,554 @@ const GET_PNL_FN = (signal, priceClose) => {
|
|
|
190
297
|
priceClose,
|
|
191
298
|
};
|
|
192
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
|
+
};
|
|
193
721
|
class ClientStrategy {
|
|
194
722
|
constructor(params) {
|
|
195
723
|
this.params = params;
|
|
196
724
|
this._pendingSignal = null;
|
|
197
|
-
this.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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);
|
|
211
746
|
}
|
|
212
747
|
return {
|
|
213
|
-
action: "
|
|
214
|
-
signal:
|
|
748
|
+
action: "opened",
|
|
749
|
+
signal: this._pendingSignal,
|
|
215
750
|
};
|
|
216
751
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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,
|
|
224
803
|
signalId: signal.id,
|
|
225
|
-
|
|
804
|
+
reason: closeReason,
|
|
805
|
+
priceClose: averagePrice,
|
|
806
|
+
closeTimestamp,
|
|
807
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
226
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);
|
|
227
846
|
let shouldClose = false;
|
|
228
847
|
let closeReason;
|
|
229
|
-
// Проверяем истечение времени
|
|
230
|
-
const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
|
|
231
|
-
if (when.getTime() >= signalEndTime) {
|
|
232
|
-
shouldClose = true;
|
|
233
|
-
closeReason = "time_expired";
|
|
234
|
-
}
|
|
235
848
|
// Проверяем достижение TP/SL для long позиции
|
|
236
849
|
if (signal.position === "long") {
|
|
237
850
|
if (averagePrice >= signal.priceTakeProfit) {
|
|
@@ -254,33 +867,55 @@ class ClientStrategy {
|
|
|
254
867
|
closeReason = "stop_loss";
|
|
255
868
|
}
|
|
256
869
|
}
|
|
257
|
-
//
|
|
870
|
+
// Если достигнут TP/SL, закрываем сигнал
|
|
258
871
|
if (shouldClose) {
|
|
259
|
-
const pnl =
|
|
260
|
-
|
|
261
|
-
|
|
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,
|
|
262
876
|
signalId: signal.id,
|
|
263
877
|
reason: closeReason,
|
|
264
878
|
priceClose: averagePrice,
|
|
879
|
+
closeTimestamp,
|
|
265
880
|
pnlPercentage: pnl.pnlPercentage,
|
|
266
881
|
});
|
|
267
882
|
if (this.params.callbacks?.onClose) {
|
|
268
|
-
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);
|
|
269
884
|
}
|
|
270
|
-
this.
|
|
885
|
+
await this.setPendingSignal(null);
|
|
271
886
|
return {
|
|
272
887
|
action: "closed",
|
|
273
888
|
signal: signal,
|
|
274
889
|
currentPrice: averagePrice,
|
|
275
890
|
closeReason: closeReason,
|
|
891
|
+
closeTimestamp: closeTimestamp,
|
|
276
892
|
pnl: pnl,
|
|
277
893
|
};
|
|
278
894
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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,
|
|
284
919
|
};
|
|
285
920
|
}
|
|
286
921
|
}
|
|
@@ -290,39 +925,108 @@ class StrategyConnectionService {
|
|
|
290
925
|
this.loggerService = inject(TYPES.loggerService);
|
|
291
926
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
292
927
|
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
293
|
-
this.
|
|
294
|
-
this.
|
|
295
|
-
|
|
928
|
+
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
929
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
930
|
+
this.getStrategy = functoolsKit.memoize((strategyName) => `${strategyName}`, (strategyName) => {
|
|
931
|
+
const { getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
|
|
296
932
|
return new ClientStrategy({
|
|
297
|
-
|
|
933
|
+
interval,
|
|
298
934
|
execution: this.executionContextService,
|
|
299
935
|
logger: this.loggerService,
|
|
300
|
-
|
|
936
|
+
exchange: this.exchangeConnectionService,
|
|
937
|
+
strategyName,
|
|
301
938
|
getSignal,
|
|
302
939
|
callbacks,
|
|
303
940
|
});
|
|
304
941
|
});
|
|
305
|
-
this.tick = async (
|
|
306
|
-
this.loggerService.log("strategyConnectionService tick"
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
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);
|
|
310
953
|
};
|
|
311
954
|
}
|
|
312
955
|
}
|
|
313
956
|
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
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}`);
|
|
317
980
|
}
|
|
318
|
-
|
|
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
|
+
}
|
|
319
1023
|
|
|
320
|
-
class
|
|
1024
|
+
class ExchangeGlobalService {
|
|
321
1025
|
constructor() {
|
|
322
1026
|
this.loggerService = inject(TYPES.loggerService);
|
|
323
|
-
this.
|
|
1027
|
+
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
324
1028
|
this.getCandles = async (symbol, interval, limit, when, backtest) => {
|
|
325
|
-
this.loggerService.log("
|
|
1029
|
+
this.loggerService.log("exchangeGlobalService getCandles", {
|
|
326
1030
|
symbol,
|
|
327
1031
|
interval,
|
|
328
1032
|
limit,
|
|
@@ -330,21 +1034,69 @@ class CandlePublicService {
|
|
|
330
1034
|
backtest,
|
|
331
1035
|
});
|
|
332
1036
|
return await ExecutionContextService.runInContext(async () => {
|
|
333
|
-
return await this.
|
|
1037
|
+
return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
334
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,
|
|
335
1056
|
when,
|
|
336
1057
|
backtest,
|
|
337
1058
|
});
|
|
338
1059
|
};
|
|
339
1060
|
this.getAveragePrice = async (symbol, when, backtest) => {
|
|
340
|
-
this.loggerService.log("
|
|
1061
|
+
this.loggerService.log("exchangeGlobalService getAveragePrice", {
|
|
1062
|
+
symbol,
|
|
1063
|
+
when,
|
|
1064
|
+
backtest,
|
|
1065
|
+
});
|
|
1066
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
1067
|
+
return await this.exchangeConnectionService.getAveragePrice(symbol);
|
|
1068
|
+
}, {
|
|
1069
|
+
symbol,
|
|
1070
|
+
when,
|
|
1071
|
+
backtest,
|
|
1072
|
+
});
|
|
1073
|
+
};
|
|
1074
|
+
this.formatPrice = async (symbol, price, when, backtest) => {
|
|
1075
|
+
this.loggerService.log("exchangeGlobalService formatPrice", {
|
|
341
1076
|
symbol,
|
|
1077
|
+
price,
|
|
342
1078
|
when,
|
|
343
1079
|
backtest,
|
|
344
1080
|
});
|
|
345
1081
|
return await ExecutionContextService.runInContext(async () => {
|
|
346
|
-
return await this.
|
|
1082
|
+
return await this.exchangeConnectionService.formatPrice(symbol, price);
|
|
347
1083
|
}, {
|
|
1084
|
+
symbol,
|
|
1085
|
+
when,
|
|
1086
|
+
backtest,
|
|
1087
|
+
});
|
|
1088
|
+
};
|
|
1089
|
+
this.formatQuantity = async (symbol, quantity, when, backtest) => {
|
|
1090
|
+
this.loggerService.log("exchangeGlobalService formatQuantity", {
|
|
1091
|
+
symbol,
|
|
1092
|
+
quantity,
|
|
1093
|
+
when,
|
|
1094
|
+
backtest,
|
|
1095
|
+
});
|
|
1096
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
1097
|
+
return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
1098
|
+
}, {
|
|
1099
|
+
symbol,
|
|
348
1100
|
when,
|
|
349
1101
|
backtest,
|
|
350
1102
|
});
|
|
@@ -352,19 +1104,34 @@ class CandlePublicService {
|
|
|
352
1104
|
}
|
|
353
1105
|
}
|
|
354
1106
|
|
|
355
|
-
class
|
|
1107
|
+
class StrategyGlobalService {
|
|
356
1108
|
constructor() {
|
|
357
1109
|
this.loggerService = inject(TYPES.loggerService);
|
|
358
1110
|
this.strategyConnectionService = inject(TYPES.strategyConnectionService);
|
|
359
1111
|
this.tick = async (symbol, when, backtest) => {
|
|
360
|
-
this.loggerService.log("
|
|
1112
|
+
this.loggerService.log("strategyGlobalService tick", {
|
|
361
1113
|
symbol,
|
|
362
1114
|
when,
|
|
363
1115
|
backtest,
|
|
364
1116
|
});
|
|
365
1117
|
return await ExecutionContextService.runInContext(async () => {
|
|
366
|
-
return await this.strategyConnectionService.tick(
|
|
1118
|
+
return await this.strategyConnectionService.tick();
|
|
367
1119
|
}, {
|
|
1120
|
+
symbol,
|
|
1121
|
+
when,
|
|
1122
|
+
backtest,
|
|
1123
|
+
});
|
|
1124
|
+
};
|
|
1125
|
+
this.backtest = async (symbol, candles, when, backtest) => {
|
|
1126
|
+
this.loggerService.log("strategyGlobalService backtest", {
|
|
1127
|
+
symbol,
|
|
1128
|
+
when,
|
|
1129
|
+
backtest,
|
|
1130
|
+
});
|
|
1131
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
1132
|
+
return await this.strategyConnectionService.backtest(candles);
|
|
1133
|
+
}, {
|
|
1134
|
+
symbol,
|
|
368
1135
|
when,
|
|
369
1136
|
backtest,
|
|
370
1137
|
});
|
|
@@ -372,19 +1139,35 @@ class StrategyPublicService {
|
|
|
372
1139
|
}
|
|
373
1140
|
}
|
|
374
1141
|
|
|
375
|
-
class
|
|
1142
|
+
class FrameGlobalService {
|
|
376
1143
|
constructor() {
|
|
377
1144
|
this.loggerService = inject(TYPES.loggerService);
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
return this.
|
|
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);
|
|
384
1151
|
};
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
class ExchangeSchemaService {
|
|
1156
|
+
constructor() {
|
|
1157
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
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);
|
|
1162
|
+
};
|
|
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);
|
|
388
1171
|
};
|
|
389
1172
|
}
|
|
390
1173
|
}
|
|
@@ -392,16 +1175,183 @@ class CandleSchemaService {
|
|
|
392
1175
|
class StrategySchemaService {
|
|
393
1176
|
constructor() {
|
|
394
1177
|
this.loggerService = inject(TYPES.loggerService);
|
|
395
|
-
this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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;
|
|
399
1288
|
}
|
|
400
|
-
|
|
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
|
+
});
|
|
401
1309
|
};
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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);
|
|
405
1355
|
};
|
|
406
1356
|
}
|
|
407
1357
|
}
|
|
@@ -411,18 +1361,32 @@ class StrategySchemaService {
|
|
|
411
1361
|
}
|
|
412
1362
|
{
|
|
413
1363
|
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
1364
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
414
1365
|
}
|
|
415
1366
|
{
|
|
416
|
-
provide(TYPES.
|
|
1367
|
+
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
417
1368
|
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
1369
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
418
1370
|
}
|
|
419
1371
|
{
|
|
420
|
-
provide(TYPES.
|
|
1372
|
+
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
421
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());
|
|
1382
|
+
}
|
|
1383
|
+
{
|
|
1384
|
+
provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
|
|
1385
|
+
provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
|
|
422
1386
|
}
|
|
423
1387
|
{
|
|
424
|
-
provide(TYPES.
|
|
425
|
-
provide(TYPES.
|
|
1388
|
+
provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
|
|
1389
|
+
provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
|
|
426
1390
|
}
|
|
427
1391
|
|
|
428
1392
|
const baseServices = {
|
|
@@ -430,39 +1394,75 @@ const baseServices = {
|
|
|
430
1394
|
};
|
|
431
1395
|
const contextServices = {
|
|
432
1396
|
executionContextService: inject(TYPES.executionContextService),
|
|
1397
|
+
methodContextService: inject(TYPES.methodContextService),
|
|
433
1398
|
};
|
|
434
1399
|
const connectionServices = {
|
|
435
|
-
|
|
1400
|
+
exchangeConnectionService: inject(TYPES.exchangeConnectionService),
|
|
436
1401
|
strategyConnectionService: inject(TYPES.strategyConnectionService),
|
|
1402
|
+
frameConnectionService: inject(TYPES.frameConnectionService),
|
|
437
1403
|
};
|
|
438
1404
|
const schemaServices = {
|
|
439
|
-
|
|
1405
|
+
exchangeSchemaService: inject(TYPES.exchangeSchemaService),
|
|
440
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),
|
|
441
1415
|
};
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
1416
|
+
const logicPrivateServices = {
|
|
1417
|
+
backtestLogicPrivateService: inject(TYPES.backtestLogicPrivateService),
|
|
1418
|
+
liveLogicPrivateService: inject(TYPES.liveLogicPrivateService),
|
|
1419
|
+
};
|
|
1420
|
+
const logicPublicServices = {
|
|
1421
|
+
backtestLogicPublicService: inject(TYPES.backtestLogicPublicService),
|
|
1422
|
+
liveLogicPublicService: inject(TYPES.liveLogicPublicService),
|
|
445
1423
|
};
|
|
446
1424
|
const backtest = {
|
|
447
1425
|
...baseServices,
|
|
448
1426
|
...contextServices,
|
|
449
1427
|
...connectionServices,
|
|
450
1428
|
...schemaServices,
|
|
451
|
-
...
|
|
1429
|
+
...globalServices,
|
|
1430
|
+
...logicPrivateServices,
|
|
1431
|
+
...logicPublicServices,
|
|
452
1432
|
};
|
|
453
1433
|
init();
|
|
1434
|
+
var backtest$1 = backtest;
|
|
454
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";
|
|
455
1443
|
function addStrategy(strategySchema) {
|
|
456
|
-
backtest.
|
|
1444
|
+
backtest$1.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
|
|
1445
|
+
strategySchema,
|
|
1446
|
+
});
|
|
1447
|
+
backtest$1.strategySchemaService.register(strategySchema.strategyName, strategySchema);
|
|
457
1448
|
}
|
|
458
|
-
function
|
|
459
|
-
backtest.
|
|
1449
|
+
function addExchange(exchangeSchema) {
|
|
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);
|
|
460
1460
|
}
|
|
461
1461
|
|
|
462
1462
|
async function runBacktest(symbol, timeframes) {
|
|
463
1463
|
const results = [];
|
|
464
1464
|
for (const when of timeframes) {
|
|
465
|
-
const result = await backtest.
|
|
1465
|
+
const result = await backtest$1.strategyGlobalService.tick(symbol, when, true);
|
|
466
1466
|
// Сохраняем только результаты closed
|
|
467
1467
|
if (result.action === "closed") {
|
|
468
1468
|
results.push(result);
|
|
@@ -542,7 +1542,7 @@ function startRun(config) {
|
|
|
542
1542
|
}
|
|
543
1543
|
const doWork = functoolsKit.singlerun(async () => {
|
|
544
1544
|
const now = new Date();
|
|
545
|
-
const result = await backtest.
|
|
1545
|
+
const result = await backtest$1.strategyGlobalService.tick(symbol, now, false);
|
|
546
1546
|
const instance = instances.get(symbol);
|
|
547
1547
|
if (instance) {
|
|
548
1548
|
instance.tickCount++;
|
|
@@ -572,22 +1572,99 @@ function stopAll() {
|
|
|
572
1572
|
instances.clear();
|
|
573
1573
|
}
|
|
574
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";
|
|
575
1581
|
async function getCandles(symbol, interval, limit) {
|
|
576
|
-
|
|
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);
|
|
577
1588
|
}
|
|
578
1589
|
async function getAveragePrice(symbol) {
|
|
579
|
-
|
|
1590
|
+
backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
|
|
1591
|
+
symbol,
|
|
1592
|
+
});
|
|
1593
|
+
return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
|
|
1594
|
+
}
|
|
1595
|
+
async function formatPrice(symbol, price) {
|
|
1596
|
+
backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
|
|
1597
|
+
symbol,
|
|
1598
|
+
price,
|
|
1599
|
+
});
|
|
1600
|
+
return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
|
|
1601
|
+
}
|
|
1602
|
+
async function formatQuantity(symbol, quantity) {
|
|
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
|
+
}
|
|
580
1645
|
}
|
|
1646
|
+
const Live = new LiveUtils();
|
|
581
1647
|
|
|
1648
|
+
exports.Backtest = Backtest;
|
|
582
1649
|
exports.ExecutionContextService = ExecutionContextService;
|
|
583
|
-
exports.
|
|
1650
|
+
exports.Live = Live;
|
|
1651
|
+
exports.MethodContextService = MethodContextService;
|
|
1652
|
+
exports.PersistBase = PersistBase;
|
|
1653
|
+
exports.PersistSignalAdaper = PersistSignalAdaper;
|
|
1654
|
+
exports.addExchange = addExchange;
|
|
1655
|
+
exports.addFrame = addFrame;
|
|
584
1656
|
exports.addStrategy = addStrategy;
|
|
585
1657
|
exports.backtest = backtest;
|
|
1658
|
+
exports.formatPrice = formatPrice;
|
|
1659
|
+
exports.formatQuantity = formatQuantity;
|
|
586
1660
|
exports.getAveragePrice = getAveragePrice;
|
|
587
1661
|
exports.getCandles = getCandles;
|
|
1662
|
+
exports.getDate = getDate;
|
|
1663
|
+
exports.getMode = getMode;
|
|
588
1664
|
exports.reduce = reduce;
|
|
589
1665
|
exports.runBacktest = runBacktest;
|
|
590
1666
|
exports.runBacktestGUI = runBacktestGUI;
|
|
1667
|
+
exports.setLogger = setLogger;
|
|
591
1668
|
exports.startRun = startRun;
|
|
592
1669
|
exports.stopAll = stopAll;
|
|
593
1670
|
exports.stopRun = stopRun;
|