backtest-kit 2.3.1 → 2.3.3
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 +85 -54
- package/build/index.cjs +928 -266
- package/build/index.mjs +929 -268
- package/package.json +2 -2
- package/types.d.ts +383 -17
package/build/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createActivator } from 'di-kit';
|
|
2
2
|
import { scoped } from 'di-scoped';
|
|
3
|
-
import { Subject, makeExtendable, singleshot, getErrorMessage, memoize,
|
|
3
|
+
import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
|
|
4
4
|
import * as fs from 'fs/promises';
|
|
5
5
|
import fs__default from 'fs/promises';
|
|
6
6
|
import path, { join, dirname } from 'path';
|
|
@@ -1558,73 +1558,73 @@ class PersistCandleUtils {
|
|
|
1558
1558
|
]));
|
|
1559
1559
|
/**
|
|
1560
1560
|
* Reads cached candles for a specific exchange, symbol, and interval.
|
|
1561
|
-
* Returns candles only if cache contains
|
|
1561
|
+
* Returns candles only if cache contains ALL requested candles.
|
|
1562
1562
|
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1563
|
+
* Algorithm (matches ClientExchange.ts logic):
|
|
1564
|
+
* 1. Calculate expected timestamps: sinceTimestamp, sinceTimestamp + stepMs, ..., sinceTimestamp + (limit-1) * stepMs
|
|
1565
|
+
* 2. Try to read each expected candle by timestamp key
|
|
1566
|
+
* 3. If ANY candle is missing, return null (cache miss)
|
|
1567
|
+
* 4. If all candles found, return them in order
|
|
1567
1568
|
*
|
|
1568
1569
|
* @param symbol - Trading pair symbol
|
|
1569
1570
|
* @param interval - Candle interval
|
|
1570
1571
|
* @param exchangeName - Exchange identifier
|
|
1571
1572
|
* @param limit - Number of candles requested
|
|
1572
|
-
* @param sinceTimestamp -
|
|
1573
|
-
* @param
|
|
1573
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1574
|
+
* @param _untilTimestamp - Unused, kept for API compatibility
|
|
1574
1575
|
* @returns Promise resolving to array of candles or null if cache is incomplete
|
|
1575
1576
|
*/
|
|
1576
|
-
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp,
|
|
1577
|
+
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, _untilTimestamp) => {
|
|
1577
1578
|
bt.loggerService.info("PersistCandleUtils.readCandlesData", {
|
|
1578
1579
|
symbol,
|
|
1579
1580
|
interval,
|
|
1580
1581
|
exchangeName,
|
|
1581
1582
|
limit,
|
|
1582
1583
|
sinceTimestamp,
|
|
1583
|
-
untilTimestamp,
|
|
1584
1584
|
});
|
|
1585
1585
|
const key = `${symbol}:${interval}:${exchangeName}`;
|
|
1586
1586
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1587
1587
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1588
1588
|
await stateStorage.waitForInit(isInitial);
|
|
1589
1589
|
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1590
|
-
//
|
|
1590
|
+
// Calculate expected timestamps and fetch each candle directly
|
|
1591
1591
|
const cachedCandles = [];
|
|
1592
|
-
for
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1592
|
+
for (let i = 0; i < limit; i++) {
|
|
1593
|
+
const expectedTimestamp = sinceTimestamp + i * stepMs;
|
|
1594
|
+
const timestampKey = String(expectedTimestamp);
|
|
1595
|
+
if (await not(stateStorage.hasValue(timestampKey))) {
|
|
1596
|
+
// Cache miss - candle not found
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
const candle = await stateStorage.readValue(timestampKey);
|
|
1601
|
+
cachedCandles.push(candle);
|
|
1602
|
+
}
|
|
1603
|
+
catch (error) {
|
|
1604
|
+
// Invalid candle in cache - treat as cache miss
|
|
1605
|
+
const message = `PersistCandleUtils.readCandlesData found invalid candle symbol=${symbol} interval=${interval} timestamp=${expectedTimestamp}`;
|
|
1606
|
+
const payload = {
|
|
1607
|
+
error: errorData(error),
|
|
1608
|
+
message: getErrorMessage(error),
|
|
1609
|
+
};
|
|
1610
|
+
bt.loggerService.warn(message, payload);
|
|
1611
|
+
console.warn(message, payload);
|
|
1612
|
+
errorEmitter.next(error);
|
|
1613
|
+
return null;
|
|
1613
1614
|
}
|
|
1614
1615
|
}
|
|
1615
|
-
// Sort by timestamp ascending
|
|
1616
|
-
cachedCandles.sort((a, b) => a.timestamp - b.timestamp);
|
|
1617
1616
|
return cachedCandles;
|
|
1618
1617
|
};
|
|
1619
1618
|
/**
|
|
1620
1619
|
* Writes candles to cache with atomic file writes.
|
|
1621
1620
|
* Each candle is stored as a separate JSON file named by its timestamp.
|
|
1622
1621
|
*
|
|
1623
|
-
* The candles passed to this function
|
|
1624
|
-
* - candle.timestamp
|
|
1625
|
-
* -
|
|
1622
|
+
* The candles passed to this function should be validated candles from the adapter:
|
|
1623
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1624
|
+
* - Exact number of candles as requested
|
|
1625
|
+
* - All candles are fully closed (timestamp + stepMs < untilTimestamp)
|
|
1626
1626
|
*
|
|
1627
|
-
* @param candles - Array of candle data to cache (
|
|
1627
|
+
* @param candles - Array of candle data to cache (validated by the caller)
|
|
1628
1628
|
* @param symbol - Trading pair symbol
|
|
1629
1629
|
* @param interval - Candle interval
|
|
1630
1630
|
* @param exchangeName - Exchange identifier
|
|
@@ -1641,8 +1641,25 @@ class PersistCandleUtils {
|
|
|
1641
1641
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1642
1642
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1643
1643
|
await stateStorage.waitForInit(isInitial);
|
|
1644
|
-
//
|
|
1644
|
+
// Calculate step in milliseconds to determine candle close time
|
|
1645
|
+
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1646
|
+
const now = Date.now();
|
|
1647
|
+
// Write each candle as a separate file, skipping incomplete candles
|
|
1645
1648
|
for (const candle of candles) {
|
|
1649
|
+
// Skip incomplete candles: candle is complete when closeTime <= now
|
|
1650
|
+
// closeTime = timestamp + stepMs
|
|
1651
|
+
const candleCloseTime = candle.timestamp + stepMs;
|
|
1652
|
+
if (candleCloseTime > now) {
|
|
1653
|
+
bt.loggerService.debug("PersistCandleUtils.writeCandlesData: skipping incomplete candle", {
|
|
1654
|
+
symbol,
|
|
1655
|
+
interval,
|
|
1656
|
+
exchangeName,
|
|
1657
|
+
timestamp: candle.timestamp,
|
|
1658
|
+
closeTime: candleCloseTime,
|
|
1659
|
+
now,
|
|
1660
|
+
});
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1646
1663
|
if (await not(stateStorage.hasValue(String(candle.timestamp)))) {
|
|
1647
1664
|
await stateStorage.writeValue(String(candle.timestamp), candle);
|
|
1648
1665
|
}
|
|
@@ -1800,6 +1817,27 @@ const INTERVAL_MINUTES$4 = {
|
|
|
1800
1817
|
"6h": 360,
|
|
1801
1818
|
"8h": 480,
|
|
1802
1819
|
};
|
|
1820
|
+
/**
|
|
1821
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
1822
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
1823
|
+
*
|
|
1824
|
+
* Candle timestamp convention:
|
|
1825
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
1826
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
1827
|
+
*
|
|
1828
|
+
* Adapter contract:
|
|
1829
|
+
* - Adapter must return candles with timestamp = openTime
|
|
1830
|
+
* - First returned candle.timestamp must equal aligned since
|
|
1831
|
+
* - Adapter must return exactly `limit` candles
|
|
1832
|
+
*
|
|
1833
|
+
* @param timestamp - Timestamp in milliseconds
|
|
1834
|
+
* @param intervalMinutes - Interval in minutes
|
|
1835
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
1836
|
+
*/
|
|
1837
|
+
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
1838
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
1839
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
1840
|
+
};
|
|
1803
1841
|
/**
|
|
1804
1842
|
* Validates that all candles have valid OHLCV data without anomalies.
|
|
1805
1843
|
* Detects incomplete candles from Binance API by checking for abnormally low prices or volumes.
|
|
@@ -1863,25 +1901,24 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
|
1863
1901
|
};
|
|
1864
1902
|
/**
|
|
1865
1903
|
* Attempts to read candles from cache.
|
|
1866
|
-
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
1867
1904
|
*
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
* - Only fully closed candles within the exclusive range are returned
|
|
1905
|
+
* Cache lookup calculates expected timestamps:
|
|
1906
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1907
|
+
* Returns all candles if found, null if any missing.
|
|
1872
1908
|
*
|
|
1873
1909
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1874
|
-
* @param sinceTimestamp -
|
|
1875
|
-
* @param untilTimestamp -
|
|
1910
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1911
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
1876
1912
|
* @param self - Instance of ClientExchange
|
|
1877
|
-
* @returns Cached candles array or null if cache miss
|
|
1913
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
1878
1914
|
*/
|
|
1879
1915
|
const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
|
|
1880
|
-
// PersistCandleAdapter.readCandlesData
|
|
1881
|
-
//
|
|
1916
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
1917
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1918
|
+
// Returns all candles if found, null if any missing
|
|
1882
1919
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
1883
1920
|
// Return cached data only if we have exactly the requested limit
|
|
1884
|
-
if (cachedCandles
|
|
1921
|
+
if (cachedCandles?.length === dto.limit) {
|
|
1885
1922
|
self.params.logger.debug(`ClientExchange READ_CANDLES_CACHE_FN: cache hit for symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
1886
1923
|
return cachedCandles;
|
|
1887
1924
|
}
|
|
@@ -1903,11 +1940,12 @@ const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimest
|
|
|
1903
1940
|
/**
|
|
1904
1941
|
* Writes candles to cache with error handling.
|
|
1905
1942
|
*
|
|
1906
|
-
* The candles passed to this function
|
|
1907
|
-
* - candle.timestamp
|
|
1908
|
-
* -
|
|
1943
|
+
* The candles passed to this function should be validated:
|
|
1944
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1945
|
+
* - Exact number of candles as requested (limit)
|
|
1946
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
1909
1947
|
*
|
|
1910
|
-
* @param candles - Array of candle data to cache
|
|
1948
|
+
* @param candles - Array of validated candle data to cache
|
|
1911
1949
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1912
1950
|
* @param self - Instance of ClientExchange
|
|
1913
1951
|
*/
|
|
@@ -2034,6 +2072,13 @@ class ClientExchange {
|
|
|
2034
2072
|
/**
|
|
2035
2073
|
* Fetches historical candles backwards from execution context time.
|
|
2036
2074
|
*
|
|
2075
|
+
* Algorithm:
|
|
2076
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2077
|
+
* 2. Calculate since = alignedWhen - limit * step
|
|
2078
|
+
* 3. Fetch candles starting from since
|
|
2079
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2080
|
+
* 5. Slice to limit
|
|
2081
|
+
*
|
|
2037
2082
|
* @param symbol - Trading pair symbol
|
|
2038
2083
|
* @param interval - Candle interval
|
|
2039
2084
|
* @param limit - Number of candles to fetch
|
|
@@ -2046,11 +2091,16 @@ class ClientExchange {
|
|
|
2046
2091
|
limit,
|
|
2047
2092
|
});
|
|
2048
2093
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
2094
|
+
if (!step) {
|
|
2095
|
+
throw new Error(`ClientExchange unknown interval=${interval}`);
|
|
2052
2096
|
}
|
|
2053
|
-
const
|
|
2097
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2098
|
+
// Align when down to interval boundary
|
|
2099
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2100
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2101
|
+
// Calculate since: go back limit candles from aligned when
|
|
2102
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2103
|
+
const since = new Date(sinceTimestamp);
|
|
2054
2104
|
let allData = [];
|
|
2055
2105
|
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
2056
2106
|
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
@@ -2063,39 +2113,34 @@ class ClientExchange {
|
|
|
2063
2113
|
remaining -= chunkLimit;
|
|
2064
2114
|
if (remaining > 0) {
|
|
2065
2115
|
// Move currentSince forward by the number of candles fetched
|
|
2066
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2116
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2067
2117
|
}
|
|
2068
2118
|
}
|
|
2069
2119
|
}
|
|
2070
2120
|
else {
|
|
2071
2121
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2072
2122
|
}
|
|
2073
|
-
// Filter candles to strictly match the requested range
|
|
2074
|
-
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2075
|
-
const sinceTimestamp = since.getTime();
|
|
2076
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
2077
|
-
const filteredData = allData.filter((candle) => {
|
|
2078
|
-
// EXCLUSIVE boundaries:
|
|
2079
|
-
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
2080
|
-
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
2081
|
-
if (candle.timestamp <= sinceTimestamp) {
|
|
2082
|
-
return false;
|
|
2083
|
-
}
|
|
2084
|
-
// Check against current time (when)
|
|
2085
|
-
// Only allow candles that have fully CLOSED before "when"
|
|
2086
|
-
return candle.timestamp + stepMs < whenTimestamp;
|
|
2087
|
-
});
|
|
2088
2123
|
// Apply distinct by timestamp to remove duplicates
|
|
2089
|
-
const uniqueData = Array.from(new Map(
|
|
2090
|
-
if (
|
|
2091
|
-
const msg = `ClientExchange Removed ${
|
|
2124
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2125
|
+
if (allData.length !== uniqueData.length) {
|
|
2126
|
+
const msg = `ClientExchange getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2092
2127
|
this.params.logger.warn(msg);
|
|
2093
2128
|
console.warn(msg);
|
|
2094
2129
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2130
|
+
// Validate adapter returned data
|
|
2131
|
+
if (uniqueData.length === 0) {
|
|
2132
|
+
throw new Error(`ClientExchange getCandles: adapter returned empty array. ` +
|
|
2133
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2134
|
+
}
|
|
2135
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2136
|
+
throw new Error(`ClientExchange getCandles: first candle timestamp mismatch. ` +
|
|
2137
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2138
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2139
|
+
}
|
|
2140
|
+
if (uniqueData.length !== limit) {
|
|
2141
|
+
throw new Error(`ClientExchange getCandles: candle count mismatch. ` +
|
|
2142
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2143
|
+
`Adapter must return exact number of candles requested.`);
|
|
2099
2144
|
}
|
|
2100
2145
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2101
2146
|
return uniqueData;
|
|
@@ -2104,6 +2149,13 @@ class ClientExchange {
|
|
|
2104
2149
|
* Fetches future candles forwards from execution context time.
|
|
2105
2150
|
* Used in backtest mode to get candles for signal duration.
|
|
2106
2151
|
*
|
|
2152
|
+
* Algorithm:
|
|
2153
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2154
|
+
* 2. since = alignedWhen (start from aligned when)
|
|
2155
|
+
* 3. Fetch candles starting from since
|
|
2156
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2157
|
+
* 5. Slice to limit
|
|
2158
|
+
*
|
|
2107
2159
|
* @param symbol - Trading pair symbol
|
|
2108
2160
|
* @param interval - Candle interval
|
|
2109
2161
|
* @param limit - Number of candles to fetch
|
|
@@ -2119,12 +2171,21 @@ class ClientExchange {
|
|
|
2119
2171
|
if (!this.params.execution.context.backtest) {
|
|
2120
2172
|
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2121
2173
|
}
|
|
2122
|
-
const since = new Date(this.params.execution.context.when.getTime());
|
|
2123
|
-
const now = Date.now();
|
|
2124
|
-
// Вычисляем конечное время запроса
|
|
2125
2174
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2126
|
-
|
|
2127
|
-
|
|
2175
|
+
if (!step) {
|
|
2176
|
+
throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
|
|
2177
|
+
}
|
|
2178
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2179
|
+
const now = Date.now();
|
|
2180
|
+
// Align when down to interval boundary
|
|
2181
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2182
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2183
|
+
// since = alignedWhen (start from aligned when, going forward)
|
|
2184
|
+
const sinceTimestamp = alignedWhen;
|
|
2185
|
+
const since = new Date(sinceTimestamp);
|
|
2186
|
+
// Calculate end time for Date.now() check
|
|
2187
|
+
const endTime = sinceTimestamp + limit * stepMs;
|
|
2188
|
+
// Check that requested period does not exceed Date.now()
|
|
2128
2189
|
if (endTime > now) {
|
|
2129
2190
|
return [];
|
|
2130
2191
|
}
|
|
@@ -2140,29 +2201,34 @@ class ClientExchange {
|
|
|
2140
2201
|
remaining -= chunkLimit;
|
|
2141
2202
|
if (remaining > 0) {
|
|
2142
2203
|
// Move currentSince forward by the number of candles fetched
|
|
2143
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2204
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2144
2205
|
}
|
|
2145
2206
|
}
|
|
2146
2207
|
}
|
|
2147
2208
|
else {
|
|
2148
2209
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2149
2210
|
}
|
|
2150
|
-
// Filter candles to strictly match the requested range
|
|
2151
|
-
const sinceTimestamp = since.getTime();
|
|
2152
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
2153
|
-
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2154
|
-
candle.timestamp + stepMs < endTime);
|
|
2155
2211
|
// Apply distinct by timestamp to remove duplicates
|
|
2156
|
-
const uniqueData = Array.from(new Map(
|
|
2157
|
-
if (
|
|
2158
|
-
const msg = `ClientExchange getNextCandles: Removed ${
|
|
2212
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2213
|
+
if (allData.length !== uniqueData.length) {
|
|
2214
|
+
const msg = `ClientExchange getNextCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2159
2215
|
this.params.logger.warn(msg);
|
|
2160
2216
|
console.warn(msg);
|
|
2161
2217
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2218
|
+
// Validate adapter returned data
|
|
2219
|
+
if (uniqueData.length === 0) {
|
|
2220
|
+
throw new Error(`ClientExchange getNextCandles: adapter returned empty array. ` +
|
|
2221
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2222
|
+
}
|
|
2223
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2224
|
+
throw new Error(`ClientExchange getNextCandles: first candle timestamp mismatch. ` +
|
|
2225
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2226
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2227
|
+
}
|
|
2228
|
+
if (uniqueData.length !== limit) {
|
|
2229
|
+
throw new Error(`ClientExchange getNextCandles: candle count mismatch. ` +
|
|
2230
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2231
|
+
`Adapter must return exact number of candles requested.`);
|
|
2166
2232
|
}
|
|
2167
2233
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2168
2234
|
return uniqueData;
|
|
@@ -2237,6 +2303,12 @@ class ClientExchange {
|
|
|
2237
2303
|
/**
|
|
2238
2304
|
* Fetches raw candles with flexible date/limit parameters.
|
|
2239
2305
|
*
|
|
2306
|
+
* Algorithm:
|
|
2307
|
+
* 1. Align all timestamps down to interval boundary
|
|
2308
|
+
* 2. Fetch candles starting from aligned since
|
|
2309
|
+
* 3. Validate first candle timestamp matches aligned since (adapter must return inclusive data)
|
|
2310
|
+
* 4. Slice to limit
|
|
2311
|
+
*
|
|
2240
2312
|
* All modes respect execution context and prevent look-ahead bias.
|
|
2241
2313
|
*
|
|
2242
2314
|
* Parameter combinations:
|
|
@@ -2271,9 +2343,10 @@ class ClientExchange {
|
|
|
2271
2343
|
if (!step) {
|
|
2272
2344
|
throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
|
|
2273
2345
|
}
|
|
2346
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2274
2347
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2348
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2275
2349
|
let sinceTimestamp;
|
|
2276
|
-
let untilTimestamp;
|
|
2277
2350
|
let calculatedLimit;
|
|
2278
2351
|
// Case 1: all three parameters provided
|
|
2279
2352
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -2283,8 +2356,8 @@ class ClientExchange {
|
|
|
2283
2356
|
if (eDate > whenTimestamp) {
|
|
2284
2357
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2285
2358
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2359
|
+
// Align sDate down to interval boundary
|
|
2360
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2288
2361
|
calculatedLimit = limit;
|
|
2289
2362
|
}
|
|
2290
2363
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -2295,9 +2368,10 @@ class ClientExchange {
|
|
|
2295
2368
|
if (eDate > whenTimestamp) {
|
|
2296
2369
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2297
2370
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2371
|
+
// Align sDate down to interval boundary
|
|
2372
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2373
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2374
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
2301
2375
|
if (calculatedLimit <= 0) {
|
|
2302
2376
|
throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
2303
2377
|
}
|
|
@@ -2307,23 +2381,24 @@ class ClientExchange {
|
|
|
2307
2381
|
if (eDate > whenTimestamp) {
|
|
2308
2382
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2309
2383
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2384
|
+
// Align eDate down and calculate sinceTimestamp
|
|
2385
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2386
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
2312
2387
|
calculatedLimit = limit;
|
|
2313
2388
|
}
|
|
2314
2389
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
2315
2390
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2391
|
+
// Align sDate down to interval boundary
|
|
2392
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2393
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
2394
|
+
if (endTimestamp > whenTimestamp) {
|
|
2395
|
+
throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2320
2396
|
}
|
|
2321
2397
|
calculatedLimit = limit;
|
|
2322
2398
|
}
|
|
2323
2399
|
// Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
|
|
2324
2400
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
2325
|
-
|
|
2326
|
-
sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
|
|
2401
|
+
sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2327
2402
|
calculatedLimit = limit;
|
|
2328
2403
|
}
|
|
2329
2404
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -2344,29 +2419,34 @@ class ClientExchange {
|
|
|
2344
2419
|
allData.push(...chunkData);
|
|
2345
2420
|
remaining -= chunkLimit;
|
|
2346
2421
|
if (remaining > 0) {
|
|
2347
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2422
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2348
2423
|
}
|
|
2349
2424
|
}
|
|
2350
2425
|
}
|
|
2351
2426
|
else {
|
|
2352
2427
|
allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
|
|
2353
2428
|
}
|
|
2354
|
-
// Filter candles to strictly match the requested range
|
|
2355
|
-
// Only include candles that have fully CLOSED before untilTimestamp
|
|
2356
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
2357
|
-
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2358
|
-
candle.timestamp + stepMs < untilTimestamp);
|
|
2359
2429
|
// Apply distinct by timestamp to remove duplicates
|
|
2360
|
-
const uniqueData = Array.from(new Map(
|
|
2361
|
-
if (
|
|
2362
|
-
const msg = `ClientExchange getRawCandles: Removed ${
|
|
2430
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2431
|
+
if (allData.length !== uniqueData.length) {
|
|
2432
|
+
const msg = `ClientExchange getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2363
2433
|
this.params.logger.warn(msg);
|
|
2364
2434
|
console.warn(msg);
|
|
2365
2435
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2436
|
+
// Validate adapter returned data
|
|
2437
|
+
if (uniqueData.length === 0) {
|
|
2438
|
+
throw new Error(`ClientExchange getRawCandles: adapter returned empty array. ` +
|
|
2439
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2440
|
+
}
|
|
2441
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2442
|
+
throw new Error(`ClientExchange getRawCandles: first candle timestamp mismatch. ` +
|
|
2443
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2444
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2445
|
+
}
|
|
2446
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
2447
|
+
throw new Error(`ClientExchange getRawCandles: candle count mismatch. ` +
|
|
2448
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
2449
|
+
`Adapter must return exact number of candles requested.`);
|
|
2370
2450
|
}
|
|
2371
2451
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
|
|
2372
2452
|
return uniqueData;
|
|
@@ -4924,6 +5004,68 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
4924
5004
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "user");
|
|
4925
5005
|
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
4926
5006
|
}
|
|
5007
|
+
// КРИТИЧНО: Проверяем был ли сигнал активирован пользователем через activateScheduled()
|
|
5008
|
+
// Обрабатываем inline (как в tick()) с риск-проверкой по averagePrice
|
|
5009
|
+
if (self._activatedSignal) {
|
|
5010
|
+
const activatedSignal = self._activatedSignal;
|
|
5011
|
+
self._activatedSignal = null;
|
|
5012
|
+
// Check if strategy was stopped
|
|
5013
|
+
if (self._isStopped) {
|
|
5014
|
+
self.params.logger.info("ClientStrategy backtest user-activated signal cancelled (stopped)", {
|
|
5015
|
+
symbol: self.params.execution.context.symbol,
|
|
5016
|
+
signalId: activatedSignal.id,
|
|
5017
|
+
});
|
|
5018
|
+
await self.setScheduledSignal(null);
|
|
5019
|
+
return { activated: false, cancelled: false, activationIndex: i, result: null };
|
|
5020
|
+
}
|
|
5021
|
+
// Риск-проверка по averagePrice (симметрия с LIVE tick())
|
|
5022
|
+
if (await not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, averagePrice, candle.timestamp, self.params.execution.context.backtest))) {
|
|
5023
|
+
self.params.logger.info("ClientStrategy backtest user-activated signal rejected by risk", {
|
|
5024
|
+
symbol: self.params.execution.context.symbol,
|
|
5025
|
+
signalId: activatedSignal.id,
|
|
5026
|
+
});
|
|
5027
|
+
await self.setScheduledSignal(null);
|
|
5028
|
+
return { activated: false, cancelled: false, activationIndex: i, result: null };
|
|
5029
|
+
}
|
|
5030
|
+
await self.setScheduledSignal(null);
|
|
5031
|
+
const pendingSignal = {
|
|
5032
|
+
...activatedSignal,
|
|
5033
|
+
pendingAt: candle.timestamp,
|
|
5034
|
+
_isScheduled: false,
|
|
5035
|
+
};
|
|
5036
|
+
await self.setPendingSignal(pendingSignal);
|
|
5037
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
5038
|
+
// Emit commit AFTER successful risk check
|
|
5039
|
+
const publicSignalForCommit = TO_PUBLIC_SIGNAL(pendingSignal);
|
|
5040
|
+
await CALL_COMMIT_FN(self, {
|
|
5041
|
+
action: "activate-scheduled",
|
|
5042
|
+
symbol: self.params.execution.context.symbol,
|
|
5043
|
+
strategyName: self.params.strategyName,
|
|
5044
|
+
exchangeName: self.params.exchangeName,
|
|
5045
|
+
frameName: self.params.frameName,
|
|
5046
|
+
signalId: activatedSignal.id,
|
|
5047
|
+
backtest: self.params.execution.context.backtest,
|
|
5048
|
+
activateId: activatedSignal.activateId,
|
|
5049
|
+
timestamp: candle.timestamp,
|
|
5050
|
+
currentPrice: averagePrice,
|
|
5051
|
+
position: publicSignalForCommit.position,
|
|
5052
|
+
priceOpen: publicSignalForCommit.priceOpen,
|
|
5053
|
+
priceTakeProfit: publicSignalForCommit.priceTakeProfit,
|
|
5054
|
+
priceStopLoss: publicSignalForCommit.priceStopLoss,
|
|
5055
|
+
originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
|
|
5056
|
+
originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
|
|
5057
|
+
scheduledAt: publicSignalForCommit.scheduledAt,
|
|
5058
|
+
pendingAt: publicSignalForCommit.pendingAt,
|
|
5059
|
+
});
|
|
5060
|
+
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
|
|
5061
|
+
await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
5062
|
+
return {
|
|
5063
|
+
activated: true,
|
|
5064
|
+
cancelled: false,
|
|
5065
|
+
activationIndex: i,
|
|
5066
|
+
result: null,
|
|
5067
|
+
};
|
|
5068
|
+
}
|
|
4927
5069
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
4928
5070
|
const elapsedTime = candle.timestamp - scheduled.scheduledAt;
|
|
4929
5071
|
if (elapsedTime >= maxTimeToWait) {
|
|
@@ -5146,6 +5288,7 @@ class ClientStrategy {
|
|
|
5146
5288
|
this._scheduledSignal = null;
|
|
5147
5289
|
this._cancelledSignal = null;
|
|
5148
5290
|
this._closedSignal = null;
|
|
5291
|
+
this._activatedSignal = null;
|
|
5149
5292
|
/** Queue for commit events to be processed in tick()/backtest() with proper timestamp */
|
|
5150
5293
|
this._commitQueue = [];
|
|
5151
5294
|
/**
|
|
@@ -5183,6 +5326,16 @@ class ClientStrategy {
|
|
|
5183
5326
|
this.params.logger.debug("ClientStrategy setPendingSignal", {
|
|
5184
5327
|
pendingSignal,
|
|
5185
5328
|
});
|
|
5329
|
+
// КРИТИЧНО: Очищаем флаг закрытия при любом изменении pending signal
|
|
5330
|
+
// - при null: сигнал закрыт по TP/SL/timeout, флаг больше не нужен
|
|
5331
|
+
// - при новом сигнале: флаг от предыдущего сигнала не должен влиять на новый
|
|
5332
|
+
this._closedSignal = null;
|
|
5333
|
+
// ЗАЩИТА ИНВАРИАНТА: При установке нового pending сигнала очищаем scheduled
|
|
5334
|
+
// Не может быть одновременно pending И scheduled (взаимоисключающие состояния)
|
|
5335
|
+
// При null: scheduled может существовать (новый сигнал после закрытия позиции)
|
|
5336
|
+
if (pendingSignal !== null) {
|
|
5337
|
+
this._scheduledSignal = null;
|
|
5338
|
+
}
|
|
5186
5339
|
this._pendingSignal = pendingSignal;
|
|
5187
5340
|
// КРИТИЧНО: Всегда вызываем коллбек onWrite для тестирования persist storage
|
|
5188
5341
|
// даже в backtest режиме, чтобы тесты могли перехватывать вызовы через mock adapter
|
|
@@ -5208,6 +5361,11 @@ class ClientStrategy {
|
|
|
5208
5361
|
this.params.logger.debug("ClientStrategy setScheduledSignal", {
|
|
5209
5362
|
scheduledSignal,
|
|
5210
5363
|
});
|
|
5364
|
+
// КРИТИЧНО: Очищаем флаги отмены и активации при любом изменении scheduled signal
|
|
5365
|
+
// - при null: сигнал отменен/активирован по timeout/SL/user, флаги больше не нужны
|
|
5366
|
+
// - при новом сигнале: флаги от предыдущего сигнала не должны влиять на новый
|
|
5367
|
+
this._cancelledSignal = null;
|
|
5368
|
+
this._activatedSignal = null;
|
|
5211
5369
|
this._scheduledSignal = scheduledSignal;
|
|
5212
5370
|
if (this.params.execution.context.backtest) {
|
|
5213
5371
|
return;
|
|
@@ -5397,12 +5555,8 @@ class ClientStrategy {
|
|
|
5397
5555
|
const currentTime = this.params.execution.context.when.getTime();
|
|
5398
5556
|
// Process queued commit events with proper timestamp
|
|
5399
5557
|
await PROCESS_COMMIT_QUEUE_FN(this, currentTime);
|
|
5400
|
-
// Early return if strategy was stopped
|
|
5401
|
-
if (this._isStopped) {
|
|
5402
|
-
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5403
|
-
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5404
|
-
}
|
|
5405
5558
|
// Check if scheduled signal was cancelled - emit cancelled event once
|
|
5559
|
+
// NOTE: No _isStopped check here - cancellation must work for graceful shutdown
|
|
5406
5560
|
if (this._cancelledSignal) {
|
|
5407
5561
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5408
5562
|
const cancelledSignal = this._cancelledSignal;
|
|
@@ -5411,6 +5565,18 @@ class ClientStrategy {
|
|
|
5411
5565
|
symbol: this.params.execution.context.symbol,
|
|
5412
5566
|
signalId: cancelledSignal.id,
|
|
5413
5567
|
});
|
|
5568
|
+
// Emit commit with correct timestamp from tick context
|
|
5569
|
+
await CALL_COMMIT_FN(this, {
|
|
5570
|
+
action: "cancel-scheduled",
|
|
5571
|
+
symbol: this.params.execution.context.symbol,
|
|
5572
|
+
strategyName: this.params.strategyName,
|
|
5573
|
+
exchangeName: this.params.exchangeName,
|
|
5574
|
+
frameName: this.params.frameName,
|
|
5575
|
+
signalId: cancelledSignal.id,
|
|
5576
|
+
backtest: this.params.execution.context.backtest,
|
|
5577
|
+
cancelId: cancelledSignal.cancelId,
|
|
5578
|
+
timestamp: currentTime,
|
|
5579
|
+
});
|
|
5414
5580
|
// Call onCancel callback
|
|
5415
5581
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
5416
5582
|
const result = {
|
|
@@ -5439,6 +5605,18 @@ class ClientStrategy {
|
|
|
5439
5605
|
symbol: this.params.execution.context.symbol,
|
|
5440
5606
|
signalId: closedSignal.id,
|
|
5441
5607
|
});
|
|
5608
|
+
// Emit commit with correct timestamp from tick context
|
|
5609
|
+
await CALL_COMMIT_FN(this, {
|
|
5610
|
+
action: "close-pending",
|
|
5611
|
+
symbol: this.params.execution.context.symbol,
|
|
5612
|
+
strategyName: this.params.strategyName,
|
|
5613
|
+
exchangeName: this.params.exchangeName,
|
|
5614
|
+
frameName: this.params.frameName,
|
|
5615
|
+
signalId: closedSignal.id,
|
|
5616
|
+
backtest: this.params.execution.context.backtest,
|
|
5617
|
+
closeId: closedSignal.closeId,
|
|
5618
|
+
timestamp: currentTime,
|
|
5619
|
+
});
|
|
5442
5620
|
// Call onClose callback
|
|
5443
5621
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
5444
5622
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
@@ -5465,6 +5643,78 @@ class ClientStrategy {
|
|
|
5465
5643
|
await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, result, currentTime, this.params.execution.context.backtest);
|
|
5466
5644
|
return result;
|
|
5467
5645
|
}
|
|
5646
|
+
// Check if scheduled signal was activated - emit opened event once
|
|
5647
|
+
if (this._activatedSignal) {
|
|
5648
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5649
|
+
const activatedSignal = this._activatedSignal;
|
|
5650
|
+
this._activatedSignal = null; // Clear after emitting
|
|
5651
|
+
this.params.logger.info("ClientStrategy tick: scheduled signal was activated", {
|
|
5652
|
+
symbol: this.params.execution.context.symbol,
|
|
5653
|
+
signalId: activatedSignal.id,
|
|
5654
|
+
});
|
|
5655
|
+
// Check if strategy was stopped (symmetry with backtest PROCESS_SCHEDULED_SIGNAL_CANDLES_FN)
|
|
5656
|
+
if (this._isStopped) {
|
|
5657
|
+
this.params.logger.info("ClientStrategy tick: user-activated signal cancelled (stopped)", {
|
|
5658
|
+
symbol: this.params.execution.context.symbol,
|
|
5659
|
+
signalId: activatedSignal.id,
|
|
5660
|
+
});
|
|
5661
|
+
await this.setScheduledSignal(null);
|
|
5662
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5663
|
+
}
|
|
5664
|
+
// Check risk before activation
|
|
5665
|
+
if (await not(CALL_RISK_CHECK_SIGNAL_FN(this, this.params.execution.context.symbol, activatedSignal, currentPrice, currentTime, this.params.execution.context.backtest))) {
|
|
5666
|
+
this.params.logger.info("ClientStrategy tick: activated signal rejected by risk", {
|
|
5667
|
+
symbol: this.params.execution.context.symbol,
|
|
5668
|
+
signalId: activatedSignal.id,
|
|
5669
|
+
});
|
|
5670
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5671
|
+
}
|
|
5672
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
|
|
5673
|
+
const pendingSignal = {
|
|
5674
|
+
...activatedSignal,
|
|
5675
|
+
pendingAt: currentTime,
|
|
5676
|
+
_isScheduled: false,
|
|
5677
|
+
};
|
|
5678
|
+
await this.setPendingSignal(pendingSignal);
|
|
5679
|
+
await CALL_RISK_ADD_SIGNAL_FN(this, this.params.execution.context.symbol, pendingSignal, currentTime, this.params.execution.context.backtest);
|
|
5680
|
+
// Emit commit AFTER successful risk check
|
|
5681
|
+
const publicSignalForCommit = TO_PUBLIC_SIGNAL(pendingSignal);
|
|
5682
|
+
await CALL_COMMIT_FN(this, {
|
|
5683
|
+
action: "activate-scheduled",
|
|
5684
|
+
symbol: this.params.execution.context.symbol,
|
|
5685
|
+
strategyName: this.params.strategyName,
|
|
5686
|
+
exchangeName: this.params.exchangeName,
|
|
5687
|
+
frameName: this.params.frameName,
|
|
5688
|
+
signalId: activatedSignal.id,
|
|
5689
|
+
backtest: this.params.execution.context.backtest,
|
|
5690
|
+
activateId: activatedSignal.activateId,
|
|
5691
|
+
timestamp: currentTime,
|
|
5692
|
+
currentPrice,
|
|
5693
|
+
position: publicSignalForCommit.position,
|
|
5694
|
+
priceOpen: publicSignalForCommit.priceOpen,
|
|
5695
|
+
priceTakeProfit: publicSignalForCommit.priceTakeProfit,
|
|
5696
|
+
priceStopLoss: publicSignalForCommit.priceStopLoss,
|
|
5697
|
+
originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
|
|
5698
|
+
originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
|
|
5699
|
+
scheduledAt: publicSignalForCommit.scheduledAt,
|
|
5700
|
+
pendingAt: publicSignalForCommit.pendingAt,
|
|
5701
|
+
});
|
|
5702
|
+
// Call onOpen callback
|
|
5703
|
+
await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
5704
|
+
const result = {
|
|
5705
|
+
action: "opened",
|
|
5706
|
+
signal: TO_PUBLIC_SIGNAL(pendingSignal),
|
|
5707
|
+
strategyName: this.params.method.context.strategyName,
|
|
5708
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
5709
|
+
frameName: this.params.method.context.frameName,
|
|
5710
|
+
symbol: this.params.execution.context.symbol,
|
|
5711
|
+
currentPrice,
|
|
5712
|
+
backtest: this.params.execution.context.backtest,
|
|
5713
|
+
createdAt: currentTime,
|
|
5714
|
+
};
|
|
5715
|
+
await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, result, currentTime, this.params.execution.context.backtest);
|
|
5716
|
+
return result;
|
|
5717
|
+
}
|
|
5468
5718
|
// Monitor scheduled signal
|
|
5469
5719
|
if (this._scheduledSignal && !this._pendingSignal) {
|
|
5470
5720
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
@@ -5488,7 +5738,12 @@ class ClientStrategy {
|
|
|
5488
5738
|
return await RETURN_SCHEDULED_SIGNAL_ACTIVE_FN(this, this._scheduledSignal, currentPrice);
|
|
5489
5739
|
}
|
|
5490
5740
|
// Generate new signal if none exists
|
|
5741
|
+
// NOTE: _isStopped blocks NEW signal generation but allows existing positions to continue
|
|
5491
5742
|
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
5743
|
+
if (this._isStopped) {
|
|
5744
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5745
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5746
|
+
}
|
|
5492
5747
|
const signal = await GET_SIGNAL_FN(this);
|
|
5493
5748
|
if (signal) {
|
|
5494
5749
|
if (signal._isScheduled === true) {
|
|
@@ -5562,6 +5817,18 @@ class ClientStrategy {
|
|
|
5562
5817
|
const cancelledSignal = this._cancelledSignal;
|
|
5563
5818
|
this._cancelledSignal = null; // Clear after using
|
|
5564
5819
|
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
5820
|
+
// Emit commit with correct timestamp from backtest context
|
|
5821
|
+
await CALL_COMMIT_FN(this, {
|
|
5822
|
+
action: "cancel-scheduled",
|
|
5823
|
+
symbol: this.params.execution.context.symbol,
|
|
5824
|
+
strategyName: this.params.strategyName,
|
|
5825
|
+
exchangeName: this.params.exchangeName,
|
|
5826
|
+
frameName: this.params.frameName,
|
|
5827
|
+
signalId: cancelledSignal.id,
|
|
5828
|
+
backtest: true,
|
|
5829
|
+
cancelId: cancelledSignal.cancelId,
|
|
5830
|
+
timestamp: closeTimestamp,
|
|
5831
|
+
});
|
|
5565
5832
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
5566
5833
|
const cancelledResult = {
|
|
5567
5834
|
action: "cancelled",
|
|
@@ -5587,6 +5854,18 @@ class ClientStrategy {
|
|
|
5587
5854
|
const closedSignal = this._closedSignal;
|
|
5588
5855
|
this._closedSignal = null; // Clear after using
|
|
5589
5856
|
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
5857
|
+
// Emit commit with correct timestamp from backtest context
|
|
5858
|
+
await CALL_COMMIT_FN(this, {
|
|
5859
|
+
action: "close-pending",
|
|
5860
|
+
symbol: this.params.execution.context.symbol,
|
|
5861
|
+
strategyName: this.params.strategyName,
|
|
5862
|
+
exchangeName: this.params.exchangeName,
|
|
5863
|
+
frameName: this.params.frameName,
|
|
5864
|
+
signalId: closedSignal.id,
|
|
5865
|
+
backtest: true,
|
|
5866
|
+
closeId: closedSignal.closeId,
|
|
5867
|
+
timestamp: closeTimestamp,
|
|
5868
|
+
});
|
|
5590
5869
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
5591
5870
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
5592
5871
|
await CALL_PARTIAL_CLEAR_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
@@ -5747,8 +6026,18 @@ class ClientStrategy {
|
|
|
5747
6026
|
symbol,
|
|
5748
6027
|
hasPendingSignal: this._pendingSignal !== null,
|
|
5749
6028
|
hasScheduledSignal: this._scheduledSignal !== null,
|
|
6029
|
+
hasActivatedSignal: this._activatedSignal !== null,
|
|
6030
|
+
hasCancelledSignal: this._cancelledSignal !== null,
|
|
6031
|
+
hasClosedSignal: this._closedSignal !== null,
|
|
5750
6032
|
});
|
|
5751
6033
|
this._isStopped = true;
|
|
6034
|
+
// Clear pending flags to start from clean state
|
|
6035
|
+
// NOTE: _isStopped blocks NEW position opening, but allows:
|
|
6036
|
+
// - cancelScheduled() / closePending() for graceful shutdown
|
|
6037
|
+
// - Monitoring existing _pendingSignal until TP/SL/timeout
|
|
6038
|
+
this._activatedSignal = null;
|
|
6039
|
+
this._cancelledSignal = null;
|
|
6040
|
+
this._closedSignal = null;
|
|
5752
6041
|
// Clear scheduled signal if exists
|
|
5753
6042
|
if (!this._scheduledSignal) {
|
|
5754
6043
|
return;
|
|
@@ -5786,8 +6075,9 @@ class ClientStrategy {
|
|
|
5786
6075
|
hasScheduledSignal: this._scheduledSignal !== null,
|
|
5787
6076
|
cancelId,
|
|
5788
6077
|
});
|
|
5789
|
-
//
|
|
5790
|
-
|
|
6078
|
+
// NOTE: No _isStopped check - cancellation must work for graceful shutdown
|
|
6079
|
+
// (cancelling scheduled signal is not opening new position)
|
|
6080
|
+
// Save cancelled signal for next tick/backtest to emit cancelled event with correct timestamp
|
|
5791
6081
|
if (this._scheduledSignal) {
|
|
5792
6082
|
this._cancelledSignal = Object.assign({}, this._scheduledSignal, {
|
|
5793
6083
|
cancelId,
|
|
@@ -5795,37 +6085,60 @@ class ClientStrategy {
|
|
|
5795
6085
|
this._scheduledSignal = null;
|
|
5796
6086
|
}
|
|
5797
6087
|
if (backtest) {
|
|
5798
|
-
//
|
|
5799
|
-
if (hadScheduledSignal) {
|
|
5800
|
-
await CALL_COMMIT_FN(this, {
|
|
5801
|
-
action: "cancel-scheduled",
|
|
5802
|
-
symbol,
|
|
5803
|
-
strategyName: this.params.strategyName,
|
|
5804
|
-
exchangeName: this.params.exchangeName,
|
|
5805
|
-
frameName: this.params.frameName,
|
|
5806
|
-
signalId: this._cancelledSignal.id,
|
|
5807
|
-
backtest,
|
|
5808
|
-
cancelId,
|
|
5809
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5810
|
-
});
|
|
5811
|
-
}
|
|
6088
|
+
// Commit will be emitted in backtest() with correct candle timestamp
|
|
5812
6089
|
return;
|
|
5813
6090
|
}
|
|
5814
6091
|
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
|
|
5815
|
-
//
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
6092
|
+
// Commit will be emitted in tick() with correct currentTime
|
|
6093
|
+
}
|
|
6094
|
+
/**
|
|
6095
|
+
* Activates the scheduled signal without waiting for price to reach priceOpen.
|
|
6096
|
+
*
|
|
6097
|
+
* Forces immediate activation of the scheduled signal at the current price.
|
|
6098
|
+
* Does NOT affect active pending signals or strategy operation.
|
|
6099
|
+
* Does NOT set stop flag - strategy can continue generating new signals.
|
|
6100
|
+
*
|
|
6101
|
+
* Use case: User-initiated early activation of a scheduled entry.
|
|
6102
|
+
*
|
|
6103
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
6104
|
+
* @param backtest - Whether running in backtest mode
|
|
6105
|
+
* @param activateId - Optional identifier for this activation operation
|
|
6106
|
+
* @returns Promise that resolves when scheduled signal is activated
|
|
6107
|
+
*
|
|
6108
|
+
* @example
|
|
6109
|
+
* ```typescript
|
|
6110
|
+
* // Activate scheduled signal without waiting for priceOpen
|
|
6111
|
+
* await strategy.activateScheduled("BTCUSDT", false, "user-activate-123");
|
|
6112
|
+
* // Scheduled signal becomes pending signal immediately
|
|
6113
|
+
* ```
|
|
6114
|
+
*/
|
|
6115
|
+
async activateScheduled(symbol, backtest, activateId) {
|
|
6116
|
+
this.params.logger.debug("ClientStrategy activateScheduled", {
|
|
6117
|
+
symbol,
|
|
6118
|
+
hasScheduledSignal: this._scheduledSignal !== null,
|
|
6119
|
+
activateId,
|
|
6120
|
+
});
|
|
6121
|
+
// Block activation if strategy stopped - activation = opening NEW position
|
|
6122
|
+
// (unlike cancelScheduled/closePending which handle existing signals for graceful shutdown)
|
|
6123
|
+
if (this._isStopped) {
|
|
6124
|
+
this.params.logger.debug("ClientStrategy activateScheduled: strategy stopped, skipping", {
|
|
5819
6125
|
symbol,
|
|
5820
|
-
strategyName: this.params.strategyName,
|
|
5821
|
-
exchangeName: this.params.exchangeName,
|
|
5822
|
-
frameName: this.params.frameName,
|
|
5823
|
-
signalId: this._cancelledSignal.id,
|
|
5824
|
-
backtest,
|
|
5825
|
-
cancelId,
|
|
5826
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5827
6126
|
});
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
// Save activated signal for next tick to emit opened event
|
|
6130
|
+
if (this._scheduledSignal) {
|
|
6131
|
+
this._activatedSignal = Object.assign({}, this._scheduledSignal, {
|
|
6132
|
+
activateId,
|
|
6133
|
+
});
|
|
6134
|
+
this._scheduledSignal = null;
|
|
6135
|
+
}
|
|
6136
|
+
if (backtest) {
|
|
6137
|
+
// Commit will be emitted AFTER successful risk check in PROCESS_SCHEDULED_SIGNAL_CANDLES_FN
|
|
6138
|
+
return;
|
|
5828
6139
|
}
|
|
6140
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
|
|
6141
|
+
// Commit will be emitted AFTER successful risk check in tick()
|
|
5829
6142
|
}
|
|
5830
6143
|
/**
|
|
5831
6144
|
* Closes the pending signal without stopping the strategy.
|
|
@@ -5854,8 +6167,8 @@ class ClientStrategy {
|
|
|
5854
6167
|
hasPendingSignal: this._pendingSignal !== null,
|
|
5855
6168
|
closeId,
|
|
5856
6169
|
});
|
|
5857
|
-
//
|
|
5858
|
-
|
|
6170
|
+
// NOTE: No _isStopped check - closing position must work for graceful shutdown
|
|
6171
|
+
// Save closed signal for next tick/backtest to emit closed event with correct timestamp
|
|
5859
6172
|
if (this._pendingSignal) {
|
|
5860
6173
|
this._closedSignal = Object.assign({}, this._pendingSignal, {
|
|
5861
6174
|
closeId,
|
|
@@ -5863,37 +6176,11 @@ class ClientStrategy {
|
|
|
5863
6176
|
this._pendingSignal = null;
|
|
5864
6177
|
}
|
|
5865
6178
|
if (backtest) {
|
|
5866
|
-
//
|
|
5867
|
-
if (hadPendingSignal) {
|
|
5868
|
-
await CALL_COMMIT_FN(this, {
|
|
5869
|
-
action: "close-pending",
|
|
5870
|
-
symbol,
|
|
5871
|
-
strategyName: this.params.strategyName,
|
|
5872
|
-
exchangeName: this.params.exchangeName,
|
|
5873
|
-
frameName: this.params.frameName,
|
|
5874
|
-
signalId: this._closedSignal.id,
|
|
5875
|
-
backtest,
|
|
5876
|
-
closeId,
|
|
5877
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5878
|
-
});
|
|
5879
|
-
}
|
|
6179
|
+
// Commit will be emitted in backtest() with correct candle timestamp
|
|
5880
6180
|
return;
|
|
5881
6181
|
}
|
|
5882
6182
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
5883
|
-
//
|
|
5884
|
-
if (hadPendingSignal) {
|
|
5885
|
-
await CALL_COMMIT_FN(this, {
|
|
5886
|
-
action: "close-pending",
|
|
5887
|
-
symbol,
|
|
5888
|
-
strategyName: this.params.strategyName,
|
|
5889
|
-
exchangeName: this.params.exchangeName,
|
|
5890
|
-
frameName: this.params.frameName,
|
|
5891
|
-
signalId: this._closedSignal.id,
|
|
5892
|
-
backtest,
|
|
5893
|
-
closeId,
|
|
5894
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5895
|
-
});
|
|
5896
|
-
}
|
|
6183
|
+
// Commit will be emitted in tick() with correct currentTime
|
|
5897
6184
|
}
|
|
5898
6185
|
/**
|
|
5899
6186
|
* Executes partial close at profit level (moving toward TP).
|
|
@@ -7732,6 +8019,39 @@ class StrategyConnectionService {
|
|
|
7732
8019
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
7733
8020
|
return await strategy.breakeven(symbol, currentPrice, backtest);
|
|
7734
8021
|
};
|
|
8022
|
+
/**
|
|
8023
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
8024
|
+
*
|
|
8025
|
+
* Delegates to ClientStrategy.activateScheduled() which sets _activatedSignal flag.
|
|
8026
|
+
* The actual activation happens on next tick() when strategy detects the flag.
|
|
8027
|
+
*
|
|
8028
|
+
* @param backtest - Whether running in backtest mode
|
|
8029
|
+
* @param symbol - Trading pair symbol
|
|
8030
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8031
|
+
* @param activateId - Optional identifier for the activation reason
|
|
8032
|
+
* @returns Promise that resolves when activation flag is set
|
|
8033
|
+
*
|
|
8034
|
+
* @example
|
|
8035
|
+
* ```typescript
|
|
8036
|
+
* // Activate scheduled signal early
|
|
8037
|
+
* await strategyConnectionService.activateScheduled(
|
|
8038
|
+
* false,
|
|
8039
|
+
* "BTCUSDT",
|
|
8040
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
8041
|
+
* "manual-activation"
|
|
8042
|
+
* );
|
|
8043
|
+
* ```
|
|
8044
|
+
*/
|
|
8045
|
+
this.activateScheduled = async (backtest, symbol, context, activateId) => {
|
|
8046
|
+
this.loggerService.log("strategyConnectionService activateScheduled", {
|
|
8047
|
+
symbol,
|
|
8048
|
+
context,
|
|
8049
|
+
backtest,
|
|
8050
|
+
activateId,
|
|
8051
|
+
});
|
|
8052
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8053
|
+
return await strategy.activateScheduled(symbol, backtest, activateId);
|
|
8054
|
+
};
|
|
7735
8055
|
}
|
|
7736
8056
|
}
|
|
7737
8057
|
|
|
@@ -11044,6 +11364,39 @@ class StrategyCoreService {
|
|
|
11044
11364
|
await this.validate(context);
|
|
11045
11365
|
return await this.strategyConnectionService.breakeven(backtest, symbol, currentPrice, context);
|
|
11046
11366
|
};
|
|
11367
|
+
/**
|
|
11368
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
11369
|
+
*
|
|
11370
|
+
* Validates strategy existence and delegates to connection service
|
|
11371
|
+
* to set the activation flag. The actual activation happens on next tick().
|
|
11372
|
+
*
|
|
11373
|
+
* @param backtest - Whether running in backtest mode
|
|
11374
|
+
* @param symbol - Trading pair symbol
|
|
11375
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
11376
|
+
* @param activateId - Optional identifier for the activation reason
|
|
11377
|
+
* @returns Promise that resolves when activation flag is set
|
|
11378
|
+
*
|
|
11379
|
+
* @example
|
|
11380
|
+
* ```typescript
|
|
11381
|
+
* // Activate scheduled signal early
|
|
11382
|
+
* await strategyCoreService.activateScheduled(
|
|
11383
|
+
* false,
|
|
11384
|
+
* "BTCUSDT",
|
|
11385
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
11386
|
+
* "manual-activation"
|
|
11387
|
+
* );
|
|
11388
|
+
* ```
|
|
11389
|
+
*/
|
|
11390
|
+
this.activateScheduled = async (backtest, symbol, context, activateId) => {
|
|
11391
|
+
this.loggerService.log("strategyCoreService activateScheduled", {
|
|
11392
|
+
symbol,
|
|
11393
|
+
context,
|
|
11394
|
+
backtest,
|
|
11395
|
+
activateId,
|
|
11396
|
+
});
|
|
11397
|
+
await this.validate(context);
|
|
11398
|
+
return await this.strategyConnectionService.activateScheduled(backtest, symbol, context, activateId);
|
|
11399
|
+
};
|
|
11047
11400
|
}
|
|
11048
11401
|
}
|
|
11049
11402
|
|
|
@@ -12323,13 +12676,8 @@ class WalkerSchemaService {
|
|
|
12323
12676
|
}
|
|
12324
12677
|
}
|
|
12325
12678
|
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
* ClientExchange.getNextCandles использует фильтр:
|
|
12329
|
-
* timestamp > since && timestamp + stepMs < endTime
|
|
12330
|
-
* который исключает первую и последнюю свечи из запрошенного диапазона.
|
|
12331
|
-
*/
|
|
12332
|
-
const CANDLE_EXCLUSIVE_BOUNDARY_OFFSET = 2;
|
|
12679
|
+
const ACTIVE_CANDLE_INCLUDED = 1;
|
|
12680
|
+
const SCHEDULE_ACTIVATION_CANDLE_SKIP = 1;
|
|
12333
12681
|
/**
|
|
12334
12682
|
* Private service for backtest orchestration using async generators.
|
|
12335
12683
|
*
|
|
@@ -12453,9 +12801,9 @@ class BacktestLogicPrivateService {
|
|
|
12453
12801
|
// - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
12454
12802
|
// - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
12455
12803
|
// - +1 потому что when включается как первая свеча
|
|
12456
|
-
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT -
|
|
12804
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - ACTIVE_CANDLE_INCLUDED;
|
|
12457
12805
|
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
12458
|
-
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime +
|
|
12806
|
+
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + SCHEDULE_ACTIVATION_CANDLE_SKIP;
|
|
12459
12807
|
let candles;
|
|
12460
12808
|
try {
|
|
12461
12809
|
candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
|
|
@@ -12593,9 +12941,9 @@ class BacktestLogicPrivateService {
|
|
|
12593
12941
|
// КРИТИЧНО: Получаем свечи включая буфер для VWAP
|
|
12594
12942
|
// Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
|
|
12595
12943
|
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
12596
|
-
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT -
|
|
12944
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - ACTIVE_CANDLE_INCLUDED;
|
|
12597
12945
|
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
12598
|
-
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT
|
|
12946
|
+
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
12599
12947
|
let candles;
|
|
12600
12948
|
try {
|
|
12601
12949
|
candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
@@ -24283,6 +24631,67 @@ class StrategyReportService {
|
|
|
24283
24631
|
walkerName: "",
|
|
24284
24632
|
});
|
|
24285
24633
|
};
|
|
24634
|
+
/**
|
|
24635
|
+
* Logs an activate-scheduled event when a scheduled signal is activated early.
|
|
24636
|
+
*
|
|
24637
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24638
|
+
* @param currentPrice - Current market price at time of activation
|
|
24639
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24640
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24641
|
+
* @param timestamp - Timestamp from StrategyCommitContract (execution context time)
|
|
24642
|
+
* @param position - Trade direction: "long" or "short"
|
|
24643
|
+
* @param priceOpen - Entry price for the position
|
|
24644
|
+
* @param priceTakeProfit - Effective take profit price
|
|
24645
|
+
* @param priceStopLoss - Effective stop loss price
|
|
24646
|
+
* @param originalPriceTakeProfit - Original take profit before trailing
|
|
24647
|
+
* @param originalPriceStopLoss - Original stop loss before trailing
|
|
24648
|
+
* @param scheduledAt - Signal creation timestamp in milliseconds
|
|
24649
|
+
* @param pendingAt - Pending timestamp in milliseconds
|
|
24650
|
+
* @param activateId - Optional identifier for the activation reason
|
|
24651
|
+
*/
|
|
24652
|
+
this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
|
|
24653
|
+
this.loggerService.log("strategyReportService activateScheduled", {
|
|
24654
|
+
symbol,
|
|
24655
|
+
currentPrice,
|
|
24656
|
+
isBacktest,
|
|
24657
|
+
activateId,
|
|
24658
|
+
});
|
|
24659
|
+
if (!this.subscribe.hasValue()) {
|
|
24660
|
+
return;
|
|
24661
|
+
}
|
|
24662
|
+
const scheduledRow = await this.strategyCoreService.getScheduledSignal(isBacktest, symbol, {
|
|
24663
|
+
exchangeName: context.exchangeName,
|
|
24664
|
+
strategyName: context.strategyName,
|
|
24665
|
+
frameName: context.frameName,
|
|
24666
|
+
});
|
|
24667
|
+
if (!scheduledRow) {
|
|
24668
|
+
return;
|
|
24669
|
+
}
|
|
24670
|
+
const createdAt = new Date(timestamp).toISOString();
|
|
24671
|
+
await Report.writeData("strategy", {
|
|
24672
|
+
action: "activate-scheduled",
|
|
24673
|
+
activateId,
|
|
24674
|
+
currentPrice,
|
|
24675
|
+
symbol,
|
|
24676
|
+
timestamp,
|
|
24677
|
+
createdAt,
|
|
24678
|
+
position,
|
|
24679
|
+
priceOpen,
|
|
24680
|
+
priceTakeProfit,
|
|
24681
|
+
priceStopLoss,
|
|
24682
|
+
originalPriceTakeProfit,
|
|
24683
|
+
originalPriceStopLoss,
|
|
24684
|
+
scheduledAt,
|
|
24685
|
+
pendingAt,
|
|
24686
|
+
}, {
|
|
24687
|
+
signalId: scheduledRow.id,
|
|
24688
|
+
exchangeName: context.exchangeName,
|
|
24689
|
+
frameName: context.frameName,
|
|
24690
|
+
strategyName: context.strategyName,
|
|
24691
|
+
symbol,
|
|
24692
|
+
walkerName: "",
|
|
24693
|
+
});
|
|
24694
|
+
};
|
|
24286
24695
|
/**
|
|
24287
24696
|
* Initializes the service for event logging.
|
|
24288
24697
|
*
|
|
@@ -24342,7 +24751,14 @@ class StrategyReportService {
|
|
|
24342
24751
|
frameName: event.frameName,
|
|
24343
24752
|
strategyName: event.strategyName,
|
|
24344
24753
|
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
|
|
24345
|
-
const
|
|
24754
|
+
const unActivateScheduled = strategyCommitSubject
|
|
24755
|
+
.filter(({ action }) => action === "activate-scheduled")
|
|
24756
|
+
.connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
|
|
24757
|
+
exchangeName: event.exchangeName,
|
|
24758
|
+
frameName: event.frameName,
|
|
24759
|
+
strategyName: event.strategyName,
|
|
24760
|
+
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
|
|
24761
|
+
const disposeFn = compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
|
|
24346
24762
|
return () => {
|
|
24347
24763
|
disposeFn();
|
|
24348
24764
|
this.subscribe.clear();
|
|
@@ -24473,6 +24889,7 @@ class ReportStorage {
|
|
|
24473
24889
|
trailingStopCount: 0,
|
|
24474
24890
|
trailingTakeCount: 0,
|
|
24475
24891
|
breakevenCount: 0,
|
|
24892
|
+
activateScheduledCount: 0,
|
|
24476
24893
|
};
|
|
24477
24894
|
}
|
|
24478
24895
|
return {
|
|
@@ -24485,6 +24902,7 @@ class ReportStorage {
|
|
|
24485
24902
|
trailingStopCount: this._eventList.filter(e => e.action === "trailing-stop").length,
|
|
24486
24903
|
trailingTakeCount: this._eventList.filter(e => e.action === "trailing-take").length,
|
|
24487
24904
|
breakevenCount: this._eventList.filter(e => e.action === "breakeven").length,
|
|
24905
|
+
activateScheduledCount: this._eventList.filter(e => e.action === "activate-scheduled").length,
|
|
24488
24906
|
};
|
|
24489
24907
|
}
|
|
24490
24908
|
/**
|
|
@@ -24533,6 +24951,7 @@ class ReportStorage {
|
|
|
24533
24951
|
`- Trailing stop: ${stats.trailingStopCount}`,
|
|
24534
24952
|
`- Trailing take: ${stats.trailingTakeCount}`,
|
|
24535
24953
|
`- Breakeven: ${stats.breakevenCount}`,
|
|
24954
|
+
`- Activate scheduled: ${stats.activateScheduledCount}`,
|
|
24536
24955
|
].join("\n");
|
|
24537
24956
|
}
|
|
24538
24957
|
/**
|
|
@@ -24999,6 +25418,66 @@ class StrategyMarkdownService {
|
|
|
24999
25418
|
pendingAt,
|
|
25000
25419
|
});
|
|
25001
25420
|
};
|
|
25421
|
+
/**
|
|
25422
|
+
* Records an activate-scheduled event when a scheduled signal is activated early.
|
|
25423
|
+
*
|
|
25424
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
25425
|
+
* @param currentPrice - Current market price at time of activation
|
|
25426
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
25427
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
25428
|
+
* @param timestamp - Timestamp from StrategyCommitContract (execution context time)
|
|
25429
|
+
* @param position - Trade direction: "long" or "short"
|
|
25430
|
+
* @param priceOpen - Entry price for the position
|
|
25431
|
+
* @param priceTakeProfit - Effective take profit price
|
|
25432
|
+
* @param priceStopLoss - Effective stop loss price
|
|
25433
|
+
* @param originalPriceTakeProfit - Original take profit before trailing
|
|
25434
|
+
* @param originalPriceStopLoss - Original stop loss before trailing
|
|
25435
|
+
* @param scheduledAt - Signal creation timestamp in milliseconds
|
|
25436
|
+
* @param pendingAt - Pending timestamp in milliseconds
|
|
25437
|
+
* @param activateId - Optional identifier for the activation reason
|
|
25438
|
+
*/
|
|
25439
|
+
this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
|
|
25440
|
+
this.loggerService.log("strategyMarkdownService activateScheduled", {
|
|
25441
|
+
symbol,
|
|
25442
|
+
currentPrice,
|
|
25443
|
+
isBacktest,
|
|
25444
|
+
activateId,
|
|
25445
|
+
});
|
|
25446
|
+
if (!this.subscribe.hasValue()) {
|
|
25447
|
+
return;
|
|
25448
|
+
}
|
|
25449
|
+
const scheduledRow = await this.strategyCoreService.getScheduledSignal(isBacktest, symbol, {
|
|
25450
|
+
exchangeName: context.exchangeName,
|
|
25451
|
+
strategyName: context.strategyName,
|
|
25452
|
+
frameName: context.frameName,
|
|
25453
|
+
});
|
|
25454
|
+
if (!scheduledRow) {
|
|
25455
|
+
return;
|
|
25456
|
+
}
|
|
25457
|
+
const createdAt = new Date(timestamp).toISOString();
|
|
25458
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
25459
|
+
storage.addEvent({
|
|
25460
|
+
timestamp,
|
|
25461
|
+
symbol,
|
|
25462
|
+
strategyName: context.strategyName,
|
|
25463
|
+
exchangeName: context.exchangeName,
|
|
25464
|
+
frameName: context.frameName,
|
|
25465
|
+
signalId: scheduledRow.id,
|
|
25466
|
+
action: "activate-scheduled",
|
|
25467
|
+
activateId,
|
|
25468
|
+
currentPrice,
|
|
25469
|
+
createdAt,
|
|
25470
|
+
backtest: isBacktest,
|
|
25471
|
+
position,
|
|
25472
|
+
priceOpen,
|
|
25473
|
+
priceTakeProfit,
|
|
25474
|
+
priceStopLoss,
|
|
25475
|
+
originalPriceTakeProfit,
|
|
25476
|
+
originalPriceStopLoss,
|
|
25477
|
+
scheduledAt,
|
|
25478
|
+
pendingAt,
|
|
25479
|
+
});
|
|
25480
|
+
};
|
|
25002
25481
|
/**
|
|
25003
25482
|
* Retrieves aggregated statistics from accumulated strategy events.
|
|
25004
25483
|
*
|
|
@@ -25172,7 +25651,14 @@ class StrategyMarkdownService {
|
|
|
25172
25651
|
frameName: event.frameName,
|
|
25173
25652
|
strategyName: event.strategyName,
|
|
25174
25653
|
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
|
|
25175
|
-
const
|
|
25654
|
+
const unActivateScheduled = strategyCommitSubject
|
|
25655
|
+
.filter(({ action }) => action === "activate-scheduled")
|
|
25656
|
+
.connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
|
|
25657
|
+
exchangeName: event.exchangeName,
|
|
25658
|
+
frameName: event.frameName,
|
|
25659
|
+
strategyName: event.strategyName,
|
|
25660
|
+
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
|
|
25661
|
+
const disposeFn = compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
|
|
25176
25662
|
return () => {
|
|
25177
25663
|
disposeFn();
|
|
25178
25664
|
this.subscribe.clear();
|
|
@@ -26098,6 +26584,7 @@ const PARTIAL_LOSS_METHOD_NAME = "strategy.commitPartialLoss";
|
|
|
26098
26584
|
const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
|
|
26099
26585
|
const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
|
|
26100
26586
|
const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
|
|
26587
|
+
const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
|
|
26101
26588
|
/**
|
|
26102
26589
|
* Cancels the scheduled signal without stopping the strategy.
|
|
26103
26590
|
*
|
|
@@ -26415,6 +26902,41 @@ async function commitBreakeven(symbol) {
|
|
|
26415
26902
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
26416
26903
|
return await bt.strategyCoreService.breakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
26417
26904
|
}
|
|
26905
|
+
/**
|
|
26906
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
26907
|
+
*
|
|
26908
|
+
* Sets the activation flag on the scheduled signal. The actual activation
|
|
26909
|
+
* happens on the next tick() when strategy detects the flag.
|
|
26910
|
+
*
|
|
26911
|
+
* Automatically detects backtest/live mode from execution context.
|
|
26912
|
+
*
|
|
26913
|
+
* @param symbol - Trading pair symbol
|
|
26914
|
+
* @param activateId - Optional activation ID for tracking user-initiated activations
|
|
26915
|
+
* @returns Promise that resolves when activation flag is set
|
|
26916
|
+
*
|
|
26917
|
+
* @example
|
|
26918
|
+
* ```typescript
|
|
26919
|
+
* import { commitActivateScheduled } from "backtest-kit";
|
|
26920
|
+
*
|
|
26921
|
+
* // Activate scheduled signal early with custom ID
|
|
26922
|
+
* await commitActivateScheduled("BTCUSDT", "manual-activate-001");
|
|
26923
|
+
* ```
|
|
26924
|
+
*/
|
|
26925
|
+
async function commitActivateScheduled(symbol, activateId) {
|
|
26926
|
+
bt.loggerService.info(ACTIVATE_SCHEDULED_METHOD_NAME, {
|
|
26927
|
+
symbol,
|
|
26928
|
+
activateId,
|
|
26929
|
+
});
|
|
26930
|
+
if (!ExecutionContextService.hasContext()) {
|
|
26931
|
+
throw new Error("commitActivateScheduled requires an execution context");
|
|
26932
|
+
}
|
|
26933
|
+
if (!MethodContextService.hasContext()) {
|
|
26934
|
+
throw new Error("commitActivateScheduled requires a method context");
|
|
26935
|
+
}
|
|
26936
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
26937
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
26938
|
+
await bt.strategyCoreService.activateScheduled(isBacktest, symbol, { exchangeName, frameName, strategyName }, activateId);
|
|
26939
|
+
}
|
|
26418
26940
|
|
|
26419
26941
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
26420
26942
|
/**
|
|
@@ -28585,6 +29107,7 @@ const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
|
|
|
28585
29107
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
|
|
28586
29108
|
const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
|
|
28587
29109
|
const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
|
|
29110
|
+
const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
|
|
28588
29111
|
const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
|
|
28589
29112
|
/**
|
|
28590
29113
|
* Internal task function that runs backtest and handles completion.
|
|
@@ -29441,6 +29964,46 @@ class BacktestUtils {
|
|
|
29441
29964
|
}
|
|
29442
29965
|
return await bt.strategyCoreService.breakeven(true, symbol, currentPrice, context);
|
|
29443
29966
|
};
|
|
29967
|
+
/**
|
|
29968
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
29969
|
+
*
|
|
29970
|
+
* Sets the activation flag on the scheduled signal. The actual activation
|
|
29971
|
+
* happens on the next tick() when strategy detects the flag.
|
|
29972
|
+
*
|
|
29973
|
+
* @param symbol - Trading pair symbol
|
|
29974
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
29975
|
+
* @param activateId - Optional activation ID for tracking user-initiated activations
|
|
29976
|
+
* @returns Promise that resolves when activation flag is set
|
|
29977
|
+
*
|
|
29978
|
+
* @example
|
|
29979
|
+
* ```typescript
|
|
29980
|
+
* // Activate scheduled signal early with custom ID
|
|
29981
|
+
* await Backtest.commitActivateScheduled("BTCUSDT", {
|
|
29982
|
+
* strategyName: "my-strategy",
|
|
29983
|
+
* exchangeName: "binance",
|
|
29984
|
+
* frameName: "1h"
|
|
29985
|
+
* }, "manual-activate-001");
|
|
29986
|
+
* ```
|
|
29987
|
+
*/
|
|
29988
|
+
this.commitActivateScheduled = async (symbol, context, activateId) => {
|
|
29989
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED, {
|
|
29990
|
+
symbol,
|
|
29991
|
+
context,
|
|
29992
|
+
activateId,
|
|
29993
|
+
});
|
|
29994
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
29995
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
29996
|
+
{
|
|
29997
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
29998
|
+
riskName &&
|
|
29999
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
30000
|
+
riskList &&
|
|
30001
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
30002
|
+
actions &&
|
|
30003
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
30004
|
+
}
|
|
30005
|
+
await bt.strategyCoreService.activateScheduled(true, symbol, context, activateId);
|
|
30006
|
+
};
|
|
29444
30007
|
/**
|
|
29445
30008
|
* Gets statistical data from all closed signals for a symbol-strategy pair.
|
|
29446
30009
|
*
|
|
@@ -29616,6 +30179,7 @@ const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
|
|
|
29616
30179
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
|
|
29617
30180
|
const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
|
|
29618
30181
|
const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
|
|
30182
|
+
const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
|
|
29619
30183
|
/**
|
|
29620
30184
|
* Internal task function that runs live trading and handles completion.
|
|
29621
30185
|
* Consumes live trading results and updates instance state flags.
|
|
@@ -30440,6 +31004,46 @@ class LiveUtils {
|
|
|
30440
31004
|
frameName: "",
|
|
30441
31005
|
});
|
|
30442
31006
|
};
|
|
31007
|
+
/**
|
|
31008
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
31009
|
+
*
|
|
31010
|
+
* Sets the activation flag on the scheduled signal. The actual activation
|
|
31011
|
+
* happens on the next tick() when strategy detects the flag.
|
|
31012
|
+
*
|
|
31013
|
+
* @param symbol - Trading pair symbol
|
|
31014
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
31015
|
+
* @param activateId - Optional activation ID for tracking user-initiated activations
|
|
31016
|
+
* @returns Promise that resolves when activation flag is set
|
|
31017
|
+
*
|
|
31018
|
+
* @example
|
|
31019
|
+
* ```typescript
|
|
31020
|
+
* // Activate scheduled signal early with custom ID
|
|
31021
|
+
* await Live.commitActivateScheduled("BTCUSDT", {
|
|
31022
|
+
* strategyName: "my-strategy",
|
|
31023
|
+
* exchangeName: "binance"
|
|
31024
|
+
* }, "manual-activate-001");
|
|
31025
|
+
* ```
|
|
31026
|
+
*/
|
|
31027
|
+
this.commitActivateScheduled = async (symbol, context, activateId) => {
|
|
31028
|
+
bt.loggerService.info(LIVE_METHOD_NAME_ACTIVATE_SCHEDULED, {
|
|
31029
|
+
symbol,
|
|
31030
|
+
context,
|
|
31031
|
+
activateId,
|
|
31032
|
+
});
|
|
31033
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
31034
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
31035
|
+
{
|
|
31036
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31037
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
31038
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
31039
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
31040
|
+
}
|
|
31041
|
+
await bt.strategyCoreService.activateScheduled(false, symbol, {
|
|
31042
|
+
strategyName: context.strategyName,
|
|
31043
|
+
exchangeName: context.exchangeName,
|
|
31044
|
+
frameName: "",
|
|
31045
|
+
}, activateId);
|
|
31046
|
+
};
|
|
30443
31047
|
/**
|
|
30444
31048
|
* Gets statistical data from all live trading events for a symbol-strategy pair.
|
|
30445
31049
|
*
|
|
@@ -33089,6 +33693,27 @@ const INTERVAL_MINUTES$1 = {
|
|
|
33089
33693
|
"6h": 360,
|
|
33090
33694
|
"8h": 480,
|
|
33091
33695
|
};
|
|
33696
|
+
/**
|
|
33697
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
33698
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
33699
|
+
*
|
|
33700
|
+
* Candle timestamp convention:
|
|
33701
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
33702
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
33703
|
+
*
|
|
33704
|
+
* Adapter contract:
|
|
33705
|
+
* - Adapter must return candles with timestamp = openTime
|
|
33706
|
+
* - First returned candle.timestamp must equal aligned since
|
|
33707
|
+
* - Adapter must return exactly `limit` candles
|
|
33708
|
+
*
|
|
33709
|
+
* @param timestamp - Timestamp in milliseconds
|
|
33710
|
+
* @param intervalMinutes - Interval in minutes
|
|
33711
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
33712
|
+
*/
|
|
33713
|
+
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
33714
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE;
|
|
33715
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
33716
|
+
};
|
|
33092
33717
|
/**
|
|
33093
33718
|
* Creates exchange instance with methods resolved once during construction.
|
|
33094
33719
|
* Applies default implementations where schema methods are not provided.
|
|
@@ -33110,25 +33735,24 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
|
33110
33735
|
};
|
|
33111
33736
|
/**
|
|
33112
33737
|
* Attempts to read candles from cache.
|
|
33113
|
-
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
33114
33738
|
*
|
|
33115
|
-
*
|
|
33116
|
-
*
|
|
33117
|
-
*
|
|
33118
|
-
* - Only fully closed candles within the exclusive range are returned
|
|
33739
|
+
* Cache lookup calculates expected timestamps:
|
|
33740
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33741
|
+
* Returns all candles if found, null if any missing.
|
|
33119
33742
|
*
|
|
33120
33743
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
33121
|
-
* @param sinceTimestamp -
|
|
33122
|
-
* @param untilTimestamp -
|
|
33744
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
33745
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
33123
33746
|
* @param exchangeName - Exchange name
|
|
33124
|
-
* @returns Cached candles array or null if cache miss
|
|
33747
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
33125
33748
|
*/
|
|
33126
33749
|
const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
33127
|
-
// PersistCandleAdapter.readCandlesData
|
|
33128
|
-
//
|
|
33750
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
33751
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33752
|
+
// Returns all candles if found, null if any missing
|
|
33129
33753
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
33130
33754
|
// Return cached data only if we have exactly the requested limit
|
|
33131
|
-
if (cachedCandles
|
|
33755
|
+
if (cachedCandles?.length === dto.limit) {
|
|
33132
33756
|
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
33133
33757
|
return cachedCandles;
|
|
33134
33758
|
}
|
|
@@ -33150,11 +33774,12 @@ const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestam
|
|
|
33150
33774
|
/**
|
|
33151
33775
|
* Writes candles to cache with error handling.
|
|
33152
33776
|
*
|
|
33153
|
-
* The candles passed to this function
|
|
33154
|
-
* - candle.timestamp
|
|
33155
|
-
* -
|
|
33777
|
+
* The candles passed to this function should be validated:
|
|
33778
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
33779
|
+
* - Exact number of candles as requested (limit)
|
|
33780
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
33156
33781
|
*
|
|
33157
|
-
* @param candles - Array of candle data to cache
|
|
33782
|
+
* @param candles - Array of validated candle data to cache
|
|
33158
33783
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
33159
33784
|
* @param exchangeName - Exchange name
|
|
33160
33785
|
*/
|
|
@@ -33225,14 +33850,18 @@ class ExchangeInstance {
|
|
|
33225
33850
|
});
|
|
33226
33851
|
const getCandles = this._methods.getCandles;
|
|
33227
33852
|
const step = INTERVAL_MINUTES$1[interval];
|
|
33228
|
-
|
|
33229
|
-
|
|
33230
|
-
throw new Error(`ExchangeInstance unknown time adjust for interval=${interval}`);
|
|
33853
|
+
if (!step) {
|
|
33854
|
+
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
33231
33855
|
}
|
|
33856
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33857
|
+
// Align when down to interval boundary
|
|
33232
33858
|
const when = await GET_TIMESTAMP_FN();
|
|
33233
|
-
const
|
|
33234
|
-
const
|
|
33235
|
-
|
|
33859
|
+
const whenTimestamp = when.getTime();
|
|
33860
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
|
|
33861
|
+
// Calculate since: go back limit candles from aligned when
|
|
33862
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
33863
|
+
const since = new Date(sinceTimestamp);
|
|
33864
|
+
const untilTimestamp = alignedWhen;
|
|
33236
33865
|
// Try to read from cache first
|
|
33237
33866
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33238
33867
|
if (cachedCandles !== null) {
|
|
@@ -33251,7 +33880,7 @@ class ExchangeInstance {
|
|
|
33251
33880
|
remaining -= chunkLimit;
|
|
33252
33881
|
if (remaining > 0) {
|
|
33253
33882
|
// Move currentSince forward by the number of candles fetched
|
|
33254
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
33883
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33255
33884
|
}
|
|
33256
33885
|
}
|
|
33257
33886
|
}
|
|
@@ -33259,27 +33888,25 @@ class ExchangeInstance {
|
|
|
33259
33888
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33260
33889
|
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
33261
33890
|
}
|
|
33262
|
-
// Filter candles to strictly match the requested range
|
|
33263
|
-
const whenTimestamp = when.getTime();
|
|
33264
|
-
const stepMs = step * MS_PER_MINUTE;
|
|
33265
|
-
const filteredData = allData.filter((candle) => {
|
|
33266
|
-
// EXCLUSIVE boundaries:
|
|
33267
|
-
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
33268
|
-
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
33269
|
-
if (candle.timestamp <= sinceTimestamp) {
|
|
33270
|
-
return false;
|
|
33271
|
-
}
|
|
33272
|
-
// Check against current time (when)
|
|
33273
|
-
// Only allow candles that have fully CLOSED before "when"
|
|
33274
|
-
return candle.timestamp + stepMs < whenTimestamp;
|
|
33275
|
-
});
|
|
33276
33891
|
// Apply distinct by timestamp to remove duplicates
|
|
33277
|
-
const uniqueData = Array.from(new Map(
|
|
33278
|
-
if (
|
|
33279
|
-
bt.loggerService.warn(`ExchangeInstance Removed ${
|
|
33892
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
33893
|
+
if (allData.length !== uniqueData.length) {
|
|
33894
|
+
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
33895
|
+
}
|
|
33896
|
+
// Validate adapter returned data
|
|
33897
|
+
if (uniqueData.length === 0) {
|
|
33898
|
+
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
33899
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33280
33900
|
}
|
|
33281
|
-
if (uniqueData.
|
|
33282
|
-
|
|
33901
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
33902
|
+
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
33903
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
33904
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
33905
|
+
}
|
|
33906
|
+
if (uniqueData.length !== limit) {
|
|
33907
|
+
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
33908
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
33909
|
+
`Adapter must return exact number of candles requested.`);
|
|
33283
33910
|
}
|
|
33284
33911
|
// Write to cache after successful fetch
|
|
33285
33912
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
@@ -33450,10 +34077,11 @@ class ExchangeInstance {
|
|
|
33450
34077
|
if (!step) {
|
|
33451
34078
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
33452
34079
|
}
|
|
34080
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33453
34081
|
const when = await GET_TIMESTAMP_FN();
|
|
33454
34082
|
const nowTimestamp = when.getTime();
|
|
34083
|
+
const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
|
|
33455
34084
|
let sinceTimestamp;
|
|
33456
|
-
let untilTimestamp;
|
|
33457
34085
|
let calculatedLimit;
|
|
33458
34086
|
// Case 1: all three parameters provided
|
|
33459
34087
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -33463,8 +34091,8 @@ class ExchangeInstance {
|
|
|
33463
34091
|
if (eDate > nowTimestamp) {
|
|
33464
34092
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33465
34093
|
}
|
|
33466
|
-
|
|
33467
|
-
|
|
34094
|
+
// Align sDate down to interval boundary
|
|
34095
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33468
34096
|
calculatedLimit = limit;
|
|
33469
34097
|
}
|
|
33470
34098
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -33475,9 +34103,10 @@ class ExchangeInstance {
|
|
|
33475
34103
|
if (eDate > nowTimestamp) {
|
|
33476
34104
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33477
34105
|
}
|
|
33478
|
-
|
|
33479
|
-
|
|
33480
|
-
|
|
34106
|
+
// Align sDate down to interval boundary
|
|
34107
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
34108
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
34109
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
33481
34110
|
if (calculatedLimit <= 0) {
|
|
33482
34111
|
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
33483
34112
|
}
|
|
@@ -33487,23 +34116,24 @@ class ExchangeInstance {
|
|
|
33487
34116
|
if (eDate > nowTimestamp) {
|
|
33488
34117
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33489
34118
|
}
|
|
33490
|
-
|
|
33491
|
-
|
|
34119
|
+
// Align eDate down and calculate sinceTimestamp
|
|
34120
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
34121
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
33492
34122
|
calculatedLimit = limit;
|
|
33493
34123
|
}
|
|
33494
34124
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
33495
34125
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
33496
|
-
|
|
33497
|
-
|
|
33498
|
-
|
|
33499
|
-
|
|
34126
|
+
// Align sDate down to interval boundary
|
|
34127
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
34128
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
34129
|
+
if (endTimestamp > nowTimestamp) {
|
|
34130
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33500
34131
|
}
|
|
33501
34132
|
calculatedLimit = limit;
|
|
33502
34133
|
}
|
|
33503
34134
|
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
33504
34135
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
33505
|
-
|
|
33506
|
-
sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
|
|
34136
|
+
sinceTimestamp = alignedNow - limit * stepMs;
|
|
33507
34137
|
calculatedLimit = limit;
|
|
33508
34138
|
}
|
|
33509
34139
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -33513,6 +34143,7 @@ class ExchangeInstance {
|
|
|
33513
34143
|
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
33514
34144
|
}
|
|
33515
34145
|
// Try to read from cache first
|
|
34146
|
+
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
33516
34147
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33517
34148
|
if (cachedCandles !== null) {
|
|
33518
34149
|
return cachedCandles;
|
|
@@ -33531,25 +34162,32 @@ class ExchangeInstance {
|
|
|
33531
34162
|
allData.push(...chunkData);
|
|
33532
34163
|
remaining -= chunkLimit;
|
|
33533
34164
|
if (remaining > 0) {
|
|
33534
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
34165
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33535
34166
|
}
|
|
33536
34167
|
}
|
|
33537
34168
|
}
|
|
33538
34169
|
else {
|
|
33539
34170
|
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
33540
34171
|
}
|
|
33541
|
-
// Filter candles to strictly match the requested range
|
|
33542
|
-
// Only include candles that have fully CLOSED before untilTimestamp
|
|
33543
|
-
const stepMs = step * MS_PER_MINUTE;
|
|
33544
|
-
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
33545
|
-
candle.timestamp + stepMs < untilTimestamp);
|
|
33546
34172
|
// Apply distinct by timestamp to remove duplicates
|
|
33547
|
-
const uniqueData = Array.from(new Map(
|
|
33548
|
-
if (
|
|
33549
|
-
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${
|
|
34173
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
34174
|
+
if (allData.length !== uniqueData.length) {
|
|
34175
|
+
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
34176
|
+
}
|
|
34177
|
+
// Validate adapter returned data
|
|
34178
|
+
if (uniqueData.length === 0) {
|
|
34179
|
+
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
34180
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33550
34181
|
}
|
|
33551
|
-
if (uniqueData.
|
|
33552
|
-
|
|
34182
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
34183
|
+
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
34184
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
34185
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
34186
|
+
}
|
|
34187
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
34188
|
+
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
34189
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
34190
|
+
`Adapter must return exact number of candles requested.`);
|
|
33553
34191
|
}
|
|
33554
34192
|
// Write to cache after successful fetch
|
|
33555
34193
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
@@ -34341,6 +34979,29 @@ class NotificationInstance {
|
|
|
34341
34979
|
createdAt: data.timestamp,
|
|
34342
34980
|
});
|
|
34343
34981
|
}
|
|
34982
|
+
else if (data.action === "activate-scheduled") {
|
|
34983
|
+
this._addNotification({
|
|
34984
|
+
type: "activate_scheduled.commit",
|
|
34985
|
+
id: CREATE_KEY_FN(),
|
|
34986
|
+
timestamp: data.timestamp,
|
|
34987
|
+
backtest: data.backtest,
|
|
34988
|
+
symbol: data.symbol,
|
|
34989
|
+
strategyName: data.strategyName,
|
|
34990
|
+
exchangeName: data.exchangeName,
|
|
34991
|
+
signalId: data.signalId,
|
|
34992
|
+
activateId: data.activateId,
|
|
34993
|
+
currentPrice: data.currentPrice,
|
|
34994
|
+
position: data.position,
|
|
34995
|
+
priceOpen: data.priceOpen,
|
|
34996
|
+
priceTakeProfit: data.priceTakeProfit,
|
|
34997
|
+
priceStopLoss: data.priceStopLoss,
|
|
34998
|
+
originalPriceTakeProfit: data.originalPriceTakeProfit,
|
|
34999
|
+
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
35000
|
+
scheduledAt: data.scheduledAt,
|
|
35001
|
+
pendingAt: data.pendingAt,
|
|
35002
|
+
createdAt: data.timestamp,
|
|
35003
|
+
});
|
|
35004
|
+
}
|
|
34344
35005
|
};
|
|
34345
35006
|
/**
|
|
34346
35007
|
* Processes risk rejection events.
|
|
@@ -35145,4 +35806,4 @@ const set = (object, path, value) => {
|
|
|
35145
35806
|
}
|
|
35146
35807
|
};
|
|
35147
35808
|
|
|
35148
|
-
export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
|
|
35809
|
+
export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
|