backtest-kit 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/build/index.cjs +155 -18
- package/build/index.mjs +156 -19
- package/package.json +1 -1
- package/types.d.ts +46 -6
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Build sophisticated trading systems with confidence. Backtest Kit empowers you t
|
|
|
12
12
|
|
|
13
13
|
## ✨ Why Choose Backtest Kit?
|
|
14
14
|
|
|
15
|
-
- 🚀 **Production-Ready Architecture**: Seamlessly switch between backtest and live modes with robust error recovery and graceful shutdown mechanisms. Your strategy code remains identical across environments.
|
|
15
|
+
- 🚀 **Production-Ready Architecture**: Seamlessly switch between backtest and live modes with robust error recovery and graceful shutdown mechanisms. Your strategy code remains identical across environments.
|
|
16
16
|
|
|
17
17
|
- 💾 **Crash-Safe Persistence**: Atomic file writes with automatic state recovery ensure no duplicate signals or lost data—even after crashes. Resume execution exactly where you left off. 🔄
|
|
18
18
|
|
|
@@ -46,7 +46,7 @@ Build sophisticated trading systems with confidence. Backtest Kit empowers you t
|
|
|
46
46
|
|
|
47
47
|
- 🔒 **Safe Math & Robustness**: All metrics protected against NaN/Infinity with unsafe numeric checks. Returns N/A for invalid calculations. ✨
|
|
48
48
|
|
|
49
|
-
- 🧪 **Comprehensive Test Coverage**: 123 unit and integration tests covering validation, PNL, callbacks, reports, performance tracking, walker, heatmap, position sizing, risk management, scheduled signals, and event system.
|
|
49
|
+
- 🧪 **Comprehensive Test Coverage**: 123 unit and integration tests covering validation, PNL, callbacks, reports, performance tracking, walker, heatmap, position sizing, risk management, scheduled signals, and event system.
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
@@ -227,6 +227,7 @@ Backtest.background("BTCUSDT", {
|
|
|
227
227
|
- 🛡️ **Signal Lifecycle**: Type-safe state machine prevents invalid state transitions. 🚑
|
|
228
228
|
- 📦 **Dependency Inversion**: Lazy-load components at runtime for modular, scalable designs. 🧩
|
|
229
229
|
- 🔍 **Schema Reflection**: Runtime introspection with `listExchanges()`, `listStrategies()`, `listFrames()`. 📊
|
|
230
|
+
- 🔬 **Data Validation**: Automatic detection and rejection of incomplete candles from Binance API with anomaly checks.
|
|
230
231
|
|
|
231
232
|
---
|
|
232
233
|
|
|
@@ -261,7 +262,7 @@ Check out the sections below for detailed examples! 📚
|
|
|
261
262
|
|
|
262
263
|
### 1. Register Exchange Data Source
|
|
263
264
|
|
|
264
|
-
You can plug any data source
|
|
265
|
+
You can plug any data source: CCXT for live data or a database for faster backtesting:
|
|
265
266
|
|
|
266
267
|
```typescript
|
|
267
268
|
import { addExchange } from "backtest-kit";
|
package/build/index.cjs
CHANGED
|
@@ -37,6 +37,44 @@ const GLOBAL_CONFIG = {
|
|
|
37
37
|
* Default: 1440 minutes (1 day)
|
|
38
38
|
*/
|
|
39
39
|
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
40
|
+
/**
|
|
41
|
+
* Number of retries for getCandles function
|
|
42
|
+
* Default: 3 retries
|
|
43
|
+
*/
|
|
44
|
+
CC_GET_CANDLES_RETRY_COUNT: 3,
|
|
45
|
+
/**
|
|
46
|
+
* Delay between retries for getCandles function (in milliseconds)
|
|
47
|
+
* Default: 5000 ms (5 seconds)
|
|
48
|
+
*/
|
|
49
|
+
CC_GET_CANDLES_RETRY_DELAY_MS: 5000,
|
|
50
|
+
/**
|
|
51
|
+
* Maximum allowed deviation factor for price anomaly detection.
|
|
52
|
+
* Price should not be more than this factor lower than reference price.
|
|
53
|
+
*
|
|
54
|
+
* Reasoning:
|
|
55
|
+
* - Incomplete candles from Binance API typically have prices near 0 (e.g., $0.01-1)
|
|
56
|
+
* - Normal BTC price ranges: $20,000-100,000
|
|
57
|
+
* - Factor 1000 catches prices below $20-100 when median is $20,000-100,000
|
|
58
|
+
* - Factor 100 would be too permissive (allows $200 when median is $20,000)
|
|
59
|
+
* - Factor 10000 might be too strict for low-cap altcoins
|
|
60
|
+
*
|
|
61
|
+
* Example: BTC at $50,000 median → threshold $50 (catches $0.01-1 anomalies)
|
|
62
|
+
*/
|
|
63
|
+
CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR: 1000,
|
|
64
|
+
/**
|
|
65
|
+
* Minimum number of candles required for reliable median calculation.
|
|
66
|
+
* Below this threshold, use simple average instead of median.
|
|
67
|
+
*
|
|
68
|
+
* Reasoning:
|
|
69
|
+
* - Each candle provides 4 price points (OHLC)
|
|
70
|
+
* - 5 candles = 20 price points, sufficient for robust median calculation
|
|
71
|
+
* - Below 5 candles, single anomaly can heavily skew median
|
|
72
|
+
* - Statistical rule of thumb: minimum 7-10 data points for median stability
|
|
73
|
+
* - Average is more stable than median for small datasets (n < 20)
|
|
74
|
+
*
|
|
75
|
+
* Example: 3 candles = 12 points (use average), 5 candles = 20 points (use median)
|
|
76
|
+
*/
|
|
77
|
+
CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: 5,
|
|
40
78
|
};
|
|
41
79
|
|
|
42
80
|
const { init, inject, provide } = diKit.createActivator("backtest");
|
|
@@ -271,6 +309,92 @@ const INTERVAL_MINUTES$2 = {
|
|
|
271
309
|
"6h": 360,
|
|
272
310
|
"8h": 480,
|
|
273
311
|
};
|
|
312
|
+
/**
|
|
313
|
+
* Validates that all candles have valid OHLCV data without anomalies.
|
|
314
|
+
* Detects incomplete candles from Binance API by checking for abnormally low prices or volumes.
|
|
315
|
+
* Incomplete candles often have prices like 0.1 instead of normal 100,000 or zero volume.
|
|
316
|
+
*
|
|
317
|
+
* @param candles - Array of candle data to validate
|
|
318
|
+
* @throws Error if any candles have anomalous OHLCV values
|
|
319
|
+
*/
|
|
320
|
+
const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
321
|
+
if (candles.length === 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Calculate reference price (median or average depending on candle count)
|
|
325
|
+
const allPrices = candles.flatMap((c) => [c.open, c.high, c.low, c.close]);
|
|
326
|
+
const validPrices = allPrices.filter(p => p > 0);
|
|
327
|
+
let referencePrice;
|
|
328
|
+
if (candles.length >= GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN) {
|
|
329
|
+
// Use median for reliable statistics with enough data
|
|
330
|
+
const sortedPrices = [...validPrices].sort((a, b) => a - b);
|
|
331
|
+
referencePrice = sortedPrices[Math.floor(sortedPrices.length / 2)] || 0;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Use average for small datasets (more stable than median)
|
|
335
|
+
const sum = validPrices.reduce((acc, p) => acc + p, 0);
|
|
336
|
+
referencePrice = validPrices.length > 0 ? sum / validPrices.length : 0;
|
|
337
|
+
}
|
|
338
|
+
if (referencePrice === 0) {
|
|
339
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: cannot calculate reference price (all prices are zero)`);
|
|
340
|
+
}
|
|
341
|
+
const minValidPrice = referencePrice / GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR;
|
|
342
|
+
for (let i = 0; i < candles.length; i++) {
|
|
343
|
+
const candle = candles[i];
|
|
344
|
+
// Check for invalid numeric values
|
|
345
|
+
if (!Number.isFinite(candle.open) ||
|
|
346
|
+
!Number.isFinite(candle.high) ||
|
|
347
|
+
!Number.isFinite(candle.low) ||
|
|
348
|
+
!Number.isFinite(candle.close) ||
|
|
349
|
+
!Number.isFinite(candle.volume) ||
|
|
350
|
+
!Number.isFinite(candle.timestamp)) {
|
|
351
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has invalid numeric values (NaN or Infinity)`);
|
|
352
|
+
}
|
|
353
|
+
// Check for negative values
|
|
354
|
+
if (candle.open <= 0 ||
|
|
355
|
+
candle.high <= 0 ||
|
|
356
|
+
candle.low <= 0 ||
|
|
357
|
+
candle.close <= 0 ||
|
|
358
|
+
candle.volume < 0) {
|
|
359
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has zero or negative values`);
|
|
360
|
+
}
|
|
361
|
+
// Check for anomalously low prices (incomplete candle indicator)
|
|
362
|
+
if (candle.open < minValidPrice ||
|
|
363
|
+
candle.high < minValidPrice ||
|
|
364
|
+
candle.low < minValidPrice ||
|
|
365
|
+
candle.close < minValidPrice) {
|
|
366
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has anomalously low price. ` +
|
|
367
|
+
`OHLC: [${candle.open}, ${candle.high}, ${candle.low}, ${candle.close}], ` +
|
|
368
|
+
`reference: ${referencePrice}, threshold: ${minValidPrice}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Retries the getCandles function with specified retry count and delay.
|
|
374
|
+
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
375
|
+
* @param since - Date object representing the start time for fetching candles
|
|
376
|
+
* @param self - Instance of ClientExchange
|
|
377
|
+
* @returns Promise resolving to array of candle data
|
|
378
|
+
*/
|
|
379
|
+
const GET_CANDLES_FN = async (dto, since, self) => {
|
|
380
|
+
let lastError;
|
|
381
|
+
for (let i = 0; i !== GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT; i++) {
|
|
382
|
+
try {
|
|
383
|
+
const result = await self.params.getCandles(dto.symbol, dto.interval, since, dto.limit);
|
|
384
|
+
VALIDATE_NO_INCOMPLETE_CANDLES_FN(result);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
self.params.logger.warn(`ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`, {
|
|
389
|
+
error: functoolsKit.errorData(err),
|
|
390
|
+
message: functoolsKit.getErrorMessage(err),
|
|
391
|
+
});
|
|
392
|
+
lastError = err;
|
|
393
|
+
await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
throw lastError;
|
|
397
|
+
};
|
|
274
398
|
/**
|
|
275
399
|
* Client implementation for exchange data access.
|
|
276
400
|
*
|
|
@@ -321,7 +445,7 @@ class ClientExchange {
|
|
|
321
445
|
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
322
446
|
}
|
|
323
447
|
const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
|
|
324
|
-
const data = await
|
|
448
|
+
const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
325
449
|
// Filter candles to strictly match the requested range
|
|
326
450
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
327
451
|
const sinceTimestamp = since.getTime();
|
|
@@ -359,7 +483,7 @@ class ClientExchange {
|
|
|
359
483
|
if (endTime > now) {
|
|
360
484
|
return [];
|
|
361
485
|
}
|
|
362
|
-
const data = await
|
|
486
|
+
const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
363
487
|
// Filter candles to strictly match the requested range
|
|
364
488
|
const sinceTimestamp = since.getTime();
|
|
365
489
|
const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
|
|
@@ -1126,7 +1250,7 @@ class PersistSignalUtils {
|
|
|
1126
1250
|
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1127
1251
|
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1128
1252
|
* }
|
|
1129
|
-
*
|
|
1253
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
1130
1254
|
* ```
|
|
1131
1255
|
*/
|
|
1132
1256
|
usePersistSignalAdapter(Ctor) {
|
|
@@ -1141,16 +1265,16 @@ class PersistSignalUtils {
|
|
|
1141
1265
|
* @example
|
|
1142
1266
|
* ```typescript
|
|
1143
1267
|
* // Custom adapter
|
|
1144
|
-
*
|
|
1268
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
1145
1269
|
*
|
|
1146
1270
|
* // Read signal
|
|
1147
|
-
* const signal = await
|
|
1271
|
+
* const signal = await PersistSignalAdapter.readSignalData("my-strategy", "BTCUSDT");
|
|
1148
1272
|
*
|
|
1149
1273
|
* // Write signal
|
|
1150
|
-
* await
|
|
1274
|
+
* await PersistSignalAdapter.writeSignalData(signal, "my-strategy", "BTCUSDT");
|
|
1151
1275
|
* ```
|
|
1152
1276
|
*/
|
|
1153
|
-
const
|
|
1277
|
+
const PersistSignalAdapter = new PersistSignalUtils();
|
|
1154
1278
|
const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
|
|
1155
1279
|
const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
|
|
1156
1280
|
const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
|
|
@@ -1523,7 +1647,7 @@ const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
|
1523
1647
|
if (self.params.execution.context.backtest) {
|
|
1524
1648
|
return;
|
|
1525
1649
|
}
|
|
1526
|
-
const pendingSignal = await
|
|
1650
|
+
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1527
1651
|
if (!pendingSignal) {
|
|
1528
1652
|
return;
|
|
1529
1653
|
}
|
|
@@ -1985,25 +2109,33 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
1985
2109
|
let shouldActivate = false;
|
|
1986
2110
|
let shouldCancel = false;
|
|
1987
2111
|
if (scheduled.position === "long") {
|
|
1988
|
-
//
|
|
1989
|
-
//
|
|
2112
|
+
// КРИТИЧНО для LONG:
|
|
2113
|
+
// - priceOpen > priceStopLoss (по валидации)
|
|
2114
|
+
// - Активация: low <= priceOpen (цена упала до входа)
|
|
2115
|
+
// - Отмена: low <= priceStopLoss (цена пробила SL)
|
|
2116
|
+
//
|
|
2117
|
+
// EDGE CASE: если low <= priceStopLoss И low <= priceOpen на ОДНОЙ свече:
|
|
2118
|
+
// => Отмена имеет ПРИОРИТЕТ! (SL пробит ДО или ВМЕСТЕ с активацией)
|
|
2119
|
+
// Сигнал НЕ открывается, сразу отменяется
|
|
1990
2120
|
if (candle.low <= scheduled.priceStopLoss) {
|
|
1991
2121
|
shouldCancel = true;
|
|
1992
2122
|
}
|
|
1993
|
-
// Long = покупаем дешевле, ждем падения цены ДО priceOpen
|
|
1994
|
-
// Активируем только если НЕ пробит StopLoss
|
|
1995
2123
|
else if (candle.low <= scheduled.priceOpen) {
|
|
1996
2124
|
shouldActivate = true;
|
|
1997
2125
|
}
|
|
1998
2126
|
}
|
|
1999
2127
|
if (scheduled.position === "short") {
|
|
2000
|
-
//
|
|
2001
|
-
//
|
|
2128
|
+
// КРИТИЧНО для SHORT:
|
|
2129
|
+
// - priceOpen < priceStopLoss (по валидации)
|
|
2130
|
+
// - Активация: high >= priceOpen (цена выросла до входа)
|
|
2131
|
+
// - Отмена: high >= priceStopLoss (цена пробила SL)
|
|
2132
|
+
//
|
|
2133
|
+
// EDGE CASE: если high >= priceStopLoss И high >= priceOpen на ОДНОЙ свече:
|
|
2134
|
+
// => Отмена имеет ПРИОРИТЕТ! (SL пробит ДО или ВМЕСТЕ с активацией)
|
|
2135
|
+
// Сигнал НЕ открывается, сразу отменяется
|
|
2002
2136
|
if (candle.high >= scheduled.priceStopLoss) {
|
|
2003
2137
|
shouldCancel = true;
|
|
2004
2138
|
}
|
|
2005
|
-
// Short = продаем дороже, ждем роста цены ДО priceOpen
|
|
2006
|
-
// Активируем только если НЕ пробит StopLoss
|
|
2007
2139
|
else if (candle.high >= scheduled.priceOpen) {
|
|
2008
2140
|
shouldActivate = true;
|
|
2009
2141
|
}
|
|
@@ -2143,10 +2275,15 @@ class ClientStrategy {
|
|
|
2143
2275
|
pendingSignal,
|
|
2144
2276
|
});
|
|
2145
2277
|
this._pendingSignal = pendingSignal;
|
|
2278
|
+
// КРИТИЧНО: Всегда вызываем коллбек onWrite для тестирования persist storage
|
|
2279
|
+
// даже в backtest режиме, чтобы тесты могли перехватывать вызовы через mock adapter
|
|
2280
|
+
if (this.params.callbacks?.onWrite) {
|
|
2281
|
+
this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, this.params.execution.context.backtest);
|
|
2282
|
+
}
|
|
2146
2283
|
if (this.params.execution.context.backtest) {
|
|
2147
2284
|
return;
|
|
2148
2285
|
}
|
|
2149
|
-
await
|
|
2286
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
2150
2287
|
}
|
|
2151
2288
|
/**
|
|
2152
2289
|
* Performs a single tick of strategy execution.
|
|
@@ -10100,7 +10237,7 @@ exports.MethodContextService = MethodContextService;
|
|
|
10100
10237
|
exports.Performance = Performance;
|
|
10101
10238
|
exports.PersistBase = PersistBase;
|
|
10102
10239
|
exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
10103
|
-
exports.
|
|
10240
|
+
exports.PersistSignalAdapter = PersistSignalAdapter;
|
|
10104
10241
|
exports.PositionSize = PositionSize;
|
|
10105
10242
|
exports.Schedule = Schedule;
|
|
10106
10243
|
exports.Walker = Walker;
|
package/build/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createActivator } from 'di-kit';
|
|
2
2
|
import { scoped } from 'di-scoped';
|
|
3
|
-
import { memoize, makeExtendable, singleshot,
|
|
3
|
+
import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, resolveDocuments, str, queued } from 'functools-kit';
|
|
4
4
|
import fs, { mkdir, writeFile } from 'fs/promises';
|
|
5
5
|
import path, { join } from 'path';
|
|
6
6
|
import crypto from 'crypto';
|
|
@@ -35,6 +35,44 @@ const GLOBAL_CONFIG = {
|
|
|
35
35
|
* Default: 1440 minutes (1 day)
|
|
36
36
|
*/
|
|
37
37
|
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
38
|
+
/**
|
|
39
|
+
* Number of retries for getCandles function
|
|
40
|
+
* Default: 3 retries
|
|
41
|
+
*/
|
|
42
|
+
CC_GET_CANDLES_RETRY_COUNT: 3,
|
|
43
|
+
/**
|
|
44
|
+
* Delay between retries for getCandles function (in milliseconds)
|
|
45
|
+
* Default: 5000 ms (5 seconds)
|
|
46
|
+
*/
|
|
47
|
+
CC_GET_CANDLES_RETRY_DELAY_MS: 5000,
|
|
48
|
+
/**
|
|
49
|
+
* Maximum allowed deviation factor for price anomaly detection.
|
|
50
|
+
* Price should not be more than this factor lower than reference price.
|
|
51
|
+
*
|
|
52
|
+
* Reasoning:
|
|
53
|
+
* - Incomplete candles from Binance API typically have prices near 0 (e.g., $0.01-1)
|
|
54
|
+
* - Normal BTC price ranges: $20,000-100,000
|
|
55
|
+
* - Factor 1000 catches prices below $20-100 when median is $20,000-100,000
|
|
56
|
+
* - Factor 100 would be too permissive (allows $200 when median is $20,000)
|
|
57
|
+
* - Factor 10000 might be too strict for low-cap altcoins
|
|
58
|
+
*
|
|
59
|
+
* Example: BTC at $50,000 median → threshold $50 (catches $0.01-1 anomalies)
|
|
60
|
+
*/
|
|
61
|
+
CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR: 1000,
|
|
62
|
+
/**
|
|
63
|
+
* Minimum number of candles required for reliable median calculation.
|
|
64
|
+
* Below this threshold, use simple average instead of median.
|
|
65
|
+
*
|
|
66
|
+
* Reasoning:
|
|
67
|
+
* - Each candle provides 4 price points (OHLC)
|
|
68
|
+
* - 5 candles = 20 price points, sufficient for robust median calculation
|
|
69
|
+
* - Below 5 candles, single anomaly can heavily skew median
|
|
70
|
+
* - Statistical rule of thumb: minimum 7-10 data points for median stability
|
|
71
|
+
* - Average is more stable than median for small datasets (n < 20)
|
|
72
|
+
*
|
|
73
|
+
* Example: 3 candles = 12 points (use average), 5 candles = 20 points (use median)
|
|
74
|
+
*/
|
|
75
|
+
CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: 5,
|
|
38
76
|
};
|
|
39
77
|
|
|
40
78
|
const { init, inject, provide } = createActivator("backtest");
|
|
@@ -269,6 +307,92 @@ const INTERVAL_MINUTES$2 = {
|
|
|
269
307
|
"6h": 360,
|
|
270
308
|
"8h": 480,
|
|
271
309
|
};
|
|
310
|
+
/**
|
|
311
|
+
* Validates that all candles have valid OHLCV data without anomalies.
|
|
312
|
+
* Detects incomplete candles from Binance API by checking for abnormally low prices or volumes.
|
|
313
|
+
* Incomplete candles often have prices like 0.1 instead of normal 100,000 or zero volume.
|
|
314
|
+
*
|
|
315
|
+
* @param candles - Array of candle data to validate
|
|
316
|
+
* @throws Error if any candles have anomalous OHLCV values
|
|
317
|
+
*/
|
|
318
|
+
const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
319
|
+
if (candles.length === 0) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Calculate reference price (median or average depending on candle count)
|
|
323
|
+
const allPrices = candles.flatMap((c) => [c.open, c.high, c.low, c.close]);
|
|
324
|
+
const validPrices = allPrices.filter(p => p > 0);
|
|
325
|
+
let referencePrice;
|
|
326
|
+
if (candles.length >= GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN) {
|
|
327
|
+
// Use median for reliable statistics with enough data
|
|
328
|
+
const sortedPrices = [...validPrices].sort((a, b) => a - b);
|
|
329
|
+
referencePrice = sortedPrices[Math.floor(sortedPrices.length / 2)] || 0;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Use average for small datasets (more stable than median)
|
|
333
|
+
const sum = validPrices.reduce((acc, p) => acc + p, 0);
|
|
334
|
+
referencePrice = validPrices.length > 0 ? sum / validPrices.length : 0;
|
|
335
|
+
}
|
|
336
|
+
if (referencePrice === 0) {
|
|
337
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: cannot calculate reference price (all prices are zero)`);
|
|
338
|
+
}
|
|
339
|
+
const minValidPrice = referencePrice / GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR;
|
|
340
|
+
for (let i = 0; i < candles.length; i++) {
|
|
341
|
+
const candle = candles[i];
|
|
342
|
+
// Check for invalid numeric values
|
|
343
|
+
if (!Number.isFinite(candle.open) ||
|
|
344
|
+
!Number.isFinite(candle.high) ||
|
|
345
|
+
!Number.isFinite(candle.low) ||
|
|
346
|
+
!Number.isFinite(candle.close) ||
|
|
347
|
+
!Number.isFinite(candle.volume) ||
|
|
348
|
+
!Number.isFinite(candle.timestamp)) {
|
|
349
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has invalid numeric values (NaN or Infinity)`);
|
|
350
|
+
}
|
|
351
|
+
// Check for negative values
|
|
352
|
+
if (candle.open <= 0 ||
|
|
353
|
+
candle.high <= 0 ||
|
|
354
|
+
candle.low <= 0 ||
|
|
355
|
+
candle.close <= 0 ||
|
|
356
|
+
candle.volume < 0) {
|
|
357
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has zero or negative values`);
|
|
358
|
+
}
|
|
359
|
+
// Check for anomalously low prices (incomplete candle indicator)
|
|
360
|
+
if (candle.open < minValidPrice ||
|
|
361
|
+
candle.high < minValidPrice ||
|
|
362
|
+
candle.low < minValidPrice ||
|
|
363
|
+
candle.close < minValidPrice) {
|
|
364
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has anomalously low price. ` +
|
|
365
|
+
`OHLC: [${candle.open}, ${candle.high}, ${candle.low}, ${candle.close}], ` +
|
|
366
|
+
`reference: ${referencePrice}, threshold: ${minValidPrice}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Retries the getCandles function with specified retry count and delay.
|
|
372
|
+
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
373
|
+
* @param since - Date object representing the start time for fetching candles
|
|
374
|
+
* @param self - Instance of ClientExchange
|
|
375
|
+
* @returns Promise resolving to array of candle data
|
|
376
|
+
*/
|
|
377
|
+
const GET_CANDLES_FN = async (dto, since, self) => {
|
|
378
|
+
let lastError;
|
|
379
|
+
for (let i = 0; i !== GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT; i++) {
|
|
380
|
+
try {
|
|
381
|
+
const result = await self.params.getCandles(dto.symbol, dto.interval, since, dto.limit);
|
|
382
|
+
VALIDATE_NO_INCOMPLETE_CANDLES_FN(result);
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
self.params.logger.warn(`ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`, {
|
|
387
|
+
error: errorData(err),
|
|
388
|
+
message: getErrorMessage(err),
|
|
389
|
+
});
|
|
390
|
+
lastError = err;
|
|
391
|
+
await sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
throw lastError;
|
|
395
|
+
};
|
|
272
396
|
/**
|
|
273
397
|
* Client implementation for exchange data access.
|
|
274
398
|
*
|
|
@@ -319,7 +443,7 @@ class ClientExchange {
|
|
|
319
443
|
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
320
444
|
}
|
|
321
445
|
const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
|
|
322
|
-
const data = await
|
|
446
|
+
const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
323
447
|
// Filter candles to strictly match the requested range
|
|
324
448
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
325
449
|
const sinceTimestamp = since.getTime();
|
|
@@ -357,7 +481,7 @@ class ClientExchange {
|
|
|
357
481
|
if (endTime > now) {
|
|
358
482
|
return [];
|
|
359
483
|
}
|
|
360
|
-
const data = await
|
|
484
|
+
const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
361
485
|
// Filter candles to strictly match the requested range
|
|
362
486
|
const sinceTimestamp = since.getTime();
|
|
363
487
|
const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
|
|
@@ -1124,7 +1248,7 @@ class PersistSignalUtils {
|
|
|
1124
1248
|
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1125
1249
|
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1126
1250
|
* }
|
|
1127
|
-
*
|
|
1251
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
1128
1252
|
* ```
|
|
1129
1253
|
*/
|
|
1130
1254
|
usePersistSignalAdapter(Ctor) {
|
|
@@ -1139,16 +1263,16 @@ class PersistSignalUtils {
|
|
|
1139
1263
|
* @example
|
|
1140
1264
|
* ```typescript
|
|
1141
1265
|
* // Custom adapter
|
|
1142
|
-
*
|
|
1266
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
1143
1267
|
*
|
|
1144
1268
|
* // Read signal
|
|
1145
|
-
* const signal = await
|
|
1269
|
+
* const signal = await PersistSignalAdapter.readSignalData("my-strategy", "BTCUSDT");
|
|
1146
1270
|
*
|
|
1147
1271
|
* // Write signal
|
|
1148
|
-
* await
|
|
1272
|
+
* await PersistSignalAdapter.writeSignalData(signal, "my-strategy", "BTCUSDT");
|
|
1149
1273
|
* ```
|
|
1150
1274
|
*/
|
|
1151
|
-
const
|
|
1275
|
+
const PersistSignalAdapter = new PersistSignalUtils();
|
|
1152
1276
|
const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
|
|
1153
1277
|
const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
|
|
1154
1278
|
const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
|
|
@@ -1521,7 +1645,7 @@ const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
|
1521
1645
|
if (self.params.execution.context.backtest) {
|
|
1522
1646
|
return;
|
|
1523
1647
|
}
|
|
1524
|
-
const pendingSignal = await
|
|
1648
|
+
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1525
1649
|
if (!pendingSignal) {
|
|
1526
1650
|
return;
|
|
1527
1651
|
}
|
|
@@ -1983,25 +2107,33 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
1983
2107
|
let shouldActivate = false;
|
|
1984
2108
|
let shouldCancel = false;
|
|
1985
2109
|
if (scheduled.position === "long") {
|
|
1986
|
-
//
|
|
1987
|
-
//
|
|
2110
|
+
// КРИТИЧНО для LONG:
|
|
2111
|
+
// - priceOpen > priceStopLoss (по валидации)
|
|
2112
|
+
// - Активация: low <= priceOpen (цена упала до входа)
|
|
2113
|
+
// - Отмена: low <= priceStopLoss (цена пробила SL)
|
|
2114
|
+
//
|
|
2115
|
+
// EDGE CASE: если low <= priceStopLoss И low <= priceOpen на ОДНОЙ свече:
|
|
2116
|
+
// => Отмена имеет ПРИОРИТЕТ! (SL пробит ДО или ВМЕСТЕ с активацией)
|
|
2117
|
+
// Сигнал НЕ открывается, сразу отменяется
|
|
1988
2118
|
if (candle.low <= scheduled.priceStopLoss) {
|
|
1989
2119
|
shouldCancel = true;
|
|
1990
2120
|
}
|
|
1991
|
-
// Long = покупаем дешевле, ждем падения цены ДО priceOpen
|
|
1992
|
-
// Активируем только если НЕ пробит StopLoss
|
|
1993
2121
|
else if (candle.low <= scheduled.priceOpen) {
|
|
1994
2122
|
shouldActivate = true;
|
|
1995
2123
|
}
|
|
1996
2124
|
}
|
|
1997
2125
|
if (scheduled.position === "short") {
|
|
1998
|
-
//
|
|
1999
|
-
//
|
|
2126
|
+
// КРИТИЧНО для SHORT:
|
|
2127
|
+
// - priceOpen < priceStopLoss (по валидации)
|
|
2128
|
+
// - Активация: high >= priceOpen (цена выросла до входа)
|
|
2129
|
+
// - Отмена: high >= priceStopLoss (цена пробила SL)
|
|
2130
|
+
//
|
|
2131
|
+
// EDGE CASE: если high >= priceStopLoss И high >= priceOpen на ОДНОЙ свече:
|
|
2132
|
+
// => Отмена имеет ПРИОРИТЕТ! (SL пробит ДО или ВМЕСТЕ с активацией)
|
|
2133
|
+
// Сигнал НЕ открывается, сразу отменяется
|
|
2000
2134
|
if (candle.high >= scheduled.priceStopLoss) {
|
|
2001
2135
|
shouldCancel = true;
|
|
2002
2136
|
}
|
|
2003
|
-
// Short = продаем дороже, ждем роста цены ДО priceOpen
|
|
2004
|
-
// Активируем только если НЕ пробит StopLoss
|
|
2005
2137
|
else if (candle.high >= scheduled.priceOpen) {
|
|
2006
2138
|
shouldActivate = true;
|
|
2007
2139
|
}
|
|
@@ -2141,10 +2273,15 @@ class ClientStrategy {
|
|
|
2141
2273
|
pendingSignal,
|
|
2142
2274
|
});
|
|
2143
2275
|
this._pendingSignal = pendingSignal;
|
|
2276
|
+
// КРИТИЧНО: Всегда вызываем коллбек onWrite для тестирования persist storage
|
|
2277
|
+
// даже в backtest режиме, чтобы тесты могли перехватывать вызовы через mock adapter
|
|
2278
|
+
if (this.params.callbacks?.onWrite) {
|
|
2279
|
+
this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, this.params.execution.context.backtest);
|
|
2280
|
+
}
|
|
2144
2281
|
if (this.params.execution.context.backtest) {
|
|
2145
2282
|
return;
|
|
2146
2283
|
}
|
|
2147
|
-
await
|
|
2284
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
2148
2285
|
}
|
|
2149
2286
|
/**
|
|
2150
2287
|
* Performs a single tick of strategy execution.
|
|
@@ -10090,4 +10227,4 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
|
|
|
10090
10227
|
};
|
|
10091
10228
|
const PositionSize = PositionSizeUtils;
|
|
10092
10229
|
|
|
10093
|
-
export { Backtest, ExecutionContextService, Heat, Live, MethodContextService, Performance, PersistBase, PersistRiskAdapter,
|
|
10230
|
+
export { Backtest, ExecutionContextService, Heat, Live, MethodContextService, Performance, PersistBase, PersistRiskAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listRisks, listSizings, listStrategies, listWalkers, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenPerformance, listenProgress, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, setConfig, setLogger };
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -31,6 +31,44 @@ declare const GLOBAL_CONFIG: {
|
|
|
31
31
|
* Default: 1440 minutes (1 day)
|
|
32
32
|
*/
|
|
33
33
|
CC_MAX_SIGNAL_LIFETIME_MINUTES: number;
|
|
34
|
+
/**
|
|
35
|
+
* Number of retries for getCandles function
|
|
36
|
+
* Default: 3 retries
|
|
37
|
+
*/
|
|
38
|
+
CC_GET_CANDLES_RETRY_COUNT: number;
|
|
39
|
+
/**
|
|
40
|
+
* Delay between retries for getCandles function (in milliseconds)
|
|
41
|
+
* Default: 5000 ms (5 seconds)
|
|
42
|
+
*/
|
|
43
|
+
CC_GET_CANDLES_RETRY_DELAY_MS: number;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum allowed deviation factor for price anomaly detection.
|
|
46
|
+
* Price should not be more than this factor lower than reference price.
|
|
47
|
+
*
|
|
48
|
+
* Reasoning:
|
|
49
|
+
* - Incomplete candles from Binance API typically have prices near 0 (e.g., $0.01-1)
|
|
50
|
+
* - Normal BTC price ranges: $20,000-100,000
|
|
51
|
+
* - Factor 1000 catches prices below $20-100 when median is $20,000-100,000
|
|
52
|
+
* - Factor 100 would be too permissive (allows $200 when median is $20,000)
|
|
53
|
+
* - Factor 10000 might be too strict for low-cap altcoins
|
|
54
|
+
*
|
|
55
|
+
* Example: BTC at $50,000 median → threshold $50 (catches $0.01-1 anomalies)
|
|
56
|
+
*/
|
|
57
|
+
CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR: number;
|
|
58
|
+
/**
|
|
59
|
+
* Minimum number of candles required for reliable median calculation.
|
|
60
|
+
* Below this threshold, use simple average instead of median.
|
|
61
|
+
*
|
|
62
|
+
* Reasoning:
|
|
63
|
+
* - Each candle provides 4 price points (OHLC)
|
|
64
|
+
* - 5 candles = 20 price points, sufficient for robust median calculation
|
|
65
|
+
* - Below 5 candles, single anomaly can heavily skew median
|
|
66
|
+
* - Statistical rule of thumb: minimum 7-10 data points for median stability
|
|
67
|
+
* - Average is more stable than median for small datasets (n < 20)
|
|
68
|
+
*
|
|
69
|
+
* Example: 3 candles = 12 points (use average), 5 candles = 20 points (use median)
|
|
70
|
+
*/
|
|
71
|
+
CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: number;
|
|
34
72
|
};
|
|
35
73
|
/**
|
|
36
74
|
* Type for global configuration object.
|
|
@@ -608,6 +646,8 @@ interface IStrategyCallbacks {
|
|
|
608
646
|
onSchedule: (symbol: string, data: IScheduledSignalRow, currentPrice: number, backtest: boolean) => void;
|
|
609
647
|
/** Called when scheduled signal is cancelled without opening position */
|
|
610
648
|
onCancel: (symbol: string, data: IScheduledSignalRow, currentPrice: number, backtest: boolean) => void;
|
|
649
|
+
/** Called when signal is written to persist storage (for testing) */
|
|
650
|
+
onWrite: (symbol: string, data: ISignalRow | null, backtest: boolean) => void;
|
|
611
651
|
}
|
|
612
652
|
/**
|
|
613
653
|
* Strategy schema registered via addStrategy().
|
|
@@ -3433,7 +3473,7 @@ declare class PersistSignalUtils {
|
|
|
3433
3473
|
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
3434
3474
|
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
3435
3475
|
* }
|
|
3436
|
-
*
|
|
3476
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
3437
3477
|
* ```
|
|
3438
3478
|
*/
|
|
3439
3479
|
usePersistSignalAdapter(Ctor: TPersistBaseCtor<StrategyName, SignalData>): void;
|
|
@@ -3468,16 +3508,16 @@ declare class PersistSignalUtils {
|
|
|
3468
3508
|
* @example
|
|
3469
3509
|
* ```typescript
|
|
3470
3510
|
* // Custom adapter
|
|
3471
|
-
*
|
|
3511
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
3472
3512
|
*
|
|
3473
3513
|
* // Read signal
|
|
3474
|
-
* const signal = await
|
|
3514
|
+
* const signal = await PersistSignalAdapter.readSignalData("my-strategy", "BTCUSDT");
|
|
3475
3515
|
*
|
|
3476
3516
|
* // Write signal
|
|
3477
|
-
* await
|
|
3517
|
+
* await PersistSignalAdapter.writeSignalData(signal, "my-strategy", "BTCUSDT");
|
|
3478
3518
|
* ```
|
|
3479
3519
|
*/
|
|
3480
|
-
declare const
|
|
3520
|
+
declare const PersistSignalAdapter: PersistSignalUtils;
|
|
3481
3521
|
/**
|
|
3482
3522
|
* Type for persisted risk positions data.
|
|
3483
3523
|
* Stores Map entries as array of [key, value] tuples for JSON serialization.
|
|
@@ -6326,4 +6366,4 @@ declare const backtest: {
|
|
|
6326
6366
|
loggerService: LoggerService;
|
|
6327
6367
|
};
|
|
6328
6368
|
|
|
6329
|
-
export { Backtest, type BacktestStatistics, type CandleInterval, type DoneContract, type EntityId, ExecutionContextService, type FrameInterval, type GlobalConfig, Heat, type ICandleData, type IExchangeSchema, type IFrameSchema, type IHeatmapRow, type IHeatmapStatistics, type IPersistBase, type IPositionSizeATRParams, type IPositionSizeFixedPercentageParams, type IPositionSizeKellyParams, type IRiskActivePosition, type IRiskCheckArgs, type IRiskSchema, type IRiskValidation, type IRiskValidationFn, type IRiskValidationPayload, type IScheduledSignalRow, type ISignalDto, type ISignalRow, type ISizingCalculateParams, type ISizingCalculateParamsATR, type ISizingCalculateParamsFixedPercentage, type ISizingCalculateParamsKelly, type ISizingSchema, type ISizingSchemaATR, type ISizingSchemaFixedPercentage, type ISizingSchemaKelly, type IStrategyPnL, type IStrategySchema, type IStrategyTickResult, type IStrategyTickResultActive, type IStrategyTickResultCancelled, type IStrategyTickResultClosed, type IStrategyTickResultIdle, type IStrategyTickResultOpened, type IStrategyTickResultScheduled, type IWalkerResults, type IWalkerSchema, type IWalkerStrategyResult, Live, type LiveStatistics, MethodContextService, Performance, type PerformanceContract, type PerformanceMetricType, type PerformanceStatistics, PersistBase, PersistRiskAdapter,
|
|
6369
|
+
export { Backtest, type BacktestStatistics, type CandleInterval, type DoneContract, type EntityId, ExecutionContextService, type FrameInterval, type GlobalConfig, Heat, type ICandleData, type IExchangeSchema, type IFrameSchema, type IHeatmapRow, type IHeatmapStatistics, type IPersistBase, type IPositionSizeATRParams, type IPositionSizeFixedPercentageParams, type IPositionSizeKellyParams, type IRiskActivePosition, type IRiskCheckArgs, type IRiskSchema, type IRiskValidation, type IRiskValidationFn, type IRiskValidationPayload, type IScheduledSignalRow, type ISignalDto, type ISignalRow, type ISizingCalculateParams, type ISizingCalculateParamsATR, type ISizingCalculateParamsFixedPercentage, type ISizingCalculateParamsKelly, type ISizingSchema, type ISizingSchemaATR, type ISizingSchemaFixedPercentage, type ISizingSchemaKelly, type IStrategyPnL, type IStrategySchema, type IStrategyTickResult, type IStrategyTickResultActive, type IStrategyTickResultCancelled, type IStrategyTickResultClosed, type IStrategyTickResultIdle, type IStrategyTickResultOpened, type IStrategyTickResultScheduled, type IWalkerResults, type IWalkerSchema, type IWalkerStrategyResult, Live, type LiveStatistics, MethodContextService, Performance, type PerformanceContract, type PerformanceMetricType, type PerformanceStatistics, PersistBase, PersistRiskAdapter, PersistSignalAdapter, PositionSize, type ProgressContract, type RiskData, Schedule, type ScheduleStatistics, type SignalData, type SignalInterval, type TPersistBase, type TPersistBaseCtor, Walker, type WalkerMetric, type WalkerStatistics, addExchange, addFrame, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listRisks, listSizings, listStrategies, listWalkers, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenPerformance, listenProgress, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, setConfig, setLogger };
|