backtest-kit 3.0.9 → 3.0.11
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 +24 -0
- package/build/index.cjs +1405 -1330
- package/build/index.mjs +1405 -1331
- package/package.json +1 -1
- package/types.d.ts +133 -109
package/build/index.cjs
CHANGED
|
@@ -715,7 +715,7 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
715
715
|
var _a$2;
|
|
716
716
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
717
717
|
// Calculate step in milliseconds for candle close time validation
|
|
718
|
-
const INTERVAL_MINUTES$
|
|
718
|
+
const INTERVAL_MINUTES$6 = {
|
|
719
719
|
"1m": 1,
|
|
720
720
|
"3m": 3,
|
|
721
721
|
"5m": 5,
|
|
@@ -727,7 +727,7 @@ const INTERVAL_MINUTES$5 = {
|
|
|
727
727
|
"6h": 360,
|
|
728
728
|
"8h": 480,
|
|
729
729
|
};
|
|
730
|
-
const MS_PER_MINUTE$
|
|
730
|
+
const MS_PER_MINUTE$4 = 60000;
|
|
731
731
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
732
732
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
733
733
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
|
|
@@ -1625,7 +1625,7 @@ class PersistCandleUtils {
|
|
|
1625
1625
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1626
1626
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1627
1627
|
await stateStorage.waitForInit(isInitial);
|
|
1628
|
-
const stepMs = INTERVAL_MINUTES$
|
|
1628
|
+
const stepMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$4;
|
|
1629
1629
|
// Calculate expected timestamps and fetch each candle directly
|
|
1630
1630
|
const cachedCandles = [];
|
|
1631
1631
|
for (let i = 0; i < limit; i++) {
|
|
@@ -1681,7 +1681,7 @@ class PersistCandleUtils {
|
|
|
1681
1681
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1682
1682
|
await stateStorage.waitForInit(isInitial);
|
|
1683
1683
|
// Calculate step in milliseconds to determine candle close time
|
|
1684
|
-
const stepMs = INTERVAL_MINUTES$
|
|
1684
|
+
const stepMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$4;
|
|
1685
1685
|
const now = Date.now();
|
|
1686
1686
|
// Write each candle as a separate file, skipping incomplete candles
|
|
1687
1687
|
for (const candle of candles) {
|
|
@@ -1938,8 +1938,8 @@ class PersistNotificationUtils {
|
|
|
1938
1938
|
*/
|
|
1939
1939
|
const PersistNotificationAdapter = new PersistNotificationUtils();
|
|
1940
1940
|
|
|
1941
|
-
const MS_PER_MINUTE$
|
|
1942
|
-
const INTERVAL_MINUTES$
|
|
1941
|
+
const MS_PER_MINUTE$3 = 60000;
|
|
1942
|
+
const INTERVAL_MINUTES$5 = {
|
|
1943
1943
|
"1m": 1,
|
|
1944
1944
|
"3m": 3,
|
|
1945
1945
|
"5m": 5,
|
|
@@ -1968,8 +1968,8 @@ const INTERVAL_MINUTES$4 = {
|
|
|
1968
1968
|
* @param intervalMinutes - Interval in minutes
|
|
1969
1969
|
* @returns Aligned timestamp rounded down to interval boundary
|
|
1970
1970
|
*/
|
|
1971
|
-
const ALIGN_TO_INTERVAL_FN$
|
|
1972
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$
|
|
1971
|
+
const ALIGN_TO_INTERVAL_FN$2 = (timestamp, intervalMinutes) => {
|
|
1972
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$3;
|
|
1973
1973
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
1974
1974
|
};
|
|
1975
1975
|
/**
|
|
@@ -2114,9 +2114,9 @@ const WRITE_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(functoolsKit.queued(async
|
|
|
2114
2114
|
* @returns Promise resolving to array of candle data
|
|
2115
2115
|
*/
|
|
2116
2116
|
const GET_CANDLES_FN = async (dto, since, self) => {
|
|
2117
|
-
const step = INTERVAL_MINUTES$
|
|
2117
|
+
const step = INTERVAL_MINUTES$5[dto.interval];
|
|
2118
2118
|
const sinceTimestamp = since.getTime();
|
|
2119
|
-
const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$
|
|
2119
|
+
const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$3;
|
|
2120
2120
|
// Try to read from cache first
|
|
2121
2121
|
const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
|
|
2122
2122
|
if (cachedCandles !== null) {
|
|
@@ -2224,14 +2224,14 @@ class ClientExchange {
|
|
|
2224
2224
|
interval,
|
|
2225
2225
|
limit,
|
|
2226
2226
|
});
|
|
2227
|
-
const step = INTERVAL_MINUTES$
|
|
2227
|
+
const step = INTERVAL_MINUTES$5[interval];
|
|
2228
2228
|
if (!step) {
|
|
2229
2229
|
throw new Error(`ClientExchange unknown interval=${interval}`);
|
|
2230
2230
|
}
|
|
2231
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
2231
|
+
const stepMs = step * MS_PER_MINUTE$3;
|
|
2232
2232
|
// Align when down to interval boundary
|
|
2233
2233
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2234
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN$
|
|
2234
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
|
|
2235
2235
|
// Calculate since: go back limit candles from aligned when
|
|
2236
2236
|
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2237
2237
|
const since = new Date(sinceTimestamp);
|
|
@@ -2305,15 +2305,15 @@ class ClientExchange {
|
|
|
2305
2305
|
if (!this.params.execution.context.backtest) {
|
|
2306
2306
|
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2307
2307
|
}
|
|
2308
|
-
const step = INTERVAL_MINUTES$
|
|
2308
|
+
const step = INTERVAL_MINUTES$5[interval];
|
|
2309
2309
|
if (!step) {
|
|
2310
2310
|
throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
|
|
2311
2311
|
}
|
|
2312
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
2312
|
+
const stepMs = step * MS_PER_MINUTE$3;
|
|
2313
2313
|
const now = Date.now();
|
|
2314
2314
|
// Align when down to interval boundary
|
|
2315
2315
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2316
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN$
|
|
2316
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
|
|
2317
2317
|
// since = alignedWhen (start from aligned when, going forward)
|
|
2318
2318
|
const sinceTimestamp = alignedWhen;
|
|
2319
2319
|
const since = new Date(sinceTimestamp);
|
|
@@ -2473,13 +2473,13 @@ class ClientExchange {
|
|
|
2473
2473
|
sDate,
|
|
2474
2474
|
eDate,
|
|
2475
2475
|
});
|
|
2476
|
-
const step = INTERVAL_MINUTES$
|
|
2476
|
+
const step = INTERVAL_MINUTES$5[interval];
|
|
2477
2477
|
if (!step) {
|
|
2478
2478
|
throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
|
|
2479
2479
|
}
|
|
2480
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
2480
|
+
const stepMs = step * MS_PER_MINUTE$3;
|
|
2481
2481
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2482
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN$
|
|
2482
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
|
|
2483
2483
|
let sinceTimestamp;
|
|
2484
2484
|
let calculatedLimit;
|
|
2485
2485
|
// Case 1: all three parameters provided
|
|
@@ -2491,7 +2491,7 @@ class ClientExchange {
|
|
|
2491
2491
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2492
2492
|
}
|
|
2493
2493
|
// Align sDate down to interval boundary
|
|
2494
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN$
|
|
2494
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
|
|
2495
2495
|
calculatedLimit = limit;
|
|
2496
2496
|
}
|
|
2497
2497
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -2503,8 +2503,8 @@ class ClientExchange {
|
|
|
2503
2503
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2504
2504
|
}
|
|
2505
2505
|
// Align sDate down to interval boundary
|
|
2506
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN$
|
|
2507
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN$
|
|
2506
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
|
|
2507
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$2(eDate, step);
|
|
2508
2508
|
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
2509
2509
|
if (calculatedLimit <= 0) {
|
|
2510
2510
|
throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
@@ -2516,14 +2516,14 @@ class ClientExchange {
|
|
|
2516
2516
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2517
2517
|
}
|
|
2518
2518
|
// Align eDate down and calculate sinceTimestamp
|
|
2519
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN$
|
|
2519
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$2(eDate, step);
|
|
2520
2520
|
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
2521
2521
|
calculatedLimit = limit;
|
|
2522
2522
|
}
|
|
2523
2523
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
2524
2524
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
2525
2525
|
// Align sDate down to interval boundary
|
|
2526
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN$
|
|
2526
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
|
|
2527
2527
|
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
2528
2528
|
if (endTimestamp > whenTimestamp) {
|
|
2529
2529
|
throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
@@ -2602,9 +2602,11 @@ class ClientExchange {
|
|
|
2602
2602
|
symbol,
|
|
2603
2603
|
depth,
|
|
2604
2604
|
});
|
|
2605
|
-
const
|
|
2606
|
-
const
|
|
2607
|
-
|
|
2605
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2606
|
+
const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
|
|
2607
|
+
const to = new Date(alignedTo);
|
|
2608
|
+
const from = new Date(alignedTo -
|
|
2609
|
+
GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$3);
|
|
2608
2610
|
return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
|
|
2609
2611
|
}
|
|
2610
2612
|
}
|
|
@@ -3069,7 +3071,7 @@ const beginTime = (run) => (...args) => {
|
|
|
3069
3071
|
return fn();
|
|
3070
3072
|
};
|
|
3071
3073
|
|
|
3072
|
-
const INTERVAL_MINUTES$
|
|
3074
|
+
const INTERVAL_MINUTES$4 = {
|
|
3073
3075
|
"1m": 1,
|
|
3074
3076
|
"3m": 3,
|
|
3075
3077
|
"5m": 5,
|
|
@@ -3582,7 +3584,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
3582
3584
|
}
|
|
3583
3585
|
const currentTime = self.params.execution.context.when.getTime();
|
|
3584
3586
|
{
|
|
3585
|
-
const intervalMinutes = INTERVAL_MINUTES$
|
|
3587
|
+
const intervalMinutes = INTERVAL_MINUTES$4[self.params.interval];
|
|
3586
3588
|
const intervalMs = intervalMinutes * 60 * 1000;
|
|
3587
3589
|
// Проверяем что прошел нужный интервал с последнего getSignal
|
|
3588
3590
|
if (self._lastSignalTimestamp !== null &&
|
|
@@ -8193,7 +8195,7 @@ class StrategyConnectionService {
|
|
|
8193
8195
|
* Maps FrameInterval to minutes for timestamp calculation.
|
|
8194
8196
|
* Used to generate timeframe arrays with proper spacing.
|
|
8195
8197
|
*/
|
|
8196
|
-
const INTERVAL_MINUTES$
|
|
8198
|
+
const INTERVAL_MINUTES$3 = {
|
|
8197
8199
|
"1m": 1,
|
|
8198
8200
|
"3m": 3,
|
|
8199
8201
|
"5m": 5,
|
|
@@ -8248,7 +8250,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
|
|
|
8248
8250
|
symbol,
|
|
8249
8251
|
});
|
|
8250
8252
|
const { interval, startDate, endDate } = self.params;
|
|
8251
|
-
const intervalMinutes = INTERVAL_MINUTES$
|
|
8253
|
+
const intervalMinutes = INTERVAL_MINUTES$3[interval];
|
|
8252
8254
|
if (!intervalMinutes) {
|
|
8253
8255
|
throw new Error(`ClientFrame unknown interval: ${interval}`);
|
|
8254
8256
|
}
|
|
@@ -26038,401 +26040,1199 @@ async function getBacktestTimeframe(symbol) {
|
|
|
26038
26040
|
return await bt.frameCoreService.getTimeframe(symbol, bt.methodContextService.context.frameName);
|
|
26039
26041
|
}
|
|
26040
26042
|
|
|
26041
|
-
const
|
|
26043
|
+
const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
|
|
26044
|
+
const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
|
|
26045
|
+
const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
|
|
26046
|
+
const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
26047
|
+
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
26048
|
+
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
26049
|
+
const MS_PER_MINUTE$2 = 60000;
|
|
26042
26050
|
/**
|
|
26043
|
-
*
|
|
26044
|
-
*
|
|
26045
|
-
* @returns Map of exchange names
|
|
26051
|
+
* Gets current timestamp from execution context if available.
|
|
26052
|
+
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
26046
26053
|
*/
|
|
26047
|
-
const
|
|
26048
|
-
|
|
26049
|
-
|
|
26050
|
-
Object.assign(exchangeMap, { [exchangeName]: exchangeName });
|
|
26054
|
+
const GET_TIMESTAMP_FN = async () => {
|
|
26055
|
+
if (ExecutionContextService.hasContext()) {
|
|
26056
|
+
return new Date(bt.executionContextService.context.when);
|
|
26051
26057
|
}
|
|
26052
|
-
return
|
|
26058
|
+
return new Date();
|
|
26053
26059
|
};
|
|
26054
26060
|
/**
|
|
26055
|
-
*
|
|
26056
|
-
*
|
|
26057
|
-
* @returns Map of frame names
|
|
26061
|
+
* Gets backtest mode flag from execution context if available.
|
|
26062
|
+
* Returns false if no execution context exists (live mode).
|
|
26058
26063
|
*/
|
|
26059
|
-
const
|
|
26060
|
-
|
|
26061
|
-
|
|
26062
|
-
Object.assign(frameMap, { [frameName]: frameName });
|
|
26064
|
+
const GET_BACKTEST_FN = async () => {
|
|
26065
|
+
if (ExecutionContextService.hasContext()) {
|
|
26066
|
+
return bt.executionContextService.context.backtest;
|
|
26063
26067
|
}
|
|
26064
|
-
return
|
|
26068
|
+
return false;
|
|
26065
26069
|
};
|
|
26066
26070
|
/**
|
|
26067
|
-
*
|
|
26068
|
-
*
|
|
26069
|
-
* @returns Map of strategy names
|
|
26071
|
+
* Default implementation for getCandles.
|
|
26072
|
+
* Throws an error indicating the method is not implemented.
|
|
26070
26073
|
*/
|
|
26071
|
-
const
|
|
26072
|
-
|
|
26073
|
-
for (const { strategyName } of await bt.strategyValidationService.list()) {
|
|
26074
|
-
Object.assign(strategyMap, { [strategyName]: strategyName });
|
|
26075
|
-
}
|
|
26076
|
-
return strategyMap;
|
|
26074
|
+
const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
|
|
26075
|
+
throw new Error(`getCandles is not implemented for this exchange`);
|
|
26077
26076
|
};
|
|
26078
26077
|
/**
|
|
26079
|
-
*
|
|
26080
|
-
*
|
|
26081
|
-
* @returns Map of risk names
|
|
26078
|
+
* Default implementation for formatQuantity.
|
|
26079
|
+
* Returns Bitcoin precision on Binance (8 decimal places).
|
|
26082
26080
|
*/
|
|
26083
|
-
const
|
|
26084
|
-
|
|
26085
|
-
for (const { riskName } of await bt.riskValidationService.list()) {
|
|
26086
|
-
Object.assign(riskMap, { [riskName]: riskName });
|
|
26087
|
-
}
|
|
26088
|
-
return riskMap;
|
|
26081
|
+
const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
|
|
26082
|
+
return quantity.toFixed(8);
|
|
26089
26083
|
};
|
|
26090
26084
|
/**
|
|
26091
|
-
*
|
|
26092
|
-
*
|
|
26093
|
-
* @returns Map of action names
|
|
26085
|
+
* Default implementation for formatPrice.
|
|
26086
|
+
* Returns Bitcoin precision on Binance (2 decimal places).
|
|
26094
26087
|
*/
|
|
26095
|
-
const
|
|
26096
|
-
|
|
26097
|
-
for (const { actionName } of await bt.actionValidationService.list()) {
|
|
26098
|
-
Object.assign(actionMap, { [actionName]: actionName });
|
|
26099
|
-
}
|
|
26100
|
-
return actionMap;
|
|
26088
|
+
const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
|
|
26089
|
+
return price.toFixed(2);
|
|
26101
26090
|
};
|
|
26102
26091
|
/**
|
|
26103
|
-
*
|
|
26104
|
-
*
|
|
26105
|
-
*
|
|
26092
|
+
* Default implementation for getOrderBook.
|
|
26093
|
+
* Throws an error indicating the method is not implemented.
|
|
26094
|
+
*
|
|
26095
|
+
* @param _symbol - Trading pair symbol (unused)
|
|
26096
|
+
* @param _depth - Maximum depth levels (unused)
|
|
26097
|
+
* @param _from - Start of time range (unused - can be ignored in live implementations)
|
|
26098
|
+
* @param _to - End of time range (unused - can be ignored in live implementations)
|
|
26099
|
+
* @param _backtest - Whether running in backtest mode (unused)
|
|
26106
26100
|
*/
|
|
26107
|
-
const
|
|
26108
|
-
|
|
26109
|
-
for (const { sizingName } of await bt.sizingValidationService.list()) {
|
|
26110
|
-
Object.assign(sizingMap, { [sizingName]: sizingName });
|
|
26111
|
-
}
|
|
26112
|
-
return sizingMap;
|
|
26101
|
+
const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
|
|
26102
|
+
throw new Error(`getOrderBook is not implemented for this exchange`);
|
|
26113
26103
|
};
|
|
26114
|
-
|
|
26115
|
-
|
|
26116
|
-
|
|
26117
|
-
|
|
26118
|
-
|
|
26119
|
-
|
|
26120
|
-
|
|
26121
|
-
|
|
26122
|
-
|
|
26123
|
-
|
|
26124
|
-
|
|
26104
|
+
const INTERVAL_MINUTES$2 = {
|
|
26105
|
+
"1m": 1,
|
|
26106
|
+
"3m": 3,
|
|
26107
|
+
"5m": 5,
|
|
26108
|
+
"15m": 15,
|
|
26109
|
+
"30m": 30,
|
|
26110
|
+
"1h": 60,
|
|
26111
|
+
"2h": 120,
|
|
26112
|
+
"4h": 240,
|
|
26113
|
+
"6h": 360,
|
|
26114
|
+
"8h": 480,
|
|
26125
26115
|
};
|
|
26126
26116
|
/**
|
|
26127
|
-
*
|
|
26117
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
26118
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
26128
26119
|
*
|
|
26129
|
-
*
|
|
26130
|
-
*
|
|
26120
|
+
* Candle timestamp convention:
|
|
26121
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
26122
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
26131
26123
|
*
|
|
26132
|
-
*
|
|
26133
|
-
*
|
|
26124
|
+
* Adapter contract:
|
|
26125
|
+
* - Adapter must return candles with timestamp = openTime
|
|
26126
|
+
* - First returned candle.timestamp must equal aligned since
|
|
26127
|
+
* - Adapter must return exactly `limit` candles
|
|
26134
26128
|
*
|
|
26135
|
-
* @
|
|
26136
|
-
* @param
|
|
26137
|
-
* @
|
|
26129
|
+
* @param timestamp - Timestamp in milliseconds
|
|
26130
|
+
* @param intervalMinutes - Interval in minutes
|
|
26131
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
26138
26132
|
*/
|
|
26139
|
-
const
|
|
26140
|
-
const
|
|
26141
|
-
|
|
26142
|
-
bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
|
|
26143
|
-
}
|
|
26144
|
-
for (const frameName of Object.values(FrameName)) {
|
|
26145
|
-
bt.frameValidationService.validate(frameName, METHOD_NAME);
|
|
26146
|
-
}
|
|
26147
|
-
for (const strategyName of Object.values(StrategyName)) {
|
|
26148
|
-
bt.strategyValidationService.validate(strategyName, METHOD_NAME);
|
|
26149
|
-
}
|
|
26150
|
-
for (const riskName of Object.values(RiskName)) {
|
|
26151
|
-
bt.riskValidationService.validate(riskName, METHOD_NAME);
|
|
26152
|
-
}
|
|
26153
|
-
for (const actionName of Object.values(ActionName)) {
|
|
26154
|
-
bt.actionValidationService.validate(actionName, METHOD_NAME);
|
|
26155
|
-
}
|
|
26156
|
-
for (const sizingName of Object.values(SizingName)) {
|
|
26157
|
-
bt.sizingValidationService.validate(sizingName, METHOD_NAME);
|
|
26158
|
-
}
|
|
26159
|
-
for (const walkerName of Object.values(WalkerName)) {
|
|
26160
|
-
bt.walkerValidationService.validate(walkerName, METHOD_NAME);
|
|
26161
|
-
}
|
|
26133
|
+
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
26134
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
|
|
26135
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
26162
26136
|
};
|
|
26163
26137
|
/**
|
|
26164
|
-
*
|
|
26165
|
-
*
|
|
26166
|
-
* This function accepts enum objects for various entity types (exchanges, frames,
|
|
26167
|
-
* strategies, risks, sizings, walkers) and validates that each entity
|
|
26168
|
-
* name exists in its respective registry. Validation results are memoized for performance.
|
|
26169
|
-
*
|
|
26170
|
-
* If no arguments are provided (or specific entity types are omitted), the function
|
|
26171
|
-
* automatically fetches and validates ALL registered entities from their respective
|
|
26172
|
-
* validation services. This is useful for comprehensive validation of the entire setup.
|
|
26173
|
-
*
|
|
26174
|
-
* Use this before running backtests or optimizations to ensure all referenced
|
|
26175
|
-
* entities are properly registered and configured.
|
|
26176
|
-
*
|
|
26177
|
-
* @public
|
|
26178
|
-
* @param args - Partial validation arguments containing entity name enums to validate.
|
|
26179
|
-
* If empty or omitted, validates all registered entities.
|
|
26180
|
-
* @throws {Error} If any entity name is not found in its validation service
|
|
26181
|
-
*
|
|
26182
|
-
* @example
|
|
26183
|
-
* ```typescript
|
|
26184
|
-
* // Validate ALL registered entities (exchanges, frames, strategies, etc.)
|
|
26185
|
-
* await validate({});
|
|
26186
|
-
* ```
|
|
26187
|
-
*
|
|
26188
|
-
* @example
|
|
26189
|
-
* ```typescript
|
|
26190
|
-
* // Define your entity name enums
|
|
26191
|
-
* enum ExchangeName {
|
|
26192
|
-
* BINANCE = "binance",
|
|
26193
|
-
* BYBIT = "bybit"
|
|
26194
|
-
* }
|
|
26195
|
-
*
|
|
26196
|
-
* enum StrategyName {
|
|
26197
|
-
* MOMENTUM_BTC = "momentum-btc"
|
|
26198
|
-
* }
|
|
26199
|
-
*
|
|
26200
|
-
* // Validate specific entities before running backtest
|
|
26201
|
-
* await validate({
|
|
26202
|
-
* ExchangeName,
|
|
26203
|
-
* StrategyName,
|
|
26204
|
-
* });
|
|
26205
|
-
* ```
|
|
26206
|
-
*
|
|
26207
|
-
* @example
|
|
26208
|
-
* ```typescript
|
|
26209
|
-
* // Validate specific entity types
|
|
26210
|
-
* await validate({
|
|
26211
|
-
* RiskName: { CONSERVATIVE: "conservative" },
|
|
26212
|
-
* SizingName: { FIXED_1000: "fixed-1000" },
|
|
26213
|
-
* });
|
|
26214
|
-
* ```
|
|
26215
|
-
*/
|
|
26216
|
-
async function validate(args = {}) {
|
|
26217
|
-
bt.loggerService.log(METHOD_NAME);
|
|
26218
|
-
return await validateInternal(args);
|
|
26219
|
-
}
|
|
26220
|
-
|
|
26221
|
-
const GET_STRATEGY_METHOD_NAME = "get.getStrategySchema";
|
|
26222
|
-
const GET_EXCHANGE_METHOD_NAME = "get.getExchangeSchema";
|
|
26223
|
-
const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
|
|
26224
|
-
const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
|
|
26225
|
-
const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
|
|
26226
|
-
const GET_RISK_METHOD_NAME = "get.getRiskSchema";
|
|
26227
|
-
const GET_ACTION_METHOD_NAME = "get.getActionSchema";
|
|
26228
|
-
/**
|
|
26229
|
-
* Retrieves a registered strategy schema by name.
|
|
26230
|
-
*
|
|
26231
|
-
* @param strategyName - Unique strategy identifier
|
|
26232
|
-
* @returns The strategy schema configuration object
|
|
26233
|
-
* @throws Error if strategy is not registered
|
|
26234
|
-
*
|
|
26235
|
-
* @example
|
|
26236
|
-
* ```typescript
|
|
26237
|
-
* const strategy = getStrategy("my-strategy");
|
|
26238
|
-
* console.log(strategy.interval); // "5m"
|
|
26239
|
-
* console.log(strategy.getSignal); // async function
|
|
26240
|
-
* ```
|
|
26241
|
-
*/
|
|
26242
|
-
function getStrategySchema(strategyName) {
|
|
26243
|
-
bt.loggerService.log(GET_STRATEGY_METHOD_NAME, {
|
|
26244
|
-
strategyName,
|
|
26245
|
-
});
|
|
26246
|
-
bt.strategyValidationService.validate(strategyName, GET_STRATEGY_METHOD_NAME);
|
|
26247
|
-
return bt.strategySchemaService.get(strategyName);
|
|
26248
|
-
}
|
|
26249
|
-
/**
|
|
26250
|
-
* Retrieves a registered exchange schema by name.
|
|
26251
|
-
*
|
|
26252
|
-
* @param exchangeName - Unique exchange identifier
|
|
26253
|
-
* @returns The exchange schema configuration object
|
|
26254
|
-
* @throws Error if exchange is not registered
|
|
26255
|
-
*
|
|
26256
|
-
* @example
|
|
26257
|
-
* ```typescript
|
|
26258
|
-
* const exchange = getExchange("binance");
|
|
26259
|
-
* console.log(exchange.getCandles); // async function
|
|
26260
|
-
* console.log(exchange.formatPrice); // async function
|
|
26261
|
-
* ```
|
|
26262
|
-
*/
|
|
26263
|
-
function getExchangeSchema(exchangeName) {
|
|
26264
|
-
bt.loggerService.log(GET_EXCHANGE_METHOD_NAME, {
|
|
26265
|
-
exchangeName,
|
|
26266
|
-
});
|
|
26267
|
-
bt.exchangeValidationService.validate(exchangeName, GET_EXCHANGE_METHOD_NAME);
|
|
26268
|
-
return bt.exchangeSchemaService.get(exchangeName);
|
|
26269
|
-
}
|
|
26270
|
-
/**
|
|
26271
|
-
* Retrieves a registered frame schema by name.
|
|
26272
|
-
*
|
|
26273
|
-
* @param frameName - Unique frame identifier
|
|
26274
|
-
* @returns The frame schema configuration object
|
|
26275
|
-
* @throws Error if frame is not registered
|
|
26276
|
-
*
|
|
26277
|
-
* @example
|
|
26278
|
-
* ```typescript
|
|
26279
|
-
* const frame = getFrame("1d-backtest");
|
|
26280
|
-
* console.log(frame.interval); // "1m"
|
|
26281
|
-
* console.log(frame.startDate); // Date object
|
|
26282
|
-
* console.log(frame.endDate); // Date object
|
|
26283
|
-
* ```
|
|
26284
|
-
*/
|
|
26285
|
-
function getFrameSchema(frameName) {
|
|
26286
|
-
bt.loggerService.log(GET_FRAME_METHOD_NAME, {
|
|
26287
|
-
frameName,
|
|
26288
|
-
});
|
|
26289
|
-
bt.frameValidationService.validate(frameName, GET_FRAME_METHOD_NAME);
|
|
26290
|
-
return bt.frameSchemaService.get(frameName);
|
|
26291
|
-
}
|
|
26292
|
-
/**
|
|
26293
|
-
* Retrieves a registered walker schema by name.
|
|
26294
|
-
*
|
|
26295
|
-
* @param walkerName - Unique walker identifier
|
|
26296
|
-
* @returns The walker schema configuration object
|
|
26297
|
-
* @throws Error if walker is not registered
|
|
26298
|
-
*
|
|
26299
|
-
* @example
|
|
26300
|
-
* ```typescript
|
|
26301
|
-
* const walker = getWalker("llm-prompt-optimizer");
|
|
26302
|
-
* console.log(walker.exchangeName); // "binance"
|
|
26303
|
-
* console.log(walker.frameName); // "1d-backtest"
|
|
26304
|
-
* console.log(walker.strategies); // ["my-strategy-v1", "my-strategy-v2"]
|
|
26305
|
-
* console.log(walker.metric); // "sharpeRatio"
|
|
26306
|
-
* ```
|
|
26307
|
-
*/
|
|
26308
|
-
function getWalkerSchema(walkerName) {
|
|
26309
|
-
bt.loggerService.log(GET_WALKER_METHOD_NAME, {
|
|
26310
|
-
walkerName,
|
|
26311
|
-
});
|
|
26312
|
-
bt.walkerValidationService.validate(walkerName, GET_WALKER_METHOD_NAME);
|
|
26313
|
-
return bt.walkerSchemaService.get(walkerName);
|
|
26314
|
-
}
|
|
26315
|
-
/**
|
|
26316
|
-
* Retrieves a registered sizing schema by name.
|
|
26317
|
-
*
|
|
26318
|
-
* @param sizingName - Unique sizing identifier
|
|
26319
|
-
* @returns The sizing schema configuration object
|
|
26320
|
-
* @throws Error if sizing is not registered
|
|
26138
|
+
* Creates exchange instance with methods resolved once during construction.
|
|
26139
|
+
* Applies default implementations where schema methods are not provided.
|
|
26321
26140
|
*
|
|
26322
|
-
* @
|
|
26323
|
-
*
|
|
26324
|
-
* const sizing = getSizing("conservative");
|
|
26325
|
-
* console.log(sizing.method); // "fixed-percentage"
|
|
26326
|
-
* console.log(sizing.riskPercentage); // 1
|
|
26327
|
-
* console.log(sizing.maxPositionPercentage); // 10
|
|
26328
|
-
* ```
|
|
26141
|
+
* @param schema - Exchange schema from registry
|
|
26142
|
+
* @returns Object with resolved exchange methods
|
|
26329
26143
|
*/
|
|
26330
|
-
|
|
26331
|
-
|
|
26332
|
-
|
|
26333
|
-
|
|
26334
|
-
|
|
26335
|
-
return
|
|
26336
|
-
|
|
26144
|
+
const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
26145
|
+
const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
|
|
26146
|
+
const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
|
|
26147
|
+
const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
|
|
26148
|
+
const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
|
|
26149
|
+
return {
|
|
26150
|
+
getCandles,
|
|
26151
|
+
formatQuantity,
|
|
26152
|
+
formatPrice,
|
|
26153
|
+
getOrderBook,
|
|
26154
|
+
};
|
|
26155
|
+
};
|
|
26337
26156
|
/**
|
|
26338
|
-
*
|
|
26157
|
+
* Attempts to read candles from cache.
|
|
26339
26158
|
*
|
|
26340
|
-
*
|
|
26341
|
-
*
|
|
26342
|
-
*
|
|
26159
|
+
* Cache lookup calculates expected timestamps:
|
|
26160
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
26161
|
+
* Returns all candles if found, null if any missing.
|
|
26343
26162
|
*
|
|
26344
|
-
* @
|
|
26345
|
-
*
|
|
26346
|
-
*
|
|
26347
|
-
*
|
|
26348
|
-
*
|
|
26349
|
-
* ```
|
|
26163
|
+
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
26164
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
26165
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
26166
|
+
* @param exchangeName - Exchange name
|
|
26167
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
26350
26168
|
*/
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26169
|
+
const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
26170
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
26171
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
26172
|
+
// Returns all candles if found, null if any missing
|
|
26173
|
+
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
26174
|
+
// Return cached data only if we have exactly the requested limit
|
|
26175
|
+
if (cachedCandles?.length === dto.limit) {
|
|
26176
|
+
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
26177
|
+
return cachedCandles;
|
|
26178
|
+
}
|
|
26179
|
+
bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
26180
|
+
return null;
|
|
26181
|
+
}, {
|
|
26182
|
+
fallback: async (error) => {
|
|
26183
|
+
const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
|
|
26184
|
+
const payload = {
|
|
26185
|
+
error: functoolsKit.errorData(error),
|
|
26186
|
+
message: functoolsKit.getErrorMessage(error),
|
|
26187
|
+
};
|
|
26188
|
+
bt.loggerService.warn(message, payload);
|
|
26189
|
+
console.warn(message, payload);
|
|
26190
|
+
errorEmitter.next(error);
|
|
26191
|
+
},
|
|
26192
|
+
defaultValue: null,
|
|
26193
|
+
});
|
|
26358
26194
|
/**
|
|
26359
|
-
*
|
|
26195
|
+
* Writes candles to cache with error handling.
|
|
26360
26196
|
*
|
|
26361
|
-
*
|
|
26362
|
-
*
|
|
26363
|
-
*
|
|
26197
|
+
* The candles passed to this function should be validated:
|
|
26198
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
26199
|
+
* - Exact number of candles as requested (limit)
|
|
26200
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
26364
26201
|
*
|
|
26365
|
-
* @
|
|
26366
|
-
*
|
|
26367
|
-
*
|
|
26368
|
-
* console.log(action.handler); // Class constructor or object
|
|
26369
|
-
* console.log(action.callbacks); // Optional lifecycle callbacks
|
|
26370
|
-
* ```
|
|
26202
|
+
* @param candles - Array of validated candle data to cache
|
|
26203
|
+
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
26204
|
+
* @param exchangeName - Exchange name
|
|
26371
26205
|
*/
|
|
26372
|
-
|
|
26373
|
-
|
|
26374
|
-
|
|
26375
|
-
|
|
26376
|
-
|
|
26377
|
-
|
|
26378
|
-
|
|
26379
|
-
|
|
26380
|
-
|
|
26381
|
-
|
|
26382
|
-
|
|
26383
|
-
|
|
26384
|
-
|
|
26385
|
-
|
|
26386
|
-
|
|
26387
|
-
|
|
26388
|
-
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
26389
|
-
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
26390
|
-
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
26391
|
-
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
26206
|
+
const WRITE_CANDLES_CACHE_FN = functoolsKit.trycatch(functoolsKit.queued(async (candles, dto, exchangeName) => {
|
|
26207
|
+
await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
|
|
26208
|
+
bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
|
|
26209
|
+
}), {
|
|
26210
|
+
fallback: async (error) => {
|
|
26211
|
+
const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
|
|
26212
|
+
const payload = {
|
|
26213
|
+
error: functoolsKit.errorData(error),
|
|
26214
|
+
message: functoolsKit.getErrorMessage(error),
|
|
26215
|
+
};
|
|
26216
|
+
bt.loggerService.warn(message, payload);
|
|
26217
|
+
console.warn(message, payload);
|
|
26218
|
+
errorEmitter.next(error);
|
|
26219
|
+
},
|
|
26220
|
+
defaultValue: null,
|
|
26221
|
+
});
|
|
26392
26222
|
/**
|
|
26393
|
-
*
|
|
26394
|
-
*
|
|
26395
|
-
* Returns true when both contexts are active, which is required for calling
|
|
26396
|
-
* exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
|
|
26397
|
-
* getDate, and getMode.
|
|
26223
|
+
* Instance class for exchange operations on a specific exchange.
|
|
26398
26224
|
*
|
|
26399
|
-
*
|
|
26225
|
+
* Provides isolated exchange operations for a single exchange.
|
|
26226
|
+
* Each instance maintains its own context and exposes IExchangeSchema methods.
|
|
26227
|
+
* The schema is retrieved once during construction for better performance.
|
|
26400
26228
|
*
|
|
26401
26229
|
* @example
|
|
26402
26230
|
* ```typescript
|
|
26403
|
-
*
|
|
26404
|
-
*
|
|
26405
|
-
* if (hasTradeContext()) {
|
|
26406
|
-
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
26407
|
-
* } else {
|
|
26408
|
-
* console.log("Trade context not active");
|
|
26409
|
-
* }
|
|
26410
|
-
* ```
|
|
26411
|
-
*/
|
|
26412
|
-
function hasTradeContext() {
|
|
26413
|
-
bt.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
|
|
26414
|
-
return ExecutionContextService.hasContext() && MethodContextService.hasContext();
|
|
26415
|
-
}
|
|
26416
|
-
/**
|
|
26417
|
-
* Fetches historical candle data from the registered exchange.
|
|
26418
|
-
*
|
|
26419
|
-
* Candles are fetched backwards from the current execution context time.
|
|
26420
|
-
* Uses the exchange's getCandles implementation.
|
|
26421
|
-
*
|
|
26422
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26423
|
-
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
26424
|
-
* @param limit - Number of candles to fetch
|
|
26425
|
-
* @returns Promise resolving to array of candle data
|
|
26231
|
+
* const instance = new ExchangeInstance("binance");
|
|
26426
26232
|
*
|
|
26427
|
-
*
|
|
26428
|
-
*
|
|
26429
|
-
* const
|
|
26430
|
-
*
|
|
26233
|
+
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
26234
|
+
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
26235
|
+
* const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
26236
|
+
* const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
26431
26237
|
* ```
|
|
26432
26238
|
*/
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26239
|
+
class ExchangeInstance {
|
|
26240
|
+
/**
|
|
26241
|
+
* Creates a new ExchangeInstance for a specific exchange.
|
|
26242
|
+
*
|
|
26243
|
+
* @param exchangeName - Exchange name (e.g., "binance")
|
|
26244
|
+
*/
|
|
26245
|
+
constructor(exchangeName) {
|
|
26246
|
+
this.exchangeName = exchangeName;
|
|
26247
|
+
/**
|
|
26248
|
+
* Fetch candles from data source (API or database).
|
|
26249
|
+
*
|
|
26250
|
+
* Automatically calculates the start date based on Date.now() and the requested interval/limit.
|
|
26251
|
+
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
26252
|
+
*
|
|
26253
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26254
|
+
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
26255
|
+
* @param limit - Maximum number of candles to fetch
|
|
26256
|
+
* @returns Promise resolving to array of OHLCV candle data
|
|
26257
|
+
*
|
|
26258
|
+
* @example
|
|
26259
|
+
* ```typescript
|
|
26260
|
+
* const instance = new ExchangeInstance("binance");
|
|
26261
|
+
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
26262
|
+
* ```
|
|
26263
|
+
*/
|
|
26264
|
+
this.getCandles = async (symbol, interval, limit) => {
|
|
26265
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_CANDLES, {
|
|
26266
|
+
exchangeName: this.exchangeName,
|
|
26267
|
+
symbol,
|
|
26268
|
+
interval,
|
|
26269
|
+
limit,
|
|
26270
|
+
});
|
|
26271
|
+
const getCandles = this._methods.getCandles;
|
|
26272
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
26273
|
+
if (!step) {
|
|
26274
|
+
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
26275
|
+
}
|
|
26276
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
26277
|
+
// Align when down to interval boundary
|
|
26278
|
+
const when = await GET_TIMESTAMP_FN();
|
|
26279
|
+
const whenTimestamp = when.getTime();
|
|
26280
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
26281
|
+
// Calculate since: go back limit candles from aligned when
|
|
26282
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
26283
|
+
const since = new Date(sinceTimestamp);
|
|
26284
|
+
const untilTimestamp = alignedWhen;
|
|
26285
|
+
// Try to read from cache first
|
|
26286
|
+
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
26287
|
+
if (cachedCandles !== null) {
|
|
26288
|
+
return cachedCandles;
|
|
26289
|
+
}
|
|
26290
|
+
let allData = [];
|
|
26291
|
+
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
26292
|
+
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
26293
|
+
let remaining = limit;
|
|
26294
|
+
let currentSince = new Date(since.getTime());
|
|
26295
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26296
|
+
while (remaining > 0) {
|
|
26297
|
+
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
26298
|
+
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
26299
|
+
allData.push(...chunkData);
|
|
26300
|
+
remaining -= chunkLimit;
|
|
26301
|
+
if (remaining > 0) {
|
|
26302
|
+
// Move currentSince forward by the number of candles fetched
|
|
26303
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
26304
|
+
}
|
|
26305
|
+
}
|
|
26306
|
+
}
|
|
26307
|
+
else {
|
|
26308
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26309
|
+
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
26310
|
+
}
|
|
26311
|
+
// Apply distinct by timestamp to remove duplicates
|
|
26312
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
26313
|
+
if (allData.length !== uniqueData.length) {
|
|
26314
|
+
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
26315
|
+
}
|
|
26316
|
+
// Validate adapter returned data
|
|
26317
|
+
if (uniqueData.length === 0) {
|
|
26318
|
+
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
26319
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
26320
|
+
}
|
|
26321
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
26322
|
+
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
26323
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
26324
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
26325
|
+
}
|
|
26326
|
+
if (uniqueData.length !== limit) {
|
|
26327
|
+
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
26328
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
26329
|
+
`Adapter must return exact number of candles requested.`);
|
|
26330
|
+
}
|
|
26331
|
+
// Write to cache after successful fetch
|
|
26332
|
+
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
26333
|
+
return uniqueData;
|
|
26334
|
+
};
|
|
26335
|
+
/**
|
|
26336
|
+
* Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
|
|
26337
|
+
* The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
|
|
26338
|
+
*
|
|
26339
|
+
* Formula:
|
|
26340
|
+
* - Typical Price = (high + low + close) / 3
|
|
26341
|
+
* - VWAP = sum(typical_price * volume) / sum(volume)
|
|
26342
|
+
*
|
|
26343
|
+
* If volume is zero, returns simple average of close prices.
|
|
26344
|
+
*
|
|
26345
|
+
* @param symbol - Trading pair symbol
|
|
26346
|
+
* @returns Promise resolving to VWAP price
|
|
26347
|
+
* @throws Error if no candles available
|
|
26348
|
+
*
|
|
26349
|
+
* @example
|
|
26350
|
+
* ```typescript
|
|
26351
|
+
* const instance = new ExchangeInstance("binance");
|
|
26352
|
+
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
26353
|
+
* console.log(vwap); // 50125.43
|
|
26354
|
+
* ```
|
|
26355
|
+
*/
|
|
26356
|
+
this.getAveragePrice = async (symbol) => {
|
|
26357
|
+
bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
|
|
26358
|
+
exchangeName: this.exchangeName,
|
|
26359
|
+
symbol,
|
|
26360
|
+
});
|
|
26361
|
+
const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
26362
|
+
if (candles.length === 0) {
|
|
26363
|
+
throw new Error(`ExchangeInstance getAveragePrice: no candles data for symbol=${symbol}`);
|
|
26364
|
+
}
|
|
26365
|
+
// VWAP (Volume Weighted Average Price)
|
|
26366
|
+
// Используем типичную цену (typical price) = (high + low + close) / 3
|
|
26367
|
+
const sumPriceVolume = candles.reduce((acc, candle) => {
|
|
26368
|
+
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
|
|
26369
|
+
return acc + typicalPrice * candle.volume;
|
|
26370
|
+
}, 0);
|
|
26371
|
+
const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
|
|
26372
|
+
if (totalVolume === 0) {
|
|
26373
|
+
// Если объем нулевой, возвращаем простое среднее close цен
|
|
26374
|
+
const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
|
|
26375
|
+
return sum / candles.length;
|
|
26376
|
+
}
|
|
26377
|
+
const vwap = sumPriceVolume / totalVolume;
|
|
26378
|
+
return vwap;
|
|
26379
|
+
};
|
|
26380
|
+
/**
|
|
26381
|
+
* Format quantity according to exchange precision rules.
|
|
26382
|
+
*
|
|
26383
|
+
* @param symbol - Trading pair symbol
|
|
26384
|
+
* @param quantity - Raw quantity value
|
|
26385
|
+
* @returns Promise resolving to formatted quantity string
|
|
26386
|
+
*
|
|
26387
|
+
* @example
|
|
26388
|
+
* ```typescript
|
|
26389
|
+
* const instance = new ExchangeInstance("binance");
|
|
26390
|
+
* const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
26391
|
+
* console.log(formatted); // "0.00100000"
|
|
26392
|
+
* ```
|
|
26393
|
+
*/
|
|
26394
|
+
this.formatQuantity = async (symbol, quantity) => {
|
|
26395
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_QUANTITY, {
|
|
26396
|
+
exchangeName: this.exchangeName,
|
|
26397
|
+
symbol,
|
|
26398
|
+
quantity,
|
|
26399
|
+
});
|
|
26400
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26401
|
+
return await this._methods.formatQuantity(symbol, quantity, isBacktest);
|
|
26402
|
+
};
|
|
26403
|
+
/**
|
|
26404
|
+
* Format price according to exchange precision rules.
|
|
26405
|
+
*
|
|
26406
|
+
* @param symbol - Trading pair symbol
|
|
26407
|
+
* @param price - Raw price value
|
|
26408
|
+
* @returns Promise resolving to formatted price string
|
|
26409
|
+
*
|
|
26410
|
+
* @example
|
|
26411
|
+
* ```typescript
|
|
26412
|
+
* const instance = new ExchangeInstance("binance");
|
|
26413
|
+
* const formatted = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
26414
|
+
* console.log(formatted); // "50000.12"
|
|
26415
|
+
* ```
|
|
26416
|
+
*/
|
|
26417
|
+
this.formatPrice = async (symbol, price) => {
|
|
26418
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_PRICE, {
|
|
26419
|
+
exchangeName: this.exchangeName,
|
|
26420
|
+
symbol,
|
|
26421
|
+
price,
|
|
26422
|
+
});
|
|
26423
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26424
|
+
return await this._methods.formatPrice(symbol, price, isBacktest);
|
|
26425
|
+
};
|
|
26426
|
+
/**
|
|
26427
|
+
* Fetch order book for a trading pair.
|
|
26428
|
+
*
|
|
26429
|
+
* Calculates time range using CC_ORDER_BOOK_TIME_OFFSET_MINUTES (default 10 minutes)
|
|
26430
|
+
* and passes it to the exchange schema implementation. The implementation may use
|
|
26431
|
+
* the time range (backtest) or ignore it (live trading).
|
|
26432
|
+
*
|
|
26433
|
+
* @param symbol - Trading pair symbol
|
|
26434
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
26435
|
+
* @returns Promise resolving to order book data
|
|
26436
|
+
* @throws Error if getOrderBook is not implemented
|
|
26437
|
+
*
|
|
26438
|
+
* @example
|
|
26439
|
+
* ```typescript
|
|
26440
|
+
* const instance = new ExchangeInstance("binance");
|
|
26441
|
+
* const orderBook = await instance.getOrderBook("BTCUSDT");
|
|
26442
|
+
* console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
|
|
26443
|
+
* const deepOrderBook = await instance.getOrderBook("BTCUSDT", 100);
|
|
26444
|
+
* ```
|
|
26445
|
+
*/
|
|
26446
|
+
this.getOrderBook = async (symbol, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
|
|
26447
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_ORDER_BOOK, {
|
|
26448
|
+
exchangeName: this.exchangeName,
|
|
26449
|
+
symbol,
|
|
26450
|
+
depth,
|
|
26451
|
+
});
|
|
26452
|
+
const when = await GET_TIMESTAMP_FN();
|
|
26453
|
+
const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
|
|
26454
|
+
const to = new Date(alignedTo);
|
|
26455
|
+
const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
|
|
26456
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26457
|
+
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
26458
|
+
};
|
|
26459
|
+
/**
|
|
26460
|
+
* Fetches raw candles with flexible date/limit parameters.
|
|
26461
|
+
*
|
|
26462
|
+
* Uses Date.now() instead of execution context when for look-ahead bias protection.
|
|
26463
|
+
*
|
|
26464
|
+
* Parameter combinations:
|
|
26465
|
+
* 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
|
|
26466
|
+
* 2. sDate + eDate: calculates limit from date range, validates eDate <= now
|
|
26467
|
+
* 3. eDate + limit: calculates sDate backward, validates eDate <= now
|
|
26468
|
+
* 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
|
|
26469
|
+
* 5. Only limit: uses Date.now() as reference (backward)
|
|
26470
|
+
*
|
|
26471
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26472
|
+
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
26473
|
+
* @param limit - Optional number of candles to fetch
|
|
26474
|
+
* @param sDate - Optional start date in milliseconds
|
|
26475
|
+
* @param eDate - Optional end date in milliseconds
|
|
26476
|
+
* @returns Promise resolving to array of candle data
|
|
26477
|
+
*
|
|
26478
|
+
* @example
|
|
26479
|
+
* ```typescript
|
|
26480
|
+
* const instance = new ExchangeInstance("binance");
|
|
26481
|
+
*
|
|
26482
|
+
* // Fetch 100 candles backward from now
|
|
26483
|
+
* const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
|
|
26484
|
+
*
|
|
26485
|
+
* // Fetch candles for specific date range
|
|
26486
|
+
* const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
|
|
26487
|
+
* ```
|
|
26488
|
+
*/
|
|
26489
|
+
this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
|
|
26490
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
|
|
26491
|
+
exchangeName: this.exchangeName,
|
|
26492
|
+
symbol,
|
|
26493
|
+
interval,
|
|
26494
|
+
limit,
|
|
26495
|
+
sDate,
|
|
26496
|
+
eDate,
|
|
26497
|
+
});
|
|
26498
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
26499
|
+
if (!step) {
|
|
26500
|
+
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
26501
|
+
}
|
|
26502
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
26503
|
+
const when = await GET_TIMESTAMP_FN();
|
|
26504
|
+
const nowTimestamp = when.getTime();
|
|
26505
|
+
const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
|
|
26506
|
+
let sinceTimestamp;
|
|
26507
|
+
let calculatedLimit;
|
|
26508
|
+
// Case 1: all three parameters provided
|
|
26509
|
+
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
26510
|
+
if (sDate >= eDate) {
|
|
26511
|
+
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
26512
|
+
}
|
|
26513
|
+
if (eDate > nowTimestamp) {
|
|
26514
|
+
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26515
|
+
}
|
|
26516
|
+
// Align sDate down to interval boundary
|
|
26517
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
26518
|
+
calculatedLimit = limit;
|
|
26519
|
+
}
|
|
26520
|
+
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
26521
|
+
else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
|
|
26522
|
+
if (sDate >= eDate) {
|
|
26523
|
+
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
26524
|
+
}
|
|
26525
|
+
if (eDate > nowTimestamp) {
|
|
26526
|
+
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26527
|
+
}
|
|
26528
|
+
// Align sDate down to interval boundary
|
|
26529
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
26530
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
26531
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
26532
|
+
if (calculatedLimit <= 0) {
|
|
26533
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
26534
|
+
}
|
|
26535
|
+
}
|
|
26536
|
+
// Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
|
|
26537
|
+
else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
|
|
26538
|
+
if (eDate > nowTimestamp) {
|
|
26539
|
+
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26540
|
+
}
|
|
26541
|
+
// Align eDate down and calculate sinceTimestamp
|
|
26542
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
26543
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
26544
|
+
calculatedLimit = limit;
|
|
26545
|
+
}
|
|
26546
|
+
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
26547
|
+
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
26548
|
+
// Align sDate down to interval boundary
|
|
26549
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
26550
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
26551
|
+
if (endTimestamp > nowTimestamp) {
|
|
26552
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26553
|
+
}
|
|
26554
|
+
calculatedLimit = limit;
|
|
26555
|
+
}
|
|
26556
|
+
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
26557
|
+
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
26558
|
+
sinceTimestamp = alignedNow - limit * stepMs;
|
|
26559
|
+
calculatedLimit = limit;
|
|
26560
|
+
}
|
|
26561
|
+
// Invalid: no parameters or only sDate or only eDate
|
|
26562
|
+
else {
|
|
26563
|
+
throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
|
|
26564
|
+
`Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
|
|
26565
|
+
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
26566
|
+
}
|
|
26567
|
+
// Try to read from cache first
|
|
26568
|
+
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
26569
|
+
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
26570
|
+
if (cachedCandles !== null) {
|
|
26571
|
+
return cachedCandles;
|
|
26572
|
+
}
|
|
26573
|
+
// Fetch candles
|
|
26574
|
+
const since = new Date(sinceTimestamp);
|
|
26575
|
+
let allData = [];
|
|
26576
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26577
|
+
const getCandles = this._methods.getCandles;
|
|
26578
|
+
if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
26579
|
+
let remaining = calculatedLimit;
|
|
26580
|
+
let currentSince = new Date(since.getTime());
|
|
26581
|
+
while (remaining > 0) {
|
|
26582
|
+
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
26583
|
+
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
26584
|
+
allData.push(...chunkData);
|
|
26585
|
+
remaining -= chunkLimit;
|
|
26586
|
+
if (remaining > 0) {
|
|
26587
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
26588
|
+
}
|
|
26589
|
+
}
|
|
26590
|
+
}
|
|
26591
|
+
else {
|
|
26592
|
+
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
26593
|
+
}
|
|
26594
|
+
// Apply distinct by timestamp to remove duplicates
|
|
26595
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
26596
|
+
if (allData.length !== uniqueData.length) {
|
|
26597
|
+
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
26598
|
+
}
|
|
26599
|
+
// Validate adapter returned data
|
|
26600
|
+
if (uniqueData.length === 0) {
|
|
26601
|
+
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
26602
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
26603
|
+
}
|
|
26604
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
26605
|
+
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
26606
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
26607
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
26608
|
+
}
|
|
26609
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
26610
|
+
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
26611
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
26612
|
+
`Adapter must return exact number of candles requested.`);
|
|
26613
|
+
}
|
|
26614
|
+
// Write to cache after successful fetch
|
|
26615
|
+
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
26616
|
+
return uniqueData;
|
|
26617
|
+
};
|
|
26618
|
+
const schema = bt.exchangeSchemaService.get(this.exchangeName);
|
|
26619
|
+
this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
|
|
26620
|
+
}
|
|
26621
|
+
}
|
|
26622
|
+
/**
|
|
26623
|
+
* Utility class for exchange operations.
|
|
26624
|
+
*
|
|
26625
|
+
* Provides simplified access to exchange schema methods with validation.
|
|
26626
|
+
* Exported as singleton instance for convenient usage.
|
|
26627
|
+
*
|
|
26628
|
+
* @example
|
|
26629
|
+
* ```typescript
|
|
26630
|
+
* import { Exchange } from "./classes/Exchange";
|
|
26631
|
+
*
|
|
26632
|
+
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
26633
|
+
* exchangeName: "binance"
|
|
26634
|
+
* });
|
|
26635
|
+
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
26636
|
+
* exchangeName: "binance"
|
|
26637
|
+
* });
|
|
26638
|
+
* const formatted = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
26639
|
+
* exchangeName: "binance"
|
|
26640
|
+
* });
|
|
26641
|
+
* ```
|
|
26642
|
+
*/
|
|
26643
|
+
class ExchangeUtils {
|
|
26644
|
+
constructor() {
|
|
26645
|
+
/**
|
|
26646
|
+
* Memoized function to get or create ExchangeInstance for an exchange.
|
|
26647
|
+
* Each exchange gets its own isolated instance.
|
|
26648
|
+
*/
|
|
26649
|
+
this._getInstance = functoolsKit.memoize(([exchangeName]) => exchangeName, (exchangeName) => new ExchangeInstance(exchangeName));
|
|
26650
|
+
/**
|
|
26651
|
+
* Fetch candles from data source (API or database).
|
|
26652
|
+
*
|
|
26653
|
+
* Automatically calculates the start date based on Date.now() and the requested interval/limit.
|
|
26654
|
+
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
26655
|
+
*
|
|
26656
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26657
|
+
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
26658
|
+
* @param limit - Maximum number of candles to fetch
|
|
26659
|
+
* @param context - Execution context with exchange name
|
|
26660
|
+
* @returns Promise resolving to array of OHLCV candle data
|
|
26661
|
+
*/
|
|
26662
|
+
this.getCandles = async (symbol, interval, limit, context) => {
|
|
26663
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_CANDLES);
|
|
26664
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26665
|
+
return await instance.getCandles(symbol, interval, limit);
|
|
26666
|
+
};
|
|
26667
|
+
/**
|
|
26668
|
+
* Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
|
|
26669
|
+
*
|
|
26670
|
+
* @param symbol - Trading pair symbol
|
|
26671
|
+
* @param context - Execution context with exchange name
|
|
26672
|
+
* @returns Promise resolving to VWAP price
|
|
26673
|
+
*/
|
|
26674
|
+
this.getAveragePrice = async (symbol, context) => {
|
|
26675
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE);
|
|
26676
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26677
|
+
return await instance.getAveragePrice(symbol);
|
|
26678
|
+
};
|
|
26679
|
+
/**
|
|
26680
|
+
* Format quantity according to exchange precision rules.
|
|
26681
|
+
*
|
|
26682
|
+
* @param symbol - Trading pair symbol
|
|
26683
|
+
* @param quantity - Raw quantity value
|
|
26684
|
+
* @param context - Execution context with exchange name
|
|
26685
|
+
* @returns Promise resolving to formatted quantity string
|
|
26686
|
+
*/
|
|
26687
|
+
this.formatQuantity = async (symbol, quantity, context) => {
|
|
26688
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_QUANTITY);
|
|
26689
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26690
|
+
return await instance.formatQuantity(symbol, quantity);
|
|
26691
|
+
};
|
|
26692
|
+
/**
|
|
26693
|
+
* Format price according to exchange precision rules.
|
|
26694
|
+
*
|
|
26695
|
+
* @param symbol - Trading pair symbol
|
|
26696
|
+
* @param price - Raw price value
|
|
26697
|
+
* @param context - Execution context with exchange name
|
|
26698
|
+
* @returns Promise resolving to formatted price string
|
|
26699
|
+
*/
|
|
26700
|
+
this.formatPrice = async (symbol, price, context) => {
|
|
26701
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_PRICE);
|
|
26702
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26703
|
+
return await instance.formatPrice(symbol, price);
|
|
26704
|
+
};
|
|
26705
|
+
/**
|
|
26706
|
+
* Fetch order book for a trading pair.
|
|
26707
|
+
*
|
|
26708
|
+
* Delegates to ExchangeInstance which calculates time range and passes it
|
|
26709
|
+
* to the exchange schema implementation. The from/to parameters may be used
|
|
26710
|
+
* (backtest) or ignored (live) depending on the implementation.
|
|
26711
|
+
*
|
|
26712
|
+
* @param symbol - Trading pair symbol
|
|
26713
|
+
* @param context - Execution context with exchange name
|
|
26714
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
26715
|
+
* @returns Promise resolving to order book data
|
|
26716
|
+
*/
|
|
26717
|
+
this.getOrderBook = async (symbol, context, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
|
|
26718
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_ORDER_BOOK);
|
|
26719
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26720
|
+
return await instance.getOrderBook(symbol, depth);
|
|
26721
|
+
};
|
|
26722
|
+
/**
|
|
26723
|
+
* Fetches raw candles with flexible date/limit parameters.
|
|
26724
|
+
*
|
|
26725
|
+
* Uses Date.now() instead of execution context when for look-ahead bias protection.
|
|
26726
|
+
*
|
|
26727
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26728
|
+
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
26729
|
+
* @param context - Execution context with exchange name
|
|
26730
|
+
* @param limit - Optional number of candles to fetch
|
|
26731
|
+
* @param sDate - Optional start date in milliseconds
|
|
26732
|
+
* @param eDate - Optional end date in milliseconds
|
|
26733
|
+
* @returns Promise resolving to array of candle data
|
|
26734
|
+
*/
|
|
26735
|
+
this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
|
|
26736
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
|
|
26737
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26738
|
+
return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
|
|
26739
|
+
};
|
|
26740
|
+
}
|
|
26741
|
+
}
|
|
26742
|
+
/**
|
|
26743
|
+
* Singleton instance of ExchangeUtils for convenient exchange operations.
|
|
26744
|
+
*
|
|
26745
|
+
* @example
|
|
26746
|
+
* ```typescript
|
|
26747
|
+
* import { Exchange } from "./classes/Exchange";
|
|
26748
|
+
*
|
|
26749
|
+
* // Using static-like API with context
|
|
26750
|
+
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
26751
|
+
* exchangeName: "binance"
|
|
26752
|
+
* });
|
|
26753
|
+
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
26754
|
+
* exchangeName: "binance"
|
|
26755
|
+
* });
|
|
26756
|
+
* const qty = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
26757
|
+
* exchangeName: "binance"
|
|
26758
|
+
* });
|
|
26759
|
+
* const price = await Exchange.formatPrice("BTCUSDT", 50000.123, {
|
|
26760
|
+
* exchangeName: "binance"
|
|
26761
|
+
* });
|
|
26762
|
+
*
|
|
26763
|
+
* // Using instance API (no context needed, exchange set in constructor)
|
|
26764
|
+
* const binance = new ExchangeInstance("binance");
|
|
26765
|
+
* const candles2 = await binance.getCandles("BTCUSDT", "1m", 100);
|
|
26766
|
+
* const vwap2 = await binance.getAveragePrice("BTCUSDT");
|
|
26767
|
+
* ```
|
|
26768
|
+
*/
|
|
26769
|
+
const Exchange = new ExchangeUtils();
|
|
26770
|
+
|
|
26771
|
+
const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
|
|
26772
|
+
const MS_PER_MINUTE$1 = 60000;
|
|
26773
|
+
const INTERVAL_MINUTES$1 = {
|
|
26774
|
+
"1m": 1,
|
|
26775
|
+
"3m": 3,
|
|
26776
|
+
"5m": 5,
|
|
26777
|
+
"15m": 15,
|
|
26778
|
+
"30m": 30,
|
|
26779
|
+
"1h": 60,
|
|
26780
|
+
"2h": 120,
|
|
26781
|
+
"4h": 240,
|
|
26782
|
+
"6h": 360,
|
|
26783
|
+
"8h": 480,
|
|
26784
|
+
};
|
|
26785
|
+
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
26786
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
26787
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
26788
|
+
};
|
|
26789
|
+
const BAR_LENGTH = 30;
|
|
26790
|
+
const BAR_FILLED_CHAR = "\u2588";
|
|
26791
|
+
const BAR_EMPTY_CHAR = "\u2591";
|
|
26792
|
+
const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
|
|
26793
|
+
const percent = Math.round((fetched / total) * 100);
|
|
26794
|
+
const filled = Math.round((fetched / total) * BAR_LENGTH);
|
|
26795
|
+
const empty = BAR_LENGTH - filled;
|
|
26796
|
+
const bar = BAR_FILLED_CHAR.repeat(filled) + BAR_EMPTY_CHAR.repeat(empty);
|
|
26797
|
+
process.stdout.write(`\r[${bar}] ${percent}% (${fetched}/${total}) ${symbol} ${interval}`);
|
|
26798
|
+
if (fetched === total) {
|
|
26799
|
+
process.stdout.write("\n");
|
|
26800
|
+
}
|
|
26801
|
+
};
|
|
26802
|
+
/**
|
|
26803
|
+
* Pre-caches candles for a date range into persist storage.
|
|
26804
|
+
* Downloads all candles matching the interval from `from` to `to`.
|
|
26805
|
+
*
|
|
26806
|
+
* @param params - Cache parameters
|
|
26807
|
+
*/
|
|
26808
|
+
async function warmCandles(params) {
|
|
26809
|
+
const { symbol, exchangeName, interval, from, to } = params;
|
|
26810
|
+
bt.loggerService.info(WARM_CANDLES_METHOD_NAME, {
|
|
26811
|
+
symbol,
|
|
26812
|
+
exchangeName,
|
|
26813
|
+
interval,
|
|
26814
|
+
from,
|
|
26815
|
+
to,
|
|
26816
|
+
});
|
|
26817
|
+
const step = INTERVAL_MINUTES$1[interval];
|
|
26818
|
+
if (!step) {
|
|
26819
|
+
throw new Error(`warmCandles: unsupported interval=${interval}`);
|
|
26820
|
+
}
|
|
26821
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
26822
|
+
const instance = new ExchangeInstance(exchangeName);
|
|
26823
|
+
const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
26824
|
+
const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
26825
|
+
const totalCandles = Math.ceil((untilTimestamp - sinceTimestamp) / stepMs);
|
|
26826
|
+
if (totalCandles <= 0) {
|
|
26827
|
+
throw new Error(`warmCandles: no candles to cache (from >= to after alignment)`);
|
|
26828
|
+
}
|
|
26829
|
+
let fetched = 0;
|
|
26830
|
+
let currentSince = sinceTimestamp;
|
|
26831
|
+
PRINT_PROGRESS_FN(fetched, totalCandles, symbol, interval);
|
|
26832
|
+
while (fetched < totalCandles) {
|
|
26833
|
+
const chunkLimit = Math.min(totalCandles - fetched, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
26834
|
+
await instance.getRawCandles(symbol, interval, chunkLimit, currentSince);
|
|
26835
|
+
fetched += chunkLimit;
|
|
26836
|
+
currentSince += chunkLimit * stepMs;
|
|
26837
|
+
PRINT_PROGRESS_FN(fetched, totalCandles, symbol, interval);
|
|
26838
|
+
}
|
|
26839
|
+
}
|
|
26840
|
+
|
|
26841
|
+
const METHOD_NAME = "validate.validate";
|
|
26842
|
+
/**
|
|
26843
|
+
* Retrieves all registered exchanges as a map
|
|
26844
|
+
* @private
|
|
26845
|
+
* @returns Map of exchange names
|
|
26846
|
+
*/
|
|
26847
|
+
const getExchangeMap = async () => {
|
|
26848
|
+
const exchangeMap = {};
|
|
26849
|
+
for (const { exchangeName } of await bt.exchangeValidationService.list()) {
|
|
26850
|
+
Object.assign(exchangeMap, { [exchangeName]: exchangeName });
|
|
26851
|
+
}
|
|
26852
|
+
return exchangeMap;
|
|
26853
|
+
};
|
|
26854
|
+
/**
|
|
26855
|
+
* Retrieves all registered frames as a map
|
|
26856
|
+
* @private
|
|
26857
|
+
* @returns Map of frame names
|
|
26858
|
+
*/
|
|
26859
|
+
const getFrameMap = async () => {
|
|
26860
|
+
const frameMap = {};
|
|
26861
|
+
for (const { frameName } of await bt.frameValidationService.list()) {
|
|
26862
|
+
Object.assign(frameMap, { [frameName]: frameName });
|
|
26863
|
+
}
|
|
26864
|
+
return frameMap;
|
|
26865
|
+
};
|
|
26866
|
+
/**
|
|
26867
|
+
* Retrieves all registered strategies as a map
|
|
26868
|
+
* @private
|
|
26869
|
+
* @returns Map of strategy names
|
|
26870
|
+
*/
|
|
26871
|
+
const getStrategyMap = async () => {
|
|
26872
|
+
const strategyMap = {};
|
|
26873
|
+
for (const { strategyName } of await bt.strategyValidationService.list()) {
|
|
26874
|
+
Object.assign(strategyMap, { [strategyName]: strategyName });
|
|
26875
|
+
}
|
|
26876
|
+
return strategyMap;
|
|
26877
|
+
};
|
|
26878
|
+
/**
|
|
26879
|
+
* Retrieves all registered risk profiles as a map
|
|
26880
|
+
* @private
|
|
26881
|
+
* @returns Map of risk names
|
|
26882
|
+
*/
|
|
26883
|
+
const getRiskMap = async () => {
|
|
26884
|
+
const riskMap = {};
|
|
26885
|
+
for (const { riskName } of await bt.riskValidationService.list()) {
|
|
26886
|
+
Object.assign(riskMap, { [riskName]: riskName });
|
|
26887
|
+
}
|
|
26888
|
+
return riskMap;
|
|
26889
|
+
};
|
|
26890
|
+
/**
|
|
26891
|
+
* Retrieves all registered action handlers as a map
|
|
26892
|
+
* @private
|
|
26893
|
+
* @returns Map of action names
|
|
26894
|
+
*/
|
|
26895
|
+
const getActionMap = async () => {
|
|
26896
|
+
const actionMap = {};
|
|
26897
|
+
for (const { actionName } of await bt.actionValidationService.list()) {
|
|
26898
|
+
Object.assign(actionMap, { [actionName]: actionName });
|
|
26899
|
+
}
|
|
26900
|
+
return actionMap;
|
|
26901
|
+
};
|
|
26902
|
+
/**
|
|
26903
|
+
* Retrieves all registered sizing strategies as a map
|
|
26904
|
+
* @private
|
|
26905
|
+
* @returns Map of sizing names
|
|
26906
|
+
*/
|
|
26907
|
+
const getSizingMap = async () => {
|
|
26908
|
+
const sizingMap = {};
|
|
26909
|
+
for (const { sizingName } of await bt.sizingValidationService.list()) {
|
|
26910
|
+
Object.assign(sizingMap, { [sizingName]: sizingName });
|
|
26911
|
+
}
|
|
26912
|
+
return sizingMap;
|
|
26913
|
+
};
|
|
26914
|
+
/**
|
|
26915
|
+
* Retrieves all registered walkers as a map
|
|
26916
|
+
* @private
|
|
26917
|
+
* @returns Map of walker names
|
|
26918
|
+
*/
|
|
26919
|
+
const getWalkerMap = async () => {
|
|
26920
|
+
const walkerMap = {};
|
|
26921
|
+
for (const { walkerName } of await bt.walkerValidationService.list()) {
|
|
26922
|
+
Object.assign(walkerMap, { [walkerName]: walkerName });
|
|
26923
|
+
}
|
|
26924
|
+
return walkerMap;
|
|
26925
|
+
};
|
|
26926
|
+
/**
|
|
26927
|
+
* Internal validation function that processes all provided entity enums.
|
|
26928
|
+
*
|
|
26929
|
+
* Iterates through each enum's values and validates them against their
|
|
26930
|
+
* respective validation services. Uses memoized validation for performance.
|
|
26931
|
+
*
|
|
26932
|
+
* If entity enums are not provided, fetches all registered entities from
|
|
26933
|
+
* their respective validation services and validates them.
|
|
26934
|
+
*
|
|
26935
|
+
* @private
|
|
26936
|
+
* @param args - Validation arguments containing entity name enums
|
|
26937
|
+
* @throws {Error} If any entity name is not found in its registry
|
|
26938
|
+
*/
|
|
26939
|
+
const validateInternal = async (args) => {
|
|
26940
|
+
const { ExchangeName = await getExchangeMap(), FrameName = await getFrameMap(), StrategyName = await getStrategyMap(), RiskName = await getRiskMap(), ActionName = await getActionMap(), SizingName = await getSizingMap(), WalkerName = await getWalkerMap(), } = args;
|
|
26941
|
+
for (const exchangeName of Object.values(ExchangeName)) {
|
|
26942
|
+
bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
|
|
26943
|
+
}
|
|
26944
|
+
for (const frameName of Object.values(FrameName)) {
|
|
26945
|
+
bt.frameValidationService.validate(frameName, METHOD_NAME);
|
|
26946
|
+
}
|
|
26947
|
+
for (const strategyName of Object.values(StrategyName)) {
|
|
26948
|
+
bt.strategyValidationService.validate(strategyName, METHOD_NAME);
|
|
26949
|
+
}
|
|
26950
|
+
for (const riskName of Object.values(RiskName)) {
|
|
26951
|
+
bt.riskValidationService.validate(riskName, METHOD_NAME);
|
|
26952
|
+
}
|
|
26953
|
+
for (const actionName of Object.values(ActionName)) {
|
|
26954
|
+
bt.actionValidationService.validate(actionName, METHOD_NAME);
|
|
26955
|
+
}
|
|
26956
|
+
for (const sizingName of Object.values(SizingName)) {
|
|
26957
|
+
bt.sizingValidationService.validate(sizingName, METHOD_NAME);
|
|
26958
|
+
}
|
|
26959
|
+
for (const walkerName of Object.values(WalkerName)) {
|
|
26960
|
+
bt.walkerValidationService.validate(walkerName, METHOD_NAME);
|
|
26961
|
+
}
|
|
26962
|
+
};
|
|
26963
|
+
/**
|
|
26964
|
+
* Validates the existence of all provided entity names across validation services.
|
|
26965
|
+
*
|
|
26966
|
+
* This function accepts enum objects for various entity types (exchanges, frames,
|
|
26967
|
+
* strategies, risks, sizings, walkers) and validates that each entity
|
|
26968
|
+
* name exists in its respective registry. Validation results are memoized for performance.
|
|
26969
|
+
*
|
|
26970
|
+
* If no arguments are provided (or specific entity types are omitted), the function
|
|
26971
|
+
* automatically fetches and validates ALL registered entities from their respective
|
|
26972
|
+
* validation services. This is useful for comprehensive validation of the entire setup.
|
|
26973
|
+
*
|
|
26974
|
+
* Use this before running backtests or optimizations to ensure all referenced
|
|
26975
|
+
* entities are properly registered and configured.
|
|
26976
|
+
*
|
|
26977
|
+
* @public
|
|
26978
|
+
* @param args - Partial validation arguments containing entity name enums to validate.
|
|
26979
|
+
* If empty or omitted, validates all registered entities.
|
|
26980
|
+
* @throws {Error} If any entity name is not found in its validation service
|
|
26981
|
+
*
|
|
26982
|
+
* @example
|
|
26983
|
+
* ```typescript
|
|
26984
|
+
* // Validate ALL registered entities (exchanges, frames, strategies, etc.)
|
|
26985
|
+
* await validate({});
|
|
26986
|
+
* ```
|
|
26987
|
+
*
|
|
26988
|
+
* @example
|
|
26989
|
+
* ```typescript
|
|
26990
|
+
* // Define your entity name enums
|
|
26991
|
+
* enum ExchangeName {
|
|
26992
|
+
* BINANCE = "binance",
|
|
26993
|
+
* BYBIT = "bybit"
|
|
26994
|
+
* }
|
|
26995
|
+
*
|
|
26996
|
+
* enum StrategyName {
|
|
26997
|
+
* MOMENTUM_BTC = "momentum-btc"
|
|
26998
|
+
* }
|
|
26999
|
+
*
|
|
27000
|
+
* // Validate specific entities before running backtest
|
|
27001
|
+
* await validate({
|
|
27002
|
+
* ExchangeName,
|
|
27003
|
+
* StrategyName,
|
|
27004
|
+
* });
|
|
27005
|
+
* ```
|
|
27006
|
+
*
|
|
27007
|
+
* @example
|
|
27008
|
+
* ```typescript
|
|
27009
|
+
* // Validate specific entity types
|
|
27010
|
+
* await validate({
|
|
27011
|
+
* RiskName: { CONSERVATIVE: "conservative" },
|
|
27012
|
+
* SizingName: { FIXED_1000: "fixed-1000" },
|
|
27013
|
+
* });
|
|
27014
|
+
* ```
|
|
27015
|
+
*/
|
|
27016
|
+
async function validate(args = {}) {
|
|
27017
|
+
bt.loggerService.log(METHOD_NAME);
|
|
27018
|
+
return await validateInternal(args);
|
|
27019
|
+
}
|
|
27020
|
+
|
|
27021
|
+
const GET_STRATEGY_METHOD_NAME = "get.getStrategySchema";
|
|
27022
|
+
const GET_EXCHANGE_METHOD_NAME = "get.getExchangeSchema";
|
|
27023
|
+
const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
|
|
27024
|
+
const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
|
|
27025
|
+
const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
|
|
27026
|
+
const GET_RISK_METHOD_NAME = "get.getRiskSchema";
|
|
27027
|
+
const GET_ACTION_METHOD_NAME = "get.getActionSchema";
|
|
27028
|
+
/**
|
|
27029
|
+
* Retrieves a registered strategy schema by name.
|
|
27030
|
+
*
|
|
27031
|
+
* @param strategyName - Unique strategy identifier
|
|
27032
|
+
* @returns The strategy schema configuration object
|
|
27033
|
+
* @throws Error if strategy is not registered
|
|
27034
|
+
*
|
|
27035
|
+
* @example
|
|
27036
|
+
* ```typescript
|
|
27037
|
+
* const strategy = getStrategy("my-strategy");
|
|
27038
|
+
* console.log(strategy.interval); // "5m"
|
|
27039
|
+
* console.log(strategy.getSignal); // async function
|
|
27040
|
+
* ```
|
|
27041
|
+
*/
|
|
27042
|
+
function getStrategySchema(strategyName) {
|
|
27043
|
+
bt.loggerService.log(GET_STRATEGY_METHOD_NAME, {
|
|
27044
|
+
strategyName,
|
|
27045
|
+
});
|
|
27046
|
+
bt.strategyValidationService.validate(strategyName, GET_STRATEGY_METHOD_NAME);
|
|
27047
|
+
return bt.strategySchemaService.get(strategyName);
|
|
27048
|
+
}
|
|
27049
|
+
/**
|
|
27050
|
+
* Retrieves a registered exchange schema by name.
|
|
27051
|
+
*
|
|
27052
|
+
* @param exchangeName - Unique exchange identifier
|
|
27053
|
+
* @returns The exchange schema configuration object
|
|
27054
|
+
* @throws Error if exchange is not registered
|
|
27055
|
+
*
|
|
27056
|
+
* @example
|
|
27057
|
+
* ```typescript
|
|
27058
|
+
* const exchange = getExchange("binance");
|
|
27059
|
+
* console.log(exchange.getCandles); // async function
|
|
27060
|
+
* console.log(exchange.formatPrice); // async function
|
|
27061
|
+
* ```
|
|
27062
|
+
*/
|
|
27063
|
+
function getExchangeSchema(exchangeName) {
|
|
27064
|
+
bt.loggerService.log(GET_EXCHANGE_METHOD_NAME, {
|
|
27065
|
+
exchangeName,
|
|
27066
|
+
});
|
|
27067
|
+
bt.exchangeValidationService.validate(exchangeName, GET_EXCHANGE_METHOD_NAME);
|
|
27068
|
+
return bt.exchangeSchemaService.get(exchangeName);
|
|
27069
|
+
}
|
|
27070
|
+
/**
|
|
27071
|
+
* Retrieves a registered frame schema by name.
|
|
27072
|
+
*
|
|
27073
|
+
* @param frameName - Unique frame identifier
|
|
27074
|
+
* @returns The frame schema configuration object
|
|
27075
|
+
* @throws Error if frame is not registered
|
|
27076
|
+
*
|
|
27077
|
+
* @example
|
|
27078
|
+
* ```typescript
|
|
27079
|
+
* const frame = getFrame("1d-backtest");
|
|
27080
|
+
* console.log(frame.interval); // "1m"
|
|
27081
|
+
* console.log(frame.startDate); // Date object
|
|
27082
|
+
* console.log(frame.endDate); // Date object
|
|
27083
|
+
* ```
|
|
27084
|
+
*/
|
|
27085
|
+
function getFrameSchema(frameName) {
|
|
27086
|
+
bt.loggerService.log(GET_FRAME_METHOD_NAME, {
|
|
27087
|
+
frameName,
|
|
27088
|
+
});
|
|
27089
|
+
bt.frameValidationService.validate(frameName, GET_FRAME_METHOD_NAME);
|
|
27090
|
+
return bt.frameSchemaService.get(frameName);
|
|
27091
|
+
}
|
|
27092
|
+
/**
|
|
27093
|
+
* Retrieves a registered walker schema by name.
|
|
27094
|
+
*
|
|
27095
|
+
* @param walkerName - Unique walker identifier
|
|
27096
|
+
* @returns The walker schema configuration object
|
|
27097
|
+
* @throws Error if walker is not registered
|
|
27098
|
+
*
|
|
27099
|
+
* @example
|
|
27100
|
+
* ```typescript
|
|
27101
|
+
* const walker = getWalker("llm-prompt-optimizer");
|
|
27102
|
+
* console.log(walker.exchangeName); // "binance"
|
|
27103
|
+
* console.log(walker.frameName); // "1d-backtest"
|
|
27104
|
+
* console.log(walker.strategies); // ["my-strategy-v1", "my-strategy-v2"]
|
|
27105
|
+
* console.log(walker.metric); // "sharpeRatio"
|
|
27106
|
+
* ```
|
|
27107
|
+
*/
|
|
27108
|
+
function getWalkerSchema(walkerName) {
|
|
27109
|
+
bt.loggerService.log(GET_WALKER_METHOD_NAME, {
|
|
27110
|
+
walkerName,
|
|
27111
|
+
});
|
|
27112
|
+
bt.walkerValidationService.validate(walkerName, GET_WALKER_METHOD_NAME);
|
|
27113
|
+
return bt.walkerSchemaService.get(walkerName);
|
|
27114
|
+
}
|
|
27115
|
+
/**
|
|
27116
|
+
* Retrieves a registered sizing schema by name.
|
|
27117
|
+
*
|
|
27118
|
+
* @param sizingName - Unique sizing identifier
|
|
27119
|
+
* @returns The sizing schema configuration object
|
|
27120
|
+
* @throws Error if sizing is not registered
|
|
27121
|
+
*
|
|
27122
|
+
* @example
|
|
27123
|
+
* ```typescript
|
|
27124
|
+
* const sizing = getSizing("conservative");
|
|
27125
|
+
* console.log(sizing.method); // "fixed-percentage"
|
|
27126
|
+
* console.log(sizing.riskPercentage); // 1
|
|
27127
|
+
* console.log(sizing.maxPositionPercentage); // 10
|
|
27128
|
+
* ```
|
|
27129
|
+
*/
|
|
27130
|
+
function getSizingSchema(sizingName) {
|
|
27131
|
+
bt.loggerService.log(GET_SIZING_METHOD_NAME, {
|
|
27132
|
+
sizingName,
|
|
27133
|
+
});
|
|
27134
|
+
bt.sizingValidationService.validate(sizingName, GET_SIZING_METHOD_NAME);
|
|
27135
|
+
return bt.sizingSchemaService.get(sizingName);
|
|
27136
|
+
}
|
|
27137
|
+
/**
|
|
27138
|
+
* Retrieves a registered risk schema by name.
|
|
27139
|
+
*
|
|
27140
|
+
* @param riskName - Unique risk identifier
|
|
27141
|
+
* @returns The risk schema configuration object
|
|
27142
|
+
* @throws Error if risk is not registered
|
|
27143
|
+
*
|
|
27144
|
+
* @example
|
|
27145
|
+
* ```typescript
|
|
27146
|
+
* const risk = getRisk("conservative");
|
|
27147
|
+
* console.log(risk.maxConcurrentPositions); // 5
|
|
27148
|
+
* console.log(risk.validations); // Array of validation functions
|
|
27149
|
+
* ```
|
|
27150
|
+
*/
|
|
27151
|
+
function getRiskSchema(riskName) {
|
|
27152
|
+
bt.loggerService.log(GET_RISK_METHOD_NAME, {
|
|
27153
|
+
riskName,
|
|
27154
|
+
});
|
|
27155
|
+
bt.riskValidationService.validate(riskName, GET_RISK_METHOD_NAME);
|
|
27156
|
+
return bt.riskSchemaService.get(riskName);
|
|
27157
|
+
}
|
|
27158
|
+
/**
|
|
27159
|
+
* Retrieves a registered action schema by name.
|
|
27160
|
+
*
|
|
27161
|
+
* @param actionName - Unique action identifier
|
|
27162
|
+
* @returns The action schema configuration object
|
|
27163
|
+
* @throws Error if action is not registered
|
|
27164
|
+
*
|
|
27165
|
+
* @example
|
|
27166
|
+
* ```typescript
|
|
27167
|
+
* const action = getAction("telegram-notifier");
|
|
27168
|
+
* console.log(action.handler); // Class constructor or object
|
|
27169
|
+
* console.log(action.callbacks); // Optional lifecycle callbacks
|
|
27170
|
+
* ```
|
|
27171
|
+
*/
|
|
27172
|
+
function getActionSchema(actionName) {
|
|
27173
|
+
bt.loggerService.log(GET_ACTION_METHOD_NAME, {
|
|
27174
|
+
actionName,
|
|
27175
|
+
});
|
|
27176
|
+
bt.actionValidationService.validate(actionName, GET_ACTION_METHOD_NAME);
|
|
27177
|
+
return bt.actionSchemaService.get(actionName);
|
|
27178
|
+
}
|
|
27179
|
+
|
|
27180
|
+
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
27181
|
+
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
27182
|
+
const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
|
|
27183
|
+
const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
27184
|
+
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
27185
|
+
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
27186
|
+
const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
|
|
27187
|
+
const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
|
|
27188
|
+
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
27189
|
+
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
27190
|
+
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
27191
|
+
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
27192
|
+
/**
|
|
27193
|
+
* Checks if trade context is active (execution and method contexts).
|
|
27194
|
+
*
|
|
27195
|
+
* Returns true when both contexts are active, which is required for calling
|
|
27196
|
+
* exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
|
|
27197
|
+
* getDate, and getMode.
|
|
27198
|
+
*
|
|
27199
|
+
* @returns true if trade context is active, false otherwise
|
|
27200
|
+
*
|
|
27201
|
+
* @example
|
|
27202
|
+
* ```typescript
|
|
27203
|
+
* import { hasTradeContext, getCandles } from "backtest-kit";
|
|
27204
|
+
*
|
|
27205
|
+
* if (hasTradeContext()) {
|
|
27206
|
+
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
27207
|
+
* } else {
|
|
27208
|
+
* console.log("Trade context not active");
|
|
27209
|
+
* }
|
|
27210
|
+
* ```
|
|
27211
|
+
*/
|
|
27212
|
+
function hasTradeContext() {
|
|
27213
|
+
bt.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
|
|
27214
|
+
return ExecutionContextService.hasContext() && MethodContextService.hasContext();
|
|
27215
|
+
}
|
|
27216
|
+
/**
|
|
27217
|
+
* Fetches historical candle data from the registered exchange.
|
|
27218
|
+
*
|
|
27219
|
+
* Candles are fetched backwards from the current execution context time.
|
|
27220
|
+
* Uses the exchange's getCandles implementation.
|
|
27221
|
+
*
|
|
27222
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
27223
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
27224
|
+
* @param limit - Number of candles to fetch
|
|
27225
|
+
* @returns Promise resolving to array of candle data
|
|
27226
|
+
*
|
|
27227
|
+
* @example
|
|
27228
|
+
* ```typescript
|
|
27229
|
+
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
27230
|
+
* console.log(candles[0]); // { timestamp, open, high, low, close, volume }
|
|
27231
|
+
* ```
|
|
27232
|
+
*/
|
|
27233
|
+
async function getCandles(symbol, interval, limit) {
|
|
27234
|
+
bt.loggerService.info(GET_CANDLES_METHOD_NAME, {
|
|
27235
|
+
symbol,
|
|
26436
27236
|
interval,
|
|
26437
27237
|
limit,
|
|
26438
27238
|
});
|
|
@@ -35165,143 +35965,7 @@ class NotificationBacktestAdapter {
|
|
|
35165
35965
|
* @param data - The strategy commit contract data
|
|
35166
35966
|
*/
|
|
35167
35967
|
this.handleStrategyCommit = async (data) => {
|
|
35168
|
-
return await this._notificationBacktestUtils.handleStrategyCommit(data);
|
|
35169
|
-
};
|
|
35170
|
-
/**
|
|
35171
|
-
* Handles risk rejection event.
|
|
35172
|
-
* Proxies call to the underlying notification adapter.
|
|
35173
|
-
* @param data - The risk contract data
|
|
35174
|
-
*/
|
|
35175
|
-
this.handleRisk = async (data) => {
|
|
35176
|
-
return await this._notificationBacktestUtils.handleRisk(data);
|
|
35177
|
-
};
|
|
35178
|
-
/**
|
|
35179
|
-
* Handles error event.
|
|
35180
|
-
* Proxies call to the underlying notification adapter.
|
|
35181
|
-
* @param error - The error object
|
|
35182
|
-
*/
|
|
35183
|
-
this.handleError = async (error) => {
|
|
35184
|
-
return await this._notificationBacktestUtils.handleError(error);
|
|
35185
|
-
};
|
|
35186
|
-
/**
|
|
35187
|
-
* Handles critical error event.
|
|
35188
|
-
* Proxies call to the underlying notification adapter.
|
|
35189
|
-
* @param error - The error object
|
|
35190
|
-
*/
|
|
35191
|
-
this.handleCriticalError = async (error) => {
|
|
35192
|
-
return await this._notificationBacktestUtils.handleCriticalError(error);
|
|
35193
|
-
};
|
|
35194
|
-
/**
|
|
35195
|
-
* Handles validation error event.
|
|
35196
|
-
* Proxies call to the underlying notification adapter.
|
|
35197
|
-
* @param error - The error object
|
|
35198
|
-
*/
|
|
35199
|
-
this.handleValidationError = async (error) => {
|
|
35200
|
-
return await this._notificationBacktestUtils.handleValidationError(error);
|
|
35201
|
-
};
|
|
35202
|
-
/**
|
|
35203
|
-
* Gets all stored notifications.
|
|
35204
|
-
* Proxies call to the underlying notification adapter.
|
|
35205
|
-
* @returns Array of all notification models
|
|
35206
|
-
*/
|
|
35207
|
-
this.getData = async () => {
|
|
35208
|
-
return await this._notificationBacktestUtils.getData();
|
|
35209
|
-
};
|
|
35210
|
-
/**
|
|
35211
|
-
* Clears all stored notifications.
|
|
35212
|
-
* Proxies call to the underlying notification adapter.
|
|
35213
|
-
*/
|
|
35214
|
-
this.clear = async () => {
|
|
35215
|
-
return await this._notificationBacktestUtils.clear();
|
|
35216
|
-
};
|
|
35217
|
-
/**
|
|
35218
|
-
* Sets the notification adapter constructor.
|
|
35219
|
-
* All future notification operations will use this adapter.
|
|
35220
|
-
*
|
|
35221
|
-
* @param Ctor - Constructor for notification adapter
|
|
35222
|
-
*/
|
|
35223
|
-
this.useNotificationAdapter = (Ctor) => {
|
|
35224
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER);
|
|
35225
|
-
this._notificationBacktestUtils = Reflect.construct(Ctor, []);
|
|
35226
|
-
};
|
|
35227
|
-
/**
|
|
35228
|
-
* Switches to dummy notification adapter.
|
|
35229
|
-
* All future notification writes will be no-ops.
|
|
35230
|
-
*/
|
|
35231
|
-
this.useDummy = () => {
|
|
35232
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY);
|
|
35233
|
-
this._notificationBacktestUtils = new NotificationDummyBacktestUtils();
|
|
35234
|
-
};
|
|
35235
|
-
/**
|
|
35236
|
-
* Switches to in-memory notification adapter (default).
|
|
35237
|
-
* Notifications will be stored in memory only.
|
|
35238
|
-
*/
|
|
35239
|
-
this.useMemory = () => {
|
|
35240
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY);
|
|
35241
|
-
this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
|
|
35242
|
-
};
|
|
35243
|
-
/**
|
|
35244
|
-
* Switches to persistent notification adapter.
|
|
35245
|
-
* Notifications will be persisted to disk.
|
|
35246
|
-
*/
|
|
35247
|
-
this.usePersist = () => {
|
|
35248
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST);
|
|
35249
|
-
this._notificationBacktestUtils = new NotificationPersistBacktestUtils();
|
|
35250
|
-
};
|
|
35251
|
-
}
|
|
35252
|
-
}
|
|
35253
|
-
/**
|
|
35254
|
-
* Live trading notification adapter with pluggable notification backend.
|
|
35255
|
-
*
|
|
35256
|
-
* Features:
|
|
35257
|
-
* - Adapter pattern for swappable notification implementations
|
|
35258
|
-
* - Default adapter: NotificationMemoryLiveUtils (in-memory storage)
|
|
35259
|
-
* - Alternative adapters: NotificationPersistLiveUtils, NotificationDummyLiveUtils
|
|
35260
|
-
* - Convenience methods: usePersist(), useMemory(), useDummy()
|
|
35261
|
-
*/
|
|
35262
|
-
class NotificationLiveAdapter {
|
|
35263
|
-
constructor() {
|
|
35264
|
-
/** Internal notification utils instance */
|
|
35265
|
-
this._notificationLiveUtils = new NotificationMemoryLiveUtils();
|
|
35266
|
-
/**
|
|
35267
|
-
* Handles signal events.
|
|
35268
|
-
* Proxies call to the underlying notification adapter.
|
|
35269
|
-
* @param data - The strategy tick result data
|
|
35270
|
-
*/
|
|
35271
|
-
this.handleSignal = async (data) => {
|
|
35272
|
-
return await this._notificationLiveUtils.handleSignal(data);
|
|
35273
|
-
};
|
|
35274
|
-
/**
|
|
35275
|
-
* Handles partial profit availability event.
|
|
35276
|
-
* Proxies call to the underlying notification adapter.
|
|
35277
|
-
* @param data - The partial profit contract data
|
|
35278
|
-
*/
|
|
35279
|
-
this.handlePartialProfit = async (data) => {
|
|
35280
|
-
return await this._notificationLiveUtils.handlePartialProfit(data);
|
|
35281
|
-
};
|
|
35282
|
-
/**
|
|
35283
|
-
* Handles partial loss availability event.
|
|
35284
|
-
* Proxies call to the underlying notification adapter.
|
|
35285
|
-
* @param data - The partial loss contract data
|
|
35286
|
-
*/
|
|
35287
|
-
this.handlePartialLoss = async (data) => {
|
|
35288
|
-
return await this._notificationLiveUtils.handlePartialLoss(data);
|
|
35289
|
-
};
|
|
35290
|
-
/**
|
|
35291
|
-
* Handles breakeven availability event.
|
|
35292
|
-
* Proxies call to the underlying notification adapter.
|
|
35293
|
-
* @param data - The breakeven contract data
|
|
35294
|
-
*/
|
|
35295
|
-
this.handleBreakeven = async (data) => {
|
|
35296
|
-
return await this._notificationLiveUtils.handleBreakeven(data);
|
|
35297
|
-
};
|
|
35298
|
-
/**
|
|
35299
|
-
* Handles strategy commit events.
|
|
35300
|
-
* Proxies call to the underlying notification adapter.
|
|
35301
|
-
* @param data - The strategy commit contract data
|
|
35302
|
-
*/
|
|
35303
|
-
this.handleStrategyCommit = async (data) => {
|
|
35304
|
-
return await this._notificationLiveUtils.handleStrategyCommit(data);
|
|
35968
|
+
return await this._notificationBacktestUtils.handleStrategyCommit(data);
|
|
35305
35969
|
};
|
|
35306
35970
|
/**
|
|
35307
35971
|
* Handles risk rejection event.
|
|
@@ -35309,7 +35973,7 @@ class NotificationLiveAdapter {
|
|
|
35309
35973
|
* @param data - The risk contract data
|
|
35310
35974
|
*/
|
|
35311
35975
|
this.handleRisk = async (data) => {
|
|
35312
|
-
return await this.
|
|
35976
|
+
return await this._notificationBacktestUtils.handleRisk(data);
|
|
35313
35977
|
};
|
|
35314
35978
|
/**
|
|
35315
35979
|
* Handles error event.
|
|
@@ -35317,7 +35981,7 @@ class NotificationLiveAdapter {
|
|
|
35317
35981
|
* @param error - The error object
|
|
35318
35982
|
*/
|
|
35319
35983
|
this.handleError = async (error) => {
|
|
35320
|
-
return await this.
|
|
35984
|
+
return await this._notificationBacktestUtils.handleError(error);
|
|
35321
35985
|
};
|
|
35322
35986
|
/**
|
|
35323
35987
|
* Handles critical error event.
|
|
@@ -35325,7 +35989,7 @@ class NotificationLiveAdapter {
|
|
|
35325
35989
|
* @param error - The error object
|
|
35326
35990
|
*/
|
|
35327
35991
|
this.handleCriticalError = async (error) => {
|
|
35328
|
-
return await this.
|
|
35992
|
+
return await this._notificationBacktestUtils.handleCriticalError(error);
|
|
35329
35993
|
};
|
|
35330
35994
|
/**
|
|
35331
35995
|
* Handles validation error event.
|
|
@@ -35333,7 +35997,7 @@ class NotificationLiveAdapter {
|
|
|
35333
35997
|
* @param error - The error object
|
|
35334
35998
|
*/
|
|
35335
35999
|
this.handleValidationError = async (error) => {
|
|
35336
|
-
return await this.
|
|
36000
|
+
return await this._notificationBacktestUtils.handleValidationError(error);
|
|
35337
36001
|
};
|
|
35338
36002
|
/**
|
|
35339
36003
|
* Gets all stored notifications.
|
|
@@ -35341,14 +36005,14 @@ class NotificationLiveAdapter {
|
|
|
35341
36005
|
* @returns Array of all notification models
|
|
35342
36006
|
*/
|
|
35343
36007
|
this.getData = async () => {
|
|
35344
|
-
return await this.
|
|
36008
|
+
return await this._notificationBacktestUtils.getData();
|
|
35345
36009
|
};
|
|
35346
36010
|
/**
|
|
35347
36011
|
* Clears all stored notifications.
|
|
35348
36012
|
* Proxies call to the underlying notification adapter.
|
|
35349
36013
|
*/
|
|
35350
36014
|
this.clear = async () => {
|
|
35351
|
-
return await this.
|
|
36015
|
+
return await this._notificationBacktestUtils.clear();
|
|
35352
36016
|
};
|
|
35353
36017
|
/**
|
|
35354
36018
|
* Sets the notification adapter constructor.
|
|
@@ -35357,895 +36021,305 @@ class NotificationLiveAdapter {
|
|
|
35357
36021
|
* @param Ctor - Constructor for notification adapter
|
|
35358
36022
|
*/
|
|
35359
36023
|
this.useNotificationAdapter = (Ctor) => {
|
|
35360
|
-
bt.loggerService.info(
|
|
35361
|
-
this.
|
|
36024
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER);
|
|
36025
|
+
this._notificationBacktestUtils = Reflect.construct(Ctor, []);
|
|
35362
36026
|
};
|
|
35363
36027
|
/**
|
|
35364
36028
|
* Switches to dummy notification adapter.
|
|
35365
36029
|
* All future notification writes will be no-ops.
|
|
35366
36030
|
*/
|
|
35367
36031
|
this.useDummy = () => {
|
|
35368
|
-
bt.loggerService.info(
|
|
35369
|
-
this.
|
|
36032
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY);
|
|
36033
|
+
this._notificationBacktestUtils = new NotificationDummyBacktestUtils();
|
|
35370
36034
|
};
|
|
35371
36035
|
/**
|
|
35372
36036
|
* Switches to in-memory notification adapter (default).
|
|
35373
36037
|
* Notifications will be stored in memory only.
|
|
35374
36038
|
*/
|
|
35375
36039
|
this.useMemory = () => {
|
|
35376
|
-
bt.loggerService.info(
|
|
35377
|
-
this.
|
|
36040
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY);
|
|
36041
|
+
this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
|
|
35378
36042
|
};
|
|
35379
36043
|
/**
|
|
35380
36044
|
* Switches to persistent notification adapter.
|
|
35381
36045
|
* Notifications will be persisted to disk.
|
|
35382
36046
|
*/
|
|
35383
36047
|
this.usePersist = () => {
|
|
35384
|
-
bt.loggerService.info(
|
|
35385
|
-
this.
|
|
36048
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST);
|
|
36049
|
+
this._notificationBacktestUtils = new NotificationPersistBacktestUtils();
|
|
35386
36050
|
};
|
|
35387
36051
|
}
|
|
35388
36052
|
}
|
|
35389
36053
|
/**
|
|
35390
|
-
*
|
|
36054
|
+
* Live trading notification adapter with pluggable notification backend.
|
|
35391
36055
|
*
|
|
35392
36056
|
* Features:
|
|
35393
|
-
* -
|
|
35394
|
-
* -
|
|
35395
|
-
* -
|
|
35396
|
-
* -
|
|
36057
|
+
* - Adapter pattern for swappable notification implementations
|
|
36058
|
+
* - Default adapter: NotificationMemoryLiveUtils (in-memory storage)
|
|
36059
|
+
* - Alternative adapters: NotificationPersistLiveUtils, NotificationDummyLiveUtils
|
|
36060
|
+
* - Convenience methods: usePersist(), useMemory(), useDummy()
|
|
35397
36061
|
*/
|
|
35398
|
-
class
|
|
36062
|
+
class NotificationLiveAdapter {
|
|
35399
36063
|
constructor() {
|
|
36064
|
+
/** Internal notification utils instance */
|
|
36065
|
+
this._notificationLiveUtils = new NotificationMemoryLiveUtils();
|
|
35400
36066
|
/**
|
|
35401
|
-
*
|
|
35402
|
-
*
|
|
35403
|
-
*
|
|
35404
|
-
* @returns Cleanup function that unsubscribes from all emitters
|
|
36067
|
+
* Handles signal events.
|
|
36068
|
+
* Proxies call to the underlying notification adapter.
|
|
36069
|
+
* @param data - The strategy tick result data
|
|
35405
36070
|
*/
|
|
35406
|
-
this.
|
|
35407
|
-
|
|
35408
|
-
|
|
35409
|
-
let unBacktest;
|
|
35410
|
-
{
|
|
35411
|
-
const unBacktestSignal = signalBacktestEmitter.subscribe((data) => NotificationBacktest.handleSignal(data));
|
|
35412
|
-
const unBacktestPartialProfit = partialProfitSubject
|
|
35413
|
-
.filter(({ backtest }) => backtest)
|
|
35414
|
-
.connect((data) => NotificationBacktest.handlePartialProfit(data));
|
|
35415
|
-
const unBacktestPartialLoss = partialLossSubject
|
|
35416
|
-
.filter(({ backtest }) => backtest)
|
|
35417
|
-
.connect((data) => NotificationBacktest.handlePartialLoss(data));
|
|
35418
|
-
const unBacktestBreakeven = breakevenSubject
|
|
35419
|
-
.filter(({ backtest }) => backtest)
|
|
35420
|
-
.connect((data) => NotificationBacktest.handleBreakeven(data));
|
|
35421
|
-
const unBacktestStrategyCommit = strategyCommitSubject
|
|
35422
|
-
.filter(({ backtest }) => backtest)
|
|
35423
|
-
.connect((data) => NotificationBacktest.handleStrategyCommit(data));
|
|
35424
|
-
const unBacktestRisk = riskSubject
|
|
35425
|
-
.filter(({ backtest }) => backtest)
|
|
35426
|
-
.connect((data) => NotificationBacktest.handleRisk(data));
|
|
35427
|
-
const unBacktestError = errorEmitter.subscribe((error) => NotificationBacktest.handleError(error));
|
|
35428
|
-
const unBacktestExit = exitEmitter.subscribe((error) => NotificationBacktest.handleCriticalError(error));
|
|
35429
|
-
const unBacktestValidation = validationSubject.subscribe((error) => NotificationBacktest.handleValidationError(error));
|
|
35430
|
-
unBacktest = functoolsKit.compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation());
|
|
35431
|
-
}
|
|
35432
|
-
{
|
|
35433
|
-
const unLiveSignal = signalLiveEmitter.subscribe((data) => NotificationLive.handleSignal(data));
|
|
35434
|
-
const unLivePartialProfit = partialProfitSubject
|
|
35435
|
-
.filter(({ backtest }) => !backtest)
|
|
35436
|
-
.connect((data) => NotificationLive.handlePartialProfit(data));
|
|
35437
|
-
const unLivePartialLoss = partialLossSubject
|
|
35438
|
-
.filter(({ backtest }) => !backtest)
|
|
35439
|
-
.connect((data) => NotificationLive.handlePartialLoss(data));
|
|
35440
|
-
const unLiveBreakeven = breakevenSubject
|
|
35441
|
-
.filter(({ backtest }) => !backtest)
|
|
35442
|
-
.connect((data) => NotificationLive.handleBreakeven(data));
|
|
35443
|
-
const unLiveStrategyCommit = strategyCommitSubject
|
|
35444
|
-
.filter(({ backtest }) => !backtest)
|
|
35445
|
-
.connect((data) => NotificationLive.handleStrategyCommit(data));
|
|
35446
|
-
const unLiveRisk = riskSubject
|
|
35447
|
-
.filter(({ backtest }) => !backtest)
|
|
35448
|
-
.connect((data) => NotificationLive.handleRisk(data));
|
|
35449
|
-
const unLiveError = errorEmitter.subscribe((error) => NotificationLive.handleError(error));
|
|
35450
|
-
const unLiveExit = exitEmitter.subscribe((error) => NotificationLive.handleCriticalError(error));
|
|
35451
|
-
const unLiveValidation = validationSubject.subscribe((error) => NotificationLive.handleValidationError(error));
|
|
35452
|
-
unLive = functoolsKit.compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation());
|
|
35453
|
-
}
|
|
35454
|
-
return () => {
|
|
35455
|
-
unLive();
|
|
35456
|
-
unBacktest();
|
|
35457
|
-
this.enable.clear();
|
|
35458
|
-
};
|
|
35459
|
-
});
|
|
36071
|
+
this.handleSignal = async (data) => {
|
|
36072
|
+
return await this._notificationLiveUtils.handleSignal(data);
|
|
36073
|
+
};
|
|
35460
36074
|
/**
|
|
35461
|
-
*
|
|
35462
|
-
*
|
|
36075
|
+
* Handles partial profit availability event.
|
|
36076
|
+
* Proxies call to the underlying notification adapter.
|
|
36077
|
+
* @param data - The partial profit contract data
|
|
35463
36078
|
*/
|
|
35464
|
-
this.
|
|
35465
|
-
|
|
35466
|
-
if (this.enable.hasValue()) {
|
|
35467
|
-
const lastSubscription = this.enable();
|
|
35468
|
-
lastSubscription();
|
|
35469
|
-
}
|
|
36079
|
+
this.handlePartialProfit = async (data) => {
|
|
36080
|
+
return await this._notificationLiveUtils.handlePartialProfit(data);
|
|
35470
36081
|
};
|
|
35471
36082
|
/**
|
|
35472
|
-
*
|
|
35473
|
-
*
|
|
35474
|
-
* @
|
|
35475
|
-
* @throws Error if NotificationAdapter is not enabled
|
|
36083
|
+
* Handles partial loss availability event.
|
|
36084
|
+
* Proxies call to the underlying notification adapter.
|
|
36085
|
+
* @param data - The partial loss contract data
|
|
35476
36086
|
*/
|
|
35477
|
-
this.
|
|
35478
|
-
|
|
35479
|
-
backtest: isBacktest,
|
|
35480
|
-
});
|
|
35481
|
-
if (!this.enable.hasValue()) {
|
|
35482
|
-
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
35483
|
-
}
|
|
35484
|
-
if (isBacktest) {
|
|
35485
|
-
return await NotificationBacktest.getData();
|
|
35486
|
-
}
|
|
35487
|
-
return await NotificationLive.getData();
|
|
36087
|
+
this.handlePartialLoss = async (data) => {
|
|
36088
|
+
return await this._notificationLiveUtils.handlePartialLoss(data);
|
|
35488
36089
|
};
|
|
35489
36090
|
/**
|
|
35490
|
-
*
|
|
35491
|
-
*
|
|
35492
|
-
* @
|
|
36091
|
+
* Handles breakeven availability event.
|
|
36092
|
+
* Proxies call to the underlying notification adapter.
|
|
36093
|
+
* @param data - The breakeven contract data
|
|
35493
36094
|
*/
|
|
35494
|
-
this.
|
|
35495
|
-
|
|
35496
|
-
backtest: isBacktest,
|
|
35497
|
-
});
|
|
35498
|
-
if (!this.enable.hasValue()) {
|
|
35499
|
-
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
35500
|
-
}
|
|
35501
|
-
if (isBacktest) {
|
|
35502
|
-
return await NotificationBacktest.clear();
|
|
35503
|
-
}
|
|
35504
|
-
return await NotificationLive.clear();
|
|
35505
|
-
};
|
|
35506
|
-
}
|
|
35507
|
-
}
|
|
35508
|
-
/**
|
|
35509
|
-
* Global singleton instance of NotificationAdapter.
|
|
35510
|
-
* Provides unified notification management for backtest and live trading.
|
|
35511
|
-
*/
|
|
35512
|
-
const Notification = new NotificationAdapter();
|
|
35513
|
-
/**
|
|
35514
|
-
* Global singleton instance of NotificationLiveAdapter.
|
|
35515
|
-
* Provides live trading notification storage with pluggable backends.
|
|
35516
|
-
*/
|
|
35517
|
-
const NotificationLive = new NotificationLiveAdapter();
|
|
35518
|
-
/**
|
|
35519
|
-
* Global singleton instance of NotificationBacktestAdapter.
|
|
35520
|
-
* Provides backtest notification storage with pluggable backends.
|
|
35521
|
-
*/
|
|
35522
|
-
const NotificationBacktest = new NotificationBacktestAdapter();
|
|
35523
|
-
|
|
35524
|
-
const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
|
|
35525
|
-
const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
|
|
35526
|
-
const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
|
|
35527
|
-
const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
35528
|
-
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
35529
|
-
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
35530
|
-
const MS_PER_MINUTE$1 = 60000;
|
|
35531
|
-
/**
|
|
35532
|
-
* Gets current timestamp from execution context if available.
|
|
35533
|
-
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
35534
|
-
*/
|
|
35535
|
-
const GET_TIMESTAMP_FN = async () => {
|
|
35536
|
-
if (ExecutionContextService.hasContext()) {
|
|
35537
|
-
return new Date(bt.executionContextService.context.when);
|
|
35538
|
-
}
|
|
35539
|
-
return new Date();
|
|
35540
|
-
};
|
|
35541
|
-
/**
|
|
35542
|
-
* Gets backtest mode flag from execution context if available.
|
|
35543
|
-
* Returns false if no execution context exists (live mode).
|
|
35544
|
-
*/
|
|
35545
|
-
const GET_BACKTEST_FN = async () => {
|
|
35546
|
-
if (ExecutionContextService.hasContext()) {
|
|
35547
|
-
return bt.executionContextService.context.backtest;
|
|
35548
|
-
}
|
|
35549
|
-
return false;
|
|
35550
|
-
};
|
|
35551
|
-
/**
|
|
35552
|
-
* Default implementation for getCandles.
|
|
35553
|
-
* Throws an error indicating the method is not implemented.
|
|
35554
|
-
*/
|
|
35555
|
-
const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
|
|
35556
|
-
throw new Error(`getCandles is not implemented for this exchange`);
|
|
35557
|
-
};
|
|
35558
|
-
/**
|
|
35559
|
-
* Default implementation for formatQuantity.
|
|
35560
|
-
* Returns Bitcoin precision on Binance (8 decimal places).
|
|
35561
|
-
*/
|
|
35562
|
-
const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
|
|
35563
|
-
return quantity.toFixed(8);
|
|
35564
|
-
};
|
|
35565
|
-
/**
|
|
35566
|
-
* Default implementation for formatPrice.
|
|
35567
|
-
* Returns Bitcoin precision on Binance (2 decimal places).
|
|
35568
|
-
*/
|
|
35569
|
-
const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
|
|
35570
|
-
return price.toFixed(2);
|
|
35571
|
-
};
|
|
35572
|
-
/**
|
|
35573
|
-
* Default implementation for getOrderBook.
|
|
35574
|
-
* Throws an error indicating the method is not implemented.
|
|
35575
|
-
*
|
|
35576
|
-
* @param _symbol - Trading pair symbol (unused)
|
|
35577
|
-
* @param _depth - Maximum depth levels (unused)
|
|
35578
|
-
* @param _from - Start of time range (unused - can be ignored in live implementations)
|
|
35579
|
-
* @param _to - End of time range (unused - can be ignored in live implementations)
|
|
35580
|
-
* @param _backtest - Whether running in backtest mode (unused)
|
|
35581
|
-
*/
|
|
35582
|
-
const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
|
|
35583
|
-
throw new Error(`getOrderBook is not implemented for this exchange`);
|
|
35584
|
-
};
|
|
35585
|
-
const INTERVAL_MINUTES$1 = {
|
|
35586
|
-
"1m": 1,
|
|
35587
|
-
"3m": 3,
|
|
35588
|
-
"5m": 5,
|
|
35589
|
-
"15m": 15,
|
|
35590
|
-
"30m": 30,
|
|
35591
|
-
"1h": 60,
|
|
35592
|
-
"2h": 120,
|
|
35593
|
-
"4h": 240,
|
|
35594
|
-
"6h": 360,
|
|
35595
|
-
"8h": 480,
|
|
35596
|
-
};
|
|
35597
|
-
/**
|
|
35598
|
-
* Aligns timestamp down to the nearest interval boundary.
|
|
35599
|
-
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
35600
|
-
*
|
|
35601
|
-
* Candle timestamp convention:
|
|
35602
|
-
* - Candle timestamp = openTime (when candle opens)
|
|
35603
|
-
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
35604
|
-
*
|
|
35605
|
-
* Adapter contract:
|
|
35606
|
-
* - Adapter must return candles with timestamp = openTime
|
|
35607
|
-
* - First returned candle.timestamp must equal aligned since
|
|
35608
|
-
* - Adapter must return exactly `limit` candles
|
|
35609
|
-
*
|
|
35610
|
-
* @param timestamp - Timestamp in milliseconds
|
|
35611
|
-
* @param intervalMinutes - Interval in minutes
|
|
35612
|
-
* @returns Aligned timestamp rounded down to interval boundary
|
|
35613
|
-
*/
|
|
35614
|
-
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
35615
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
35616
|
-
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
35617
|
-
};
|
|
35618
|
-
/**
|
|
35619
|
-
* Creates exchange instance with methods resolved once during construction.
|
|
35620
|
-
* Applies default implementations where schema methods are not provided.
|
|
35621
|
-
*
|
|
35622
|
-
* @param schema - Exchange schema from registry
|
|
35623
|
-
* @returns Object with resolved exchange methods
|
|
35624
|
-
*/
|
|
35625
|
-
const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
35626
|
-
const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
|
|
35627
|
-
const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
|
|
35628
|
-
const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
|
|
35629
|
-
const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
|
|
35630
|
-
return {
|
|
35631
|
-
getCandles,
|
|
35632
|
-
formatQuantity,
|
|
35633
|
-
formatPrice,
|
|
35634
|
-
getOrderBook,
|
|
35635
|
-
};
|
|
35636
|
-
};
|
|
35637
|
-
/**
|
|
35638
|
-
* Attempts to read candles from cache.
|
|
35639
|
-
*
|
|
35640
|
-
* Cache lookup calculates expected timestamps:
|
|
35641
|
-
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
35642
|
-
* Returns all candles if found, null if any missing.
|
|
35643
|
-
*
|
|
35644
|
-
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
35645
|
-
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
35646
|
-
* @param untilTimestamp - Unused, kept for API compatibility
|
|
35647
|
-
* @param exchangeName - Exchange name
|
|
35648
|
-
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
35649
|
-
*/
|
|
35650
|
-
const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
35651
|
-
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
35652
|
-
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
35653
|
-
// Returns all candles if found, null if any missing
|
|
35654
|
-
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
35655
|
-
// Return cached data only if we have exactly the requested limit
|
|
35656
|
-
if (cachedCandles?.length === dto.limit) {
|
|
35657
|
-
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
35658
|
-
return cachedCandles;
|
|
35659
|
-
}
|
|
35660
|
-
bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
35661
|
-
return null;
|
|
35662
|
-
}, {
|
|
35663
|
-
fallback: async (error) => {
|
|
35664
|
-
const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
|
|
35665
|
-
const payload = {
|
|
35666
|
-
error: functoolsKit.errorData(error),
|
|
35667
|
-
message: functoolsKit.getErrorMessage(error),
|
|
35668
|
-
};
|
|
35669
|
-
bt.loggerService.warn(message, payload);
|
|
35670
|
-
console.warn(message, payload);
|
|
35671
|
-
errorEmitter.next(error);
|
|
35672
|
-
},
|
|
35673
|
-
defaultValue: null,
|
|
35674
|
-
});
|
|
35675
|
-
/**
|
|
35676
|
-
* Writes candles to cache with error handling.
|
|
35677
|
-
*
|
|
35678
|
-
* The candles passed to this function should be validated:
|
|
35679
|
-
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
35680
|
-
* - Exact number of candles as requested (limit)
|
|
35681
|
-
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
35682
|
-
*
|
|
35683
|
-
* @param candles - Array of validated candle data to cache
|
|
35684
|
-
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
35685
|
-
* @param exchangeName - Exchange name
|
|
35686
|
-
*/
|
|
35687
|
-
const WRITE_CANDLES_CACHE_FN = functoolsKit.trycatch(functoolsKit.queued(async (candles, dto, exchangeName) => {
|
|
35688
|
-
await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
|
|
35689
|
-
bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
|
|
35690
|
-
}), {
|
|
35691
|
-
fallback: async (error) => {
|
|
35692
|
-
const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
|
|
35693
|
-
const payload = {
|
|
35694
|
-
error: functoolsKit.errorData(error),
|
|
35695
|
-
message: functoolsKit.getErrorMessage(error),
|
|
36095
|
+
this.handleBreakeven = async (data) => {
|
|
36096
|
+
return await this._notificationLiveUtils.handleBreakeven(data);
|
|
35696
36097
|
};
|
|
35697
|
-
bt.loggerService.warn(message, payload);
|
|
35698
|
-
console.warn(message, payload);
|
|
35699
|
-
errorEmitter.next(error);
|
|
35700
|
-
},
|
|
35701
|
-
defaultValue: null,
|
|
35702
|
-
});
|
|
35703
|
-
/**
|
|
35704
|
-
* Instance class for exchange operations on a specific exchange.
|
|
35705
|
-
*
|
|
35706
|
-
* Provides isolated exchange operations for a single exchange.
|
|
35707
|
-
* Each instance maintains its own context and exposes IExchangeSchema methods.
|
|
35708
|
-
* The schema is retrieved once during construction for better performance.
|
|
35709
|
-
*
|
|
35710
|
-
* @example
|
|
35711
|
-
* ```typescript
|
|
35712
|
-
* const instance = new ExchangeInstance("binance");
|
|
35713
|
-
*
|
|
35714
|
-
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
35715
|
-
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
35716
|
-
* const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
35717
|
-
* const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
35718
|
-
* ```
|
|
35719
|
-
*/
|
|
35720
|
-
class ExchangeInstance {
|
|
35721
|
-
/**
|
|
35722
|
-
* Creates a new ExchangeInstance for a specific exchange.
|
|
35723
|
-
*
|
|
35724
|
-
* @param exchangeName - Exchange name (e.g., "binance")
|
|
35725
|
-
*/
|
|
35726
|
-
constructor(exchangeName) {
|
|
35727
|
-
this.exchangeName = exchangeName;
|
|
35728
36098
|
/**
|
|
35729
|
-
*
|
|
35730
|
-
*
|
|
35731
|
-
*
|
|
35732
|
-
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
35733
|
-
*
|
|
35734
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
35735
|
-
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
35736
|
-
* @param limit - Maximum number of candles to fetch
|
|
35737
|
-
* @returns Promise resolving to array of OHLCV candle data
|
|
35738
|
-
*
|
|
35739
|
-
* @example
|
|
35740
|
-
* ```typescript
|
|
35741
|
-
* const instance = new ExchangeInstance("binance");
|
|
35742
|
-
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
35743
|
-
* ```
|
|
36099
|
+
* Handles strategy commit events.
|
|
36100
|
+
* Proxies call to the underlying notification adapter.
|
|
36101
|
+
* @param data - The strategy commit contract data
|
|
35744
36102
|
*/
|
|
35745
|
-
this.
|
|
35746
|
-
|
|
35747
|
-
exchangeName: this.exchangeName,
|
|
35748
|
-
symbol,
|
|
35749
|
-
interval,
|
|
35750
|
-
limit,
|
|
35751
|
-
});
|
|
35752
|
-
const getCandles = this._methods.getCandles;
|
|
35753
|
-
const step = INTERVAL_MINUTES$1[interval];
|
|
35754
|
-
if (!step) {
|
|
35755
|
-
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
35756
|
-
}
|
|
35757
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
35758
|
-
// Align when down to interval boundary
|
|
35759
|
-
const when = await GET_TIMESTAMP_FN();
|
|
35760
|
-
const whenTimestamp = when.getTime();
|
|
35761
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
|
|
35762
|
-
// Calculate since: go back limit candles from aligned when
|
|
35763
|
-
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
35764
|
-
const since = new Date(sinceTimestamp);
|
|
35765
|
-
const untilTimestamp = alignedWhen;
|
|
35766
|
-
// Try to read from cache first
|
|
35767
|
-
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
35768
|
-
if (cachedCandles !== null) {
|
|
35769
|
-
return cachedCandles;
|
|
35770
|
-
}
|
|
35771
|
-
let allData = [];
|
|
35772
|
-
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
35773
|
-
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
35774
|
-
let remaining = limit;
|
|
35775
|
-
let currentSince = new Date(since.getTime());
|
|
35776
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35777
|
-
while (remaining > 0) {
|
|
35778
|
-
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
35779
|
-
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
35780
|
-
allData.push(...chunkData);
|
|
35781
|
-
remaining -= chunkLimit;
|
|
35782
|
-
if (remaining > 0) {
|
|
35783
|
-
// Move currentSince forward by the number of candles fetched
|
|
35784
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
35785
|
-
}
|
|
35786
|
-
}
|
|
35787
|
-
}
|
|
35788
|
-
else {
|
|
35789
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35790
|
-
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
35791
|
-
}
|
|
35792
|
-
// Apply distinct by timestamp to remove duplicates
|
|
35793
|
-
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
35794
|
-
if (allData.length !== uniqueData.length) {
|
|
35795
|
-
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
35796
|
-
}
|
|
35797
|
-
// Validate adapter returned data
|
|
35798
|
-
if (uniqueData.length === 0) {
|
|
35799
|
-
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
35800
|
-
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
35801
|
-
}
|
|
35802
|
-
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
35803
|
-
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
35804
|
-
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
35805
|
-
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
35806
|
-
}
|
|
35807
|
-
if (uniqueData.length !== limit) {
|
|
35808
|
-
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
35809
|
-
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
35810
|
-
`Adapter must return exact number of candles requested.`);
|
|
35811
|
-
}
|
|
35812
|
-
// Write to cache after successful fetch
|
|
35813
|
-
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
35814
|
-
return uniqueData;
|
|
36103
|
+
this.handleStrategyCommit = async (data) => {
|
|
36104
|
+
return await this._notificationLiveUtils.handleStrategyCommit(data);
|
|
35815
36105
|
};
|
|
35816
36106
|
/**
|
|
35817
|
-
*
|
|
35818
|
-
*
|
|
35819
|
-
*
|
|
35820
|
-
|
|
35821
|
-
|
|
35822
|
-
|
|
35823
|
-
*
|
|
35824
|
-
* If volume is zero, returns simple average of close prices.
|
|
35825
|
-
*
|
|
35826
|
-
* @param symbol - Trading pair symbol
|
|
35827
|
-
* @returns Promise resolving to VWAP price
|
|
35828
|
-
* @throws Error if no candles available
|
|
35829
|
-
*
|
|
35830
|
-
* @example
|
|
35831
|
-
* ```typescript
|
|
35832
|
-
* const instance = new ExchangeInstance("binance");
|
|
35833
|
-
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
35834
|
-
* console.log(vwap); // 50125.43
|
|
35835
|
-
* ```
|
|
35836
|
-
*/
|
|
35837
|
-
this.getAveragePrice = async (symbol) => {
|
|
35838
|
-
bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
|
|
35839
|
-
exchangeName: this.exchangeName,
|
|
35840
|
-
symbol,
|
|
35841
|
-
});
|
|
35842
|
-
const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
35843
|
-
if (candles.length === 0) {
|
|
35844
|
-
throw new Error(`ExchangeInstance getAveragePrice: no candles data for symbol=${symbol}`);
|
|
35845
|
-
}
|
|
35846
|
-
// VWAP (Volume Weighted Average Price)
|
|
35847
|
-
// Используем типичную цену (typical price) = (high + low + close) / 3
|
|
35848
|
-
const sumPriceVolume = candles.reduce((acc, candle) => {
|
|
35849
|
-
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
|
|
35850
|
-
return acc + typicalPrice * candle.volume;
|
|
35851
|
-
}, 0);
|
|
35852
|
-
const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
|
|
35853
|
-
if (totalVolume === 0) {
|
|
35854
|
-
// Если объем нулевой, возвращаем простое среднее close цен
|
|
35855
|
-
const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
|
|
35856
|
-
return sum / candles.length;
|
|
35857
|
-
}
|
|
35858
|
-
const vwap = sumPriceVolume / totalVolume;
|
|
35859
|
-
return vwap;
|
|
36107
|
+
* Handles risk rejection event.
|
|
36108
|
+
* Proxies call to the underlying notification adapter.
|
|
36109
|
+
* @param data - The risk contract data
|
|
36110
|
+
*/
|
|
36111
|
+
this.handleRisk = async (data) => {
|
|
36112
|
+
return await this._notificationLiveUtils.handleRisk(data);
|
|
35860
36113
|
};
|
|
35861
36114
|
/**
|
|
35862
|
-
*
|
|
35863
|
-
*
|
|
35864
|
-
* @param
|
|
35865
|
-
* @param quantity - Raw quantity value
|
|
35866
|
-
* @returns Promise resolving to formatted quantity string
|
|
35867
|
-
*
|
|
35868
|
-
* @example
|
|
35869
|
-
* ```typescript
|
|
35870
|
-
* const instance = new ExchangeInstance("binance");
|
|
35871
|
-
* const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
35872
|
-
* console.log(formatted); // "0.00100000"
|
|
35873
|
-
* ```
|
|
36115
|
+
* Handles error event.
|
|
36116
|
+
* Proxies call to the underlying notification adapter.
|
|
36117
|
+
* @param error - The error object
|
|
35874
36118
|
*/
|
|
35875
|
-
this.
|
|
35876
|
-
|
|
35877
|
-
exchangeName: this.exchangeName,
|
|
35878
|
-
symbol,
|
|
35879
|
-
quantity,
|
|
35880
|
-
});
|
|
35881
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35882
|
-
return await this._methods.formatQuantity(symbol, quantity, isBacktest);
|
|
36119
|
+
this.handleError = async (error) => {
|
|
36120
|
+
return await this._notificationLiveUtils.handleError(error);
|
|
35883
36121
|
};
|
|
35884
36122
|
/**
|
|
35885
|
-
*
|
|
35886
|
-
*
|
|
35887
|
-
* @param
|
|
35888
|
-
* @param price - Raw price value
|
|
35889
|
-
* @returns Promise resolving to formatted price string
|
|
35890
|
-
*
|
|
35891
|
-
* @example
|
|
35892
|
-
* ```typescript
|
|
35893
|
-
* const instance = new ExchangeInstance("binance");
|
|
35894
|
-
* const formatted = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
35895
|
-
* console.log(formatted); // "50000.12"
|
|
35896
|
-
* ```
|
|
36123
|
+
* Handles critical error event.
|
|
36124
|
+
* Proxies call to the underlying notification adapter.
|
|
36125
|
+
* @param error - The error object
|
|
35897
36126
|
*/
|
|
35898
|
-
this.
|
|
35899
|
-
|
|
35900
|
-
exchangeName: this.exchangeName,
|
|
35901
|
-
symbol,
|
|
35902
|
-
price,
|
|
35903
|
-
});
|
|
35904
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35905
|
-
return await this._methods.formatPrice(symbol, price, isBacktest);
|
|
36127
|
+
this.handleCriticalError = async (error) => {
|
|
36128
|
+
return await this._notificationLiveUtils.handleCriticalError(error);
|
|
35906
36129
|
};
|
|
35907
36130
|
/**
|
|
35908
|
-
*
|
|
35909
|
-
*
|
|
35910
|
-
*
|
|
35911
|
-
* and passes it to the exchange schema implementation. The implementation may use
|
|
35912
|
-
* the time range (backtest) or ignore it (live trading).
|
|
35913
|
-
*
|
|
35914
|
-
* @param symbol - Trading pair symbol
|
|
35915
|
-
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
35916
|
-
* @returns Promise resolving to order book data
|
|
35917
|
-
* @throws Error if getOrderBook is not implemented
|
|
35918
|
-
*
|
|
35919
|
-
* @example
|
|
35920
|
-
* ```typescript
|
|
35921
|
-
* const instance = new ExchangeInstance("binance");
|
|
35922
|
-
* const orderBook = await instance.getOrderBook("BTCUSDT");
|
|
35923
|
-
* console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
|
|
35924
|
-
* const deepOrderBook = await instance.getOrderBook("BTCUSDT", 100);
|
|
35925
|
-
* ```
|
|
36131
|
+
* Handles validation error event.
|
|
36132
|
+
* Proxies call to the underlying notification adapter.
|
|
36133
|
+
* @param error - The error object
|
|
35926
36134
|
*/
|
|
35927
|
-
this.
|
|
35928
|
-
|
|
35929
|
-
exchangeName: this.exchangeName,
|
|
35930
|
-
symbol,
|
|
35931
|
-
depth,
|
|
35932
|
-
});
|
|
35933
|
-
const to = await GET_TIMESTAMP_FN();
|
|
35934
|
-
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
|
|
35935
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35936
|
-
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
36135
|
+
this.handleValidationError = async (error) => {
|
|
36136
|
+
return await this._notificationLiveUtils.handleValidationError(error);
|
|
35937
36137
|
};
|
|
35938
36138
|
/**
|
|
35939
|
-
*
|
|
35940
|
-
*
|
|
35941
|
-
*
|
|
35942
|
-
*
|
|
35943
|
-
* Parameter combinations:
|
|
35944
|
-
* 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
|
|
35945
|
-
* 2. sDate + eDate: calculates limit from date range, validates eDate <= now
|
|
35946
|
-
* 3. eDate + limit: calculates sDate backward, validates eDate <= now
|
|
35947
|
-
* 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
|
|
35948
|
-
* 5. Only limit: uses Date.now() as reference (backward)
|
|
35949
|
-
*
|
|
35950
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
35951
|
-
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
35952
|
-
* @param limit - Optional number of candles to fetch
|
|
35953
|
-
* @param sDate - Optional start date in milliseconds
|
|
35954
|
-
* @param eDate - Optional end date in milliseconds
|
|
35955
|
-
* @returns Promise resolving to array of candle data
|
|
35956
|
-
*
|
|
35957
|
-
* @example
|
|
35958
|
-
* ```typescript
|
|
35959
|
-
* const instance = new ExchangeInstance("binance");
|
|
35960
|
-
*
|
|
35961
|
-
* // Fetch 100 candles backward from now
|
|
35962
|
-
* const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
|
|
35963
|
-
*
|
|
35964
|
-
* // Fetch candles for specific date range
|
|
35965
|
-
* const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
|
|
35966
|
-
* ```
|
|
36139
|
+
* Gets all stored notifications.
|
|
36140
|
+
* Proxies call to the underlying notification adapter.
|
|
36141
|
+
* @returns Array of all notification models
|
|
35967
36142
|
*/
|
|
35968
|
-
this.
|
|
35969
|
-
|
|
35970
|
-
exchangeName: this.exchangeName,
|
|
35971
|
-
symbol,
|
|
35972
|
-
interval,
|
|
35973
|
-
limit,
|
|
35974
|
-
sDate,
|
|
35975
|
-
eDate,
|
|
35976
|
-
});
|
|
35977
|
-
const step = INTERVAL_MINUTES$1[interval];
|
|
35978
|
-
if (!step) {
|
|
35979
|
-
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
35980
|
-
}
|
|
35981
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
35982
|
-
const when = await GET_TIMESTAMP_FN();
|
|
35983
|
-
const nowTimestamp = when.getTime();
|
|
35984
|
-
const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
|
|
35985
|
-
let sinceTimestamp;
|
|
35986
|
-
let calculatedLimit;
|
|
35987
|
-
// Case 1: all three parameters provided
|
|
35988
|
-
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
35989
|
-
if (sDate >= eDate) {
|
|
35990
|
-
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
35991
|
-
}
|
|
35992
|
-
if (eDate > nowTimestamp) {
|
|
35993
|
-
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
35994
|
-
}
|
|
35995
|
-
// Align sDate down to interval boundary
|
|
35996
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
35997
|
-
calculatedLimit = limit;
|
|
35998
|
-
}
|
|
35999
|
-
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
36000
|
-
else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
|
|
36001
|
-
if (sDate >= eDate) {
|
|
36002
|
-
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
36003
|
-
}
|
|
36004
|
-
if (eDate > nowTimestamp) {
|
|
36005
|
-
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
36006
|
-
}
|
|
36007
|
-
// Align sDate down to interval boundary
|
|
36008
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
36009
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
36010
|
-
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
36011
|
-
if (calculatedLimit <= 0) {
|
|
36012
|
-
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
36013
|
-
}
|
|
36014
|
-
}
|
|
36015
|
-
// Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
|
|
36016
|
-
else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
|
|
36017
|
-
if (eDate > nowTimestamp) {
|
|
36018
|
-
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
36019
|
-
}
|
|
36020
|
-
// Align eDate down and calculate sinceTimestamp
|
|
36021
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
36022
|
-
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
36023
|
-
calculatedLimit = limit;
|
|
36024
|
-
}
|
|
36025
|
-
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
36026
|
-
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
36027
|
-
// Align sDate down to interval boundary
|
|
36028
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
36029
|
-
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
36030
|
-
if (endTimestamp > nowTimestamp) {
|
|
36031
|
-
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
36032
|
-
}
|
|
36033
|
-
calculatedLimit = limit;
|
|
36034
|
-
}
|
|
36035
|
-
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
36036
|
-
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
36037
|
-
sinceTimestamp = alignedNow - limit * stepMs;
|
|
36038
|
-
calculatedLimit = limit;
|
|
36039
|
-
}
|
|
36040
|
-
// Invalid: no parameters or only sDate or only eDate
|
|
36041
|
-
else {
|
|
36042
|
-
throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
|
|
36043
|
-
`Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
|
|
36044
|
-
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
36045
|
-
}
|
|
36046
|
-
// Try to read from cache first
|
|
36047
|
-
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
36048
|
-
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
36049
|
-
if (cachedCandles !== null) {
|
|
36050
|
-
return cachedCandles;
|
|
36051
|
-
}
|
|
36052
|
-
// Fetch candles
|
|
36053
|
-
const since = new Date(sinceTimestamp);
|
|
36054
|
-
let allData = [];
|
|
36055
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
36056
|
-
const getCandles = this._methods.getCandles;
|
|
36057
|
-
if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
36058
|
-
let remaining = calculatedLimit;
|
|
36059
|
-
let currentSince = new Date(since.getTime());
|
|
36060
|
-
while (remaining > 0) {
|
|
36061
|
-
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
36062
|
-
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
36063
|
-
allData.push(...chunkData);
|
|
36064
|
-
remaining -= chunkLimit;
|
|
36065
|
-
if (remaining > 0) {
|
|
36066
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
36067
|
-
}
|
|
36068
|
-
}
|
|
36069
|
-
}
|
|
36070
|
-
else {
|
|
36071
|
-
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
36072
|
-
}
|
|
36073
|
-
// Apply distinct by timestamp to remove duplicates
|
|
36074
|
-
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
36075
|
-
if (allData.length !== uniqueData.length) {
|
|
36076
|
-
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
36077
|
-
}
|
|
36078
|
-
// Validate adapter returned data
|
|
36079
|
-
if (uniqueData.length === 0) {
|
|
36080
|
-
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
36081
|
-
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
36082
|
-
}
|
|
36083
|
-
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
36084
|
-
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
36085
|
-
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
36086
|
-
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
36087
|
-
}
|
|
36088
|
-
if (uniqueData.length !== calculatedLimit) {
|
|
36089
|
-
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
36090
|
-
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
36091
|
-
`Adapter must return exact number of candles requested.`);
|
|
36092
|
-
}
|
|
36093
|
-
// Write to cache after successful fetch
|
|
36094
|
-
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
36095
|
-
return uniqueData;
|
|
36143
|
+
this.getData = async () => {
|
|
36144
|
+
return await this._notificationLiveUtils.getData();
|
|
36096
36145
|
};
|
|
36097
|
-
const schema = bt.exchangeSchemaService.get(this.exchangeName);
|
|
36098
|
-
this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
|
|
36099
|
-
}
|
|
36100
|
-
}
|
|
36101
|
-
/**
|
|
36102
|
-
* Utility class for exchange operations.
|
|
36103
|
-
*
|
|
36104
|
-
* Provides simplified access to exchange schema methods with validation.
|
|
36105
|
-
* Exported as singleton instance for convenient usage.
|
|
36106
|
-
*
|
|
36107
|
-
* @example
|
|
36108
|
-
* ```typescript
|
|
36109
|
-
* import { Exchange } from "./classes/Exchange";
|
|
36110
|
-
*
|
|
36111
|
-
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
36112
|
-
* exchangeName: "binance"
|
|
36113
|
-
* });
|
|
36114
|
-
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
36115
|
-
* exchangeName: "binance"
|
|
36116
|
-
* });
|
|
36117
|
-
* const formatted = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
36118
|
-
* exchangeName: "binance"
|
|
36119
|
-
* });
|
|
36120
|
-
* ```
|
|
36121
|
-
*/
|
|
36122
|
-
class ExchangeUtils {
|
|
36123
|
-
constructor() {
|
|
36124
36146
|
/**
|
|
36125
|
-
*
|
|
36126
|
-
*
|
|
36147
|
+
* Clears all stored notifications.
|
|
36148
|
+
* Proxies call to the underlying notification adapter.
|
|
36127
36149
|
*/
|
|
36128
|
-
this.
|
|
36150
|
+
this.clear = async () => {
|
|
36151
|
+
return await this._notificationLiveUtils.clear();
|
|
36152
|
+
};
|
|
36129
36153
|
/**
|
|
36130
|
-
*
|
|
36131
|
-
*
|
|
36132
|
-
* Automatically calculates the start date based on Date.now() and the requested interval/limit.
|
|
36133
|
-
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
36154
|
+
* Sets the notification adapter constructor.
|
|
36155
|
+
* All future notification operations will use this adapter.
|
|
36134
36156
|
*
|
|
36135
|
-
* @param
|
|
36136
|
-
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
36137
|
-
* @param limit - Maximum number of candles to fetch
|
|
36138
|
-
* @param context - Execution context with exchange name
|
|
36139
|
-
* @returns Promise resolving to array of OHLCV candle data
|
|
36157
|
+
* @param Ctor - Constructor for notification adapter
|
|
36140
36158
|
*/
|
|
36141
|
-
this.
|
|
36142
|
-
bt.
|
|
36143
|
-
|
|
36144
|
-
return await instance.getCandles(symbol, interval, limit);
|
|
36159
|
+
this.useNotificationAdapter = (Ctor) => {
|
|
36160
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_ADAPTER);
|
|
36161
|
+
this._notificationLiveUtils = Reflect.construct(Ctor, []);
|
|
36145
36162
|
};
|
|
36146
36163
|
/**
|
|
36147
|
-
*
|
|
36148
|
-
*
|
|
36149
|
-
* @param symbol - Trading pair symbol
|
|
36150
|
-
* @param context - Execution context with exchange name
|
|
36151
|
-
* @returns Promise resolving to VWAP price
|
|
36164
|
+
* Switches to dummy notification adapter.
|
|
36165
|
+
* All future notification writes will be no-ops.
|
|
36152
36166
|
*/
|
|
36153
|
-
this.
|
|
36154
|
-
bt.
|
|
36155
|
-
|
|
36156
|
-
return await instance.getAveragePrice(symbol);
|
|
36167
|
+
this.useDummy = () => {
|
|
36168
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_DUMMY);
|
|
36169
|
+
this._notificationLiveUtils = new NotificationDummyLiveUtils();
|
|
36157
36170
|
};
|
|
36158
36171
|
/**
|
|
36159
|
-
*
|
|
36160
|
-
*
|
|
36161
|
-
* @param symbol - Trading pair symbol
|
|
36162
|
-
* @param quantity - Raw quantity value
|
|
36163
|
-
* @param context - Execution context with exchange name
|
|
36164
|
-
* @returns Promise resolving to formatted quantity string
|
|
36172
|
+
* Switches to in-memory notification adapter (default).
|
|
36173
|
+
* Notifications will be stored in memory only.
|
|
36165
36174
|
*/
|
|
36166
|
-
this.
|
|
36167
|
-
bt.
|
|
36168
|
-
|
|
36169
|
-
return await instance.formatQuantity(symbol, quantity);
|
|
36175
|
+
this.useMemory = () => {
|
|
36176
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_MEMORY);
|
|
36177
|
+
this._notificationLiveUtils = new NotificationMemoryLiveUtils();
|
|
36170
36178
|
};
|
|
36171
36179
|
/**
|
|
36172
|
-
*
|
|
36173
|
-
*
|
|
36174
|
-
* @param symbol - Trading pair symbol
|
|
36175
|
-
* @param price - Raw price value
|
|
36176
|
-
* @param context - Execution context with exchange name
|
|
36177
|
-
* @returns Promise resolving to formatted price string
|
|
36180
|
+
* Switches to persistent notification adapter.
|
|
36181
|
+
* Notifications will be persisted to disk.
|
|
36178
36182
|
*/
|
|
36179
|
-
this.
|
|
36180
|
-
bt.
|
|
36181
|
-
|
|
36182
|
-
return await instance.formatPrice(symbol, price);
|
|
36183
|
+
this.usePersist = () => {
|
|
36184
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_PERSIST);
|
|
36185
|
+
this._notificationLiveUtils = new NotificationPersistLiveUtils();
|
|
36183
36186
|
};
|
|
36187
|
+
}
|
|
36188
|
+
}
|
|
36189
|
+
/**
|
|
36190
|
+
* Main notification adapter that manages both backtest and live notification storage.
|
|
36191
|
+
*
|
|
36192
|
+
* Features:
|
|
36193
|
+
* - Subscribes to signal emitters for automatic notification updates
|
|
36194
|
+
* - Provides unified access to both backtest and live notifications
|
|
36195
|
+
* - Singleshot enable pattern prevents duplicate subscriptions
|
|
36196
|
+
* - Cleanup function for proper unsubscription
|
|
36197
|
+
*/
|
|
36198
|
+
class NotificationAdapter {
|
|
36199
|
+
constructor() {
|
|
36184
36200
|
/**
|
|
36185
|
-
*
|
|
36186
|
-
*
|
|
36187
|
-
* Delegates to ExchangeInstance which calculates time range and passes it
|
|
36188
|
-
* to the exchange schema implementation. The from/to parameters may be used
|
|
36189
|
-
* (backtest) or ignored (live) depending on the implementation.
|
|
36201
|
+
* Enables notification storage by subscribing to signal emitters.
|
|
36202
|
+
* Uses singleshot to ensure one-time subscription.
|
|
36190
36203
|
*
|
|
36191
|
-
* @
|
|
36192
|
-
* @param context - Execution context with exchange name
|
|
36193
|
-
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
36194
|
-
* @returns Promise resolving to order book data
|
|
36204
|
+
* @returns Cleanup function that unsubscribes from all emitters
|
|
36195
36205
|
*/
|
|
36196
|
-
this.
|
|
36197
|
-
bt.
|
|
36198
|
-
|
|
36199
|
-
|
|
36206
|
+
this.enable = functoolsKit.singleshot(() => {
|
|
36207
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE);
|
|
36208
|
+
let unLive;
|
|
36209
|
+
let unBacktest;
|
|
36210
|
+
{
|
|
36211
|
+
const unBacktestSignal = signalBacktestEmitter.subscribe((data) => NotificationBacktest.handleSignal(data));
|
|
36212
|
+
const unBacktestPartialProfit = partialProfitSubject
|
|
36213
|
+
.filter(({ backtest }) => backtest)
|
|
36214
|
+
.connect((data) => NotificationBacktest.handlePartialProfit(data));
|
|
36215
|
+
const unBacktestPartialLoss = partialLossSubject
|
|
36216
|
+
.filter(({ backtest }) => backtest)
|
|
36217
|
+
.connect((data) => NotificationBacktest.handlePartialLoss(data));
|
|
36218
|
+
const unBacktestBreakeven = breakevenSubject
|
|
36219
|
+
.filter(({ backtest }) => backtest)
|
|
36220
|
+
.connect((data) => NotificationBacktest.handleBreakeven(data));
|
|
36221
|
+
const unBacktestStrategyCommit = strategyCommitSubject
|
|
36222
|
+
.filter(({ backtest }) => backtest)
|
|
36223
|
+
.connect((data) => NotificationBacktest.handleStrategyCommit(data));
|
|
36224
|
+
const unBacktestRisk = riskSubject
|
|
36225
|
+
.filter(({ backtest }) => backtest)
|
|
36226
|
+
.connect((data) => NotificationBacktest.handleRisk(data));
|
|
36227
|
+
const unBacktestError = errorEmitter.subscribe((error) => NotificationBacktest.handleError(error));
|
|
36228
|
+
const unBacktestExit = exitEmitter.subscribe((error) => NotificationBacktest.handleCriticalError(error));
|
|
36229
|
+
const unBacktestValidation = validationSubject.subscribe((error) => NotificationBacktest.handleValidationError(error));
|
|
36230
|
+
unBacktest = functoolsKit.compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation());
|
|
36231
|
+
}
|
|
36232
|
+
{
|
|
36233
|
+
const unLiveSignal = signalLiveEmitter.subscribe((data) => NotificationLive.handleSignal(data));
|
|
36234
|
+
const unLivePartialProfit = partialProfitSubject
|
|
36235
|
+
.filter(({ backtest }) => !backtest)
|
|
36236
|
+
.connect((data) => NotificationLive.handlePartialProfit(data));
|
|
36237
|
+
const unLivePartialLoss = partialLossSubject
|
|
36238
|
+
.filter(({ backtest }) => !backtest)
|
|
36239
|
+
.connect((data) => NotificationLive.handlePartialLoss(data));
|
|
36240
|
+
const unLiveBreakeven = breakevenSubject
|
|
36241
|
+
.filter(({ backtest }) => !backtest)
|
|
36242
|
+
.connect((data) => NotificationLive.handleBreakeven(data));
|
|
36243
|
+
const unLiveStrategyCommit = strategyCommitSubject
|
|
36244
|
+
.filter(({ backtest }) => !backtest)
|
|
36245
|
+
.connect((data) => NotificationLive.handleStrategyCommit(data));
|
|
36246
|
+
const unLiveRisk = riskSubject
|
|
36247
|
+
.filter(({ backtest }) => !backtest)
|
|
36248
|
+
.connect((data) => NotificationLive.handleRisk(data));
|
|
36249
|
+
const unLiveError = errorEmitter.subscribe((error) => NotificationLive.handleError(error));
|
|
36250
|
+
const unLiveExit = exitEmitter.subscribe((error) => NotificationLive.handleCriticalError(error));
|
|
36251
|
+
const unLiveValidation = validationSubject.subscribe((error) => NotificationLive.handleValidationError(error));
|
|
36252
|
+
unLive = functoolsKit.compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation());
|
|
36253
|
+
}
|
|
36254
|
+
return () => {
|
|
36255
|
+
unLive();
|
|
36256
|
+
unBacktest();
|
|
36257
|
+
this.enable.clear();
|
|
36258
|
+
};
|
|
36259
|
+
});
|
|
36260
|
+
/**
|
|
36261
|
+
* Disables notification storage by unsubscribing from all emitters.
|
|
36262
|
+
* Safe to call multiple times.
|
|
36263
|
+
*/
|
|
36264
|
+
this.disable = () => {
|
|
36265
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_DISABLE);
|
|
36266
|
+
if (this.enable.hasValue()) {
|
|
36267
|
+
const lastSubscription = this.enable();
|
|
36268
|
+
lastSubscription();
|
|
36269
|
+
}
|
|
36200
36270
|
};
|
|
36201
36271
|
/**
|
|
36202
|
-
*
|
|
36272
|
+
* Gets all backtest/live notifications from storage.
|
|
36203
36273
|
*
|
|
36204
|
-
*
|
|
36274
|
+
* @returns Array of all backtest notification models
|
|
36275
|
+
* @throws Error if NotificationAdapter is not enabled
|
|
36276
|
+
*/
|
|
36277
|
+
this.getData = async (isBacktest) => {
|
|
36278
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_BACKTEST, {
|
|
36279
|
+
backtest: isBacktest,
|
|
36280
|
+
});
|
|
36281
|
+
if (!this.enable.hasValue()) {
|
|
36282
|
+
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
36283
|
+
}
|
|
36284
|
+
if (isBacktest) {
|
|
36285
|
+
return await NotificationBacktest.getData();
|
|
36286
|
+
}
|
|
36287
|
+
return await NotificationLive.getData();
|
|
36288
|
+
};
|
|
36289
|
+
/**
|
|
36290
|
+
* Clears all backtest/live notifications from storage.
|
|
36205
36291
|
*
|
|
36206
|
-
* @
|
|
36207
|
-
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
36208
|
-
* @param context - Execution context with exchange name
|
|
36209
|
-
* @param limit - Optional number of candles to fetch
|
|
36210
|
-
* @param sDate - Optional start date in milliseconds
|
|
36211
|
-
* @param eDate - Optional end date in milliseconds
|
|
36212
|
-
* @returns Promise resolving to array of candle data
|
|
36292
|
+
* @throws Error if NotificationAdapter is not enabled
|
|
36213
36293
|
*/
|
|
36214
|
-
this.
|
|
36215
|
-
bt.
|
|
36216
|
-
|
|
36217
|
-
|
|
36294
|
+
this.clear = async (isBacktest) => {
|
|
36295
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_LIVE, {
|
|
36296
|
+
backtest: isBacktest,
|
|
36297
|
+
});
|
|
36298
|
+
if (!this.enable.hasValue()) {
|
|
36299
|
+
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
36300
|
+
}
|
|
36301
|
+
if (isBacktest) {
|
|
36302
|
+
return await NotificationBacktest.clear();
|
|
36303
|
+
}
|
|
36304
|
+
return await NotificationLive.clear();
|
|
36218
36305
|
};
|
|
36219
36306
|
}
|
|
36220
36307
|
}
|
|
36221
36308
|
/**
|
|
36222
|
-
*
|
|
36223
|
-
*
|
|
36224
|
-
* @example
|
|
36225
|
-
* ```typescript
|
|
36226
|
-
* import { Exchange } from "./classes/Exchange";
|
|
36227
|
-
*
|
|
36228
|
-
* // Using static-like API with context
|
|
36229
|
-
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
36230
|
-
* exchangeName: "binance"
|
|
36231
|
-
* });
|
|
36232
|
-
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
36233
|
-
* exchangeName: "binance"
|
|
36234
|
-
* });
|
|
36235
|
-
* const qty = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
36236
|
-
* exchangeName: "binance"
|
|
36237
|
-
* });
|
|
36238
|
-
* const price = await Exchange.formatPrice("BTCUSDT", 50000.123, {
|
|
36239
|
-
* exchangeName: "binance"
|
|
36240
|
-
* });
|
|
36241
|
-
*
|
|
36242
|
-
* // Using instance API (no context needed, exchange set in constructor)
|
|
36243
|
-
* const binance = new ExchangeInstance("binance");
|
|
36244
|
-
* const candles2 = await binance.getCandles("BTCUSDT", "1m", 100);
|
|
36245
|
-
* const vwap2 = await binance.getAveragePrice("BTCUSDT");
|
|
36246
|
-
* ```
|
|
36309
|
+
* Global singleton instance of NotificationAdapter.
|
|
36310
|
+
* Provides unified notification management for backtest and live trading.
|
|
36247
36311
|
*/
|
|
36248
|
-
const
|
|
36312
|
+
const Notification = new NotificationAdapter();
|
|
36313
|
+
/**
|
|
36314
|
+
* Global singleton instance of NotificationLiveAdapter.
|
|
36315
|
+
* Provides live trading notification storage with pluggable backends.
|
|
36316
|
+
*/
|
|
36317
|
+
const NotificationLive = new NotificationLiveAdapter();
|
|
36318
|
+
/**
|
|
36319
|
+
* Global singleton instance of NotificationBacktestAdapter.
|
|
36320
|
+
* Provides backtest notification storage with pluggable backends.
|
|
36321
|
+
*/
|
|
36322
|
+
const NotificationBacktest = new NotificationBacktestAdapter();
|
|
36249
36323
|
|
|
36250
36324
|
const CACHE_METHOD_NAME_FLUSH = "CacheUtils.flush";
|
|
36251
36325
|
const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
|
|
@@ -37312,3 +37386,4 @@ exports.setConfig = setConfig;
|
|
|
37312
37386
|
exports.setLogger = setLogger;
|
|
37313
37387
|
exports.stopStrategy = stopStrategy;
|
|
37314
37388
|
exports.validate = validate;
|
|
37389
|
+
exports.warmCandles = warmCandles;
|