backtest-kit 9.8.5 → 10.2.0
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/LICENSE +21 -21
- package/README.md +1995 -1898
- package/build/index.cjs +1387 -412
- package/build/index.mjs +1386 -413
- package/package.json +86 -86
- package/types.d.ts +448 -8
package/build/index.mjs
CHANGED
|
@@ -942,7 +942,7 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
942
942
|
|
|
943
943
|
var _a$3;
|
|
944
944
|
/** Logger service injected as DI singleton */
|
|
945
|
-
const LOGGER_SERVICE$
|
|
945
|
+
const LOGGER_SERVICE$9 = new LoggerService();
|
|
946
946
|
/** Symbol key for the singleshot waitForInit function on PersistBase instances. */
|
|
947
947
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
948
948
|
// Calculate step in milliseconds for candle close time validation
|
|
@@ -1065,7 +1065,7 @@ const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
|
|
|
1065
1065
|
const BASE_UNLINK_RETRY_COUNT = 5;
|
|
1066
1066
|
const BASE_UNLINK_RETRY_DELAY = 1000;
|
|
1067
1067
|
const BASE_WAIT_FOR_INIT_FN = async (self) => {
|
|
1068
|
-
LOGGER_SERVICE$
|
|
1068
|
+
LOGGER_SERVICE$9.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
|
|
1069
1069
|
entityName: self.entityName,
|
|
1070
1070
|
directory: self._directory,
|
|
1071
1071
|
});
|
|
@@ -1123,7 +1123,7 @@ class PersistBase {
|
|
|
1123
1123
|
this.entityName = entityName;
|
|
1124
1124
|
this.baseDir = baseDir;
|
|
1125
1125
|
this[_a$3] = singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
|
|
1126
|
-
LOGGER_SERVICE$
|
|
1126
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
|
|
1127
1127
|
entityName: this.entityName,
|
|
1128
1128
|
baseDir,
|
|
1129
1129
|
});
|
|
@@ -1139,14 +1139,14 @@ class PersistBase {
|
|
|
1139
1139
|
return join(this.baseDir, this.entityName, `${entityId}.json`);
|
|
1140
1140
|
}
|
|
1141
1141
|
async waitForInit(initial) {
|
|
1142
|
-
LOGGER_SERVICE$
|
|
1142
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
|
|
1143
1143
|
entityName: this.entityName,
|
|
1144
1144
|
initial,
|
|
1145
1145
|
});
|
|
1146
1146
|
await this[BASE_WAIT_FOR_INIT_SYMBOL]();
|
|
1147
1147
|
}
|
|
1148
1148
|
async readValue(entityId) {
|
|
1149
|
-
LOGGER_SERVICE$
|
|
1149
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
|
|
1150
1150
|
entityName: this.entityName,
|
|
1151
1151
|
entityId,
|
|
1152
1152
|
});
|
|
@@ -1163,7 +1163,7 @@ class PersistBase {
|
|
|
1163
1163
|
}
|
|
1164
1164
|
}
|
|
1165
1165
|
async hasValue(entityId) {
|
|
1166
|
-
LOGGER_SERVICE$
|
|
1166
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
|
|
1167
1167
|
entityName: this.entityName,
|
|
1168
1168
|
entityId,
|
|
1169
1169
|
});
|
|
@@ -1180,7 +1180,7 @@ class PersistBase {
|
|
|
1180
1180
|
}
|
|
1181
1181
|
}
|
|
1182
1182
|
async writeValue(entityId, entity) {
|
|
1183
|
-
LOGGER_SERVICE$
|
|
1183
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
|
|
1184
1184
|
entityName: this.entityName,
|
|
1185
1185
|
entityId,
|
|
1186
1186
|
});
|
|
@@ -1202,7 +1202,7 @@ class PersistBase {
|
|
|
1202
1202
|
* @throws Error if reading fails
|
|
1203
1203
|
*/
|
|
1204
1204
|
async *keys() {
|
|
1205
|
-
LOGGER_SERVICE$
|
|
1205
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
|
|
1206
1206
|
entityName: this.entityName,
|
|
1207
1207
|
});
|
|
1208
1208
|
try {
|
|
@@ -1347,7 +1347,7 @@ class PersistSignalUtils {
|
|
|
1347
1347
|
* @returns Promise resolving to signal or null if none persisted
|
|
1348
1348
|
*/
|
|
1349
1349
|
this.readSignalData = async (symbol, strategyName, exchangeName) => {
|
|
1350
|
-
LOGGER_SERVICE$
|
|
1350
|
+
LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
|
|
1351
1351
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1352
1352
|
const isInitial = !this.getStorage.has(key);
|
|
1353
1353
|
const instance = this.getStorage(symbol, strategyName, exchangeName);
|
|
@@ -1365,7 +1365,7 @@ class PersistSignalUtils {
|
|
|
1365
1365
|
* @returns Promise that resolves when write is complete
|
|
1366
1366
|
*/
|
|
1367
1367
|
this.writeSignalData = async (signalRow, symbol, strategyName, exchangeName) => {
|
|
1368
|
-
LOGGER_SERVICE$
|
|
1368
|
+
LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1369
1369
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1370
1370
|
const isInitial = !this.getStorage.has(key);
|
|
1371
1371
|
const instance = this.getStorage(symbol, strategyName, exchangeName);
|
|
@@ -1380,7 +1380,7 @@ class PersistSignalUtils {
|
|
|
1380
1380
|
* @param Ctor - Custom IPersistSignalInstance constructor
|
|
1381
1381
|
*/
|
|
1382
1382
|
usePersistSignalAdapter(Ctor) {
|
|
1383
|
-
LOGGER_SERVICE$
|
|
1383
|
+
LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
|
|
1384
1384
|
this.PersistSignalInstanceCtor = Ctor;
|
|
1385
1385
|
this.getStorage.clear();
|
|
1386
1386
|
}
|
|
@@ -1389,21 +1389,21 @@ class PersistSignalUtils {
|
|
|
1389
1389
|
* Call when process.cwd() changes between strategy iterations.
|
|
1390
1390
|
*/
|
|
1391
1391
|
clear() {
|
|
1392
|
-
LOGGER_SERVICE$
|
|
1392
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_CLEAR);
|
|
1393
1393
|
this.getStorage.clear();
|
|
1394
1394
|
}
|
|
1395
1395
|
/**
|
|
1396
1396
|
* Switches to the default file-based PersistSignalInstance.
|
|
1397
1397
|
*/
|
|
1398
1398
|
useJson() {
|
|
1399
|
-
LOGGER_SERVICE$
|
|
1399
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
|
|
1400
1400
|
this.usePersistSignalAdapter(PersistSignalInstance);
|
|
1401
1401
|
}
|
|
1402
1402
|
/**
|
|
1403
1403
|
* Switches to PersistSignalDummyInstance (all operations are no-ops).
|
|
1404
1404
|
*/
|
|
1405
1405
|
useDummy() {
|
|
1406
|
-
LOGGER_SERVICE$
|
|
1406
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1407
1407
|
this.usePersistSignalAdapter(PersistSignalDummyInstance);
|
|
1408
1408
|
}
|
|
1409
1409
|
}
|
|
@@ -1543,7 +1543,7 @@ class PersistRiskUtils {
|
|
|
1543
1543
|
* @returns Promise resolving to position entries (empty array if none)
|
|
1544
1544
|
*/
|
|
1545
1545
|
this.readPositionData = async (riskName, exchangeName, when) => {
|
|
1546
|
-
LOGGER_SERVICE$
|
|
1546
|
+
LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
|
|
1547
1547
|
const key = `${riskName}:${exchangeName}`;
|
|
1548
1548
|
const isInitial = !this.getRiskStorage.has(key);
|
|
1549
1549
|
const instance = this.getRiskStorage(riskName, exchangeName);
|
|
@@ -1561,7 +1561,7 @@ class PersistRiskUtils {
|
|
|
1561
1561
|
* @returns Promise that resolves when write is complete
|
|
1562
1562
|
*/
|
|
1563
1563
|
this.writePositionData = async (riskRow, riskName, exchangeName, when) => {
|
|
1564
|
-
LOGGER_SERVICE$
|
|
1564
|
+
LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1565
1565
|
const key = `${riskName}:${exchangeName}`;
|
|
1566
1566
|
const isInitial = !this.getRiskStorage.has(key);
|
|
1567
1567
|
const instance = this.getRiskStorage(riskName, exchangeName);
|
|
@@ -1576,7 +1576,7 @@ class PersistRiskUtils {
|
|
|
1576
1576
|
* @param Ctor - Custom IPersistRiskInstance constructor
|
|
1577
1577
|
*/
|
|
1578
1578
|
usePersistRiskAdapter(Ctor) {
|
|
1579
|
-
LOGGER_SERVICE$
|
|
1579
|
+
LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
|
|
1580
1580
|
this.PersistRiskInstanceCtor = Ctor;
|
|
1581
1581
|
this.getRiskStorage.clear();
|
|
1582
1582
|
}
|
|
@@ -1585,21 +1585,21 @@ class PersistRiskUtils {
|
|
|
1585
1585
|
* Call when process.cwd() changes between strategy iterations.
|
|
1586
1586
|
*/
|
|
1587
1587
|
clear() {
|
|
1588
|
-
LOGGER_SERVICE$
|
|
1588
|
+
LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_CLEAR);
|
|
1589
1589
|
this.getRiskStorage.clear();
|
|
1590
1590
|
}
|
|
1591
1591
|
/**
|
|
1592
1592
|
* Switches to the default file-based PersistRiskInstance.
|
|
1593
1593
|
*/
|
|
1594
1594
|
useJson() {
|
|
1595
|
-
LOGGER_SERVICE$
|
|
1595
|
+
LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
|
|
1596
1596
|
this.usePersistRiskAdapter(PersistRiskInstance);
|
|
1597
1597
|
}
|
|
1598
1598
|
/**
|
|
1599
1599
|
* Switches to PersistRiskDummyInstance (all operations are no-ops).
|
|
1600
1600
|
*/
|
|
1601
1601
|
useDummy() {
|
|
1602
|
-
LOGGER_SERVICE$
|
|
1602
|
+
LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1603
1603
|
this.usePersistRiskAdapter(PersistRiskDummyInstance);
|
|
1604
1604
|
}
|
|
1605
1605
|
}
|
|
@@ -1738,7 +1738,7 @@ class PersistScheduleUtils {
|
|
|
1738
1738
|
* @returns Promise resolving to scheduled signal or null if none persisted
|
|
1739
1739
|
*/
|
|
1740
1740
|
this.readScheduleData = async (symbol, strategyName, exchangeName) => {
|
|
1741
|
-
LOGGER_SERVICE$
|
|
1741
|
+
LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
|
|
1742
1742
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1743
1743
|
const isInitial = !this.getScheduleStorage.has(key);
|
|
1744
1744
|
const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
|
|
@@ -1756,7 +1756,7 @@ class PersistScheduleUtils {
|
|
|
1756
1756
|
* @returns Promise that resolves when write is complete
|
|
1757
1757
|
*/
|
|
1758
1758
|
this.writeScheduleData = async (scheduledSignalRow, symbol, strategyName, exchangeName) => {
|
|
1759
|
-
LOGGER_SERVICE$
|
|
1759
|
+
LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1760
1760
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1761
1761
|
const isInitial = !this.getScheduleStorage.has(key);
|
|
1762
1762
|
const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
|
|
@@ -1771,7 +1771,7 @@ class PersistScheduleUtils {
|
|
|
1771
1771
|
* @param Ctor - Custom IPersistScheduleInstance constructor
|
|
1772
1772
|
*/
|
|
1773
1773
|
usePersistScheduleAdapter(Ctor) {
|
|
1774
|
-
LOGGER_SERVICE$
|
|
1774
|
+
LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
|
|
1775
1775
|
this.PersistScheduleInstanceCtor = Ctor;
|
|
1776
1776
|
this.getScheduleStorage.clear();
|
|
1777
1777
|
}
|
|
@@ -1780,21 +1780,21 @@ class PersistScheduleUtils {
|
|
|
1780
1780
|
* Call when process.cwd() changes between strategy iterations.
|
|
1781
1781
|
*/
|
|
1782
1782
|
clear() {
|
|
1783
|
-
LOGGER_SERVICE$
|
|
1783
|
+
LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR);
|
|
1784
1784
|
this.getScheduleStorage.clear();
|
|
1785
1785
|
}
|
|
1786
1786
|
/**
|
|
1787
1787
|
* Switches to the default file-based PersistScheduleInstance.
|
|
1788
1788
|
*/
|
|
1789
1789
|
useJson() {
|
|
1790
|
-
LOGGER_SERVICE$
|
|
1790
|
+
LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
|
|
1791
1791
|
this.usePersistScheduleAdapter(PersistScheduleInstance);
|
|
1792
1792
|
}
|
|
1793
1793
|
/**
|
|
1794
1794
|
* Switches to PersistScheduleDummyInstance (all operations are no-ops).
|
|
1795
1795
|
*/
|
|
1796
1796
|
useDummy() {
|
|
1797
|
-
LOGGER_SERVICE$
|
|
1797
|
+
LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1798
1798
|
this.usePersistScheduleAdapter(PersistScheduleDummyInstance);
|
|
1799
1799
|
}
|
|
1800
1800
|
}
|
|
@@ -1939,7 +1939,7 @@ class PersistPartialUtils {
|
|
|
1939
1939
|
* @returns Promise resolving to partial data record (empty object if none)
|
|
1940
1940
|
*/
|
|
1941
1941
|
this.readPartialData = async (symbol, strategyName, signalId, exchangeName, when) => {
|
|
1942
|
-
LOGGER_SERVICE$
|
|
1942
|
+
LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
|
|
1943
1943
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1944
1944
|
const isInitial = !this.getPartialStorage.has(key);
|
|
1945
1945
|
const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
|
|
@@ -1959,7 +1959,7 @@ class PersistPartialUtils {
|
|
|
1959
1959
|
* @returns Promise that resolves when write is complete
|
|
1960
1960
|
*/
|
|
1961
1961
|
this.writePartialData = async (partialData, symbol, strategyName, signalId, exchangeName, when) => {
|
|
1962
|
-
LOGGER_SERVICE$
|
|
1962
|
+
LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1963
1963
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1964
1964
|
const isInitial = !this.getPartialStorage.has(key);
|
|
1965
1965
|
const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
|
|
@@ -1974,7 +1974,7 @@ class PersistPartialUtils {
|
|
|
1974
1974
|
* @param Ctor - Custom IPersistPartialInstance constructor
|
|
1975
1975
|
*/
|
|
1976
1976
|
usePersistPartialAdapter(Ctor) {
|
|
1977
|
-
LOGGER_SERVICE$
|
|
1977
|
+
LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
|
|
1978
1978
|
this.PersistPartialInstanceCtor = Ctor;
|
|
1979
1979
|
this.getPartialStorage.clear();
|
|
1980
1980
|
}
|
|
@@ -1983,21 +1983,21 @@ class PersistPartialUtils {
|
|
|
1983
1983
|
* Call when process.cwd() changes between strategy iterations.
|
|
1984
1984
|
*/
|
|
1985
1985
|
clear() {
|
|
1986
|
-
LOGGER_SERVICE$
|
|
1986
|
+
LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_CLEAR);
|
|
1987
1987
|
this.getPartialStorage.clear();
|
|
1988
1988
|
}
|
|
1989
1989
|
/**
|
|
1990
1990
|
* Switches to the default file-based PersistPartialInstance.
|
|
1991
1991
|
*/
|
|
1992
1992
|
useJson() {
|
|
1993
|
-
LOGGER_SERVICE$
|
|
1993
|
+
LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
|
|
1994
1994
|
this.usePersistPartialAdapter(PersistPartialInstance);
|
|
1995
1995
|
}
|
|
1996
1996
|
/**
|
|
1997
1997
|
* Switches to PersistPartialDummyInstance (all operations are no-ops).
|
|
1998
1998
|
*/
|
|
1999
1999
|
useDummy() {
|
|
2000
|
-
LOGGER_SERVICE$
|
|
2000
|
+
LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2001
2001
|
this.usePersistPartialAdapter(PersistPartialDummyInstance);
|
|
2002
2002
|
}
|
|
2003
2003
|
}
|
|
@@ -2162,7 +2162,7 @@ class PersistBreakevenUtils {
|
|
|
2162
2162
|
* @returns Promise resolving to breakeven data record (empty object if none)
|
|
2163
2163
|
*/
|
|
2164
2164
|
this.readBreakevenData = async (symbol, strategyName, signalId, exchangeName, when) => {
|
|
2165
|
-
LOGGER_SERVICE$
|
|
2165
|
+
LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA);
|
|
2166
2166
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
2167
2167
|
const isInitial = !this.getBreakevenStorage.has(key);
|
|
2168
2168
|
const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
|
|
@@ -2182,7 +2182,7 @@ class PersistBreakevenUtils {
|
|
|
2182
2182
|
* @returns Promise that resolves when write is complete
|
|
2183
2183
|
*/
|
|
2184
2184
|
this.writeBreakevenData = async (breakevenData, symbol, strategyName, signalId, exchangeName, when) => {
|
|
2185
|
-
LOGGER_SERVICE$
|
|
2185
|
+
LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2186
2186
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
2187
2187
|
const isInitial = !this.getBreakevenStorage.has(key);
|
|
2188
2188
|
const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
|
|
@@ -2197,7 +2197,7 @@ class PersistBreakevenUtils {
|
|
|
2197
2197
|
* @param Ctor - Custom IPersistBreakevenInstance constructor
|
|
2198
2198
|
*/
|
|
2199
2199
|
usePersistBreakevenAdapter(Ctor) {
|
|
2200
|
-
LOGGER_SERVICE$
|
|
2200
|
+
LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
|
|
2201
2201
|
this.PersistBreakevenInstanceCtor = Ctor;
|
|
2202
2202
|
this.getBreakevenStorage.clear();
|
|
2203
2203
|
}
|
|
@@ -2206,21 +2206,21 @@ class PersistBreakevenUtils {
|
|
|
2206
2206
|
* Call when process.cwd() changes between strategy iterations.
|
|
2207
2207
|
*/
|
|
2208
2208
|
clear() {
|
|
2209
|
-
LOGGER_SERVICE$
|
|
2209
|
+
LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_CLEAR);
|
|
2210
2210
|
this.getBreakevenStorage.clear();
|
|
2211
2211
|
}
|
|
2212
2212
|
/**
|
|
2213
2213
|
* Switches to the default file-based PersistBreakevenInstance.
|
|
2214
2214
|
*/
|
|
2215
2215
|
useJson() {
|
|
2216
|
-
LOGGER_SERVICE$
|
|
2216
|
+
LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
|
|
2217
2217
|
this.usePersistBreakevenAdapter(PersistBreakevenInstance);
|
|
2218
2218
|
}
|
|
2219
2219
|
/**
|
|
2220
2220
|
* Switches to PersistBreakevenDummyInstance (all operations are no-ops).
|
|
2221
2221
|
*/
|
|
2222
2222
|
useDummy() {
|
|
2223
|
-
LOGGER_SERVICE$
|
|
2223
|
+
LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2224
2224
|
this.usePersistBreakevenAdapter(PersistBreakevenDummyInstance);
|
|
2225
2225
|
}
|
|
2226
2226
|
}
|
|
@@ -2311,7 +2311,7 @@ class PersistCandleInstance {
|
|
|
2311
2311
|
error: errorData(error),
|
|
2312
2312
|
message: getErrorMessage(error),
|
|
2313
2313
|
};
|
|
2314
|
-
LOGGER_SERVICE$
|
|
2314
|
+
LOGGER_SERVICE$9.warn(message, payload);
|
|
2315
2315
|
console.warn(message, payload);
|
|
2316
2316
|
errorEmitter.next(error);
|
|
2317
2317
|
return null;
|
|
@@ -2333,7 +2333,7 @@ class PersistCandleInstance {
|
|
|
2333
2333
|
for (const candle of candles) {
|
|
2334
2334
|
const candleCloseTime = candle.timestamp + stepMs;
|
|
2335
2335
|
if (candleCloseTime > now) {
|
|
2336
|
-
LOGGER_SERVICE$
|
|
2336
|
+
LOGGER_SERVICE$9.debug("PersistCandleInstance.writeCandlesData: skipping incomplete candle", {
|
|
2337
2337
|
symbol: this.symbol,
|
|
2338
2338
|
interval: this.interval,
|
|
2339
2339
|
exchangeName: this.exchangeName,
|
|
@@ -2410,7 +2410,7 @@ class PersistCandleUtils {
|
|
|
2410
2410
|
* @returns Promise resolving to candles in order, or null on cache miss
|
|
2411
2411
|
*/
|
|
2412
2412
|
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
|
|
2413
|
-
LOGGER_SERVICE$
|
|
2413
|
+
LOGGER_SERVICE$9.info("PersistCandleUtils.readCandlesData", {
|
|
2414
2414
|
symbol,
|
|
2415
2415
|
interval,
|
|
2416
2416
|
exchangeName,
|
|
@@ -2434,7 +2434,7 @@ class PersistCandleUtils {
|
|
|
2434
2434
|
* @returns Promise that resolves when all writes are complete
|
|
2435
2435
|
*/
|
|
2436
2436
|
this.writeCandlesData = async (candles, symbol, interval, exchangeName) => {
|
|
2437
|
-
LOGGER_SERVICE$
|
|
2437
|
+
LOGGER_SERVICE$9.info("PersistCandleUtils.writeCandlesData", {
|
|
2438
2438
|
symbol,
|
|
2439
2439
|
interval,
|
|
2440
2440
|
exchangeName,
|
|
@@ -2454,7 +2454,7 @@ class PersistCandleUtils {
|
|
|
2454
2454
|
* @param Ctor - Custom IPersistCandleInstance constructor
|
|
2455
2455
|
*/
|
|
2456
2456
|
usePersistCandleAdapter(Ctor) {
|
|
2457
|
-
LOGGER_SERVICE$
|
|
2457
|
+
LOGGER_SERVICE$9.info("PersistCandleUtils.usePersistCandleAdapter");
|
|
2458
2458
|
this.PersistCandleInstanceCtor = Ctor;
|
|
2459
2459
|
this.getCandlesStorage.clear();
|
|
2460
2460
|
}
|
|
@@ -2463,21 +2463,21 @@ class PersistCandleUtils {
|
|
|
2463
2463
|
* Call when process.cwd() changes between strategy iterations.
|
|
2464
2464
|
*/
|
|
2465
2465
|
clear() {
|
|
2466
|
-
LOGGER_SERVICE$
|
|
2466
|
+
LOGGER_SERVICE$9.log(PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR);
|
|
2467
2467
|
this.getCandlesStorage.clear();
|
|
2468
2468
|
}
|
|
2469
2469
|
/**
|
|
2470
2470
|
* Switches to the default file-based PersistCandleInstance.
|
|
2471
2471
|
*/
|
|
2472
2472
|
useJson() {
|
|
2473
|
-
LOGGER_SERVICE$
|
|
2473
|
+
LOGGER_SERVICE$9.log("PersistCandleUtils.useJson");
|
|
2474
2474
|
this.usePersistCandleAdapter(PersistCandleInstance);
|
|
2475
2475
|
}
|
|
2476
2476
|
/**
|
|
2477
2477
|
* Switches to PersistCandleDummyInstance (always returns null on read, discards writes).
|
|
2478
2478
|
*/
|
|
2479
2479
|
useDummy() {
|
|
2480
|
-
LOGGER_SERVICE$
|
|
2480
|
+
LOGGER_SERVICE$9.log("PersistCandleUtils.useDummy");
|
|
2481
2481
|
this.usePersistCandleAdapter(PersistCandleDummyInstance);
|
|
2482
2482
|
}
|
|
2483
2483
|
}
|
|
@@ -2615,7 +2615,7 @@ class PersistStorageUtils {
|
|
|
2615
2615
|
* @returns Promise resolving to array of signal entries
|
|
2616
2616
|
*/
|
|
2617
2617
|
this.readStorageData = async (backtest) => {
|
|
2618
|
-
LOGGER_SERVICE$
|
|
2618
|
+
LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_READ_DATA);
|
|
2619
2619
|
const key = backtest ? `backtest` : `live`;
|
|
2620
2620
|
const isInitial = !this.getStorage.has(key);
|
|
2621
2621
|
const instance = this.getStorage(backtest);
|
|
@@ -2631,7 +2631,7 @@ class PersistStorageUtils {
|
|
|
2631
2631
|
* @returns Promise that resolves when write is complete
|
|
2632
2632
|
*/
|
|
2633
2633
|
this.writeStorageData = async (signalData, backtest) => {
|
|
2634
|
-
LOGGER_SERVICE$
|
|
2634
|
+
LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2635
2635
|
const key = backtest ? `backtest` : `live`;
|
|
2636
2636
|
const isInitial = !this.getStorage.has(key);
|
|
2637
2637
|
const instance = this.getStorage(backtest);
|
|
@@ -2646,7 +2646,7 @@ class PersistStorageUtils {
|
|
|
2646
2646
|
* @param Ctor - Custom IPersistStorageInstance constructor
|
|
2647
2647
|
*/
|
|
2648
2648
|
usePersistStorageAdapter(Ctor) {
|
|
2649
|
-
LOGGER_SERVICE$
|
|
2649
|
+
LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER);
|
|
2650
2650
|
this.PersistStorageInstanceCtor = Ctor;
|
|
2651
2651
|
this.getStorage.clear();
|
|
2652
2652
|
}
|
|
@@ -2655,21 +2655,21 @@ class PersistStorageUtils {
|
|
|
2655
2655
|
* Call when process.cwd() changes between strategy iterations.
|
|
2656
2656
|
*/
|
|
2657
2657
|
clear() {
|
|
2658
|
-
LOGGER_SERVICE$
|
|
2658
|
+
LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_CLEAR);
|
|
2659
2659
|
this.getStorage.clear();
|
|
2660
2660
|
}
|
|
2661
2661
|
/**
|
|
2662
2662
|
* Switches to the default file-based PersistStorageInstance.
|
|
2663
2663
|
*/
|
|
2664
2664
|
useJson() {
|
|
2665
|
-
LOGGER_SERVICE$
|
|
2665
|
+
LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON);
|
|
2666
2666
|
this.usePersistStorageAdapter(PersistStorageInstance);
|
|
2667
2667
|
}
|
|
2668
2668
|
/**
|
|
2669
2669
|
* Switches to PersistStorageDummyInstance (all operations are no-ops).
|
|
2670
2670
|
*/
|
|
2671
2671
|
useDummy() {
|
|
2672
|
-
LOGGER_SERVICE$
|
|
2672
|
+
LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2673
2673
|
this.usePersistStorageAdapter(PersistStorageDummyInstance);
|
|
2674
2674
|
}
|
|
2675
2675
|
}
|
|
@@ -2796,7 +2796,7 @@ class PersistNotificationUtils {
|
|
|
2796
2796
|
* @returns Promise resolving to array of notification entries
|
|
2797
2797
|
*/
|
|
2798
2798
|
this.readNotificationData = async (backtest) => {
|
|
2799
|
-
LOGGER_SERVICE$
|
|
2799
|
+
LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
|
|
2800
2800
|
const key = backtest ? `backtest` : `live`;
|
|
2801
2801
|
const isInitial = !this.getNotificationStorage.has(key);
|
|
2802
2802
|
const instance = this.getNotificationStorage(backtest);
|
|
@@ -2812,7 +2812,7 @@ class PersistNotificationUtils {
|
|
|
2812
2812
|
* @returns Promise that resolves when write is complete
|
|
2813
2813
|
*/
|
|
2814
2814
|
this.writeNotificationData = async (notificationData, backtest) => {
|
|
2815
|
-
LOGGER_SERVICE$
|
|
2815
|
+
LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2816
2816
|
const key = backtest ? `backtest` : `live`;
|
|
2817
2817
|
const isInitial = !this.getNotificationStorage.has(key);
|
|
2818
2818
|
const instance = this.getNotificationStorage(backtest);
|
|
@@ -2827,7 +2827,7 @@ class PersistNotificationUtils {
|
|
|
2827
2827
|
* @param Ctor - Custom IPersistNotificationInstance constructor
|
|
2828
2828
|
*/
|
|
2829
2829
|
usePersistNotificationAdapter(Ctor) {
|
|
2830
|
-
LOGGER_SERVICE$
|
|
2830
|
+
LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
|
|
2831
2831
|
this.PersistNotificationInstanceCtor = Ctor;
|
|
2832
2832
|
this.getNotificationStorage.clear();
|
|
2833
2833
|
}
|
|
@@ -2837,21 +2837,21 @@ class PersistNotificationUtils {
|
|
|
2837
2837
|
* instances are created with the updated base path.
|
|
2838
2838
|
*/
|
|
2839
2839
|
clear() {
|
|
2840
|
-
LOGGER_SERVICE$
|
|
2840
|
+
LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_CLEAR);
|
|
2841
2841
|
this.getNotificationStorage.clear();
|
|
2842
2842
|
}
|
|
2843
2843
|
/**
|
|
2844
2844
|
* Switches to the default file-based PersistNotificationInstance.
|
|
2845
2845
|
*/
|
|
2846
2846
|
useJson() {
|
|
2847
|
-
LOGGER_SERVICE$
|
|
2847
|
+
LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
|
|
2848
2848
|
this.usePersistNotificationAdapter(PersistNotificationInstance);
|
|
2849
2849
|
}
|
|
2850
2850
|
/**
|
|
2851
2851
|
* Switches to PersistNotificationDummyInstance (all operations are no-ops).
|
|
2852
2852
|
*/
|
|
2853
2853
|
useDummy() {
|
|
2854
|
-
LOGGER_SERVICE$
|
|
2854
|
+
LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2855
2855
|
this.usePersistNotificationAdapter(PersistNotificationDummyInstance);
|
|
2856
2856
|
}
|
|
2857
2857
|
}
|
|
@@ -2975,7 +2975,7 @@ class PersistLogUtils {
|
|
|
2975
2975
|
* @returns Promise resolving to array of log entries
|
|
2976
2976
|
*/
|
|
2977
2977
|
this.readLogData = async () => {
|
|
2978
|
-
LOGGER_SERVICE$
|
|
2978
|
+
LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
|
|
2979
2979
|
const isInitial = !this._logInstance;
|
|
2980
2980
|
const instance = this.getLogInstance();
|
|
2981
2981
|
await instance.waitForInit(isInitial);
|
|
@@ -2989,7 +2989,7 @@ class PersistLogUtils {
|
|
|
2989
2989
|
* @returns Promise that resolves when write is complete
|
|
2990
2990
|
*/
|
|
2991
2991
|
this.writeLogData = async (logData) => {
|
|
2992
|
-
LOGGER_SERVICE$
|
|
2992
|
+
LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2993
2993
|
const isInitial = !this._logInstance;
|
|
2994
2994
|
const instance = this.getLogInstance();
|
|
2995
2995
|
await instance.waitForInit(isInitial);
|
|
@@ -3014,7 +3014,7 @@ class PersistLogUtils {
|
|
|
3014
3014
|
* @param Ctor - Custom IPersistLogInstance constructor
|
|
3015
3015
|
*/
|
|
3016
3016
|
usePersistLogAdapter(Ctor) {
|
|
3017
|
-
LOGGER_SERVICE$
|
|
3017
|
+
LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
|
|
3018
3018
|
this.PersistLogInstanceCtor = Ctor;
|
|
3019
3019
|
this._logInstance = null;
|
|
3020
3020
|
}
|
|
@@ -3023,21 +3023,21 @@ class PersistLogUtils {
|
|
|
3023
3023
|
* Call when process.cwd() changes between strategy iterations.
|
|
3024
3024
|
*/
|
|
3025
3025
|
clear() {
|
|
3026
|
-
LOGGER_SERVICE$
|
|
3026
|
+
LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_CLEAR);
|
|
3027
3027
|
this._logInstance = null;
|
|
3028
3028
|
}
|
|
3029
3029
|
/**
|
|
3030
3030
|
* Switches to the default file-based PersistLogInstance.
|
|
3031
3031
|
*/
|
|
3032
3032
|
useJson() {
|
|
3033
|
-
LOGGER_SERVICE$
|
|
3033
|
+
LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
|
|
3034
3034
|
this.usePersistLogAdapter(PersistLogInstance);
|
|
3035
3035
|
}
|
|
3036
3036
|
/**
|
|
3037
3037
|
* Switches to PersistLogDummyInstance (all operations are no-ops).
|
|
3038
3038
|
*/
|
|
3039
3039
|
useDummy() {
|
|
3040
|
-
LOGGER_SERVICE$
|
|
3040
|
+
LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3041
3041
|
this.usePersistLogAdapter(PersistLogDummyInstance);
|
|
3042
3042
|
}
|
|
3043
3043
|
}
|
|
@@ -3199,7 +3199,7 @@ class PersistMeasureUtils {
|
|
|
3199
3199
|
* @returns Promise resolving to cached value, or null if not found / soft-deleted
|
|
3200
3200
|
*/
|
|
3201
3201
|
this.readMeasureData = async (bucket, key) => {
|
|
3202
|
-
LOGGER_SERVICE$
|
|
3202
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
|
|
3203
3203
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3204
3204
|
const instance = this.getMeasureStorage(bucket);
|
|
3205
3205
|
await instance.waitForInit(isInitial);
|
|
@@ -3215,7 +3215,7 @@ class PersistMeasureUtils {
|
|
|
3215
3215
|
* @returns Promise that resolves when write is complete
|
|
3216
3216
|
*/
|
|
3217
3217
|
this.writeMeasureData = async (data, bucket, key, when) => {
|
|
3218
|
-
LOGGER_SERVICE$
|
|
3218
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
|
|
3219
3219
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3220
3220
|
const instance = this.getMeasureStorage(bucket);
|
|
3221
3221
|
await instance.waitForInit(isInitial);
|
|
@@ -3230,7 +3230,7 @@ class PersistMeasureUtils {
|
|
|
3230
3230
|
* @returns Promise that resolves when removal is complete
|
|
3231
3231
|
*/
|
|
3232
3232
|
this.removeMeasureData = async (bucket, key) => {
|
|
3233
|
-
LOGGER_SERVICE$
|
|
3233
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
|
|
3234
3234
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3235
3235
|
const instance = this.getMeasureStorage(bucket);
|
|
3236
3236
|
await instance.waitForInit(isInitial);
|
|
@@ -3244,7 +3244,7 @@ class PersistMeasureUtils {
|
|
|
3244
3244
|
* @param Ctor - Custom IPersistMeasureInstance constructor
|
|
3245
3245
|
*/
|
|
3246
3246
|
usePersistMeasureAdapter(Ctor) {
|
|
3247
|
-
LOGGER_SERVICE$
|
|
3247
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
|
|
3248
3248
|
this.PersistMeasureInstanceCtor = Ctor;
|
|
3249
3249
|
this.getMeasureStorage.clear();
|
|
3250
3250
|
}
|
|
@@ -3256,7 +3256,7 @@ class PersistMeasureUtils {
|
|
|
3256
3256
|
* @returns AsyncGenerator yielding entry keys
|
|
3257
3257
|
*/
|
|
3258
3258
|
async *listMeasureData(bucket) {
|
|
3259
|
-
LOGGER_SERVICE$
|
|
3259
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
|
|
3260
3260
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3261
3261
|
const instance = this.getMeasureStorage(bucket);
|
|
3262
3262
|
await instance.waitForInit(isInitial);
|
|
@@ -3267,21 +3267,21 @@ class PersistMeasureUtils {
|
|
|
3267
3267
|
* Call when process.cwd() changes between strategy iterations.
|
|
3268
3268
|
*/
|
|
3269
3269
|
clear() {
|
|
3270
|
-
LOGGER_SERVICE$
|
|
3270
|
+
LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR);
|
|
3271
3271
|
this.getMeasureStorage.clear();
|
|
3272
3272
|
}
|
|
3273
3273
|
/**
|
|
3274
3274
|
* Switches to the default file-based PersistMeasureInstance.
|
|
3275
3275
|
*/
|
|
3276
3276
|
useJson() {
|
|
3277
|
-
LOGGER_SERVICE$
|
|
3277
|
+
LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
|
|
3278
3278
|
this.usePersistMeasureAdapter(PersistMeasureInstance);
|
|
3279
3279
|
}
|
|
3280
3280
|
/**
|
|
3281
3281
|
* Switches to PersistMeasureDummyInstance (all operations are no-ops).
|
|
3282
3282
|
*/
|
|
3283
3283
|
useDummy() {
|
|
3284
|
-
LOGGER_SERVICE$
|
|
3284
|
+
LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3285
3285
|
this.usePersistMeasureAdapter(PersistMeasureDummyInstance);
|
|
3286
3286
|
}
|
|
3287
3287
|
}
|
|
@@ -3440,7 +3440,7 @@ class PersistIntervalUtils {
|
|
|
3440
3440
|
* @returns Promise resolving to marker data, or null if not found / soft-deleted
|
|
3441
3441
|
*/
|
|
3442
3442
|
this.readIntervalData = async (bucket, key) => {
|
|
3443
|
-
LOGGER_SERVICE$
|
|
3443
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
|
|
3444
3444
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3445
3445
|
const instance = this.getIntervalStorage(bucket);
|
|
3446
3446
|
await instance.waitForInit(isInitial);
|
|
@@ -3456,7 +3456,7 @@ class PersistIntervalUtils {
|
|
|
3456
3456
|
* @returns Promise that resolves when write is complete
|
|
3457
3457
|
*/
|
|
3458
3458
|
this.writeIntervalData = async (data, bucket, key, when) => {
|
|
3459
|
-
LOGGER_SERVICE$
|
|
3459
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
|
|
3460
3460
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3461
3461
|
const instance = this.getIntervalStorage(bucket);
|
|
3462
3462
|
await instance.waitForInit(isInitial);
|
|
@@ -3471,7 +3471,7 @@ class PersistIntervalUtils {
|
|
|
3471
3471
|
* @returns Promise that resolves when removal is complete
|
|
3472
3472
|
*/
|
|
3473
3473
|
this.removeIntervalData = async (bucket, key) => {
|
|
3474
|
-
LOGGER_SERVICE$
|
|
3474
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
|
|
3475
3475
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3476
3476
|
const instance = this.getIntervalStorage(bucket);
|
|
3477
3477
|
await instance.waitForInit(isInitial);
|
|
@@ -3485,7 +3485,7 @@ class PersistIntervalUtils {
|
|
|
3485
3485
|
* @param Ctor - Custom IPersistIntervalInstance constructor
|
|
3486
3486
|
*/
|
|
3487
3487
|
usePersistIntervalAdapter(Ctor) {
|
|
3488
|
-
LOGGER_SERVICE$
|
|
3488
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
|
|
3489
3489
|
this.PersistIntervalInstanceCtor = Ctor;
|
|
3490
3490
|
this.getIntervalStorage.clear();
|
|
3491
3491
|
}
|
|
@@ -3497,7 +3497,7 @@ class PersistIntervalUtils {
|
|
|
3497
3497
|
* @returns AsyncGenerator yielding marker keys
|
|
3498
3498
|
*/
|
|
3499
3499
|
async *listIntervalData(bucket) {
|
|
3500
|
-
LOGGER_SERVICE$
|
|
3500
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
|
|
3501
3501
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3502
3502
|
const instance = this.getIntervalStorage(bucket);
|
|
3503
3503
|
await instance.waitForInit(isInitial);
|
|
@@ -3508,21 +3508,21 @@ class PersistIntervalUtils {
|
|
|
3508
3508
|
* Call when process.cwd() changes between strategy iterations.
|
|
3509
3509
|
*/
|
|
3510
3510
|
clear() {
|
|
3511
|
-
LOGGER_SERVICE$
|
|
3511
|
+
LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
|
|
3512
3512
|
this.getIntervalStorage.clear();
|
|
3513
3513
|
}
|
|
3514
3514
|
/**
|
|
3515
3515
|
* Switches to the default file-based PersistIntervalInstance.
|
|
3516
3516
|
*/
|
|
3517
3517
|
useJson() {
|
|
3518
|
-
LOGGER_SERVICE$
|
|
3518
|
+
LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
|
|
3519
3519
|
this.usePersistIntervalAdapter(PersistIntervalInstance);
|
|
3520
3520
|
}
|
|
3521
3521
|
/**
|
|
3522
3522
|
* Switches to PersistIntervalDummyInstance (all operations are no-ops).
|
|
3523
3523
|
*/
|
|
3524
3524
|
useDummy() {
|
|
3525
|
-
LOGGER_SERVICE$
|
|
3525
|
+
LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3526
3526
|
this.usePersistIntervalAdapter(PersistIntervalDummyInstance);
|
|
3527
3527
|
}
|
|
3528
3528
|
}
|
|
@@ -3728,7 +3728,7 @@ class PersistMemoryUtils {
|
|
|
3728
3728
|
* @returns Promise resolving to entry data, or null if not found / soft-deleted
|
|
3729
3729
|
*/
|
|
3730
3730
|
this.readMemoryData = async (signalId, bucketName, memoryId) => {
|
|
3731
|
-
LOGGER_SERVICE$
|
|
3731
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName, memoryId });
|
|
3732
3732
|
const key = `${signalId}:${bucketName}`;
|
|
3733
3733
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3734
3734
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3745,7 +3745,7 @@ class PersistMemoryUtils {
|
|
|
3745
3745
|
* @returns Promise resolving to true if entry exists
|
|
3746
3746
|
*/
|
|
3747
3747
|
this.hasMemoryData = async (signalId, bucketName, memoryId) => {
|
|
3748
|
-
LOGGER_SERVICE$
|
|
3748
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, { signalId, bucketName, memoryId });
|
|
3749
3749
|
const key = `${signalId}:${bucketName}`;
|
|
3750
3750
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3751
3751
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3764,7 +3764,7 @@ class PersistMemoryUtils {
|
|
|
3764
3764
|
* @returns Promise that resolves when write is complete
|
|
3765
3765
|
*/
|
|
3766
3766
|
this.writeMemoryData = async (data, signalId, bucketName, memoryId, when) => {
|
|
3767
|
-
LOGGER_SERVICE$
|
|
3767
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
|
|
3768
3768
|
const key = `${signalId}:${bucketName}`;
|
|
3769
3769
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3770
3770
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3781,7 +3781,7 @@ class PersistMemoryUtils {
|
|
|
3781
3781
|
* @returns Promise that resolves when removal is complete
|
|
3782
3782
|
*/
|
|
3783
3783
|
this.removeMemoryData = async (signalId, bucketName, memoryId) => {
|
|
3784
|
-
LOGGER_SERVICE$
|
|
3784
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, { signalId, bucketName, memoryId });
|
|
3785
3785
|
const key = `${signalId}:${bucketName}`;
|
|
3786
3786
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3787
3787
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3793,7 +3793,7 @@ class PersistMemoryUtils {
|
|
|
3793
3793
|
* Call when process.cwd() changes between strategy iterations.
|
|
3794
3794
|
*/
|
|
3795
3795
|
this.clear = () => {
|
|
3796
|
-
LOGGER_SERVICE$
|
|
3796
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_CLEAR);
|
|
3797
3797
|
this.getMemoryStorage.clear();
|
|
3798
3798
|
};
|
|
3799
3799
|
/**
|
|
@@ -3804,7 +3804,7 @@ class PersistMemoryUtils {
|
|
|
3804
3804
|
* @param bucketName - Bucket name
|
|
3805
3805
|
*/
|
|
3806
3806
|
this.dispose = (signalId, bucketName) => {
|
|
3807
|
-
LOGGER_SERVICE$
|
|
3807
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_DISPOSE);
|
|
3808
3808
|
const key = `${signalId}:${bucketName}`;
|
|
3809
3809
|
this.getMemoryStorage.clear(key);
|
|
3810
3810
|
};
|
|
@@ -3816,7 +3816,7 @@ class PersistMemoryUtils {
|
|
|
3816
3816
|
* @param Ctor - Custom IPersistMemoryInstance constructor
|
|
3817
3817
|
*/
|
|
3818
3818
|
usePersistMemoryAdapter(Ctor) {
|
|
3819
|
-
LOGGER_SERVICE$
|
|
3819
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER);
|
|
3820
3820
|
this.PersistMemoryInstanceCtor = Ctor;
|
|
3821
3821
|
this.getMemoryStorage.clear();
|
|
3822
3822
|
}
|
|
@@ -3830,7 +3830,7 @@ class PersistMemoryUtils {
|
|
|
3830
3830
|
* @returns AsyncGenerator yielding `{ memoryId, data }` tuples
|
|
3831
3831
|
*/
|
|
3832
3832
|
async *listMemoryData(signalId, bucketName) {
|
|
3833
|
-
LOGGER_SERVICE$
|
|
3833
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, { signalId, bucketName });
|
|
3834
3834
|
const key = `${signalId}:${bucketName}`;
|
|
3835
3835
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3836
3836
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3841,14 +3841,14 @@ class PersistMemoryUtils {
|
|
|
3841
3841
|
* Switches to the default file-based PersistMemoryInstance.
|
|
3842
3842
|
*/
|
|
3843
3843
|
useJson() {
|
|
3844
|
-
LOGGER_SERVICE$
|
|
3844
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
|
|
3845
3845
|
this.usePersistMemoryAdapter(PersistMemoryInstance);
|
|
3846
3846
|
}
|
|
3847
3847
|
/**
|
|
3848
3848
|
* Switches to PersistMemoryDummyInstance (all operations are no-ops).
|
|
3849
3849
|
*/
|
|
3850
3850
|
useDummy() {
|
|
3851
|
-
LOGGER_SERVICE$
|
|
3851
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3852
3852
|
this.usePersistMemoryAdapter(PersistMemoryDummyInstance);
|
|
3853
3853
|
}
|
|
3854
3854
|
}
|
|
@@ -3997,7 +3997,7 @@ class PersistRecentUtils {
|
|
|
3997
3997
|
* @returns Promise resolving to recent signal or null if none persisted
|
|
3998
3998
|
*/
|
|
3999
3999
|
this.readRecentData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
4000
|
-
LOGGER_SERVICE$
|
|
4000
|
+
LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_READ_DATA);
|
|
4001
4001
|
const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
|
|
4002
4002
|
const isInitial = !this.getStorage.has(key);
|
|
4003
4003
|
const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
@@ -4018,7 +4018,7 @@ class PersistRecentUtils {
|
|
|
4018
4018
|
* @returns Promise that resolves when write is complete
|
|
4019
4019
|
*/
|
|
4020
4020
|
this.writeRecentData = async (signalRow, symbol, strategyName, exchangeName, frameName, backtest, when) => {
|
|
4021
|
-
LOGGER_SERVICE$
|
|
4021
|
+
LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
|
|
4022
4022
|
const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
|
|
4023
4023
|
const isInitial = !this.getStorage.has(key);
|
|
4024
4024
|
const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
@@ -4051,7 +4051,7 @@ class PersistRecentUtils {
|
|
|
4051
4051
|
* @param Ctor - Custom IPersistRecentInstance constructor
|
|
4052
4052
|
*/
|
|
4053
4053
|
usePersistRecentAdapter(Ctor) {
|
|
4054
|
-
LOGGER_SERVICE$
|
|
4054
|
+
LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_USE_PERSIST_RECENT_ADAPTER);
|
|
4055
4055
|
this.PersistRecentInstanceCtor = Ctor;
|
|
4056
4056
|
this.getStorage.clear();
|
|
4057
4057
|
}
|
|
@@ -4060,21 +4060,21 @@ class PersistRecentUtils {
|
|
|
4060
4060
|
* Call when process.cwd() changes between strategy iterations.
|
|
4061
4061
|
*/
|
|
4062
4062
|
clear() {
|
|
4063
|
-
LOGGER_SERVICE$
|
|
4063
|
+
LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_CLEAR);
|
|
4064
4064
|
this.getStorage.clear();
|
|
4065
4065
|
}
|
|
4066
4066
|
/**
|
|
4067
4067
|
* Switches to the default file-based PersistRecentInstance.
|
|
4068
4068
|
*/
|
|
4069
4069
|
useJson() {
|
|
4070
|
-
LOGGER_SERVICE$
|
|
4070
|
+
LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_JSON);
|
|
4071
4071
|
this.usePersistRecentAdapter(PersistRecentInstance);
|
|
4072
4072
|
}
|
|
4073
4073
|
/**
|
|
4074
4074
|
* Switches to PersistRecentDummyInstance (all operations are no-ops).
|
|
4075
4075
|
*/
|
|
4076
4076
|
useDummy() {
|
|
4077
|
-
LOGGER_SERVICE$
|
|
4077
|
+
LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_DUMMY);
|
|
4078
4078
|
this.usePersistRecentAdapter(PersistRecentDummyInstance);
|
|
4079
4079
|
}
|
|
4080
4080
|
}
|
|
@@ -4209,7 +4209,7 @@ class PersistStateUtils {
|
|
|
4209
4209
|
* @returns Promise that resolves when initialization is complete
|
|
4210
4210
|
*/
|
|
4211
4211
|
this.waitForInit = async (signalId, bucketName, initial) => {
|
|
4212
|
-
LOGGER_SERVICE$
|
|
4212
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, { signalId, bucketName, initial });
|
|
4213
4213
|
const key = `${signalId}:${bucketName}`;
|
|
4214
4214
|
const isInitial = initial && !this.getStateStorage.has(key);
|
|
4215
4215
|
const instance = this.getStateStorage(signalId, bucketName);
|
|
@@ -4224,7 +4224,7 @@ class PersistStateUtils {
|
|
|
4224
4224
|
* @returns Promise resolving to state data or null if none persisted
|
|
4225
4225
|
*/
|
|
4226
4226
|
this.readStateData = async (signalId, bucketName) => {
|
|
4227
|
-
LOGGER_SERVICE$
|
|
4227
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName });
|
|
4228
4228
|
const key = `${signalId}:${bucketName}`;
|
|
4229
4229
|
const isInitial = !this.getStateStorage.has(key);
|
|
4230
4230
|
const instance = this.getStateStorage(signalId, bucketName);
|
|
@@ -4242,7 +4242,7 @@ class PersistStateUtils {
|
|
|
4242
4242
|
* @returns Promise that resolves when write is complete
|
|
4243
4243
|
*/
|
|
4244
4244
|
this.writeStateData = async (data, signalId, bucketName, when) => {
|
|
4245
|
-
LOGGER_SERVICE$
|
|
4245
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
|
|
4246
4246
|
const key = `${signalId}:${bucketName}`;
|
|
4247
4247
|
const isInitial = !this.getStateStorage.has(key);
|
|
4248
4248
|
const instance = this.getStateStorage(signalId, bucketName);
|
|
@@ -4253,14 +4253,14 @@ class PersistStateUtils {
|
|
|
4253
4253
|
* Switches to PersistStateDummyInstance (all operations are no-ops).
|
|
4254
4254
|
*/
|
|
4255
4255
|
this.useDummy = () => {
|
|
4256
|
-
LOGGER_SERVICE$
|
|
4256
|
+
LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
4257
4257
|
this.usePersistStateAdapter(PersistStateDummyInstance);
|
|
4258
4258
|
};
|
|
4259
4259
|
/**
|
|
4260
4260
|
* Switches to the default file-based PersistStateInstance.
|
|
4261
4261
|
*/
|
|
4262
4262
|
this.useJson = () => {
|
|
4263
|
-
LOGGER_SERVICE$
|
|
4263
|
+
LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_JSON);
|
|
4264
4264
|
this.usePersistStateAdapter(PersistStateInstance);
|
|
4265
4265
|
};
|
|
4266
4266
|
/**
|
|
@@ -4268,7 +4268,7 @@ class PersistStateUtils {
|
|
|
4268
4268
|
* Call when process.cwd() changes between strategy iterations.
|
|
4269
4269
|
*/
|
|
4270
4270
|
this.clear = () => {
|
|
4271
|
-
LOGGER_SERVICE$
|
|
4271
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_CLEAR);
|
|
4272
4272
|
this.getStateStorage.clear();
|
|
4273
4273
|
};
|
|
4274
4274
|
/**
|
|
@@ -4279,7 +4279,7 @@ class PersistStateUtils {
|
|
|
4279
4279
|
* @param bucketName - Bucket name
|
|
4280
4280
|
*/
|
|
4281
4281
|
this.dispose = (signalId, bucketName) => {
|
|
4282
|
-
LOGGER_SERVICE$
|
|
4282
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_DISPOSE);
|
|
4283
4283
|
const key = `${signalId}:${bucketName}`;
|
|
4284
4284
|
this.getStateStorage.clear(key);
|
|
4285
4285
|
};
|
|
@@ -4291,7 +4291,7 @@ class PersistStateUtils {
|
|
|
4291
4291
|
* @param Ctor - Custom IPersistStateInstance constructor
|
|
4292
4292
|
*/
|
|
4293
4293
|
usePersistStateAdapter(Ctor) {
|
|
4294
|
-
LOGGER_SERVICE$
|
|
4294
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_USE_PERSIST_STATE_ADAPTER);
|
|
4295
4295
|
this.PersistStateInstanceCtor = Ctor;
|
|
4296
4296
|
this.getStateStorage.clear();
|
|
4297
4297
|
}
|
|
@@ -4431,7 +4431,7 @@ class PersistSessionUtils {
|
|
|
4431
4431
|
* @returns Promise that resolves when initialization is complete
|
|
4432
4432
|
*/
|
|
4433
4433
|
this.waitForInit = async (strategyName, exchangeName, frameName, initial) => {
|
|
4434
|
-
LOGGER_SERVICE$
|
|
4434
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, { strategyName, exchangeName, frameName, initial });
|
|
4435
4435
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4436
4436
|
const isInitial = initial && !this.getSessionStorage.has(key);
|
|
4437
4437
|
const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
|
|
@@ -4447,7 +4447,7 @@ class PersistSessionUtils {
|
|
|
4447
4447
|
* @returns Promise resolving to session data or null if none persisted
|
|
4448
4448
|
*/
|
|
4449
4449
|
this.readSessionData = async (strategyName, exchangeName, frameName) => {
|
|
4450
|
-
LOGGER_SERVICE$
|
|
4450
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, { strategyName, exchangeName, frameName });
|
|
4451
4451
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4452
4452
|
const isInitial = !this.getSessionStorage.has(key);
|
|
4453
4453
|
const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
|
|
@@ -4466,7 +4466,7 @@ class PersistSessionUtils {
|
|
|
4466
4466
|
* @returns Promise that resolves when write is complete
|
|
4467
4467
|
*/
|
|
4468
4468
|
this.writeSessionData = async (data, strategyName, exchangeName, frameName, when) => {
|
|
4469
|
-
LOGGER_SERVICE$
|
|
4469
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
|
|
4470
4470
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4471
4471
|
const isInitial = !this.getSessionStorage.has(key);
|
|
4472
4472
|
const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
|
|
@@ -4477,14 +4477,14 @@ class PersistSessionUtils {
|
|
|
4477
4477
|
* Switches to PersistSessionDummyInstance (all operations are no-ops).
|
|
4478
4478
|
*/
|
|
4479
4479
|
this.useDummy = () => {
|
|
4480
|
-
LOGGER_SERVICE$
|
|
4480
|
+
LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_DUMMY);
|
|
4481
4481
|
this.usePersistSessionAdapter(PersistSessionDummyInstance);
|
|
4482
4482
|
};
|
|
4483
4483
|
/**
|
|
4484
4484
|
* Switches to the default file-based PersistSessionInstance.
|
|
4485
4485
|
*/
|
|
4486
4486
|
this.useJson = () => {
|
|
4487
|
-
LOGGER_SERVICE$
|
|
4487
|
+
LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_JSON);
|
|
4488
4488
|
this.usePersistSessionAdapter(PersistSessionInstance);
|
|
4489
4489
|
};
|
|
4490
4490
|
/**
|
|
@@ -4492,7 +4492,7 @@ class PersistSessionUtils {
|
|
|
4492
4492
|
* Call when process.cwd() changes between strategy iterations.
|
|
4493
4493
|
*/
|
|
4494
4494
|
this.clear = () => {
|
|
4495
|
-
LOGGER_SERVICE$
|
|
4495
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_CLEAR);
|
|
4496
4496
|
this.getSessionStorage.clear();
|
|
4497
4497
|
};
|
|
4498
4498
|
/**
|
|
@@ -4504,7 +4504,7 @@ class PersistSessionUtils {
|
|
|
4504
4504
|
* @param frameName - Frame identifier
|
|
4505
4505
|
*/
|
|
4506
4506
|
this.dispose = (strategyName, exchangeName, frameName) => {
|
|
4507
|
-
LOGGER_SERVICE$
|
|
4507
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_DISPOSE);
|
|
4508
4508
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4509
4509
|
this.getSessionStorage.clear(key);
|
|
4510
4510
|
};
|
|
@@ -4516,7 +4516,7 @@ class PersistSessionUtils {
|
|
|
4516
4516
|
* @param Ctor - Custom IPersistSessionInstance constructor
|
|
4517
4517
|
*/
|
|
4518
4518
|
usePersistSessionAdapter(Ctor) {
|
|
4519
|
-
LOGGER_SERVICE$
|
|
4519
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_USE_PERSIST_SESSION_ADAPTER);
|
|
4520
4520
|
this.PersistSessionInstanceCtor = Ctor;
|
|
4521
4521
|
this.getSessionStorage.clear();
|
|
4522
4522
|
}
|
|
@@ -4638,7 +4638,7 @@ const METHOD_NAME_ADD_ACTIVITY = "LookupUtils.addActivity";
|
|
|
4638
4638
|
const METHOD_NAME_REMOVE_ACTIVITY = "LookupUtils.removeActivity";
|
|
4639
4639
|
const METHOD_NAME_LIST_ACTIVITY = "LookupUtils.listActivity";
|
|
4640
4640
|
/** Logger service injected as DI singleton */
|
|
4641
|
-
const LOGGER_SERVICE$
|
|
4641
|
+
const LOGGER_SERVICE$8 = new LoggerService();
|
|
4642
4642
|
/**
|
|
4643
4643
|
* Builds the composite {@link Key} used to register an activity in `_lookupMap`.
|
|
4644
4644
|
*
|
|
@@ -4692,7 +4692,7 @@ class LookupUtils {
|
|
|
4692
4692
|
* @param activity - Activity descriptor identifying the running workload.
|
|
4693
4693
|
*/
|
|
4694
4694
|
this.addActivity = (activity) => {
|
|
4695
|
-
LOGGER_SERVICE$
|
|
4695
|
+
LOGGER_SERVICE$8.info(METHOD_NAME_ADD_ACTIVITY, {
|
|
4696
4696
|
activity,
|
|
4697
4697
|
});
|
|
4698
4698
|
const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
|
|
@@ -4706,7 +4706,7 @@ class LookupUtils {
|
|
|
4706
4706
|
* @param activity - Activity descriptor matching the one passed to {@link addActivity}.
|
|
4707
4707
|
*/
|
|
4708
4708
|
this.removeActivity = (activity) => {
|
|
4709
|
-
LOGGER_SERVICE$
|
|
4709
|
+
LOGGER_SERVICE$8.info(METHOD_NAME_REMOVE_ACTIVITY, {
|
|
4710
4710
|
activity,
|
|
4711
4711
|
});
|
|
4712
4712
|
const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
|
|
@@ -4718,7 +4718,7 @@ class LookupUtils {
|
|
|
4718
4718
|
* @returns Array of all activities present in the lookup map at call time.
|
|
4719
4719
|
*/
|
|
4720
4720
|
this.listActivity = () => {
|
|
4721
|
-
LOGGER_SERVICE$
|
|
4721
|
+
LOGGER_SERVICE$8.info(METHOD_NAME_LIST_ACTIVITY);
|
|
4722
4722
|
return Array.from(this._lookupMap.values());
|
|
4723
4723
|
};
|
|
4724
4724
|
}
|
|
@@ -4747,7 +4747,7 @@ const METHOD_NAME_SPIN_LOCK = "CandleUtils.spinLock";
|
|
|
4747
4747
|
*/
|
|
4748
4748
|
const ROTATE_DELAY = 50;
|
|
4749
4749
|
/** Logger service injected as DI singleton */
|
|
4750
|
-
const LOGGER_SERVICE$
|
|
4750
|
+
const LOGGER_SERVICE$7 = new LoggerService();
|
|
4751
4751
|
/**
|
|
4752
4752
|
* Process-wide coordinator for candle-fetch serialization and cooperative
|
|
4753
4753
|
* yielding between parallel backtests.
|
|
@@ -4796,7 +4796,7 @@ class CandleUtils {
|
|
|
4796
4796
|
* @param source - Caller identifier for logging.
|
|
4797
4797
|
*/
|
|
4798
4798
|
this.acquireLock = async (source) => {
|
|
4799
|
-
LOGGER_SERVICE$
|
|
4799
|
+
LOGGER_SERVICE$7.info(METHOD_NAME_ACQUIRE_LOCK, {
|
|
4800
4800
|
source,
|
|
4801
4801
|
});
|
|
4802
4802
|
if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
|
|
@@ -4812,7 +4812,7 @@ class CandleUtils {
|
|
|
4812
4812
|
* @param source - Caller identifier for logging.
|
|
4813
4813
|
*/
|
|
4814
4814
|
this.releaseLock = async (source) => {
|
|
4815
|
-
LOGGER_SERVICE$
|
|
4815
|
+
LOGGER_SERVICE$7.info(METHOD_NAME_RELEASE_LOCK, {
|
|
4816
4816
|
source,
|
|
4817
4817
|
});
|
|
4818
4818
|
if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
|
|
@@ -4837,7 +4837,7 @@ class CandleUtils {
|
|
|
4837
4837
|
* @param source - Caller identifier for logging.
|
|
4838
4838
|
*/
|
|
4839
4839
|
this.spinLock = async (source) => {
|
|
4840
|
-
LOGGER_SERVICE$
|
|
4840
|
+
LOGGER_SERVICE$7.info(METHOD_NAME_SPIN_LOCK, {
|
|
4841
4841
|
source,
|
|
4842
4842
|
});
|
|
4843
4843
|
if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
|
|
@@ -6347,7 +6347,7 @@ const validateCommonSignal = (signal) => {
|
|
|
6347
6347
|
}
|
|
6348
6348
|
// Кидаем ошибку если есть проблемы
|
|
6349
6349
|
if (errors.length > 0) {
|
|
6350
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6350
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6351
6351
|
}
|
|
6352
6352
|
};
|
|
6353
6353
|
|
|
@@ -6400,7 +6400,7 @@ const validatePendingSignal = (signal, currentPrice) => {
|
|
|
6400
6400
|
}
|
|
6401
6401
|
}
|
|
6402
6402
|
if (errors.length > 0) {
|
|
6403
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6403
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6404
6404
|
}
|
|
6405
6405
|
validateCommonSignal(signal);
|
|
6406
6406
|
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
@@ -6448,7 +6448,7 @@ const validatePendingSignal = (signal, currentPrice) => {
|
|
|
6448
6448
|
}
|
|
6449
6449
|
}
|
|
6450
6450
|
if (errors.length > 0) {
|
|
6451
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6451
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6452
6452
|
}
|
|
6453
6453
|
};
|
|
6454
6454
|
|
|
@@ -6501,7 +6501,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
|
|
|
6501
6501
|
}
|
|
6502
6502
|
}
|
|
6503
6503
|
if (errors.length > 0) {
|
|
6504
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6504
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6505
6505
|
}
|
|
6506
6506
|
validateCommonSignal(signal);
|
|
6507
6507
|
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
@@ -6547,7 +6547,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
|
|
|
6547
6547
|
// pendingAt === 0 is allowed for scheduled signals (set to SCHEDULED_SIGNAL_PENDING_MOCK until activation)
|
|
6548
6548
|
}
|
|
6549
6549
|
if (errors.length > 0) {
|
|
6550
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6550
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6551
6551
|
}
|
|
6552
6552
|
};
|
|
6553
6553
|
|
|
@@ -6994,6 +6994,13 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
6994
6994
|
if (!signal) {
|
|
6995
6995
|
return null;
|
|
6996
6996
|
}
|
|
6997
|
+
if (signal?.symbol && signal?.symbol !== self.params.execution.context.symbol) {
|
|
6998
|
+
throw new Error(`Symbol mismatch: expected ${self.params.execution.context.symbol}, got ${signal.symbol}`);
|
|
6999
|
+
}
|
|
7000
|
+
// Whipsaw protection: skip signal if its id matches the last accepted pending id
|
|
7001
|
+
if (signal.id && signal.id === self._lastPendingId) {
|
|
7002
|
+
return null;
|
|
7003
|
+
}
|
|
6997
7004
|
if (self._isStopped) {
|
|
6998
7005
|
return null;
|
|
6999
7006
|
}
|
|
@@ -7037,6 +7044,9 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
7037
7044
|
}
|
|
7038
7045
|
// Валидируем сигнал перед возвратом
|
|
7039
7046
|
validatePendingSignal(signalRow, currentPrice);
|
|
7047
|
+
if (signal.id) {
|
|
7048
|
+
self._lastPendingId = signal.id;
|
|
7049
|
+
}
|
|
7040
7050
|
return signalRow;
|
|
7041
7051
|
}
|
|
7042
7052
|
// ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
|
|
@@ -7063,6 +7073,9 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
7063
7073
|
};
|
|
7064
7074
|
// Валидируем сигнал перед возвратом
|
|
7065
7075
|
validateScheduledSignal(scheduledSignalRow, currentPrice);
|
|
7076
|
+
if (signal.id) {
|
|
7077
|
+
self._lastPendingId = signal.id;
|
|
7078
|
+
}
|
|
7066
7079
|
return scheduledSignalRow;
|
|
7067
7080
|
}
|
|
7068
7081
|
const signalRow = {
|
|
@@ -7090,6 +7103,9 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
7090
7103
|
}
|
|
7091
7104
|
// Валидируем сигнал перед возвратом
|
|
7092
7105
|
validatePendingSignal(signalRow, currentPrice);
|
|
7106
|
+
if (signal.id) {
|
|
7107
|
+
self._lastPendingId = signal.id;
|
|
7108
|
+
}
|
|
7093
7109
|
return signalRow;
|
|
7094
7110
|
}, {
|
|
7095
7111
|
defaultValue: null,
|
|
@@ -7119,6 +7135,13 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
|
|
|
7119
7135
|
if (self.params.execution.context.backtest) {
|
|
7120
7136
|
return;
|
|
7121
7137
|
}
|
|
7138
|
+
// Restore last pending signal id for whipsaw protection in GET_SIGNAL_FN
|
|
7139
|
+
{
|
|
7140
|
+
const recentSignal = await PersistRecentAdapter.readRecentData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, false);
|
|
7141
|
+
if (recentSignal?.id) {
|
|
7142
|
+
self._lastPendingId = recentSignal.id;
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7122
7145
|
// Restore pending signal
|
|
7123
7146
|
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
7124
7147
|
if (pendingSignal) {
|
|
@@ -9259,6 +9282,7 @@ class ClientStrategy {
|
|
|
9259
9282
|
this._isStopped = false;
|
|
9260
9283
|
this._pendingSignal = null;
|
|
9261
9284
|
this._lastSignalTimestamp = null;
|
|
9285
|
+
this._lastPendingId = null;
|
|
9262
9286
|
this._scheduledSignal = null;
|
|
9263
9287
|
this._cancelledSignal = null;
|
|
9264
9288
|
this._closedSignal = null;
|
|
@@ -12048,7 +12072,7 @@ const RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE = "MergeRisk.checkSignalAndReser
|
|
|
12048
12072
|
const RISK_METHOD_NAME_ADD_SIGNAL = "MergeRisk.addSignal";
|
|
12049
12073
|
const RISK_METHOD_NAME_REMOVE_SIGNAL = "MergeRisk.removeSignal";
|
|
12050
12074
|
/** Logger service injected as DI singleton */
|
|
12051
|
-
const LOGGER_SERVICE$
|
|
12075
|
+
const LOGGER_SERVICE$6 = new LoggerService();
|
|
12052
12076
|
/**
|
|
12053
12077
|
* Composite risk management class that combines multiple risk profiles.
|
|
12054
12078
|
*
|
|
@@ -12104,7 +12128,7 @@ class MergeRisk {
|
|
|
12104
12128
|
* @returns Promise resolving to true if all risks approve, false if any risk rejects
|
|
12105
12129
|
*/
|
|
12106
12130
|
async checkSignal(params, options = {}) {
|
|
12107
|
-
LOGGER_SERVICE$
|
|
12131
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL, {
|
|
12108
12132
|
params,
|
|
12109
12133
|
});
|
|
12110
12134
|
for (const [riskName, risk] of Object.entries(this._riskMap)) {
|
|
@@ -12134,7 +12158,7 @@ class MergeRisk {
|
|
|
12134
12158
|
* @returns Promise resolving to true if all risks approve (and reserved), false if any risk rejects
|
|
12135
12159
|
*/
|
|
12136
12160
|
async checkSignalAndReserve(params) {
|
|
12137
|
-
LOGGER_SERVICE$
|
|
12161
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE, {
|
|
12138
12162
|
params,
|
|
12139
12163
|
});
|
|
12140
12164
|
for (const [riskName, risk] of Object.entries(this._riskMap)) {
|
|
@@ -12158,7 +12182,7 @@ class MergeRisk {
|
|
|
12158
12182
|
* @returns Promise that resolves when all risks have registered the signal
|
|
12159
12183
|
*/
|
|
12160
12184
|
async addSignal(symbol, context, positionData) {
|
|
12161
|
-
LOGGER_SERVICE$
|
|
12185
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_ADD_SIGNAL, {
|
|
12162
12186
|
symbol,
|
|
12163
12187
|
context,
|
|
12164
12188
|
});
|
|
@@ -12175,7 +12199,7 @@ class MergeRisk {
|
|
|
12175
12199
|
* @returns Promise that resolves when all risks have removed the signal
|
|
12176
12200
|
*/
|
|
12177
12201
|
async removeSignal(symbol, context) {
|
|
12178
|
-
LOGGER_SERVICE$
|
|
12202
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_REMOVE_SIGNAL, {
|
|
12179
12203
|
symbol,
|
|
12180
12204
|
context,
|
|
12181
12205
|
});
|
|
@@ -14968,7 +14992,7 @@ class RiskConnectionService {
|
|
|
14968
14992
|
}
|
|
14969
14993
|
|
|
14970
14994
|
/** Logger service injected as DI singleton */
|
|
14971
|
-
const LOGGER_SERVICE$
|
|
14995
|
+
const LOGGER_SERVICE$5 = new LoggerService();
|
|
14972
14996
|
/**
|
|
14973
14997
|
* Wrapper to call init method with error capture.
|
|
14974
14998
|
*/
|
|
@@ -14983,7 +15007,7 @@ const CALL_INIT_FN = trycatch(async (self) => {
|
|
|
14983
15007
|
error: errorData(error),
|
|
14984
15008
|
message: getErrorMessage(error),
|
|
14985
15009
|
};
|
|
14986
|
-
LOGGER_SERVICE$
|
|
15010
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
14987
15011
|
console.warn(message, payload);
|
|
14988
15012
|
errorEmitter.next(error);
|
|
14989
15013
|
},
|
|
@@ -15003,7 +15027,7 @@ const CALL_SIGNAL_FN = trycatch(async (event, self) => {
|
|
|
15003
15027
|
error: errorData(error),
|
|
15004
15028
|
message: getErrorMessage(error),
|
|
15005
15029
|
};
|
|
15006
|
-
LOGGER_SERVICE$
|
|
15030
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15007
15031
|
console.warn(message, payload);
|
|
15008
15032
|
errorEmitter.next(error);
|
|
15009
15033
|
},
|
|
@@ -15023,7 +15047,7 @@ const CALL_SIGNAL_LIVE_FN = trycatch(async (event, self) => {
|
|
|
15023
15047
|
error: errorData(error),
|
|
15024
15048
|
message: getErrorMessage(error),
|
|
15025
15049
|
};
|
|
15026
|
-
LOGGER_SERVICE$
|
|
15050
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15027
15051
|
console.warn(message, payload);
|
|
15028
15052
|
errorEmitter.next(error);
|
|
15029
15053
|
},
|
|
@@ -15043,7 +15067,7 @@ const CALL_SIGNAL_BACKTEST_FN = trycatch(async (event, self) => {
|
|
|
15043
15067
|
error: errorData(error),
|
|
15044
15068
|
message: getErrorMessage(error),
|
|
15045
15069
|
};
|
|
15046
|
-
LOGGER_SERVICE$
|
|
15070
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15047
15071
|
console.warn(message, payload);
|
|
15048
15072
|
errorEmitter.next(error);
|
|
15049
15073
|
},
|
|
@@ -15071,7 +15095,7 @@ const CALL_BREAKEVEN_AVAILABLE_FN = trycatch(async (event, self) => {
|
|
|
15071
15095
|
error: errorData(error),
|
|
15072
15096
|
message: getErrorMessage(error),
|
|
15073
15097
|
};
|
|
15074
|
-
LOGGER_SERVICE$
|
|
15098
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15075
15099
|
console.warn(message, payload);
|
|
15076
15100
|
errorEmitter.next(error);
|
|
15077
15101
|
},
|
|
@@ -15099,7 +15123,7 @@ const CALL_PARTIAL_PROFIT_AVAILABLE_FN = trycatch(async (event, self) => {
|
|
|
15099
15123
|
error: errorData(error),
|
|
15100
15124
|
message: getErrorMessage(error),
|
|
15101
15125
|
};
|
|
15102
|
-
LOGGER_SERVICE$
|
|
15126
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15103
15127
|
console.warn(message, payload);
|
|
15104
15128
|
errorEmitter.next(error);
|
|
15105
15129
|
},
|
|
@@ -15127,7 +15151,7 @@ const CALL_PARTIAL_LOSS_AVAILABLE_FN = trycatch(async (event, self) => {
|
|
|
15127
15151
|
error: errorData(error),
|
|
15128
15152
|
message: getErrorMessage(error),
|
|
15129
15153
|
};
|
|
15130
|
-
LOGGER_SERVICE$
|
|
15154
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15131
15155
|
console.warn(message, payload);
|
|
15132
15156
|
errorEmitter.next(error);
|
|
15133
15157
|
},
|
|
@@ -15155,7 +15179,7 @@ const CALL_PING_SCHEDULED_FN = trycatch(async (event, self) => {
|
|
|
15155
15179
|
error: errorData(error),
|
|
15156
15180
|
message: getErrorMessage(error),
|
|
15157
15181
|
};
|
|
15158
|
-
LOGGER_SERVICE$
|
|
15182
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15159
15183
|
console.warn(message, payload);
|
|
15160
15184
|
errorEmitter.next(error);
|
|
15161
15185
|
},
|
|
@@ -15183,7 +15207,7 @@ const CALL_PING_IDLE_FN = trycatch(async (event, self) => {
|
|
|
15183
15207
|
error: errorData(error),
|
|
15184
15208
|
message: getErrorMessage(error),
|
|
15185
15209
|
};
|
|
15186
|
-
LOGGER_SERVICE$
|
|
15210
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15187
15211
|
console.warn(message, payload);
|
|
15188
15212
|
errorEmitter.next(error);
|
|
15189
15213
|
},
|
|
@@ -15211,7 +15235,7 @@ const CALL_PING_ACTIVE_FN = trycatch(async (event, self) => {
|
|
|
15211
15235
|
error: errorData(error),
|
|
15212
15236
|
message: getErrorMessage(error),
|
|
15213
15237
|
};
|
|
15214
|
-
LOGGER_SERVICE$
|
|
15238
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15215
15239
|
console.warn(message, payload);
|
|
15216
15240
|
errorEmitter.next(error);
|
|
15217
15241
|
},
|
|
@@ -15231,7 +15255,7 @@ const CALL_RISK_REJECTION_FN = trycatch(async (event, self) => {
|
|
|
15231
15255
|
error: errorData(error),
|
|
15232
15256
|
message: getErrorMessage(error),
|
|
15233
15257
|
};
|
|
15234
|
-
LOGGER_SERVICE$
|
|
15258
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15235
15259
|
console.warn(message, payload);
|
|
15236
15260
|
errorEmitter.next(error);
|
|
15237
15261
|
},
|
|
@@ -15251,7 +15275,7 @@ const CALL_DISPOSE_FN = trycatch(async (self) => {
|
|
|
15251
15275
|
error: errorData(error),
|
|
15252
15276
|
message: getErrorMessage(error),
|
|
15253
15277
|
};
|
|
15254
|
-
LOGGER_SERVICE$
|
|
15278
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15255
15279
|
console.warn(message, payload);
|
|
15256
15280
|
errorEmitter.next(error);
|
|
15257
15281
|
},
|
|
@@ -23115,7 +23139,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY$1 = "ReportUtils.useDummy";
|
|
|
23115
23139
|
const REPORT_UTILS_METHOD_NAME_USE_JSONL$1 = "ReportUtils.useJsonl";
|
|
23116
23140
|
const REPORT_UTILS_METHOD_NAME_CLEAR$1 = "ReportUtils.clear";
|
|
23117
23141
|
/** Logger service injected as DI singleton */
|
|
23118
|
-
const LOGGER_SERVICE$
|
|
23142
|
+
const LOGGER_SERVICE$4 = new LoggerService();
|
|
23119
23143
|
/** Symbol key for the singleshot waitForInit function on MarkdownFileBase instances. */
|
|
23120
23144
|
const WAIT_FOR_INIT_SYMBOL$1 = Symbol("wait-for-init");
|
|
23121
23145
|
/** Symbol key for the timeout-protected write function on MarkdownFileBase instances. */
|
|
@@ -23196,7 +23220,7 @@ class MarkdownFileBase {
|
|
|
23196
23220
|
* @throws Error if stream not initialized or write timeout exceeded
|
|
23197
23221
|
*/
|
|
23198
23222
|
async dump(data, options) {
|
|
23199
|
-
LOGGER_SERVICE$
|
|
23223
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FILE_DUMP, {
|
|
23200
23224
|
markdownName: this.markdownName,
|
|
23201
23225
|
options,
|
|
23202
23226
|
});
|
|
@@ -23276,7 +23300,7 @@ class MarkdownFolderBase {
|
|
|
23276
23300
|
* @throws Error if directory creation or file write fails
|
|
23277
23301
|
*/
|
|
23278
23302
|
async dump(content, options) {
|
|
23279
|
-
LOGGER_SERVICE$
|
|
23303
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FOLDER_DUMP, {
|
|
23280
23304
|
markdownName: this.markdownName,
|
|
23281
23305
|
options,
|
|
23282
23306
|
});
|
|
@@ -23341,7 +23365,7 @@ class MarkdownWriterAdapter {
|
|
|
23341
23365
|
* @param Ctor - Constructor for markdown storage adapter
|
|
23342
23366
|
*/
|
|
23343
23367
|
useMarkdownAdapter(Ctor) {
|
|
23344
|
-
LOGGER_SERVICE$
|
|
23368
|
+
LOGGER_SERVICE$4.info(MARKDOWN_METHOD_NAME_USE_ADAPTER$1);
|
|
23345
23369
|
this.MarkdownFactory = Ctor;
|
|
23346
23370
|
}
|
|
23347
23371
|
/**
|
|
@@ -23355,7 +23379,7 @@ class MarkdownWriterAdapter {
|
|
|
23355
23379
|
* @throws Error if write fails or storage initialization fails
|
|
23356
23380
|
*/
|
|
23357
23381
|
async writeData(markdownName, content, options) {
|
|
23358
|
-
LOGGER_SERVICE$
|
|
23382
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_WRITE_DATA, {
|
|
23359
23383
|
markdownName,
|
|
23360
23384
|
options,
|
|
23361
23385
|
});
|
|
@@ -23369,7 +23393,7 @@ class MarkdownWriterAdapter {
|
|
|
23369
23393
|
* Each report is written as a separate .md file.
|
|
23370
23394
|
*/
|
|
23371
23395
|
useMd() {
|
|
23372
|
-
LOGGER_SERVICE$
|
|
23396
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_MD$1);
|
|
23373
23397
|
this.useMarkdownAdapter(MarkdownFolderBase);
|
|
23374
23398
|
}
|
|
23375
23399
|
/**
|
|
@@ -23377,7 +23401,7 @@ class MarkdownWriterAdapter {
|
|
|
23377
23401
|
* All reports are appended to a single .jsonl file per markdown type.
|
|
23378
23402
|
*/
|
|
23379
23403
|
useJsonl() {
|
|
23380
|
-
LOGGER_SERVICE$
|
|
23404
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_JSONL$1);
|
|
23381
23405
|
this.useMarkdownAdapter(MarkdownFileBase);
|
|
23382
23406
|
}
|
|
23383
23407
|
/**
|
|
@@ -23386,7 +23410,7 @@ class MarkdownWriterAdapter {
|
|
|
23386
23410
|
* so new storage instances are created with the updated base path.
|
|
23387
23411
|
*/
|
|
23388
23412
|
clear() {
|
|
23389
|
-
LOGGER_SERVICE$
|
|
23413
|
+
LOGGER_SERVICE$4.log(MARKDOWN_METHOD_NAME_CLEAR$1);
|
|
23390
23414
|
this.getMarkdownStorage.clear();
|
|
23391
23415
|
}
|
|
23392
23416
|
/**
|
|
@@ -23394,7 +23418,7 @@ class MarkdownWriterAdapter {
|
|
|
23394
23418
|
* All future markdown writes will be no-ops.
|
|
23395
23419
|
*/
|
|
23396
23420
|
useDummy() {
|
|
23397
|
-
LOGGER_SERVICE$
|
|
23421
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_DUMMY$1);
|
|
23398
23422
|
this.useMarkdownAdapter(MarkdownDummy);
|
|
23399
23423
|
}
|
|
23400
23424
|
}
|
|
@@ -23455,7 +23479,7 @@ class ReportBase {
|
|
|
23455
23479
|
});
|
|
23456
23480
|
}
|
|
23457
23481
|
}, 15000));
|
|
23458
|
-
LOGGER_SERVICE$
|
|
23482
|
+
LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_CTOR, {
|
|
23459
23483
|
reportName: this.reportName,
|
|
23460
23484
|
baseDir,
|
|
23461
23485
|
});
|
|
@@ -23469,7 +23493,7 @@ class ReportBase {
|
|
|
23469
23493
|
* @returns Promise that resolves when initialization is complete
|
|
23470
23494
|
*/
|
|
23471
23495
|
async waitForInit(initial) {
|
|
23472
|
-
LOGGER_SERVICE$
|
|
23496
|
+
LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
|
|
23473
23497
|
reportName: this.reportName,
|
|
23474
23498
|
initial,
|
|
23475
23499
|
});
|
|
@@ -23488,7 +23512,7 @@ class ReportBase {
|
|
|
23488
23512
|
* @throws Error if stream not initialized or write timeout exceeded
|
|
23489
23513
|
*/
|
|
23490
23514
|
async write(data, options) {
|
|
23491
|
-
LOGGER_SERVICE$
|
|
23515
|
+
LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WRITE, {
|
|
23492
23516
|
reportName: this.reportName,
|
|
23493
23517
|
options,
|
|
23494
23518
|
});
|
|
@@ -23585,7 +23609,7 @@ class ReportWriterAdapter {
|
|
|
23585
23609
|
* @internal - Automatically called by report services, not for direct use
|
|
23586
23610
|
*/
|
|
23587
23611
|
this.writeData = async (reportName, data, options) => {
|
|
23588
|
-
LOGGER_SERVICE$
|
|
23612
|
+
LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
|
|
23589
23613
|
reportName,
|
|
23590
23614
|
options,
|
|
23591
23615
|
});
|
|
@@ -23602,7 +23626,7 @@ class ReportWriterAdapter {
|
|
|
23602
23626
|
* @param Ctor - Constructor for report storage adapter
|
|
23603
23627
|
*/
|
|
23604
23628
|
useReportAdapter(Ctor) {
|
|
23605
|
-
LOGGER_SERVICE$
|
|
23629
|
+
LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER$1);
|
|
23606
23630
|
this.ReportFactory = Ctor;
|
|
23607
23631
|
}
|
|
23608
23632
|
/**
|
|
@@ -23611,7 +23635,7 @@ class ReportWriterAdapter {
|
|
|
23611
23635
|
* so new storage instances are created with the updated base path.
|
|
23612
23636
|
*/
|
|
23613
23637
|
clear() {
|
|
23614
|
-
LOGGER_SERVICE$
|
|
23638
|
+
LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_CLEAR$1);
|
|
23615
23639
|
this.getReportStorage.clear();
|
|
23616
23640
|
}
|
|
23617
23641
|
/**
|
|
@@ -23619,7 +23643,7 @@ class ReportWriterAdapter {
|
|
|
23619
23643
|
* All future report writes will be no-ops.
|
|
23620
23644
|
*/
|
|
23621
23645
|
useDummy() {
|
|
23622
|
-
LOGGER_SERVICE$
|
|
23646
|
+
LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY$1);
|
|
23623
23647
|
this.useReportAdapter(ReportDummy);
|
|
23624
23648
|
}
|
|
23625
23649
|
/**
|
|
@@ -23627,7 +23651,7 @@ class ReportWriterAdapter {
|
|
|
23627
23651
|
* All future report writes will use JSONL storage.
|
|
23628
23652
|
*/
|
|
23629
23653
|
useJsonl() {
|
|
23630
|
-
LOGGER_SERVICE$
|
|
23654
|
+
LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_JSONL$1);
|
|
23631
23655
|
this.useReportAdapter(ReportBase);
|
|
23632
23656
|
}
|
|
23633
23657
|
}
|
|
@@ -23682,7 +23706,7 @@ const CREATE_FILE_NAME_FN$c = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
23682
23706
|
* @param value - Value to check
|
|
23683
23707
|
* @returns true if value is unsafe, false otherwise
|
|
23684
23708
|
*/
|
|
23685
|
-
function isUnsafe$
|
|
23709
|
+
function isUnsafe$4(value) {
|
|
23686
23710
|
if (typeof value !== "number") {
|
|
23687
23711
|
return true;
|
|
23688
23712
|
}
|
|
@@ -23694,6 +23718,25 @@ function isUnsafe$3(value) {
|
|
|
23694
23718
|
}
|
|
23695
23719
|
return false;
|
|
23696
23720
|
}
|
|
23721
|
+
/** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
|
|
23722
|
+
const MIN_SIGNALS_FOR_ANNUALIZATION$2 = 10;
|
|
23723
|
+
/** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
|
|
23724
|
+
* sample size is too small to estimate variance meaningfully. */
|
|
23725
|
+
const MIN_SIGNALS_FOR_RATIOS$2 = 10;
|
|
23726
|
+
/** Minimum calendar span (days) for trade-frequency extrapolation. */
|
|
23727
|
+
const MIN_CALENDAR_SPAN_DAYS$2 = 14;
|
|
23728
|
+
/** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
|
|
23729
|
+
const MAX_TRADES_PER_YEAR$2 = 365;
|
|
23730
|
+
/** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
|
|
23731
|
+
* blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
|
|
23732
|
+
* anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
|
|
23733
|
+
const MAX_EXPECTED_YEARLY_RETURNS$2 = 100;
|
|
23734
|
+
/** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
|
|
23735
|
+
const MAX_CALMAR_RATIO$2 = 1000;
|
|
23736
|
+
/** Minimum stdDev required for Sharpe/Sortino computation. Identical-returns series produce
|
|
23737
|
+
* float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously inflates
|
|
23738
|
+
* sharpe to astronomical values. Treat any stdDev below this threshold as zero. */
|
|
23739
|
+
const STDDEV_EPSILON$2 = 1e-9;
|
|
23697
23740
|
/**
|
|
23698
23741
|
* Storage class for accumulating closed signals per strategy.
|
|
23699
23742
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
@@ -23747,65 +23790,190 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23747
23790
|
recoveryFactor: null,
|
|
23748
23791
|
};
|
|
23749
23792
|
}
|
|
23750
|
-
|
|
23751
|
-
|
|
23752
|
-
|
|
23753
|
-
//
|
|
23754
|
-
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
const
|
|
23760
|
-
const
|
|
23761
|
-
const
|
|
23762
|
-
|
|
23763
|
-
//
|
|
23764
|
-
const
|
|
23765
|
-
|
|
23793
|
+
// Valid signal set — those with usable pendingAt AND closeTimestamp. Single source
|
|
23794
|
+
// of truth for EVERY metric in this method (counts, sums, span, equity curve,
|
|
23795
|
+
// ratios, annualization). If we used different subsets for different metrics, the
|
|
23796
|
+
// numerator of one ratio could be drawn from a different population than the
|
|
23797
|
+
// denominator of another and the report would silently lie. On clean data
|
|
23798
|
+
// validSignals === this._signalList; the filter only matters for corrupted runtime
|
|
23799
|
+
// data.
|
|
23800
|
+
const validSignals = this._signalList.filter((s) => typeof s.signal.pendingAt === "number" && s.signal.pendingAt > 0 &&
|
|
23801
|
+
typeof s.closeTimestamp === "number" && s.closeTimestamp > 0);
|
|
23802
|
+
const totalSignals = validSignals.length;
|
|
23803
|
+
const winCount = validSignals.filter((s) => s.pnl.pnlPercentage > 0).length;
|
|
23804
|
+
const lossCount = validSignals.filter((s) => s.pnl.pnlPercentage < 0).length;
|
|
23805
|
+
// Basic statistics — guard against an empty validSignals (e.g. every signal had
|
|
23806
|
+
// corrupted timestamps) so we don't divide by zero.
|
|
23807
|
+
const avgPnl = totalSignals > 0
|
|
23808
|
+
? validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals
|
|
23809
|
+
: 0;
|
|
23810
|
+
const totalPnl = validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
|
|
23811
|
+
// Win rate excludes break-even trades from both numerator and denominator.
|
|
23812
|
+
const decisiveTrades = winCount + lossCount;
|
|
23813
|
+
const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
|
|
23814
|
+
// Calendar span over the same validSignals set used for ratios.
|
|
23815
|
+
let firstPendingAt = Infinity;
|
|
23816
|
+
let lastCloseAt = -Infinity;
|
|
23817
|
+
for (const s of validSignals) {
|
|
23818
|
+
if (s.signal.pendingAt < firstPendingAt)
|
|
23819
|
+
firstPendingAt = s.signal.pendingAt;
|
|
23820
|
+
if (s.closeTimestamp > lastCloseAt)
|
|
23821
|
+
lastCloseAt = s.closeTimestamp;
|
|
23822
|
+
}
|
|
23823
|
+
const calendarSpanDays = isFinite(firstPendingAt) && isFinite(lastCloseAt)
|
|
23824
|
+
? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
|
|
23825
|
+
: 0;
|
|
23826
|
+
// tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
|
|
23827
|
+
// silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
|
|
23828
|
+
// raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
|
|
23829
|
+
// for reliable annualization and surface every annualized metric as null.
|
|
23830
|
+
const rawTradesPerYear = totalSignals >= MIN_SIGNALS_FOR_ANNUALIZATION$2 &&
|
|
23831
|
+
calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$2
|
|
23832
|
+
? (totalSignals / calendarSpanDays) * 365
|
|
23833
|
+
: 0;
|
|
23834
|
+
const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$2;
|
|
23835
|
+
const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
|
|
23836
|
+
// Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1) for unbiased estimate.
|
|
23837
|
+
// Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
|
|
23838
|
+
// are too noisy to publish (high chance of spurious ±Sharpe).
|
|
23839
|
+
const returns = validSignals.map((s) => s.pnl.pnlPercentage);
|
|
23840
|
+
const canComputeRatios = totalSignals >= MIN_SIGNALS_FOR_RATIOS$2;
|
|
23841
|
+
const stdDev = canComputeRatios
|
|
23842
|
+
? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (totalSignals - 1))
|
|
23843
|
+
: 0;
|
|
23844
|
+
// Use STDDEV_EPSILON gate (not stdDev > 0) — identical-returns series produce
|
|
23845
|
+
// float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously
|
|
23846
|
+
// inflates sharpe to astronomical magnitudes (avgPnl / epsilon).
|
|
23847
|
+
const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$2
|
|
23848
|
+
? avgPnl / stdDev
|
|
23849
|
+
: null;
|
|
23850
|
+
// Annualize only when gate passes; otherwise null.
|
|
23851
|
+
const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
|
|
23852
|
+
? sharpeRatio * Math.sqrt(tradesPerYear)
|
|
23853
|
+
: null;
|
|
23854
|
+
// Equity-curve max drawdown via compounded equity (multiplicative, not additive).
|
|
23855
|
+
// Returns are per-trade on cost basis — compounding assumes equal capital allocation
|
|
23856
|
+
// per trade ("as-if 100% allocation"). Walks validSignals in chronological order
|
|
23857
|
+
// (storage is newest-first, so iterate in reverse). Using validSignals (same set as
|
|
23858
|
+
// tradesPerYear) keeps equityFinal consistent with the annualization exponent.
|
|
23859
|
+
// If equity goes ≤ 0 (e.g. leveraged short with r < -100%) — account blown,
|
|
23860
|
+
// fix DD at 100% and stop walking the curve.
|
|
23861
|
+
let equity = 1;
|
|
23862
|
+
let peak = 1;
|
|
23863
|
+
let equityMaxDrawdown = 0;
|
|
23864
|
+
let blown = false;
|
|
23865
|
+
for (let i = validSignals.length - 1; i >= 0; i--) {
|
|
23866
|
+
equity *= 1 + validSignals[i].pnl.pnlPercentage / 100;
|
|
23867
|
+
if (equity <= 0) {
|
|
23868
|
+
equityMaxDrawdown = 100;
|
|
23869
|
+
blown = true;
|
|
23870
|
+
break;
|
|
23871
|
+
}
|
|
23872
|
+
if (equity > peak)
|
|
23873
|
+
peak = equity;
|
|
23874
|
+
const dd = (peak - equity) / peak * 100;
|
|
23875
|
+
if (dd > equityMaxDrawdown)
|
|
23876
|
+
equityMaxDrawdown = dd;
|
|
23877
|
+
}
|
|
23878
|
+
const equityFinal = blown ? 0 : equity;
|
|
23879
|
+
// Compounded yearly return via geometric mean of equity curve.
|
|
23880
|
+
// equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag that
|
|
23881
|
+
// arithmetic-mean compounding ((1+avgPnl)^N) misses. If account is blown, full loss.
|
|
23882
|
+
// If the raw value would exceed MAX_EXPECTED_YEARLY_RETURNS, return null rather than
|
|
23883
|
+
// showing the cap as a real figure — capped numbers mislead users into trusting them.
|
|
23884
|
+
const expectedYearlyReturns = canAnnualize
|
|
23885
|
+
? blown
|
|
23886
|
+
? -100
|
|
23887
|
+
: (() => {
|
|
23888
|
+
// Geometric annualization uses validSignals.length (same set that defined
|
|
23889
|
+
// tradesPerYear); using totalSignals here would mismatch numerator/denominator.
|
|
23890
|
+
const raw = (Math.pow(equityFinal, tradesPerYear / validSignals.length) - 1) * 100;
|
|
23891
|
+
return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$2 ? null : raw;
|
|
23892
|
+
})()
|
|
23893
|
+
: null;
|
|
23894
|
+
// Certainty Ratio — over validSignals so wins/losses come from the same set as
|
|
23895
|
+
// winCount/lossCount/avgPnl above.
|
|
23896
|
+
const wins = validSignals.filter((s) => s.pnl.pnlPercentage > 0);
|
|
23897
|
+
const losses = validSignals.filter((s) => s.pnl.pnlPercentage < 0);
|
|
23766
23898
|
const avgWin = wins.length > 0
|
|
23767
23899
|
? wins.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / wins.length
|
|
23768
23900
|
: 0;
|
|
23769
23901
|
const avgLoss = losses.length > 0
|
|
23770
23902
|
? losses.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / losses.length
|
|
23771
23903
|
: 0;
|
|
23772
|
-
|
|
23773
|
-
//
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
|
|
23777
|
-
|
|
23778
|
-
|
|
23779
|
-
|
|
23780
|
-
|
|
23781
|
-
//
|
|
23782
|
-
|
|
23783
|
-
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
const
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
const
|
|
23790
|
-
|
|
23904
|
+
// Null below MIN_SIGNALS_FOR_RATIOS — on a handful of trades the win/loss
|
|
23905
|
+
// means are too noisy to publish a ratio (same sample-size gate as Sharpe/
|
|
23906
|
+
// Sortino, so the report doesn't surface certainty while withholding the rest).
|
|
23907
|
+
// Also null when no losing trades OR when |avgLoss| is below STDDEV_EPSILON
|
|
23908
|
+
// (float-artifact losses (-1e-15) would otherwise produce a spurious
|
|
23909
|
+
// astronomical certaintyRatio ≈1e14).
|
|
23910
|
+
const certaintyRatio = canComputeRatios && Math.abs(avgLoss) > STDDEV_EPSILON$2 && avgLoss < 0
|
|
23911
|
+
? avgWin / Math.abs(avgLoss)
|
|
23912
|
+
: null;
|
|
23913
|
+
// Average peak/fall PNL — over validSignals; only signals that actually have the
|
|
23914
|
+
// value contribute (no zero dilution from missing peakProfit/maxDrawdown).
|
|
23915
|
+
const peakValues = validSignals
|
|
23916
|
+
.map((s) => s.signal.peakProfit?.pnlPercentage)
|
|
23917
|
+
.filter((v) => typeof v === "number");
|
|
23918
|
+
const fallValues = validSignals
|
|
23919
|
+
.map((s) => s.signal.maxDrawdown?.pnlPercentage)
|
|
23920
|
+
.filter((v) => typeof v === "number");
|
|
23921
|
+
const avgPeakPnl = peakValues.length > 0
|
|
23922
|
+
? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
|
|
23923
|
+
: null;
|
|
23924
|
+
const avgFallPnl = fallValues.length > 0
|
|
23925
|
+
? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
|
|
23926
|
+
: null;
|
|
23927
|
+
// Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
|
|
23928
|
+
// downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
|
|
23929
|
+
// so the numerator reduces to avgPnl and the squared term to r² for r < 0.
|
|
23930
|
+
// Dividing by N_total (not N_negative) properly penalises strategies with frequent
|
|
23931
|
+
// losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
|
|
23932
|
+
// strategies.
|
|
23933
|
+
const negativeReturns = returns.filter((r) => r < 0);
|
|
23934
|
+
const sortinoRatio = (() => {
|
|
23935
|
+
if (!canComputeRatios)
|
|
23936
|
+
return null;
|
|
23937
|
+
if (negativeReturns.length === 0)
|
|
23938
|
+
return null;
|
|
23939
|
+
const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
|
|
23940
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
23941
|
+
// Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
|
|
23942
|
+
return downsideDeviation > STDDEV_EPSILON$2 ? avgPnl / downsideDeviation : null;
|
|
23943
|
+
})();
|
|
23944
|
+
// Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
|
|
23945
|
+
const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
|
|
23946
|
+
? Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, expectedYearlyReturns / equityMaxDrawdown))
|
|
23947
|
+
: null;
|
|
23948
|
+
// Recovery Factor: numerator must be the compounded total return (equityFinal − 1) × 100,
|
|
23949
|
+
// not the arithmetic totalPnl — denominator (equityMaxDrawdown) is from the compounded
|
|
23950
|
+
// curve, so mixing units would inflate Recovery on long winning streaks.
|
|
23951
|
+
// Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
|
|
23952
|
+
// so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
|
|
23953
|
+
// Null when account is blown — ratio is meaningless after total loss.
|
|
23954
|
+
// Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
|
|
23955
|
+
// and explode the same way when DD is near zero.
|
|
23956
|
+
const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
|
|
23957
|
+
? null
|
|
23958
|
+
: Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, ((equityFinal - 1) * 100) / equityMaxDrawdown));
|
|
23791
23959
|
return {
|
|
23792
23960
|
signalList: this._signalList,
|
|
23793
23961
|
totalSignals,
|
|
23794
23962
|
winCount,
|
|
23795
23963
|
lossCount,
|
|
23796
|
-
winRate: isUnsafe$
|
|
23797
|
-
avgPnl: isUnsafe$
|
|
23798
|
-
totalPnl: isUnsafe$
|
|
23799
|
-
stdDev: isUnsafe$
|
|
23800
|
-
sharpeRatio: isUnsafe$
|
|
23801
|
-
annualizedSharpeRatio: isUnsafe$
|
|
23802
|
-
certaintyRatio: isUnsafe$
|
|
23803
|
-
expectedYearlyReturns: isUnsafe$
|
|
23804
|
-
avgPeakPnl: isUnsafe$
|
|
23805
|
-
avgFallPnl: isUnsafe$
|
|
23806
|
-
sortinoRatio: isUnsafe$
|
|
23807
|
-
calmarRatio: isUnsafe$
|
|
23808
|
-
recoveryFactor: isUnsafe$
|
|
23964
|
+
winRate: isUnsafe$4(winRate) ? null : winRate,
|
|
23965
|
+
avgPnl: isUnsafe$4(avgPnl) ? null : avgPnl,
|
|
23966
|
+
totalPnl: isUnsafe$4(totalPnl) ? null : totalPnl,
|
|
23967
|
+
stdDev: isUnsafe$4(stdDev) ? null : stdDev,
|
|
23968
|
+
sharpeRatio: isUnsafe$4(sharpeRatio) ? null : sharpeRatio,
|
|
23969
|
+
annualizedSharpeRatio: isUnsafe$4(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
23970
|
+
certaintyRatio: isUnsafe$4(certaintyRatio) ? null : certaintyRatio,
|
|
23971
|
+
expectedYearlyReturns: isUnsafe$4(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
23972
|
+
avgPeakPnl: isUnsafe$4(avgPeakPnl) ? null : avgPeakPnl,
|
|
23973
|
+
avgFallPnl: isUnsafe$4(avgFallPnl) ? null : avgFallPnl,
|
|
23974
|
+
sortinoRatio: isUnsafe$4(sortinoRatio) ? null : sortinoRatio,
|
|
23975
|
+
calmarRatio: isUnsafe$4(calmarRatio) ? null : calmarRatio,
|
|
23976
|
+
recoveryFactor: isUnsafe$4(recoveryFactor) ? null : recoveryFactor,
|
|
23809
23977
|
};
|
|
23810
23978
|
}
|
|
23811
23979
|
/**
|
|
@@ -23847,24 +24015,26 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23847
24015
|
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
23848
24016
|
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
23849
24017
|
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
23850
|
-
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better
|
|
24018
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
23851
24019
|
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
23852
|
-
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better
|
|
24020
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
23853
24021
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
23854
24022
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
23855
24023
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
23856
|
-
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better
|
|
24024
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
23857
24025
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
23858
24026
|
"",
|
|
23859
24027
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
23860
24028
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
23861
|
-
`*Annualized Sharpe Ratio:
|
|
23862
|
-
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
24029
|
+
`*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$2} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$2} days. Assumes returns are iid — autocorrelated strategies are overstated.*`,
|
|
24030
|
+
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
|
|
23863
24031
|
`*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
|
|
23864
|
-
`*Expected Yearly Returns:
|
|
23865
|
-
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong.
|
|
23866
|
-
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
|
|
23867
|
-
`*All metrics require 100+ signals to be statistically reliable.
|
|
24032
|
+
`*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS$2}% — values above the cap return N/A.*`,
|
|
24033
|
+
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at ±${MAX_CALMAR_RATIO$2}.*`,
|
|
24034
|
+
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
|
|
24035
|
+
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24036
|
+
`*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
|
|
24037
|
+
`*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
|
|
23868
24038
|
].join("\n");
|
|
23869
24039
|
}
|
|
23870
24040
|
/**
|
|
@@ -24176,7 +24346,7 @@ const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
24176
24346
|
* @param value - Value to check
|
|
24177
24347
|
* @returns true if value is unsafe, false otherwise
|
|
24178
24348
|
*/
|
|
24179
|
-
function isUnsafe$
|
|
24349
|
+
function isUnsafe$3(value) {
|
|
24180
24350
|
if (typeof value !== "number") {
|
|
24181
24351
|
return true;
|
|
24182
24352
|
}
|
|
@@ -24188,6 +24358,25 @@ function isUnsafe$2(value) {
|
|
|
24188
24358
|
}
|
|
24189
24359
|
return false;
|
|
24190
24360
|
}
|
|
24361
|
+
/** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
|
|
24362
|
+
const MIN_SIGNALS_FOR_ANNUALIZATION$1 = 10;
|
|
24363
|
+
/** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
|
|
24364
|
+
* sample size is too small to estimate variance meaningfully. */
|
|
24365
|
+
const MIN_SIGNALS_FOR_RATIOS$1 = 10;
|
|
24366
|
+
/** Minimum calendar span (days) for trade-frequency extrapolation. */
|
|
24367
|
+
const MIN_CALENDAR_SPAN_DAYS$1 = 14;
|
|
24368
|
+
/** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
|
|
24369
|
+
const MAX_TRADES_PER_YEAR$1 = 365;
|
|
24370
|
+
/** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
|
|
24371
|
+
* blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
|
|
24372
|
+
* anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
|
|
24373
|
+
const MAX_EXPECTED_YEARLY_RETURNS$1 = 100;
|
|
24374
|
+
/** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
|
|
24375
|
+
const MAX_CALMAR_RATIO$1 = 1000;
|
|
24376
|
+
/** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
|
|
24377
|
+
* float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
|
|
24378
|
+
* astronomical magnitudes (avgPnl / epsilon). */
|
|
24379
|
+
const STDDEV_EPSILON$1 = 1e-9;
|
|
24191
24380
|
/**
|
|
24192
24381
|
* Storage class for accumulating all tick events per strategy.
|
|
24193
24382
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
@@ -24471,84 +24660,190 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24471
24660
|
};
|
|
24472
24661
|
}
|
|
24473
24662
|
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
24474
|
-
|
|
24475
|
-
|
|
24476
|
-
|
|
24477
|
-
|
|
24478
|
-
|
|
24479
|
-
|
|
24663
|
+
// Valid closed set — single source of truth. Events must have numeric pnl AND valid
|
|
24664
|
+
// timestamps. Win/loss counts, returns, calendar span, equity curve — all derived
|
|
24665
|
+
// from this set so they cannot disagree.
|
|
24666
|
+
const validClosed = closedEvents.filter((e) => typeof e.pnl === "number" &&
|
|
24667
|
+
typeof e.timestamp === "number" &&
|
|
24668
|
+
e.timestamp > 0 &&
|
|
24669
|
+
typeof (e.pendingAt ?? e.timestamp) === "number");
|
|
24670
|
+
const totalClosed = validClosed.length;
|
|
24671
|
+
const winCount = validClosed.filter((e) => e.pnl > 0).length;
|
|
24672
|
+
const lossCount = validClosed.filter((e) => e.pnl < 0).length;
|
|
24673
|
+
const returns = validClosed.map((e) => e.pnl);
|
|
24674
|
+
const avgPnl = returns.length > 0
|
|
24675
|
+
? returns.reduce((sum, r) => sum + r, 0) / returns.length
|
|
24480
24676
|
: 0;
|
|
24481
|
-
const totalPnl =
|
|
24482
|
-
|
|
24483
|
-
|
|
24484
|
-
|
|
24485
|
-
|
|
24486
|
-
|
|
24487
|
-
|
|
24488
|
-
|
|
24489
|
-
|
|
24490
|
-
|
|
24491
|
-
|
|
24492
|
-
|
|
24493
|
-
|
|
24494
|
-
|
|
24495
|
-
|
|
24496
|
-
|
|
24497
|
-
|
|
24677
|
+
const totalPnl = returns.reduce((sum, r) => sum + r, 0);
|
|
24678
|
+
// Win rate excludes break-even trades from both numerator and denominator.
|
|
24679
|
+
const decisiveTrades = winCount + lossCount;
|
|
24680
|
+
const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
|
|
24681
|
+
// Trade frequency from calendar span — gated by minimum span and sample size to
|
|
24682
|
+
// suppress absurd annualization on short / sparse runs. Span built from validClosed
|
|
24683
|
+
// so denominator (calendarSpanDays) and numerator (returns.length) come from the
|
|
24684
|
+
// same event set.
|
|
24685
|
+
let firstPendingAt = Infinity;
|
|
24686
|
+
let lastCloseAt = -Infinity;
|
|
24687
|
+
for (const e of validClosed) {
|
|
24688
|
+
const startAt = e.pendingAt ?? e.timestamp;
|
|
24689
|
+
if (startAt < firstPendingAt)
|
|
24690
|
+
firstPendingAt = startAt;
|
|
24691
|
+
if (e.timestamp > lastCloseAt)
|
|
24692
|
+
lastCloseAt = e.timestamp;
|
|
24693
|
+
}
|
|
24694
|
+
const calendarSpanDays = validClosed.length > 0
|
|
24695
|
+
? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
|
|
24696
|
+
: 0;
|
|
24697
|
+
// tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
|
|
24698
|
+
// silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
|
|
24699
|
+
// raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
|
|
24700
|
+
// for reliable annualization and surface every annualized metric as null.
|
|
24701
|
+
const rawTradesPerYear = returns.length >= MIN_SIGNALS_FOR_ANNUALIZATION$1 &&
|
|
24702
|
+
calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$1
|
|
24703
|
+
? (returns.length / calendarSpanDays) * 365
|
|
24704
|
+
: 0;
|
|
24705
|
+
const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$1;
|
|
24706
|
+
const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
|
|
24707
|
+
// Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1).
|
|
24708
|
+
// Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
|
|
24709
|
+
// are too noisy to publish (high chance of spurious ±Sharpe).
|
|
24710
|
+
const canComputeRatios = returns.length >= MIN_SIGNALS_FOR_RATIOS$1;
|
|
24711
|
+
const stdDev = canComputeRatios
|
|
24712
|
+
? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (returns.length - 1))
|
|
24713
|
+
: 0;
|
|
24714
|
+
// STDDEV_EPSILON guard — protects against float-artifact stdDev from identical
|
|
24715
|
+
// returns producing spuriously astronomical sharpe.
|
|
24716
|
+
const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$1
|
|
24717
|
+
? avgPnl / stdDev
|
|
24718
|
+
: null;
|
|
24719
|
+
// Annualize only when gate passes; otherwise null.
|
|
24720
|
+
const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
|
|
24721
|
+
? sharpeRatio * Math.sqrt(tradesPerYear)
|
|
24722
|
+
: null;
|
|
24723
|
+
// Certainty Ratio: null (not zero) when there are no losing trades — a flawless
|
|
24724
|
+
// strategy has undefined Certainty Ratio, not "worst case zero". Computed on
|
|
24725
|
+
// validClosed for consistency with other ratios.
|
|
24726
|
+
// Gated below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as Sharpe/Sortino,
|
|
24727
|
+
// so the report doesn't surface certainty on a handful of trades while
|
|
24728
|
+
// withholding the rest.
|
|
24729
|
+
let certaintyRatio = null;
|
|
24730
|
+
if (canComputeRatios && totalClosed > 0) {
|
|
24731
|
+
const wins = validClosed.filter((e) => e.pnl > 0);
|
|
24732
|
+
const losses = validClosed.filter((e) => e.pnl < 0);
|
|
24498
24733
|
const avgWin = wins.length > 0
|
|
24499
|
-
? wins.reduce((sum, e) => sum +
|
|
24734
|
+
? wins.reduce((sum, e) => sum + e.pnl, 0) / wins.length
|
|
24500
24735
|
: 0;
|
|
24501
24736
|
const avgLoss = losses.length > 0
|
|
24502
|
-
? losses.reduce((sum, e) => sum +
|
|
24737
|
+
? losses.reduce((sum, e) => sum + e.pnl, 0) / losses.length
|
|
24503
24738
|
: 0;
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
|
|
24509
|
-
|
|
24510
|
-
|
|
24511
|
-
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
|
|
24518
|
-
|
|
24519
|
-
|
|
24520
|
-
|
|
24521
|
-
const
|
|
24522
|
-
|
|
24523
|
-
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
|
|
24527
|
-
|
|
24528
|
-
|
|
24529
|
-
//
|
|
24530
|
-
const
|
|
24531
|
-
|
|
24532
|
-
|
|
24739
|
+
// STDDEV_EPSILON guard on |avgLoss| protects against float-artifact
|
|
24740
|
+
// losses producing spurious astronomical certaintyRatio.
|
|
24741
|
+
certaintyRatio = Math.abs(avgLoss) > STDDEV_EPSILON$1 && avgLoss < 0
|
|
24742
|
+
? avgWin / Math.abs(avgLoss)
|
|
24743
|
+
: null;
|
|
24744
|
+
}
|
|
24745
|
+
// Average only over signals that have the value — do not dilute the mean with zeros.
|
|
24746
|
+
// Use validClosed to keep all metric denominators consistent.
|
|
24747
|
+
const peakValues = validClosed
|
|
24748
|
+
.map((e) => e.peakPnl)
|
|
24749
|
+
.filter((v) => typeof v === "number");
|
|
24750
|
+
const fallValues = validClosed
|
|
24751
|
+
.map((e) => e.fallPnl)
|
|
24752
|
+
.filter((v) => typeof v === "number");
|
|
24753
|
+
const avgPeakPnl = peakValues.length > 0
|
|
24754
|
+
? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
|
|
24755
|
+
: null;
|
|
24756
|
+
const avgFallPnl = fallValues.length > 0
|
|
24757
|
+
? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
|
|
24758
|
+
: null;
|
|
24759
|
+
// Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
|
|
24760
|
+
// downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
|
|
24761
|
+
// so the numerator reduces to avgPnl and the squared term to r² for r < 0.
|
|
24762
|
+
// Dividing by N_total (not N_negative) properly penalises strategies with frequent
|
|
24763
|
+
// losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
|
|
24764
|
+
// strategies.
|
|
24765
|
+
const sortinoRatio = (() => {
|
|
24766
|
+
if (!canComputeRatios)
|
|
24767
|
+
return null;
|
|
24768
|
+
const negativeReturns = returns.filter((r) => r < 0);
|
|
24769
|
+
if (negativeReturns.length === 0)
|
|
24770
|
+
return null;
|
|
24771
|
+
const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
|
|
24772
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
24773
|
+
// Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
|
|
24774
|
+
return downsideDeviation > STDDEV_EPSILON$1 ? avgPnl / downsideDeviation : null;
|
|
24775
|
+
})();
|
|
24776
|
+
// Equity-curve max drawdown via compounded equity (multiplicative). Returns are per-trade
|
|
24777
|
+
// on cost basis — compounding assumes equal capital allocation per trade ("as-if 100%").
|
|
24778
|
+
// If equity ≤ 0 (leveraged short with r < -100%) — account blown, fix DD at 100%.
|
|
24779
|
+
// Built from validClosed (newest-first), iterated reverse for chronological order.
|
|
24780
|
+
const chronologicalReturns = [];
|
|
24781
|
+
for (let i = validClosed.length - 1; i >= 0; i--) {
|
|
24782
|
+
chronologicalReturns.push(validClosed[i].pnl);
|
|
24783
|
+
}
|
|
24784
|
+
let equity = 1;
|
|
24785
|
+
let peak = 1;
|
|
24786
|
+
let equityMaxDrawdown = 0;
|
|
24787
|
+
let blown = false;
|
|
24788
|
+
for (const r of chronologicalReturns) {
|
|
24789
|
+
equity *= 1 + r / 100;
|
|
24790
|
+
if (equity <= 0) {
|
|
24791
|
+
equityMaxDrawdown = 100;
|
|
24792
|
+
blown = true;
|
|
24793
|
+
break;
|
|
24794
|
+
}
|
|
24795
|
+
if (equity > peak)
|
|
24796
|
+
peak = equity;
|
|
24797
|
+
const dd = (peak - equity) / peak * 100;
|
|
24798
|
+
if (dd > equityMaxDrawdown)
|
|
24799
|
+
equityMaxDrawdown = dd;
|
|
24800
|
+
}
|
|
24801
|
+
const equityFinal = blown ? 0 : equity;
|
|
24802
|
+
// Compounded yearly return via geometric mean of equity curve:
|
|
24803
|
+
// equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag.
|
|
24804
|
+
// If account is blown, full loss. If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS,
|
|
24805
|
+
// return null rather than showing the cap — capped numbers mislead users.
|
|
24806
|
+
const expectedYearlyReturns = canAnnualize
|
|
24807
|
+
? blown
|
|
24808
|
+
? -100
|
|
24809
|
+
: (() => {
|
|
24810
|
+
const raw = (Math.pow(equityFinal, tradesPerYear / returns.length) - 1) * 100;
|
|
24811
|
+
return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$1 ? null : raw;
|
|
24812
|
+
})()
|
|
24813
|
+
: null;
|
|
24814
|
+
// Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
|
|
24815
|
+
const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
|
|
24816
|
+
? Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, expectedYearlyReturns / equityMaxDrawdown))
|
|
24817
|
+
: null;
|
|
24818
|
+
// Recovery Factor: numerator must be the compounded total return, not arithmetic totalPnl —
|
|
24819
|
+
// denominator is from the compounded equity curve, so mixing units inflates Recovery.
|
|
24820
|
+
// Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
|
|
24821
|
+
// so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
|
|
24822
|
+
// Null when account is blown.
|
|
24823
|
+
// Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
|
|
24824
|
+
// and explode the same way when DD is near zero.
|
|
24825
|
+
const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
|
|
24826
|
+
? null
|
|
24827
|
+
: Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, ((equityFinal - 1) * 100) / equityMaxDrawdown));
|
|
24533
24828
|
return {
|
|
24534
24829
|
eventList: this._eventList,
|
|
24535
24830
|
totalEvents: this._eventList.length,
|
|
24536
24831
|
totalClosed,
|
|
24537
24832
|
winCount,
|
|
24538
24833
|
lossCount,
|
|
24539
|
-
winRate: isUnsafe$
|
|
24540
|
-
avgPnl: isUnsafe$
|
|
24541
|
-
totalPnl: isUnsafe$
|
|
24542
|
-
stdDev: isUnsafe$
|
|
24543
|
-
sharpeRatio: isUnsafe$
|
|
24544
|
-
annualizedSharpeRatio: isUnsafe$
|
|
24545
|
-
certaintyRatio: isUnsafe$
|
|
24546
|
-
expectedYearlyReturns: isUnsafe$
|
|
24547
|
-
avgPeakPnl: isUnsafe$
|
|
24548
|
-
avgFallPnl: isUnsafe$
|
|
24549
|
-
sortinoRatio: isUnsafe$
|
|
24550
|
-
calmarRatio: isUnsafe$
|
|
24551
|
-
recoveryFactor: isUnsafe$
|
|
24834
|
+
winRate: isUnsafe$3(winRate) ? null : winRate,
|
|
24835
|
+
avgPnl: isUnsafe$3(avgPnl) ? null : avgPnl,
|
|
24836
|
+
totalPnl: isUnsafe$3(totalPnl) ? null : totalPnl,
|
|
24837
|
+
stdDev: isUnsafe$3(stdDev) ? null : stdDev,
|
|
24838
|
+
sharpeRatio: isUnsafe$3(sharpeRatio) ? null : sharpeRatio,
|
|
24839
|
+
annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
24840
|
+
certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
|
|
24841
|
+
expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
24842
|
+
avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
|
|
24843
|
+
avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
|
|
24844
|
+
sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
|
|
24845
|
+
calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
|
|
24846
|
+
recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
|
|
24552
24847
|
};
|
|
24553
24848
|
}
|
|
24554
24849
|
/**
|
|
@@ -24596,18 +24891,20 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24596
24891
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
24597
24892
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
24598
24893
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
24599
|
-
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better
|
|
24894
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
24600
24895
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
24601
24896
|
"",
|
|
24602
24897
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
24603
24898
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
24604
|
-
`*Annualized Sharpe Ratio:
|
|
24605
|
-
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
24899
|
+
`*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$1} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$1} days. Assumes returns are iid — autocorrelated strategies are overstated.*`,
|
|
24900
|
+
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
|
|
24606
24901
|
`*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
|
|
24607
|
-
`*Expected Yearly Returns:
|
|
24608
|
-
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong.
|
|
24609
|
-
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
|
|
24610
|
-
`*All metrics require 100+ signals to be statistically reliable.
|
|
24902
|
+
`*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS$1}% — values above the cap return N/A.*`,
|
|
24903
|
+
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at ±${MAX_CALMAR_RATIO$1}.*`,
|
|
24904
|
+
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
|
|
24905
|
+
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24906
|
+
`*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
|
|
24907
|
+
`*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
|
|
24611
24908
|
].join("\n");
|
|
24612
24909
|
}
|
|
24613
24910
|
/**
|
|
@@ -24986,7 +25283,9 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
24986
25283
|
*/
|
|
24987
25284
|
addOpenedEvent(data) {
|
|
24988
25285
|
const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
|
|
24989
|
-
|
|
25286
|
+
// Keep fractional minutes — rounding to whole minutes zeroed out sub-30s durations,
|
|
25287
|
+
// which dragged high-frequency averages towards zero.
|
|
25288
|
+
const durationMin = durationMs / 60000;
|
|
24990
25289
|
const newEvent = {
|
|
24991
25290
|
timestamp: data.signal.pendingAt,
|
|
24992
25291
|
action: "opened",
|
|
@@ -25022,7 +25321,8 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
25022
25321
|
*/
|
|
25023
25322
|
addCancelledEvent(data) {
|
|
25024
25323
|
const durationMs = data.closeTimestamp - data.signal.scheduledAt;
|
|
25025
|
-
|
|
25324
|
+
// Keep fractional minutes — rounding to whole minutes zeroed out sub-30s durations.
|
|
25325
|
+
const durationMin = durationMs / 60000;
|
|
25026
25326
|
const newEvent = {
|
|
25027
25327
|
timestamp: data.closeTimestamp,
|
|
25028
25328
|
action: "cancelled",
|
|
@@ -25078,19 +25378,33 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
25078
25378
|
const totalScheduled = scheduledEvents.length;
|
|
25079
25379
|
const totalOpened = openedEvents.length;
|
|
25080
25380
|
const totalCancelled = cancelledEvents.length;
|
|
25081
|
-
//
|
|
25082
|
-
|
|
25083
|
-
//
|
|
25084
|
-
|
|
25085
|
-
|
|
25086
|
-
const
|
|
25087
|
-
|
|
25088
|
-
|
|
25381
|
+
// Rate denominators must include only scheduled events whose outcome (opened/cancelled)
|
|
25382
|
+
// is also in the buffer. Otherwise a sliding window of 250 entries can drop the
|
|
25383
|
+
// "scheduled" record before its outcome arrives, inflating rates above 100% or
|
|
25384
|
+
// causing one rate to fire without the other. Match by signalId.
|
|
25385
|
+
const scheduledIds = new Set(scheduledEvents.map((e) => e.signalId).filter((id) => typeof id === "string"));
|
|
25386
|
+
const openedFromScheduled = openedEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
|
|
25387
|
+
const cancelledFromScheduled = cancelledEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
|
|
25388
|
+
const resolvedScheduled = openedFromScheduled.length + cancelledFromScheduled.length;
|
|
25389
|
+
const cancellationRate = resolvedScheduled > 0
|
|
25390
|
+
? (cancelledFromScheduled.length / resolvedScheduled) * 100
|
|
25391
|
+
: null;
|
|
25392
|
+
const activationRate = resolvedScheduled > 0
|
|
25393
|
+
? (openedFromScheduled.length / resolvedScheduled) * 100
|
|
25089
25394
|
: null;
|
|
25090
|
-
//
|
|
25091
|
-
|
|
25092
|
-
|
|
25093
|
-
|
|
25395
|
+
// Average durations — include only events with a numeric duration, do not dilute
|
|
25396
|
+
// the mean with zeros for missing values.
|
|
25397
|
+
const cancelledDurations = cancelledEvents
|
|
25398
|
+
.map((e) => e.duration)
|
|
25399
|
+
.filter((d) => typeof d === "number");
|
|
25400
|
+
const openedDurations = openedEvents
|
|
25401
|
+
.map((e) => e.duration)
|
|
25402
|
+
.filter((d) => typeof d === "number");
|
|
25403
|
+
const avgWaitTime = cancelledDurations.length > 0
|
|
25404
|
+
? cancelledDurations.reduce((sum, d) => sum + d, 0) / cancelledDurations.length
|
|
25405
|
+
: null;
|
|
25406
|
+
const avgActivationTime = openedDurations.length > 0
|
|
25407
|
+
? openedDurations.reduce((sum, d) => sum + d, 0) / openedDurations.length
|
|
25094
25408
|
: null;
|
|
25095
25409
|
return {
|
|
25096
25410
|
eventList: this._eventList,
|
|
@@ -25137,13 +25451,15 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
25137
25451
|
table,
|
|
25138
25452
|
"",
|
|
25139
25453
|
`**Total events:** ${stats.totalEvents}`,
|
|
25140
|
-
`**Scheduled signals:** ${stats.totalScheduled}`,
|
|
25454
|
+
`**Scheduled signals (raw):** ${stats.totalScheduled}`,
|
|
25141
25455
|
`**Opened signals:** ${stats.totalOpened}`,
|
|
25142
25456
|
`**Cancelled signals:** ${stats.totalCancelled}`,
|
|
25143
25457
|
`**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
|
|
25144
25458
|
`**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
|
|
25145
25459
|
`**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
|
|
25146
|
-
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}
|
|
25460
|
+
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`,
|
|
25461
|
+
"",
|
|
25462
|
+
`*Activation / Cancellation rates are computed over scheduled signals whose outcome (opened or cancelled) is also in the buffer — matched by signalId. "Scheduled signals (raw)" above is the unmatched count and may include records whose outcome has not yet arrived or was evicted from the buffer.*`
|
|
25147
25463
|
].join("\n");
|
|
25148
25464
|
}
|
|
25149
25465
|
/**
|
|
@@ -25448,13 +25764,37 @@ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
25448
25764
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
25449
25765
|
};
|
|
25450
25766
|
/**
|
|
25451
|
-
*
|
|
25767
|
+
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
25768
|
+
*/
|
|
25769
|
+
function isUnsafe$2(value) {
|
|
25770
|
+
if (typeof value !== "number") {
|
|
25771
|
+
return true;
|
|
25772
|
+
}
|
|
25773
|
+
if (isNaN(value)) {
|
|
25774
|
+
return true;
|
|
25775
|
+
}
|
|
25776
|
+
if (!isFinite(value)) {
|
|
25777
|
+
return true;
|
|
25778
|
+
}
|
|
25779
|
+
return false;
|
|
25780
|
+
}
|
|
25781
|
+
/**
|
|
25782
|
+
* Calculates percentile value from sorted array using linear interpolation
|
|
25783
|
+
* between adjacent ranks (equivalent to numpy.percentile with default linear method).
|
|
25784
|
+
* Falls back to nearest-rank for length 0/1.
|
|
25452
25785
|
*/
|
|
25453
25786
|
function percentile(sortedArray, p) {
|
|
25454
25787
|
if (sortedArray.length === 0)
|
|
25455
25788
|
return 0;
|
|
25456
|
-
|
|
25457
|
-
|
|
25789
|
+
if (sortedArray.length === 1)
|
|
25790
|
+
return sortedArray[0];
|
|
25791
|
+
const rank = (p / 100) * (sortedArray.length - 1);
|
|
25792
|
+
const lower = Math.floor(rank);
|
|
25793
|
+
const upper = Math.ceil(rank);
|
|
25794
|
+
if (lower === upper)
|
|
25795
|
+
return sortedArray[lower];
|
|
25796
|
+
const fraction = rank - lower;
|
|
25797
|
+
return sortedArray[lower] * (1 - fraction) + sortedArray[upper] * fraction;
|
|
25458
25798
|
}
|
|
25459
25799
|
/**
|
|
25460
25800
|
* Storage class for accumulating performance metrics per strategy.
|
|
@@ -25510,10 +25850,12 @@ class PerformanceStorage {
|
|
|
25510
25850
|
const durations = events.map((e) => e.duration).sort((a, b) => a - b);
|
|
25511
25851
|
const totalDuration = durations.reduce((sum, d) => sum + d, 0);
|
|
25512
25852
|
const avgDuration = totalDuration / durations.length;
|
|
25513
|
-
//
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25853
|
+
// Sample standard deviation (Bessel correction: divide by N-1, not N) — consistent
|
|
25854
|
+
// with Sharpe/Sortino calculations in Backtest/Live/Heat services.
|
|
25855
|
+
const stdDev = durations.length > 1
|
|
25856
|
+
? Math.sqrt(durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
|
|
25857
|
+
(durations.length - 1))
|
|
25858
|
+
: 0;
|
|
25517
25859
|
// Calculate wait times between events
|
|
25518
25860
|
const waitTimes = [];
|
|
25519
25861
|
for (let i = 0; i < events.length; i++) {
|
|
@@ -25586,9 +25928,13 @@ class PerformanceStorage {
|
|
|
25586
25928
|
const rows = await Promise.all(sortedMetrics.map(async (metric, index) => Promise.all(visibleColumns.map((col) => col.format(metric, index)))));
|
|
25587
25929
|
const tableData = [header, separator, ...rows];
|
|
25588
25930
|
const summaryTable = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
25589
|
-
// Calculate percentage of total time for each metric
|
|
25931
|
+
// Calculate percentage of total time for each metric. Guard against zero total
|
|
25932
|
+
// duration (all-instant operations) to avoid NaN% in the rendered report.
|
|
25590
25933
|
const percentages = sortedMetrics.map((metric) => {
|
|
25591
|
-
const
|
|
25934
|
+
const pctRaw = stats.totalDuration > 0
|
|
25935
|
+
? (metric.totalDuration / stats.totalDuration) * 100
|
|
25936
|
+
: 0;
|
|
25937
|
+
const pct = isUnsafe$2(pctRaw) ? 0 : pctRaw;
|
|
25592
25938
|
return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
|
|
25593
25939
|
});
|
|
25594
25940
|
return [
|
|
@@ -26357,6 +26703,25 @@ function isUnsafe(value) {
|
|
|
26357
26703
|
}
|
|
26358
26704
|
return false;
|
|
26359
26705
|
}
|
|
26706
|
+
/** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
|
|
26707
|
+
const MIN_SIGNALS_FOR_ANNUALIZATION = 10;
|
|
26708
|
+
/** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
|
|
26709
|
+
* sample size is too small to estimate variance meaningfully. */
|
|
26710
|
+
const MIN_SIGNALS_FOR_RATIOS = 10;
|
|
26711
|
+
/** Minimum calendar span (days) for trade-frequency extrapolation. */
|
|
26712
|
+
const MIN_CALENDAR_SPAN_DAYS = 14;
|
|
26713
|
+
/** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
|
|
26714
|
+
const MAX_TRADES_PER_YEAR = 365;
|
|
26715
|
+
/** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
|
|
26716
|
+
* blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
|
|
26717
|
+
* anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
|
|
26718
|
+
const MAX_EXPECTED_YEARLY_RETURNS = 100;
|
|
26719
|
+
/** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
|
|
26720
|
+
const MAX_CALMAR_RATIO = 1000;
|
|
26721
|
+
/** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
|
|
26722
|
+
* float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
|
|
26723
|
+
* astronomical magnitudes (avgPnl / epsilon). */
|
|
26724
|
+
const STDDEV_EPSILON = 1e-9;
|
|
26360
26725
|
/**
|
|
26361
26726
|
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
26362
26727
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
@@ -26398,7 +26763,7 @@ class HeatmapStorage {
|
|
|
26398
26763
|
* - **totalPnl** — sum of `pnlPercentage` across all signals
|
|
26399
26764
|
* - **avgPnl** — arithmetic mean of `pnlPercentage`
|
|
26400
26765
|
* - **stdDev** — population standard deviation of `pnlPercentage`
|
|
26401
|
-
* - **sharpeRatio** — `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
|
|
26766
|
+
* - **sharpeRatio** — per-trade Sharpe: `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
|
|
26402
26767
|
* - **maxDrawdown** — largest cumulative loss streak (absolute value of peak negative equity)
|
|
26403
26768
|
* - **profitFactor** — `sumWins / |sumLosses|`; requires at least one win and one loss
|
|
26404
26769
|
* - **avgWin / avgLoss** — mean of positive / negative trades respectively
|
|
@@ -26414,10 +26779,12 @@ class HeatmapStorage {
|
|
|
26414
26779
|
const totalTrades = signals.length;
|
|
26415
26780
|
const winCount = signals.filter((s) => s.pnl.pnlPercentage > 0).length;
|
|
26416
26781
|
const lossCount = signals.filter((s) => s.pnl.pnlPercentage < 0).length;
|
|
26417
|
-
//
|
|
26782
|
+
// Win rate excludes break-even trades from both numerator and denominator —
|
|
26783
|
+
// they are neither wins nor losses.
|
|
26418
26784
|
let winRate = null;
|
|
26419
|
-
|
|
26420
|
-
|
|
26785
|
+
const decisiveTrades = winCount + lossCount;
|
|
26786
|
+
if (decisiveTrades > 0) {
|
|
26787
|
+
winRate = (winCount / decisiveTrades) * 100;
|
|
26421
26788
|
}
|
|
26422
26789
|
// Calculate total PNL
|
|
26423
26790
|
let totalPnl = null;
|
|
@@ -26429,36 +26796,47 @@ class HeatmapStorage {
|
|
|
26429
26796
|
if (signals.length > 0) {
|
|
26430
26797
|
avgPnl = totalPnl / signals.length;
|
|
26431
26798
|
}
|
|
26432
|
-
//
|
|
26799
|
+
// Sample standard deviation (Bessel correction: divide by N-1, not N).
|
|
26800
|
+
// Per-symbol ratios are gated by MIN_SIGNALS_FOR_RATIOS — variance estimates from
|
|
26801
|
+
// tiny samples are too noisy to publish.
|
|
26802
|
+
const canComputeRatios = signals.length >= MIN_SIGNALS_FOR_RATIOS;
|
|
26433
26803
|
let stdDev = null;
|
|
26434
|
-
if (
|
|
26435
|
-
const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / signals.length;
|
|
26804
|
+
if (canComputeRatios && avgPnl !== null) {
|
|
26805
|
+
const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / (signals.length - 1);
|
|
26436
26806
|
stdDev = Math.sqrt(variance);
|
|
26437
26807
|
}
|
|
26438
|
-
//
|
|
26808
|
+
// Per-trade Sharpe Ratio
|
|
26439
26809
|
let sharpeRatio = null;
|
|
26440
|
-
|
|
26810
|
+
// STDDEV_EPSILON guard — protects against float-artifact stdDev producing
|
|
26811
|
+
// spuriously astronomical sharpe on identical-returns symbols.
|
|
26812
|
+
if (avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
|
|
26441
26813
|
sharpeRatio = avgPnl / stdDev;
|
|
26442
26814
|
}
|
|
26443
|
-
//
|
|
26815
|
+
// Equity-curve max drawdown via compounded equity ("as-if 100% allocation per trade").
|
|
26816
|
+
// Signals are stored newest-first (unshift in addSignal), so iterate in reverse.
|
|
26817
|
+
// If equity ≤ 0 — account blown, fix DD at 100%. equityFinal feeds expectedYearlyReturns.
|
|
26444
26818
|
let maxDrawdown = null;
|
|
26819
|
+
let equityFinal = 1;
|
|
26820
|
+
let blown = false;
|
|
26445
26821
|
if (signals.length > 0) {
|
|
26446
|
-
let
|
|
26447
|
-
let
|
|
26822
|
+
let equity = 1;
|
|
26823
|
+
let peak = 1;
|
|
26448
26824
|
let maxDD = 0;
|
|
26449
|
-
for (
|
|
26450
|
-
|
|
26451
|
-
if (
|
|
26452
|
-
|
|
26453
|
-
|
|
26454
|
-
|
|
26455
|
-
currentDrawdown = Math.abs(peak);
|
|
26456
|
-
if (currentDrawdown > maxDD) {
|
|
26457
|
-
maxDD = currentDrawdown;
|
|
26458
|
-
}
|
|
26825
|
+
for (let i = signals.length - 1; i >= 0; i--) {
|
|
26826
|
+
equity *= 1 + signals[i].pnl.pnlPercentage / 100;
|
|
26827
|
+
if (equity <= 0) {
|
|
26828
|
+
maxDD = 100;
|
|
26829
|
+
blown = true;
|
|
26830
|
+
break;
|
|
26459
26831
|
}
|
|
26832
|
+
if (equity > peak)
|
|
26833
|
+
peak = equity;
|
|
26834
|
+
const dd = (peak - equity) / peak * 100;
|
|
26835
|
+
if (dd > maxDD)
|
|
26836
|
+
maxDD = dd;
|
|
26460
26837
|
}
|
|
26461
26838
|
maxDrawdown = maxDD;
|
|
26839
|
+
equityFinal = blown ? 0 : equity;
|
|
26462
26840
|
}
|
|
26463
26841
|
// Calculate Profit Factor
|
|
26464
26842
|
let profitFactor = null;
|
|
@@ -26469,7 +26847,9 @@ class HeatmapStorage {
|
|
|
26469
26847
|
const sumLosses = Math.abs(signals
|
|
26470
26848
|
.filter((s) => s.pnl.pnlPercentage < 0)
|
|
26471
26849
|
.reduce((acc, s) => acc + s.pnl.pnlPercentage, 0));
|
|
26472
|
-
|
|
26850
|
+
// STDDEV_EPSILON guard — float-artifact losses (≈1e-15) would otherwise
|
|
26851
|
+
// produce spurious astronomical profitFactor (≈1e14).
|
|
26852
|
+
if (sumLosses > STDDEV_EPSILON) {
|
|
26473
26853
|
profitFactor = sumWins / sumLosses;
|
|
26474
26854
|
}
|
|
26475
26855
|
}
|
|
@@ -26509,45 +26889,110 @@ class HeatmapStorage {
|
|
|
26509
26889
|
}
|
|
26510
26890
|
}
|
|
26511
26891
|
}
|
|
26512
|
-
//
|
|
26892
|
+
// Expectancy — probabilities from observed win/loss counts (break-evens contribute 0).
|
|
26513
26893
|
let expectancy = null;
|
|
26514
|
-
if (
|
|
26515
|
-
const
|
|
26516
|
-
|
|
26894
|
+
if (totalTrades > 0 && avgWin !== null && avgLoss !== null) {
|
|
26895
|
+
const winProb = winCount / totalTrades;
|
|
26896
|
+
const lossProb = lossCount / totalTrades;
|
|
26897
|
+
expectancy = winProb * avgWin + lossProb * avgLoss;
|
|
26898
|
+
}
|
|
26899
|
+
else if (totalTrades > 0 && avgWin !== null && avgLoss === null) {
|
|
26900
|
+
// No losing trades — expectancy is just average win frequency × avgWin
|
|
26901
|
+
expectancy = (winCount / totalTrades) * avgWin;
|
|
26517
26902
|
}
|
|
26518
|
-
|
|
26903
|
+
else if (totalTrades > 0 && avgWin === null && avgLoss !== null) {
|
|
26904
|
+
expectancy = (lossCount / totalTrades) * avgLoss;
|
|
26905
|
+
}
|
|
26906
|
+
// Average only over signals that have the value — do not dilute the mean with zeros.
|
|
26519
26907
|
let avgPeakPnl = null;
|
|
26520
26908
|
let avgFallPnl = null;
|
|
26521
26909
|
if (signals.length > 0) {
|
|
26522
|
-
|
|
26523
|
-
|
|
26910
|
+
const peakValues = signals
|
|
26911
|
+
.map((s) => s.signal.peakProfit?.pnlPercentage)
|
|
26912
|
+
.filter((v) => typeof v === "number");
|
|
26913
|
+
const fallValues = signals
|
|
26914
|
+
.map((s) => s.signal.maxDrawdown?.pnlPercentage)
|
|
26915
|
+
.filter((v) => typeof v === "number");
|
|
26916
|
+
avgPeakPnl = peakValues.length > 0
|
|
26917
|
+
? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
|
|
26918
|
+
: null;
|
|
26919
|
+
avgFallPnl = fallValues.length > 0
|
|
26920
|
+
? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
|
|
26921
|
+
: null;
|
|
26524
26922
|
}
|
|
26525
|
-
//
|
|
26526
|
-
|
|
26527
|
-
//
|
|
26923
|
+
// Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
|
|
26924
|
+
// downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
|
|
26925
|
+
// so the numerator reduces to avgPnl and the squared term to r² for r < 0.
|
|
26926
|
+
// Dividing by N_total (not N_negative) properly penalises strategies with frequent
|
|
26927
|
+
// losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
|
|
26928
|
+
// strategies.
|
|
26528
26929
|
let sortinoRatio = null;
|
|
26529
|
-
if (
|
|
26530
|
-
const
|
|
26531
|
-
|
|
26532
|
-
|
|
26533
|
-
|
|
26534
|
-
|
|
26535
|
-
|
|
26536
|
-
|
|
26537
|
-
|
|
26538
|
-
|
|
26539
|
-
|
|
26540
|
-
|
|
26541
|
-
|
|
26542
|
-
|
|
26543
|
-
|
|
26544
|
-
|
|
26930
|
+
if (canComputeRatios && avgPnl !== null) {
|
|
26931
|
+
const negativeReturns = signals
|
|
26932
|
+
.map((s) => s.pnl.pnlPercentage)
|
|
26933
|
+
.filter((r) => r < 0);
|
|
26934
|
+
if (negativeReturns.length > 0) {
|
|
26935
|
+
const downsideVariance = negativeReturns.reduce((acc, r) => acc + r * r, 0) / signals.length;
|
|
26936
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
26937
|
+
// Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
|
|
26938
|
+
if (downsideDeviation > STDDEV_EPSILON) {
|
|
26939
|
+
sortinoRatio = avgPnl / downsideDeviation;
|
|
26940
|
+
}
|
|
26941
|
+
}
|
|
26942
|
+
}
|
|
26943
|
+
// Expected yearly returns via geometric mean of equity curve.
|
|
26944
|
+
// equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag.
|
|
26945
|
+
// Gated by sample size and calendar span; if account blown → full loss.
|
|
26946
|
+
let expectedYearlyReturns = null;
|
|
26947
|
+
let tradesPerYear = null;
|
|
26948
|
+
if (signals.length >= MIN_SIGNALS_FOR_ANNUALIZATION) {
|
|
26949
|
+
let firstPendingAt = Infinity;
|
|
26950
|
+
let lastCloseAt = -Infinity;
|
|
26951
|
+
for (const s of signals) {
|
|
26952
|
+
if (s.signal.pendingAt < firstPendingAt)
|
|
26953
|
+
firstPendingAt = s.signal.pendingAt;
|
|
26954
|
+
if (s.closeTimestamp > lastCloseAt)
|
|
26955
|
+
lastCloseAt = s.closeTimestamp;
|
|
26956
|
+
}
|
|
26957
|
+
const calendarSpanDays = (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24);
|
|
26958
|
+
if (calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS) {
|
|
26959
|
+
// tradesPerYear uses RAW observed frequency — no clipping. If the raw value
|
|
26960
|
+
// exceeds MAX_TRADES_PER_YEAR the sample is too clustered for reliable
|
|
26961
|
+
// annualization, and we leave the annualized metric null instead of silently
|
|
26962
|
+
// understating it with a clipped frequency.
|
|
26963
|
+
const rawTradesPerYear = (signals.length / calendarSpanDays) * 365;
|
|
26964
|
+
if (rawTradesPerYear <= MAX_TRADES_PER_YEAR) {
|
|
26965
|
+
tradesPerYear = rawTradesPerYear;
|
|
26966
|
+
if (blown) {
|
|
26967
|
+
expectedYearlyReturns = -100;
|
|
26968
|
+
}
|
|
26969
|
+
else {
|
|
26970
|
+
// If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS, leave null rather than
|
|
26971
|
+
// show the cap — capped numbers mislead users into trusting them.
|
|
26972
|
+
const raw = (Math.pow(equityFinal, tradesPerYear / signals.length) - 1) * 100;
|
|
26973
|
+
expectedYearlyReturns = Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS ? null : raw;
|
|
26974
|
+
}
|
|
26975
|
+
}
|
|
26976
|
+
}
|
|
26545
26977
|
}
|
|
26978
|
+
// Calmar = annualized return / equity-curve max drawdown, capped at ±MAX_CALMAR_RATIO.
|
|
26979
|
+
// Recovery Factor uses the compounded total return (equityFinal-1)*100, not arithmetic
|
|
26980
|
+
// totalPnl — denominator is compounded so numerator must match. Null when account blown.
|
|
26546
26981
|
let calmarRatio = null;
|
|
26547
26982
|
let recoveryFactor = null;
|
|
26548
|
-
if (
|
|
26549
|
-
|
|
26550
|
-
|
|
26983
|
+
if (maxDrawdown !== null && maxDrawdown > 0) {
|
|
26984
|
+
if (expectedYearlyReturns !== null) {
|
|
26985
|
+
const raw = expectedYearlyReturns / maxDrawdown;
|
|
26986
|
+
calmarRatio = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, raw));
|
|
26987
|
+
}
|
|
26988
|
+
if (!blown && canComputeRatios) {
|
|
26989
|
+
// Gated below MIN_SIGNALS_FOR_RATIOS like Sharpe — a Recovery Factor on
|
|
26990
|
+
// a handful of trades is statistically meaningless, so don't surface it
|
|
26991
|
+
// per-symbol while Sharpe is N/A.
|
|
26992
|
+
// Same MAX_CALMAR_RATIO clamp as Calmar — both compounded-profit/DD ratios.
|
|
26993
|
+
const rawRec = ((equityFinal - 1) * 100) / maxDrawdown;
|
|
26994
|
+
recoveryFactor = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawRec));
|
|
26995
|
+
}
|
|
26551
26996
|
}
|
|
26552
26997
|
// Apply safe math checks
|
|
26553
26998
|
if (isUnsafe(winRate))
|
|
@@ -26612,12 +27057,18 @@ class HeatmapStorage {
|
|
|
26612
27057
|
* 2. Sorts symbols by `sharpeRatio` descending — best performers first,
|
|
26613
27058
|
* symbols with `null` sharpeRatio placed at the end.
|
|
26614
27059
|
* 3. Computes portfolio-wide aggregates:
|
|
26615
|
-
* - `portfolioTotalPnl` — sum of
|
|
26616
|
-
*
|
|
26617
|
-
*
|
|
26618
|
-
*
|
|
26619
|
-
*
|
|
26620
|
-
*
|
|
27060
|
+
* - `portfolioTotalPnl` — sum of per-symbol `totalPnl` values, skipping `null` entries
|
|
27061
|
+
* (so a symbol with no data does not silently contribute 0). If every symbol's
|
|
27062
|
+
* `totalPnl` is null, the portfolio value is null.
|
|
27063
|
+
* - `portfolioTotalTrades` — sum of per-symbol `totalTrades`
|
|
27064
|
+
* - `portfolioSharpeRatio` — POOLED Sharpe over all trades across symbols (sample
|
|
27065
|
+
* stddev, N-1). NOT a Markowitz portfolio Sharpe — ignores cross-symbol
|
|
27066
|
+
* correlations and capital allocation. Rendered as "Pooled Sharpe" in the report.
|
|
27067
|
+
* Gated by `MIN_SIGNALS_FOR_RATIOS` on the pooled count.
|
|
27068
|
+
* - `portfolioAvgPeakPnl` / `portfolioAvgFallPnl` — trade-count-weighted means
|
|
27069
|
+
* over symbols that have non-null values.
|
|
27070
|
+
*
|
|
27071
|
+
* @returns Promise resolving to `HeatmapStatisticsModel`
|
|
26621
27072
|
*/
|
|
26622
27073
|
async getData() {
|
|
26623
27074
|
const symbols = [];
|
|
@@ -26636,31 +27087,53 @@ class HeatmapStorage {
|
|
|
26636
27087
|
return -1;
|
|
26637
27088
|
return b.sharpeRatio - a.sharpeRatio;
|
|
26638
27089
|
});
|
|
26639
|
-
//
|
|
27090
|
+
// Portfolio totals — sum only over symbols with non-null totalPnl. `s.totalPnl || 0`
|
|
27091
|
+
// would silently treat a missing value as zero and hide that some symbols had no data.
|
|
26640
27092
|
const totalSymbols = symbols.length;
|
|
26641
27093
|
let portfolioTotalPnl = null;
|
|
26642
27094
|
let portfolioTotalTrades = 0;
|
|
26643
27095
|
if (symbols.length > 0) {
|
|
26644
|
-
|
|
27096
|
+
const validTotalPnls = symbols.filter((s) => s.totalPnl !== null);
|
|
27097
|
+
portfolioTotalPnl = validTotalPnls.length > 0
|
|
27098
|
+
? validTotalPnls.reduce((acc, s) => acc + s.totalPnl, 0)
|
|
27099
|
+
: null;
|
|
26645
27100
|
portfolioTotalTrades = symbols.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
26646
27101
|
}
|
|
26647
|
-
//
|
|
27102
|
+
// Pooled Sharpe over all returns across symbols. NOTE: this is NOT a Markowitz
|
|
27103
|
+
// portfolio Sharpe — it ignores cross-symbol correlations and treats trades as a
|
|
27104
|
+
// single pooled sample. Gated by MIN_SIGNALS_FOR_RATIOS so a 2-trade pool cannot
|
|
27105
|
+
// produce a noisy ±Sharpe.
|
|
26648
27106
|
let portfolioSharpeRatio = null;
|
|
26649
|
-
const
|
|
26650
|
-
|
|
26651
|
-
|
|
26652
|
-
|
|
27107
|
+
const allReturns = [];
|
|
27108
|
+
for (const signals of this.symbolData.values()) {
|
|
27109
|
+
for (const s of signals) {
|
|
27110
|
+
allReturns.push(s.pnl.pnlPercentage);
|
|
27111
|
+
}
|
|
27112
|
+
}
|
|
27113
|
+
if (allReturns.length >= MIN_SIGNALS_FOR_RATIOS) {
|
|
27114
|
+
const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
|
|
27115
|
+
const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
|
|
27116
|
+
(allReturns.length - 1);
|
|
27117
|
+
const portfolioStdDev = Math.sqrt(portfolioVariance);
|
|
27118
|
+
// STDDEV_EPSILON guard — same protection as per-symbol Sharpe.
|
|
27119
|
+
if (portfolioStdDev > STDDEV_EPSILON) {
|
|
27120
|
+
portfolioSharpeRatio = portfolioAvg / portfolioStdDev;
|
|
27121
|
+
}
|
|
26653
27122
|
}
|
|
26654
|
-
//
|
|
27123
|
+
// Portfolio-wide weighted average peak/fall PNL. Denominator must include only
|
|
27124
|
+
// symbols that contributed a value — otherwise trade-count-weighted mean is diluted
|
|
27125
|
+
// by symbols without the metric.
|
|
26655
27126
|
let portfolioAvgPeakPnl = null;
|
|
26656
27127
|
let portfolioAvgFallPnl = null;
|
|
26657
27128
|
const validPeak = symbols.filter((s) => s.avgPeakPnl !== null);
|
|
26658
27129
|
const validFall = symbols.filter((s) => s.avgFallPnl !== null);
|
|
26659
|
-
|
|
26660
|
-
|
|
27130
|
+
const peakTradesTotal = validPeak.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
27131
|
+
const fallTradesTotal = validFall.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
27132
|
+
if (validPeak.length > 0 && peakTradesTotal > 0) {
|
|
27133
|
+
portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / peakTradesTotal;
|
|
26661
27134
|
}
|
|
26662
|
-
if (validFall.length > 0 &&
|
|
26663
|
-
portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) /
|
|
27135
|
+
if (validFall.length > 0 && fallTradesTotal > 0) {
|
|
27136
|
+
portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / fallTradesTotal;
|
|
26664
27137
|
}
|
|
26665
27138
|
// Apply safe math
|
|
26666
27139
|
if (isUnsafe(portfolioTotalPnl))
|
|
@@ -26688,7 +27161,7 @@ class HeatmapStorage {
|
|
|
26688
27161
|
* ```
|
|
26689
27162
|
* # Portfolio Heatmap: {strategyName}
|
|
26690
27163
|
*
|
|
26691
|
-
* **Total Symbols:** N | **Portfolio PNL:** X% | **
|
|
27164
|
+
* **Total Symbols:** N | **Portfolio PNL:** X% | **Pooled Sharpe:** Y | **Total Trades:** Z
|
|
26692
27165
|
*
|
|
26693
27166
|
* | col1 | col2 | ... |
|
|
26694
27167
|
* | --- | --- | ... |
|
|
@@ -26727,18 +27200,21 @@ class HeatmapStorage {
|
|
|
26727
27200
|
return [
|
|
26728
27201
|
`# Portfolio Heatmap: ${strategyName}`,
|
|
26729
27202
|
"",
|
|
26730
|
-
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"} | **
|
|
27203
|
+
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"} | **Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
|
|
26731
27204
|
"",
|
|
26732
27205
|
table,
|
|
26733
27206
|
"",
|
|
26734
27207
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
27208
|
+
`*Pooled Sharpe: Sharpe computed over all trades across symbols treated as one sample. NOT a Markowitz portfolio Sharpe — ignores cross-symbol correlations and capital allocation. N/A unless ≥${MIN_SIGNALS_FOR_RATIOS} pooled trades.*`,
|
|
26735
27209
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals per symbol.*`,
|
|
26736
|
-
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
27210
|
+
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
|
|
26737
27211
|
`*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
|
|
26738
27212
|
`*Profit Factor: below 1.0 means strategy is losing overall. Above 1.5 is considered good.*`,
|
|
26739
|
-
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong.
|
|
26740
|
-
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
|
|
26741
|
-
`*All metrics require 100+ signals per symbol to be statistically reliable.
|
|
27213
|
+
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals per symbol and span ≥${MIN_CALENDAR_SPAN_DAYS} days. Capped at ±${MAX_CALMAR_RATIO}.*`,
|
|
27214
|
+
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
|
|
27215
|
+
`*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
|
|
27216
|
+
`*IMPORTANT: Per-symbol equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). If your strategy risks X% of capital per trade, the realized return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
|
|
27217
|
+
`*Negative values for Sharpe / Sortino / Calmar / Recovery indicate a losing symbol (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
|
|
26742
27218
|
].join("\n");
|
|
26743
27219
|
}
|
|
26744
27220
|
/**
|
|
@@ -26933,7 +27409,7 @@ class HeatMarkdownService {
|
|
|
26933
27409
|
* console.log(markdown);
|
|
26934
27410
|
* // # Portfolio Heatmap: my-strategy
|
|
26935
27411
|
* //
|
|
26936
|
-
* // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **
|
|
27412
|
+
* // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Pooled Sharpe:** 1.85 | **Total Trades:** 120
|
|
26937
27413
|
* //
|
|
26938
27414
|
* // | Symbol | Total PNL | Sharpe | Max DD | Trades |
|
|
26939
27415
|
* // | --- | --- | --- | --- | --- |
|
|
@@ -53928,7 +54404,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY = "ReportUtils.useDummy";
|
|
|
53928
54404
|
const REPORT_UTILS_METHOD_NAME_USE_JSONL = "ReportUtils.useJsonl";
|
|
53929
54405
|
const REPORT_UTILS_METHOD_NAME_CLEAR = "ReportUtils.clear";
|
|
53930
54406
|
/** Logger service injected as DI singleton */
|
|
53931
|
-
const LOGGER_SERVICE$
|
|
54407
|
+
const LOGGER_SERVICE$3 = new LoggerService();
|
|
53932
54408
|
/**
|
|
53933
54409
|
* Default configuration that enables all report services.
|
|
53934
54410
|
* Used when no specific configuration is provided to enable().
|
|
@@ -53985,7 +54461,7 @@ class ReportUtils {
|
|
|
53985
54461
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
53986
54462
|
*/
|
|
53987
54463
|
this.enable = singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
|
|
53988
|
-
LOGGER_SERVICE$
|
|
54464
|
+
LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
|
|
53989
54465
|
backtest: bt,
|
|
53990
54466
|
breakeven,
|
|
53991
54467
|
heat,
|
|
@@ -54080,7 +54556,7 @@ class ReportUtils {
|
|
|
54080
54556
|
* ```
|
|
54081
54557
|
*/
|
|
54082
54558
|
this.disable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
|
|
54083
|
-
LOGGER_SERVICE$
|
|
54559
|
+
LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
|
|
54084
54560
|
backtest: bt,
|
|
54085
54561
|
breakeven,
|
|
54086
54562
|
heat,
|
|
@@ -54160,7 +54636,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54160
54636
|
* @param Ctor - Constructor for report storage adapter
|
|
54161
54637
|
*/
|
|
54162
54638
|
useReportAdapter(Ctor) {
|
|
54163
|
-
LOGGER_SERVICE$
|
|
54639
|
+
LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
|
|
54164
54640
|
ReportWriter.useReportAdapter(Ctor);
|
|
54165
54641
|
}
|
|
54166
54642
|
/**
|
|
@@ -54169,7 +54645,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54169
54645
|
* so new storage instances are created with the updated base path.
|
|
54170
54646
|
*/
|
|
54171
54647
|
clear() {
|
|
54172
|
-
LOGGER_SERVICE$
|
|
54648
|
+
LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_CLEAR);
|
|
54173
54649
|
ReportWriter.clear();
|
|
54174
54650
|
}
|
|
54175
54651
|
/**
|
|
@@ -54177,7 +54653,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54177
54653
|
* All future report writes will be no-ops.
|
|
54178
54654
|
*/
|
|
54179
54655
|
useDummy() {
|
|
54180
|
-
LOGGER_SERVICE$
|
|
54656
|
+
LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
|
|
54181
54657
|
ReportWriter.useDummy();
|
|
54182
54658
|
}
|
|
54183
54659
|
/**
|
|
@@ -54185,7 +54661,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54185
54661
|
* All future report writes will use JSONL storage.
|
|
54186
54662
|
*/
|
|
54187
54663
|
useJsonl() {
|
|
54188
|
-
LOGGER_SERVICE$
|
|
54664
|
+
LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
|
|
54189
54665
|
ReportWriter.useJsonl();
|
|
54190
54666
|
}
|
|
54191
54667
|
}
|
|
@@ -54203,7 +54679,7 @@ const MARKDOWN_METHOD_NAME_USE_JSONL = "MarkdownAdapter.useJsonl";
|
|
|
54203
54679
|
const MARKDOWN_METHOD_NAME_USE_DUMMY = "MarkdownAdapter.useDummy";
|
|
54204
54680
|
const MARKDOWN_METHOD_NAME_CLEAR = "MarkdownAdapter.clear";
|
|
54205
54681
|
/** Logger service injected as DI singleton */
|
|
54206
|
-
const LOGGER_SERVICE$
|
|
54682
|
+
const LOGGER_SERVICE$2 = new LoggerService();
|
|
54207
54683
|
/**
|
|
54208
54684
|
* Default configuration that enables all markdown services.
|
|
54209
54685
|
* Used when no specific configuration is provided to `enable()`.
|
|
@@ -54260,7 +54736,7 @@ class MarkdownUtils {
|
|
|
54260
54736
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
54261
54737
|
*/
|
|
54262
54738
|
this.enable = singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
|
|
54263
|
-
LOGGER_SERVICE$
|
|
54739
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_ENABLE, {
|
|
54264
54740
|
backtest: bt,
|
|
54265
54741
|
breakeven,
|
|
54266
54742
|
heat,
|
|
@@ -54357,7 +54833,7 @@ class MarkdownUtils {
|
|
|
54357
54833
|
* ```
|
|
54358
54834
|
*/
|
|
54359
54835
|
this.disable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, strategy = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
|
|
54360
|
-
LOGGER_SERVICE$
|
|
54836
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_DISABLE, {
|
|
54361
54837
|
backtest: bt,
|
|
54362
54838
|
breakeven,
|
|
54363
54839
|
heat,
|
|
@@ -54443,7 +54919,7 @@ class MarkdownUtils {
|
|
|
54443
54919
|
* @param config.max_drawdown - Clear max drawdown report data
|
|
54444
54920
|
*/
|
|
54445
54921
|
this.clear = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, strategy = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
|
|
54446
|
-
LOGGER_SERVICE$
|
|
54922
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_CLEAR, {
|
|
54447
54923
|
backtest: bt,
|
|
54448
54924
|
breakeven,
|
|
54449
54925
|
heat,
|
|
@@ -54518,7 +54994,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54518
54994
|
* @param Ctor - Constructor for markdown storage adapter
|
|
54519
54995
|
*/
|
|
54520
54996
|
useMarkdownAdapter(Ctor) {
|
|
54521
|
-
LOGGER_SERVICE$
|
|
54997
|
+
LOGGER_SERVICE$2.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
|
|
54522
54998
|
return MarkdownWriter.useMarkdownAdapter(Ctor);
|
|
54523
54999
|
}
|
|
54524
55000
|
/**
|
|
@@ -54527,7 +55003,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54527
55003
|
* Each dump creates a separate .md file.
|
|
54528
55004
|
*/
|
|
54529
55005
|
useMd() {
|
|
54530
|
-
LOGGER_SERVICE$
|
|
55006
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_MD);
|
|
54531
55007
|
MarkdownWriter.useMd();
|
|
54532
55008
|
}
|
|
54533
55009
|
/**
|
|
@@ -54536,7 +55012,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54536
55012
|
* All dumps append to a single .jsonl file per markdown type.
|
|
54537
55013
|
*/
|
|
54538
55014
|
useJsonl() {
|
|
54539
|
-
LOGGER_SERVICE$
|
|
55015
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_JSONL);
|
|
54540
55016
|
MarkdownWriter.useJsonl();
|
|
54541
55017
|
}
|
|
54542
55018
|
/**
|
|
@@ -54544,7 +55020,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54544
55020
|
* All future markdown writes will be no-ops.
|
|
54545
55021
|
*/
|
|
54546
55022
|
useDummy() {
|
|
54547
|
-
LOGGER_SERVICE$
|
|
55023
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_DUMMY);
|
|
54548
55024
|
MarkdownWriter.useDummy();
|
|
54549
55025
|
}
|
|
54550
55026
|
}
|
|
@@ -63233,6 +63709,503 @@ class IntervalUtils {
|
|
|
63233
63709
|
*/
|
|
63234
63710
|
const Interval = new IntervalUtils();
|
|
63235
63711
|
|
|
63712
|
+
const CRON_METHOD_NAME_REGISTER = "CronUtils.register";
|
|
63713
|
+
const CRON_METHOD_NAME_UNREGISTER = "CronUtils.unregister";
|
|
63714
|
+
const CRON_METHOD_NAME_CLEAR = "CronUtils.clear";
|
|
63715
|
+
const CRON_METHOD_NAME_TICK = "CronUtils._tick";
|
|
63716
|
+
const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
|
|
63717
|
+
const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
|
|
63718
|
+
const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
|
|
63719
|
+
/**
|
|
63720
|
+
* Local logger instance.
|
|
63721
|
+
*
|
|
63722
|
+
* Created directly rather than resolved from the DI container so that
|
|
63723
|
+
* `CronUtils` has no compile-time dependency on the rest of the framework
|
|
63724
|
+
* being bootstrapped — `Cron` can be imported and used in isolation.
|
|
63725
|
+
*/
|
|
63726
|
+
const LOGGER_SERVICE$1 = new LoggerService();
|
|
63727
|
+
/**
|
|
63728
|
+
* Utility class for registering periodic tasks that fire on candle-interval
|
|
63729
|
+
* boundaries of the virtual time produced by parallel backtests.
|
|
63730
|
+
*
|
|
63731
|
+
* Exported as singleton instance `Cron` for convenient usage.
|
|
63732
|
+
*
|
|
63733
|
+
* Key property — **singleshot coordination across parallel backtests**:
|
|
63734
|
+
* when several `Backtest.background(symbol, ...)` runs hit the same aligned
|
|
63735
|
+
* boundary concurrently, the handler is invoked exactly once. Every parallel
|
|
63736
|
+
* `tick` for that boundary awaits the same in-flight promise and is released
|
|
63737
|
+
* together when the promise settles. After settlement the slot is cleared and
|
|
63738
|
+
* the next boundary produces a fresh promise.
|
|
63739
|
+
*
|
|
63740
|
+
* Typical wiring:
|
|
63741
|
+
*
|
|
63742
|
+
* @example
|
|
63743
|
+
* ```typescript
|
|
63744
|
+
* import { Cron, Backtest } from "backtest-kit";
|
|
63745
|
+
*
|
|
63746
|
+
* Cron.register({
|
|
63747
|
+
* name: "tg-signal-parser",
|
|
63748
|
+
* interval: "1h",
|
|
63749
|
+
* handler: async (symbol, when, backtest) => {
|
|
63750
|
+
* await parseTelegramSignalsToMongo(when);
|
|
63751
|
+
* },
|
|
63752
|
+
* });
|
|
63753
|
+
*
|
|
63754
|
+
* // Subscribe Cron to the engine's lifecycle subjects (beforeStart,
|
|
63755
|
+
* // idlePing, activePing, schedulePing) once at startup. After this every
|
|
63756
|
+
* // strategy tick is forwarded into Cron automatically.
|
|
63757
|
+
* Cron.enable();
|
|
63758
|
+
*
|
|
63759
|
+
* for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "TRXUSDT"]) {
|
|
63760
|
+
* Backtest.background(symbol, { strategyName, exchangeName, frameName });
|
|
63761
|
+
* }
|
|
63762
|
+
*
|
|
63763
|
+
* // On shutdown:
|
|
63764
|
+
* // Cron.disable();
|
|
63765
|
+
* ```
|
|
63766
|
+
*/
|
|
63767
|
+
class CronUtils {
|
|
63768
|
+
constructor() {
|
|
63769
|
+
/**
|
|
63770
|
+
* Registered entries by `name`.
|
|
63771
|
+
*
|
|
63772
|
+
* Each record carries a monotonically increasing `generation` counter that
|
|
63773
|
+
* is bumped on every `register(entry)` call for the same name. The
|
|
63774
|
+
* generation participates in `firedKey` so writes from a still-in-flight
|
|
63775
|
+
* handler of a previous incarnation cannot poison `_firedOnce` for the
|
|
63776
|
+
* current incarnation — their key has a different generation suffix and
|
|
63777
|
+
* is simply ignored on lookup.
|
|
63778
|
+
*/
|
|
63779
|
+
this._entries = new Map();
|
|
63780
|
+
/** Monotonic counter used to mint new entry generations on `register`. */
|
|
63781
|
+
this._generationCounter = 0;
|
|
63782
|
+
/**
|
|
63783
|
+
* In-flight handler slots.
|
|
63784
|
+
*
|
|
63785
|
+
* Slot key shape (always includes the generation suffix `:g${generation}`;
|
|
63786
|
+
* the `:${symbol}` scope is present only in fan-out mode):
|
|
63787
|
+
* - Periodic global: `${name}:${alignedMs}:g${generation}`.
|
|
63788
|
+
* - Periodic fan-out: `${name}:${alignedMs}:${symbol}:g${generation}`.
|
|
63789
|
+
* - Fire-once global: `${name}:once:g${generation}`.
|
|
63790
|
+
* - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
|
|
63791
|
+
*
|
|
63792
|
+
* Value is the shared in-flight handler promise. Every parallel `tick` for
|
|
63793
|
+
* the same slot key awaits this exact promise (mutex semantics) and is
|
|
63794
|
+
* released together when it settles. `_inFlight` is owned exclusively by
|
|
63795
|
+
* `_runEntry` — `clear()` does **not** touch it, so the singleshot promise
|
|
63796
|
+
* survives concurrent `clear` calls and continues to coordinate parallel
|
|
63797
|
+
* ticks until it settles.
|
|
63798
|
+
*/
|
|
63799
|
+
this._inFlight = new Map();
|
|
63800
|
+
/**
|
|
63801
|
+
* Keys of fire-once entries whose handler has already settled successfully.
|
|
63802
|
+
*
|
|
63803
|
+
* Key shape (always includes the entry generation suffix `:g${generation}`):
|
|
63804
|
+
* - Global fire-once: `${name}:g${generation}`.
|
|
63805
|
+
* - Fan-out fire-once: `${name}:${symbol}:g${generation}` — one entry per
|
|
63806
|
+
* whitelisted symbol.
|
|
63807
|
+
*
|
|
63808
|
+
* The generation suffix isolates incarnations of the same `name`: writes
|
|
63809
|
+
* landing from a still-in-flight handler of a previous `register()` carry
|
|
63810
|
+
* the old generation and are never matched by the new entry's lookup.
|
|
63811
|
+
* Stale entries are pruned by `_clearFiredOnceFor` on `register`/`unregister`
|
|
63812
|
+
* and wiped by `clear()`.
|
|
63813
|
+
*
|
|
63814
|
+
* Looked up by `_tick` to decide whether to skip; written by `_runEntry`
|
|
63815
|
+
* on successful settle.
|
|
63816
|
+
*/
|
|
63817
|
+
this._firedOnce = new Set();
|
|
63818
|
+
/**
|
|
63819
|
+
* Register a periodic cron entry.
|
|
63820
|
+
*
|
|
63821
|
+
* Idempotent on `name`: re-registering the same name replaces the previous
|
|
63822
|
+
* entry (interval/symbols/handler can all change). Re-registration does
|
|
63823
|
+
* **not** clear in-flight promises — entries still resolving complete with
|
|
63824
|
+
* the previous handler.
|
|
63825
|
+
*
|
|
63826
|
+
* @param entry - Entry configuration; see {@link CronEntry}.
|
|
63827
|
+
* @returns Disposer function — call it to unregister the entry.
|
|
63828
|
+
*
|
|
63829
|
+
* @example
|
|
63830
|
+
* ```typescript
|
|
63831
|
+
* const dispose = Cron.register({
|
|
63832
|
+
* name: "fetch-funding",
|
|
63833
|
+
* interval: "8h",
|
|
63834
|
+
* symbols: ["BTCUSDT", "ETHUSDT"],
|
|
63835
|
+
* handler: async (symbol, when, backtest) => { ... },
|
|
63836
|
+
* });
|
|
63837
|
+
* // Later:
|
|
63838
|
+
* dispose();
|
|
63839
|
+
* ```
|
|
63840
|
+
*/
|
|
63841
|
+
this.register = (entry) => {
|
|
63842
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_REGISTER, {
|
|
63843
|
+
name: entry.name,
|
|
63844
|
+
interval: entry.interval,
|
|
63845
|
+
symbols: entry.symbols,
|
|
63846
|
+
});
|
|
63847
|
+
if (!entry.name) {
|
|
63848
|
+
throw new Error("CronUtils.register requires a non-empty name");
|
|
63849
|
+
}
|
|
63850
|
+
if (entry.name.includes(":")) {
|
|
63851
|
+
throw new Error(`CronUtils.register: name must not contain ':' (got "${entry.name}"). ` +
|
|
63852
|
+
`':' is reserved as the segment separator in slot keys.`);
|
|
63853
|
+
}
|
|
63854
|
+
if (entry.symbols) {
|
|
63855
|
+
for (const symbol of entry.symbols) {
|
|
63856
|
+
if (symbol.includes(":")) {
|
|
63857
|
+
throw new Error(`CronUtils.register: symbols[] entry must not contain ':' (got "${symbol}"). ` +
|
|
63858
|
+
`':' is reserved as the segment separator in slot keys.`);
|
|
63859
|
+
}
|
|
63860
|
+
}
|
|
63861
|
+
}
|
|
63862
|
+
this._clearFiredOnceFor(entry.name);
|
|
63863
|
+
const generation = ++this._generationCounter;
|
|
63864
|
+
this._entries.set(entry.name, { entry, generation });
|
|
63865
|
+
return () => this.unregister(entry.name);
|
|
63866
|
+
};
|
|
63867
|
+
/**
|
|
63868
|
+
* Remove a registered entry by name.
|
|
63869
|
+
*
|
|
63870
|
+
* Does not cancel handlers already in flight — those resolve on their own
|
|
63871
|
+
* and clear their slot via `.finally()`.
|
|
63872
|
+
*
|
|
63873
|
+
* @param name - Name passed to `register`.
|
|
63874
|
+
*/
|
|
63875
|
+
this.unregister = (name) => {
|
|
63876
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_UNREGISTER, { name });
|
|
63877
|
+
this._entries.delete(name);
|
|
63878
|
+
this._clearFiredOnceFor(name);
|
|
63879
|
+
};
|
|
63880
|
+
/**
|
|
63881
|
+
* Clear fire-once marks so that fire-once entries can fire again.
|
|
63882
|
+
*
|
|
63883
|
+
* Does **not** touch `_inFlight` — that map holds shared in-flight handler
|
|
63884
|
+
* promises through which parallel `tick`s coordinate. Wiping it mid-flight
|
|
63885
|
+
* would let a new `tick` start a second handler for a boundary that's
|
|
63886
|
+
* already running, breaking the singleshot contract.
|
|
63887
|
+
*
|
|
63888
|
+
* Two modes:
|
|
63889
|
+
* - **Per-symbol** (`symbol` provided): clears only fan-out fire-once
|
|
63890
|
+
* marks for that symbol — keys of the shape `${name}:${symbol}:g${gen}`.
|
|
63891
|
+
* Global fire-once marks (`${name}:g${gen}`, no symbol component) are
|
|
63892
|
+
* left intact, since they are not attributable to a single symbol.
|
|
63893
|
+
* Useful for re-arming fan-out fire-once entries when a particular
|
|
63894
|
+
* symbol's run finishes and you want a future re-run to fire again.
|
|
63895
|
+
* - **All** (no argument): wipes every fire-once mark across all entries
|
|
63896
|
+
* and symbols. Registered entries are not removed — use `unregister`
|
|
63897
|
+
* (or the disposer returned by `register`) for that.
|
|
63898
|
+
*
|
|
63899
|
+
* **Race with in-flight handlers.** `_firedOnce` is written in
|
|
63900
|
+
* `_runEntry`'s `.finally()`, which can run *after* a concurrent
|
|
63901
|
+
* `clear()` call. In that case the fire-once mark reappears immediately
|
|
63902
|
+
* after being wiped, and the next tick will treat the entry as already
|
|
63903
|
+
* fired. This is consistent with the singleshot promise itself surviving
|
|
63904
|
+
* `clear()` — the handler is allowed to finish — and the entry's
|
|
63905
|
+
* generation suffix in `firedKey` guarantees the stale mark cannot
|
|
63906
|
+
* outlive a subsequent `register()` of the same name. If you need a hard
|
|
63907
|
+
* re-arm, `unregister` + `register` bumps the generation and makes any
|
|
63908
|
+
* late write a no-op.
|
|
63909
|
+
*
|
|
63910
|
+
* @param symbol - Optional symbol filter; if omitted, clears all fire-once
|
|
63911
|
+
* marks.
|
|
63912
|
+
*/
|
|
63913
|
+
this.clear = (symbol) => {
|
|
63914
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_CLEAR, { symbol });
|
|
63915
|
+
if (!symbol) {
|
|
63916
|
+
this._firedOnce.clear();
|
|
63917
|
+
return;
|
|
63918
|
+
}
|
|
63919
|
+
const symbolSegment = `:${symbol}:`;
|
|
63920
|
+
for (const key of this._firedOnce) {
|
|
63921
|
+
if (key.includes(symbolSegment)) {
|
|
63922
|
+
this._firedOnce.delete(key);
|
|
63923
|
+
}
|
|
63924
|
+
}
|
|
63925
|
+
};
|
|
63926
|
+
/**
|
|
63927
|
+
* Process a virtual-time tick for `symbol` and fire any due cron entries.
|
|
63928
|
+
*
|
|
63929
|
+
* **Private.** Invoked exclusively by the lifecycle bridge installed in
|
|
63930
|
+
* {@link enable} — `beforeStart` / `idlePing` / `activePing` / `schedulePing`
|
|
63931
|
+
* are funneled here through a shared `singlerun` queue, so calls to
|
|
63932
|
+
* `_tick` are serialised end-to-end. Do not call directly.
|
|
63933
|
+
*
|
|
63934
|
+
* Algorithm (per registered entry):
|
|
63935
|
+
* 0. Base-align the incoming `when` down to the 1-minute boundary (`ts`).
|
|
63936
|
+
* Lifecycle subjects may emit with sub-second jitter; rounding here
|
|
63937
|
+
* guarantees that `beforeStart` / `idlePing` / `activePing` /
|
|
63938
|
+
* `schedulePing` for the same virtual minute all hash to the same
|
|
63939
|
+
* slot key.
|
|
63940
|
+
* 1. If `entry.symbols` is non-empty and does not include `symbol`, skip.
|
|
63941
|
+
* 2. Decide scope from `entry.symbols`:
|
|
63942
|
+
* - Empty/undefined → **global** (slot key has no symbol component).
|
|
63943
|
+
* - Non-empty → **fan-out**, slot key carries `:${symbol}` so each
|
|
63944
|
+
* whitelisted symbol gets its own slot and handler invocation.
|
|
63945
|
+
* 3. Append the current entry generation suffix `:g${generation}` to both
|
|
63946
|
+
* slot key and fired-once key. This isolates incarnations of the same
|
|
63947
|
+
* `name`: a `register()` after an in-flight handler bumps the
|
|
63948
|
+
* generation, so the late `_firedOnce` write from the old handler can
|
|
63949
|
+
* never block the new entry.
|
|
63950
|
+
* 4. **Fire-once** (`entry.interval === undefined`):
|
|
63951
|
+
* - If the entry's fired-once key is already in `_firedOnce`, skip.
|
|
63952
|
+
* - Slot key: `${name}:once` (+ scope) (+ gen).
|
|
63953
|
+
* - `aligned` = the 1-minute-aligned `when` from step 0.
|
|
63954
|
+
* 5. **Periodic** (`entry.interval` set):
|
|
63955
|
+
* - Align `when` further to the entry's interval via {@link alignToInterval}.
|
|
63956
|
+
* - If `ts !== alignedMs`, the tick is mid-interval — skip.
|
|
63957
|
+
* (This is the "remainder === 0" boundary check from the spec;
|
|
63958
|
+
* since `ts` is already on the 1-minute boundary, the check is exact
|
|
63959
|
+
* for `1m` and consistent for higher intervals.)
|
|
63960
|
+
* - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
|
|
63961
|
+
* 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
|
|
63962
|
+
* already exists, `await` the same promise. Otherwise invoke
|
|
63963
|
+
* `entry.handler`, store the promise, and `await` it. The slot is
|
|
63964
|
+
* removed in `.finally()` so the next boundary creates a fresh promise;
|
|
63965
|
+
* for fire-once entries the fired-once key is also added to
|
|
63966
|
+
* `_firedOnce` on success so subsequent ticks skip it.
|
|
63967
|
+
*
|
|
63968
|
+
* Errors thrown by `handler` are caught, logged via `console.error`, and
|
|
63969
|
+
* **not** rethrown — a failing handler must not break the per-symbol
|
|
63970
|
+
* tick loop or unblock other parallel backtests with an unhandled
|
|
63971
|
+
* rejection. A failed fire-once handler is **not** marked as fired and
|
|
63972
|
+
* will retry on the next tick.
|
|
63973
|
+
*
|
|
63974
|
+
* Requires active method context and execution context.
|
|
63975
|
+
*
|
|
63976
|
+
* @param symbol - Trading symbol from the current tick.
|
|
63977
|
+
* @param when - Virtual time of the current tick.
|
|
63978
|
+
* @param backtest - `true` for backtest ticks, `false` for live ticks.
|
|
63979
|
+
* Forwarded as the third argument to `entry.handler`. Only the value
|
|
63980
|
+
* from the tick that **opens** a given slot is observed by all parallel
|
|
63981
|
+
* awaiters of that slot.
|
|
63982
|
+
* @throws Error if method or execution context is missing.
|
|
63983
|
+
*/
|
|
63984
|
+
this._tick = async (symbol, when, backtest) => {
|
|
63985
|
+
LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
|
|
63986
|
+
symbol,
|
|
63987
|
+
when,
|
|
63988
|
+
});
|
|
63989
|
+
if (!MethodContextService.hasContext()) {
|
|
63990
|
+
throw new Error("CronUtils _tick requires method context");
|
|
63991
|
+
}
|
|
63992
|
+
if (!ExecutionContextService.hasContext()) {
|
|
63993
|
+
throw new Error("CronUtils _tick requires execution context");
|
|
63994
|
+
}
|
|
63995
|
+
const ts = alignToInterval(when, "1m").getTime();
|
|
63996
|
+
const taskList = [];
|
|
63997
|
+
for (const { entry, generation } of this._entries.values()) {
|
|
63998
|
+
if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
|
|
63999
|
+
continue;
|
|
64000
|
+
}
|
|
64001
|
+
const perSymbol = !!entry.symbols?.length;
|
|
64002
|
+
const scope = perSymbol ? `:${symbol}` : "";
|
|
64003
|
+
const genSuffix = `:g${generation}`;
|
|
64004
|
+
let aligned;
|
|
64005
|
+
let alignedMs;
|
|
64006
|
+
let slotKey;
|
|
64007
|
+
let firedKey;
|
|
64008
|
+
if (entry.interval === undefined) {
|
|
64009
|
+
const onceKey = `${entry.name}${scope}${genSuffix}`;
|
|
64010
|
+
if (this._firedOnce.has(onceKey)) {
|
|
64011
|
+
continue;
|
|
64012
|
+
}
|
|
64013
|
+
aligned = alignToInterval(when, "1m");
|
|
64014
|
+
alignedMs = ts;
|
|
64015
|
+
slotKey = `${entry.name}:once${scope}${genSuffix}`;
|
|
64016
|
+
firedKey = onceKey;
|
|
64017
|
+
}
|
|
64018
|
+
else {
|
|
64019
|
+
aligned = alignToInterval(when, entry.interval);
|
|
64020
|
+
alignedMs = aligned.getTime();
|
|
64021
|
+
if (ts !== alignedMs) {
|
|
64022
|
+
continue;
|
|
64023
|
+
}
|
|
64024
|
+
slotKey = `${entry.name}:${alignedMs}${scope}${genSuffix}`;
|
|
64025
|
+
firedKey = null;
|
|
64026
|
+
}
|
|
64027
|
+
let pending = this._inFlight.get(slotKey);
|
|
64028
|
+
if (!pending) {
|
|
64029
|
+
pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
|
|
64030
|
+
this._inFlight.set(slotKey, pending);
|
|
64031
|
+
}
|
|
64032
|
+
taskList.push(pending);
|
|
64033
|
+
}
|
|
64034
|
+
await Promise.all(taskList);
|
|
64035
|
+
};
|
|
64036
|
+
/**
|
|
64037
|
+
* Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
|
|
64038
|
+
* entries fire automatically — no manual wiring of `listenTickBacktest` /
|
|
64039
|
+
* `listenSchedulePing` etc. needed.
|
|
64040
|
+
*
|
|
64041
|
+
* Subjects funneled into {@link _tick}:
|
|
64042
|
+
* - `beforeStartSubject` — first event of every run.
|
|
64043
|
+
* - `idlePingSubject` — every tick when no signal is pending or scheduled.
|
|
64044
|
+
* - `activePingSubject` — every tick while a pending signal is being monitored.
|
|
64045
|
+
* - `schedulePingSubject` — every tick while a scheduled signal is being monitored.
|
|
64046
|
+
*
|
|
64047
|
+
* All four subjects are subscribed to a single `singlerun`-wrapped
|
|
64048
|
+
* handler that builds `_tick(event.symbol, new Date(event.timestamp),
|
|
64049
|
+
* event.backtest)`. `singlerun` merges the four streams into one serial
|
|
64050
|
+
* queue: at most one `_tick` runs at a time, the next waits. This matters
|
|
64051
|
+
* because the engine can emit `beforeStart` and an immediate `idlePing`
|
|
64052
|
+
* on the very same minute, and concurrent `_tick`s on the same
|
|
64053
|
+
* `(symbol, minute)` would otherwise race to open the same `_inFlight`
|
|
64054
|
+
* slot before either commit. Together these four sources cover every
|
|
64055
|
+
* tick the engine processes for every `(symbol, virtual-minute)` pair
|
|
64056
|
+
* regardless of whether the strategy is idle, active, or scheduled.
|
|
64057
|
+
*
|
|
64058
|
+
* `enable` itself is wrapped in `singleshot`, so calling it repeatedly is
|
|
64059
|
+
* a no-op — subsequent calls return the same disposer. The disposer
|
|
64060
|
+
* unsubscribes from every subject and resets the singleshot so a future
|
|
64061
|
+
* `enable()` can re-subscribe cleanly. Equivalent to the
|
|
64062
|
+
* `RecentAdapter.enable` pattern.
|
|
64063
|
+
*
|
|
64064
|
+
* The `.subscribe` callbacks are synchronous wrappers around the
|
|
64065
|
+
* `singlerun`-async handler; `_tick`'s returned promise is awaited inside
|
|
64066
|
+
* `singlerun` to enforce ordering but not bubbled back to the subject.
|
|
64067
|
+
* Errors are caught and logged inside `_runEntry`.
|
|
64068
|
+
*
|
|
64069
|
+
* @returns Cleanup function that unsubscribes from all four subjects and
|
|
64070
|
+
* resets the singleshot. Idempotent.
|
|
64071
|
+
*
|
|
64072
|
+
* @example
|
|
64073
|
+
* ```typescript
|
|
64074
|
+
* import { Cron } from "backtest-kit";
|
|
64075
|
+
*
|
|
64076
|
+
* Cron.register({ name: "tg-parser", interval: "1h", handler });
|
|
64077
|
+
* Cron.enable(); // wire once at startup
|
|
64078
|
+
* // ... run backtests / live as usual
|
|
64079
|
+
* Cron.disable(); // on shutdown
|
|
64080
|
+
* ```
|
|
64081
|
+
*/
|
|
64082
|
+
this.enable = singleshot(() => {
|
|
64083
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
|
|
64084
|
+
const handleTick = singlerun(async (event) => {
|
|
64085
|
+
return await this._tick(event.symbol, new Date(event.timestamp), event.backtest);
|
|
64086
|
+
});
|
|
64087
|
+
const unBeforeStart = beforeStartSubject.subscribe(handleTick);
|
|
64088
|
+
const unIdlePing = idlePingSubject.subscribe(handleTick);
|
|
64089
|
+
const unActivePing = activePingSubject.subscribe(handleTick);
|
|
64090
|
+
const unSchedulePing = schedulePingSubject.subscribe(handleTick);
|
|
64091
|
+
return compose(() => unBeforeStart(), () => unIdlePing(), () => unActivePing(), () => unSchedulePing(), () => this.enable.clear());
|
|
64092
|
+
});
|
|
64093
|
+
/**
|
|
64094
|
+
* Tear down the lifecycle subscriptions installed by {@link enable}.
|
|
64095
|
+
*
|
|
64096
|
+
* Safe to call multiple times and safe to call before `enable()` — both
|
|
64097
|
+
* are no-ops. Does **not** unregister entries, does **not** touch
|
|
64098
|
+
* `_inFlight`, and does **not** wipe `_firedOnce` (use `unregister` or
|
|
64099
|
+
* `clear()` for those).
|
|
64100
|
+
*/
|
|
64101
|
+
this.disable = () => {
|
|
64102
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISABLE);
|
|
64103
|
+
if (this.enable.hasValue()) {
|
|
64104
|
+
const lastSubscription = this.enable();
|
|
64105
|
+
lastSubscription();
|
|
64106
|
+
}
|
|
64107
|
+
};
|
|
64108
|
+
/**
|
|
64109
|
+
* Hard-reset the entire `Cron` state.
|
|
64110
|
+
*
|
|
64111
|
+
* Performs in order:
|
|
64112
|
+
* 1. {@link disable} — tears down lifecycle subscriptions and resets the
|
|
64113
|
+
* `enable` singleshot so a future `enable()` re-subscribes cleanly.
|
|
64114
|
+
* 2. Wipes `_entries` — every {@link register}'ed entry is forgotten.
|
|
64115
|
+
* Disposers returned by previous `register()` calls become no-ops
|
|
64116
|
+
* (their `unregister(name)` will not find anything to remove).
|
|
64117
|
+
* 3. Wipes `_firedOnce` — all fire-once marks are dropped, so any future
|
|
64118
|
+
* re-registration of the same `name` fires again on the next matching
|
|
64119
|
+
* tick.
|
|
64120
|
+
* 4. Does **not** touch `_inFlight` — in-flight handlers continue to
|
|
64121
|
+
* settle in the background and clear their own slots via `.finally()`.
|
|
64122
|
+
* Their final `_firedOnce.add(firedKey)` writes carry old-generation
|
|
64123
|
+
* keys and are harmless (lookup uses the post-dispose generation).
|
|
64124
|
+
*
|
|
64125
|
+
* Use from a CLI/session teardown when you want to throw away every
|
|
64126
|
+
* registration along with the lifecycle wiring — e.g. between two
|
|
64127
|
+
* independent runner scopes. For "just snap the subscriptions but keep
|
|
64128
|
+
* registrations" use {@link disable} instead; for "just re-arm fire-once
|
|
64129
|
+
* marks" use {@link clear}.
|
|
64130
|
+
*
|
|
64131
|
+
* Idempotent. Safe to call multiple times and safe to call before
|
|
64132
|
+
* `enable()` / without any registrations.
|
|
64133
|
+
*/
|
|
64134
|
+
this.dispose = () => {
|
|
64135
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISPOSE);
|
|
64136
|
+
this.disable();
|
|
64137
|
+
this._entries.clear();
|
|
64138
|
+
this._firedOnce.clear();
|
|
64139
|
+
};
|
|
64140
|
+
}
|
|
64141
|
+
/**
|
|
64142
|
+
* Garbage-collect every `_firedOnce` key that belongs to the entry `name`
|
|
64143
|
+
* (any generation, global or fan-out).
|
|
64144
|
+
*
|
|
64145
|
+
* Called from `register`/`unregister` to free memory; **not** required
|
|
64146
|
+
* for correctness — the generation suffix already isolates re-registrations,
|
|
64147
|
+
* so leftover keys from old generations can never block a new entry.
|
|
64148
|
+
* They just sit unused until they are GC'd here or wiped by `clear()`.
|
|
64149
|
+
*/
|
|
64150
|
+
_clearFiredOnceFor(name) {
|
|
64151
|
+
if (!name) {
|
|
64152
|
+
return;
|
|
64153
|
+
}
|
|
64154
|
+
const prefix = `${name}:`;
|
|
64155
|
+
for (const key of this._firedOnce) {
|
|
64156
|
+
if (key === name || key.startsWith(prefix)) {
|
|
64157
|
+
this._firedOnce.delete(key);
|
|
64158
|
+
}
|
|
64159
|
+
}
|
|
64160
|
+
}
|
|
64161
|
+
/**
|
|
64162
|
+
* Build the singleshot promise for a single in-flight slot.
|
|
64163
|
+
*
|
|
64164
|
+
* Invokes `entry.handler(symbol, aligned, backtest)`, swallows and logs
|
|
64165
|
+
* any error via `console.error`, and clears the `_inFlight` slot
|
|
64166
|
+
* in `.finally()` so the next boundary produces a fresh promise. For
|
|
64167
|
+
* fire-once entries `firedKey` is added to `_firedOnce` on success so
|
|
64168
|
+
* subsequent ticks skip it.
|
|
64169
|
+
*
|
|
64170
|
+
* @param firedKey - Key to add to `_firedOnce` on success, or `null` for
|
|
64171
|
+
* periodic entries (which never populate `_firedOnce`).
|
|
64172
|
+
* @param backtest - Value forwarded as the third handler argument; the
|
|
64173
|
+
* "winner" tick's flag is what all parallel awaiters of this slot see.
|
|
64174
|
+
*/
|
|
64175
|
+
async _runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest) {
|
|
64176
|
+
let failed = false;
|
|
64177
|
+
try {
|
|
64178
|
+
await entry.handler(symbol, aligned, backtest);
|
|
64179
|
+
}
|
|
64180
|
+
catch (err) {
|
|
64181
|
+
failed = true;
|
|
64182
|
+
console.error(`${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`, { symbol, alignedMs, err });
|
|
64183
|
+
}
|
|
64184
|
+
finally {
|
|
64185
|
+
this._inFlight.delete(slotKey);
|
|
64186
|
+
if (!failed && firedKey !== null) {
|
|
64187
|
+
this._firedOnce.add(firedKey);
|
|
64188
|
+
}
|
|
64189
|
+
}
|
|
64190
|
+
}
|
|
64191
|
+
}
|
|
64192
|
+
/**
|
|
64193
|
+
* Singleton instance of {@link CronUtils} for registering periodic tasks
|
|
64194
|
+
* coordinated across parallel `Backtest.background` runs.
|
|
64195
|
+
*
|
|
64196
|
+
* @example
|
|
64197
|
+
* ```typescript
|
|
64198
|
+
* import { Cron } from "backtest-kit";
|
|
64199
|
+
*
|
|
64200
|
+
* Cron.register({
|
|
64201
|
+
* name: "tg-parser",
|
|
64202
|
+
* interval: "1h",
|
|
64203
|
+
* handler: async (symbol, when, backtest) => { ... },
|
|
64204
|
+
* });
|
|
64205
|
+
* ```
|
|
64206
|
+
*/
|
|
64207
|
+
const Cron = new CronUtils();
|
|
64208
|
+
|
|
63236
64209
|
const BREAKEVEN_METHOD_NAME_GET_DATA = "BreakevenUtils.getData";
|
|
63237
64210
|
const BREAKEVEN_METHOD_NAME_GET_REPORT = "BreakevenUtils.getReport";
|
|
63238
64211
|
const BREAKEVEN_METHOD_NAME_DUMP = "BreakevenUtils.dump";
|
|
@@ -64315,7 +65288,7 @@ const validateSignal = (signal, currentPrice) => {
|
|
|
64315
65288
|
}
|
|
64316
65289
|
}
|
|
64317
65290
|
if (errors.length > 0) {
|
|
64318
|
-
console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
65291
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
64319
65292
|
return false;
|
|
64320
65293
|
}
|
|
64321
65294
|
try {
|
|
@@ -64388,9 +65361,9 @@ const validateSignal = (signal, currentPrice) => {
|
|
|
64388
65361
|
}
|
|
64389
65362
|
}
|
|
64390
65363
|
if (errors.length > 0) {
|
|
64391
|
-
console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
65364
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
64392
65365
|
}
|
|
64393
65366
|
return !errors.length;
|
|
64394
65367
|
};
|
|
64395
65368
|
|
|
64396
|
-
export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getRawCandles, getRiskSchema, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };
|
|
65369
|
+
export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getRawCandles, getRiskSchema, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };
|