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.cjs
CHANGED
|
@@ -962,7 +962,7 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
962
962
|
|
|
963
963
|
var _a$3;
|
|
964
964
|
/** Logger service injected as DI singleton */
|
|
965
|
-
const LOGGER_SERVICE$
|
|
965
|
+
const LOGGER_SERVICE$9 = new LoggerService();
|
|
966
966
|
/** Symbol key for the singleshot waitForInit function on PersistBase instances. */
|
|
967
967
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
968
968
|
// Calculate step in milliseconds for candle close time validation
|
|
@@ -1085,7 +1085,7 @@ const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
|
|
|
1085
1085
|
const BASE_UNLINK_RETRY_COUNT = 5;
|
|
1086
1086
|
const BASE_UNLINK_RETRY_DELAY = 1000;
|
|
1087
1087
|
const BASE_WAIT_FOR_INIT_FN = async (self) => {
|
|
1088
|
-
LOGGER_SERVICE$
|
|
1088
|
+
LOGGER_SERVICE$9.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
|
|
1089
1089
|
entityName: self.entityName,
|
|
1090
1090
|
directory: self._directory,
|
|
1091
1091
|
});
|
|
@@ -1143,7 +1143,7 @@ class PersistBase {
|
|
|
1143
1143
|
this.entityName = entityName;
|
|
1144
1144
|
this.baseDir = baseDir;
|
|
1145
1145
|
this[_a$3] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
|
|
1146
|
-
LOGGER_SERVICE$
|
|
1146
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
|
|
1147
1147
|
entityName: this.entityName,
|
|
1148
1148
|
baseDir,
|
|
1149
1149
|
});
|
|
@@ -1159,14 +1159,14 @@ class PersistBase {
|
|
|
1159
1159
|
return path.join(this.baseDir, this.entityName, `${entityId}.json`);
|
|
1160
1160
|
}
|
|
1161
1161
|
async waitForInit(initial) {
|
|
1162
|
-
LOGGER_SERVICE$
|
|
1162
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
|
|
1163
1163
|
entityName: this.entityName,
|
|
1164
1164
|
initial,
|
|
1165
1165
|
});
|
|
1166
1166
|
await this[BASE_WAIT_FOR_INIT_SYMBOL]();
|
|
1167
1167
|
}
|
|
1168
1168
|
async readValue(entityId) {
|
|
1169
|
-
LOGGER_SERVICE$
|
|
1169
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
|
|
1170
1170
|
entityName: this.entityName,
|
|
1171
1171
|
entityId,
|
|
1172
1172
|
});
|
|
@@ -1183,7 +1183,7 @@ class PersistBase {
|
|
|
1183
1183
|
}
|
|
1184
1184
|
}
|
|
1185
1185
|
async hasValue(entityId) {
|
|
1186
|
-
LOGGER_SERVICE$
|
|
1186
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
|
|
1187
1187
|
entityName: this.entityName,
|
|
1188
1188
|
entityId,
|
|
1189
1189
|
});
|
|
@@ -1200,7 +1200,7 @@ class PersistBase {
|
|
|
1200
1200
|
}
|
|
1201
1201
|
}
|
|
1202
1202
|
async writeValue(entityId, entity) {
|
|
1203
|
-
LOGGER_SERVICE$
|
|
1203
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
|
|
1204
1204
|
entityName: this.entityName,
|
|
1205
1205
|
entityId,
|
|
1206
1206
|
});
|
|
@@ -1222,7 +1222,7 @@ class PersistBase {
|
|
|
1222
1222
|
* @throws Error if reading fails
|
|
1223
1223
|
*/
|
|
1224
1224
|
async *keys() {
|
|
1225
|
-
LOGGER_SERVICE$
|
|
1225
|
+
LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
|
|
1226
1226
|
entityName: this.entityName,
|
|
1227
1227
|
});
|
|
1228
1228
|
try {
|
|
@@ -1367,7 +1367,7 @@ class PersistSignalUtils {
|
|
|
1367
1367
|
* @returns Promise resolving to signal or null if none persisted
|
|
1368
1368
|
*/
|
|
1369
1369
|
this.readSignalData = async (symbol, strategyName, exchangeName) => {
|
|
1370
|
-
LOGGER_SERVICE$
|
|
1370
|
+
LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
|
|
1371
1371
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1372
1372
|
const isInitial = !this.getStorage.has(key);
|
|
1373
1373
|
const instance = this.getStorage(symbol, strategyName, exchangeName);
|
|
@@ -1385,7 +1385,7 @@ class PersistSignalUtils {
|
|
|
1385
1385
|
* @returns Promise that resolves when write is complete
|
|
1386
1386
|
*/
|
|
1387
1387
|
this.writeSignalData = async (signalRow, symbol, strategyName, exchangeName) => {
|
|
1388
|
-
LOGGER_SERVICE$
|
|
1388
|
+
LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1389
1389
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1390
1390
|
const isInitial = !this.getStorage.has(key);
|
|
1391
1391
|
const instance = this.getStorage(symbol, strategyName, exchangeName);
|
|
@@ -1400,7 +1400,7 @@ class PersistSignalUtils {
|
|
|
1400
1400
|
* @param Ctor - Custom IPersistSignalInstance constructor
|
|
1401
1401
|
*/
|
|
1402
1402
|
usePersistSignalAdapter(Ctor) {
|
|
1403
|
-
LOGGER_SERVICE$
|
|
1403
|
+
LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
|
|
1404
1404
|
this.PersistSignalInstanceCtor = Ctor;
|
|
1405
1405
|
this.getStorage.clear();
|
|
1406
1406
|
}
|
|
@@ -1409,21 +1409,21 @@ class PersistSignalUtils {
|
|
|
1409
1409
|
* Call when process.cwd() changes between strategy iterations.
|
|
1410
1410
|
*/
|
|
1411
1411
|
clear() {
|
|
1412
|
-
LOGGER_SERVICE$
|
|
1412
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_CLEAR);
|
|
1413
1413
|
this.getStorage.clear();
|
|
1414
1414
|
}
|
|
1415
1415
|
/**
|
|
1416
1416
|
* Switches to the default file-based PersistSignalInstance.
|
|
1417
1417
|
*/
|
|
1418
1418
|
useJson() {
|
|
1419
|
-
LOGGER_SERVICE$
|
|
1419
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
|
|
1420
1420
|
this.usePersistSignalAdapter(PersistSignalInstance);
|
|
1421
1421
|
}
|
|
1422
1422
|
/**
|
|
1423
1423
|
* Switches to PersistSignalDummyInstance (all operations are no-ops).
|
|
1424
1424
|
*/
|
|
1425
1425
|
useDummy() {
|
|
1426
|
-
LOGGER_SERVICE$
|
|
1426
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1427
1427
|
this.usePersistSignalAdapter(PersistSignalDummyInstance);
|
|
1428
1428
|
}
|
|
1429
1429
|
}
|
|
@@ -1563,7 +1563,7 @@ class PersistRiskUtils {
|
|
|
1563
1563
|
* @returns Promise resolving to position entries (empty array if none)
|
|
1564
1564
|
*/
|
|
1565
1565
|
this.readPositionData = async (riskName, exchangeName, when) => {
|
|
1566
|
-
LOGGER_SERVICE$
|
|
1566
|
+
LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
|
|
1567
1567
|
const key = `${riskName}:${exchangeName}`;
|
|
1568
1568
|
const isInitial = !this.getRiskStorage.has(key);
|
|
1569
1569
|
const instance = this.getRiskStorage(riskName, exchangeName);
|
|
@@ -1581,7 +1581,7 @@ class PersistRiskUtils {
|
|
|
1581
1581
|
* @returns Promise that resolves when write is complete
|
|
1582
1582
|
*/
|
|
1583
1583
|
this.writePositionData = async (riskRow, riskName, exchangeName, when) => {
|
|
1584
|
-
LOGGER_SERVICE$
|
|
1584
|
+
LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1585
1585
|
const key = `${riskName}:${exchangeName}`;
|
|
1586
1586
|
const isInitial = !this.getRiskStorage.has(key);
|
|
1587
1587
|
const instance = this.getRiskStorage(riskName, exchangeName);
|
|
@@ -1596,7 +1596,7 @@ class PersistRiskUtils {
|
|
|
1596
1596
|
* @param Ctor - Custom IPersistRiskInstance constructor
|
|
1597
1597
|
*/
|
|
1598
1598
|
usePersistRiskAdapter(Ctor) {
|
|
1599
|
-
LOGGER_SERVICE$
|
|
1599
|
+
LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
|
|
1600
1600
|
this.PersistRiskInstanceCtor = Ctor;
|
|
1601
1601
|
this.getRiskStorage.clear();
|
|
1602
1602
|
}
|
|
@@ -1605,21 +1605,21 @@ class PersistRiskUtils {
|
|
|
1605
1605
|
* Call when process.cwd() changes between strategy iterations.
|
|
1606
1606
|
*/
|
|
1607
1607
|
clear() {
|
|
1608
|
-
LOGGER_SERVICE$
|
|
1608
|
+
LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_CLEAR);
|
|
1609
1609
|
this.getRiskStorage.clear();
|
|
1610
1610
|
}
|
|
1611
1611
|
/**
|
|
1612
1612
|
* Switches to the default file-based PersistRiskInstance.
|
|
1613
1613
|
*/
|
|
1614
1614
|
useJson() {
|
|
1615
|
-
LOGGER_SERVICE$
|
|
1615
|
+
LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
|
|
1616
1616
|
this.usePersistRiskAdapter(PersistRiskInstance);
|
|
1617
1617
|
}
|
|
1618
1618
|
/**
|
|
1619
1619
|
* Switches to PersistRiskDummyInstance (all operations are no-ops).
|
|
1620
1620
|
*/
|
|
1621
1621
|
useDummy() {
|
|
1622
|
-
LOGGER_SERVICE$
|
|
1622
|
+
LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1623
1623
|
this.usePersistRiskAdapter(PersistRiskDummyInstance);
|
|
1624
1624
|
}
|
|
1625
1625
|
}
|
|
@@ -1758,7 +1758,7 @@ class PersistScheduleUtils {
|
|
|
1758
1758
|
* @returns Promise resolving to scheduled signal or null if none persisted
|
|
1759
1759
|
*/
|
|
1760
1760
|
this.readScheduleData = async (symbol, strategyName, exchangeName) => {
|
|
1761
|
-
LOGGER_SERVICE$
|
|
1761
|
+
LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
|
|
1762
1762
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1763
1763
|
const isInitial = !this.getScheduleStorage.has(key);
|
|
1764
1764
|
const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
|
|
@@ -1776,7 +1776,7 @@ class PersistScheduleUtils {
|
|
|
1776
1776
|
* @returns Promise that resolves when write is complete
|
|
1777
1777
|
*/
|
|
1778
1778
|
this.writeScheduleData = async (scheduledSignalRow, symbol, strategyName, exchangeName) => {
|
|
1779
|
-
LOGGER_SERVICE$
|
|
1779
|
+
LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1780
1780
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1781
1781
|
const isInitial = !this.getScheduleStorage.has(key);
|
|
1782
1782
|
const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
|
|
@@ -1791,7 +1791,7 @@ class PersistScheduleUtils {
|
|
|
1791
1791
|
* @param Ctor - Custom IPersistScheduleInstance constructor
|
|
1792
1792
|
*/
|
|
1793
1793
|
usePersistScheduleAdapter(Ctor) {
|
|
1794
|
-
LOGGER_SERVICE$
|
|
1794
|
+
LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
|
|
1795
1795
|
this.PersistScheduleInstanceCtor = Ctor;
|
|
1796
1796
|
this.getScheduleStorage.clear();
|
|
1797
1797
|
}
|
|
@@ -1800,21 +1800,21 @@ class PersistScheduleUtils {
|
|
|
1800
1800
|
* Call when process.cwd() changes between strategy iterations.
|
|
1801
1801
|
*/
|
|
1802
1802
|
clear() {
|
|
1803
|
-
LOGGER_SERVICE$
|
|
1803
|
+
LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR);
|
|
1804
1804
|
this.getScheduleStorage.clear();
|
|
1805
1805
|
}
|
|
1806
1806
|
/**
|
|
1807
1807
|
* Switches to the default file-based PersistScheduleInstance.
|
|
1808
1808
|
*/
|
|
1809
1809
|
useJson() {
|
|
1810
|
-
LOGGER_SERVICE$
|
|
1810
|
+
LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
|
|
1811
1811
|
this.usePersistScheduleAdapter(PersistScheduleInstance);
|
|
1812
1812
|
}
|
|
1813
1813
|
/**
|
|
1814
1814
|
* Switches to PersistScheduleDummyInstance (all operations are no-ops).
|
|
1815
1815
|
*/
|
|
1816
1816
|
useDummy() {
|
|
1817
|
-
LOGGER_SERVICE$
|
|
1817
|
+
LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1818
1818
|
this.usePersistScheduleAdapter(PersistScheduleDummyInstance);
|
|
1819
1819
|
}
|
|
1820
1820
|
}
|
|
@@ -1959,7 +1959,7 @@ class PersistPartialUtils {
|
|
|
1959
1959
|
* @returns Promise resolving to partial data record (empty object if none)
|
|
1960
1960
|
*/
|
|
1961
1961
|
this.readPartialData = async (symbol, strategyName, signalId, exchangeName, when) => {
|
|
1962
|
-
LOGGER_SERVICE$
|
|
1962
|
+
LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_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);
|
|
@@ -1979,7 +1979,7 @@ class PersistPartialUtils {
|
|
|
1979
1979
|
* @returns Promise that resolves when write is complete
|
|
1980
1980
|
*/
|
|
1981
1981
|
this.writePartialData = async (partialData, symbol, strategyName, signalId, exchangeName, when) => {
|
|
1982
|
-
LOGGER_SERVICE$
|
|
1982
|
+
LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1983
1983
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
1984
1984
|
const isInitial = !this.getPartialStorage.has(key);
|
|
1985
1985
|
const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
|
|
@@ -1994,7 +1994,7 @@ class PersistPartialUtils {
|
|
|
1994
1994
|
* @param Ctor - Custom IPersistPartialInstance constructor
|
|
1995
1995
|
*/
|
|
1996
1996
|
usePersistPartialAdapter(Ctor) {
|
|
1997
|
-
LOGGER_SERVICE$
|
|
1997
|
+
LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
|
|
1998
1998
|
this.PersistPartialInstanceCtor = Ctor;
|
|
1999
1999
|
this.getPartialStorage.clear();
|
|
2000
2000
|
}
|
|
@@ -2003,21 +2003,21 @@ class PersistPartialUtils {
|
|
|
2003
2003
|
* Call when process.cwd() changes between strategy iterations.
|
|
2004
2004
|
*/
|
|
2005
2005
|
clear() {
|
|
2006
|
-
LOGGER_SERVICE$
|
|
2006
|
+
LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_CLEAR);
|
|
2007
2007
|
this.getPartialStorage.clear();
|
|
2008
2008
|
}
|
|
2009
2009
|
/**
|
|
2010
2010
|
* Switches to the default file-based PersistPartialInstance.
|
|
2011
2011
|
*/
|
|
2012
2012
|
useJson() {
|
|
2013
|
-
LOGGER_SERVICE$
|
|
2013
|
+
LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
|
|
2014
2014
|
this.usePersistPartialAdapter(PersistPartialInstance);
|
|
2015
2015
|
}
|
|
2016
2016
|
/**
|
|
2017
2017
|
* Switches to PersistPartialDummyInstance (all operations are no-ops).
|
|
2018
2018
|
*/
|
|
2019
2019
|
useDummy() {
|
|
2020
|
-
LOGGER_SERVICE$
|
|
2020
|
+
LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2021
2021
|
this.usePersistPartialAdapter(PersistPartialDummyInstance);
|
|
2022
2022
|
}
|
|
2023
2023
|
}
|
|
@@ -2182,7 +2182,7 @@ class PersistBreakevenUtils {
|
|
|
2182
2182
|
* @returns Promise resolving to breakeven data record (empty object if none)
|
|
2183
2183
|
*/
|
|
2184
2184
|
this.readBreakevenData = async (symbol, strategyName, signalId, exchangeName, when) => {
|
|
2185
|
-
LOGGER_SERVICE$
|
|
2185
|
+
LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_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);
|
|
@@ -2202,7 +2202,7 @@ class PersistBreakevenUtils {
|
|
|
2202
2202
|
* @returns Promise that resolves when write is complete
|
|
2203
2203
|
*/
|
|
2204
2204
|
this.writeBreakevenData = async (breakevenData, symbol, strategyName, signalId, exchangeName, when) => {
|
|
2205
|
-
LOGGER_SERVICE$
|
|
2205
|
+
LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2206
2206
|
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
2207
2207
|
const isInitial = !this.getBreakevenStorage.has(key);
|
|
2208
2208
|
const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
|
|
@@ -2217,7 +2217,7 @@ class PersistBreakevenUtils {
|
|
|
2217
2217
|
* @param Ctor - Custom IPersistBreakevenInstance constructor
|
|
2218
2218
|
*/
|
|
2219
2219
|
usePersistBreakevenAdapter(Ctor) {
|
|
2220
|
-
LOGGER_SERVICE$
|
|
2220
|
+
LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
|
|
2221
2221
|
this.PersistBreakevenInstanceCtor = Ctor;
|
|
2222
2222
|
this.getBreakevenStorage.clear();
|
|
2223
2223
|
}
|
|
@@ -2226,21 +2226,21 @@ class PersistBreakevenUtils {
|
|
|
2226
2226
|
* Call when process.cwd() changes between strategy iterations.
|
|
2227
2227
|
*/
|
|
2228
2228
|
clear() {
|
|
2229
|
-
LOGGER_SERVICE$
|
|
2229
|
+
LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_CLEAR);
|
|
2230
2230
|
this.getBreakevenStorage.clear();
|
|
2231
2231
|
}
|
|
2232
2232
|
/**
|
|
2233
2233
|
* Switches to the default file-based PersistBreakevenInstance.
|
|
2234
2234
|
*/
|
|
2235
2235
|
useJson() {
|
|
2236
|
-
LOGGER_SERVICE$
|
|
2236
|
+
LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
|
|
2237
2237
|
this.usePersistBreakevenAdapter(PersistBreakevenInstance);
|
|
2238
2238
|
}
|
|
2239
2239
|
/**
|
|
2240
2240
|
* Switches to PersistBreakevenDummyInstance (all operations are no-ops).
|
|
2241
2241
|
*/
|
|
2242
2242
|
useDummy() {
|
|
2243
|
-
LOGGER_SERVICE$
|
|
2243
|
+
LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2244
2244
|
this.usePersistBreakevenAdapter(PersistBreakevenDummyInstance);
|
|
2245
2245
|
}
|
|
2246
2246
|
}
|
|
@@ -2331,7 +2331,7 @@ class PersistCandleInstance {
|
|
|
2331
2331
|
error: functoolsKit.errorData(error),
|
|
2332
2332
|
message: functoolsKit.getErrorMessage(error),
|
|
2333
2333
|
};
|
|
2334
|
-
LOGGER_SERVICE$
|
|
2334
|
+
LOGGER_SERVICE$9.warn(message, payload);
|
|
2335
2335
|
console.warn(message, payload);
|
|
2336
2336
|
errorEmitter.next(error);
|
|
2337
2337
|
return null;
|
|
@@ -2353,7 +2353,7 @@ class PersistCandleInstance {
|
|
|
2353
2353
|
for (const candle of candles) {
|
|
2354
2354
|
const candleCloseTime = candle.timestamp + stepMs;
|
|
2355
2355
|
if (candleCloseTime > now) {
|
|
2356
|
-
LOGGER_SERVICE$
|
|
2356
|
+
LOGGER_SERVICE$9.debug("PersistCandleInstance.writeCandlesData: skipping incomplete candle", {
|
|
2357
2357
|
symbol: this.symbol,
|
|
2358
2358
|
interval: this.interval,
|
|
2359
2359
|
exchangeName: this.exchangeName,
|
|
@@ -2430,7 +2430,7 @@ class PersistCandleUtils {
|
|
|
2430
2430
|
* @returns Promise resolving to candles in order, or null on cache miss
|
|
2431
2431
|
*/
|
|
2432
2432
|
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
|
|
2433
|
-
LOGGER_SERVICE$
|
|
2433
|
+
LOGGER_SERVICE$9.info("PersistCandleUtils.readCandlesData", {
|
|
2434
2434
|
symbol,
|
|
2435
2435
|
interval,
|
|
2436
2436
|
exchangeName,
|
|
@@ -2454,7 +2454,7 @@ class PersistCandleUtils {
|
|
|
2454
2454
|
* @returns Promise that resolves when all writes are complete
|
|
2455
2455
|
*/
|
|
2456
2456
|
this.writeCandlesData = async (candles, symbol, interval, exchangeName) => {
|
|
2457
|
-
LOGGER_SERVICE$
|
|
2457
|
+
LOGGER_SERVICE$9.info("PersistCandleUtils.writeCandlesData", {
|
|
2458
2458
|
symbol,
|
|
2459
2459
|
interval,
|
|
2460
2460
|
exchangeName,
|
|
@@ -2474,7 +2474,7 @@ class PersistCandleUtils {
|
|
|
2474
2474
|
* @param Ctor - Custom IPersistCandleInstance constructor
|
|
2475
2475
|
*/
|
|
2476
2476
|
usePersistCandleAdapter(Ctor) {
|
|
2477
|
-
LOGGER_SERVICE$
|
|
2477
|
+
LOGGER_SERVICE$9.info("PersistCandleUtils.usePersistCandleAdapter");
|
|
2478
2478
|
this.PersistCandleInstanceCtor = Ctor;
|
|
2479
2479
|
this.getCandlesStorage.clear();
|
|
2480
2480
|
}
|
|
@@ -2483,21 +2483,21 @@ class PersistCandleUtils {
|
|
|
2483
2483
|
* Call when process.cwd() changes between strategy iterations.
|
|
2484
2484
|
*/
|
|
2485
2485
|
clear() {
|
|
2486
|
-
LOGGER_SERVICE$
|
|
2486
|
+
LOGGER_SERVICE$9.log(PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR);
|
|
2487
2487
|
this.getCandlesStorage.clear();
|
|
2488
2488
|
}
|
|
2489
2489
|
/**
|
|
2490
2490
|
* Switches to the default file-based PersistCandleInstance.
|
|
2491
2491
|
*/
|
|
2492
2492
|
useJson() {
|
|
2493
|
-
LOGGER_SERVICE$
|
|
2493
|
+
LOGGER_SERVICE$9.log("PersistCandleUtils.useJson");
|
|
2494
2494
|
this.usePersistCandleAdapter(PersistCandleInstance);
|
|
2495
2495
|
}
|
|
2496
2496
|
/**
|
|
2497
2497
|
* Switches to PersistCandleDummyInstance (always returns null on read, discards writes).
|
|
2498
2498
|
*/
|
|
2499
2499
|
useDummy() {
|
|
2500
|
-
LOGGER_SERVICE$
|
|
2500
|
+
LOGGER_SERVICE$9.log("PersistCandleUtils.useDummy");
|
|
2501
2501
|
this.usePersistCandleAdapter(PersistCandleDummyInstance);
|
|
2502
2502
|
}
|
|
2503
2503
|
}
|
|
@@ -2635,7 +2635,7 @@ class PersistStorageUtils {
|
|
|
2635
2635
|
* @returns Promise resolving to array of signal entries
|
|
2636
2636
|
*/
|
|
2637
2637
|
this.readStorageData = async (backtest) => {
|
|
2638
|
-
LOGGER_SERVICE$
|
|
2638
|
+
LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_READ_DATA);
|
|
2639
2639
|
const key = backtest ? `backtest` : `live`;
|
|
2640
2640
|
const isInitial = !this.getStorage.has(key);
|
|
2641
2641
|
const instance = this.getStorage(backtest);
|
|
@@ -2651,7 +2651,7 @@ class PersistStorageUtils {
|
|
|
2651
2651
|
* @returns Promise that resolves when write is complete
|
|
2652
2652
|
*/
|
|
2653
2653
|
this.writeStorageData = async (signalData, backtest) => {
|
|
2654
|
-
LOGGER_SERVICE$
|
|
2654
|
+
LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2655
2655
|
const key = backtest ? `backtest` : `live`;
|
|
2656
2656
|
const isInitial = !this.getStorage.has(key);
|
|
2657
2657
|
const instance = this.getStorage(backtest);
|
|
@@ -2666,7 +2666,7 @@ class PersistStorageUtils {
|
|
|
2666
2666
|
* @param Ctor - Custom IPersistStorageInstance constructor
|
|
2667
2667
|
*/
|
|
2668
2668
|
usePersistStorageAdapter(Ctor) {
|
|
2669
|
-
LOGGER_SERVICE$
|
|
2669
|
+
LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER);
|
|
2670
2670
|
this.PersistStorageInstanceCtor = Ctor;
|
|
2671
2671
|
this.getStorage.clear();
|
|
2672
2672
|
}
|
|
@@ -2675,21 +2675,21 @@ class PersistStorageUtils {
|
|
|
2675
2675
|
* Call when process.cwd() changes between strategy iterations.
|
|
2676
2676
|
*/
|
|
2677
2677
|
clear() {
|
|
2678
|
-
LOGGER_SERVICE$
|
|
2678
|
+
LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_CLEAR);
|
|
2679
2679
|
this.getStorage.clear();
|
|
2680
2680
|
}
|
|
2681
2681
|
/**
|
|
2682
2682
|
* Switches to the default file-based PersistStorageInstance.
|
|
2683
2683
|
*/
|
|
2684
2684
|
useJson() {
|
|
2685
|
-
LOGGER_SERVICE$
|
|
2685
|
+
LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON);
|
|
2686
2686
|
this.usePersistStorageAdapter(PersistStorageInstance);
|
|
2687
2687
|
}
|
|
2688
2688
|
/**
|
|
2689
2689
|
* Switches to PersistStorageDummyInstance (all operations are no-ops).
|
|
2690
2690
|
*/
|
|
2691
2691
|
useDummy() {
|
|
2692
|
-
LOGGER_SERVICE$
|
|
2692
|
+
LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2693
2693
|
this.usePersistStorageAdapter(PersistStorageDummyInstance);
|
|
2694
2694
|
}
|
|
2695
2695
|
}
|
|
@@ -2816,7 +2816,7 @@ class PersistNotificationUtils {
|
|
|
2816
2816
|
* @returns Promise resolving to array of notification entries
|
|
2817
2817
|
*/
|
|
2818
2818
|
this.readNotificationData = async (backtest) => {
|
|
2819
|
-
LOGGER_SERVICE$
|
|
2819
|
+
LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
|
|
2820
2820
|
const key = backtest ? `backtest` : `live`;
|
|
2821
2821
|
const isInitial = !this.getNotificationStorage.has(key);
|
|
2822
2822
|
const instance = this.getNotificationStorage(backtest);
|
|
@@ -2832,7 +2832,7 @@ class PersistNotificationUtils {
|
|
|
2832
2832
|
* @returns Promise that resolves when write is complete
|
|
2833
2833
|
*/
|
|
2834
2834
|
this.writeNotificationData = async (notificationData, backtest) => {
|
|
2835
|
-
LOGGER_SERVICE$
|
|
2835
|
+
LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2836
2836
|
const key = backtest ? `backtest` : `live`;
|
|
2837
2837
|
const isInitial = !this.getNotificationStorage.has(key);
|
|
2838
2838
|
const instance = this.getNotificationStorage(backtest);
|
|
@@ -2847,7 +2847,7 @@ class PersistNotificationUtils {
|
|
|
2847
2847
|
* @param Ctor - Custom IPersistNotificationInstance constructor
|
|
2848
2848
|
*/
|
|
2849
2849
|
usePersistNotificationAdapter(Ctor) {
|
|
2850
|
-
LOGGER_SERVICE$
|
|
2850
|
+
LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
|
|
2851
2851
|
this.PersistNotificationInstanceCtor = Ctor;
|
|
2852
2852
|
this.getNotificationStorage.clear();
|
|
2853
2853
|
}
|
|
@@ -2857,21 +2857,21 @@ class PersistNotificationUtils {
|
|
|
2857
2857
|
* instances are created with the updated base path.
|
|
2858
2858
|
*/
|
|
2859
2859
|
clear() {
|
|
2860
|
-
LOGGER_SERVICE$
|
|
2860
|
+
LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_CLEAR);
|
|
2861
2861
|
this.getNotificationStorage.clear();
|
|
2862
2862
|
}
|
|
2863
2863
|
/**
|
|
2864
2864
|
* Switches to the default file-based PersistNotificationInstance.
|
|
2865
2865
|
*/
|
|
2866
2866
|
useJson() {
|
|
2867
|
-
LOGGER_SERVICE$
|
|
2867
|
+
LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
|
|
2868
2868
|
this.usePersistNotificationAdapter(PersistNotificationInstance);
|
|
2869
2869
|
}
|
|
2870
2870
|
/**
|
|
2871
2871
|
* Switches to PersistNotificationDummyInstance (all operations are no-ops).
|
|
2872
2872
|
*/
|
|
2873
2873
|
useDummy() {
|
|
2874
|
-
LOGGER_SERVICE$
|
|
2874
|
+
LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2875
2875
|
this.usePersistNotificationAdapter(PersistNotificationDummyInstance);
|
|
2876
2876
|
}
|
|
2877
2877
|
}
|
|
@@ -2995,7 +2995,7 @@ class PersistLogUtils {
|
|
|
2995
2995
|
* @returns Promise resolving to array of log entries
|
|
2996
2996
|
*/
|
|
2997
2997
|
this.readLogData = async () => {
|
|
2998
|
-
LOGGER_SERVICE$
|
|
2998
|
+
LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
|
|
2999
2999
|
const isInitial = !this._logInstance;
|
|
3000
3000
|
const instance = this.getLogInstance();
|
|
3001
3001
|
await instance.waitForInit(isInitial);
|
|
@@ -3009,7 +3009,7 @@ class PersistLogUtils {
|
|
|
3009
3009
|
* @returns Promise that resolves when write is complete
|
|
3010
3010
|
*/
|
|
3011
3011
|
this.writeLogData = async (logData) => {
|
|
3012
|
-
LOGGER_SERVICE$
|
|
3012
|
+
LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
|
|
3013
3013
|
const isInitial = !this._logInstance;
|
|
3014
3014
|
const instance = this.getLogInstance();
|
|
3015
3015
|
await instance.waitForInit(isInitial);
|
|
@@ -3034,7 +3034,7 @@ class PersistLogUtils {
|
|
|
3034
3034
|
* @param Ctor - Custom IPersistLogInstance constructor
|
|
3035
3035
|
*/
|
|
3036
3036
|
usePersistLogAdapter(Ctor) {
|
|
3037
|
-
LOGGER_SERVICE$
|
|
3037
|
+
LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
|
|
3038
3038
|
this.PersistLogInstanceCtor = Ctor;
|
|
3039
3039
|
this._logInstance = null;
|
|
3040
3040
|
}
|
|
@@ -3043,21 +3043,21 @@ class PersistLogUtils {
|
|
|
3043
3043
|
* Call when process.cwd() changes between strategy iterations.
|
|
3044
3044
|
*/
|
|
3045
3045
|
clear() {
|
|
3046
|
-
LOGGER_SERVICE$
|
|
3046
|
+
LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_CLEAR);
|
|
3047
3047
|
this._logInstance = null;
|
|
3048
3048
|
}
|
|
3049
3049
|
/**
|
|
3050
3050
|
* Switches to the default file-based PersistLogInstance.
|
|
3051
3051
|
*/
|
|
3052
3052
|
useJson() {
|
|
3053
|
-
LOGGER_SERVICE$
|
|
3053
|
+
LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
|
|
3054
3054
|
this.usePersistLogAdapter(PersistLogInstance);
|
|
3055
3055
|
}
|
|
3056
3056
|
/**
|
|
3057
3057
|
* Switches to PersistLogDummyInstance (all operations are no-ops).
|
|
3058
3058
|
*/
|
|
3059
3059
|
useDummy() {
|
|
3060
|
-
LOGGER_SERVICE$
|
|
3060
|
+
LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3061
3061
|
this.usePersistLogAdapter(PersistLogDummyInstance);
|
|
3062
3062
|
}
|
|
3063
3063
|
}
|
|
@@ -3219,7 +3219,7 @@ class PersistMeasureUtils {
|
|
|
3219
3219
|
* @returns Promise resolving to cached value, or null if not found / soft-deleted
|
|
3220
3220
|
*/
|
|
3221
3221
|
this.readMeasureData = async (bucket, key) => {
|
|
3222
|
-
LOGGER_SERVICE$
|
|
3222
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
|
|
3223
3223
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3224
3224
|
const instance = this.getMeasureStorage(bucket);
|
|
3225
3225
|
await instance.waitForInit(isInitial);
|
|
@@ -3235,7 +3235,7 @@ class PersistMeasureUtils {
|
|
|
3235
3235
|
* @returns Promise that resolves when write is complete
|
|
3236
3236
|
*/
|
|
3237
3237
|
this.writeMeasureData = async (data, bucket, key, when) => {
|
|
3238
|
-
LOGGER_SERVICE$
|
|
3238
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
|
|
3239
3239
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3240
3240
|
const instance = this.getMeasureStorage(bucket);
|
|
3241
3241
|
await instance.waitForInit(isInitial);
|
|
@@ -3250,7 +3250,7 @@ class PersistMeasureUtils {
|
|
|
3250
3250
|
* @returns Promise that resolves when removal is complete
|
|
3251
3251
|
*/
|
|
3252
3252
|
this.removeMeasureData = async (bucket, key) => {
|
|
3253
|
-
LOGGER_SERVICE$
|
|
3253
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
|
|
3254
3254
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3255
3255
|
const instance = this.getMeasureStorage(bucket);
|
|
3256
3256
|
await instance.waitForInit(isInitial);
|
|
@@ -3264,7 +3264,7 @@ class PersistMeasureUtils {
|
|
|
3264
3264
|
* @param Ctor - Custom IPersistMeasureInstance constructor
|
|
3265
3265
|
*/
|
|
3266
3266
|
usePersistMeasureAdapter(Ctor) {
|
|
3267
|
-
LOGGER_SERVICE$
|
|
3267
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
|
|
3268
3268
|
this.PersistMeasureInstanceCtor = Ctor;
|
|
3269
3269
|
this.getMeasureStorage.clear();
|
|
3270
3270
|
}
|
|
@@ -3276,7 +3276,7 @@ class PersistMeasureUtils {
|
|
|
3276
3276
|
* @returns AsyncGenerator yielding entry keys
|
|
3277
3277
|
*/
|
|
3278
3278
|
async *listMeasureData(bucket) {
|
|
3279
|
-
LOGGER_SERVICE$
|
|
3279
|
+
LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
|
|
3280
3280
|
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
3281
3281
|
const instance = this.getMeasureStorage(bucket);
|
|
3282
3282
|
await instance.waitForInit(isInitial);
|
|
@@ -3287,21 +3287,21 @@ class PersistMeasureUtils {
|
|
|
3287
3287
|
* Call when process.cwd() changes between strategy iterations.
|
|
3288
3288
|
*/
|
|
3289
3289
|
clear() {
|
|
3290
|
-
LOGGER_SERVICE$
|
|
3290
|
+
LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR);
|
|
3291
3291
|
this.getMeasureStorage.clear();
|
|
3292
3292
|
}
|
|
3293
3293
|
/**
|
|
3294
3294
|
* Switches to the default file-based PersistMeasureInstance.
|
|
3295
3295
|
*/
|
|
3296
3296
|
useJson() {
|
|
3297
|
-
LOGGER_SERVICE$
|
|
3297
|
+
LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
|
|
3298
3298
|
this.usePersistMeasureAdapter(PersistMeasureInstance);
|
|
3299
3299
|
}
|
|
3300
3300
|
/**
|
|
3301
3301
|
* Switches to PersistMeasureDummyInstance (all operations are no-ops).
|
|
3302
3302
|
*/
|
|
3303
3303
|
useDummy() {
|
|
3304
|
-
LOGGER_SERVICE$
|
|
3304
|
+
LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3305
3305
|
this.usePersistMeasureAdapter(PersistMeasureDummyInstance);
|
|
3306
3306
|
}
|
|
3307
3307
|
}
|
|
@@ -3460,7 +3460,7 @@ class PersistIntervalUtils {
|
|
|
3460
3460
|
* @returns Promise resolving to marker data, or null if not found / soft-deleted
|
|
3461
3461
|
*/
|
|
3462
3462
|
this.readIntervalData = async (bucket, key) => {
|
|
3463
|
-
LOGGER_SERVICE$
|
|
3463
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
|
|
3464
3464
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3465
3465
|
const instance = this.getIntervalStorage(bucket);
|
|
3466
3466
|
await instance.waitForInit(isInitial);
|
|
@@ -3476,7 +3476,7 @@ class PersistIntervalUtils {
|
|
|
3476
3476
|
* @returns Promise that resolves when write is complete
|
|
3477
3477
|
*/
|
|
3478
3478
|
this.writeIntervalData = async (data, bucket, key, when) => {
|
|
3479
|
-
LOGGER_SERVICE$
|
|
3479
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
|
|
3480
3480
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3481
3481
|
const instance = this.getIntervalStorage(bucket);
|
|
3482
3482
|
await instance.waitForInit(isInitial);
|
|
@@ -3491,7 +3491,7 @@ class PersistIntervalUtils {
|
|
|
3491
3491
|
* @returns Promise that resolves when removal is complete
|
|
3492
3492
|
*/
|
|
3493
3493
|
this.removeIntervalData = async (bucket, key) => {
|
|
3494
|
-
LOGGER_SERVICE$
|
|
3494
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
|
|
3495
3495
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3496
3496
|
const instance = this.getIntervalStorage(bucket);
|
|
3497
3497
|
await instance.waitForInit(isInitial);
|
|
@@ -3505,7 +3505,7 @@ class PersistIntervalUtils {
|
|
|
3505
3505
|
* @param Ctor - Custom IPersistIntervalInstance constructor
|
|
3506
3506
|
*/
|
|
3507
3507
|
usePersistIntervalAdapter(Ctor) {
|
|
3508
|
-
LOGGER_SERVICE$
|
|
3508
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
|
|
3509
3509
|
this.PersistIntervalInstanceCtor = Ctor;
|
|
3510
3510
|
this.getIntervalStorage.clear();
|
|
3511
3511
|
}
|
|
@@ -3517,7 +3517,7 @@ class PersistIntervalUtils {
|
|
|
3517
3517
|
* @returns AsyncGenerator yielding marker keys
|
|
3518
3518
|
*/
|
|
3519
3519
|
async *listIntervalData(bucket) {
|
|
3520
|
-
LOGGER_SERVICE$
|
|
3520
|
+
LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
|
|
3521
3521
|
const isInitial = !this.getIntervalStorage.has(bucket);
|
|
3522
3522
|
const instance = this.getIntervalStorage(bucket);
|
|
3523
3523
|
await instance.waitForInit(isInitial);
|
|
@@ -3528,21 +3528,21 @@ class PersistIntervalUtils {
|
|
|
3528
3528
|
* Call when process.cwd() changes between strategy iterations.
|
|
3529
3529
|
*/
|
|
3530
3530
|
clear() {
|
|
3531
|
-
LOGGER_SERVICE$
|
|
3531
|
+
LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
|
|
3532
3532
|
this.getIntervalStorage.clear();
|
|
3533
3533
|
}
|
|
3534
3534
|
/**
|
|
3535
3535
|
* Switches to the default file-based PersistIntervalInstance.
|
|
3536
3536
|
*/
|
|
3537
3537
|
useJson() {
|
|
3538
|
-
LOGGER_SERVICE$
|
|
3538
|
+
LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
|
|
3539
3539
|
this.usePersistIntervalAdapter(PersistIntervalInstance);
|
|
3540
3540
|
}
|
|
3541
3541
|
/**
|
|
3542
3542
|
* Switches to PersistIntervalDummyInstance (all operations are no-ops).
|
|
3543
3543
|
*/
|
|
3544
3544
|
useDummy() {
|
|
3545
|
-
LOGGER_SERVICE$
|
|
3545
|
+
LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3546
3546
|
this.usePersistIntervalAdapter(PersistIntervalDummyInstance);
|
|
3547
3547
|
}
|
|
3548
3548
|
}
|
|
@@ -3748,7 +3748,7 @@ class PersistMemoryUtils {
|
|
|
3748
3748
|
* @returns Promise resolving to entry data, or null if not found / soft-deleted
|
|
3749
3749
|
*/
|
|
3750
3750
|
this.readMemoryData = async (signalId, bucketName, memoryId) => {
|
|
3751
|
-
LOGGER_SERVICE$
|
|
3751
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName, memoryId });
|
|
3752
3752
|
const key = `${signalId}:${bucketName}`;
|
|
3753
3753
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3754
3754
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3765,7 +3765,7 @@ class PersistMemoryUtils {
|
|
|
3765
3765
|
* @returns Promise resolving to true if entry exists
|
|
3766
3766
|
*/
|
|
3767
3767
|
this.hasMemoryData = async (signalId, bucketName, memoryId) => {
|
|
3768
|
-
LOGGER_SERVICE$
|
|
3768
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, { signalId, bucketName, memoryId });
|
|
3769
3769
|
const key = `${signalId}:${bucketName}`;
|
|
3770
3770
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3771
3771
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3784,7 +3784,7 @@ class PersistMemoryUtils {
|
|
|
3784
3784
|
* @returns Promise that resolves when write is complete
|
|
3785
3785
|
*/
|
|
3786
3786
|
this.writeMemoryData = async (data, signalId, bucketName, memoryId, when) => {
|
|
3787
|
-
LOGGER_SERVICE$
|
|
3787
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
|
|
3788
3788
|
const key = `${signalId}:${bucketName}`;
|
|
3789
3789
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3790
3790
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3801,7 +3801,7 @@ class PersistMemoryUtils {
|
|
|
3801
3801
|
* @returns Promise that resolves when removal is complete
|
|
3802
3802
|
*/
|
|
3803
3803
|
this.removeMemoryData = async (signalId, bucketName, memoryId) => {
|
|
3804
|
-
LOGGER_SERVICE$
|
|
3804
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, { signalId, bucketName, memoryId });
|
|
3805
3805
|
const key = `${signalId}:${bucketName}`;
|
|
3806
3806
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3807
3807
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3813,7 +3813,7 @@ class PersistMemoryUtils {
|
|
|
3813
3813
|
* Call when process.cwd() changes between strategy iterations.
|
|
3814
3814
|
*/
|
|
3815
3815
|
this.clear = () => {
|
|
3816
|
-
LOGGER_SERVICE$
|
|
3816
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_CLEAR);
|
|
3817
3817
|
this.getMemoryStorage.clear();
|
|
3818
3818
|
};
|
|
3819
3819
|
/**
|
|
@@ -3824,7 +3824,7 @@ class PersistMemoryUtils {
|
|
|
3824
3824
|
* @param bucketName - Bucket name
|
|
3825
3825
|
*/
|
|
3826
3826
|
this.dispose = (signalId, bucketName) => {
|
|
3827
|
-
LOGGER_SERVICE$
|
|
3827
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_DISPOSE);
|
|
3828
3828
|
const key = `${signalId}:${bucketName}`;
|
|
3829
3829
|
this.getMemoryStorage.clear(key);
|
|
3830
3830
|
};
|
|
@@ -3836,7 +3836,7 @@ class PersistMemoryUtils {
|
|
|
3836
3836
|
* @param Ctor - Custom IPersistMemoryInstance constructor
|
|
3837
3837
|
*/
|
|
3838
3838
|
usePersistMemoryAdapter(Ctor) {
|
|
3839
|
-
LOGGER_SERVICE$
|
|
3839
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER);
|
|
3840
3840
|
this.PersistMemoryInstanceCtor = Ctor;
|
|
3841
3841
|
this.getMemoryStorage.clear();
|
|
3842
3842
|
}
|
|
@@ -3850,7 +3850,7 @@ class PersistMemoryUtils {
|
|
|
3850
3850
|
* @returns AsyncGenerator yielding `{ memoryId, data }` tuples
|
|
3851
3851
|
*/
|
|
3852
3852
|
async *listMemoryData(signalId, bucketName) {
|
|
3853
|
-
LOGGER_SERVICE$
|
|
3853
|
+
LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, { signalId, bucketName });
|
|
3854
3854
|
const key = `${signalId}:${bucketName}`;
|
|
3855
3855
|
const isInitial = !this.getMemoryStorage.has(key);
|
|
3856
3856
|
const instance = this.getMemoryStorage(signalId, bucketName);
|
|
@@ -3861,14 +3861,14 @@ class PersistMemoryUtils {
|
|
|
3861
3861
|
* Switches to the default file-based PersistMemoryInstance.
|
|
3862
3862
|
*/
|
|
3863
3863
|
useJson() {
|
|
3864
|
-
LOGGER_SERVICE$
|
|
3864
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
|
|
3865
3865
|
this.usePersistMemoryAdapter(PersistMemoryInstance);
|
|
3866
3866
|
}
|
|
3867
3867
|
/**
|
|
3868
3868
|
* Switches to PersistMemoryDummyInstance (all operations are no-ops).
|
|
3869
3869
|
*/
|
|
3870
3870
|
useDummy() {
|
|
3871
|
-
LOGGER_SERVICE$
|
|
3871
|
+
LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
3872
3872
|
this.usePersistMemoryAdapter(PersistMemoryDummyInstance);
|
|
3873
3873
|
}
|
|
3874
3874
|
}
|
|
@@ -4017,7 +4017,7 @@ class PersistRecentUtils {
|
|
|
4017
4017
|
* @returns Promise resolving to recent signal or null if none persisted
|
|
4018
4018
|
*/
|
|
4019
4019
|
this.readRecentData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
4020
|
-
LOGGER_SERVICE$
|
|
4020
|
+
LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_READ_DATA);
|
|
4021
4021
|
const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
|
|
4022
4022
|
const isInitial = !this.getStorage.has(key);
|
|
4023
4023
|
const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
@@ -4038,7 +4038,7 @@ class PersistRecentUtils {
|
|
|
4038
4038
|
* @returns Promise that resolves when write is complete
|
|
4039
4039
|
*/
|
|
4040
4040
|
this.writeRecentData = async (signalRow, symbol, strategyName, exchangeName, frameName, backtest, when) => {
|
|
4041
|
-
LOGGER_SERVICE$
|
|
4041
|
+
LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
|
|
4042
4042
|
const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
|
|
4043
4043
|
const isInitial = !this.getStorage.has(key);
|
|
4044
4044
|
const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
@@ -4071,7 +4071,7 @@ class PersistRecentUtils {
|
|
|
4071
4071
|
* @param Ctor - Custom IPersistRecentInstance constructor
|
|
4072
4072
|
*/
|
|
4073
4073
|
usePersistRecentAdapter(Ctor) {
|
|
4074
|
-
LOGGER_SERVICE$
|
|
4074
|
+
LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_USE_PERSIST_RECENT_ADAPTER);
|
|
4075
4075
|
this.PersistRecentInstanceCtor = Ctor;
|
|
4076
4076
|
this.getStorage.clear();
|
|
4077
4077
|
}
|
|
@@ -4080,21 +4080,21 @@ class PersistRecentUtils {
|
|
|
4080
4080
|
* Call when process.cwd() changes between strategy iterations.
|
|
4081
4081
|
*/
|
|
4082
4082
|
clear() {
|
|
4083
|
-
LOGGER_SERVICE$
|
|
4083
|
+
LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_CLEAR);
|
|
4084
4084
|
this.getStorage.clear();
|
|
4085
4085
|
}
|
|
4086
4086
|
/**
|
|
4087
4087
|
* Switches to the default file-based PersistRecentInstance.
|
|
4088
4088
|
*/
|
|
4089
4089
|
useJson() {
|
|
4090
|
-
LOGGER_SERVICE$
|
|
4090
|
+
LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_JSON);
|
|
4091
4091
|
this.usePersistRecentAdapter(PersistRecentInstance);
|
|
4092
4092
|
}
|
|
4093
4093
|
/**
|
|
4094
4094
|
* Switches to PersistRecentDummyInstance (all operations are no-ops).
|
|
4095
4095
|
*/
|
|
4096
4096
|
useDummy() {
|
|
4097
|
-
LOGGER_SERVICE$
|
|
4097
|
+
LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_DUMMY);
|
|
4098
4098
|
this.usePersistRecentAdapter(PersistRecentDummyInstance);
|
|
4099
4099
|
}
|
|
4100
4100
|
}
|
|
@@ -4229,7 +4229,7 @@ class PersistStateUtils {
|
|
|
4229
4229
|
* @returns Promise that resolves when initialization is complete
|
|
4230
4230
|
*/
|
|
4231
4231
|
this.waitForInit = async (signalId, bucketName, initial) => {
|
|
4232
|
-
LOGGER_SERVICE$
|
|
4232
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, { signalId, bucketName, initial });
|
|
4233
4233
|
const key = `${signalId}:${bucketName}`;
|
|
4234
4234
|
const isInitial = initial && !this.getStateStorage.has(key);
|
|
4235
4235
|
const instance = this.getStateStorage(signalId, bucketName);
|
|
@@ -4244,7 +4244,7 @@ class PersistStateUtils {
|
|
|
4244
4244
|
* @returns Promise resolving to state data or null if none persisted
|
|
4245
4245
|
*/
|
|
4246
4246
|
this.readStateData = async (signalId, bucketName) => {
|
|
4247
|
-
LOGGER_SERVICE$
|
|
4247
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName });
|
|
4248
4248
|
const key = `${signalId}:${bucketName}`;
|
|
4249
4249
|
const isInitial = !this.getStateStorage.has(key);
|
|
4250
4250
|
const instance = this.getStateStorage(signalId, bucketName);
|
|
@@ -4262,7 +4262,7 @@ class PersistStateUtils {
|
|
|
4262
4262
|
* @returns Promise that resolves when write is complete
|
|
4263
4263
|
*/
|
|
4264
4264
|
this.writeStateData = async (data, signalId, bucketName, when) => {
|
|
4265
|
-
LOGGER_SERVICE$
|
|
4265
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
|
|
4266
4266
|
const key = `${signalId}:${bucketName}`;
|
|
4267
4267
|
const isInitial = !this.getStateStorage.has(key);
|
|
4268
4268
|
const instance = this.getStateStorage(signalId, bucketName);
|
|
@@ -4273,14 +4273,14 @@ class PersistStateUtils {
|
|
|
4273
4273
|
* Switches to PersistStateDummyInstance (all operations are no-ops).
|
|
4274
4274
|
*/
|
|
4275
4275
|
this.useDummy = () => {
|
|
4276
|
-
LOGGER_SERVICE$
|
|
4276
|
+
LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
4277
4277
|
this.usePersistStateAdapter(PersistStateDummyInstance);
|
|
4278
4278
|
};
|
|
4279
4279
|
/**
|
|
4280
4280
|
* Switches to the default file-based PersistStateInstance.
|
|
4281
4281
|
*/
|
|
4282
4282
|
this.useJson = () => {
|
|
4283
|
-
LOGGER_SERVICE$
|
|
4283
|
+
LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_JSON);
|
|
4284
4284
|
this.usePersistStateAdapter(PersistStateInstance);
|
|
4285
4285
|
};
|
|
4286
4286
|
/**
|
|
@@ -4288,7 +4288,7 @@ class PersistStateUtils {
|
|
|
4288
4288
|
* Call when process.cwd() changes between strategy iterations.
|
|
4289
4289
|
*/
|
|
4290
4290
|
this.clear = () => {
|
|
4291
|
-
LOGGER_SERVICE$
|
|
4291
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_CLEAR);
|
|
4292
4292
|
this.getStateStorage.clear();
|
|
4293
4293
|
};
|
|
4294
4294
|
/**
|
|
@@ -4299,7 +4299,7 @@ class PersistStateUtils {
|
|
|
4299
4299
|
* @param bucketName - Bucket name
|
|
4300
4300
|
*/
|
|
4301
4301
|
this.dispose = (signalId, bucketName) => {
|
|
4302
|
-
LOGGER_SERVICE$
|
|
4302
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_DISPOSE);
|
|
4303
4303
|
const key = `${signalId}:${bucketName}`;
|
|
4304
4304
|
this.getStateStorage.clear(key);
|
|
4305
4305
|
};
|
|
@@ -4311,7 +4311,7 @@ class PersistStateUtils {
|
|
|
4311
4311
|
* @param Ctor - Custom IPersistStateInstance constructor
|
|
4312
4312
|
*/
|
|
4313
4313
|
usePersistStateAdapter(Ctor) {
|
|
4314
|
-
LOGGER_SERVICE$
|
|
4314
|
+
LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_USE_PERSIST_STATE_ADAPTER);
|
|
4315
4315
|
this.PersistStateInstanceCtor = Ctor;
|
|
4316
4316
|
this.getStateStorage.clear();
|
|
4317
4317
|
}
|
|
@@ -4451,7 +4451,7 @@ class PersistSessionUtils {
|
|
|
4451
4451
|
* @returns Promise that resolves when initialization is complete
|
|
4452
4452
|
*/
|
|
4453
4453
|
this.waitForInit = async (strategyName, exchangeName, frameName, initial) => {
|
|
4454
|
-
LOGGER_SERVICE$
|
|
4454
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, { strategyName, exchangeName, frameName, initial });
|
|
4455
4455
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4456
4456
|
const isInitial = initial && !this.getSessionStorage.has(key);
|
|
4457
4457
|
const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
|
|
@@ -4467,7 +4467,7 @@ class PersistSessionUtils {
|
|
|
4467
4467
|
* @returns Promise resolving to session data or null if none persisted
|
|
4468
4468
|
*/
|
|
4469
4469
|
this.readSessionData = async (strategyName, exchangeName, frameName) => {
|
|
4470
|
-
LOGGER_SERVICE$
|
|
4470
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, { strategyName, exchangeName, frameName });
|
|
4471
4471
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4472
4472
|
const isInitial = !this.getSessionStorage.has(key);
|
|
4473
4473
|
const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
|
|
@@ -4486,7 +4486,7 @@ class PersistSessionUtils {
|
|
|
4486
4486
|
* @returns Promise that resolves when write is complete
|
|
4487
4487
|
*/
|
|
4488
4488
|
this.writeSessionData = async (data, strategyName, exchangeName, frameName, when) => {
|
|
4489
|
-
LOGGER_SERVICE$
|
|
4489
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
|
|
4490
4490
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4491
4491
|
const isInitial = !this.getSessionStorage.has(key);
|
|
4492
4492
|
const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
|
|
@@ -4497,14 +4497,14 @@ class PersistSessionUtils {
|
|
|
4497
4497
|
* Switches to PersistSessionDummyInstance (all operations are no-ops).
|
|
4498
4498
|
*/
|
|
4499
4499
|
this.useDummy = () => {
|
|
4500
|
-
LOGGER_SERVICE$
|
|
4500
|
+
LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_DUMMY);
|
|
4501
4501
|
this.usePersistSessionAdapter(PersistSessionDummyInstance);
|
|
4502
4502
|
};
|
|
4503
4503
|
/**
|
|
4504
4504
|
* Switches to the default file-based PersistSessionInstance.
|
|
4505
4505
|
*/
|
|
4506
4506
|
this.useJson = () => {
|
|
4507
|
-
LOGGER_SERVICE$
|
|
4507
|
+
LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_JSON);
|
|
4508
4508
|
this.usePersistSessionAdapter(PersistSessionInstance);
|
|
4509
4509
|
};
|
|
4510
4510
|
/**
|
|
@@ -4512,7 +4512,7 @@ class PersistSessionUtils {
|
|
|
4512
4512
|
* Call when process.cwd() changes between strategy iterations.
|
|
4513
4513
|
*/
|
|
4514
4514
|
this.clear = () => {
|
|
4515
|
-
LOGGER_SERVICE$
|
|
4515
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_CLEAR);
|
|
4516
4516
|
this.getSessionStorage.clear();
|
|
4517
4517
|
};
|
|
4518
4518
|
/**
|
|
@@ -4524,7 +4524,7 @@ class PersistSessionUtils {
|
|
|
4524
4524
|
* @param frameName - Frame identifier
|
|
4525
4525
|
*/
|
|
4526
4526
|
this.dispose = (strategyName, exchangeName, frameName) => {
|
|
4527
|
-
LOGGER_SERVICE$
|
|
4527
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_DISPOSE);
|
|
4528
4528
|
const key = `${strategyName}:${exchangeName}:${frameName}`;
|
|
4529
4529
|
this.getSessionStorage.clear(key);
|
|
4530
4530
|
};
|
|
@@ -4536,7 +4536,7 @@ class PersistSessionUtils {
|
|
|
4536
4536
|
* @param Ctor - Custom IPersistSessionInstance constructor
|
|
4537
4537
|
*/
|
|
4538
4538
|
usePersistSessionAdapter(Ctor) {
|
|
4539
|
-
LOGGER_SERVICE$
|
|
4539
|
+
LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_USE_PERSIST_SESSION_ADAPTER);
|
|
4540
4540
|
this.PersistSessionInstanceCtor = Ctor;
|
|
4541
4541
|
this.getSessionStorage.clear();
|
|
4542
4542
|
}
|
|
@@ -4658,7 +4658,7 @@ const METHOD_NAME_ADD_ACTIVITY = "LookupUtils.addActivity";
|
|
|
4658
4658
|
const METHOD_NAME_REMOVE_ACTIVITY = "LookupUtils.removeActivity";
|
|
4659
4659
|
const METHOD_NAME_LIST_ACTIVITY = "LookupUtils.listActivity";
|
|
4660
4660
|
/** Logger service injected as DI singleton */
|
|
4661
|
-
const LOGGER_SERVICE$
|
|
4661
|
+
const LOGGER_SERVICE$8 = new LoggerService();
|
|
4662
4662
|
/**
|
|
4663
4663
|
* Builds the composite {@link Key} used to register an activity in `_lookupMap`.
|
|
4664
4664
|
*
|
|
@@ -4712,7 +4712,7 @@ class LookupUtils {
|
|
|
4712
4712
|
* @param activity - Activity descriptor identifying the running workload.
|
|
4713
4713
|
*/
|
|
4714
4714
|
this.addActivity = (activity) => {
|
|
4715
|
-
LOGGER_SERVICE$
|
|
4715
|
+
LOGGER_SERVICE$8.info(METHOD_NAME_ADD_ACTIVITY, {
|
|
4716
4716
|
activity,
|
|
4717
4717
|
});
|
|
4718
4718
|
const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
|
|
@@ -4726,7 +4726,7 @@ class LookupUtils {
|
|
|
4726
4726
|
* @param activity - Activity descriptor matching the one passed to {@link addActivity}.
|
|
4727
4727
|
*/
|
|
4728
4728
|
this.removeActivity = (activity) => {
|
|
4729
|
-
LOGGER_SERVICE$
|
|
4729
|
+
LOGGER_SERVICE$8.info(METHOD_NAME_REMOVE_ACTIVITY, {
|
|
4730
4730
|
activity,
|
|
4731
4731
|
});
|
|
4732
4732
|
const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
|
|
@@ -4738,7 +4738,7 @@ class LookupUtils {
|
|
|
4738
4738
|
* @returns Array of all activities present in the lookup map at call time.
|
|
4739
4739
|
*/
|
|
4740
4740
|
this.listActivity = () => {
|
|
4741
|
-
LOGGER_SERVICE$
|
|
4741
|
+
LOGGER_SERVICE$8.info(METHOD_NAME_LIST_ACTIVITY);
|
|
4742
4742
|
return Array.from(this._lookupMap.values());
|
|
4743
4743
|
};
|
|
4744
4744
|
}
|
|
@@ -4767,7 +4767,7 @@ const METHOD_NAME_SPIN_LOCK = "CandleUtils.spinLock";
|
|
|
4767
4767
|
*/
|
|
4768
4768
|
const ROTATE_DELAY = 50;
|
|
4769
4769
|
/** Logger service injected as DI singleton */
|
|
4770
|
-
const LOGGER_SERVICE$
|
|
4770
|
+
const LOGGER_SERVICE$7 = new LoggerService();
|
|
4771
4771
|
/**
|
|
4772
4772
|
* Process-wide coordinator for candle-fetch serialization and cooperative
|
|
4773
4773
|
* yielding between parallel backtests.
|
|
@@ -4816,7 +4816,7 @@ class CandleUtils {
|
|
|
4816
4816
|
* @param source - Caller identifier for logging.
|
|
4817
4817
|
*/
|
|
4818
4818
|
this.acquireLock = async (source) => {
|
|
4819
|
-
LOGGER_SERVICE$
|
|
4819
|
+
LOGGER_SERVICE$7.info(METHOD_NAME_ACQUIRE_LOCK, {
|
|
4820
4820
|
source,
|
|
4821
4821
|
});
|
|
4822
4822
|
if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
|
|
@@ -4832,7 +4832,7 @@ class CandleUtils {
|
|
|
4832
4832
|
* @param source - Caller identifier for logging.
|
|
4833
4833
|
*/
|
|
4834
4834
|
this.releaseLock = async (source) => {
|
|
4835
|
-
LOGGER_SERVICE$
|
|
4835
|
+
LOGGER_SERVICE$7.info(METHOD_NAME_RELEASE_LOCK, {
|
|
4836
4836
|
source,
|
|
4837
4837
|
});
|
|
4838
4838
|
if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
|
|
@@ -4857,7 +4857,7 @@ class CandleUtils {
|
|
|
4857
4857
|
* @param source - Caller identifier for logging.
|
|
4858
4858
|
*/
|
|
4859
4859
|
this.spinLock = async (source) => {
|
|
4860
|
-
LOGGER_SERVICE$
|
|
4860
|
+
LOGGER_SERVICE$7.info(METHOD_NAME_SPIN_LOCK, {
|
|
4861
4861
|
source,
|
|
4862
4862
|
});
|
|
4863
4863
|
if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
|
|
@@ -6367,7 +6367,7 @@ const validateCommonSignal = (signal) => {
|
|
|
6367
6367
|
}
|
|
6368
6368
|
// Кидаем ошибку если есть проблемы
|
|
6369
6369
|
if (errors.length > 0) {
|
|
6370
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6370
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6371
6371
|
}
|
|
6372
6372
|
};
|
|
6373
6373
|
|
|
@@ -6420,7 +6420,7 @@ const validatePendingSignal = (signal, currentPrice) => {
|
|
|
6420
6420
|
}
|
|
6421
6421
|
}
|
|
6422
6422
|
if (errors.length > 0) {
|
|
6423
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6423
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6424
6424
|
}
|
|
6425
6425
|
validateCommonSignal(signal);
|
|
6426
6426
|
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
@@ -6468,7 +6468,7 @@ const validatePendingSignal = (signal, currentPrice) => {
|
|
|
6468
6468
|
}
|
|
6469
6469
|
}
|
|
6470
6470
|
if (errors.length > 0) {
|
|
6471
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6471
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6472
6472
|
}
|
|
6473
6473
|
};
|
|
6474
6474
|
|
|
@@ -6521,7 +6521,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
|
|
|
6521
6521
|
}
|
|
6522
6522
|
}
|
|
6523
6523
|
if (errors.length > 0) {
|
|
6524
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6524
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6525
6525
|
}
|
|
6526
6526
|
validateCommonSignal(signal);
|
|
6527
6527
|
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
@@ -6567,7 +6567,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
|
|
|
6567
6567
|
// pendingAt === 0 is allowed for scheduled signals (set to SCHEDULED_SIGNAL_PENDING_MOCK until activation)
|
|
6568
6568
|
}
|
|
6569
6569
|
if (errors.length > 0) {
|
|
6570
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
6570
|
+
throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6571
6571
|
}
|
|
6572
6572
|
};
|
|
6573
6573
|
|
|
@@ -7014,6 +7014,13 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
7014
7014
|
if (!signal) {
|
|
7015
7015
|
return null;
|
|
7016
7016
|
}
|
|
7017
|
+
if (signal?.symbol && signal?.symbol !== self.params.execution.context.symbol) {
|
|
7018
|
+
throw new Error(`Symbol mismatch: expected ${self.params.execution.context.symbol}, got ${signal.symbol}`);
|
|
7019
|
+
}
|
|
7020
|
+
// Whipsaw protection: skip signal if its id matches the last accepted pending id
|
|
7021
|
+
if (signal.id && signal.id === self._lastPendingId) {
|
|
7022
|
+
return null;
|
|
7023
|
+
}
|
|
7017
7024
|
if (self._isStopped) {
|
|
7018
7025
|
return null;
|
|
7019
7026
|
}
|
|
@@ -7057,6 +7064,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
7057
7064
|
}
|
|
7058
7065
|
// Валидируем сигнал перед возвратом
|
|
7059
7066
|
validatePendingSignal(signalRow, currentPrice);
|
|
7067
|
+
if (signal.id) {
|
|
7068
|
+
self._lastPendingId = signal.id;
|
|
7069
|
+
}
|
|
7060
7070
|
return signalRow;
|
|
7061
7071
|
}
|
|
7062
7072
|
// ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
|
|
@@ -7083,6 +7093,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
7083
7093
|
};
|
|
7084
7094
|
// Валидируем сигнал перед возвратом
|
|
7085
7095
|
validateScheduledSignal(scheduledSignalRow, currentPrice);
|
|
7096
|
+
if (signal.id) {
|
|
7097
|
+
self._lastPendingId = signal.id;
|
|
7098
|
+
}
|
|
7086
7099
|
return scheduledSignalRow;
|
|
7087
7100
|
}
|
|
7088
7101
|
const signalRow = {
|
|
@@ -7110,6 +7123,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
7110
7123
|
}
|
|
7111
7124
|
// Валидируем сигнал перед возвратом
|
|
7112
7125
|
validatePendingSignal(signalRow, currentPrice);
|
|
7126
|
+
if (signal.id) {
|
|
7127
|
+
self._lastPendingId = signal.id;
|
|
7128
|
+
}
|
|
7113
7129
|
return signalRow;
|
|
7114
7130
|
}, {
|
|
7115
7131
|
defaultValue: null,
|
|
@@ -7139,6 +7155,13 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
|
|
|
7139
7155
|
if (self.params.execution.context.backtest) {
|
|
7140
7156
|
return;
|
|
7141
7157
|
}
|
|
7158
|
+
// Restore last pending signal id for whipsaw protection in GET_SIGNAL_FN
|
|
7159
|
+
{
|
|
7160
|
+
const recentSignal = await PersistRecentAdapter.readRecentData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, false);
|
|
7161
|
+
if (recentSignal?.id) {
|
|
7162
|
+
self._lastPendingId = recentSignal.id;
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7142
7165
|
// Restore pending signal
|
|
7143
7166
|
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
7144
7167
|
if (pendingSignal) {
|
|
@@ -9279,6 +9302,7 @@ class ClientStrategy {
|
|
|
9279
9302
|
this._isStopped = false;
|
|
9280
9303
|
this._pendingSignal = null;
|
|
9281
9304
|
this._lastSignalTimestamp = null;
|
|
9305
|
+
this._lastPendingId = null;
|
|
9282
9306
|
this._scheduledSignal = null;
|
|
9283
9307
|
this._cancelledSignal = null;
|
|
9284
9308
|
this._closedSignal = null;
|
|
@@ -12068,7 +12092,7 @@ const RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE = "MergeRisk.checkSignalAndReser
|
|
|
12068
12092
|
const RISK_METHOD_NAME_ADD_SIGNAL = "MergeRisk.addSignal";
|
|
12069
12093
|
const RISK_METHOD_NAME_REMOVE_SIGNAL = "MergeRisk.removeSignal";
|
|
12070
12094
|
/** Logger service injected as DI singleton */
|
|
12071
|
-
const LOGGER_SERVICE$
|
|
12095
|
+
const LOGGER_SERVICE$6 = new LoggerService();
|
|
12072
12096
|
/**
|
|
12073
12097
|
* Composite risk management class that combines multiple risk profiles.
|
|
12074
12098
|
*
|
|
@@ -12124,7 +12148,7 @@ class MergeRisk {
|
|
|
12124
12148
|
* @returns Promise resolving to true if all risks approve, false if any risk rejects
|
|
12125
12149
|
*/
|
|
12126
12150
|
async checkSignal(params, options = {}) {
|
|
12127
|
-
LOGGER_SERVICE$
|
|
12151
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL, {
|
|
12128
12152
|
params,
|
|
12129
12153
|
});
|
|
12130
12154
|
for (const [riskName, risk] of Object.entries(this._riskMap)) {
|
|
@@ -12154,7 +12178,7 @@ class MergeRisk {
|
|
|
12154
12178
|
* @returns Promise resolving to true if all risks approve (and reserved), false if any risk rejects
|
|
12155
12179
|
*/
|
|
12156
12180
|
async checkSignalAndReserve(params) {
|
|
12157
|
-
LOGGER_SERVICE$
|
|
12181
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE, {
|
|
12158
12182
|
params,
|
|
12159
12183
|
});
|
|
12160
12184
|
for (const [riskName, risk] of Object.entries(this._riskMap)) {
|
|
@@ -12178,7 +12202,7 @@ class MergeRisk {
|
|
|
12178
12202
|
* @returns Promise that resolves when all risks have registered the signal
|
|
12179
12203
|
*/
|
|
12180
12204
|
async addSignal(symbol, context, positionData) {
|
|
12181
|
-
LOGGER_SERVICE$
|
|
12205
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_ADD_SIGNAL, {
|
|
12182
12206
|
symbol,
|
|
12183
12207
|
context,
|
|
12184
12208
|
});
|
|
@@ -12195,7 +12219,7 @@ class MergeRisk {
|
|
|
12195
12219
|
* @returns Promise that resolves when all risks have removed the signal
|
|
12196
12220
|
*/
|
|
12197
12221
|
async removeSignal(symbol, context) {
|
|
12198
|
-
LOGGER_SERVICE$
|
|
12222
|
+
LOGGER_SERVICE$6.info(RISK_METHOD_NAME_REMOVE_SIGNAL, {
|
|
12199
12223
|
symbol,
|
|
12200
12224
|
context,
|
|
12201
12225
|
});
|
|
@@ -14988,7 +15012,7 @@ class RiskConnectionService {
|
|
|
14988
15012
|
}
|
|
14989
15013
|
|
|
14990
15014
|
/** Logger service injected as DI singleton */
|
|
14991
|
-
const LOGGER_SERVICE$
|
|
15015
|
+
const LOGGER_SERVICE$5 = new LoggerService();
|
|
14992
15016
|
/**
|
|
14993
15017
|
* Wrapper to call init method with error capture.
|
|
14994
15018
|
*/
|
|
@@ -15003,7 +15027,7 @@ const CALL_INIT_FN = functoolsKit.trycatch(async (self) => {
|
|
|
15003
15027
|
error: functoolsKit.errorData(error),
|
|
15004
15028
|
message: functoolsKit.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_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
15023
15047
|
error: functoolsKit.errorData(error),
|
|
15024
15048
|
message: functoolsKit.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_LIVE_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
15043
15067
|
error: functoolsKit.errorData(error),
|
|
15044
15068
|
message: functoolsKit.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
|
},
|
|
@@ -15063,7 +15087,7 @@ const CALL_SIGNAL_BACKTEST_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
15063
15087
|
error: functoolsKit.errorData(error),
|
|
15064
15088
|
message: functoolsKit.getErrorMessage(error),
|
|
15065
15089
|
};
|
|
15066
|
-
LOGGER_SERVICE$
|
|
15090
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15067
15091
|
console.warn(message, payload);
|
|
15068
15092
|
errorEmitter.next(error);
|
|
15069
15093
|
},
|
|
@@ -15091,7 +15115,7 @@ const CALL_BREAKEVEN_AVAILABLE_FN = functoolsKit.trycatch(async (event, self) =>
|
|
|
15091
15115
|
error: functoolsKit.errorData(error),
|
|
15092
15116
|
message: functoolsKit.getErrorMessage(error),
|
|
15093
15117
|
};
|
|
15094
|
-
LOGGER_SERVICE$
|
|
15118
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15095
15119
|
console.warn(message, payload);
|
|
15096
15120
|
errorEmitter.next(error);
|
|
15097
15121
|
},
|
|
@@ -15119,7 +15143,7 @@ const CALL_PARTIAL_PROFIT_AVAILABLE_FN = functoolsKit.trycatch(async (event, sel
|
|
|
15119
15143
|
error: functoolsKit.errorData(error),
|
|
15120
15144
|
message: functoolsKit.getErrorMessage(error),
|
|
15121
15145
|
};
|
|
15122
|
-
LOGGER_SERVICE$
|
|
15146
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15123
15147
|
console.warn(message, payload);
|
|
15124
15148
|
errorEmitter.next(error);
|
|
15125
15149
|
},
|
|
@@ -15147,7 +15171,7 @@ const CALL_PARTIAL_LOSS_AVAILABLE_FN = functoolsKit.trycatch(async (event, self)
|
|
|
15147
15171
|
error: functoolsKit.errorData(error),
|
|
15148
15172
|
message: functoolsKit.getErrorMessage(error),
|
|
15149
15173
|
};
|
|
15150
|
-
LOGGER_SERVICE$
|
|
15174
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15151
15175
|
console.warn(message, payload);
|
|
15152
15176
|
errorEmitter.next(error);
|
|
15153
15177
|
},
|
|
@@ -15175,7 +15199,7 @@ const CALL_PING_SCHEDULED_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
15175
15199
|
error: functoolsKit.errorData(error),
|
|
15176
15200
|
message: functoolsKit.getErrorMessage(error),
|
|
15177
15201
|
};
|
|
15178
|
-
LOGGER_SERVICE$
|
|
15202
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15179
15203
|
console.warn(message, payload);
|
|
15180
15204
|
errorEmitter.next(error);
|
|
15181
15205
|
},
|
|
@@ -15203,7 +15227,7 @@ const CALL_PING_IDLE_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
15203
15227
|
error: functoolsKit.errorData(error),
|
|
15204
15228
|
message: functoolsKit.getErrorMessage(error),
|
|
15205
15229
|
};
|
|
15206
|
-
LOGGER_SERVICE$
|
|
15230
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15207
15231
|
console.warn(message, payload);
|
|
15208
15232
|
errorEmitter.next(error);
|
|
15209
15233
|
},
|
|
@@ -15231,7 +15255,7 @@ const CALL_PING_ACTIVE_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
15231
15255
|
error: functoolsKit.errorData(error),
|
|
15232
15256
|
message: functoolsKit.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_RISK_REJECTION_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
15251
15275
|
error: functoolsKit.errorData(error),
|
|
15252
15276
|
message: functoolsKit.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
|
},
|
|
@@ -15271,7 +15295,7 @@ const CALL_DISPOSE_FN = functoolsKit.trycatch(async (self) => {
|
|
|
15271
15295
|
error: functoolsKit.errorData(error),
|
|
15272
15296
|
message: functoolsKit.getErrorMessage(error),
|
|
15273
15297
|
};
|
|
15274
|
-
LOGGER_SERVICE$
|
|
15298
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
15275
15299
|
console.warn(message, payload);
|
|
15276
15300
|
errorEmitter.next(error);
|
|
15277
15301
|
},
|
|
@@ -23135,7 +23159,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY$1 = "ReportUtils.useDummy";
|
|
|
23135
23159
|
const REPORT_UTILS_METHOD_NAME_USE_JSONL$1 = "ReportUtils.useJsonl";
|
|
23136
23160
|
const REPORT_UTILS_METHOD_NAME_CLEAR$1 = "ReportUtils.clear";
|
|
23137
23161
|
/** Logger service injected as DI singleton */
|
|
23138
|
-
const LOGGER_SERVICE$
|
|
23162
|
+
const LOGGER_SERVICE$4 = new LoggerService();
|
|
23139
23163
|
/** Symbol key for the singleshot waitForInit function on MarkdownFileBase instances. */
|
|
23140
23164
|
const WAIT_FOR_INIT_SYMBOL$1 = Symbol("wait-for-init");
|
|
23141
23165
|
/** Symbol key for the timeout-protected write function on MarkdownFileBase instances. */
|
|
@@ -23216,7 +23240,7 @@ class MarkdownFileBase {
|
|
|
23216
23240
|
* @throws Error if stream not initialized or write timeout exceeded
|
|
23217
23241
|
*/
|
|
23218
23242
|
async dump(data, options) {
|
|
23219
|
-
LOGGER_SERVICE$
|
|
23243
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FILE_DUMP, {
|
|
23220
23244
|
markdownName: this.markdownName,
|
|
23221
23245
|
options,
|
|
23222
23246
|
});
|
|
@@ -23296,7 +23320,7 @@ class MarkdownFolderBase {
|
|
|
23296
23320
|
* @throws Error if directory creation or file write fails
|
|
23297
23321
|
*/
|
|
23298
23322
|
async dump(content, options) {
|
|
23299
|
-
LOGGER_SERVICE$
|
|
23323
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FOLDER_DUMP, {
|
|
23300
23324
|
markdownName: this.markdownName,
|
|
23301
23325
|
options,
|
|
23302
23326
|
});
|
|
@@ -23361,7 +23385,7 @@ class MarkdownWriterAdapter {
|
|
|
23361
23385
|
* @param Ctor - Constructor for markdown storage adapter
|
|
23362
23386
|
*/
|
|
23363
23387
|
useMarkdownAdapter(Ctor) {
|
|
23364
|
-
LOGGER_SERVICE$
|
|
23388
|
+
LOGGER_SERVICE$4.info(MARKDOWN_METHOD_NAME_USE_ADAPTER$1);
|
|
23365
23389
|
this.MarkdownFactory = Ctor;
|
|
23366
23390
|
}
|
|
23367
23391
|
/**
|
|
@@ -23375,7 +23399,7 @@ class MarkdownWriterAdapter {
|
|
|
23375
23399
|
* @throws Error if write fails or storage initialization fails
|
|
23376
23400
|
*/
|
|
23377
23401
|
async writeData(markdownName, content, options) {
|
|
23378
|
-
LOGGER_SERVICE$
|
|
23402
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_WRITE_DATA, {
|
|
23379
23403
|
markdownName,
|
|
23380
23404
|
options,
|
|
23381
23405
|
});
|
|
@@ -23389,7 +23413,7 @@ class MarkdownWriterAdapter {
|
|
|
23389
23413
|
* Each report is written as a separate .md file.
|
|
23390
23414
|
*/
|
|
23391
23415
|
useMd() {
|
|
23392
|
-
LOGGER_SERVICE$
|
|
23416
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_MD$1);
|
|
23393
23417
|
this.useMarkdownAdapter(MarkdownFolderBase);
|
|
23394
23418
|
}
|
|
23395
23419
|
/**
|
|
@@ -23397,7 +23421,7 @@ class MarkdownWriterAdapter {
|
|
|
23397
23421
|
* All reports are appended to a single .jsonl file per markdown type.
|
|
23398
23422
|
*/
|
|
23399
23423
|
useJsonl() {
|
|
23400
|
-
LOGGER_SERVICE$
|
|
23424
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_JSONL$1);
|
|
23401
23425
|
this.useMarkdownAdapter(MarkdownFileBase);
|
|
23402
23426
|
}
|
|
23403
23427
|
/**
|
|
@@ -23406,7 +23430,7 @@ class MarkdownWriterAdapter {
|
|
|
23406
23430
|
* so new storage instances are created with the updated base path.
|
|
23407
23431
|
*/
|
|
23408
23432
|
clear() {
|
|
23409
|
-
LOGGER_SERVICE$
|
|
23433
|
+
LOGGER_SERVICE$4.log(MARKDOWN_METHOD_NAME_CLEAR$1);
|
|
23410
23434
|
this.getMarkdownStorage.clear();
|
|
23411
23435
|
}
|
|
23412
23436
|
/**
|
|
@@ -23414,7 +23438,7 @@ class MarkdownWriterAdapter {
|
|
|
23414
23438
|
* All future markdown writes will be no-ops.
|
|
23415
23439
|
*/
|
|
23416
23440
|
useDummy() {
|
|
23417
|
-
LOGGER_SERVICE$
|
|
23441
|
+
LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_DUMMY$1);
|
|
23418
23442
|
this.useMarkdownAdapter(MarkdownDummy);
|
|
23419
23443
|
}
|
|
23420
23444
|
}
|
|
@@ -23475,7 +23499,7 @@ class ReportBase {
|
|
|
23475
23499
|
});
|
|
23476
23500
|
}
|
|
23477
23501
|
}, 15000));
|
|
23478
|
-
LOGGER_SERVICE$
|
|
23502
|
+
LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_CTOR, {
|
|
23479
23503
|
reportName: this.reportName,
|
|
23480
23504
|
baseDir,
|
|
23481
23505
|
});
|
|
@@ -23489,7 +23513,7 @@ class ReportBase {
|
|
|
23489
23513
|
* @returns Promise that resolves when initialization is complete
|
|
23490
23514
|
*/
|
|
23491
23515
|
async waitForInit(initial) {
|
|
23492
|
-
LOGGER_SERVICE$
|
|
23516
|
+
LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
|
|
23493
23517
|
reportName: this.reportName,
|
|
23494
23518
|
initial,
|
|
23495
23519
|
});
|
|
@@ -23508,7 +23532,7 @@ class ReportBase {
|
|
|
23508
23532
|
* @throws Error if stream not initialized or write timeout exceeded
|
|
23509
23533
|
*/
|
|
23510
23534
|
async write(data, options) {
|
|
23511
|
-
LOGGER_SERVICE$
|
|
23535
|
+
LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WRITE, {
|
|
23512
23536
|
reportName: this.reportName,
|
|
23513
23537
|
options,
|
|
23514
23538
|
});
|
|
@@ -23605,7 +23629,7 @@ class ReportWriterAdapter {
|
|
|
23605
23629
|
* @internal - Automatically called by report services, not for direct use
|
|
23606
23630
|
*/
|
|
23607
23631
|
this.writeData = async (reportName, data, options) => {
|
|
23608
|
-
LOGGER_SERVICE$
|
|
23632
|
+
LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
|
|
23609
23633
|
reportName,
|
|
23610
23634
|
options,
|
|
23611
23635
|
});
|
|
@@ -23622,7 +23646,7 @@ class ReportWriterAdapter {
|
|
|
23622
23646
|
* @param Ctor - Constructor for report storage adapter
|
|
23623
23647
|
*/
|
|
23624
23648
|
useReportAdapter(Ctor) {
|
|
23625
|
-
LOGGER_SERVICE$
|
|
23649
|
+
LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER$1);
|
|
23626
23650
|
this.ReportFactory = Ctor;
|
|
23627
23651
|
}
|
|
23628
23652
|
/**
|
|
@@ -23631,7 +23655,7 @@ class ReportWriterAdapter {
|
|
|
23631
23655
|
* so new storage instances are created with the updated base path.
|
|
23632
23656
|
*/
|
|
23633
23657
|
clear() {
|
|
23634
|
-
LOGGER_SERVICE$
|
|
23658
|
+
LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_CLEAR$1);
|
|
23635
23659
|
this.getReportStorage.clear();
|
|
23636
23660
|
}
|
|
23637
23661
|
/**
|
|
@@ -23639,7 +23663,7 @@ class ReportWriterAdapter {
|
|
|
23639
23663
|
* All future report writes will be no-ops.
|
|
23640
23664
|
*/
|
|
23641
23665
|
useDummy() {
|
|
23642
|
-
LOGGER_SERVICE$
|
|
23666
|
+
LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY$1);
|
|
23643
23667
|
this.useReportAdapter(ReportDummy);
|
|
23644
23668
|
}
|
|
23645
23669
|
/**
|
|
@@ -23647,7 +23671,7 @@ class ReportWriterAdapter {
|
|
|
23647
23671
|
* All future report writes will use JSONL storage.
|
|
23648
23672
|
*/
|
|
23649
23673
|
useJsonl() {
|
|
23650
|
-
LOGGER_SERVICE$
|
|
23674
|
+
LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_JSONL$1);
|
|
23651
23675
|
this.useReportAdapter(ReportBase);
|
|
23652
23676
|
}
|
|
23653
23677
|
}
|
|
@@ -23702,7 +23726,7 @@ const CREATE_FILE_NAME_FN$c = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
23702
23726
|
* @param value - Value to check
|
|
23703
23727
|
* @returns true if value is unsafe, false otherwise
|
|
23704
23728
|
*/
|
|
23705
|
-
function isUnsafe$
|
|
23729
|
+
function isUnsafe$4(value) {
|
|
23706
23730
|
if (typeof value !== "number") {
|
|
23707
23731
|
return true;
|
|
23708
23732
|
}
|
|
@@ -23714,6 +23738,25 @@ function isUnsafe$3(value) {
|
|
|
23714
23738
|
}
|
|
23715
23739
|
return false;
|
|
23716
23740
|
}
|
|
23741
|
+
/** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
|
|
23742
|
+
const MIN_SIGNALS_FOR_ANNUALIZATION$2 = 10;
|
|
23743
|
+
/** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
|
|
23744
|
+
* sample size is too small to estimate variance meaningfully. */
|
|
23745
|
+
const MIN_SIGNALS_FOR_RATIOS$2 = 10;
|
|
23746
|
+
/** Minimum calendar span (days) for trade-frequency extrapolation. */
|
|
23747
|
+
const MIN_CALENDAR_SPAN_DAYS$2 = 14;
|
|
23748
|
+
/** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
|
|
23749
|
+
const MAX_TRADES_PER_YEAR$2 = 365;
|
|
23750
|
+
/** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
|
|
23751
|
+
* blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
|
|
23752
|
+
* anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
|
|
23753
|
+
const MAX_EXPECTED_YEARLY_RETURNS$2 = 100;
|
|
23754
|
+
/** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
|
|
23755
|
+
const MAX_CALMAR_RATIO$2 = 1000;
|
|
23756
|
+
/** Minimum stdDev required for Sharpe/Sortino computation. Identical-returns series produce
|
|
23757
|
+
* float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously inflates
|
|
23758
|
+
* sharpe to astronomical values. Treat any stdDev below this threshold as zero. */
|
|
23759
|
+
const STDDEV_EPSILON$2 = 1e-9;
|
|
23717
23760
|
/**
|
|
23718
23761
|
* Storage class for accumulating closed signals per strategy.
|
|
23719
23762
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
@@ -23767,65 +23810,190 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23767
23810
|
recoveryFactor: null,
|
|
23768
23811
|
};
|
|
23769
23812
|
}
|
|
23770
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
//
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
|
|
23777
|
-
|
|
23778
|
-
|
|
23779
|
-
const
|
|
23780
|
-
const
|
|
23781
|
-
const
|
|
23782
|
-
|
|
23783
|
-
//
|
|
23784
|
-
const
|
|
23785
|
-
|
|
23813
|
+
// Valid signal set — those with usable pendingAt AND closeTimestamp. Single source
|
|
23814
|
+
// of truth for EVERY metric in this method (counts, sums, span, equity curve,
|
|
23815
|
+
// ratios, annualization). If we used different subsets for different metrics, the
|
|
23816
|
+
// numerator of one ratio could be drawn from a different population than the
|
|
23817
|
+
// denominator of another and the report would silently lie. On clean data
|
|
23818
|
+
// validSignals === this._signalList; the filter only matters for corrupted runtime
|
|
23819
|
+
// data.
|
|
23820
|
+
const validSignals = this._signalList.filter((s) => typeof s.signal.pendingAt === "number" && s.signal.pendingAt > 0 &&
|
|
23821
|
+
typeof s.closeTimestamp === "number" && s.closeTimestamp > 0);
|
|
23822
|
+
const totalSignals = validSignals.length;
|
|
23823
|
+
const winCount = validSignals.filter((s) => s.pnl.pnlPercentage > 0).length;
|
|
23824
|
+
const lossCount = validSignals.filter((s) => s.pnl.pnlPercentage < 0).length;
|
|
23825
|
+
// Basic statistics — guard against an empty validSignals (e.g. every signal had
|
|
23826
|
+
// corrupted timestamps) so we don't divide by zero.
|
|
23827
|
+
const avgPnl = totalSignals > 0
|
|
23828
|
+
? validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals
|
|
23829
|
+
: 0;
|
|
23830
|
+
const totalPnl = validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
|
|
23831
|
+
// Win rate excludes break-even trades from both numerator and denominator.
|
|
23832
|
+
const decisiveTrades = winCount + lossCount;
|
|
23833
|
+
const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
|
|
23834
|
+
// Calendar span over the same validSignals set used for ratios.
|
|
23835
|
+
let firstPendingAt = Infinity;
|
|
23836
|
+
let lastCloseAt = -Infinity;
|
|
23837
|
+
for (const s of validSignals) {
|
|
23838
|
+
if (s.signal.pendingAt < firstPendingAt)
|
|
23839
|
+
firstPendingAt = s.signal.pendingAt;
|
|
23840
|
+
if (s.closeTimestamp > lastCloseAt)
|
|
23841
|
+
lastCloseAt = s.closeTimestamp;
|
|
23842
|
+
}
|
|
23843
|
+
const calendarSpanDays = isFinite(firstPendingAt) && isFinite(lastCloseAt)
|
|
23844
|
+
? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
|
|
23845
|
+
: 0;
|
|
23846
|
+
// tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
|
|
23847
|
+
// silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
|
|
23848
|
+
// raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
|
|
23849
|
+
// for reliable annualization and surface every annualized metric as null.
|
|
23850
|
+
const rawTradesPerYear = totalSignals >= MIN_SIGNALS_FOR_ANNUALIZATION$2 &&
|
|
23851
|
+
calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$2
|
|
23852
|
+
? (totalSignals / calendarSpanDays) * 365
|
|
23853
|
+
: 0;
|
|
23854
|
+
const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$2;
|
|
23855
|
+
const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
|
|
23856
|
+
// Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1) for unbiased estimate.
|
|
23857
|
+
// Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
|
|
23858
|
+
// are too noisy to publish (high chance of spurious ±Sharpe).
|
|
23859
|
+
const returns = validSignals.map((s) => s.pnl.pnlPercentage);
|
|
23860
|
+
const canComputeRatios = totalSignals >= MIN_SIGNALS_FOR_RATIOS$2;
|
|
23861
|
+
const stdDev = canComputeRatios
|
|
23862
|
+
? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (totalSignals - 1))
|
|
23863
|
+
: 0;
|
|
23864
|
+
// Use STDDEV_EPSILON gate (not stdDev > 0) — identical-returns series produce
|
|
23865
|
+
// float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously
|
|
23866
|
+
// inflates sharpe to astronomical magnitudes (avgPnl / epsilon).
|
|
23867
|
+
const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$2
|
|
23868
|
+
? avgPnl / stdDev
|
|
23869
|
+
: null;
|
|
23870
|
+
// Annualize only when gate passes; otherwise null.
|
|
23871
|
+
const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
|
|
23872
|
+
? sharpeRatio * Math.sqrt(tradesPerYear)
|
|
23873
|
+
: null;
|
|
23874
|
+
// Equity-curve max drawdown via compounded equity (multiplicative, not additive).
|
|
23875
|
+
// Returns are per-trade on cost basis — compounding assumes equal capital allocation
|
|
23876
|
+
// per trade ("as-if 100% allocation"). Walks validSignals in chronological order
|
|
23877
|
+
// (storage is newest-first, so iterate in reverse). Using validSignals (same set as
|
|
23878
|
+
// tradesPerYear) keeps equityFinal consistent with the annualization exponent.
|
|
23879
|
+
// If equity goes ≤ 0 (e.g. leveraged short with r < -100%) — account blown,
|
|
23880
|
+
// fix DD at 100% and stop walking the curve.
|
|
23881
|
+
let equity = 1;
|
|
23882
|
+
let peak = 1;
|
|
23883
|
+
let equityMaxDrawdown = 0;
|
|
23884
|
+
let blown = false;
|
|
23885
|
+
for (let i = validSignals.length - 1; i >= 0; i--) {
|
|
23886
|
+
equity *= 1 + validSignals[i].pnl.pnlPercentage / 100;
|
|
23887
|
+
if (equity <= 0) {
|
|
23888
|
+
equityMaxDrawdown = 100;
|
|
23889
|
+
blown = true;
|
|
23890
|
+
break;
|
|
23891
|
+
}
|
|
23892
|
+
if (equity > peak)
|
|
23893
|
+
peak = equity;
|
|
23894
|
+
const dd = (peak - equity) / peak * 100;
|
|
23895
|
+
if (dd > equityMaxDrawdown)
|
|
23896
|
+
equityMaxDrawdown = dd;
|
|
23897
|
+
}
|
|
23898
|
+
const equityFinal = blown ? 0 : equity;
|
|
23899
|
+
// Compounded yearly return via geometric mean of equity curve.
|
|
23900
|
+
// equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag that
|
|
23901
|
+
// arithmetic-mean compounding ((1+avgPnl)^N) misses. If account is blown, full loss.
|
|
23902
|
+
// If the raw value would exceed MAX_EXPECTED_YEARLY_RETURNS, return null rather than
|
|
23903
|
+
// showing the cap as a real figure — capped numbers mislead users into trusting them.
|
|
23904
|
+
const expectedYearlyReturns = canAnnualize
|
|
23905
|
+
? blown
|
|
23906
|
+
? -100
|
|
23907
|
+
: (() => {
|
|
23908
|
+
// Geometric annualization uses validSignals.length (same set that defined
|
|
23909
|
+
// tradesPerYear); using totalSignals here would mismatch numerator/denominator.
|
|
23910
|
+
const raw = (Math.pow(equityFinal, tradesPerYear / validSignals.length) - 1) * 100;
|
|
23911
|
+
return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$2 ? null : raw;
|
|
23912
|
+
})()
|
|
23913
|
+
: null;
|
|
23914
|
+
// Certainty Ratio — over validSignals so wins/losses come from the same set as
|
|
23915
|
+
// winCount/lossCount/avgPnl above.
|
|
23916
|
+
const wins = validSignals.filter((s) => s.pnl.pnlPercentage > 0);
|
|
23917
|
+
const losses = validSignals.filter((s) => s.pnl.pnlPercentage < 0);
|
|
23786
23918
|
const avgWin = wins.length > 0
|
|
23787
23919
|
? wins.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / wins.length
|
|
23788
23920
|
: 0;
|
|
23789
23921
|
const avgLoss = losses.length > 0
|
|
23790
23922
|
? losses.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / losses.length
|
|
23791
23923
|
: 0;
|
|
23792
|
-
|
|
23793
|
-
//
|
|
23794
|
-
|
|
23795
|
-
|
|
23796
|
-
|
|
23797
|
-
|
|
23798
|
-
|
|
23799
|
-
|
|
23800
|
-
|
|
23801
|
-
//
|
|
23802
|
-
|
|
23803
|
-
|
|
23804
|
-
|
|
23805
|
-
|
|
23806
|
-
const
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
const
|
|
23810
|
-
|
|
23924
|
+
// Null below MIN_SIGNALS_FOR_RATIOS — on a handful of trades the win/loss
|
|
23925
|
+
// means are too noisy to publish a ratio (same sample-size gate as Sharpe/
|
|
23926
|
+
// Sortino, so the report doesn't surface certainty while withholding the rest).
|
|
23927
|
+
// Also null when no losing trades OR when |avgLoss| is below STDDEV_EPSILON
|
|
23928
|
+
// (float-artifact losses (-1e-15) would otherwise produce a spurious
|
|
23929
|
+
// astronomical certaintyRatio ≈1e14).
|
|
23930
|
+
const certaintyRatio = canComputeRatios && Math.abs(avgLoss) > STDDEV_EPSILON$2 && avgLoss < 0
|
|
23931
|
+
? avgWin / Math.abs(avgLoss)
|
|
23932
|
+
: null;
|
|
23933
|
+
// Average peak/fall PNL — over validSignals; only signals that actually have the
|
|
23934
|
+
// value contribute (no zero dilution from missing peakProfit/maxDrawdown).
|
|
23935
|
+
const peakValues = validSignals
|
|
23936
|
+
.map((s) => s.signal.peakProfit?.pnlPercentage)
|
|
23937
|
+
.filter((v) => typeof v === "number");
|
|
23938
|
+
const fallValues = validSignals
|
|
23939
|
+
.map((s) => s.signal.maxDrawdown?.pnlPercentage)
|
|
23940
|
+
.filter((v) => typeof v === "number");
|
|
23941
|
+
const avgPeakPnl = peakValues.length > 0
|
|
23942
|
+
? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
|
|
23943
|
+
: null;
|
|
23944
|
+
const avgFallPnl = fallValues.length > 0
|
|
23945
|
+
? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
|
|
23946
|
+
: null;
|
|
23947
|
+
// Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
|
|
23948
|
+
// downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
|
|
23949
|
+
// so the numerator reduces to avgPnl and the squared term to r² for r < 0.
|
|
23950
|
+
// Dividing by N_total (not N_negative) properly penalises strategies with frequent
|
|
23951
|
+
// losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
|
|
23952
|
+
// strategies.
|
|
23953
|
+
const negativeReturns = returns.filter((r) => r < 0);
|
|
23954
|
+
const sortinoRatio = (() => {
|
|
23955
|
+
if (!canComputeRatios)
|
|
23956
|
+
return null;
|
|
23957
|
+
if (negativeReturns.length === 0)
|
|
23958
|
+
return null;
|
|
23959
|
+
const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
|
|
23960
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
23961
|
+
// Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
|
|
23962
|
+
return downsideDeviation > STDDEV_EPSILON$2 ? avgPnl / downsideDeviation : null;
|
|
23963
|
+
})();
|
|
23964
|
+
// Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
|
|
23965
|
+
const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
|
|
23966
|
+
? Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, expectedYearlyReturns / equityMaxDrawdown))
|
|
23967
|
+
: null;
|
|
23968
|
+
// Recovery Factor: numerator must be the compounded total return (equityFinal − 1) × 100,
|
|
23969
|
+
// not the arithmetic totalPnl — denominator (equityMaxDrawdown) is from the compounded
|
|
23970
|
+
// curve, so mixing units would inflate Recovery on long winning streaks.
|
|
23971
|
+
// Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
|
|
23972
|
+
// so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
|
|
23973
|
+
// Null when account is blown — ratio is meaningless after total loss.
|
|
23974
|
+
// Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
|
|
23975
|
+
// and explode the same way when DD is near zero.
|
|
23976
|
+
const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
|
|
23977
|
+
? null
|
|
23978
|
+
: Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, ((equityFinal - 1) * 100) / equityMaxDrawdown));
|
|
23811
23979
|
return {
|
|
23812
23980
|
signalList: this._signalList,
|
|
23813
23981
|
totalSignals,
|
|
23814
23982
|
winCount,
|
|
23815
23983
|
lossCount,
|
|
23816
|
-
winRate: isUnsafe$
|
|
23817
|
-
avgPnl: isUnsafe$
|
|
23818
|
-
totalPnl: isUnsafe$
|
|
23819
|
-
stdDev: isUnsafe$
|
|
23820
|
-
sharpeRatio: isUnsafe$
|
|
23821
|
-
annualizedSharpeRatio: isUnsafe$
|
|
23822
|
-
certaintyRatio: isUnsafe$
|
|
23823
|
-
expectedYearlyReturns: isUnsafe$
|
|
23824
|
-
avgPeakPnl: isUnsafe$
|
|
23825
|
-
avgFallPnl: isUnsafe$
|
|
23826
|
-
sortinoRatio: isUnsafe$
|
|
23827
|
-
calmarRatio: isUnsafe$
|
|
23828
|
-
recoveryFactor: isUnsafe$
|
|
23984
|
+
winRate: isUnsafe$4(winRate) ? null : winRate,
|
|
23985
|
+
avgPnl: isUnsafe$4(avgPnl) ? null : avgPnl,
|
|
23986
|
+
totalPnl: isUnsafe$4(totalPnl) ? null : totalPnl,
|
|
23987
|
+
stdDev: isUnsafe$4(stdDev) ? null : stdDev,
|
|
23988
|
+
sharpeRatio: isUnsafe$4(sharpeRatio) ? null : sharpeRatio,
|
|
23989
|
+
annualizedSharpeRatio: isUnsafe$4(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
23990
|
+
certaintyRatio: isUnsafe$4(certaintyRatio) ? null : certaintyRatio,
|
|
23991
|
+
expectedYearlyReturns: isUnsafe$4(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
23992
|
+
avgPeakPnl: isUnsafe$4(avgPeakPnl) ? null : avgPeakPnl,
|
|
23993
|
+
avgFallPnl: isUnsafe$4(avgFallPnl) ? null : avgFallPnl,
|
|
23994
|
+
sortinoRatio: isUnsafe$4(sortinoRatio) ? null : sortinoRatio,
|
|
23995
|
+
calmarRatio: isUnsafe$4(calmarRatio) ? null : calmarRatio,
|
|
23996
|
+
recoveryFactor: isUnsafe$4(recoveryFactor) ? null : recoveryFactor,
|
|
23829
23997
|
};
|
|
23830
23998
|
}
|
|
23831
23999
|
/**
|
|
@@ -23867,24 +24035,26 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23867
24035
|
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
23868
24036
|
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
23869
24037
|
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
23870
|
-
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better
|
|
24038
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
23871
24039
|
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
23872
|
-
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better
|
|
24040
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
23873
24041
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
23874
24042
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
23875
24043
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
23876
|
-
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better
|
|
24044
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
23877
24045
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
23878
24046
|
"",
|
|
23879
24047
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
23880
24048
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
23881
|
-
`*Annualized Sharpe Ratio:
|
|
23882
|
-
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
24049
|
+
`*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.*`,
|
|
24050
|
+
`*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".*`,
|
|
23883
24051
|
`*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
|
|
23884
|
-
`*Expected Yearly Returns:
|
|
23885
|
-
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong.
|
|
23886
|
-
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
|
|
23887
|
-
`*All metrics require 100+ signals to be statistically reliable.
|
|
24052
|
+
`*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.*`,
|
|
24053
|
+
`*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}.*`,
|
|
24054
|
+
`*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.*`,
|
|
24055
|
+
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24056
|
+
`*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.*`,
|
|
24057
|
+
`*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.*`,
|
|
23888
24058
|
].join("\n");
|
|
23889
24059
|
}
|
|
23890
24060
|
/**
|
|
@@ -24196,7 +24366,7 @@ const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
24196
24366
|
* @param value - Value to check
|
|
24197
24367
|
* @returns true if value is unsafe, false otherwise
|
|
24198
24368
|
*/
|
|
24199
|
-
function isUnsafe$
|
|
24369
|
+
function isUnsafe$3(value) {
|
|
24200
24370
|
if (typeof value !== "number") {
|
|
24201
24371
|
return true;
|
|
24202
24372
|
}
|
|
@@ -24208,6 +24378,25 @@ function isUnsafe$2(value) {
|
|
|
24208
24378
|
}
|
|
24209
24379
|
return false;
|
|
24210
24380
|
}
|
|
24381
|
+
/** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
|
|
24382
|
+
const MIN_SIGNALS_FOR_ANNUALIZATION$1 = 10;
|
|
24383
|
+
/** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
|
|
24384
|
+
* sample size is too small to estimate variance meaningfully. */
|
|
24385
|
+
const MIN_SIGNALS_FOR_RATIOS$1 = 10;
|
|
24386
|
+
/** Minimum calendar span (days) for trade-frequency extrapolation. */
|
|
24387
|
+
const MIN_CALENDAR_SPAN_DAYS$1 = 14;
|
|
24388
|
+
/** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
|
|
24389
|
+
const MAX_TRADES_PER_YEAR$1 = 365;
|
|
24390
|
+
/** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
|
|
24391
|
+
* blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
|
|
24392
|
+
* anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
|
|
24393
|
+
const MAX_EXPECTED_YEARLY_RETURNS$1 = 100;
|
|
24394
|
+
/** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
|
|
24395
|
+
const MAX_CALMAR_RATIO$1 = 1000;
|
|
24396
|
+
/** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
|
|
24397
|
+
* float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
|
|
24398
|
+
* astronomical magnitudes (avgPnl / epsilon). */
|
|
24399
|
+
const STDDEV_EPSILON$1 = 1e-9;
|
|
24211
24400
|
/**
|
|
24212
24401
|
* Storage class for accumulating all tick events per strategy.
|
|
24213
24402
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
@@ -24491,84 +24680,190 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24491
24680
|
};
|
|
24492
24681
|
}
|
|
24493
24682
|
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
24494
|
-
|
|
24495
|
-
|
|
24496
|
-
|
|
24497
|
-
|
|
24498
|
-
|
|
24499
|
-
|
|
24683
|
+
// Valid closed set — single source of truth. Events must have numeric pnl AND valid
|
|
24684
|
+
// timestamps. Win/loss counts, returns, calendar span, equity curve — all derived
|
|
24685
|
+
// from this set so they cannot disagree.
|
|
24686
|
+
const validClosed = closedEvents.filter((e) => typeof e.pnl === "number" &&
|
|
24687
|
+
typeof e.timestamp === "number" &&
|
|
24688
|
+
e.timestamp > 0 &&
|
|
24689
|
+
typeof (e.pendingAt ?? e.timestamp) === "number");
|
|
24690
|
+
const totalClosed = validClosed.length;
|
|
24691
|
+
const winCount = validClosed.filter((e) => e.pnl > 0).length;
|
|
24692
|
+
const lossCount = validClosed.filter((e) => e.pnl < 0).length;
|
|
24693
|
+
const returns = validClosed.map((e) => e.pnl);
|
|
24694
|
+
const avgPnl = returns.length > 0
|
|
24695
|
+
? returns.reduce((sum, r) => sum + r, 0) / returns.length
|
|
24500
24696
|
: 0;
|
|
24501
|
-
const totalPnl =
|
|
24502
|
-
|
|
24503
|
-
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
|
|
24509
|
-
|
|
24510
|
-
|
|
24511
|
-
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
|
|
24697
|
+
const totalPnl = returns.reduce((sum, r) => sum + r, 0);
|
|
24698
|
+
// Win rate excludes break-even trades from both numerator and denominator.
|
|
24699
|
+
const decisiveTrades = winCount + lossCount;
|
|
24700
|
+
const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
|
|
24701
|
+
// Trade frequency from calendar span — gated by minimum span and sample size to
|
|
24702
|
+
// suppress absurd annualization on short / sparse runs. Span built from validClosed
|
|
24703
|
+
// so denominator (calendarSpanDays) and numerator (returns.length) come from the
|
|
24704
|
+
// same event set.
|
|
24705
|
+
let firstPendingAt = Infinity;
|
|
24706
|
+
let lastCloseAt = -Infinity;
|
|
24707
|
+
for (const e of validClosed) {
|
|
24708
|
+
const startAt = e.pendingAt ?? e.timestamp;
|
|
24709
|
+
if (startAt < firstPendingAt)
|
|
24710
|
+
firstPendingAt = startAt;
|
|
24711
|
+
if (e.timestamp > lastCloseAt)
|
|
24712
|
+
lastCloseAt = e.timestamp;
|
|
24713
|
+
}
|
|
24714
|
+
const calendarSpanDays = validClosed.length > 0
|
|
24715
|
+
? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
|
|
24716
|
+
: 0;
|
|
24717
|
+
// tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
|
|
24718
|
+
// silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
|
|
24719
|
+
// raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
|
|
24720
|
+
// for reliable annualization and surface every annualized metric as null.
|
|
24721
|
+
const rawTradesPerYear = returns.length >= MIN_SIGNALS_FOR_ANNUALIZATION$1 &&
|
|
24722
|
+
calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$1
|
|
24723
|
+
? (returns.length / calendarSpanDays) * 365
|
|
24724
|
+
: 0;
|
|
24725
|
+
const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$1;
|
|
24726
|
+
const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
|
|
24727
|
+
// Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1).
|
|
24728
|
+
// Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
|
|
24729
|
+
// are too noisy to publish (high chance of spurious ±Sharpe).
|
|
24730
|
+
const canComputeRatios = returns.length >= MIN_SIGNALS_FOR_RATIOS$1;
|
|
24731
|
+
const stdDev = canComputeRatios
|
|
24732
|
+
? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (returns.length - 1))
|
|
24733
|
+
: 0;
|
|
24734
|
+
// STDDEV_EPSILON guard — protects against float-artifact stdDev from identical
|
|
24735
|
+
// returns producing spuriously astronomical sharpe.
|
|
24736
|
+
const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$1
|
|
24737
|
+
? avgPnl / stdDev
|
|
24738
|
+
: null;
|
|
24739
|
+
// Annualize only when gate passes; otherwise null.
|
|
24740
|
+
const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
|
|
24741
|
+
? sharpeRatio * Math.sqrt(tradesPerYear)
|
|
24742
|
+
: null;
|
|
24743
|
+
// Certainty Ratio: null (not zero) when there are no losing trades — a flawless
|
|
24744
|
+
// strategy has undefined Certainty Ratio, not "worst case zero". Computed on
|
|
24745
|
+
// validClosed for consistency with other ratios.
|
|
24746
|
+
// Gated below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as Sharpe/Sortino,
|
|
24747
|
+
// so the report doesn't surface certainty on a handful of trades while
|
|
24748
|
+
// withholding the rest.
|
|
24749
|
+
let certaintyRatio = null;
|
|
24750
|
+
if (canComputeRatios && totalClosed > 0) {
|
|
24751
|
+
const wins = validClosed.filter((e) => e.pnl > 0);
|
|
24752
|
+
const losses = validClosed.filter((e) => e.pnl < 0);
|
|
24518
24753
|
const avgWin = wins.length > 0
|
|
24519
|
-
? wins.reduce((sum, e) => sum +
|
|
24754
|
+
? wins.reduce((sum, e) => sum + e.pnl, 0) / wins.length
|
|
24520
24755
|
: 0;
|
|
24521
24756
|
const avgLoss = losses.length > 0
|
|
24522
|
-
? losses.reduce((sum, e) => sum +
|
|
24757
|
+
? losses.reduce((sum, e) => sum + e.pnl, 0) / losses.length
|
|
24523
24758
|
: 0;
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
|
|
24527
|
-
|
|
24528
|
-
|
|
24529
|
-
|
|
24530
|
-
|
|
24531
|
-
|
|
24532
|
-
|
|
24533
|
-
|
|
24534
|
-
|
|
24535
|
-
|
|
24536
|
-
|
|
24537
|
-
|
|
24538
|
-
|
|
24539
|
-
|
|
24540
|
-
|
|
24541
|
-
const
|
|
24542
|
-
|
|
24543
|
-
|
|
24544
|
-
|
|
24545
|
-
|
|
24546
|
-
|
|
24547
|
-
|
|
24548
|
-
|
|
24549
|
-
//
|
|
24550
|
-
const
|
|
24551
|
-
|
|
24552
|
-
|
|
24759
|
+
// STDDEV_EPSILON guard on |avgLoss| protects against float-artifact
|
|
24760
|
+
// losses producing spurious astronomical certaintyRatio.
|
|
24761
|
+
certaintyRatio = Math.abs(avgLoss) > STDDEV_EPSILON$1 && avgLoss < 0
|
|
24762
|
+
? avgWin / Math.abs(avgLoss)
|
|
24763
|
+
: null;
|
|
24764
|
+
}
|
|
24765
|
+
// Average only over signals that have the value — do not dilute the mean with zeros.
|
|
24766
|
+
// Use validClosed to keep all metric denominators consistent.
|
|
24767
|
+
const peakValues = validClosed
|
|
24768
|
+
.map((e) => e.peakPnl)
|
|
24769
|
+
.filter((v) => typeof v === "number");
|
|
24770
|
+
const fallValues = validClosed
|
|
24771
|
+
.map((e) => e.fallPnl)
|
|
24772
|
+
.filter((v) => typeof v === "number");
|
|
24773
|
+
const avgPeakPnl = peakValues.length > 0
|
|
24774
|
+
? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
|
|
24775
|
+
: null;
|
|
24776
|
+
const avgFallPnl = fallValues.length > 0
|
|
24777
|
+
? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
|
|
24778
|
+
: null;
|
|
24779
|
+
// Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
|
|
24780
|
+
// downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
|
|
24781
|
+
// so the numerator reduces to avgPnl and the squared term to r² for r < 0.
|
|
24782
|
+
// Dividing by N_total (not N_negative) properly penalises strategies with frequent
|
|
24783
|
+
// losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
|
|
24784
|
+
// strategies.
|
|
24785
|
+
const sortinoRatio = (() => {
|
|
24786
|
+
if (!canComputeRatios)
|
|
24787
|
+
return null;
|
|
24788
|
+
const negativeReturns = returns.filter((r) => r < 0);
|
|
24789
|
+
if (negativeReturns.length === 0)
|
|
24790
|
+
return null;
|
|
24791
|
+
const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
|
|
24792
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
24793
|
+
// Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
|
|
24794
|
+
return downsideDeviation > STDDEV_EPSILON$1 ? avgPnl / downsideDeviation : null;
|
|
24795
|
+
})();
|
|
24796
|
+
// Equity-curve max drawdown via compounded equity (multiplicative). Returns are per-trade
|
|
24797
|
+
// on cost basis — compounding assumes equal capital allocation per trade ("as-if 100%").
|
|
24798
|
+
// If equity ≤ 0 (leveraged short with r < -100%) — account blown, fix DD at 100%.
|
|
24799
|
+
// Built from validClosed (newest-first), iterated reverse for chronological order.
|
|
24800
|
+
const chronologicalReturns = [];
|
|
24801
|
+
for (let i = validClosed.length - 1; i >= 0; i--) {
|
|
24802
|
+
chronologicalReturns.push(validClosed[i].pnl);
|
|
24803
|
+
}
|
|
24804
|
+
let equity = 1;
|
|
24805
|
+
let peak = 1;
|
|
24806
|
+
let equityMaxDrawdown = 0;
|
|
24807
|
+
let blown = false;
|
|
24808
|
+
for (const r of chronologicalReturns) {
|
|
24809
|
+
equity *= 1 + r / 100;
|
|
24810
|
+
if (equity <= 0) {
|
|
24811
|
+
equityMaxDrawdown = 100;
|
|
24812
|
+
blown = true;
|
|
24813
|
+
break;
|
|
24814
|
+
}
|
|
24815
|
+
if (equity > peak)
|
|
24816
|
+
peak = equity;
|
|
24817
|
+
const dd = (peak - equity) / peak * 100;
|
|
24818
|
+
if (dd > equityMaxDrawdown)
|
|
24819
|
+
equityMaxDrawdown = dd;
|
|
24820
|
+
}
|
|
24821
|
+
const equityFinal = blown ? 0 : equity;
|
|
24822
|
+
// Compounded yearly return via geometric mean of equity curve:
|
|
24823
|
+
// equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag.
|
|
24824
|
+
// If account is blown, full loss. If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS,
|
|
24825
|
+
// return null rather than showing the cap — capped numbers mislead users.
|
|
24826
|
+
const expectedYearlyReturns = canAnnualize
|
|
24827
|
+
? blown
|
|
24828
|
+
? -100
|
|
24829
|
+
: (() => {
|
|
24830
|
+
const raw = (Math.pow(equityFinal, tradesPerYear / returns.length) - 1) * 100;
|
|
24831
|
+
return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$1 ? null : raw;
|
|
24832
|
+
})()
|
|
24833
|
+
: null;
|
|
24834
|
+
// Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
|
|
24835
|
+
const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
|
|
24836
|
+
? Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, expectedYearlyReturns / equityMaxDrawdown))
|
|
24837
|
+
: null;
|
|
24838
|
+
// Recovery Factor: numerator must be the compounded total return, not arithmetic totalPnl —
|
|
24839
|
+
// denominator is from the compounded equity curve, so mixing units inflates Recovery.
|
|
24840
|
+
// Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
|
|
24841
|
+
// so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
|
|
24842
|
+
// Null when account is blown.
|
|
24843
|
+
// Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
|
|
24844
|
+
// and explode the same way when DD is near zero.
|
|
24845
|
+
const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
|
|
24846
|
+
? null
|
|
24847
|
+
: Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, ((equityFinal - 1) * 100) / equityMaxDrawdown));
|
|
24553
24848
|
return {
|
|
24554
24849
|
eventList: this._eventList,
|
|
24555
24850
|
totalEvents: this._eventList.length,
|
|
24556
24851
|
totalClosed,
|
|
24557
24852
|
winCount,
|
|
24558
24853
|
lossCount,
|
|
24559
|
-
winRate: isUnsafe$
|
|
24560
|
-
avgPnl: isUnsafe$
|
|
24561
|
-
totalPnl: isUnsafe$
|
|
24562
|
-
stdDev: isUnsafe$
|
|
24563
|
-
sharpeRatio: isUnsafe$
|
|
24564
|
-
annualizedSharpeRatio: isUnsafe$
|
|
24565
|
-
certaintyRatio: isUnsafe$
|
|
24566
|
-
expectedYearlyReturns: isUnsafe$
|
|
24567
|
-
avgPeakPnl: isUnsafe$
|
|
24568
|
-
avgFallPnl: isUnsafe$
|
|
24569
|
-
sortinoRatio: isUnsafe$
|
|
24570
|
-
calmarRatio: isUnsafe$
|
|
24571
|
-
recoveryFactor: isUnsafe$
|
|
24854
|
+
winRate: isUnsafe$3(winRate) ? null : winRate,
|
|
24855
|
+
avgPnl: isUnsafe$3(avgPnl) ? null : avgPnl,
|
|
24856
|
+
totalPnl: isUnsafe$3(totalPnl) ? null : totalPnl,
|
|
24857
|
+
stdDev: isUnsafe$3(stdDev) ? null : stdDev,
|
|
24858
|
+
sharpeRatio: isUnsafe$3(sharpeRatio) ? null : sharpeRatio,
|
|
24859
|
+
annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
24860
|
+
certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
|
|
24861
|
+
expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
24862
|
+
avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
|
|
24863
|
+
avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
|
|
24864
|
+
sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
|
|
24865
|
+
calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
|
|
24866
|
+
recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
|
|
24572
24867
|
};
|
|
24573
24868
|
}
|
|
24574
24869
|
/**
|
|
@@ -24616,18 +24911,20 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24616
24911
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
24617
24912
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
24618
24913
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
24619
|
-
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better
|
|
24914
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
24620
24915
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
24621
24916
|
"",
|
|
24622
24917
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
24623
24918
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
24624
|
-
`*Annualized Sharpe Ratio:
|
|
24625
|
-
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
24919
|
+
`*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.*`,
|
|
24920
|
+
`*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".*`,
|
|
24626
24921
|
`*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
|
|
24627
|
-
`*Expected Yearly Returns:
|
|
24628
|
-
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong.
|
|
24629
|
-
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
|
|
24630
|
-
`*All metrics require 100+ signals to be statistically reliable.
|
|
24922
|
+
`*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.*`,
|
|
24923
|
+
`*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}.*`,
|
|
24924
|
+
`*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.*`,
|
|
24925
|
+
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24926
|
+
`*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.*`,
|
|
24927
|
+
`*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.*`,
|
|
24631
24928
|
].join("\n");
|
|
24632
24929
|
}
|
|
24633
24930
|
/**
|
|
@@ -25006,7 +25303,9 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
25006
25303
|
*/
|
|
25007
25304
|
addOpenedEvent(data) {
|
|
25008
25305
|
const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
|
|
25009
|
-
|
|
25306
|
+
// Keep fractional minutes — rounding to whole minutes zeroed out sub-30s durations,
|
|
25307
|
+
// which dragged high-frequency averages towards zero.
|
|
25308
|
+
const durationMin = durationMs / 60000;
|
|
25010
25309
|
const newEvent = {
|
|
25011
25310
|
timestamp: data.signal.pendingAt,
|
|
25012
25311
|
action: "opened",
|
|
@@ -25042,7 +25341,8 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
25042
25341
|
*/
|
|
25043
25342
|
addCancelledEvent(data) {
|
|
25044
25343
|
const durationMs = data.closeTimestamp - data.signal.scheduledAt;
|
|
25045
|
-
|
|
25344
|
+
// Keep fractional minutes — rounding to whole minutes zeroed out sub-30s durations.
|
|
25345
|
+
const durationMin = durationMs / 60000;
|
|
25046
25346
|
const newEvent = {
|
|
25047
25347
|
timestamp: data.closeTimestamp,
|
|
25048
25348
|
action: "cancelled",
|
|
@@ -25098,19 +25398,33 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
25098
25398
|
const totalScheduled = scheduledEvents.length;
|
|
25099
25399
|
const totalOpened = openedEvents.length;
|
|
25100
25400
|
const totalCancelled = cancelledEvents.length;
|
|
25101
|
-
//
|
|
25102
|
-
|
|
25103
|
-
//
|
|
25104
|
-
|
|
25105
|
-
|
|
25106
|
-
const
|
|
25107
|
-
|
|
25108
|
-
|
|
25401
|
+
// Rate denominators must include only scheduled events whose outcome (opened/cancelled)
|
|
25402
|
+
// is also in the buffer. Otherwise a sliding window of 250 entries can drop the
|
|
25403
|
+
// "scheduled" record before its outcome arrives, inflating rates above 100% or
|
|
25404
|
+
// causing one rate to fire without the other. Match by signalId.
|
|
25405
|
+
const scheduledIds = new Set(scheduledEvents.map((e) => e.signalId).filter((id) => typeof id === "string"));
|
|
25406
|
+
const openedFromScheduled = openedEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
|
|
25407
|
+
const cancelledFromScheduled = cancelledEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
|
|
25408
|
+
const resolvedScheduled = openedFromScheduled.length + cancelledFromScheduled.length;
|
|
25409
|
+
const cancellationRate = resolvedScheduled > 0
|
|
25410
|
+
? (cancelledFromScheduled.length / resolvedScheduled) * 100
|
|
25411
|
+
: null;
|
|
25412
|
+
const activationRate = resolvedScheduled > 0
|
|
25413
|
+
? (openedFromScheduled.length / resolvedScheduled) * 100
|
|
25414
|
+
: null;
|
|
25415
|
+
// Average durations — include only events with a numeric duration, do not dilute
|
|
25416
|
+
// the mean with zeros for missing values.
|
|
25417
|
+
const cancelledDurations = cancelledEvents
|
|
25418
|
+
.map((e) => e.duration)
|
|
25419
|
+
.filter((d) => typeof d === "number");
|
|
25420
|
+
const openedDurations = openedEvents
|
|
25421
|
+
.map((e) => e.duration)
|
|
25422
|
+
.filter((d) => typeof d === "number");
|
|
25423
|
+
const avgWaitTime = cancelledDurations.length > 0
|
|
25424
|
+
? cancelledDurations.reduce((sum, d) => sum + d, 0) / cancelledDurations.length
|
|
25109
25425
|
: null;
|
|
25110
|
-
|
|
25111
|
-
|
|
25112
|
-
? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
25113
|
-
totalOpened
|
|
25426
|
+
const avgActivationTime = openedDurations.length > 0
|
|
25427
|
+
? openedDurations.reduce((sum, d) => sum + d, 0) / openedDurations.length
|
|
25114
25428
|
: null;
|
|
25115
25429
|
return {
|
|
25116
25430
|
eventList: this._eventList,
|
|
@@ -25157,13 +25471,15 @@ let ReportStorage$8 = class ReportStorage {
|
|
|
25157
25471
|
table,
|
|
25158
25472
|
"",
|
|
25159
25473
|
`**Total events:** ${stats.totalEvents}`,
|
|
25160
|
-
`**Scheduled signals:** ${stats.totalScheduled}`,
|
|
25474
|
+
`**Scheduled signals (raw):** ${stats.totalScheduled}`,
|
|
25161
25475
|
`**Opened signals:** ${stats.totalOpened}`,
|
|
25162
25476
|
`**Cancelled signals:** ${stats.totalCancelled}`,
|
|
25163
25477
|
`**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
|
|
25164
25478
|
`**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
|
|
25165
25479
|
`**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
|
|
25166
|
-
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}
|
|
25480
|
+
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`,
|
|
25481
|
+
"",
|
|
25482
|
+
`*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.*`
|
|
25167
25483
|
].join("\n");
|
|
25168
25484
|
}
|
|
25169
25485
|
/**
|
|
@@ -25468,13 +25784,37 @@ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
25468
25784
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
25469
25785
|
};
|
|
25470
25786
|
/**
|
|
25471
|
-
*
|
|
25787
|
+
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
25788
|
+
*/
|
|
25789
|
+
function isUnsafe$2(value) {
|
|
25790
|
+
if (typeof value !== "number") {
|
|
25791
|
+
return true;
|
|
25792
|
+
}
|
|
25793
|
+
if (isNaN(value)) {
|
|
25794
|
+
return true;
|
|
25795
|
+
}
|
|
25796
|
+
if (!isFinite(value)) {
|
|
25797
|
+
return true;
|
|
25798
|
+
}
|
|
25799
|
+
return false;
|
|
25800
|
+
}
|
|
25801
|
+
/**
|
|
25802
|
+
* Calculates percentile value from sorted array using linear interpolation
|
|
25803
|
+
* between adjacent ranks (equivalent to numpy.percentile with default linear method).
|
|
25804
|
+
* Falls back to nearest-rank for length 0/1.
|
|
25472
25805
|
*/
|
|
25473
25806
|
function percentile(sortedArray, p) {
|
|
25474
25807
|
if (sortedArray.length === 0)
|
|
25475
25808
|
return 0;
|
|
25476
|
-
|
|
25477
|
-
|
|
25809
|
+
if (sortedArray.length === 1)
|
|
25810
|
+
return sortedArray[0];
|
|
25811
|
+
const rank = (p / 100) * (sortedArray.length - 1);
|
|
25812
|
+
const lower = Math.floor(rank);
|
|
25813
|
+
const upper = Math.ceil(rank);
|
|
25814
|
+
if (lower === upper)
|
|
25815
|
+
return sortedArray[lower];
|
|
25816
|
+
const fraction = rank - lower;
|
|
25817
|
+
return sortedArray[lower] * (1 - fraction) + sortedArray[upper] * fraction;
|
|
25478
25818
|
}
|
|
25479
25819
|
/**
|
|
25480
25820
|
* Storage class for accumulating performance metrics per strategy.
|
|
@@ -25530,10 +25870,12 @@ class PerformanceStorage {
|
|
|
25530
25870
|
const durations = events.map((e) => e.duration).sort((a, b) => a - b);
|
|
25531
25871
|
const totalDuration = durations.reduce((sum, d) => sum + d, 0);
|
|
25532
25872
|
const avgDuration = totalDuration / durations.length;
|
|
25533
|
-
//
|
|
25534
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25873
|
+
// Sample standard deviation (Bessel correction: divide by N-1, not N) — consistent
|
|
25874
|
+
// with Sharpe/Sortino calculations in Backtest/Live/Heat services.
|
|
25875
|
+
const stdDev = durations.length > 1
|
|
25876
|
+
? Math.sqrt(durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
|
|
25877
|
+
(durations.length - 1))
|
|
25878
|
+
: 0;
|
|
25537
25879
|
// Calculate wait times between events
|
|
25538
25880
|
const waitTimes = [];
|
|
25539
25881
|
for (let i = 0; i < events.length; i++) {
|
|
@@ -25606,9 +25948,13 @@ class PerformanceStorage {
|
|
|
25606
25948
|
const rows = await Promise.all(sortedMetrics.map(async (metric, index) => Promise.all(visibleColumns.map((col) => col.format(metric, index)))));
|
|
25607
25949
|
const tableData = [header, separator, ...rows];
|
|
25608
25950
|
const summaryTable = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
25609
|
-
// Calculate percentage of total time for each metric
|
|
25951
|
+
// Calculate percentage of total time for each metric. Guard against zero total
|
|
25952
|
+
// duration (all-instant operations) to avoid NaN% in the rendered report.
|
|
25610
25953
|
const percentages = sortedMetrics.map((metric) => {
|
|
25611
|
-
const
|
|
25954
|
+
const pctRaw = stats.totalDuration > 0
|
|
25955
|
+
? (metric.totalDuration / stats.totalDuration) * 100
|
|
25956
|
+
: 0;
|
|
25957
|
+
const pct = isUnsafe$2(pctRaw) ? 0 : pctRaw;
|
|
25612
25958
|
return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
|
|
25613
25959
|
});
|
|
25614
25960
|
return [
|
|
@@ -26377,6 +26723,25 @@ function isUnsafe(value) {
|
|
|
26377
26723
|
}
|
|
26378
26724
|
return false;
|
|
26379
26725
|
}
|
|
26726
|
+
/** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
|
|
26727
|
+
const MIN_SIGNALS_FOR_ANNUALIZATION = 10;
|
|
26728
|
+
/** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
|
|
26729
|
+
* sample size is too small to estimate variance meaningfully. */
|
|
26730
|
+
const MIN_SIGNALS_FOR_RATIOS = 10;
|
|
26731
|
+
/** Minimum calendar span (days) for trade-frequency extrapolation. */
|
|
26732
|
+
const MIN_CALENDAR_SPAN_DAYS = 14;
|
|
26733
|
+
/** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
|
|
26734
|
+
const MAX_TRADES_PER_YEAR = 365;
|
|
26735
|
+
/** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
|
|
26736
|
+
* blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
|
|
26737
|
+
* anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
|
|
26738
|
+
const MAX_EXPECTED_YEARLY_RETURNS = 100;
|
|
26739
|
+
/** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
|
|
26740
|
+
const MAX_CALMAR_RATIO = 1000;
|
|
26741
|
+
/** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
|
|
26742
|
+
* float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
|
|
26743
|
+
* astronomical magnitudes (avgPnl / epsilon). */
|
|
26744
|
+
const STDDEV_EPSILON = 1e-9;
|
|
26380
26745
|
/**
|
|
26381
26746
|
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
26382
26747
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
@@ -26418,7 +26783,7 @@ class HeatmapStorage {
|
|
|
26418
26783
|
* - **totalPnl** — sum of `pnlPercentage` across all signals
|
|
26419
26784
|
* - **avgPnl** — arithmetic mean of `pnlPercentage`
|
|
26420
26785
|
* - **stdDev** — population standard deviation of `pnlPercentage`
|
|
26421
|
-
* - **sharpeRatio** — `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
|
|
26786
|
+
* - **sharpeRatio** — per-trade Sharpe: `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
|
|
26422
26787
|
* - **maxDrawdown** — largest cumulative loss streak (absolute value of peak negative equity)
|
|
26423
26788
|
* - **profitFactor** — `sumWins / |sumLosses|`; requires at least one win and one loss
|
|
26424
26789
|
* - **avgWin / avgLoss** — mean of positive / negative trades respectively
|
|
@@ -26434,10 +26799,12 @@ class HeatmapStorage {
|
|
|
26434
26799
|
const totalTrades = signals.length;
|
|
26435
26800
|
const winCount = signals.filter((s) => s.pnl.pnlPercentage > 0).length;
|
|
26436
26801
|
const lossCount = signals.filter((s) => s.pnl.pnlPercentage < 0).length;
|
|
26437
|
-
//
|
|
26802
|
+
// Win rate excludes break-even trades from both numerator and denominator —
|
|
26803
|
+
// they are neither wins nor losses.
|
|
26438
26804
|
let winRate = null;
|
|
26439
|
-
|
|
26440
|
-
|
|
26805
|
+
const decisiveTrades = winCount + lossCount;
|
|
26806
|
+
if (decisiveTrades > 0) {
|
|
26807
|
+
winRate = (winCount / decisiveTrades) * 100;
|
|
26441
26808
|
}
|
|
26442
26809
|
// Calculate total PNL
|
|
26443
26810
|
let totalPnl = null;
|
|
@@ -26449,36 +26816,47 @@ class HeatmapStorage {
|
|
|
26449
26816
|
if (signals.length > 0) {
|
|
26450
26817
|
avgPnl = totalPnl / signals.length;
|
|
26451
26818
|
}
|
|
26452
|
-
//
|
|
26819
|
+
// Sample standard deviation (Bessel correction: divide by N-1, not N).
|
|
26820
|
+
// Per-symbol ratios are gated by MIN_SIGNALS_FOR_RATIOS — variance estimates from
|
|
26821
|
+
// tiny samples are too noisy to publish.
|
|
26822
|
+
const canComputeRatios = signals.length >= MIN_SIGNALS_FOR_RATIOS;
|
|
26453
26823
|
let stdDev = null;
|
|
26454
|
-
if (
|
|
26455
|
-
const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / signals.length;
|
|
26824
|
+
if (canComputeRatios && avgPnl !== null) {
|
|
26825
|
+
const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / (signals.length - 1);
|
|
26456
26826
|
stdDev = Math.sqrt(variance);
|
|
26457
26827
|
}
|
|
26458
|
-
//
|
|
26828
|
+
// Per-trade Sharpe Ratio
|
|
26459
26829
|
let sharpeRatio = null;
|
|
26460
|
-
|
|
26830
|
+
// STDDEV_EPSILON guard — protects against float-artifact stdDev producing
|
|
26831
|
+
// spuriously astronomical sharpe on identical-returns symbols.
|
|
26832
|
+
if (avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
|
|
26461
26833
|
sharpeRatio = avgPnl / stdDev;
|
|
26462
26834
|
}
|
|
26463
|
-
//
|
|
26835
|
+
// Equity-curve max drawdown via compounded equity ("as-if 100% allocation per trade").
|
|
26836
|
+
// Signals are stored newest-first (unshift in addSignal), so iterate in reverse.
|
|
26837
|
+
// If equity ≤ 0 — account blown, fix DD at 100%. equityFinal feeds expectedYearlyReturns.
|
|
26464
26838
|
let maxDrawdown = null;
|
|
26839
|
+
let equityFinal = 1;
|
|
26840
|
+
let blown = false;
|
|
26465
26841
|
if (signals.length > 0) {
|
|
26466
|
-
let
|
|
26467
|
-
let
|
|
26842
|
+
let equity = 1;
|
|
26843
|
+
let peak = 1;
|
|
26468
26844
|
let maxDD = 0;
|
|
26469
|
-
for (
|
|
26470
|
-
|
|
26471
|
-
if (
|
|
26472
|
-
|
|
26473
|
-
|
|
26474
|
-
|
|
26475
|
-
currentDrawdown = Math.abs(peak);
|
|
26476
|
-
if (currentDrawdown > maxDD) {
|
|
26477
|
-
maxDD = currentDrawdown;
|
|
26478
|
-
}
|
|
26845
|
+
for (let i = signals.length - 1; i >= 0; i--) {
|
|
26846
|
+
equity *= 1 + signals[i].pnl.pnlPercentage / 100;
|
|
26847
|
+
if (equity <= 0) {
|
|
26848
|
+
maxDD = 100;
|
|
26849
|
+
blown = true;
|
|
26850
|
+
break;
|
|
26479
26851
|
}
|
|
26852
|
+
if (equity > peak)
|
|
26853
|
+
peak = equity;
|
|
26854
|
+
const dd = (peak - equity) / peak * 100;
|
|
26855
|
+
if (dd > maxDD)
|
|
26856
|
+
maxDD = dd;
|
|
26480
26857
|
}
|
|
26481
26858
|
maxDrawdown = maxDD;
|
|
26859
|
+
equityFinal = blown ? 0 : equity;
|
|
26482
26860
|
}
|
|
26483
26861
|
// Calculate Profit Factor
|
|
26484
26862
|
let profitFactor = null;
|
|
@@ -26489,7 +26867,9 @@ class HeatmapStorage {
|
|
|
26489
26867
|
const sumLosses = Math.abs(signals
|
|
26490
26868
|
.filter((s) => s.pnl.pnlPercentage < 0)
|
|
26491
26869
|
.reduce((acc, s) => acc + s.pnl.pnlPercentage, 0));
|
|
26492
|
-
|
|
26870
|
+
// STDDEV_EPSILON guard — float-artifact losses (≈1e-15) would otherwise
|
|
26871
|
+
// produce spurious astronomical profitFactor (≈1e14).
|
|
26872
|
+
if (sumLosses > STDDEV_EPSILON) {
|
|
26493
26873
|
profitFactor = sumWins / sumLosses;
|
|
26494
26874
|
}
|
|
26495
26875
|
}
|
|
@@ -26529,45 +26909,110 @@ class HeatmapStorage {
|
|
|
26529
26909
|
}
|
|
26530
26910
|
}
|
|
26531
26911
|
}
|
|
26532
|
-
//
|
|
26912
|
+
// Expectancy — probabilities from observed win/loss counts (break-evens contribute 0).
|
|
26533
26913
|
let expectancy = null;
|
|
26534
|
-
if (
|
|
26535
|
-
const
|
|
26536
|
-
|
|
26914
|
+
if (totalTrades > 0 && avgWin !== null && avgLoss !== null) {
|
|
26915
|
+
const winProb = winCount / totalTrades;
|
|
26916
|
+
const lossProb = lossCount / totalTrades;
|
|
26917
|
+
expectancy = winProb * avgWin + lossProb * avgLoss;
|
|
26918
|
+
}
|
|
26919
|
+
else if (totalTrades > 0 && avgWin !== null && avgLoss === null) {
|
|
26920
|
+
// No losing trades — expectancy is just average win frequency × avgWin
|
|
26921
|
+
expectancy = (winCount / totalTrades) * avgWin;
|
|
26922
|
+
}
|
|
26923
|
+
else if (totalTrades > 0 && avgWin === null && avgLoss !== null) {
|
|
26924
|
+
expectancy = (lossCount / totalTrades) * avgLoss;
|
|
26537
26925
|
}
|
|
26538
|
-
//
|
|
26926
|
+
// Average only over signals that have the value — do not dilute the mean with zeros.
|
|
26539
26927
|
let avgPeakPnl = null;
|
|
26540
26928
|
let avgFallPnl = null;
|
|
26541
26929
|
if (signals.length > 0) {
|
|
26542
|
-
|
|
26543
|
-
|
|
26930
|
+
const peakValues = signals
|
|
26931
|
+
.map((s) => s.signal.peakProfit?.pnlPercentage)
|
|
26932
|
+
.filter((v) => typeof v === "number");
|
|
26933
|
+
const fallValues = signals
|
|
26934
|
+
.map((s) => s.signal.maxDrawdown?.pnlPercentage)
|
|
26935
|
+
.filter((v) => typeof v === "number");
|
|
26936
|
+
avgPeakPnl = peakValues.length > 0
|
|
26937
|
+
? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
|
|
26938
|
+
: null;
|
|
26939
|
+
avgFallPnl = fallValues.length > 0
|
|
26940
|
+
? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
|
|
26941
|
+
: null;
|
|
26544
26942
|
}
|
|
26545
|
-
//
|
|
26546
|
-
|
|
26547
|
-
//
|
|
26943
|
+
// Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
|
|
26944
|
+
// downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
|
|
26945
|
+
// so the numerator reduces to avgPnl and the squared term to r² for r < 0.
|
|
26946
|
+
// Dividing by N_total (not N_negative) properly penalises strategies with frequent
|
|
26947
|
+
// losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
|
|
26948
|
+
// strategies.
|
|
26548
26949
|
let sortinoRatio = null;
|
|
26549
|
-
if (
|
|
26550
|
-
const
|
|
26551
|
-
|
|
26552
|
-
|
|
26553
|
-
|
|
26554
|
-
|
|
26555
|
-
|
|
26556
|
-
|
|
26557
|
-
|
|
26558
|
-
|
|
26559
|
-
|
|
26560
|
-
|
|
26561
|
-
const avgDurationMs = signals.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / signals.length;
|
|
26562
|
-
const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
|
|
26563
|
-
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
26564
|
-
expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
26950
|
+
if (canComputeRatios && avgPnl !== null) {
|
|
26951
|
+
const negativeReturns = signals
|
|
26952
|
+
.map((s) => s.pnl.pnlPercentage)
|
|
26953
|
+
.filter((r) => r < 0);
|
|
26954
|
+
if (negativeReturns.length > 0) {
|
|
26955
|
+
const downsideVariance = negativeReturns.reduce((acc, r) => acc + r * r, 0) / signals.length;
|
|
26956
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
26957
|
+
// Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
|
|
26958
|
+
if (downsideDeviation > STDDEV_EPSILON) {
|
|
26959
|
+
sortinoRatio = avgPnl / downsideDeviation;
|
|
26960
|
+
}
|
|
26961
|
+
}
|
|
26565
26962
|
}
|
|
26963
|
+
// Expected yearly returns via geometric mean of equity curve.
|
|
26964
|
+
// equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag.
|
|
26965
|
+
// Gated by sample size and calendar span; if account blown → full loss.
|
|
26966
|
+
let expectedYearlyReturns = null;
|
|
26967
|
+
let tradesPerYear = null;
|
|
26968
|
+
if (signals.length >= MIN_SIGNALS_FOR_ANNUALIZATION) {
|
|
26969
|
+
let firstPendingAt = Infinity;
|
|
26970
|
+
let lastCloseAt = -Infinity;
|
|
26971
|
+
for (const s of signals) {
|
|
26972
|
+
if (s.signal.pendingAt < firstPendingAt)
|
|
26973
|
+
firstPendingAt = s.signal.pendingAt;
|
|
26974
|
+
if (s.closeTimestamp > lastCloseAt)
|
|
26975
|
+
lastCloseAt = s.closeTimestamp;
|
|
26976
|
+
}
|
|
26977
|
+
const calendarSpanDays = (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24);
|
|
26978
|
+
if (calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS) {
|
|
26979
|
+
// tradesPerYear uses RAW observed frequency — no clipping. If the raw value
|
|
26980
|
+
// exceeds MAX_TRADES_PER_YEAR the sample is too clustered for reliable
|
|
26981
|
+
// annualization, and we leave the annualized metric null instead of silently
|
|
26982
|
+
// understating it with a clipped frequency.
|
|
26983
|
+
const rawTradesPerYear = (signals.length / calendarSpanDays) * 365;
|
|
26984
|
+
if (rawTradesPerYear <= MAX_TRADES_PER_YEAR) {
|
|
26985
|
+
tradesPerYear = rawTradesPerYear;
|
|
26986
|
+
if (blown) {
|
|
26987
|
+
expectedYearlyReturns = -100;
|
|
26988
|
+
}
|
|
26989
|
+
else {
|
|
26990
|
+
// If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS, leave null rather than
|
|
26991
|
+
// show the cap — capped numbers mislead users into trusting them.
|
|
26992
|
+
const raw = (Math.pow(equityFinal, tradesPerYear / signals.length) - 1) * 100;
|
|
26993
|
+
expectedYearlyReturns = Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS ? null : raw;
|
|
26994
|
+
}
|
|
26995
|
+
}
|
|
26996
|
+
}
|
|
26997
|
+
}
|
|
26998
|
+
// Calmar = annualized return / equity-curve max drawdown, capped at ±MAX_CALMAR_RATIO.
|
|
26999
|
+
// Recovery Factor uses the compounded total return (equityFinal-1)*100, not arithmetic
|
|
27000
|
+
// totalPnl — denominator is compounded so numerator must match. Null when account blown.
|
|
26566
27001
|
let calmarRatio = null;
|
|
26567
27002
|
let recoveryFactor = null;
|
|
26568
|
-
if (
|
|
26569
|
-
|
|
26570
|
-
|
|
27003
|
+
if (maxDrawdown !== null && maxDrawdown > 0) {
|
|
27004
|
+
if (expectedYearlyReturns !== null) {
|
|
27005
|
+
const raw = expectedYearlyReturns / maxDrawdown;
|
|
27006
|
+
calmarRatio = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, raw));
|
|
27007
|
+
}
|
|
27008
|
+
if (!blown && canComputeRatios) {
|
|
27009
|
+
// Gated below MIN_SIGNALS_FOR_RATIOS like Sharpe — a Recovery Factor on
|
|
27010
|
+
// a handful of trades is statistically meaningless, so don't surface it
|
|
27011
|
+
// per-symbol while Sharpe is N/A.
|
|
27012
|
+
// Same MAX_CALMAR_RATIO clamp as Calmar — both compounded-profit/DD ratios.
|
|
27013
|
+
const rawRec = ((equityFinal - 1) * 100) / maxDrawdown;
|
|
27014
|
+
recoveryFactor = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawRec));
|
|
27015
|
+
}
|
|
26571
27016
|
}
|
|
26572
27017
|
// Apply safe math checks
|
|
26573
27018
|
if (isUnsafe(winRate))
|
|
@@ -26632,12 +27077,18 @@ class HeatmapStorage {
|
|
|
26632
27077
|
* 2. Sorts symbols by `sharpeRatio` descending — best performers first,
|
|
26633
27078
|
* symbols with `null` sharpeRatio placed at the end.
|
|
26634
27079
|
* 3. Computes portfolio-wide aggregates:
|
|
26635
|
-
* - `portfolioTotalPnl` — sum of
|
|
26636
|
-
*
|
|
26637
|
-
*
|
|
26638
|
-
*
|
|
26639
|
-
*
|
|
26640
|
-
*
|
|
27080
|
+
* - `portfolioTotalPnl` — sum of per-symbol `totalPnl` values, skipping `null` entries
|
|
27081
|
+
* (so a symbol with no data does not silently contribute 0). If every symbol's
|
|
27082
|
+
* `totalPnl` is null, the portfolio value is null.
|
|
27083
|
+
* - `portfolioTotalTrades` — sum of per-symbol `totalTrades`
|
|
27084
|
+
* - `portfolioSharpeRatio` — POOLED Sharpe over all trades across symbols (sample
|
|
27085
|
+
* stddev, N-1). NOT a Markowitz portfolio Sharpe — ignores cross-symbol
|
|
27086
|
+
* correlations and capital allocation. Rendered as "Pooled Sharpe" in the report.
|
|
27087
|
+
* Gated by `MIN_SIGNALS_FOR_RATIOS` on the pooled count.
|
|
27088
|
+
* - `portfolioAvgPeakPnl` / `portfolioAvgFallPnl` — trade-count-weighted means
|
|
27089
|
+
* over symbols that have non-null values.
|
|
27090
|
+
*
|
|
27091
|
+
* @returns Promise resolving to `HeatmapStatisticsModel`
|
|
26641
27092
|
*/
|
|
26642
27093
|
async getData() {
|
|
26643
27094
|
const symbols = [];
|
|
@@ -26656,31 +27107,53 @@ class HeatmapStorage {
|
|
|
26656
27107
|
return -1;
|
|
26657
27108
|
return b.sharpeRatio - a.sharpeRatio;
|
|
26658
27109
|
});
|
|
26659
|
-
//
|
|
27110
|
+
// Portfolio totals — sum only over symbols with non-null totalPnl. `s.totalPnl || 0`
|
|
27111
|
+
// would silently treat a missing value as zero and hide that some symbols had no data.
|
|
26660
27112
|
const totalSymbols = symbols.length;
|
|
26661
27113
|
let portfolioTotalPnl = null;
|
|
26662
27114
|
let portfolioTotalTrades = 0;
|
|
26663
27115
|
if (symbols.length > 0) {
|
|
26664
|
-
|
|
27116
|
+
const validTotalPnls = symbols.filter((s) => s.totalPnl !== null);
|
|
27117
|
+
portfolioTotalPnl = validTotalPnls.length > 0
|
|
27118
|
+
? validTotalPnls.reduce((acc, s) => acc + s.totalPnl, 0)
|
|
27119
|
+
: null;
|
|
26665
27120
|
portfolioTotalTrades = symbols.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
26666
27121
|
}
|
|
26667
|
-
//
|
|
27122
|
+
// Pooled Sharpe over all returns across symbols. NOTE: this is NOT a Markowitz
|
|
27123
|
+
// portfolio Sharpe — it ignores cross-symbol correlations and treats trades as a
|
|
27124
|
+
// single pooled sample. Gated by MIN_SIGNALS_FOR_RATIOS so a 2-trade pool cannot
|
|
27125
|
+
// produce a noisy ±Sharpe.
|
|
26668
27126
|
let portfolioSharpeRatio = null;
|
|
26669
|
-
const
|
|
26670
|
-
|
|
26671
|
-
|
|
26672
|
-
|
|
27127
|
+
const allReturns = [];
|
|
27128
|
+
for (const signals of this.symbolData.values()) {
|
|
27129
|
+
for (const s of signals) {
|
|
27130
|
+
allReturns.push(s.pnl.pnlPercentage);
|
|
27131
|
+
}
|
|
27132
|
+
}
|
|
27133
|
+
if (allReturns.length >= MIN_SIGNALS_FOR_RATIOS) {
|
|
27134
|
+
const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
|
|
27135
|
+
const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
|
|
27136
|
+
(allReturns.length - 1);
|
|
27137
|
+
const portfolioStdDev = Math.sqrt(portfolioVariance);
|
|
27138
|
+
// STDDEV_EPSILON guard — same protection as per-symbol Sharpe.
|
|
27139
|
+
if (portfolioStdDev > STDDEV_EPSILON) {
|
|
27140
|
+
portfolioSharpeRatio = portfolioAvg / portfolioStdDev;
|
|
27141
|
+
}
|
|
26673
27142
|
}
|
|
26674
|
-
//
|
|
27143
|
+
// Portfolio-wide weighted average peak/fall PNL. Denominator must include only
|
|
27144
|
+
// symbols that contributed a value — otherwise trade-count-weighted mean is diluted
|
|
27145
|
+
// by symbols without the metric.
|
|
26675
27146
|
let portfolioAvgPeakPnl = null;
|
|
26676
27147
|
let portfolioAvgFallPnl = null;
|
|
26677
27148
|
const validPeak = symbols.filter((s) => s.avgPeakPnl !== null);
|
|
26678
27149
|
const validFall = symbols.filter((s) => s.avgFallPnl !== null);
|
|
26679
|
-
|
|
26680
|
-
|
|
27150
|
+
const peakTradesTotal = validPeak.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
27151
|
+
const fallTradesTotal = validFall.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
27152
|
+
if (validPeak.length > 0 && peakTradesTotal > 0) {
|
|
27153
|
+
portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / peakTradesTotal;
|
|
26681
27154
|
}
|
|
26682
|
-
if (validFall.length > 0 &&
|
|
26683
|
-
portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) /
|
|
27155
|
+
if (validFall.length > 0 && fallTradesTotal > 0) {
|
|
27156
|
+
portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / fallTradesTotal;
|
|
26684
27157
|
}
|
|
26685
27158
|
// Apply safe math
|
|
26686
27159
|
if (isUnsafe(portfolioTotalPnl))
|
|
@@ -26708,7 +27181,7 @@ class HeatmapStorage {
|
|
|
26708
27181
|
* ```
|
|
26709
27182
|
* # Portfolio Heatmap: {strategyName}
|
|
26710
27183
|
*
|
|
26711
|
-
* **Total Symbols:** N | **Portfolio PNL:** X% | **
|
|
27184
|
+
* **Total Symbols:** N | **Portfolio PNL:** X% | **Pooled Sharpe:** Y | **Total Trades:** Z
|
|
26712
27185
|
*
|
|
26713
27186
|
* | col1 | col2 | ... |
|
|
26714
27187
|
* | --- | --- | ... |
|
|
@@ -26747,18 +27220,21 @@ class HeatmapStorage {
|
|
|
26747
27220
|
return [
|
|
26748
27221
|
`# Portfolio Heatmap: ${strategyName}`,
|
|
26749
27222
|
"",
|
|
26750
|
-
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **
|
|
27223
|
+
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? functoolsKit.str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? functoolsKit.str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
|
|
26751
27224
|
"",
|
|
26752
27225
|
table,
|
|
26753
27226
|
"",
|
|
26754
27227
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
27228
|
+
`*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.*`,
|
|
26755
27229
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals per symbol.*`,
|
|
26756
|
-
`*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
27230
|
+
`*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".*`,
|
|
26757
27231
|
`*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
|
|
26758
27232
|
`*Profit Factor: below 1.0 means strategy is losing overall. Above 1.5 is considered good.*`,
|
|
26759
|
-
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong.
|
|
26760
|
-
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
|
|
26761
|
-
`*All metrics require 100+ signals per symbol to be statistically reliable.
|
|
27233
|
+
`*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}.*`,
|
|
27234
|
+
`*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.*`,
|
|
27235
|
+
`*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
|
|
27236
|
+
`*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.*`,
|
|
27237
|
+
`*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.*`,
|
|
26762
27238
|
].join("\n");
|
|
26763
27239
|
}
|
|
26764
27240
|
/**
|
|
@@ -26953,7 +27429,7 @@ class HeatMarkdownService {
|
|
|
26953
27429
|
* console.log(markdown);
|
|
26954
27430
|
* // # Portfolio Heatmap: my-strategy
|
|
26955
27431
|
* //
|
|
26956
|
-
* // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **
|
|
27432
|
+
* // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Pooled Sharpe:** 1.85 | **Total Trades:** 120
|
|
26957
27433
|
* //
|
|
26958
27434
|
* // | Symbol | Total PNL | Sharpe | Max DD | Trades |
|
|
26959
27435
|
* // | --- | --- | --- | --- | --- |
|
|
@@ -53948,7 +54424,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY = "ReportUtils.useDummy";
|
|
|
53948
54424
|
const REPORT_UTILS_METHOD_NAME_USE_JSONL = "ReportUtils.useJsonl";
|
|
53949
54425
|
const REPORT_UTILS_METHOD_NAME_CLEAR = "ReportUtils.clear";
|
|
53950
54426
|
/** Logger service injected as DI singleton */
|
|
53951
|
-
const LOGGER_SERVICE$
|
|
54427
|
+
const LOGGER_SERVICE$3 = new LoggerService();
|
|
53952
54428
|
/**
|
|
53953
54429
|
* Default configuration that enables all report services.
|
|
53954
54430
|
* Used when no specific configuration is provided to enable().
|
|
@@ -54005,7 +54481,7 @@ class ReportUtils {
|
|
|
54005
54481
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
54006
54482
|
*/
|
|
54007
54483
|
this.enable = functoolsKit.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) => {
|
|
54008
|
-
LOGGER_SERVICE$
|
|
54484
|
+
LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
|
|
54009
54485
|
backtest: bt,
|
|
54010
54486
|
breakeven,
|
|
54011
54487
|
heat,
|
|
@@ -54100,7 +54576,7 @@ class ReportUtils {
|
|
|
54100
54576
|
* ```
|
|
54101
54577
|
*/
|
|
54102
54578
|
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) => {
|
|
54103
|
-
LOGGER_SERVICE$
|
|
54579
|
+
LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
|
|
54104
54580
|
backtest: bt,
|
|
54105
54581
|
breakeven,
|
|
54106
54582
|
heat,
|
|
@@ -54180,7 +54656,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54180
54656
|
* @param Ctor - Constructor for report storage adapter
|
|
54181
54657
|
*/
|
|
54182
54658
|
useReportAdapter(Ctor) {
|
|
54183
|
-
LOGGER_SERVICE$
|
|
54659
|
+
LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
|
|
54184
54660
|
ReportWriter.useReportAdapter(Ctor);
|
|
54185
54661
|
}
|
|
54186
54662
|
/**
|
|
@@ -54189,7 +54665,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54189
54665
|
* so new storage instances are created with the updated base path.
|
|
54190
54666
|
*/
|
|
54191
54667
|
clear() {
|
|
54192
|
-
LOGGER_SERVICE$
|
|
54668
|
+
LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_CLEAR);
|
|
54193
54669
|
ReportWriter.clear();
|
|
54194
54670
|
}
|
|
54195
54671
|
/**
|
|
@@ -54197,7 +54673,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54197
54673
|
* All future report writes will be no-ops.
|
|
54198
54674
|
*/
|
|
54199
54675
|
useDummy() {
|
|
54200
|
-
LOGGER_SERVICE$
|
|
54676
|
+
LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
|
|
54201
54677
|
ReportWriter.useDummy();
|
|
54202
54678
|
}
|
|
54203
54679
|
/**
|
|
@@ -54205,7 +54681,7 @@ class ReportAdapter extends ReportUtils {
|
|
|
54205
54681
|
* All future report writes will use JSONL storage.
|
|
54206
54682
|
*/
|
|
54207
54683
|
useJsonl() {
|
|
54208
|
-
LOGGER_SERVICE$
|
|
54684
|
+
LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
|
|
54209
54685
|
ReportWriter.useJsonl();
|
|
54210
54686
|
}
|
|
54211
54687
|
}
|
|
@@ -54223,7 +54699,7 @@ const MARKDOWN_METHOD_NAME_USE_JSONL = "MarkdownAdapter.useJsonl";
|
|
|
54223
54699
|
const MARKDOWN_METHOD_NAME_USE_DUMMY = "MarkdownAdapter.useDummy";
|
|
54224
54700
|
const MARKDOWN_METHOD_NAME_CLEAR = "MarkdownAdapter.clear";
|
|
54225
54701
|
/** Logger service injected as DI singleton */
|
|
54226
|
-
const LOGGER_SERVICE$
|
|
54702
|
+
const LOGGER_SERVICE$2 = new LoggerService();
|
|
54227
54703
|
/**
|
|
54228
54704
|
* Default configuration that enables all markdown services.
|
|
54229
54705
|
* Used when no specific configuration is provided to `enable()`.
|
|
@@ -54280,7 +54756,7 @@ class MarkdownUtils {
|
|
|
54280
54756
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
54281
54757
|
*/
|
|
54282
54758
|
this.enable = functoolsKit.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) => {
|
|
54283
|
-
LOGGER_SERVICE$
|
|
54759
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_ENABLE, {
|
|
54284
54760
|
backtest: bt,
|
|
54285
54761
|
breakeven,
|
|
54286
54762
|
heat,
|
|
@@ -54377,7 +54853,7 @@ class MarkdownUtils {
|
|
|
54377
54853
|
* ```
|
|
54378
54854
|
*/
|
|
54379
54855
|
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) => {
|
|
54380
|
-
LOGGER_SERVICE$
|
|
54856
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_DISABLE, {
|
|
54381
54857
|
backtest: bt,
|
|
54382
54858
|
breakeven,
|
|
54383
54859
|
heat,
|
|
@@ -54463,7 +54939,7 @@ class MarkdownUtils {
|
|
|
54463
54939
|
* @param config.max_drawdown - Clear max drawdown report data
|
|
54464
54940
|
*/
|
|
54465
54941
|
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) => {
|
|
54466
|
-
LOGGER_SERVICE$
|
|
54942
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_CLEAR, {
|
|
54467
54943
|
backtest: bt,
|
|
54468
54944
|
breakeven,
|
|
54469
54945
|
heat,
|
|
@@ -54538,7 +55014,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54538
55014
|
* @param Ctor - Constructor for markdown storage adapter
|
|
54539
55015
|
*/
|
|
54540
55016
|
useMarkdownAdapter(Ctor) {
|
|
54541
|
-
LOGGER_SERVICE$
|
|
55017
|
+
LOGGER_SERVICE$2.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
|
|
54542
55018
|
return MarkdownWriter.useMarkdownAdapter(Ctor);
|
|
54543
55019
|
}
|
|
54544
55020
|
/**
|
|
@@ -54547,7 +55023,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54547
55023
|
* Each dump creates a separate .md file.
|
|
54548
55024
|
*/
|
|
54549
55025
|
useMd() {
|
|
54550
|
-
LOGGER_SERVICE$
|
|
55026
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_MD);
|
|
54551
55027
|
MarkdownWriter.useMd();
|
|
54552
55028
|
}
|
|
54553
55029
|
/**
|
|
@@ -54556,7 +55032,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54556
55032
|
* All dumps append to a single .jsonl file per markdown type.
|
|
54557
55033
|
*/
|
|
54558
55034
|
useJsonl() {
|
|
54559
|
-
LOGGER_SERVICE$
|
|
55035
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_JSONL);
|
|
54560
55036
|
MarkdownWriter.useJsonl();
|
|
54561
55037
|
}
|
|
54562
55038
|
/**
|
|
@@ -54564,7 +55040,7 @@ class MarkdownAdapter extends MarkdownUtils {
|
|
|
54564
55040
|
* All future markdown writes will be no-ops.
|
|
54565
55041
|
*/
|
|
54566
55042
|
useDummy() {
|
|
54567
|
-
LOGGER_SERVICE$
|
|
55043
|
+
LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_DUMMY);
|
|
54568
55044
|
MarkdownWriter.useDummy();
|
|
54569
55045
|
}
|
|
54570
55046
|
}
|
|
@@ -63253,6 +63729,503 @@ class IntervalUtils {
|
|
|
63253
63729
|
*/
|
|
63254
63730
|
const Interval = new IntervalUtils();
|
|
63255
63731
|
|
|
63732
|
+
const CRON_METHOD_NAME_REGISTER = "CronUtils.register";
|
|
63733
|
+
const CRON_METHOD_NAME_UNREGISTER = "CronUtils.unregister";
|
|
63734
|
+
const CRON_METHOD_NAME_CLEAR = "CronUtils.clear";
|
|
63735
|
+
const CRON_METHOD_NAME_TICK = "CronUtils._tick";
|
|
63736
|
+
const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
|
|
63737
|
+
const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
|
|
63738
|
+
const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
|
|
63739
|
+
/**
|
|
63740
|
+
* Local logger instance.
|
|
63741
|
+
*
|
|
63742
|
+
* Created directly rather than resolved from the DI container so that
|
|
63743
|
+
* `CronUtils` has no compile-time dependency on the rest of the framework
|
|
63744
|
+
* being bootstrapped — `Cron` can be imported and used in isolation.
|
|
63745
|
+
*/
|
|
63746
|
+
const LOGGER_SERVICE$1 = new LoggerService();
|
|
63747
|
+
/**
|
|
63748
|
+
* Utility class for registering periodic tasks that fire on candle-interval
|
|
63749
|
+
* boundaries of the virtual time produced by parallel backtests.
|
|
63750
|
+
*
|
|
63751
|
+
* Exported as singleton instance `Cron` for convenient usage.
|
|
63752
|
+
*
|
|
63753
|
+
* Key property — **singleshot coordination across parallel backtests**:
|
|
63754
|
+
* when several `Backtest.background(symbol, ...)` runs hit the same aligned
|
|
63755
|
+
* boundary concurrently, the handler is invoked exactly once. Every parallel
|
|
63756
|
+
* `tick` for that boundary awaits the same in-flight promise and is released
|
|
63757
|
+
* together when the promise settles. After settlement the slot is cleared and
|
|
63758
|
+
* the next boundary produces a fresh promise.
|
|
63759
|
+
*
|
|
63760
|
+
* Typical wiring:
|
|
63761
|
+
*
|
|
63762
|
+
* @example
|
|
63763
|
+
* ```typescript
|
|
63764
|
+
* import { Cron, Backtest } from "backtest-kit";
|
|
63765
|
+
*
|
|
63766
|
+
* Cron.register({
|
|
63767
|
+
* name: "tg-signal-parser",
|
|
63768
|
+
* interval: "1h",
|
|
63769
|
+
* handler: async (symbol, when, backtest) => {
|
|
63770
|
+
* await parseTelegramSignalsToMongo(when);
|
|
63771
|
+
* },
|
|
63772
|
+
* });
|
|
63773
|
+
*
|
|
63774
|
+
* // Subscribe Cron to the engine's lifecycle subjects (beforeStart,
|
|
63775
|
+
* // idlePing, activePing, schedulePing) once at startup. After this every
|
|
63776
|
+
* // strategy tick is forwarded into Cron automatically.
|
|
63777
|
+
* Cron.enable();
|
|
63778
|
+
*
|
|
63779
|
+
* for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "TRXUSDT"]) {
|
|
63780
|
+
* Backtest.background(symbol, { strategyName, exchangeName, frameName });
|
|
63781
|
+
* }
|
|
63782
|
+
*
|
|
63783
|
+
* // On shutdown:
|
|
63784
|
+
* // Cron.disable();
|
|
63785
|
+
* ```
|
|
63786
|
+
*/
|
|
63787
|
+
class CronUtils {
|
|
63788
|
+
constructor() {
|
|
63789
|
+
/**
|
|
63790
|
+
* Registered entries by `name`.
|
|
63791
|
+
*
|
|
63792
|
+
* Each record carries a monotonically increasing `generation` counter that
|
|
63793
|
+
* is bumped on every `register(entry)` call for the same name. The
|
|
63794
|
+
* generation participates in `firedKey` so writes from a still-in-flight
|
|
63795
|
+
* handler of a previous incarnation cannot poison `_firedOnce` for the
|
|
63796
|
+
* current incarnation — their key has a different generation suffix and
|
|
63797
|
+
* is simply ignored on lookup.
|
|
63798
|
+
*/
|
|
63799
|
+
this._entries = new Map();
|
|
63800
|
+
/** Monotonic counter used to mint new entry generations on `register`. */
|
|
63801
|
+
this._generationCounter = 0;
|
|
63802
|
+
/**
|
|
63803
|
+
* In-flight handler slots.
|
|
63804
|
+
*
|
|
63805
|
+
* Slot key shape (always includes the generation suffix `:g${generation}`;
|
|
63806
|
+
* the `:${symbol}` scope is present only in fan-out mode):
|
|
63807
|
+
* - Periodic global: `${name}:${alignedMs}:g${generation}`.
|
|
63808
|
+
* - Periodic fan-out: `${name}:${alignedMs}:${symbol}:g${generation}`.
|
|
63809
|
+
* - Fire-once global: `${name}:once:g${generation}`.
|
|
63810
|
+
* - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
|
|
63811
|
+
*
|
|
63812
|
+
* Value is the shared in-flight handler promise. Every parallel `tick` for
|
|
63813
|
+
* the same slot key awaits this exact promise (mutex semantics) and is
|
|
63814
|
+
* released together when it settles. `_inFlight` is owned exclusively by
|
|
63815
|
+
* `_runEntry` — `clear()` does **not** touch it, so the singleshot promise
|
|
63816
|
+
* survives concurrent `clear` calls and continues to coordinate parallel
|
|
63817
|
+
* ticks until it settles.
|
|
63818
|
+
*/
|
|
63819
|
+
this._inFlight = new Map();
|
|
63820
|
+
/**
|
|
63821
|
+
* Keys of fire-once entries whose handler has already settled successfully.
|
|
63822
|
+
*
|
|
63823
|
+
* Key shape (always includes the entry generation suffix `:g${generation}`):
|
|
63824
|
+
* - Global fire-once: `${name}:g${generation}`.
|
|
63825
|
+
* - Fan-out fire-once: `${name}:${symbol}:g${generation}` — one entry per
|
|
63826
|
+
* whitelisted symbol.
|
|
63827
|
+
*
|
|
63828
|
+
* The generation suffix isolates incarnations of the same `name`: writes
|
|
63829
|
+
* landing from a still-in-flight handler of a previous `register()` carry
|
|
63830
|
+
* the old generation and are never matched by the new entry's lookup.
|
|
63831
|
+
* Stale entries are pruned by `_clearFiredOnceFor` on `register`/`unregister`
|
|
63832
|
+
* and wiped by `clear()`.
|
|
63833
|
+
*
|
|
63834
|
+
* Looked up by `_tick` to decide whether to skip; written by `_runEntry`
|
|
63835
|
+
* on successful settle.
|
|
63836
|
+
*/
|
|
63837
|
+
this._firedOnce = new Set();
|
|
63838
|
+
/**
|
|
63839
|
+
* Register a periodic cron entry.
|
|
63840
|
+
*
|
|
63841
|
+
* Idempotent on `name`: re-registering the same name replaces the previous
|
|
63842
|
+
* entry (interval/symbols/handler can all change). Re-registration does
|
|
63843
|
+
* **not** clear in-flight promises — entries still resolving complete with
|
|
63844
|
+
* the previous handler.
|
|
63845
|
+
*
|
|
63846
|
+
* @param entry - Entry configuration; see {@link CronEntry}.
|
|
63847
|
+
* @returns Disposer function — call it to unregister the entry.
|
|
63848
|
+
*
|
|
63849
|
+
* @example
|
|
63850
|
+
* ```typescript
|
|
63851
|
+
* const dispose = Cron.register({
|
|
63852
|
+
* name: "fetch-funding",
|
|
63853
|
+
* interval: "8h",
|
|
63854
|
+
* symbols: ["BTCUSDT", "ETHUSDT"],
|
|
63855
|
+
* handler: async (symbol, when, backtest) => { ... },
|
|
63856
|
+
* });
|
|
63857
|
+
* // Later:
|
|
63858
|
+
* dispose();
|
|
63859
|
+
* ```
|
|
63860
|
+
*/
|
|
63861
|
+
this.register = (entry) => {
|
|
63862
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_REGISTER, {
|
|
63863
|
+
name: entry.name,
|
|
63864
|
+
interval: entry.interval,
|
|
63865
|
+
symbols: entry.symbols,
|
|
63866
|
+
});
|
|
63867
|
+
if (!entry.name) {
|
|
63868
|
+
throw new Error("CronUtils.register requires a non-empty name");
|
|
63869
|
+
}
|
|
63870
|
+
if (entry.name.includes(":")) {
|
|
63871
|
+
throw new Error(`CronUtils.register: name must not contain ':' (got "${entry.name}"). ` +
|
|
63872
|
+
`':' is reserved as the segment separator in slot keys.`);
|
|
63873
|
+
}
|
|
63874
|
+
if (entry.symbols) {
|
|
63875
|
+
for (const symbol of entry.symbols) {
|
|
63876
|
+
if (symbol.includes(":")) {
|
|
63877
|
+
throw new Error(`CronUtils.register: symbols[] entry must not contain ':' (got "${symbol}"). ` +
|
|
63878
|
+
`':' is reserved as the segment separator in slot keys.`);
|
|
63879
|
+
}
|
|
63880
|
+
}
|
|
63881
|
+
}
|
|
63882
|
+
this._clearFiredOnceFor(entry.name);
|
|
63883
|
+
const generation = ++this._generationCounter;
|
|
63884
|
+
this._entries.set(entry.name, { entry, generation });
|
|
63885
|
+
return () => this.unregister(entry.name);
|
|
63886
|
+
};
|
|
63887
|
+
/**
|
|
63888
|
+
* Remove a registered entry by name.
|
|
63889
|
+
*
|
|
63890
|
+
* Does not cancel handlers already in flight — those resolve on their own
|
|
63891
|
+
* and clear their slot via `.finally()`.
|
|
63892
|
+
*
|
|
63893
|
+
* @param name - Name passed to `register`.
|
|
63894
|
+
*/
|
|
63895
|
+
this.unregister = (name) => {
|
|
63896
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_UNREGISTER, { name });
|
|
63897
|
+
this._entries.delete(name);
|
|
63898
|
+
this._clearFiredOnceFor(name);
|
|
63899
|
+
};
|
|
63900
|
+
/**
|
|
63901
|
+
* Clear fire-once marks so that fire-once entries can fire again.
|
|
63902
|
+
*
|
|
63903
|
+
* Does **not** touch `_inFlight` — that map holds shared in-flight handler
|
|
63904
|
+
* promises through which parallel `tick`s coordinate. Wiping it mid-flight
|
|
63905
|
+
* would let a new `tick` start a second handler for a boundary that's
|
|
63906
|
+
* already running, breaking the singleshot contract.
|
|
63907
|
+
*
|
|
63908
|
+
* Two modes:
|
|
63909
|
+
* - **Per-symbol** (`symbol` provided): clears only fan-out fire-once
|
|
63910
|
+
* marks for that symbol — keys of the shape `${name}:${symbol}:g${gen}`.
|
|
63911
|
+
* Global fire-once marks (`${name}:g${gen}`, no symbol component) are
|
|
63912
|
+
* left intact, since they are not attributable to a single symbol.
|
|
63913
|
+
* Useful for re-arming fan-out fire-once entries when a particular
|
|
63914
|
+
* symbol's run finishes and you want a future re-run to fire again.
|
|
63915
|
+
* - **All** (no argument): wipes every fire-once mark across all entries
|
|
63916
|
+
* and symbols. Registered entries are not removed — use `unregister`
|
|
63917
|
+
* (or the disposer returned by `register`) for that.
|
|
63918
|
+
*
|
|
63919
|
+
* **Race with in-flight handlers.** `_firedOnce` is written in
|
|
63920
|
+
* `_runEntry`'s `.finally()`, which can run *after* a concurrent
|
|
63921
|
+
* `clear()` call. In that case the fire-once mark reappears immediately
|
|
63922
|
+
* after being wiped, and the next tick will treat the entry as already
|
|
63923
|
+
* fired. This is consistent with the singleshot promise itself surviving
|
|
63924
|
+
* `clear()` — the handler is allowed to finish — and the entry's
|
|
63925
|
+
* generation suffix in `firedKey` guarantees the stale mark cannot
|
|
63926
|
+
* outlive a subsequent `register()` of the same name. If you need a hard
|
|
63927
|
+
* re-arm, `unregister` + `register` bumps the generation and makes any
|
|
63928
|
+
* late write a no-op.
|
|
63929
|
+
*
|
|
63930
|
+
* @param symbol - Optional symbol filter; if omitted, clears all fire-once
|
|
63931
|
+
* marks.
|
|
63932
|
+
*/
|
|
63933
|
+
this.clear = (symbol) => {
|
|
63934
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_CLEAR, { symbol });
|
|
63935
|
+
if (!symbol) {
|
|
63936
|
+
this._firedOnce.clear();
|
|
63937
|
+
return;
|
|
63938
|
+
}
|
|
63939
|
+
const symbolSegment = `:${symbol}:`;
|
|
63940
|
+
for (const key of this._firedOnce) {
|
|
63941
|
+
if (key.includes(symbolSegment)) {
|
|
63942
|
+
this._firedOnce.delete(key);
|
|
63943
|
+
}
|
|
63944
|
+
}
|
|
63945
|
+
};
|
|
63946
|
+
/**
|
|
63947
|
+
* Process a virtual-time tick for `symbol` and fire any due cron entries.
|
|
63948
|
+
*
|
|
63949
|
+
* **Private.** Invoked exclusively by the lifecycle bridge installed in
|
|
63950
|
+
* {@link enable} — `beforeStart` / `idlePing` / `activePing` / `schedulePing`
|
|
63951
|
+
* are funneled here through a shared `singlerun` queue, so calls to
|
|
63952
|
+
* `_tick` are serialised end-to-end. Do not call directly.
|
|
63953
|
+
*
|
|
63954
|
+
* Algorithm (per registered entry):
|
|
63955
|
+
* 0. Base-align the incoming `when` down to the 1-minute boundary (`ts`).
|
|
63956
|
+
* Lifecycle subjects may emit with sub-second jitter; rounding here
|
|
63957
|
+
* guarantees that `beforeStart` / `idlePing` / `activePing` /
|
|
63958
|
+
* `schedulePing` for the same virtual minute all hash to the same
|
|
63959
|
+
* slot key.
|
|
63960
|
+
* 1. If `entry.symbols` is non-empty and does not include `symbol`, skip.
|
|
63961
|
+
* 2. Decide scope from `entry.symbols`:
|
|
63962
|
+
* - Empty/undefined → **global** (slot key has no symbol component).
|
|
63963
|
+
* - Non-empty → **fan-out**, slot key carries `:${symbol}` so each
|
|
63964
|
+
* whitelisted symbol gets its own slot and handler invocation.
|
|
63965
|
+
* 3. Append the current entry generation suffix `:g${generation}` to both
|
|
63966
|
+
* slot key and fired-once key. This isolates incarnations of the same
|
|
63967
|
+
* `name`: a `register()` after an in-flight handler bumps the
|
|
63968
|
+
* generation, so the late `_firedOnce` write from the old handler can
|
|
63969
|
+
* never block the new entry.
|
|
63970
|
+
* 4. **Fire-once** (`entry.interval === undefined`):
|
|
63971
|
+
* - If the entry's fired-once key is already in `_firedOnce`, skip.
|
|
63972
|
+
* - Slot key: `${name}:once` (+ scope) (+ gen).
|
|
63973
|
+
* - `aligned` = the 1-minute-aligned `when` from step 0.
|
|
63974
|
+
* 5. **Periodic** (`entry.interval` set):
|
|
63975
|
+
* - Align `when` further to the entry's interval via {@link alignToInterval}.
|
|
63976
|
+
* - If `ts !== alignedMs`, the tick is mid-interval — skip.
|
|
63977
|
+
* (This is the "remainder === 0" boundary check from the spec;
|
|
63978
|
+
* since `ts` is already on the 1-minute boundary, the check is exact
|
|
63979
|
+
* for `1m` and consistent for higher intervals.)
|
|
63980
|
+
* - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
|
|
63981
|
+
* 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
|
|
63982
|
+
* already exists, `await` the same promise. Otherwise invoke
|
|
63983
|
+
* `entry.handler`, store the promise, and `await` it. The slot is
|
|
63984
|
+
* removed in `.finally()` so the next boundary creates a fresh promise;
|
|
63985
|
+
* for fire-once entries the fired-once key is also added to
|
|
63986
|
+
* `_firedOnce` on success so subsequent ticks skip it.
|
|
63987
|
+
*
|
|
63988
|
+
* Errors thrown by `handler` are caught, logged via `console.error`, and
|
|
63989
|
+
* **not** rethrown — a failing handler must not break the per-symbol
|
|
63990
|
+
* tick loop or unblock other parallel backtests with an unhandled
|
|
63991
|
+
* rejection. A failed fire-once handler is **not** marked as fired and
|
|
63992
|
+
* will retry on the next tick.
|
|
63993
|
+
*
|
|
63994
|
+
* Requires active method context and execution context.
|
|
63995
|
+
*
|
|
63996
|
+
* @param symbol - Trading symbol from the current tick.
|
|
63997
|
+
* @param when - Virtual time of the current tick.
|
|
63998
|
+
* @param backtest - `true` for backtest ticks, `false` for live ticks.
|
|
63999
|
+
* Forwarded as the third argument to `entry.handler`. Only the value
|
|
64000
|
+
* from the tick that **opens** a given slot is observed by all parallel
|
|
64001
|
+
* awaiters of that slot.
|
|
64002
|
+
* @throws Error if method or execution context is missing.
|
|
64003
|
+
*/
|
|
64004
|
+
this._tick = async (symbol, when, backtest) => {
|
|
64005
|
+
LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
|
|
64006
|
+
symbol,
|
|
64007
|
+
when,
|
|
64008
|
+
});
|
|
64009
|
+
if (!MethodContextService.hasContext()) {
|
|
64010
|
+
throw new Error("CronUtils _tick requires method context");
|
|
64011
|
+
}
|
|
64012
|
+
if (!ExecutionContextService.hasContext()) {
|
|
64013
|
+
throw new Error("CronUtils _tick requires execution context");
|
|
64014
|
+
}
|
|
64015
|
+
const ts = alignToInterval(when, "1m").getTime();
|
|
64016
|
+
const taskList = [];
|
|
64017
|
+
for (const { entry, generation } of this._entries.values()) {
|
|
64018
|
+
if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
|
|
64019
|
+
continue;
|
|
64020
|
+
}
|
|
64021
|
+
const perSymbol = !!entry.symbols?.length;
|
|
64022
|
+
const scope = perSymbol ? `:${symbol}` : "";
|
|
64023
|
+
const genSuffix = `:g${generation}`;
|
|
64024
|
+
let aligned;
|
|
64025
|
+
let alignedMs;
|
|
64026
|
+
let slotKey;
|
|
64027
|
+
let firedKey;
|
|
64028
|
+
if (entry.interval === undefined) {
|
|
64029
|
+
const onceKey = `${entry.name}${scope}${genSuffix}`;
|
|
64030
|
+
if (this._firedOnce.has(onceKey)) {
|
|
64031
|
+
continue;
|
|
64032
|
+
}
|
|
64033
|
+
aligned = alignToInterval(when, "1m");
|
|
64034
|
+
alignedMs = ts;
|
|
64035
|
+
slotKey = `${entry.name}:once${scope}${genSuffix}`;
|
|
64036
|
+
firedKey = onceKey;
|
|
64037
|
+
}
|
|
64038
|
+
else {
|
|
64039
|
+
aligned = alignToInterval(when, entry.interval);
|
|
64040
|
+
alignedMs = aligned.getTime();
|
|
64041
|
+
if (ts !== alignedMs) {
|
|
64042
|
+
continue;
|
|
64043
|
+
}
|
|
64044
|
+
slotKey = `${entry.name}:${alignedMs}${scope}${genSuffix}`;
|
|
64045
|
+
firedKey = null;
|
|
64046
|
+
}
|
|
64047
|
+
let pending = this._inFlight.get(slotKey);
|
|
64048
|
+
if (!pending) {
|
|
64049
|
+
pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
|
|
64050
|
+
this._inFlight.set(slotKey, pending);
|
|
64051
|
+
}
|
|
64052
|
+
taskList.push(pending);
|
|
64053
|
+
}
|
|
64054
|
+
await Promise.all(taskList);
|
|
64055
|
+
};
|
|
64056
|
+
/**
|
|
64057
|
+
* Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
|
|
64058
|
+
* entries fire automatically — no manual wiring of `listenTickBacktest` /
|
|
64059
|
+
* `listenSchedulePing` etc. needed.
|
|
64060
|
+
*
|
|
64061
|
+
* Subjects funneled into {@link _tick}:
|
|
64062
|
+
* - `beforeStartSubject` — first event of every run.
|
|
64063
|
+
* - `idlePingSubject` — every tick when no signal is pending or scheduled.
|
|
64064
|
+
* - `activePingSubject` — every tick while a pending signal is being monitored.
|
|
64065
|
+
* - `schedulePingSubject` — every tick while a scheduled signal is being monitored.
|
|
64066
|
+
*
|
|
64067
|
+
* All four subjects are subscribed to a single `singlerun`-wrapped
|
|
64068
|
+
* handler that builds `_tick(event.symbol, new Date(event.timestamp),
|
|
64069
|
+
* event.backtest)`. `singlerun` merges the four streams into one serial
|
|
64070
|
+
* queue: at most one `_tick` runs at a time, the next waits. This matters
|
|
64071
|
+
* because the engine can emit `beforeStart` and an immediate `idlePing`
|
|
64072
|
+
* on the very same minute, and concurrent `_tick`s on the same
|
|
64073
|
+
* `(symbol, minute)` would otherwise race to open the same `_inFlight`
|
|
64074
|
+
* slot before either commit. Together these four sources cover every
|
|
64075
|
+
* tick the engine processes for every `(symbol, virtual-minute)` pair
|
|
64076
|
+
* regardless of whether the strategy is idle, active, or scheduled.
|
|
64077
|
+
*
|
|
64078
|
+
* `enable` itself is wrapped in `singleshot`, so calling it repeatedly is
|
|
64079
|
+
* a no-op — subsequent calls return the same disposer. The disposer
|
|
64080
|
+
* unsubscribes from every subject and resets the singleshot so a future
|
|
64081
|
+
* `enable()` can re-subscribe cleanly. Equivalent to the
|
|
64082
|
+
* `RecentAdapter.enable` pattern.
|
|
64083
|
+
*
|
|
64084
|
+
* The `.subscribe` callbacks are synchronous wrappers around the
|
|
64085
|
+
* `singlerun`-async handler; `_tick`'s returned promise is awaited inside
|
|
64086
|
+
* `singlerun` to enforce ordering but not bubbled back to the subject.
|
|
64087
|
+
* Errors are caught and logged inside `_runEntry`.
|
|
64088
|
+
*
|
|
64089
|
+
* @returns Cleanup function that unsubscribes from all four subjects and
|
|
64090
|
+
* resets the singleshot. Idempotent.
|
|
64091
|
+
*
|
|
64092
|
+
* @example
|
|
64093
|
+
* ```typescript
|
|
64094
|
+
* import { Cron } from "backtest-kit";
|
|
64095
|
+
*
|
|
64096
|
+
* Cron.register({ name: "tg-parser", interval: "1h", handler });
|
|
64097
|
+
* Cron.enable(); // wire once at startup
|
|
64098
|
+
* // ... run backtests / live as usual
|
|
64099
|
+
* Cron.disable(); // on shutdown
|
|
64100
|
+
* ```
|
|
64101
|
+
*/
|
|
64102
|
+
this.enable = functoolsKit.singleshot(() => {
|
|
64103
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
|
|
64104
|
+
const handleTick = functoolsKit.singlerun(async (event) => {
|
|
64105
|
+
return await this._tick(event.symbol, new Date(event.timestamp), event.backtest);
|
|
64106
|
+
});
|
|
64107
|
+
const unBeforeStart = beforeStartSubject.subscribe(handleTick);
|
|
64108
|
+
const unIdlePing = idlePingSubject.subscribe(handleTick);
|
|
64109
|
+
const unActivePing = activePingSubject.subscribe(handleTick);
|
|
64110
|
+
const unSchedulePing = schedulePingSubject.subscribe(handleTick);
|
|
64111
|
+
return functoolsKit.compose(() => unBeforeStart(), () => unIdlePing(), () => unActivePing(), () => unSchedulePing(), () => this.enable.clear());
|
|
64112
|
+
});
|
|
64113
|
+
/**
|
|
64114
|
+
* Tear down the lifecycle subscriptions installed by {@link enable}.
|
|
64115
|
+
*
|
|
64116
|
+
* Safe to call multiple times and safe to call before `enable()` — both
|
|
64117
|
+
* are no-ops. Does **not** unregister entries, does **not** touch
|
|
64118
|
+
* `_inFlight`, and does **not** wipe `_firedOnce` (use `unregister` or
|
|
64119
|
+
* `clear()` for those).
|
|
64120
|
+
*/
|
|
64121
|
+
this.disable = () => {
|
|
64122
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISABLE);
|
|
64123
|
+
if (this.enable.hasValue()) {
|
|
64124
|
+
const lastSubscription = this.enable();
|
|
64125
|
+
lastSubscription();
|
|
64126
|
+
}
|
|
64127
|
+
};
|
|
64128
|
+
/**
|
|
64129
|
+
* Hard-reset the entire `Cron` state.
|
|
64130
|
+
*
|
|
64131
|
+
* Performs in order:
|
|
64132
|
+
* 1. {@link disable} — tears down lifecycle subscriptions and resets the
|
|
64133
|
+
* `enable` singleshot so a future `enable()` re-subscribes cleanly.
|
|
64134
|
+
* 2. Wipes `_entries` — every {@link register}'ed entry is forgotten.
|
|
64135
|
+
* Disposers returned by previous `register()` calls become no-ops
|
|
64136
|
+
* (their `unregister(name)` will not find anything to remove).
|
|
64137
|
+
* 3. Wipes `_firedOnce` — all fire-once marks are dropped, so any future
|
|
64138
|
+
* re-registration of the same `name` fires again on the next matching
|
|
64139
|
+
* tick.
|
|
64140
|
+
* 4. Does **not** touch `_inFlight` — in-flight handlers continue to
|
|
64141
|
+
* settle in the background and clear their own slots via `.finally()`.
|
|
64142
|
+
* Their final `_firedOnce.add(firedKey)` writes carry old-generation
|
|
64143
|
+
* keys and are harmless (lookup uses the post-dispose generation).
|
|
64144
|
+
*
|
|
64145
|
+
* Use from a CLI/session teardown when you want to throw away every
|
|
64146
|
+
* registration along with the lifecycle wiring — e.g. between two
|
|
64147
|
+
* independent runner scopes. For "just snap the subscriptions but keep
|
|
64148
|
+
* registrations" use {@link disable} instead; for "just re-arm fire-once
|
|
64149
|
+
* marks" use {@link clear}.
|
|
64150
|
+
*
|
|
64151
|
+
* Idempotent. Safe to call multiple times and safe to call before
|
|
64152
|
+
* `enable()` / without any registrations.
|
|
64153
|
+
*/
|
|
64154
|
+
this.dispose = () => {
|
|
64155
|
+
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISPOSE);
|
|
64156
|
+
this.disable();
|
|
64157
|
+
this._entries.clear();
|
|
64158
|
+
this._firedOnce.clear();
|
|
64159
|
+
};
|
|
64160
|
+
}
|
|
64161
|
+
/**
|
|
64162
|
+
* Garbage-collect every `_firedOnce` key that belongs to the entry `name`
|
|
64163
|
+
* (any generation, global or fan-out).
|
|
64164
|
+
*
|
|
64165
|
+
* Called from `register`/`unregister` to free memory; **not** required
|
|
64166
|
+
* for correctness — the generation suffix already isolates re-registrations,
|
|
64167
|
+
* so leftover keys from old generations can never block a new entry.
|
|
64168
|
+
* They just sit unused until they are GC'd here or wiped by `clear()`.
|
|
64169
|
+
*/
|
|
64170
|
+
_clearFiredOnceFor(name) {
|
|
64171
|
+
if (!name) {
|
|
64172
|
+
return;
|
|
64173
|
+
}
|
|
64174
|
+
const prefix = `${name}:`;
|
|
64175
|
+
for (const key of this._firedOnce) {
|
|
64176
|
+
if (key === name || key.startsWith(prefix)) {
|
|
64177
|
+
this._firedOnce.delete(key);
|
|
64178
|
+
}
|
|
64179
|
+
}
|
|
64180
|
+
}
|
|
64181
|
+
/**
|
|
64182
|
+
* Build the singleshot promise for a single in-flight slot.
|
|
64183
|
+
*
|
|
64184
|
+
* Invokes `entry.handler(symbol, aligned, backtest)`, swallows and logs
|
|
64185
|
+
* any error via `console.error`, and clears the `_inFlight` slot
|
|
64186
|
+
* in `.finally()` so the next boundary produces a fresh promise. For
|
|
64187
|
+
* fire-once entries `firedKey` is added to `_firedOnce` on success so
|
|
64188
|
+
* subsequent ticks skip it.
|
|
64189
|
+
*
|
|
64190
|
+
* @param firedKey - Key to add to `_firedOnce` on success, or `null` for
|
|
64191
|
+
* periodic entries (which never populate `_firedOnce`).
|
|
64192
|
+
* @param backtest - Value forwarded as the third handler argument; the
|
|
64193
|
+
* "winner" tick's flag is what all parallel awaiters of this slot see.
|
|
64194
|
+
*/
|
|
64195
|
+
async _runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest) {
|
|
64196
|
+
let failed = false;
|
|
64197
|
+
try {
|
|
64198
|
+
await entry.handler(symbol, aligned, backtest);
|
|
64199
|
+
}
|
|
64200
|
+
catch (err) {
|
|
64201
|
+
failed = true;
|
|
64202
|
+
console.error(`${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`, { symbol, alignedMs, err });
|
|
64203
|
+
}
|
|
64204
|
+
finally {
|
|
64205
|
+
this._inFlight.delete(slotKey);
|
|
64206
|
+
if (!failed && firedKey !== null) {
|
|
64207
|
+
this._firedOnce.add(firedKey);
|
|
64208
|
+
}
|
|
64209
|
+
}
|
|
64210
|
+
}
|
|
64211
|
+
}
|
|
64212
|
+
/**
|
|
64213
|
+
* Singleton instance of {@link CronUtils} for registering periodic tasks
|
|
64214
|
+
* coordinated across parallel `Backtest.background` runs.
|
|
64215
|
+
*
|
|
64216
|
+
* @example
|
|
64217
|
+
* ```typescript
|
|
64218
|
+
* import { Cron } from "backtest-kit";
|
|
64219
|
+
*
|
|
64220
|
+
* Cron.register({
|
|
64221
|
+
* name: "tg-parser",
|
|
64222
|
+
* interval: "1h",
|
|
64223
|
+
* handler: async (symbol, when, backtest) => { ... },
|
|
64224
|
+
* });
|
|
64225
|
+
* ```
|
|
64226
|
+
*/
|
|
64227
|
+
const Cron = new CronUtils();
|
|
64228
|
+
|
|
63256
64229
|
const BREAKEVEN_METHOD_NAME_GET_DATA = "BreakevenUtils.getData";
|
|
63257
64230
|
const BREAKEVEN_METHOD_NAME_GET_REPORT = "BreakevenUtils.getReport";
|
|
63258
64231
|
const BREAKEVEN_METHOD_NAME_DUMP = "BreakevenUtils.dump";
|
|
@@ -64335,7 +65308,7 @@ const validateSignal = (signal, currentPrice) => {
|
|
|
64335
65308
|
}
|
|
64336
65309
|
}
|
|
64337
65310
|
if (errors.length > 0) {
|
|
64338
|
-
console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
65311
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
64339
65312
|
return false;
|
|
64340
65313
|
}
|
|
64341
65314
|
try {
|
|
@@ -64408,7 +65381,7 @@ const validateSignal = (signal, currentPrice) => {
|
|
|
64408
65381
|
}
|
|
64409
65382
|
}
|
|
64410
65383
|
if (errors.length > 0) {
|
|
64411
|
-
console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
65384
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
64412
65385
|
}
|
|
64413
65386
|
return !errors.length;
|
|
64414
65387
|
};
|
|
@@ -64420,6 +65393,7 @@ exports.Broker = Broker;
|
|
|
64420
65393
|
exports.BrokerBase = BrokerBase;
|
|
64421
65394
|
exports.Cache = Cache;
|
|
64422
65395
|
exports.Constant = Constant;
|
|
65396
|
+
exports.Cron = Cron;
|
|
64423
65397
|
exports.Dump = Dump;
|
|
64424
65398
|
exports.Exchange = Exchange;
|
|
64425
65399
|
exports.ExecutionContextService = ExecutionContextService;
|
|
@@ -64694,6 +65668,7 @@ exports.shutdown = shutdown;
|
|
|
64694
65668
|
exports.slPercentShiftToPrice = slPercentShiftToPrice;
|
|
64695
65669
|
exports.slPriceToPercentShift = slPriceToPercentShift;
|
|
64696
65670
|
exports.stopStrategy = stopStrategy;
|
|
65671
|
+
exports.toPlainString = toPlainString;
|
|
64697
65672
|
exports.toProfitLossDto = toProfitLossDto;
|
|
64698
65673
|
exports.tpPercentShiftToPrice = tpPercentShiftToPrice;
|
|
64699
65674
|
exports.tpPriceToPercentShift = tpPriceToPercentShift;
|