backtest-kit 1.10.4 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/index.cjs +2942 -573
- package/build/index.mjs +2935 -589
- package/package.json +2 -2
- package/types.d.ts +999 -185
package/build/index.cjs
CHANGED
|
@@ -9,6 +9,25 @@ var crypto = require('crypto');
|
|
|
9
9
|
var os = require('os');
|
|
10
10
|
var fs$1 = require('fs');
|
|
11
11
|
|
|
12
|
+
function _interopNamespaceDefault(e) {
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
30
|
+
|
|
12
31
|
const { init, inject, provide } = diKit.createActivator("backtest");
|
|
13
32
|
|
|
14
33
|
/**
|
|
@@ -102,6 +121,17 @@ const markdownServices$1 = {
|
|
|
102
121
|
outlineMarkdownService: Symbol('outlineMarkdownService'),
|
|
103
122
|
riskMarkdownService: Symbol('riskMarkdownService'),
|
|
104
123
|
};
|
|
124
|
+
const reportServices$1 = {
|
|
125
|
+
backtestReportService: Symbol('backtestReportService'),
|
|
126
|
+
liveReportService: Symbol('liveReportService'),
|
|
127
|
+
scheduleReportService: Symbol('scheduleReportService'),
|
|
128
|
+
performanceReportService: Symbol('performanceReportService'),
|
|
129
|
+
walkerReportService: Symbol('walkerReportService'),
|
|
130
|
+
heatReportService: Symbol('heatReportService'),
|
|
131
|
+
partialReportService: Symbol('partialReportService'),
|
|
132
|
+
breakevenReportService: Symbol('breakevenReportService'),
|
|
133
|
+
riskReportService: Symbol('riskReportService'),
|
|
134
|
+
};
|
|
105
135
|
const validationServices$1 = {
|
|
106
136
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
107
137
|
strategyValidationService: Symbol('strategyValidationService'),
|
|
@@ -127,6 +157,7 @@ const TYPES = {
|
|
|
127
157
|
...logicPrivateServices$1,
|
|
128
158
|
...logicPublicServices$1,
|
|
129
159
|
...markdownServices$1,
|
|
160
|
+
...reportServices$1,
|
|
130
161
|
...validationServices$1,
|
|
131
162
|
...templateServices$1,
|
|
132
163
|
};
|
|
@@ -327,6 +358,12 @@ const GLOBAL_CONFIG = {
|
|
|
327
358
|
* Default: 5000 ms (5 seconds)
|
|
328
359
|
*/
|
|
329
360
|
CC_GET_CANDLES_RETRY_DELAY_MS: 5000,
|
|
361
|
+
/**
|
|
362
|
+
* Maximum number of candles to request per single API call.
|
|
363
|
+
* If a request exceeds this limit, data will be fetched using pagination.
|
|
364
|
+
* Default: 1000 candles per request
|
|
365
|
+
*/
|
|
366
|
+
CC_MAX_CANDLES_PER_REQUEST: 1000,
|
|
330
367
|
/**
|
|
331
368
|
* Maximum allowed deviation factor for price anomaly detection.
|
|
332
369
|
* Price should not be more than this factor lower than reference price.
|
|
@@ -694,11 +731,29 @@ class ClientExchange {
|
|
|
694
731
|
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
695
732
|
}
|
|
696
733
|
const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
|
|
697
|
-
|
|
734
|
+
let allData = [];
|
|
735
|
+
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
736
|
+
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
737
|
+
let remaining = limit;
|
|
738
|
+
let currentSince = new Date(since.getTime());
|
|
739
|
+
while (remaining > 0) {
|
|
740
|
+
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
741
|
+
const chunkData = await GET_CANDLES_FN({ symbol, interval, limit: chunkLimit }, currentSince, this);
|
|
742
|
+
allData.push(...chunkData);
|
|
743
|
+
remaining -= chunkLimit;
|
|
744
|
+
if (remaining > 0) {
|
|
745
|
+
// Move currentSince forward by the number of candles fetched
|
|
746
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
752
|
+
}
|
|
698
753
|
// Filter candles to strictly match the requested range
|
|
699
754
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
700
755
|
const sinceTimestamp = since.getTime();
|
|
701
|
-
const filteredData =
|
|
756
|
+
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
|
|
702
757
|
if (filteredData.length < limit) {
|
|
703
758
|
this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${filteredData.length}`);
|
|
704
759
|
}
|
|
@@ -730,10 +785,28 @@ class ClientExchange {
|
|
|
730
785
|
if (endTime > now) {
|
|
731
786
|
return [];
|
|
732
787
|
}
|
|
733
|
-
|
|
788
|
+
let allData = [];
|
|
789
|
+
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
790
|
+
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
791
|
+
let remaining = limit;
|
|
792
|
+
let currentSince = new Date(since.getTime());
|
|
793
|
+
while (remaining > 0) {
|
|
794
|
+
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
795
|
+
const chunkData = await GET_CANDLES_FN({ symbol, interval, limit: chunkLimit }, currentSince, this);
|
|
796
|
+
allData.push(...chunkData);
|
|
797
|
+
remaining -= chunkLimit;
|
|
798
|
+
if (remaining > 0) {
|
|
799
|
+
// Move currentSince forward by the number of candles fetched
|
|
800
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
806
|
+
}
|
|
734
807
|
// Filter candles to strictly match the requested range
|
|
735
808
|
const sinceTimestamp = since.getTime();
|
|
736
|
-
const filteredData =
|
|
809
|
+
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
|
|
737
810
|
if (filteredData.length < limit) {
|
|
738
811
|
this.params.logger.warn(`ClientExchange getNextCandles: Expected ${limit} candles, got ${filteredData.length}`);
|
|
739
812
|
}
|
|
@@ -1211,20 +1284,33 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
1211
1284
|
}
|
|
1212
1285
|
}
|
|
1213
1286
|
|
|
1214
|
-
var _a;
|
|
1287
|
+
var _a$2;
|
|
1215
1288
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
1216
1289
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
1217
1290
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
1218
1291
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
|
|
1292
|
+
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON = "PersistSignalUtils.useJson";
|
|
1293
|
+
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY = "PersistSignalUtils.useDummy";
|
|
1219
1294
|
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER = "PersistScheduleUtils.usePersistScheduleAdapter";
|
|
1220
1295
|
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA = "PersistScheduleUtils.readScheduleData";
|
|
1221
1296
|
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writeScheduleData";
|
|
1297
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON = "PersistScheduleUtils.useJson";
|
|
1298
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY = "PersistScheduleUtils.useDummy";
|
|
1222
1299
|
const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
|
|
1223
1300
|
const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
|
|
1224
1301
|
const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
|
|
1302
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON = "PersistPartialUtils.useJson";
|
|
1303
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY = "PersistPartialUtils.useDummy";
|
|
1225
1304
|
const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER = "PersistBreakevenUtils.usePersistBreakevenAdapter";
|
|
1226
1305
|
const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA = "PersistBreakevenUtils.readBreakevenData";
|
|
1227
1306
|
const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA = "PersistBreakevenUtils.writeBreakevenData";
|
|
1307
|
+
const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON = "PersistBreakevenUtils.useJson";
|
|
1308
|
+
const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY = "PersistBreakevenUtils.useDummy";
|
|
1309
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
|
|
1310
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
|
|
1311
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
|
|
1312
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON = "PersistRiskUtils.useJson";
|
|
1313
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY = "PersistRiskUtils.useDummy";
|
|
1228
1314
|
const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
|
|
1229
1315
|
const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
|
|
1230
1316
|
const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
|
|
@@ -1295,7 +1381,7 @@ const PersistBase = functoolsKit.makeExtendable(class {
|
|
|
1295
1381
|
constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
|
|
1296
1382
|
this.entityName = entityName;
|
|
1297
1383
|
this.baseDir = baseDir;
|
|
1298
|
-
this[_a] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
|
|
1384
|
+
this[_a$2] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
|
|
1299
1385
|
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
|
|
1300
1386
|
entityName: this.entityName,
|
|
1301
1387
|
baseDir,
|
|
@@ -1483,7 +1569,7 @@ const PersistBase = functoolsKit.makeExtendable(class {
|
|
|
1483
1569
|
*
|
|
1484
1570
|
* @returns AsyncIterableIterator yielding entities
|
|
1485
1571
|
*/
|
|
1486
|
-
async *[(_a = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
|
|
1572
|
+
async *[(_a$2 = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
|
|
1487
1573
|
for await (const entity of this.values()) {
|
|
1488
1574
|
yield entity;
|
|
1489
1575
|
}
|
|
@@ -1533,6 +1619,44 @@ const PersistBase = functoolsKit.makeExtendable(class {
|
|
|
1533
1619
|
}
|
|
1534
1620
|
}
|
|
1535
1621
|
});
|
|
1622
|
+
/**
|
|
1623
|
+
* Dummy persist adapter that discards all writes.
|
|
1624
|
+
* Used for disabling persistence.
|
|
1625
|
+
*/
|
|
1626
|
+
class PersistDummy {
|
|
1627
|
+
/**
|
|
1628
|
+
* No-op initialization function.
|
|
1629
|
+
* @returns Promise that resolves immediately
|
|
1630
|
+
*/
|
|
1631
|
+
async waitForInit() {
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* No-op read function.
|
|
1635
|
+
* @returns Promise that resolves with empty object
|
|
1636
|
+
*/
|
|
1637
|
+
async readValue() {
|
|
1638
|
+
return {};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* No-op has value check.
|
|
1642
|
+
* @returns Promise that resolves to false
|
|
1643
|
+
*/
|
|
1644
|
+
async hasValue() {
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* No-op write function.
|
|
1649
|
+
* @returns Promise that resolves immediately
|
|
1650
|
+
*/
|
|
1651
|
+
async writeValue() {
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* No-op keys generator.
|
|
1655
|
+
* @returns Empty async generator
|
|
1656
|
+
*/
|
|
1657
|
+
async *keys() {
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1536
1660
|
/**
|
|
1537
1661
|
* Utility class for managing signal persistence.
|
|
1538
1662
|
*
|
|
@@ -1612,6 +1736,22 @@ class PersistSignalUtils {
|
|
|
1612
1736
|
bt.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
|
|
1613
1737
|
this.PersistSignalFactory = Ctor;
|
|
1614
1738
|
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Switches to the default JSON persist adapter.
|
|
1741
|
+
* All future persistence writes will use JSON storage.
|
|
1742
|
+
*/
|
|
1743
|
+
useJson() {
|
|
1744
|
+
bt.loggerService.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
|
|
1745
|
+
this.usePersistSignalAdapter(PersistBase);
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Switches to a dummy persist adapter that discards all writes.
|
|
1749
|
+
* All future persistence writes will be no-ops.
|
|
1750
|
+
*/
|
|
1751
|
+
useDummy() {
|
|
1752
|
+
bt.loggerService.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1753
|
+
this.usePersistSignalAdapter(PersistDummy);
|
|
1754
|
+
}
|
|
1615
1755
|
}
|
|
1616
1756
|
/**
|
|
1617
1757
|
* Global singleton instance of PersistSignalUtils.
|
|
@@ -1630,9 +1770,6 @@ class PersistSignalUtils {
|
|
|
1630
1770
|
* ```
|
|
1631
1771
|
*/
|
|
1632
1772
|
const PersistSignalAdapter = new PersistSignalUtils();
|
|
1633
|
-
const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
|
|
1634
|
-
const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
|
|
1635
|
-
const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
|
|
1636
1773
|
/**
|
|
1637
1774
|
* Utility class for managing risk active positions persistence.
|
|
1638
1775
|
*
|
|
@@ -1712,6 +1849,22 @@ class PersistRiskUtils {
|
|
|
1712
1849
|
bt.loggerService.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
|
|
1713
1850
|
this.PersistRiskFactory = Ctor;
|
|
1714
1851
|
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Switches to the default JSON persist adapter.
|
|
1854
|
+
* All future persistence writes will use JSON storage.
|
|
1855
|
+
*/
|
|
1856
|
+
useJson() {
|
|
1857
|
+
bt.loggerService.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
|
|
1858
|
+
this.usePersistRiskAdapter(PersistBase);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Switches to a dummy persist adapter that discards all writes.
|
|
1862
|
+
* All future persistence writes will be no-ops.
|
|
1863
|
+
*/
|
|
1864
|
+
useDummy() {
|
|
1865
|
+
bt.loggerService.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1866
|
+
this.usePersistRiskAdapter(PersistDummy);
|
|
1867
|
+
}
|
|
1715
1868
|
}
|
|
1716
1869
|
/**
|
|
1717
1870
|
* Global singleton instance of PersistRiskUtils.
|
|
@@ -1809,6 +1962,22 @@ class PersistScheduleUtils {
|
|
|
1809
1962
|
bt.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
|
|
1810
1963
|
this.PersistScheduleFactory = Ctor;
|
|
1811
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Switches to the default JSON persist adapter.
|
|
1967
|
+
* All future persistence writes will use JSON storage.
|
|
1968
|
+
*/
|
|
1969
|
+
useJson() {
|
|
1970
|
+
bt.loggerService.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
|
|
1971
|
+
this.usePersistScheduleAdapter(PersistBase);
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Switches to a dummy persist adapter that discards all writes.
|
|
1975
|
+
* All future persistence writes will be no-ops.
|
|
1976
|
+
*/
|
|
1977
|
+
useDummy() {
|
|
1978
|
+
bt.loggerService.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
1979
|
+
this.usePersistScheduleAdapter(PersistDummy);
|
|
1980
|
+
}
|
|
1812
1981
|
}
|
|
1813
1982
|
/**
|
|
1814
1983
|
* Global singleton instance of PersistScheduleUtils.
|
|
@@ -1908,6 +2077,22 @@ class PersistPartialUtils {
|
|
|
1908
2077
|
bt.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
|
|
1909
2078
|
this.PersistPartialFactory = Ctor;
|
|
1910
2079
|
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Switches to the default JSON persist adapter.
|
|
2082
|
+
* All future persistence writes will use JSON storage.
|
|
2083
|
+
*/
|
|
2084
|
+
useJson() {
|
|
2085
|
+
bt.loggerService.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
|
|
2086
|
+
this.usePersistPartialAdapter(PersistBase);
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Switches to a dummy persist adapter that discards all writes.
|
|
2090
|
+
* All future persistence writes will be no-ops.
|
|
2091
|
+
*/
|
|
2092
|
+
useDummy() {
|
|
2093
|
+
bt.loggerService.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2094
|
+
this.usePersistPartialAdapter(PersistDummy);
|
|
2095
|
+
}
|
|
1911
2096
|
}
|
|
1912
2097
|
/**
|
|
1913
2098
|
* Global singleton instance of PersistPartialUtils.
|
|
@@ -2042,6 +2227,22 @@ class PersistBreakevenUtils {
|
|
|
2042
2227
|
bt.loggerService.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
|
|
2043
2228
|
this.PersistBreakevenFactory = Ctor;
|
|
2044
2229
|
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Switches to the default JSON persist adapter.
|
|
2232
|
+
* All future persistence writes will use JSON storage.
|
|
2233
|
+
*/
|
|
2234
|
+
useJson() {
|
|
2235
|
+
bt.loggerService.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
|
|
2236
|
+
this.usePersistBreakevenAdapter(PersistBase);
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Switches to a dummy persist adapter that discards all writes.
|
|
2240
|
+
* All future persistence writes will be no-ops.
|
|
2241
|
+
*/
|
|
2242
|
+
useDummy() {
|
|
2243
|
+
bt.loggerService.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2244
|
+
this.usePersistBreakevenAdapter(PersistDummy);
|
|
2245
|
+
}
|
|
2045
2246
|
}
|
|
2046
2247
|
/**
|
|
2047
2248
|
* Global singleton instance of PersistBreakevenUtils.
|
|
@@ -2165,14 +2366,18 @@ const TIMEOUT_SYMBOL = Symbol('timeout');
|
|
|
2165
2366
|
* ```
|
|
2166
2367
|
*/
|
|
2167
2368
|
const TO_PUBLIC_SIGNAL = (signal) => {
|
|
2168
|
-
const hasTrailingSL = signal._trailingPriceStopLoss !== undefined;
|
|
2169
|
-
const hasTrailingTP = signal._trailingPriceTakeProfit !== undefined;
|
|
2369
|
+
const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
|
|
2370
|
+
const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
|
|
2371
|
+
const totalExecuted = ("_partial" in signal && Array.isArray(signal._partial))
|
|
2372
|
+
? signal._partial.reduce((sum, partial) => sum + partial.percent, 0)
|
|
2373
|
+
: 0;
|
|
2170
2374
|
return {
|
|
2171
2375
|
...structuredClone(signal),
|
|
2172
2376
|
priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
|
|
2173
2377
|
priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
|
|
2174
2378
|
originalPriceStopLoss: signal.priceStopLoss,
|
|
2175
2379
|
originalPriceTakeProfit: signal.priceTakeProfit,
|
|
2380
|
+
totalExecuted,
|
|
2176
2381
|
};
|
|
2177
2382
|
};
|
|
2178
2383
|
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
@@ -2623,7 +2828,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
2623
2828
|
percentToClose,
|
|
2624
2829
|
newTotalClosed,
|
|
2625
2830
|
});
|
|
2626
|
-
return;
|
|
2831
|
+
return false;
|
|
2627
2832
|
}
|
|
2628
2833
|
// Add new partial close entry
|
|
2629
2834
|
signal._partial.push({
|
|
@@ -2638,6 +2843,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
2638
2843
|
currentPrice,
|
|
2639
2844
|
tpClosed: tpClosed + percentToClose,
|
|
2640
2845
|
});
|
|
2846
|
+
return true;
|
|
2641
2847
|
};
|
|
2642
2848
|
const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
2643
2849
|
// Initialize partial array if not present
|
|
@@ -2660,7 +2866,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
2660
2866
|
percentToClose,
|
|
2661
2867
|
newTotalClosed,
|
|
2662
2868
|
});
|
|
2663
|
-
return;
|
|
2869
|
+
return false;
|
|
2664
2870
|
}
|
|
2665
2871
|
// Add new partial close entry
|
|
2666
2872
|
signal._partial.push({
|
|
@@ -2675,6 +2881,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
2675
2881
|
currentPrice,
|
|
2676
2882
|
slClosed: slClosed + percentToClose,
|
|
2677
2883
|
});
|
|
2884
|
+
return true;
|
|
2678
2885
|
};
|
|
2679
2886
|
const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
|
|
2680
2887
|
// CRITICAL: Always calculate from ORIGINAL SL, not from current trailing SL
|
|
@@ -3467,11 +3674,12 @@ const CALL_PARTIAL_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signal,
|
|
|
3467
3674
|
const CALL_RISK_CHECK_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, pendingSignal, currentPrice, timestamp, backtest) => {
|
|
3468
3675
|
return await ExecutionContextService.runInContext(async () => {
|
|
3469
3676
|
return await self.params.risk.checkSignal({
|
|
3470
|
-
pendingSignal,
|
|
3677
|
+
pendingSignal: TO_PUBLIC_SIGNAL(pendingSignal),
|
|
3471
3678
|
symbol: symbol,
|
|
3472
3679
|
strategyName: self.params.method.context.strategyName,
|
|
3473
3680
|
exchangeName: self.params.method.context.exchangeName,
|
|
3474
3681
|
frameName: self.params.method.context.frameName,
|
|
3682
|
+
riskName: self.params.riskName,
|
|
3475
3683
|
currentPrice,
|
|
3476
3684
|
timestamp,
|
|
3477
3685
|
});
|
|
@@ -3589,6 +3797,7 @@ const CALL_BREAKEVEN_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signa
|
|
|
3589
3797
|
const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
|
|
3590
3798
|
const currentTime = self.params.execution.context.when.getTime();
|
|
3591
3799
|
await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentTime, self.params.execution.context.backtest);
|
|
3800
|
+
const pnl = toProfitLossDto(scheduled, currentPrice);
|
|
3592
3801
|
const result = {
|
|
3593
3802
|
action: "active",
|
|
3594
3803
|
signal: TO_PUBLIC_SIGNAL(scheduled),
|
|
@@ -3599,6 +3808,7 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
|
|
|
3599
3808
|
symbol: self.params.execution.context.symbol,
|
|
3600
3809
|
percentTp: 0,
|
|
3601
3810
|
percentSl: 0,
|
|
3811
|
+
pnl,
|
|
3602
3812
|
backtest: self.params.execution.context.backtest,
|
|
3603
3813
|
};
|
|
3604
3814
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
@@ -3767,6 +3977,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
3767
3977
|
}
|
|
3768
3978
|
}
|
|
3769
3979
|
}
|
|
3980
|
+
const pnl = toProfitLossDto(signal, currentPrice);
|
|
3770
3981
|
const result = {
|
|
3771
3982
|
action: "active",
|
|
3772
3983
|
signal: TO_PUBLIC_SIGNAL(signal),
|
|
@@ -3777,6 +3988,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
3777
3988
|
symbol: self.params.execution.context.symbol,
|
|
3778
3989
|
percentTp,
|
|
3779
3990
|
percentSl,
|
|
3991
|
+
pnl,
|
|
3780
3992
|
backtest: self.params.execution.context.backtest,
|
|
3781
3993
|
};
|
|
3782
3994
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
|
|
@@ -4566,12 +4778,14 @@ class ClientStrategy {
|
|
|
4566
4778
|
// Don't cancel - just return last active state
|
|
4567
4779
|
// In real backtest flow this won't happen as we process all candles at once,
|
|
4568
4780
|
// but this is correct behavior if someone calls backtest() with partial data
|
|
4781
|
+
const pnl = toProfitLossDto(scheduled, lastPrice);
|
|
4569
4782
|
const result = {
|
|
4570
4783
|
action: "active",
|
|
4571
4784
|
signal: TO_PUBLIC_SIGNAL(scheduled),
|
|
4572
4785
|
currentPrice: lastPrice,
|
|
4573
4786
|
percentSl: 0,
|
|
4574
4787
|
percentTp: 0,
|
|
4788
|
+
pnl,
|
|
4575
4789
|
strategyName: this.params.method.context.strategyName,
|
|
4576
4790
|
exchangeName: this.params.method.context.exchangeName,
|
|
4577
4791
|
frameName: this.params.method.context.frameName,
|
|
@@ -4696,7 +4910,8 @@ class ClientStrategy {
|
|
|
4696
4910
|
* Behavior:
|
|
4697
4911
|
* - Adds entry to signal's `_partial` array with type "profit"
|
|
4698
4912
|
* - Validates percentToClose is in range (0, 100]
|
|
4699
|
-
* -
|
|
4913
|
+
* - Returns false if total closed would exceed 100%
|
|
4914
|
+
* - Returns false if currentPrice already crossed TP level
|
|
4700
4915
|
* - Persists updated signal state (backtest and live modes)
|
|
4701
4916
|
* - Calls onWrite callback for persistence testing
|
|
4702
4917
|
*
|
|
@@ -4713,17 +4928,21 @@ class ClientStrategy {
|
|
|
4713
4928
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
4714
4929
|
* @param currentPrice - Current market price for this partial close (must be in profit direction)
|
|
4715
4930
|
* @param backtest - Whether running in backtest mode (controls persistence)
|
|
4716
|
-
* @returns Promise
|
|
4931
|
+
* @returns Promise<boolean> - true if partial close was executed, false if skipped
|
|
4717
4932
|
*
|
|
4718
4933
|
* @example
|
|
4719
4934
|
* ```typescript
|
|
4720
4935
|
* // Close 30% of position at profit (moving toward TP)
|
|
4721
|
-
* await strategy.partialProfit("BTCUSDT", 30, 45000, false);
|
|
4936
|
+
* const success1 = await strategy.partialProfit("BTCUSDT", 30, 45000, false);
|
|
4937
|
+
* // success1 = true (executed)
|
|
4722
4938
|
*
|
|
4723
4939
|
* // Later close another 20%
|
|
4724
|
-
* await strategy.partialProfit("BTCUSDT", 20, 46000, false);
|
|
4940
|
+
* const success2 = await strategy.partialProfit("BTCUSDT", 20, 46000, false);
|
|
4941
|
+
* // success2 = true (executed, total 50% closed)
|
|
4725
4942
|
*
|
|
4726
|
-
* //
|
|
4943
|
+
* // Try to close 60% more (would exceed 100%)
|
|
4944
|
+
* const success3 = await strategy.partialProfit("BTCUSDT", 60, 47000, false);
|
|
4945
|
+
* // success3 = false (skipped, would exceed 100%)
|
|
4727
4946
|
* ```
|
|
4728
4947
|
*/
|
|
4729
4948
|
async partialProfit(symbol, percentToClose, currentPrice, backtest) {
|
|
@@ -4774,7 +4993,7 @@ class ClientStrategy {
|
|
|
4774
4993
|
effectiveTakeProfit,
|
|
4775
4994
|
reason: "currentPrice >= effectiveTakeProfit (LONG position)"
|
|
4776
4995
|
});
|
|
4777
|
-
return;
|
|
4996
|
+
return false;
|
|
4778
4997
|
}
|
|
4779
4998
|
if (this._pendingSignal.position === "short" && currentPrice <= effectiveTakeProfit) {
|
|
4780
4999
|
this.params.logger.debug("ClientStrategy partialProfit: price already at/below TP, skipping partial close", {
|
|
@@ -4784,10 +5003,14 @@ class ClientStrategy {
|
|
|
4784
5003
|
effectiveTakeProfit,
|
|
4785
5004
|
reason: "currentPrice <= effectiveTakeProfit (SHORT position)"
|
|
4786
5005
|
});
|
|
4787
|
-
return;
|
|
5006
|
+
return false;
|
|
4788
5007
|
}
|
|
4789
5008
|
// Execute partial close logic
|
|
4790
|
-
PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
5009
|
+
const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
5010
|
+
// If partial was not executed (exceeded 100%), return false without persistence
|
|
5011
|
+
if (!wasExecuted) {
|
|
5012
|
+
return false;
|
|
5013
|
+
}
|
|
4791
5014
|
// Persist updated signal state (inline setPendingSignal content)
|
|
4792
5015
|
// Note: this._pendingSignal already mutated by PARTIAL_PROFIT_FN, no reassignment needed
|
|
4793
5016
|
this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
|
|
@@ -4800,6 +5023,7 @@ class ClientStrategy {
|
|
|
4800
5023
|
if (!backtest) {
|
|
4801
5024
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
|
|
4802
5025
|
}
|
|
5026
|
+
return true;
|
|
4803
5027
|
}
|
|
4804
5028
|
/**
|
|
4805
5029
|
* Executes partial close at loss level (moving toward SL).
|
|
@@ -4810,7 +5034,8 @@ class ClientStrategy {
|
|
|
4810
5034
|
* Behavior:
|
|
4811
5035
|
* - Adds entry to signal's `_partial` array with type "loss"
|
|
4812
5036
|
* - Validates percentToClose is in range (0, 100]
|
|
4813
|
-
* -
|
|
5037
|
+
* - Returns false if total closed would exceed 100%
|
|
5038
|
+
* - Returns false if currentPrice already crossed SL level
|
|
4814
5039
|
* - Persists updated signal state (backtest and live modes)
|
|
4815
5040
|
* - Calls onWrite callback for persistence testing
|
|
4816
5041
|
*
|
|
@@ -4827,17 +5052,21 @@ class ClientStrategy {
|
|
|
4827
5052
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
4828
5053
|
* @param currentPrice - Current market price for this partial close (must be in loss direction)
|
|
4829
5054
|
* @param backtest - Whether running in backtest mode (controls persistence)
|
|
4830
|
-
* @returns Promise
|
|
5055
|
+
* @returns Promise<boolean> - true if partial close was executed, false if skipped
|
|
4831
5056
|
*
|
|
4832
5057
|
* @example
|
|
4833
5058
|
* ```typescript
|
|
4834
5059
|
* // Close 40% of position at loss (moving toward SL)
|
|
4835
|
-
* await strategy.partialLoss("BTCUSDT", 40, 38000, false);
|
|
5060
|
+
* const success1 = await strategy.partialLoss("BTCUSDT", 40, 38000, false);
|
|
5061
|
+
* // success1 = true (executed)
|
|
4836
5062
|
*
|
|
4837
5063
|
* // Later close another 30%
|
|
4838
|
-
* await strategy.partialLoss("BTCUSDT", 30, 37000, false);
|
|
5064
|
+
* const success2 = await strategy.partialLoss("BTCUSDT", 30, 37000, false);
|
|
5065
|
+
* // success2 = true (executed, total 70% closed)
|
|
4839
5066
|
*
|
|
4840
|
-
* //
|
|
5067
|
+
* // Try to close 40% more (would exceed 100%)
|
|
5068
|
+
* const success3 = await strategy.partialLoss("BTCUSDT", 40, 36000, false);
|
|
5069
|
+
* // success3 = false (skipped, would exceed 100%)
|
|
4841
5070
|
* ```
|
|
4842
5071
|
*/
|
|
4843
5072
|
async partialLoss(symbol, percentToClose, currentPrice, backtest) {
|
|
@@ -4888,7 +5117,7 @@ class ClientStrategy {
|
|
|
4888
5117
|
effectiveStopLoss,
|
|
4889
5118
|
reason: "currentPrice <= effectiveStopLoss (LONG position)"
|
|
4890
5119
|
});
|
|
4891
|
-
return;
|
|
5120
|
+
return false;
|
|
4892
5121
|
}
|
|
4893
5122
|
if (this._pendingSignal.position === "short" && currentPrice >= effectiveStopLoss) {
|
|
4894
5123
|
this.params.logger.debug("ClientStrategy partialLoss: price already at/above SL, skipping partial close", {
|
|
@@ -4898,10 +5127,14 @@ class ClientStrategy {
|
|
|
4898
5127
|
effectiveStopLoss,
|
|
4899
5128
|
reason: "currentPrice >= effectiveStopLoss (SHORT position)"
|
|
4900
5129
|
});
|
|
4901
|
-
return;
|
|
5130
|
+
return false;
|
|
4902
5131
|
}
|
|
4903
5132
|
// Execute partial close logic
|
|
4904
|
-
PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
5133
|
+
const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
5134
|
+
// If partial was not executed (exceeded 100%), return false without persistence
|
|
5135
|
+
if (!wasExecuted) {
|
|
5136
|
+
return false;
|
|
5137
|
+
}
|
|
4905
5138
|
// Persist updated signal state (inline setPendingSignal content)
|
|
4906
5139
|
// Note: this._pendingSignal already mutated by PARTIAL_LOSS_FN, no reassignment needed
|
|
4907
5140
|
this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
|
|
@@ -4914,6 +5147,7 @@ class ClientStrategy {
|
|
|
4914
5147
|
if (!backtest) {
|
|
4915
5148
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
|
|
4916
5149
|
}
|
|
5150
|
+
return true;
|
|
4917
5151
|
}
|
|
4918
5152
|
/**
|
|
4919
5153
|
* Moves stop-loss to breakeven (entry price) when price reaches threshold.
|
|
@@ -5398,7 +5632,10 @@ const RISK_METHOD_NAME_DUMP = "RiskUtils.dump";
|
|
|
5398
5632
|
* // Combine multiple risk profiles
|
|
5399
5633
|
* const maxPositionsRisk = new MaxPositionsRisk(3);
|
|
5400
5634
|
* const correlationRisk = new CorrelationRisk(0.7);
|
|
5401
|
-
* const mergedRisk = new MergeRisk(
|
|
5635
|
+
* const mergedRisk = new MergeRisk({
|
|
5636
|
+
* "max-positions": maxPositionsRisk,
|
|
5637
|
+
* "correlation": correlationRisk
|
|
5638
|
+
* });
|
|
5402
5639
|
*
|
|
5403
5640
|
* // Check if signal passes all risks
|
|
5404
5641
|
* const canTrade = await mergedRisk.checkSignal({
|
|
@@ -5416,10 +5653,10 @@ class MergeRisk {
|
|
|
5416
5653
|
/**
|
|
5417
5654
|
* Creates a merged risk profile from multiple risk instances.
|
|
5418
5655
|
*
|
|
5419
|
-
* @param
|
|
5656
|
+
* @param _riskMap - Object mapping RiskName to IRisk instances to combine
|
|
5420
5657
|
*/
|
|
5421
|
-
constructor(
|
|
5422
|
-
this.
|
|
5658
|
+
constructor(_riskMap) {
|
|
5659
|
+
this._riskMap = _riskMap;
|
|
5423
5660
|
}
|
|
5424
5661
|
/**
|
|
5425
5662
|
* Checks if signal passes all combined risk profiles.
|
|
@@ -5434,8 +5671,11 @@ class MergeRisk {
|
|
|
5434
5671
|
bt.loggerService.info("MergeRisk checkSignal", {
|
|
5435
5672
|
params,
|
|
5436
5673
|
});
|
|
5437
|
-
for (const risk of this.
|
|
5438
|
-
if (await functoolsKit.not(risk.checkSignal(
|
|
5674
|
+
for (const [riskName, risk] of Object.entries(this._riskMap)) {
|
|
5675
|
+
if (await functoolsKit.not(risk.checkSignal({
|
|
5676
|
+
...params,
|
|
5677
|
+
riskName,
|
|
5678
|
+
}))) {
|
|
5439
5679
|
return false;
|
|
5440
5680
|
}
|
|
5441
5681
|
}
|
|
@@ -5456,7 +5696,7 @@ class MergeRisk {
|
|
|
5456
5696
|
symbol,
|
|
5457
5697
|
context,
|
|
5458
5698
|
});
|
|
5459
|
-
await Promise.all(this.
|
|
5699
|
+
await Promise.all(Object.entries(this._riskMap).map(async ([riskName, risk]) => await risk.addSignal(symbol, { ...context, riskName }, positionData)));
|
|
5460
5700
|
}
|
|
5461
5701
|
/**
|
|
5462
5702
|
* Removes a signal from all child risk profiles.
|
|
@@ -5473,7 +5713,7 @@ class MergeRisk {
|
|
|
5473
5713
|
symbol,
|
|
5474
5714
|
context,
|
|
5475
5715
|
});
|
|
5476
|
-
await Promise.all(this.
|
|
5716
|
+
await Promise.all(Object.entries(this._riskMap).map(async ([riskName, risk]) => await risk.removeSignal(symbol, { ...context, riskName })));
|
|
5477
5717
|
}
|
|
5478
5718
|
}
|
|
5479
5719
|
/**
|
|
@@ -5711,13 +5951,22 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
|
|
|
5711
5951
|
}
|
|
5712
5952
|
// Есть только riskList (без riskName)
|
|
5713
5953
|
if (!hasRiskName && hasRiskList) {
|
|
5714
|
-
return new MergeRisk(dto.riskList.
|
|
5954
|
+
return new MergeRisk(dto.riskList.reduce((acc, riskName) => {
|
|
5955
|
+
acc[riskName] = self.riskConnectionService.getRisk(riskName, exchangeName, frameName, backtest);
|
|
5956
|
+
return acc;
|
|
5957
|
+
}, {}));
|
|
5715
5958
|
}
|
|
5716
5959
|
// Есть и riskName, и riskList - объединяем (riskName в начало)
|
|
5717
|
-
return new MergeRisk(
|
|
5718
|
-
self.riskConnectionService.getRisk(dto.riskName, exchangeName, frameName, backtest),
|
|
5719
|
-
...dto.riskList.
|
|
5720
|
-
|
|
5960
|
+
return new MergeRisk({
|
|
5961
|
+
[dto.riskName]: self.riskConnectionService.getRisk(dto.riskName, exchangeName, frameName, backtest),
|
|
5962
|
+
...dto.riskList.reduce((acc, riskName) => {
|
|
5963
|
+
if (riskName === dto.riskName) {
|
|
5964
|
+
return acc;
|
|
5965
|
+
}
|
|
5966
|
+
acc[riskName] = self.riskConnectionService.getRisk(riskName, exchangeName, frameName, backtest);
|
|
5967
|
+
return acc;
|
|
5968
|
+
}, {})
|
|
5969
|
+
});
|
|
5721
5970
|
};
|
|
5722
5971
|
/**
|
|
5723
5972
|
* Creates a unique key for memoizing ClientStrategy instances.
|
|
@@ -6063,18 +6312,21 @@ class StrategyConnectionService {
|
|
|
6063
6312
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
6064
6313
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
6065
6314
|
* @param currentPrice - Current market price for this partial close
|
|
6066
|
-
* @returns Promise
|
|
6315
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
6067
6316
|
*
|
|
6068
6317
|
* @example
|
|
6069
6318
|
* ```typescript
|
|
6070
6319
|
* // Close 30% of position at profit
|
|
6071
|
-
* await strategyConnectionService.partialProfit(
|
|
6320
|
+
* const success = await strategyConnectionService.partialProfit(
|
|
6072
6321
|
* false,
|
|
6073
6322
|
* "BTCUSDT",
|
|
6074
6323
|
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
6075
6324
|
* 30,
|
|
6076
6325
|
* 45000
|
|
6077
6326
|
* );
|
|
6327
|
+
* if (success) {
|
|
6328
|
+
* console.log('Partial profit executed');
|
|
6329
|
+
* }
|
|
6078
6330
|
* ```
|
|
6079
6331
|
*/
|
|
6080
6332
|
this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
@@ -6086,7 +6338,7 @@ class StrategyConnectionService {
|
|
|
6086
6338
|
backtest,
|
|
6087
6339
|
});
|
|
6088
6340
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
6089
|
-
await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
|
|
6341
|
+
return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
|
|
6090
6342
|
};
|
|
6091
6343
|
/**
|
|
6092
6344
|
* Executes partial close at loss level (moving toward SL).
|
|
@@ -6101,18 +6353,21 @@ class StrategyConnectionService {
|
|
|
6101
6353
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
6102
6354
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
6103
6355
|
* @param currentPrice - Current market price for this partial close
|
|
6104
|
-
* @returns Promise
|
|
6356
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
6105
6357
|
*
|
|
6106
6358
|
* @example
|
|
6107
6359
|
* ```typescript
|
|
6108
6360
|
* // Close 40% of position at loss
|
|
6109
|
-
* await strategyConnectionService.partialLoss(
|
|
6361
|
+
* const success = await strategyConnectionService.partialLoss(
|
|
6110
6362
|
* false,
|
|
6111
6363
|
* "BTCUSDT",
|
|
6112
6364
|
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
6113
6365
|
* 40,
|
|
6114
6366
|
* 38000
|
|
6115
6367
|
* );
|
|
6368
|
+
* if (success) {
|
|
6369
|
+
* console.log('Partial loss executed');
|
|
6370
|
+
* }
|
|
6116
6371
|
* ```
|
|
6117
6372
|
*/
|
|
6118
6373
|
this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
@@ -6124,7 +6379,7 @@ class StrategyConnectionService {
|
|
|
6124
6379
|
backtest,
|
|
6125
6380
|
});
|
|
6126
6381
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
6127
|
-
await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
|
|
6382
|
+
return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
|
|
6128
6383
|
};
|
|
6129
6384
|
/**
|
|
6130
6385
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
@@ -6704,6 +6959,9 @@ const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
|
|
|
6704
6959
|
const TO_RISK_SIGNAL = (signal, currentPrice) => {
|
|
6705
6960
|
const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
|
|
6706
6961
|
const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
|
|
6962
|
+
const totalExecuted = ("_partial" in signal && Array.isArray(signal._partial))
|
|
6963
|
+
? signal._partial.reduce((sum, partial) => sum + partial.percent, 0)
|
|
6964
|
+
: 0;
|
|
6707
6965
|
return {
|
|
6708
6966
|
...structuredClone(signal),
|
|
6709
6967
|
priceOpen: signal.priceOpen ?? currentPrice,
|
|
@@ -6711,6 +6969,7 @@ const TO_RISK_SIGNAL = (signal, currentPrice) => {
|
|
|
6711
6969
|
priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
|
|
6712
6970
|
originalPriceStopLoss: signal.priceStopLoss,
|
|
6713
6971
|
originalPriceTakeProfit: signal.priceTakeProfit,
|
|
6972
|
+
totalExecuted,
|
|
6714
6973
|
};
|
|
6715
6974
|
};
|
|
6716
6975
|
/** Key generator for active position map */
|
|
@@ -7554,18 +7813,21 @@ class StrategyCoreService {
|
|
|
7554
7813
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
7555
7814
|
* @param currentPrice - Current market price for this partial close (must be in profit direction)
|
|
7556
7815
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
7557
|
-
* @returns Promise
|
|
7816
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
7558
7817
|
*
|
|
7559
7818
|
* @example
|
|
7560
7819
|
* ```typescript
|
|
7561
7820
|
* // Close 30% of position at profit
|
|
7562
|
-
* await strategyCoreService.partialProfit(
|
|
7821
|
+
* const success = await strategyCoreService.partialProfit(
|
|
7563
7822
|
* false,
|
|
7564
7823
|
* "BTCUSDT",
|
|
7565
7824
|
* 30,
|
|
7566
7825
|
* 45000,
|
|
7567
7826
|
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
|
|
7568
7827
|
* );
|
|
7828
|
+
* if (success) {
|
|
7829
|
+
* console.log('Partial profit executed');
|
|
7830
|
+
* }
|
|
7569
7831
|
* ```
|
|
7570
7832
|
*/
|
|
7571
7833
|
this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
@@ -7592,18 +7854,21 @@ class StrategyCoreService {
|
|
|
7592
7854
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
7593
7855
|
* @param currentPrice - Current market price for this partial close (must be in loss direction)
|
|
7594
7856
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
7595
|
-
* @returns Promise
|
|
7857
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
7596
7858
|
*
|
|
7597
7859
|
* @example
|
|
7598
7860
|
* ```typescript
|
|
7599
7861
|
* // Close 40% of position at loss
|
|
7600
|
-
* await strategyCoreService.partialLoss(
|
|
7862
|
+
* const success = await strategyCoreService.partialLoss(
|
|
7601
7863
|
* false,
|
|
7602
7864
|
* "BTCUSDT",
|
|
7603
7865
|
* 40,
|
|
7604
7866
|
* 38000,
|
|
7605
7867
|
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
|
|
7606
7868
|
* );
|
|
7869
|
+
* if (success) {
|
|
7870
|
+
* console.log('Partial loss executed');
|
|
7871
|
+
* }
|
|
7607
7872
|
* ```
|
|
7608
7873
|
*/
|
|
7609
7874
|
this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
|
|
@@ -9038,6 +9303,9 @@ class WalkerLogicPrivateService {
|
|
|
9038
9303
|
stoppedCount: stoppedStrategies.size,
|
|
9039
9304
|
});
|
|
9040
9305
|
});
|
|
9306
|
+
// Ensure BacktestMarkdownService is subscribed
|
|
9307
|
+
// to enable statistics collection
|
|
9308
|
+
this.backtestMarkdownService.subscribe();
|
|
9041
9309
|
try {
|
|
9042
9310
|
// Run backtest for each strategy
|
|
9043
9311
|
for (const strategyName of strategies) {
|
|
@@ -9530,6 +9798,18 @@ const backtest_columns = [
|
|
|
9530
9798
|
format: (data) => `${data.signal.priceStopLoss.toFixed(8)} USD`,
|
|
9531
9799
|
isVisible: () => true,
|
|
9532
9800
|
},
|
|
9801
|
+
{
|
|
9802
|
+
key: "originalPriceTakeProfit",
|
|
9803
|
+
label: "Original TP",
|
|
9804
|
+
format: (data) => `${data.signal.originalPriceTakeProfit.toFixed(8)} USD`,
|
|
9805
|
+
isVisible: () => true,
|
|
9806
|
+
},
|
|
9807
|
+
{
|
|
9808
|
+
key: "originalPriceStopLoss",
|
|
9809
|
+
label: "Original SL",
|
|
9810
|
+
format: (data) => `${data.signal.originalPriceStopLoss.toFixed(8)} USD`,
|
|
9811
|
+
isVisible: () => true,
|
|
9812
|
+
},
|
|
9533
9813
|
{
|
|
9534
9814
|
key: "pnl",
|
|
9535
9815
|
label: "PNL (net)",
|
|
@@ -9539,6 +9819,12 @@ const backtest_columns = [
|
|
|
9539
9819
|
},
|
|
9540
9820
|
isVisible: () => true,
|
|
9541
9821
|
},
|
|
9822
|
+
{
|
|
9823
|
+
key: "totalExecuted",
|
|
9824
|
+
label: "Total Executed",
|
|
9825
|
+
format: (data) => `${data.signal.totalExecuted.toFixed(1)}%`,
|
|
9826
|
+
isVisible: () => true,
|
|
9827
|
+
},
|
|
9542
9828
|
{
|
|
9543
9829
|
key: "partialCloses",
|
|
9544
9830
|
label: "Partial Closes",
|
|
@@ -9793,6 +10079,28 @@ const live_columns = [
|
|
|
9793
10079
|
format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
|
|
9794
10080
|
isVisible: () => true,
|
|
9795
10081
|
},
|
|
10082
|
+
{
|
|
10083
|
+
key: "originalPriceTakeProfit",
|
|
10084
|
+
label: "Original TP",
|
|
10085
|
+
format: (data) => data.originalPriceTakeProfit !== undefined
|
|
10086
|
+
? `${data.originalPriceTakeProfit.toFixed(8)} USD`
|
|
10087
|
+
: "N/A",
|
|
10088
|
+
isVisible: () => true,
|
|
10089
|
+
},
|
|
10090
|
+
{
|
|
10091
|
+
key: "originalPriceStopLoss",
|
|
10092
|
+
label: "Original SL",
|
|
10093
|
+
format: (data) => data.originalPriceStopLoss !== undefined
|
|
10094
|
+
? `${data.originalPriceStopLoss.toFixed(8)} USD`
|
|
10095
|
+
: "N/A",
|
|
10096
|
+
isVisible: () => true,
|
|
10097
|
+
},
|
|
10098
|
+
{
|
|
10099
|
+
key: "totalExecuted",
|
|
10100
|
+
label: "Total Executed",
|
|
10101
|
+
format: (data) => data.totalExecuted !== undefined ? `${data.totalExecuted.toFixed(1)}%` : "N/A",
|
|
10102
|
+
isVisible: () => true,
|
|
10103
|
+
},
|
|
9796
10104
|
{
|
|
9797
10105
|
key: "percentTp",
|
|
9798
10106
|
label: "% to TP",
|
|
@@ -10225,6 +10533,30 @@ const risk_columns = [
|
|
|
10225
10533
|
: "N/A",
|
|
10226
10534
|
isVisible: () => true,
|
|
10227
10535
|
},
|
|
10536
|
+
{
|
|
10537
|
+
key: "originalPriceTakeProfit",
|
|
10538
|
+
label: "Original TP",
|
|
10539
|
+
format: (data) => data.pendingSignal.originalPriceTakeProfit !== undefined
|
|
10540
|
+
? `${data.pendingSignal.originalPriceTakeProfit.toFixed(8)} USD`
|
|
10541
|
+
: "N/A",
|
|
10542
|
+
isVisible: () => true,
|
|
10543
|
+
},
|
|
10544
|
+
{
|
|
10545
|
+
key: "originalPriceStopLoss",
|
|
10546
|
+
label: "Original SL",
|
|
10547
|
+
format: (data) => data.pendingSignal.originalPriceStopLoss !== undefined
|
|
10548
|
+
? `${data.pendingSignal.originalPriceStopLoss.toFixed(8)} USD`
|
|
10549
|
+
: "N/A",
|
|
10550
|
+
isVisible: () => true,
|
|
10551
|
+
},
|
|
10552
|
+
{
|
|
10553
|
+
key: "totalExecuted",
|
|
10554
|
+
label: "Total Executed",
|
|
10555
|
+
format: (data) => data.pendingSignal.totalExecuted !== undefined
|
|
10556
|
+
? `${data.pendingSignal.totalExecuted.toFixed(1)}%`
|
|
10557
|
+
: "N/A",
|
|
10558
|
+
isVisible: () => true,
|
|
10559
|
+
},
|
|
10228
10560
|
{
|
|
10229
10561
|
key: "currentPrice",
|
|
10230
10562
|
label: "Current Price",
|
|
@@ -10359,6 +10691,28 @@ const schedule_columns = [
|
|
|
10359
10691
|
format: (data) => `${data.stopLoss.toFixed(8)} USD`,
|
|
10360
10692
|
isVisible: () => true,
|
|
10361
10693
|
},
|
|
10694
|
+
{
|
|
10695
|
+
key: "originalPriceTakeProfit",
|
|
10696
|
+
label: "Original TP",
|
|
10697
|
+
format: (data) => data.originalPriceTakeProfit !== undefined
|
|
10698
|
+
? `${data.originalPriceTakeProfit.toFixed(8)} USD`
|
|
10699
|
+
: "N/A",
|
|
10700
|
+
isVisible: () => true,
|
|
10701
|
+
},
|
|
10702
|
+
{
|
|
10703
|
+
key: "originalPriceStopLoss",
|
|
10704
|
+
label: "Original SL",
|
|
10705
|
+
format: (data) => data.originalPriceStopLoss !== undefined
|
|
10706
|
+
? `${data.originalPriceStopLoss.toFixed(8)} USD`
|
|
10707
|
+
: "N/A",
|
|
10708
|
+
isVisible: () => true,
|
|
10709
|
+
},
|
|
10710
|
+
{
|
|
10711
|
+
key: "totalExecuted",
|
|
10712
|
+
label: "Total Executed",
|
|
10713
|
+
format: (data) => data.totalExecuted !== undefined ? `${data.totalExecuted.toFixed(1)}%` : "N/A",
|
|
10714
|
+
isVisible: () => true,
|
|
10715
|
+
},
|
|
10362
10716
|
{
|
|
10363
10717
|
key: "duration",
|
|
10364
10718
|
label: "Wait Time (min)",
|
|
@@ -10604,77 +10958,482 @@ const COLUMN_CONFIG = {
|
|
|
10604
10958
|
*/
|
|
10605
10959
|
const DEFAULT_COLUMNS = Object.freeze({ ...COLUMN_CONFIG });
|
|
10606
10960
|
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
*
|
|
10614
|
-
*
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10961
|
+
var _a$1, _b$1, _c$1;
|
|
10962
|
+
const MARKDOWN_METHOD_NAME_ENABLE = "MarkdownUtils.enable";
|
|
10963
|
+
const MARKDOWN_METHOD_NAME_USE_ADAPTER = "MarkdownAdapter.useMarkdownAdapter";
|
|
10964
|
+
const WAIT_FOR_INIT_SYMBOL$1 = Symbol("wait-for-init");
|
|
10965
|
+
const WRITE_SAFE_SYMBOL$1 = Symbol("write-safe");
|
|
10966
|
+
/**
|
|
10967
|
+
* Default configuration that enables all markdown services.
|
|
10968
|
+
* Used when no specific configuration is provided to `enable()`.
|
|
10969
|
+
*/
|
|
10970
|
+
const WILDCARD_TARGET$1 = {
|
|
10971
|
+
backtest: true,
|
|
10972
|
+
breakeven: true,
|
|
10973
|
+
heat: true,
|
|
10974
|
+
live: true,
|
|
10975
|
+
partial: true,
|
|
10976
|
+
performance: true,
|
|
10977
|
+
risk: true,
|
|
10978
|
+
schedule: true,
|
|
10979
|
+
walker: true,
|
|
10623
10980
|
};
|
|
10624
10981
|
/**
|
|
10625
|
-
*
|
|
10982
|
+
* JSONL-based markdown adapter with append-only writes.
|
|
10626
10983
|
*
|
|
10627
|
-
*
|
|
10628
|
-
*
|
|
10984
|
+
* Features:
|
|
10985
|
+
* - Writes markdown reports as JSONL entries to a single file per markdown type
|
|
10986
|
+
* - Stream-based writes with backpressure handling
|
|
10987
|
+
* - 15-second timeout protection for write operations
|
|
10988
|
+
* - Automatic directory creation
|
|
10989
|
+
* - Error handling via exitEmitter
|
|
10990
|
+
* - Search metadata for filtering (symbol, strategy, exchange, frame, signalId)
|
|
10991
|
+
*
|
|
10992
|
+
* File format: ./dump/markdown/{markdownName}.jsonl
|
|
10993
|
+
* Each line contains: markdownName, data, symbol, strategyName, exchangeName, frameName, signalId, timestamp
|
|
10994
|
+
*
|
|
10995
|
+
* Use this adapter for centralized logging and post-processing with JSONL tools.
|
|
10996
|
+
*/
|
|
10997
|
+
const MarkdownFileBase = functoolsKit.makeExtendable((_c$1 = class {
|
|
10998
|
+
/**
|
|
10999
|
+
* Creates a new JSONL markdown adapter instance.
|
|
11000
|
+
*
|
|
11001
|
+
* @param markdownName - Type of markdown report (backtest, live, walker, etc.)
|
|
11002
|
+
*/
|
|
11003
|
+
constructor(markdownName) {
|
|
11004
|
+
this.markdownName = markdownName;
|
|
11005
|
+
/** WriteStream instance for append-only writes, null until initialized */
|
|
11006
|
+
this._stream = null;
|
|
11007
|
+
/** Base directory for all JSONL markdown files */
|
|
11008
|
+
this._baseDir = path.join(process.cwd(), "./dump/markdown");
|
|
11009
|
+
/**
|
|
11010
|
+
* Singleshot initialization function that creates directory and stream.
|
|
11011
|
+
* Protected by singleshot to ensure one-time execution.
|
|
11012
|
+
* Sets up error handler that emits to exitEmitter.
|
|
11013
|
+
*/
|
|
11014
|
+
this[_a$1] = functoolsKit.singleshot(async () => {
|
|
11015
|
+
await fs__namespace.mkdir(this._baseDir, { recursive: true });
|
|
11016
|
+
this._stream = fs$1.createWriteStream(this._filePath, { flags: "a" });
|
|
11017
|
+
this._stream.on('error', (err) => {
|
|
11018
|
+
exitEmitter.next(new Error(`MarkdownFileAdapter stream error for markdownName=${this.markdownName} message=${functoolsKit.getErrorMessage(err)}`));
|
|
11019
|
+
});
|
|
11020
|
+
});
|
|
11021
|
+
/**
|
|
11022
|
+
* Timeout-protected write function with backpressure handling.
|
|
11023
|
+
* Waits for drain event if write buffer is full.
|
|
11024
|
+
* Times out after 15 seconds and returns TIMEOUT_SYMBOL.
|
|
11025
|
+
*/
|
|
11026
|
+
this[_b$1] = functoolsKit.timeout(async (line) => {
|
|
11027
|
+
if (!this._stream.write(line)) {
|
|
11028
|
+
await new Promise((resolve) => {
|
|
11029
|
+
this._stream.once('drain', resolve);
|
|
11030
|
+
});
|
|
11031
|
+
}
|
|
11032
|
+
}, 15000);
|
|
11033
|
+
this._filePath = path.join(this._baseDir, `${markdownName}.jsonl`);
|
|
11034
|
+
}
|
|
11035
|
+
/**
|
|
11036
|
+
* Initializes the JSONL file and write stream.
|
|
11037
|
+
* Safe to call multiple times - singleshot ensures one-time execution.
|
|
11038
|
+
*
|
|
11039
|
+
* @returns Promise that resolves when initialization is complete
|
|
11040
|
+
*/
|
|
11041
|
+
async waitForInit() {
|
|
11042
|
+
await this[WAIT_FOR_INIT_SYMBOL$1]();
|
|
11043
|
+
}
|
|
11044
|
+
/**
|
|
11045
|
+
* Writes markdown content to JSONL file with metadata.
|
|
11046
|
+
* Appends a single line with JSON object containing:
|
|
11047
|
+
* - markdownName: Type of report
|
|
11048
|
+
* - data: Markdown content
|
|
11049
|
+
* - Search flags: symbol, strategyName, exchangeName, frameName, signalId
|
|
11050
|
+
* - timestamp: Current timestamp in milliseconds
|
|
11051
|
+
*
|
|
11052
|
+
* @param data - Markdown content to write
|
|
11053
|
+
* @param options - Path and metadata options
|
|
11054
|
+
* @throws Error if stream not initialized or write timeout exceeded
|
|
11055
|
+
*/
|
|
11056
|
+
async dump(data, options) {
|
|
11057
|
+
bt.loggerService.debug("MarkdownFileAdapter.dump", {
|
|
11058
|
+
markdownName: this.markdownName,
|
|
11059
|
+
options,
|
|
11060
|
+
});
|
|
11061
|
+
if (!this._stream) {
|
|
11062
|
+
throw new Error(`Stream not initialized for markdown ${this.markdownName}. Call waitForInit() first.`);
|
|
11063
|
+
}
|
|
11064
|
+
const searchFlags = {};
|
|
11065
|
+
{
|
|
11066
|
+
if (options.symbol) {
|
|
11067
|
+
searchFlags.symbol = options.symbol;
|
|
11068
|
+
}
|
|
11069
|
+
if (options.strategyName) {
|
|
11070
|
+
searchFlags.strategyName = options.strategyName;
|
|
11071
|
+
}
|
|
11072
|
+
if (options.exchangeName) {
|
|
11073
|
+
searchFlags.exchangeName = options.exchangeName;
|
|
11074
|
+
}
|
|
11075
|
+
if (options.frameName) {
|
|
11076
|
+
searchFlags.frameName = options.frameName;
|
|
11077
|
+
}
|
|
11078
|
+
if (options.signalId) {
|
|
11079
|
+
searchFlags.signalId = options.signalId;
|
|
11080
|
+
}
|
|
11081
|
+
}
|
|
11082
|
+
const line = JSON.stringify({
|
|
11083
|
+
markdownName: this.markdownName,
|
|
11084
|
+
data,
|
|
11085
|
+
...searchFlags,
|
|
11086
|
+
timestamp: Date.now(),
|
|
11087
|
+
}) + "\n";
|
|
11088
|
+
const status = await this[WRITE_SAFE_SYMBOL$1](line);
|
|
11089
|
+
if (status === functoolsKit.TIMEOUT_SYMBOL) {
|
|
11090
|
+
throw new Error(`Timeout writing to markdown ${this.markdownName}`);
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
},
|
|
11094
|
+
_a$1 = WAIT_FOR_INIT_SYMBOL$1,
|
|
11095
|
+
_b$1 = WRITE_SAFE_SYMBOL$1,
|
|
11096
|
+
_c$1));
|
|
11097
|
+
/**
|
|
11098
|
+
* Folder-based markdown adapter with separate files per report.
|
|
11099
|
+
*
|
|
11100
|
+
* Features:
|
|
11101
|
+
* - Writes each markdown report as a separate .md file
|
|
11102
|
+
* - File path based on options.path and options.file
|
|
11103
|
+
* - Automatic directory creation
|
|
11104
|
+
* - No stream management (direct writeFile)
|
|
11105
|
+
* - Suitable for human-readable report directories
|
|
11106
|
+
*
|
|
11107
|
+
* File format: {options.path}/{options.file}
|
|
11108
|
+
* Example: ./dump/backtest/BTCUSDT_my-strategy_binance_2024-Q1_backtest-1736601234567.md
|
|
11109
|
+
*
|
|
11110
|
+
* Use this adapter (default) for organized report directories and manual review.
|
|
10629
11111
|
*/
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
11112
|
+
const MarkdownFolderBase = functoolsKit.makeExtendable(class {
|
|
11113
|
+
/**
|
|
11114
|
+
* Creates a new folder-based markdown adapter instance.
|
|
11115
|
+
*
|
|
11116
|
+
* @param markdownName - Type of markdown report (backtest, live, walker, etc.)
|
|
11117
|
+
*/
|
|
11118
|
+
constructor(markdownName) {
|
|
11119
|
+
this.markdownName = markdownName;
|
|
10633
11120
|
}
|
|
10634
|
-
|
|
10635
|
-
|
|
11121
|
+
/**
|
|
11122
|
+
* No-op initialization for folder adapter.
|
|
11123
|
+
* This adapter doesn't need initialization since it uses direct writeFile.
|
|
11124
|
+
*
|
|
11125
|
+
* @returns Promise that resolves immediately
|
|
11126
|
+
*/
|
|
11127
|
+
async waitForInit() {
|
|
10636
11128
|
}
|
|
10637
|
-
|
|
10638
|
-
|
|
11129
|
+
/**
|
|
11130
|
+
* Writes markdown content to a separate file.
|
|
11131
|
+
* Creates directory structure automatically.
|
|
11132
|
+
* File path is determined by options.path and options.file.
|
|
11133
|
+
*
|
|
11134
|
+
* @param content - Markdown content to write
|
|
11135
|
+
* @param options - Path and file options for the dump
|
|
11136
|
+
* @throws Error if directory creation or file write fails
|
|
11137
|
+
*/
|
|
11138
|
+
async dump(content, options) {
|
|
11139
|
+
bt.loggerService.debug("MarkdownFolderAdapter.dump", {
|
|
11140
|
+
markdownName: this.markdownName,
|
|
11141
|
+
options,
|
|
11142
|
+
});
|
|
11143
|
+
// Combine into full file path
|
|
11144
|
+
const filePath = path.join(process.cwd(), options.path, options.file);
|
|
11145
|
+
// Extract directory from file path
|
|
11146
|
+
const dir = path.dirname(filePath);
|
|
11147
|
+
await fs__namespace.mkdir(dir, { recursive: true });
|
|
11148
|
+
await fs__namespace.writeFile(filePath, content, "utf8");
|
|
10639
11149
|
}
|
|
10640
|
-
|
|
10641
|
-
}
|
|
10642
|
-
/** Maximum number of signals to store in backtest reports */
|
|
10643
|
-
const MAX_EVENTS$7 = 250;
|
|
11150
|
+
});
|
|
10644
11151
|
/**
|
|
10645
|
-
*
|
|
10646
|
-
*
|
|
11152
|
+
* Dummy markdown adapter that discards all writes.
|
|
11153
|
+
* Used for disabling markdown report generation.
|
|
10647
11154
|
*/
|
|
10648
|
-
|
|
10649
|
-
constructor() {
|
|
10650
|
-
/** Internal list of all closed signals for this strategy */
|
|
10651
|
-
this._signalList = [];
|
|
10652
|
-
}
|
|
11155
|
+
class MarkdownDummy {
|
|
10653
11156
|
/**
|
|
10654
|
-
*
|
|
10655
|
-
*
|
|
10656
|
-
* @param data - Closed signal data with PNL and close reason
|
|
11157
|
+
* No-op dump function.
|
|
11158
|
+
* @returns Promise that resolves immediately
|
|
10657
11159
|
*/
|
|
10658
|
-
|
|
10659
|
-
this._signalList.unshift(data);
|
|
10660
|
-
// Trim queue if exceeded MAX_EVENTS
|
|
10661
|
-
if (this._signalList.length > MAX_EVENTS$7) {
|
|
10662
|
-
this._signalList.pop();
|
|
10663
|
-
}
|
|
11160
|
+
async dump() {
|
|
10664
11161
|
}
|
|
10665
11162
|
/**
|
|
10666
|
-
*
|
|
10667
|
-
*
|
|
10668
|
-
*
|
|
10669
|
-
* @returns Statistical data (empty object if no signals)
|
|
11163
|
+
* No-op initialization function.
|
|
11164
|
+
* @returns Promise that resolves immediately
|
|
10670
11165
|
*/
|
|
10671
|
-
async
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
11166
|
+
async waitForInit() {
|
|
11167
|
+
}
|
|
11168
|
+
}
|
|
11169
|
+
/**
|
|
11170
|
+
* Utility class for managing markdown report services.
|
|
11171
|
+
*
|
|
11172
|
+
* Provides methods to enable/disable markdown report generation across
|
|
11173
|
+
* different service types (backtest, live, walker, performance, etc.).
|
|
11174
|
+
*
|
|
11175
|
+
* Typically extended by MarkdownAdapter for additional functionality.
|
|
11176
|
+
*/
|
|
11177
|
+
class MarkdownUtils {
|
|
11178
|
+
constructor() {
|
|
11179
|
+
/**
|
|
11180
|
+
* Enables markdown report services selectively.
|
|
11181
|
+
*
|
|
11182
|
+
* Subscribes to specified markdown services and returns a cleanup function
|
|
11183
|
+
* that unsubscribes from all enabled services at once.
|
|
11184
|
+
*
|
|
11185
|
+
* Each enabled service will:
|
|
11186
|
+
* - Start listening to relevant events
|
|
11187
|
+
* - Accumulate data for reports
|
|
11188
|
+
* - Generate markdown files when requested
|
|
11189
|
+
*
|
|
11190
|
+
* IMPORTANT: Always call the returned unsubscribe function to prevent memory leaks.
|
|
11191
|
+
*
|
|
11192
|
+
* @param config - Service configuration object. Defaults to enabling all services.
|
|
11193
|
+
* @param config.backtest - Enable backtest result reports with full trade history
|
|
11194
|
+
* @param config.breakeven - Enable breakeven event tracking (when stop loss moves to entry)
|
|
11195
|
+
* @param config.partial - Enable partial profit/loss event tracking
|
|
11196
|
+
* @param config.heat - Enable portfolio heatmap analysis across all symbols
|
|
11197
|
+
* @param config.walker - Enable walker strategy comparison and optimization reports
|
|
11198
|
+
* @param config.performance - Enable performance bottleneck analysis
|
|
11199
|
+
* @param config.risk - Enable risk rejection tracking (signals blocked by risk limits)
|
|
11200
|
+
* @param config.schedule - Enable scheduled signal tracking (signals waiting for trigger)
|
|
11201
|
+
* @param config.live - Enable live trading event reports (all tick events)
|
|
11202
|
+
*
|
|
11203
|
+
* @returns Cleanup function that unsubscribes from all enabled services
|
|
11204
|
+
*/
|
|
11205
|
+
this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET$1) => {
|
|
11206
|
+
bt.loggerService.debug(MARKDOWN_METHOD_NAME_ENABLE, {
|
|
11207
|
+
backtest: bt$1,
|
|
11208
|
+
breakeven,
|
|
11209
|
+
heat,
|
|
11210
|
+
live,
|
|
11211
|
+
partial,
|
|
11212
|
+
performance,
|
|
11213
|
+
risk,
|
|
11214
|
+
schedule,
|
|
11215
|
+
walker,
|
|
11216
|
+
});
|
|
11217
|
+
const unList = [];
|
|
11218
|
+
if (bt$1) {
|
|
11219
|
+
unList.push(bt.backtestMarkdownService.subscribe());
|
|
11220
|
+
}
|
|
11221
|
+
if (breakeven) {
|
|
11222
|
+
unList.push(bt.breakevenMarkdownService.subscribe());
|
|
11223
|
+
}
|
|
11224
|
+
if (heat) {
|
|
11225
|
+
unList.push(bt.heatMarkdownService.subscribe());
|
|
11226
|
+
}
|
|
11227
|
+
if (live) {
|
|
11228
|
+
unList.push(bt.liveMarkdownService.subscribe());
|
|
11229
|
+
}
|
|
11230
|
+
if (partial) {
|
|
11231
|
+
unList.push(bt.partialMarkdownService.subscribe());
|
|
11232
|
+
}
|
|
11233
|
+
if (performance) {
|
|
11234
|
+
unList.push(bt.performanceMarkdownService.subscribe());
|
|
11235
|
+
}
|
|
11236
|
+
if (risk) {
|
|
11237
|
+
unList.push(bt.riskMarkdownService.subscribe());
|
|
11238
|
+
}
|
|
11239
|
+
if (schedule) {
|
|
11240
|
+
unList.push(bt.scheduleMarkdownService.subscribe());
|
|
11241
|
+
}
|
|
11242
|
+
if (walker) {
|
|
11243
|
+
unList.push(bt.walkerMarkdownService.subscribe());
|
|
11244
|
+
}
|
|
11245
|
+
return functoolsKit.compose(...unList.map((un) => () => void un()));
|
|
11246
|
+
};
|
|
11247
|
+
}
|
|
11248
|
+
}
|
|
11249
|
+
/**
|
|
11250
|
+
* Markdown adapter with pluggable storage backend and instance memoization.
|
|
11251
|
+
*
|
|
11252
|
+
* Features:
|
|
11253
|
+
* - Adapter pattern for swappable storage implementations
|
|
11254
|
+
* - Memoized storage instances (one per markdown type)
|
|
11255
|
+
* - Default adapter: MarkdownFolderBase (separate files)
|
|
11256
|
+
* - Alternative adapter: MarkdownFileBase (JSONL append)
|
|
11257
|
+
* - Lazy initialization on first write
|
|
11258
|
+
* - Convenience methods: useMd(), useJsonl()
|
|
11259
|
+
*/
|
|
11260
|
+
class MarkdownAdapter extends MarkdownUtils {
|
|
11261
|
+
constructor() {
|
|
11262
|
+
super(...arguments);
|
|
11263
|
+
/**
|
|
11264
|
+
* Current markdown storage adapter constructor.
|
|
11265
|
+
* Defaults to MarkdownFolderBase for separate file storage.
|
|
11266
|
+
* Can be changed via useMarkdownAdapter().
|
|
11267
|
+
*/
|
|
11268
|
+
this.MarkdownFactory = MarkdownFolderBase;
|
|
11269
|
+
/**
|
|
11270
|
+
* Memoized storage instances cache.
|
|
11271
|
+
* Key: markdownName (backtest, live, walker, etc.)
|
|
11272
|
+
* Value: TMarkdownBase instance created with current MarkdownFactory.
|
|
11273
|
+
* Ensures single instance per markdown type for the lifetime of the application.
|
|
11274
|
+
*/
|
|
11275
|
+
this.getMarkdownStorage = functoolsKit.memoize(([markdownName]) => markdownName, (markdownName) => Reflect.construct(this.MarkdownFactory, [markdownName]));
|
|
11276
|
+
}
|
|
11277
|
+
/**
|
|
11278
|
+
* Sets the markdown storage adapter constructor.
|
|
11279
|
+
* All future markdown instances will use this adapter.
|
|
11280
|
+
*
|
|
11281
|
+
* @param Ctor - Constructor for markdown storage adapter
|
|
11282
|
+
*/
|
|
11283
|
+
useMarkdownAdapter(Ctor) {
|
|
11284
|
+
bt.loggerService.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
|
|
11285
|
+
this.MarkdownFactory = Ctor;
|
|
11286
|
+
}
|
|
11287
|
+
/**
|
|
11288
|
+
* Writes markdown data to storage using the configured adapter.
|
|
11289
|
+
* Automatically initializes storage on first write for each markdown type.
|
|
11290
|
+
*
|
|
11291
|
+
* @param markdownName - Type of markdown report (backtest, live, walker, etc.)
|
|
11292
|
+
* @param content - Markdown content to write
|
|
11293
|
+
* @param options - Path, file, and metadata options
|
|
11294
|
+
* @returns Promise that resolves when write is complete
|
|
11295
|
+
* @throws Error if write fails or storage initialization fails
|
|
11296
|
+
*
|
|
11297
|
+
* @internal - Use service-specific dump methods instead (e.g., Backtest.dump)
|
|
11298
|
+
*/
|
|
11299
|
+
async writeData(markdownName, content, options) {
|
|
11300
|
+
bt.loggerService.debug("MarkdownAdapter.writeData", {
|
|
11301
|
+
markdownName,
|
|
11302
|
+
options,
|
|
11303
|
+
});
|
|
11304
|
+
const isInitial = !this.getMarkdownStorage.has(markdownName);
|
|
11305
|
+
const markdown = this.getMarkdownStorage(markdownName);
|
|
11306
|
+
await markdown.waitForInit(isInitial);
|
|
11307
|
+
await markdown.dump(content, options);
|
|
11308
|
+
}
|
|
11309
|
+
/**
|
|
11310
|
+
* Switches to folder-based markdown storage (default).
|
|
11311
|
+
* Shorthand for useMarkdownAdapter(MarkdownFolderBase).
|
|
11312
|
+
* Each dump creates a separate .md file.
|
|
11313
|
+
*/
|
|
11314
|
+
useMd() {
|
|
11315
|
+
bt.loggerService.debug("MarkdownAdapter.useMd");
|
|
11316
|
+
this.useMarkdownAdapter(MarkdownFolderBase);
|
|
11317
|
+
}
|
|
11318
|
+
/**
|
|
11319
|
+
* Switches to JSONL-based markdown storage.
|
|
11320
|
+
* Shorthand for useMarkdownAdapter(MarkdownFileBase).
|
|
11321
|
+
* All dumps append to a single .jsonl file per markdown type.
|
|
11322
|
+
*/
|
|
11323
|
+
useJsonl() {
|
|
11324
|
+
bt.loggerService.debug("MarkdownAdapter.useJsonl");
|
|
11325
|
+
this.useMarkdownAdapter(MarkdownFileBase);
|
|
11326
|
+
}
|
|
11327
|
+
/**
|
|
11328
|
+
* Switches to a dummy markdown adapter that discards all writes.
|
|
11329
|
+
* All future markdown writes will be no-ops.
|
|
11330
|
+
*/
|
|
11331
|
+
useDummy() {
|
|
11332
|
+
bt.loggerService.debug("MarkdownAdapter.useDummy");
|
|
11333
|
+
this.useMarkdownAdapter(MarkdownDummy);
|
|
11334
|
+
}
|
|
11335
|
+
}
|
|
11336
|
+
/**
|
|
11337
|
+
* Global singleton instance of MarkdownAdapter.
|
|
11338
|
+
* Provides markdown report generation with pluggable storage backends.
|
|
11339
|
+
*/
|
|
11340
|
+
const Markdown = new MarkdownAdapter();
|
|
11341
|
+
|
|
11342
|
+
/**
|
|
11343
|
+
* Creates a unique key for memoizing ReportStorage instances.
|
|
11344
|
+
* Key format: "symbol:strategyName:exchangeName:frameName:backtest" or "symbol:strategyName:exchangeName:live"
|
|
11345
|
+
* @param symbol - Trading pair symbol
|
|
11346
|
+
* @param strategyName - Name of the strategy
|
|
11347
|
+
* @param exchangeName - Exchange name
|
|
11348
|
+
* @param frameName - Frame name
|
|
11349
|
+
* @param backtest - Whether running in backtest mode
|
|
11350
|
+
* @returns Unique string key for memoization
|
|
11351
|
+
*/
|
|
11352
|
+
const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
11353
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
11354
|
+
if (frameName)
|
|
11355
|
+
parts.push(frameName);
|
|
11356
|
+
parts.push(backtest ? "backtest" : "live");
|
|
11357
|
+
return parts.join(":");
|
|
11358
|
+
};
|
|
11359
|
+
/**
|
|
11360
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
11361
|
+
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
11362
|
+
* @param symbol - Trading pair symbol
|
|
11363
|
+
* @param strategyName - Name of the strategy
|
|
11364
|
+
* @param exchangeName - Exchange name
|
|
11365
|
+
* @param frameName - Frame name
|
|
11366
|
+
* @param timestamp - Unix timestamp in milliseconds
|
|
11367
|
+
* @returns Filename string
|
|
11368
|
+
*/
|
|
11369
|
+
const CREATE_FILE_NAME_FN$8 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
11370
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
11371
|
+
if (frameName) {
|
|
11372
|
+
parts.push(frameName);
|
|
11373
|
+
parts.push("backtest");
|
|
11374
|
+
}
|
|
11375
|
+
else
|
|
11376
|
+
parts.push("live");
|
|
11377
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
11378
|
+
};
|
|
11379
|
+
/**
|
|
11380
|
+
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
11381
|
+
*
|
|
11382
|
+
* @param value - Value to check
|
|
11383
|
+
* @returns true if value is unsafe, false otherwise
|
|
11384
|
+
*/
|
|
11385
|
+
function isUnsafe$3(value) {
|
|
11386
|
+
if (typeof value !== "number") {
|
|
11387
|
+
return true;
|
|
11388
|
+
}
|
|
11389
|
+
if (isNaN(value)) {
|
|
11390
|
+
return true;
|
|
11391
|
+
}
|
|
11392
|
+
if (!isFinite(value)) {
|
|
11393
|
+
return true;
|
|
11394
|
+
}
|
|
11395
|
+
return false;
|
|
11396
|
+
}
|
|
11397
|
+
/** Maximum number of signals to store in backtest reports */
|
|
11398
|
+
const MAX_EVENTS$7 = 250;
|
|
11399
|
+
/**
|
|
11400
|
+
* Storage class for accumulating closed signals per strategy.
|
|
11401
|
+
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
11402
|
+
*/
|
|
11403
|
+
let ReportStorage$6 = class ReportStorage {
|
|
11404
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
11405
|
+
this.symbol = symbol;
|
|
11406
|
+
this.strategyName = strategyName;
|
|
11407
|
+
this.exchangeName = exchangeName;
|
|
11408
|
+
this.frameName = frameName;
|
|
11409
|
+
/** Internal list of all closed signals for this strategy */
|
|
11410
|
+
this._signalList = [];
|
|
11411
|
+
}
|
|
11412
|
+
/**
|
|
11413
|
+
* Adds a closed signal to the storage.
|
|
11414
|
+
*
|
|
11415
|
+
* @param data - Closed signal data with PNL and close reason
|
|
11416
|
+
*/
|
|
11417
|
+
addSignal(data) {
|
|
11418
|
+
this._signalList.unshift(data);
|
|
11419
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
11420
|
+
if (this._signalList.length > MAX_EVENTS$7) {
|
|
11421
|
+
this._signalList.pop();
|
|
11422
|
+
}
|
|
11423
|
+
}
|
|
11424
|
+
/**
|
|
11425
|
+
* Calculates statistical data from closed signals (Controller).
|
|
11426
|
+
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
11427
|
+
*
|
|
11428
|
+
* @returns Statistical data (empty object if no signals)
|
|
11429
|
+
*/
|
|
11430
|
+
async getData() {
|
|
11431
|
+
if (this._signalList.length === 0) {
|
|
11432
|
+
return {
|
|
11433
|
+
signalList: [],
|
|
11434
|
+
totalSignals: 0,
|
|
11435
|
+
winCount: 0,
|
|
11436
|
+
lossCount: 0,
|
|
10678
11437
|
winRate: null,
|
|
10679
11438
|
avgPnl: null,
|
|
10680
11439
|
totalPnl: null,
|
|
@@ -10779,19 +11538,19 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
10779
11538
|
* @param path - Directory path to save report (default: "./dump/backtest")
|
|
10780
11539
|
* @param columns - Column configuration for formatting the table
|
|
10781
11540
|
*/
|
|
10782
|
-
async dump(strategyName, path
|
|
11541
|
+
async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
|
|
10783
11542
|
const markdown = await this.getReport(strategyName, columns);
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
}
|
|
11543
|
+
const timestamp = Date.now();
|
|
11544
|
+
const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
11545
|
+
await Markdown.writeData("backtest", markdown, {
|
|
11546
|
+
path,
|
|
11547
|
+
file: filename,
|
|
11548
|
+
symbol: this.symbol,
|
|
11549
|
+
strategyName: this.strategyName,
|
|
11550
|
+
exchangeName: this.exchangeName,
|
|
11551
|
+
frameName: this.frameName,
|
|
11552
|
+
signalId: "",
|
|
11553
|
+
});
|
|
10795
11554
|
}
|
|
10796
11555
|
};
|
|
10797
11556
|
/**
|
|
@@ -10829,7 +11588,7 @@ class BacktestMarkdownService {
|
|
|
10829
11588
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
10830
11589
|
* Each combination gets its own isolated storage instance.
|
|
10831
11590
|
*/
|
|
10832
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$6());
|
|
11591
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
|
|
10833
11592
|
/**
|
|
10834
11593
|
* Processes tick events and accumulates closed signals.
|
|
10835
11594
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -10885,6 +11644,9 @@ class BacktestMarkdownService {
|
|
|
10885
11644
|
frameName,
|
|
10886
11645
|
backtest,
|
|
10887
11646
|
});
|
|
11647
|
+
if (!this.subscribe.hasValue()) {
|
|
11648
|
+
throw new Error("BacktestMarkdownService not initialized. Call subscribe() before getting data.");
|
|
11649
|
+
}
|
|
10888
11650
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
10889
11651
|
return storage.getData();
|
|
10890
11652
|
};
|
|
@@ -10915,6 +11677,9 @@ class BacktestMarkdownService {
|
|
|
10915
11677
|
frameName,
|
|
10916
11678
|
backtest,
|
|
10917
11679
|
});
|
|
11680
|
+
if (!this.subscribe.hasValue()) {
|
|
11681
|
+
throw new Error("BacktestMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
11682
|
+
}
|
|
10918
11683
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
10919
11684
|
return storage.getReport(strategyName, columns);
|
|
10920
11685
|
};
|
|
@@ -10951,6 +11716,9 @@ class BacktestMarkdownService {
|
|
|
10951
11716
|
backtest,
|
|
10952
11717
|
path,
|
|
10953
11718
|
});
|
|
11719
|
+
if (!this.subscribe.hasValue()) {
|
|
11720
|
+
throw new Error("BacktestMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
11721
|
+
}
|
|
10954
11722
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
10955
11723
|
await storage.dump(strategyName, path, columns);
|
|
10956
11724
|
};
|
|
@@ -10985,20 +11753,47 @@ class BacktestMarkdownService {
|
|
|
10985
11753
|
}
|
|
10986
11754
|
};
|
|
10987
11755
|
/**
|
|
10988
|
-
*
|
|
10989
|
-
*
|
|
10990
|
-
*
|
|
11756
|
+
* Subscribes to backtest signal emitter to receive tick events.
|
|
11757
|
+
* Protected against multiple subscriptions.
|
|
11758
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
10991
11759
|
*
|
|
10992
11760
|
* @example
|
|
10993
11761
|
* ```typescript
|
|
10994
11762
|
* const service = new BacktestMarkdownService();
|
|
10995
|
-
*
|
|
11763
|
+
* const unsubscribe = service.subscribe();
|
|
11764
|
+
* // ... later
|
|
11765
|
+
* unsubscribe();
|
|
10996
11766
|
* ```
|
|
10997
11767
|
*/
|
|
10998
|
-
this.
|
|
11768
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
10999
11769
|
this.loggerService.log("backtestMarkdownService init");
|
|
11000
|
-
|
|
11770
|
+
const unsubscribe = signalBacktestEmitter.subscribe(this.tick);
|
|
11771
|
+
return () => {
|
|
11772
|
+
this.subscribe.clear();
|
|
11773
|
+
this.clear();
|
|
11774
|
+
unsubscribe();
|
|
11775
|
+
};
|
|
11001
11776
|
});
|
|
11777
|
+
/**
|
|
11778
|
+
* Unsubscribes from backtest signal emitter to stop receiving tick events.
|
|
11779
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
11780
|
+
* If not subscribed, does nothing.
|
|
11781
|
+
*
|
|
11782
|
+
* @example
|
|
11783
|
+
* ```typescript
|
|
11784
|
+
* const service = new BacktestMarkdownService();
|
|
11785
|
+
* service.subscribe();
|
|
11786
|
+
* // ... later
|
|
11787
|
+
* service.unsubscribe();
|
|
11788
|
+
* ```
|
|
11789
|
+
*/
|
|
11790
|
+
this.unsubscribe = async () => {
|
|
11791
|
+
this.loggerService.log("backtestMarkdownService unsubscribe");
|
|
11792
|
+
if (this.subscribe.hasValue()) {
|
|
11793
|
+
const lastSubscription = this.subscribe();
|
|
11794
|
+
lastSubscription();
|
|
11795
|
+
}
|
|
11796
|
+
};
|
|
11002
11797
|
}
|
|
11003
11798
|
}
|
|
11004
11799
|
|
|
@@ -11019,6 +11814,26 @@ const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
11019
11814
|
parts.push(backtest ? "backtest" : "live");
|
|
11020
11815
|
return parts.join(":");
|
|
11021
11816
|
};
|
|
11817
|
+
/**
|
|
11818
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
11819
|
+
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
11820
|
+
* @param symbol - Trading pair symbol
|
|
11821
|
+
* @param strategyName - Name of the strategy
|
|
11822
|
+
* @param exchangeName - Exchange name
|
|
11823
|
+
* @param frameName - Frame name
|
|
11824
|
+
* @param timestamp - Unix timestamp in milliseconds
|
|
11825
|
+
* @returns Filename string
|
|
11826
|
+
*/
|
|
11827
|
+
const CREATE_FILE_NAME_FN$7 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
11828
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
11829
|
+
if (frameName) {
|
|
11830
|
+
parts.push(frameName);
|
|
11831
|
+
parts.push("backtest");
|
|
11832
|
+
}
|
|
11833
|
+
else
|
|
11834
|
+
parts.push("live");
|
|
11835
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
11836
|
+
};
|
|
11022
11837
|
/**
|
|
11023
11838
|
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
11024
11839
|
*
|
|
@@ -11044,7 +11859,11 @@ const MAX_EVENTS$6 = 250;
|
|
|
11044
11859
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
11045
11860
|
*/
|
|
11046
11861
|
let ReportStorage$5 = class ReportStorage {
|
|
11047
|
-
constructor() {
|
|
11862
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
11863
|
+
this.symbol = symbol;
|
|
11864
|
+
this.strategyName = strategyName;
|
|
11865
|
+
this.exchangeName = exchangeName;
|
|
11866
|
+
this.frameName = frameName;
|
|
11048
11867
|
/** Internal list of all tick events for this strategy */
|
|
11049
11868
|
this._eventList = [];
|
|
11050
11869
|
}
|
|
@@ -11093,6 +11912,9 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
11093
11912
|
openPrice: data.signal.priceOpen,
|
|
11094
11913
|
takeProfit: data.signal.priceTakeProfit,
|
|
11095
11914
|
stopLoss: data.signal.priceStopLoss,
|
|
11915
|
+
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
11916
|
+
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
11917
|
+
totalExecuted: data.signal.totalExecuted,
|
|
11096
11918
|
});
|
|
11097
11919
|
// Trim queue if exceeded MAX_EVENTS
|
|
11098
11920
|
if (this._eventList.length > MAX_EVENTS$6) {
|
|
@@ -11117,8 +11939,12 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
11117
11939
|
openPrice: data.signal.priceOpen,
|
|
11118
11940
|
takeProfit: data.signal.priceTakeProfit,
|
|
11119
11941
|
stopLoss: data.signal.priceStopLoss,
|
|
11942
|
+
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
11943
|
+
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
11944
|
+
totalExecuted: data.signal.totalExecuted,
|
|
11120
11945
|
percentTp: data.percentTp,
|
|
11121
11946
|
percentSl: data.percentSl,
|
|
11947
|
+
pnl: data.pnl.pnlPercentage,
|
|
11122
11948
|
};
|
|
11123
11949
|
// Find the last active event with the same signalId
|
|
11124
11950
|
const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
|
|
@@ -11153,6 +11979,9 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
11153
11979
|
openPrice: data.signal.priceOpen,
|
|
11154
11980
|
takeProfit: data.signal.priceTakeProfit,
|
|
11155
11981
|
stopLoss: data.signal.priceStopLoss,
|
|
11982
|
+
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
11983
|
+
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
11984
|
+
totalExecuted: data.signal.totalExecuted,
|
|
11156
11985
|
pnl: data.pnl.pnlPercentage,
|
|
11157
11986
|
closeReason: data.closeReason,
|
|
11158
11987
|
duration: durationMin,
|
|
@@ -11295,19 +12124,19 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
11295
12124
|
* @param path - Directory path to save report (default: "./dump/live")
|
|
11296
12125
|
* @param columns - Column configuration for formatting the table
|
|
11297
12126
|
*/
|
|
11298
|
-
async dump(strategyName, path
|
|
12127
|
+
async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
|
|
11299
12128
|
const markdown = await this.getReport(strategyName, columns);
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
}
|
|
12129
|
+
const timestamp = Date.now();
|
|
12130
|
+
const filename = CREATE_FILE_NAME_FN$7(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
12131
|
+
await Markdown.writeData("live", markdown, {
|
|
12132
|
+
path,
|
|
12133
|
+
signalId: "",
|
|
12134
|
+
file: filename,
|
|
12135
|
+
symbol: this.symbol,
|
|
12136
|
+
strategyName: this.strategyName,
|
|
12137
|
+
exchangeName: this.exchangeName,
|
|
12138
|
+
frameName: this.frameName
|
|
12139
|
+
});
|
|
11311
12140
|
}
|
|
11312
12141
|
};
|
|
11313
12142
|
/**
|
|
@@ -11348,7 +12177,49 @@ class LiveMarkdownService {
|
|
|
11348
12177
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
11349
12178
|
* Each combination gets its own isolated storage instance.
|
|
11350
12179
|
*/
|
|
11351
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$5());
|
|
12180
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$5(symbol, strategyName, exchangeName, frameName));
|
|
12181
|
+
/**
|
|
12182
|
+
* Subscribes to live signal emitter to receive tick events.
|
|
12183
|
+
* Protected against multiple subscriptions.
|
|
12184
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
12185
|
+
*
|
|
12186
|
+
* @example
|
|
12187
|
+
* ```typescript
|
|
12188
|
+
* const service = new LiveMarkdownService();
|
|
12189
|
+
* const unsubscribe = service.subscribe();
|
|
12190
|
+
* // ... later
|
|
12191
|
+
* unsubscribe();
|
|
12192
|
+
* ```
|
|
12193
|
+
*/
|
|
12194
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
12195
|
+
this.loggerService.log("liveMarkdownService init");
|
|
12196
|
+
const unsubscribe = signalLiveEmitter.subscribe(this.tick);
|
|
12197
|
+
return () => {
|
|
12198
|
+
this.subscribe.clear();
|
|
12199
|
+
this.clear();
|
|
12200
|
+
unsubscribe();
|
|
12201
|
+
};
|
|
12202
|
+
});
|
|
12203
|
+
/**
|
|
12204
|
+
* Unsubscribes from live signal emitter to stop receiving tick events.
|
|
12205
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
12206
|
+
* If not subscribed, does nothing.
|
|
12207
|
+
*
|
|
12208
|
+
* @example
|
|
12209
|
+
* ```typescript
|
|
12210
|
+
* const service = new LiveMarkdownService();
|
|
12211
|
+
* service.subscribe();
|
|
12212
|
+
* // ... later
|
|
12213
|
+
* service.unsubscribe();
|
|
12214
|
+
* ```
|
|
12215
|
+
*/
|
|
12216
|
+
this.unsubscribe = async () => {
|
|
12217
|
+
this.loggerService.log("liveMarkdownService unsubscribe");
|
|
12218
|
+
if (this.subscribe.hasValue()) {
|
|
12219
|
+
const lastSubscription = this.subscribe();
|
|
12220
|
+
lastSubscription();
|
|
12221
|
+
}
|
|
12222
|
+
};
|
|
11352
12223
|
/**
|
|
11353
12224
|
* Processes tick events and accumulates all event types.
|
|
11354
12225
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -11414,6 +12285,9 @@ class LiveMarkdownService {
|
|
|
11414
12285
|
frameName,
|
|
11415
12286
|
backtest,
|
|
11416
12287
|
});
|
|
12288
|
+
if (!this.subscribe.hasValue()) {
|
|
12289
|
+
throw new Error("LiveMarkdownService not initialized. Call subscribe() before getting data.");
|
|
12290
|
+
}
|
|
11417
12291
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
11418
12292
|
return storage.getData();
|
|
11419
12293
|
};
|
|
@@ -11444,6 +12318,9 @@ class LiveMarkdownService {
|
|
|
11444
12318
|
frameName,
|
|
11445
12319
|
backtest,
|
|
11446
12320
|
});
|
|
12321
|
+
if (!this.subscribe.hasValue()) {
|
|
12322
|
+
throw new Error("LiveMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
12323
|
+
}
|
|
11447
12324
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
11448
12325
|
return storage.getReport(strategyName, columns);
|
|
11449
12326
|
};
|
|
@@ -11480,6 +12357,9 @@ class LiveMarkdownService {
|
|
|
11480
12357
|
backtest,
|
|
11481
12358
|
path,
|
|
11482
12359
|
});
|
|
12360
|
+
if (!this.subscribe.hasValue()) {
|
|
12361
|
+
throw new Error("LiveMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
12362
|
+
}
|
|
11483
12363
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
11484
12364
|
await storage.dump(strategyName, path, columns);
|
|
11485
12365
|
};
|
|
@@ -11513,21 +12393,6 @@ class LiveMarkdownService {
|
|
|
11513
12393
|
this.getStorage.clear();
|
|
11514
12394
|
}
|
|
11515
12395
|
};
|
|
11516
|
-
/**
|
|
11517
|
-
* Initializes the service by subscribing to live signal events.
|
|
11518
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
11519
|
-
* Automatically called on first use.
|
|
11520
|
-
*
|
|
11521
|
-
* @example
|
|
11522
|
-
* ```typescript
|
|
11523
|
-
* const service = new LiveMarkdownService();
|
|
11524
|
-
* await service.init(); // Subscribe to live events
|
|
11525
|
-
* ```
|
|
11526
|
-
*/
|
|
11527
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
11528
|
-
this.loggerService.log("liveMarkdownService init");
|
|
11529
|
-
this.unsubscribe = signalLiveEmitter.subscribe(this.tick);
|
|
11530
|
-
});
|
|
11531
12396
|
}
|
|
11532
12397
|
}
|
|
11533
12398
|
|
|
@@ -11548,6 +12413,26 @@ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
11548
12413
|
parts.push(backtest ? "backtest" : "live");
|
|
11549
12414
|
return parts.join(":");
|
|
11550
12415
|
};
|
|
12416
|
+
/**
|
|
12417
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
12418
|
+
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
12419
|
+
* @param symbol - Trading pair symbol
|
|
12420
|
+
* @param strategyName - Name of the strategy
|
|
12421
|
+
* @param exchangeName - Exchange name
|
|
12422
|
+
* @param frameName - Frame name
|
|
12423
|
+
* @param timestamp - Unix timestamp in milliseconds
|
|
12424
|
+
* @returns Filename string
|
|
12425
|
+
*/
|
|
12426
|
+
const CREATE_FILE_NAME_FN$6 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
12427
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
12428
|
+
if (frameName) {
|
|
12429
|
+
parts.push(frameName);
|
|
12430
|
+
parts.push("backtest");
|
|
12431
|
+
}
|
|
12432
|
+
else
|
|
12433
|
+
parts.push("live");
|
|
12434
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
12435
|
+
};
|
|
11551
12436
|
/** Maximum number of events to store in schedule reports */
|
|
11552
12437
|
const MAX_EVENTS$5 = 250;
|
|
11553
12438
|
/**
|
|
@@ -11555,7 +12440,11 @@ const MAX_EVENTS$5 = 250;
|
|
|
11555
12440
|
* Maintains a chronological list of scheduled and cancelled events.
|
|
11556
12441
|
*/
|
|
11557
12442
|
let ReportStorage$4 = class ReportStorage {
|
|
11558
|
-
constructor() {
|
|
12443
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
12444
|
+
this.symbol = symbol;
|
|
12445
|
+
this.strategyName = strategyName;
|
|
12446
|
+
this.exchangeName = exchangeName;
|
|
12447
|
+
this.frameName = frameName;
|
|
11559
12448
|
/** Internal list of all scheduled events for this strategy */
|
|
11560
12449
|
this._eventList = [];
|
|
11561
12450
|
}
|
|
@@ -11576,6 +12465,9 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
11576
12465
|
priceOpen: data.signal.priceOpen,
|
|
11577
12466
|
takeProfit: data.signal.priceTakeProfit,
|
|
11578
12467
|
stopLoss: data.signal.priceStopLoss,
|
|
12468
|
+
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12469
|
+
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12470
|
+
totalExecuted: data.signal.totalExecuted,
|
|
11579
12471
|
});
|
|
11580
12472
|
// Trim queue if exceeded MAX_EVENTS
|
|
11581
12473
|
if (this._eventList.length > MAX_EVENTS$5) {
|
|
@@ -11601,6 +12493,9 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
11601
12493
|
priceOpen: data.signal.priceOpen,
|
|
11602
12494
|
takeProfit: data.signal.priceTakeProfit,
|
|
11603
12495
|
stopLoss: data.signal.priceStopLoss,
|
|
12496
|
+
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12497
|
+
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12498
|
+
totalExecuted: data.signal.totalExecuted,
|
|
11604
12499
|
duration: durationMin,
|
|
11605
12500
|
};
|
|
11606
12501
|
this._eventList.unshift(newEvent);
|
|
@@ -11628,6 +12523,9 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
11628
12523
|
priceOpen: data.signal.priceOpen,
|
|
11629
12524
|
takeProfit: data.signal.priceTakeProfit,
|
|
11630
12525
|
stopLoss: data.signal.priceStopLoss,
|
|
12526
|
+
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12527
|
+
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12528
|
+
totalExecuted: data.signal.totalExecuted,
|
|
11631
12529
|
closeTimestamp: data.closeTimestamp,
|
|
11632
12530
|
duration: durationMin,
|
|
11633
12531
|
cancelReason: data.reason,
|
|
@@ -11739,19 +12637,19 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
11739
12637
|
* @param path - Directory path to save report (default: "./dump/schedule")
|
|
11740
12638
|
* @param columns - Column configuration for formatting the table
|
|
11741
12639
|
*/
|
|
11742
|
-
async dump(strategyName, path
|
|
12640
|
+
async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
|
|
11743
12641
|
const markdown = await this.getReport(strategyName, columns);
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
}
|
|
12642
|
+
const timestamp = Date.now();
|
|
12643
|
+
const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
12644
|
+
await Markdown.writeData("schedule", markdown, {
|
|
12645
|
+
path,
|
|
12646
|
+
file: filename,
|
|
12647
|
+
symbol: this.symbol,
|
|
12648
|
+
signalId: "",
|
|
12649
|
+
strategyName: this.strategyName,
|
|
12650
|
+
exchangeName: this.exchangeName,
|
|
12651
|
+
frameName: this.frameName
|
|
12652
|
+
});
|
|
11755
12653
|
}
|
|
11756
12654
|
};
|
|
11757
12655
|
/**
|
|
@@ -11783,28 +12681,70 @@ class ScheduleMarkdownService {
|
|
|
11783
12681
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
11784
12682
|
* Each combination gets its own isolated storage instance.
|
|
11785
12683
|
*/
|
|
11786
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$4());
|
|
12684
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
|
|
11787
12685
|
/**
|
|
11788
|
-
*
|
|
11789
|
-
*
|
|
11790
|
-
*
|
|
11791
|
-
* Processes only scheduled, opened and cancelled event types.
|
|
11792
|
-
*
|
|
11793
|
-
* @param data - Tick result from strategy execution with frameName wrapper
|
|
12686
|
+
* Subscribes to signal emitter to receive scheduled signal events.
|
|
12687
|
+
* Protected against multiple subscriptions.
|
|
12688
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
11794
12689
|
*
|
|
11795
12690
|
* @example
|
|
11796
12691
|
* ```typescript
|
|
11797
12692
|
* const service = new ScheduleMarkdownService();
|
|
11798
|
-
*
|
|
12693
|
+
* const unsubscribe = service.subscribe();
|
|
12694
|
+
* // ... later
|
|
12695
|
+
* unsubscribe();
|
|
11799
12696
|
* ```
|
|
11800
12697
|
*/
|
|
11801
|
-
this.
|
|
11802
|
-
this.loggerService.log("scheduleMarkdownService
|
|
11803
|
-
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
12698
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
12699
|
+
this.loggerService.log("scheduleMarkdownService init");
|
|
12700
|
+
const unsubscribe = signalEmitter.subscribe(this.tick);
|
|
12701
|
+
return () => {
|
|
12702
|
+
this.subscribe.clear();
|
|
12703
|
+
this.clear();
|
|
12704
|
+
unsubscribe();
|
|
12705
|
+
};
|
|
12706
|
+
});
|
|
12707
|
+
/**
|
|
12708
|
+
* Unsubscribes from signal emitter to stop receiving scheduled signal events.
|
|
12709
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
12710
|
+
* If not subscribed, does nothing.
|
|
12711
|
+
*
|
|
12712
|
+
* @example
|
|
12713
|
+
* ```typescript
|
|
12714
|
+
* const service = new ScheduleMarkdownService();
|
|
12715
|
+
* service.subscribe();
|
|
12716
|
+
* // ... later
|
|
12717
|
+
* service.unsubscribe();
|
|
12718
|
+
* ```
|
|
12719
|
+
*/
|
|
12720
|
+
this.unsubscribe = async () => {
|
|
12721
|
+
this.loggerService.log("scheduleMarkdownService unsubscribe");
|
|
12722
|
+
if (this.subscribe.hasValue()) {
|
|
12723
|
+
const lastSubscription = this.subscribe();
|
|
12724
|
+
lastSubscription();
|
|
12725
|
+
}
|
|
12726
|
+
};
|
|
12727
|
+
/**
|
|
12728
|
+
* Processes tick events and accumulates scheduled/opened/cancelled events.
|
|
12729
|
+
* Should be called from signalEmitter subscription.
|
|
12730
|
+
*
|
|
12731
|
+
* Processes only scheduled, opened and cancelled event types.
|
|
12732
|
+
*
|
|
12733
|
+
* @param data - Tick result from strategy execution with frameName wrapper
|
|
12734
|
+
*
|
|
12735
|
+
* @example
|
|
12736
|
+
* ```typescript
|
|
12737
|
+
* const service = new ScheduleMarkdownService();
|
|
12738
|
+
* // Service automatically subscribes in init()
|
|
12739
|
+
* ```
|
|
12740
|
+
*/
|
|
12741
|
+
this.tick = async (data) => {
|
|
12742
|
+
this.loggerService.log("scheduleMarkdownService tick", {
|
|
12743
|
+
data,
|
|
12744
|
+
});
|
|
12745
|
+
const storage = this.getStorage(data.symbol, data.strategyName, data.exchangeName, data.frameName, data.backtest);
|
|
12746
|
+
if (data.action === "scheduled") {
|
|
12747
|
+
storage.addScheduledEvent(data);
|
|
11808
12748
|
}
|
|
11809
12749
|
else if (data.action === "opened") {
|
|
11810
12750
|
// Check if this opened signal was previously scheduled
|
|
@@ -11843,6 +12783,9 @@ class ScheduleMarkdownService {
|
|
|
11843
12783
|
frameName,
|
|
11844
12784
|
backtest,
|
|
11845
12785
|
});
|
|
12786
|
+
if (!this.subscribe.hasValue()) {
|
|
12787
|
+
throw new Error("ScheduleMarkdownService not initialized. Call subscribe() before getting data.");
|
|
12788
|
+
}
|
|
11846
12789
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
11847
12790
|
return storage.getData();
|
|
11848
12791
|
};
|
|
@@ -11873,6 +12816,9 @@ class ScheduleMarkdownService {
|
|
|
11873
12816
|
frameName,
|
|
11874
12817
|
backtest,
|
|
11875
12818
|
});
|
|
12819
|
+
if (!this.subscribe.hasValue()) {
|
|
12820
|
+
throw new Error("ScheduleMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
12821
|
+
}
|
|
11876
12822
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
11877
12823
|
return storage.getReport(strategyName, columns);
|
|
11878
12824
|
};
|
|
@@ -11909,6 +12855,9 @@ class ScheduleMarkdownService {
|
|
|
11909
12855
|
backtest,
|
|
11910
12856
|
path,
|
|
11911
12857
|
});
|
|
12858
|
+
if (!this.subscribe.hasValue()) {
|
|
12859
|
+
throw new Error("ScheduleMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
12860
|
+
}
|
|
11912
12861
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
11913
12862
|
await storage.dump(strategyName, path, columns);
|
|
11914
12863
|
};
|
|
@@ -11942,21 +12891,6 @@ class ScheduleMarkdownService {
|
|
|
11942
12891
|
this.getStorage.clear();
|
|
11943
12892
|
}
|
|
11944
12893
|
};
|
|
11945
|
-
/**
|
|
11946
|
-
* Initializes the service by subscribing to live signal events.
|
|
11947
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
11948
|
-
* Automatically called on first use.
|
|
11949
|
-
*
|
|
11950
|
-
* @example
|
|
11951
|
-
* ```typescript
|
|
11952
|
-
* const service = new ScheduleMarkdownService();
|
|
11953
|
-
* await service.init(); // Subscribe to live events
|
|
11954
|
-
* ```
|
|
11955
|
-
*/
|
|
11956
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
11957
|
-
this.loggerService.log("scheduleMarkdownService init");
|
|
11958
|
-
this.unsubscribe = signalEmitter.subscribe(this.tick);
|
|
11959
|
-
});
|
|
11960
12894
|
}
|
|
11961
12895
|
}
|
|
11962
12896
|
|
|
@@ -11977,6 +12911,26 @@ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
11977
12911
|
parts.push(backtest ? "backtest" : "live");
|
|
11978
12912
|
return parts.join(":");
|
|
11979
12913
|
};
|
|
12914
|
+
/**
|
|
12915
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
12916
|
+
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
12917
|
+
* @param symbol - Trading pair symbol
|
|
12918
|
+
* @param strategyName - Name of the strategy
|
|
12919
|
+
* @param exchangeName - Exchange name
|
|
12920
|
+
* @param frameName - Frame name
|
|
12921
|
+
* @param timestamp - Unix timestamp in milliseconds
|
|
12922
|
+
* @returns Filename string
|
|
12923
|
+
*/
|
|
12924
|
+
const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
12925
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
12926
|
+
if (frameName) {
|
|
12927
|
+
parts.push(frameName);
|
|
12928
|
+
parts.push("backtest");
|
|
12929
|
+
}
|
|
12930
|
+
else
|
|
12931
|
+
parts.push("live");
|
|
12932
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
12933
|
+
};
|
|
11980
12934
|
/**
|
|
11981
12935
|
* Calculates percentile value from sorted array.
|
|
11982
12936
|
*/
|
|
@@ -11993,7 +12947,11 @@ const MAX_EVENTS$4 = 10000;
|
|
|
11993
12947
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
11994
12948
|
*/
|
|
11995
12949
|
class PerformanceStorage {
|
|
11996
|
-
constructor() {
|
|
12950
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
12951
|
+
this.symbol = symbol;
|
|
12952
|
+
this.strategyName = strategyName;
|
|
12953
|
+
this.exchangeName = exchangeName;
|
|
12954
|
+
this.frameName = frameName;
|
|
11997
12955
|
/** Internal list of all performance events for this strategy */
|
|
11998
12956
|
this._events = [];
|
|
11999
12957
|
}
|
|
@@ -12144,19 +13102,19 @@ class PerformanceStorage {
|
|
|
12144
13102
|
* @param path - Directory path to save report
|
|
12145
13103
|
* @param columns - Column configuration for formatting the table
|
|
12146
13104
|
*/
|
|
12147
|
-
async dump(strategyName, path
|
|
13105
|
+
async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
|
|
12148
13106
|
const markdown = await this.getReport(strategyName, columns);
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
}
|
|
13107
|
+
const timestamp = Date.now();
|
|
13108
|
+
const filename = CREATE_FILE_NAME_FN$5(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
13109
|
+
await Markdown.writeData("performance", markdown, {
|
|
13110
|
+
path,
|
|
13111
|
+
file: filename,
|
|
13112
|
+
symbol: this.symbol,
|
|
13113
|
+
signalId: "",
|
|
13114
|
+
strategyName: this.strategyName,
|
|
13115
|
+
exchangeName: this.exchangeName,
|
|
13116
|
+
frameName: this.frameName
|
|
13117
|
+
});
|
|
12160
13118
|
}
|
|
12161
13119
|
}
|
|
12162
13120
|
/**
|
|
@@ -12194,7 +13152,49 @@ class PerformanceMarkdownService {
|
|
|
12194
13152
|
* Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
12195
13153
|
* Each combination gets its own isolated storage instance.
|
|
12196
13154
|
*/
|
|
12197
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), () => new PerformanceStorage());
|
|
13155
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
|
|
13156
|
+
/**
|
|
13157
|
+
* Subscribes to performance emitter to receive performance events.
|
|
13158
|
+
* Protected against multiple subscriptions.
|
|
13159
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
13160
|
+
*
|
|
13161
|
+
* @example
|
|
13162
|
+
* ```typescript
|
|
13163
|
+
* const service = new PerformanceMarkdownService();
|
|
13164
|
+
* const unsubscribe = service.subscribe();
|
|
13165
|
+
* // ... later
|
|
13166
|
+
* unsubscribe();
|
|
13167
|
+
* ```
|
|
13168
|
+
*/
|
|
13169
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
13170
|
+
this.loggerService.log("performanceMarkdownService init");
|
|
13171
|
+
const unsubscribe = performanceEmitter.subscribe(this.track);
|
|
13172
|
+
return () => {
|
|
13173
|
+
this.subscribe.clear();
|
|
13174
|
+
this.clear();
|
|
13175
|
+
unsubscribe();
|
|
13176
|
+
};
|
|
13177
|
+
});
|
|
13178
|
+
/**
|
|
13179
|
+
* Unsubscribes from performance emitter to stop receiving events.
|
|
13180
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
13181
|
+
* If not subscribed, does nothing.
|
|
13182
|
+
*
|
|
13183
|
+
* @example
|
|
13184
|
+
* ```typescript
|
|
13185
|
+
* const service = new PerformanceMarkdownService();
|
|
13186
|
+
* service.subscribe();
|
|
13187
|
+
* // ... later
|
|
13188
|
+
* service.unsubscribe();
|
|
13189
|
+
* ```
|
|
13190
|
+
*/
|
|
13191
|
+
this.unsubscribe = async () => {
|
|
13192
|
+
this.loggerService.log("performanceMarkdownService unsubscribe");
|
|
13193
|
+
if (this.subscribe.hasValue()) {
|
|
13194
|
+
const lastSubscription = this.subscribe();
|
|
13195
|
+
lastSubscription();
|
|
13196
|
+
}
|
|
13197
|
+
};
|
|
12198
13198
|
/**
|
|
12199
13199
|
* Processes performance events and accumulates metrics.
|
|
12200
13200
|
* Should be called from performance tracking code.
|
|
@@ -12237,6 +13237,9 @@ class PerformanceMarkdownService {
|
|
|
12237
13237
|
frameName,
|
|
12238
13238
|
backtest,
|
|
12239
13239
|
});
|
|
13240
|
+
if (!this.subscribe.hasValue()) {
|
|
13241
|
+
throw new Error("PerformanceMarkdownService not initialized. Call subscribe() before getting data.");
|
|
13242
|
+
}
|
|
12240
13243
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
12241
13244
|
return storage.getData(strategyName);
|
|
12242
13245
|
};
|
|
@@ -12265,6 +13268,9 @@ class PerformanceMarkdownService {
|
|
|
12265
13268
|
frameName,
|
|
12266
13269
|
backtest,
|
|
12267
13270
|
});
|
|
13271
|
+
if (!this.subscribe.hasValue()) {
|
|
13272
|
+
throw new Error("PerformanceMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
13273
|
+
}
|
|
12268
13274
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
12269
13275
|
return storage.getReport(strategyName, columns);
|
|
12270
13276
|
};
|
|
@@ -12297,6 +13303,9 @@ class PerformanceMarkdownService {
|
|
|
12297
13303
|
backtest,
|
|
12298
13304
|
path,
|
|
12299
13305
|
});
|
|
13306
|
+
if (!this.subscribe.hasValue()) {
|
|
13307
|
+
throw new Error("PerformanceMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
13308
|
+
}
|
|
12300
13309
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
12301
13310
|
await storage.dump(strategyName, path, columns);
|
|
12302
13311
|
};
|
|
@@ -12317,17 +13326,16 @@ class PerformanceMarkdownService {
|
|
|
12317
13326
|
this.getStorage.clear();
|
|
12318
13327
|
}
|
|
12319
13328
|
};
|
|
12320
|
-
/**
|
|
12321
|
-
* Initializes the service by subscribing to performance events.
|
|
12322
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
12323
|
-
*/
|
|
12324
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
12325
|
-
this.loggerService.log("performanceMarkdownService init");
|
|
12326
|
-
this.unsubscribe = performanceEmitter.subscribe(this.track);
|
|
12327
|
-
});
|
|
12328
13329
|
}
|
|
12329
13330
|
}
|
|
12330
13331
|
|
|
13332
|
+
/**
|
|
13333
|
+
* Creates a filename for markdown report based on walker name.
|
|
13334
|
+
* Filename format: "walkerName-timestamp.md"
|
|
13335
|
+
*/
|
|
13336
|
+
const CREATE_FILE_NAME_FN$4 = (walkerName, timestamp) => {
|
|
13337
|
+
return `${walkerName}-${timestamp}.md`;
|
|
13338
|
+
};
|
|
12331
13339
|
/**
|
|
12332
13340
|
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
12333
13341
|
*/
|
|
@@ -12545,19 +13553,19 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
12545
13553
|
* @param strategyColumns - Column configuration for strategy comparison table
|
|
12546
13554
|
* @param pnlColumns - Column configuration for PNL table
|
|
12547
13555
|
*/
|
|
12548
|
-
async dump(symbol, metric, context, path
|
|
13556
|
+
async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
|
|
12549
13557
|
const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
}
|
|
13558
|
+
const timestamp = Date.now();
|
|
13559
|
+
const filename = CREATE_FILE_NAME_FN$4(this.walkerName, timestamp);
|
|
13560
|
+
await Markdown.writeData("walker", markdown, {
|
|
13561
|
+
path,
|
|
13562
|
+
file: filename,
|
|
13563
|
+
symbol: "",
|
|
13564
|
+
signalId: "",
|
|
13565
|
+
strategyName: "",
|
|
13566
|
+
exchangeName: "",
|
|
13567
|
+
frameName: ""
|
|
13568
|
+
});
|
|
12561
13569
|
}
|
|
12562
13570
|
};
|
|
12563
13571
|
/**
|
|
@@ -12585,6 +13593,48 @@ class WalkerMarkdownService {
|
|
|
12585
13593
|
* Each walker gets its own isolated storage instance.
|
|
12586
13594
|
*/
|
|
12587
13595
|
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$3(walkerName));
|
|
13596
|
+
/**
|
|
13597
|
+
* Subscribes to walker emitter to receive walker progress events.
|
|
13598
|
+
* Protected against multiple subscriptions.
|
|
13599
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
13600
|
+
*
|
|
13601
|
+
* @example
|
|
13602
|
+
* ```typescript
|
|
13603
|
+
* const service = new WalkerMarkdownService();
|
|
13604
|
+
* const unsubscribe = service.subscribe();
|
|
13605
|
+
* // ... later
|
|
13606
|
+
* unsubscribe();
|
|
13607
|
+
* ```
|
|
13608
|
+
*/
|
|
13609
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
13610
|
+
this.loggerService.log("walkerMarkdownService init");
|
|
13611
|
+
const unsubscribe = walkerEmitter.subscribe(this.tick);
|
|
13612
|
+
return () => {
|
|
13613
|
+
this.subscribe.clear();
|
|
13614
|
+
this.clear();
|
|
13615
|
+
unsubscribe();
|
|
13616
|
+
};
|
|
13617
|
+
});
|
|
13618
|
+
/**
|
|
13619
|
+
* Unsubscribes from walker emitter to stop receiving events.
|
|
13620
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
13621
|
+
* If not subscribed, does nothing.
|
|
13622
|
+
*
|
|
13623
|
+
* @example
|
|
13624
|
+
* ```typescript
|
|
13625
|
+
* const service = new WalkerMarkdownService();
|
|
13626
|
+
* service.subscribe();
|
|
13627
|
+
* // ... later
|
|
13628
|
+
* service.unsubscribe();
|
|
13629
|
+
* ```
|
|
13630
|
+
*/
|
|
13631
|
+
this.unsubscribe = async () => {
|
|
13632
|
+
this.loggerService.log("walkerMarkdownService unsubscribe");
|
|
13633
|
+
if (this.subscribe.hasValue()) {
|
|
13634
|
+
const lastSubscription = this.subscribe();
|
|
13635
|
+
lastSubscription();
|
|
13636
|
+
}
|
|
13637
|
+
};
|
|
12588
13638
|
/**
|
|
12589
13639
|
* Processes walker progress events and accumulates strategy results.
|
|
12590
13640
|
* Should be called from walkerEmitter.
|
|
@@ -12628,6 +13678,9 @@ class WalkerMarkdownService {
|
|
|
12628
13678
|
metric,
|
|
12629
13679
|
context,
|
|
12630
13680
|
});
|
|
13681
|
+
if (!this.subscribe.hasValue()) {
|
|
13682
|
+
throw new Error("WalkerMarkdownService not initialized. Call subscribe() before getting data.");
|
|
13683
|
+
}
|
|
12631
13684
|
const storage = this.getStorage(walkerName);
|
|
12632
13685
|
return storage.getData(symbol, metric, context);
|
|
12633
13686
|
};
|
|
@@ -12657,6 +13710,9 @@ class WalkerMarkdownService {
|
|
|
12657
13710
|
metric,
|
|
12658
13711
|
context,
|
|
12659
13712
|
});
|
|
13713
|
+
if (!this.subscribe.hasValue()) {
|
|
13714
|
+
throw new Error("WalkerMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
13715
|
+
}
|
|
12660
13716
|
const storage = this.getStorage(walkerName);
|
|
12661
13717
|
return storage.getReport(symbol, metric, context, strategyColumns, pnlColumns);
|
|
12662
13718
|
};
|
|
@@ -12692,6 +13748,9 @@ class WalkerMarkdownService {
|
|
|
12692
13748
|
context,
|
|
12693
13749
|
path,
|
|
12694
13750
|
});
|
|
13751
|
+
if (!this.subscribe.hasValue()) {
|
|
13752
|
+
throw new Error("WalkerMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
13753
|
+
}
|
|
12695
13754
|
const storage = this.getStorage(walkerName);
|
|
12696
13755
|
await storage.dump(symbol, metric, context, path, strategyColumns, pnlColumns);
|
|
12697
13756
|
};
|
|
@@ -12719,21 +13778,6 @@ class WalkerMarkdownService {
|
|
|
12719
13778
|
});
|
|
12720
13779
|
this.getStorage.clear(walkerName);
|
|
12721
13780
|
};
|
|
12722
|
-
/**
|
|
12723
|
-
* Initializes the service by subscribing to walker events.
|
|
12724
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
12725
|
-
* Automatically called on first use.
|
|
12726
|
-
*
|
|
12727
|
-
* @example
|
|
12728
|
-
* ```typescript
|
|
12729
|
-
* const service = new WalkerMarkdownService();
|
|
12730
|
-
* await service.init(); // Subscribe to walker events
|
|
12731
|
-
* ```
|
|
12732
|
-
*/
|
|
12733
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
12734
|
-
this.loggerService.log("walkerMarkdownService init");
|
|
12735
|
-
this.unsubscribe = walkerEmitter.subscribe(this.tick);
|
|
12736
|
-
});
|
|
12737
13781
|
}
|
|
12738
13782
|
}
|
|
12739
13783
|
|
|
@@ -12752,6 +13796,20 @@ const CREATE_KEY_FN$7 = (exchangeName, frameName, backtest) => {
|
|
|
12752
13796
|
parts.push(backtest ? "backtest" : "live");
|
|
12753
13797
|
return parts.join(":");
|
|
12754
13798
|
};
|
|
13799
|
+
/**
|
|
13800
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
13801
|
+
* Filename format: "strategyName_exchangeName_frameName-timestamp.md"
|
|
13802
|
+
*/
|
|
13803
|
+
const CREATE_FILE_NAME_FN$3 = (strategyName, exchangeName, frameName, timestamp) => {
|
|
13804
|
+
const parts = [strategyName, exchangeName];
|
|
13805
|
+
if (frameName) {
|
|
13806
|
+
parts.push(frameName);
|
|
13807
|
+
parts.push("backtest");
|
|
13808
|
+
}
|
|
13809
|
+
else
|
|
13810
|
+
parts.push("live");
|
|
13811
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
13812
|
+
};
|
|
12755
13813
|
const HEATMAP_METHOD_NAME_GET_DATA = "HeatMarkdownService.getData";
|
|
12756
13814
|
const HEATMAP_METHOD_NAME_GET_REPORT = "HeatMarkdownService.getReport";
|
|
12757
13815
|
const HEATMAP_METHOD_NAME_DUMP = "HeatMarkdownService.dump";
|
|
@@ -12781,7 +13839,10 @@ const MAX_EVENTS$3 = 250;
|
|
|
12781
13839
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
12782
13840
|
*/
|
|
12783
13841
|
class HeatmapStorage {
|
|
12784
|
-
constructor() {
|
|
13842
|
+
constructor(exchangeName, frameName, backtest) {
|
|
13843
|
+
this.exchangeName = exchangeName;
|
|
13844
|
+
this.frameName = frameName;
|
|
13845
|
+
this.backtest = backtest;
|
|
12785
13846
|
/** Internal storage of closed signals per symbol */
|
|
12786
13847
|
this.symbolData = new Map();
|
|
12787
13848
|
}
|
|
@@ -13046,19 +14107,19 @@ class HeatmapStorage {
|
|
|
13046
14107
|
* @param path - Directory path to save report (default: "./dump/heatmap")
|
|
13047
14108
|
* @param columns - Column configuration for formatting the table
|
|
13048
14109
|
*/
|
|
13049
|
-
async dump(strategyName, path
|
|
14110
|
+
async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
|
|
13050
14111
|
const markdown = await this.getReport(strategyName, columns);
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
}
|
|
14112
|
+
const timestamp = Date.now();
|
|
14113
|
+
const filename = CREATE_FILE_NAME_FN$3(strategyName, this.exchangeName, this.frameName, timestamp);
|
|
14114
|
+
await Markdown.writeData("heat", markdown, {
|
|
14115
|
+
path,
|
|
14116
|
+
file: filename,
|
|
14117
|
+
symbol: "",
|
|
14118
|
+
strategyName: "",
|
|
14119
|
+
signalId: "",
|
|
14120
|
+
exchangeName: this.exchangeName,
|
|
14121
|
+
frameName: this.frameName
|
|
14122
|
+
});
|
|
13062
14123
|
}
|
|
13063
14124
|
}
|
|
13064
14125
|
/**
|
|
@@ -13095,7 +14156,49 @@ class HeatMarkdownService {
|
|
|
13095
14156
|
* Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
|
|
13096
14157
|
* Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
|
|
13097
14158
|
*/
|
|
13098
|
-
this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(exchangeName, frameName, backtest), () => new HeatmapStorage());
|
|
14159
|
+
this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
|
|
14160
|
+
/**
|
|
14161
|
+
* Subscribes to signal emitter to receive tick events.
|
|
14162
|
+
* Protected against multiple subscriptions.
|
|
14163
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
14164
|
+
*
|
|
14165
|
+
* @example
|
|
14166
|
+
* ```typescript
|
|
14167
|
+
* const service = new HeatMarkdownService();
|
|
14168
|
+
* const unsubscribe = service.subscribe();
|
|
14169
|
+
* // ... later
|
|
14170
|
+
* unsubscribe();
|
|
14171
|
+
* ```
|
|
14172
|
+
*/
|
|
14173
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
14174
|
+
this.loggerService.log("heatMarkdownService init");
|
|
14175
|
+
const unsubscribe = signalEmitter.subscribe(this.tick);
|
|
14176
|
+
return () => {
|
|
14177
|
+
this.subscribe.clear();
|
|
14178
|
+
this.clear();
|
|
14179
|
+
unsubscribe();
|
|
14180
|
+
};
|
|
14181
|
+
});
|
|
14182
|
+
/**
|
|
14183
|
+
* Unsubscribes from signal emitter to stop receiving tick events.
|
|
14184
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
14185
|
+
* If not subscribed, does nothing.
|
|
14186
|
+
*
|
|
14187
|
+
* @example
|
|
14188
|
+
* ```typescript
|
|
14189
|
+
* const service = new HeatMarkdownService();
|
|
14190
|
+
* service.subscribe();
|
|
14191
|
+
* // ... later
|
|
14192
|
+
* service.unsubscribe();
|
|
14193
|
+
* ```
|
|
14194
|
+
*/
|
|
14195
|
+
this.unsubscribe = async () => {
|
|
14196
|
+
this.loggerService.log("heatMarkdownService unsubscribe");
|
|
14197
|
+
if (this.subscribe.hasValue()) {
|
|
14198
|
+
const lastSubscription = this.subscribe();
|
|
14199
|
+
lastSubscription();
|
|
14200
|
+
}
|
|
14201
|
+
};
|
|
13099
14202
|
/**
|
|
13100
14203
|
* Processes tick events and accumulates closed signals.
|
|
13101
14204
|
* Should be called from signal emitter subscription.
|
|
@@ -13141,6 +14244,9 @@ class HeatMarkdownService {
|
|
|
13141
14244
|
frameName,
|
|
13142
14245
|
backtest,
|
|
13143
14246
|
});
|
|
14247
|
+
if (!this.subscribe.hasValue()) {
|
|
14248
|
+
throw new Error("HeatMarkdownService not initialized. Call subscribe() before getting data.");
|
|
14249
|
+
}
|
|
13144
14250
|
const storage = this.getStorage(exchangeName, frameName, backtest);
|
|
13145
14251
|
return storage.getData();
|
|
13146
14252
|
};
|
|
@@ -13178,6 +14284,9 @@ class HeatMarkdownService {
|
|
|
13178
14284
|
frameName,
|
|
13179
14285
|
backtest,
|
|
13180
14286
|
});
|
|
14287
|
+
if (!this.subscribe.hasValue()) {
|
|
14288
|
+
throw new Error("HeatMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
14289
|
+
}
|
|
13181
14290
|
const storage = this.getStorage(exchangeName, frameName, backtest);
|
|
13182
14291
|
return storage.getReport(strategyName, columns);
|
|
13183
14292
|
};
|
|
@@ -13213,6 +14322,9 @@ class HeatMarkdownService {
|
|
|
13213
14322
|
backtest,
|
|
13214
14323
|
path,
|
|
13215
14324
|
});
|
|
14325
|
+
if (!this.subscribe.hasValue()) {
|
|
14326
|
+
throw new Error("HeatMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
14327
|
+
}
|
|
13216
14328
|
const storage = this.getStorage(exchangeName, frameName, backtest);
|
|
13217
14329
|
await storage.dump(strategyName, path, columns);
|
|
13218
14330
|
};
|
|
@@ -13246,21 +14358,6 @@ class HeatMarkdownService {
|
|
|
13246
14358
|
this.getStorage.clear();
|
|
13247
14359
|
}
|
|
13248
14360
|
};
|
|
13249
|
-
/**
|
|
13250
|
-
* Initializes the service by subscribing to signal events.
|
|
13251
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
13252
|
-
* Automatically called on first use.
|
|
13253
|
-
*
|
|
13254
|
-
* @example
|
|
13255
|
-
* ```typescript
|
|
13256
|
-
* const service = new HeatMarkdownService();
|
|
13257
|
-
* await service.init(); // Subscribe to signal events
|
|
13258
|
-
* ```
|
|
13259
|
-
*/
|
|
13260
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
13261
|
-
this.loggerService.log("heatMarkdownService init");
|
|
13262
|
-
this.unsubscribe = signalEmitter.subscribe(this.tick);
|
|
13263
|
-
});
|
|
13264
14361
|
}
|
|
13265
14362
|
}
|
|
13266
14363
|
|
|
@@ -15677,6 +16774,20 @@ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
15677
16774
|
parts.push(backtest ? "backtest" : "live");
|
|
15678
16775
|
return parts.join(":");
|
|
15679
16776
|
};
|
|
16777
|
+
/**
|
|
16778
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
16779
|
+
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
16780
|
+
*/
|
|
16781
|
+
const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
16782
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
16783
|
+
if (frameName) {
|
|
16784
|
+
parts.push(frameName);
|
|
16785
|
+
parts.push("backtest");
|
|
16786
|
+
}
|
|
16787
|
+
else
|
|
16788
|
+
parts.push("live");
|
|
16789
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
16790
|
+
};
|
|
15680
16791
|
/** Maximum number of events to store in partial reports */
|
|
15681
16792
|
const MAX_EVENTS$2 = 250;
|
|
15682
16793
|
/**
|
|
@@ -15684,7 +16795,11 @@ const MAX_EVENTS$2 = 250;
|
|
|
15684
16795
|
* Maintains a chronological list of profit and loss level events.
|
|
15685
16796
|
*/
|
|
15686
16797
|
let ReportStorage$2 = class ReportStorage {
|
|
15687
|
-
constructor() {
|
|
16798
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
16799
|
+
this.symbol = symbol;
|
|
16800
|
+
this.strategyName = strategyName;
|
|
16801
|
+
this.exchangeName = exchangeName;
|
|
16802
|
+
this.frameName = frameName;
|
|
15688
16803
|
/** Internal list of all partial events for this symbol */
|
|
15689
16804
|
this._eventList = [];
|
|
15690
16805
|
}
|
|
@@ -15807,19 +16922,19 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
15807
16922
|
* @param path - Directory path to save report (default: "./dump/partial")
|
|
15808
16923
|
* @param columns - Column configuration for formatting the table
|
|
15809
16924
|
*/
|
|
15810
|
-
async dump(symbol, strategyName, path
|
|
16925
|
+
async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
|
|
15811
16926
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
|
|
15816
|
-
|
|
15817
|
-
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
}
|
|
16927
|
+
const timestamp = Date.now();
|
|
16928
|
+
const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
16929
|
+
await Markdown.writeData("partial", markdown, {
|
|
16930
|
+
path,
|
|
16931
|
+
file: filename,
|
|
16932
|
+
symbol: this.symbol,
|
|
16933
|
+
signalId: "",
|
|
16934
|
+
strategyName: this.strategyName,
|
|
16935
|
+
exchangeName: this.exchangeName,
|
|
16936
|
+
frameName: this.frameName
|
|
16937
|
+
});
|
|
15823
16938
|
}
|
|
15824
16939
|
};
|
|
15825
16940
|
/**
|
|
@@ -15851,7 +16966,51 @@ class PartialMarkdownService {
|
|
|
15851
16966
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
15852
16967
|
* Each combination gets its own isolated storage instance.
|
|
15853
16968
|
*/
|
|
15854
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$2());
|
|
16969
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
|
|
16970
|
+
/**
|
|
16971
|
+
* Subscribes to partial profit/loss signal emitters to receive events.
|
|
16972
|
+
* Protected against multiple subscriptions.
|
|
16973
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
16974
|
+
*
|
|
16975
|
+
* @example
|
|
16976
|
+
* ```typescript
|
|
16977
|
+
* const service = new PartialMarkdownService();
|
|
16978
|
+
* const unsubscribe = service.subscribe();
|
|
16979
|
+
* // ... later
|
|
16980
|
+
* unsubscribe();
|
|
16981
|
+
* ```
|
|
16982
|
+
*/
|
|
16983
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
16984
|
+
this.loggerService.log("partialMarkdownService init");
|
|
16985
|
+
const unProfit = partialProfitSubject.subscribe(this.tickProfit);
|
|
16986
|
+
const unLoss = partialLossSubject.subscribe(this.tickLoss);
|
|
16987
|
+
return () => {
|
|
16988
|
+
this.subscribe.clear();
|
|
16989
|
+
this.clear();
|
|
16990
|
+
unProfit();
|
|
16991
|
+
unLoss();
|
|
16992
|
+
};
|
|
16993
|
+
});
|
|
16994
|
+
/**
|
|
16995
|
+
* Unsubscribes from partial profit/loss signal emitters to stop receiving events.
|
|
16996
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
16997
|
+
* If not subscribed, does nothing.
|
|
16998
|
+
*
|
|
16999
|
+
* @example
|
|
17000
|
+
* ```typescript
|
|
17001
|
+
* const service = new PartialMarkdownService();
|
|
17002
|
+
* service.subscribe();
|
|
17003
|
+
* // ... later
|
|
17004
|
+
* service.unsubscribe();
|
|
17005
|
+
* ```
|
|
17006
|
+
*/
|
|
17007
|
+
this.unsubscribe = async () => {
|
|
17008
|
+
this.loggerService.log("partialMarkdownService unsubscribe");
|
|
17009
|
+
if (this.subscribe.hasValue()) {
|
|
17010
|
+
const lastSubscription = this.subscribe();
|
|
17011
|
+
lastSubscription();
|
|
17012
|
+
}
|
|
17013
|
+
};
|
|
15855
17014
|
/**
|
|
15856
17015
|
* Processes profit events and accumulates them.
|
|
15857
17016
|
* Should be called from partialProfitSubject subscription.
|
|
@@ -15916,6 +17075,9 @@ class PartialMarkdownService {
|
|
|
15916
17075
|
frameName,
|
|
15917
17076
|
backtest,
|
|
15918
17077
|
});
|
|
17078
|
+
if (!this.subscribe.hasValue()) {
|
|
17079
|
+
throw new Error("PartialMarkdownService not initialized. Call subscribe() before getting data.");
|
|
17080
|
+
}
|
|
15919
17081
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
15920
17082
|
return storage.getData();
|
|
15921
17083
|
};
|
|
@@ -15946,6 +17108,9 @@ class PartialMarkdownService {
|
|
|
15946
17108
|
frameName,
|
|
15947
17109
|
backtest,
|
|
15948
17110
|
});
|
|
17111
|
+
if (!this.subscribe.hasValue()) {
|
|
17112
|
+
throw new Error("PartialMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
17113
|
+
}
|
|
15949
17114
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
15950
17115
|
return storage.getReport(symbol, strategyName, columns);
|
|
15951
17116
|
};
|
|
@@ -15982,6 +17147,9 @@ class PartialMarkdownService {
|
|
|
15982
17147
|
backtest,
|
|
15983
17148
|
path,
|
|
15984
17149
|
});
|
|
17150
|
+
if (!this.subscribe.hasValue()) {
|
|
17151
|
+
throw new Error("PartialMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
17152
|
+
}
|
|
15985
17153
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
15986
17154
|
await storage.dump(symbol, strategyName, path, columns);
|
|
15987
17155
|
};
|
|
@@ -16015,23 +17183,6 @@ class PartialMarkdownService {
|
|
|
16015
17183
|
this.getStorage.clear();
|
|
16016
17184
|
}
|
|
16017
17185
|
};
|
|
16018
|
-
/**
|
|
16019
|
-
* Initializes the service by subscribing to partial profit/loss events.
|
|
16020
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
16021
|
-
* Automatically called on first use.
|
|
16022
|
-
*
|
|
16023
|
-
* @example
|
|
16024
|
-
* ```typescript
|
|
16025
|
-
* const service = new PartialMarkdownService();
|
|
16026
|
-
* await service.init(); // Subscribe to profit/loss events
|
|
16027
|
-
* ```
|
|
16028
|
-
*/
|
|
16029
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
16030
|
-
this.loggerService.log("partialMarkdownService init");
|
|
16031
|
-
const unProfit = partialProfitSubject.subscribe(this.tickProfit);
|
|
16032
|
-
const unLoss = partialLossSubject.subscribe(this.tickLoss);
|
|
16033
|
-
this.unsubscribe = functoolsKit.compose(() => unProfit(), () => unLoss());
|
|
16034
|
-
});
|
|
16035
17186
|
}
|
|
16036
17187
|
}
|
|
16037
17188
|
|
|
@@ -16715,6 +17866,20 @@ const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
16715
17866
|
parts.push(backtest ? "backtest" : "live");
|
|
16716
17867
|
return parts.join(":");
|
|
16717
17868
|
};
|
|
17869
|
+
/**
|
|
17870
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
17871
|
+
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
17872
|
+
*/
|
|
17873
|
+
const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
17874
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
17875
|
+
if (frameName) {
|
|
17876
|
+
parts.push(frameName);
|
|
17877
|
+
parts.push("backtest");
|
|
17878
|
+
}
|
|
17879
|
+
else
|
|
17880
|
+
parts.push("live");
|
|
17881
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
17882
|
+
};
|
|
16718
17883
|
/** Maximum number of events to store in breakeven reports */
|
|
16719
17884
|
const MAX_EVENTS$1 = 250;
|
|
16720
17885
|
/**
|
|
@@ -16722,7 +17887,11 @@ const MAX_EVENTS$1 = 250;
|
|
|
16722
17887
|
* Maintains a chronological list of breakeven events.
|
|
16723
17888
|
*/
|
|
16724
17889
|
let ReportStorage$1 = class ReportStorage {
|
|
16725
|
-
constructor() {
|
|
17890
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
17891
|
+
this.symbol = symbol;
|
|
17892
|
+
this.strategyName = strategyName;
|
|
17893
|
+
this.exchangeName = exchangeName;
|
|
17894
|
+
this.frameName = frameName;
|
|
16726
17895
|
/** Internal list of all breakeven events for this symbol */
|
|
16727
17896
|
this._eventList = [];
|
|
16728
17897
|
}
|
|
@@ -16811,19 +17980,19 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
16811
17980
|
* @param path - Directory path to save report (default: "./dump/breakeven")
|
|
16812
17981
|
* @param columns - Column configuration for formatting the table
|
|
16813
17982
|
*/
|
|
16814
|
-
async dump(symbol, strategyName, path
|
|
17983
|
+
async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
|
|
16815
17984
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
}
|
|
17985
|
+
const timestamp = Date.now();
|
|
17986
|
+
const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
17987
|
+
await Markdown.writeData("breakeven", markdown, {
|
|
17988
|
+
path,
|
|
17989
|
+
file: filename,
|
|
17990
|
+
symbol: this.symbol,
|
|
17991
|
+
strategyName: this.strategyName,
|
|
17992
|
+
exchangeName: this.exchangeName,
|
|
17993
|
+
signalId: "",
|
|
17994
|
+
frameName: this.frameName
|
|
17995
|
+
});
|
|
16827
17996
|
}
|
|
16828
17997
|
};
|
|
16829
17998
|
/**
|
|
@@ -16855,7 +18024,49 @@ class BreakevenMarkdownService {
|
|
|
16855
18024
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
16856
18025
|
* Each combination gets its own isolated storage instance.
|
|
16857
18026
|
*/
|
|
16858
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$1());
|
|
18027
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
|
|
18028
|
+
/**
|
|
18029
|
+
* Subscribes to breakeven signal emitter to receive events.
|
|
18030
|
+
* Protected against multiple subscriptions.
|
|
18031
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
18032
|
+
*
|
|
18033
|
+
* @example
|
|
18034
|
+
* ```typescript
|
|
18035
|
+
* const service = new BreakevenMarkdownService();
|
|
18036
|
+
* const unsubscribe = service.subscribe();
|
|
18037
|
+
* // ... later
|
|
18038
|
+
* unsubscribe();
|
|
18039
|
+
* ```
|
|
18040
|
+
*/
|
|
18041
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
18042
|
+
this.loggerService.log("breakevenMarkdownService init");
|
|
18043
|
+
const unBreakeven = breakevenSubject.subscribe(this.tickBreakeven);
|
|
18044
|
+
return () => {
|
|
18045
|
+
this.subscribe.clear();
|
|
18046
|
+
this.clear();
|
|
18047
|
+
unBreakeven();
|
|
18048
|
+
};
|
|
18049
|
+
});
|
|
18050
|
+
/**
|
|
18051
|
+
* Unsubscribes from breakeven signal emitter to stop receiving events.
|
|
18052
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
18053
|
+
* If not subscribed, does nothing.
|
|
18054
|
+
*
|
|
18055
|
+
* @example
|
|
18056
|
+
* ```typescript
|
|
18057
|
+
* const service = new BreakevenMarkdownService();
|
|
18058
|
+
* service.subscribe();
|
|
18059
|
+
* // ... later
|
|
18060
|
+
* service.unsubscribe();
|
|
18061
|
+
* ```
|
|
18062
|
+
*/
|
|
18063
|
+
this.unsubscribe = async () => {
|
|
18064
|
+
this.loggerService.log("breakevenMarkdownService unsubscribe");
|
|
18065
|
+
if (this.subscribe.hasValue()) {
|
|
18066
|
+
const lastSubscription = this.subscribe();
|
|
18067
|
+
lastSubscription();
|
|
18068
|
+
}
|
|
18069
|
+
};
|
|
16859
18070
|
/**
|
|
16860
18071
|
* Processes breakeven events and accumulates them.
|
|
16861
18072
|
* Should be called from breakevenSubject subscription.
|
|
@@ -16901,6 +18112,9 @@ class BreakevenMarkdownService {
|
|
|
16901
18112
|
frameName,
|
|
16902
18113
|
backtest,
|
|
16903
18114
|
});
|
|
18115
|
+
if (!this.subscribe.hasValue()) {
|
|
18116
|
+
throw new Error("BreakevenMarkdownService not initialized. Call subscribe() before getting data.");
|
|
18117
|
+
}
|
|
16904
18118
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
16905
18119
|
return storage.getData();
|
|
16906
18120
|
};
|
|
@@ -16931,6 +18145,9 @@ class BreakevenMarkdownService {
|
|
|
16931
18145
|
frameName,
|
|
16932
18146
|
backtest,
|
|
16933
18147
|
});
|
|
18148
|
+
if (!this.subscribe.hasValue()) {
|
|
18149
|
+
throw new Error("BreakevenMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
18150
|
+
}
|
|
16934
18151
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
16935
18152
|
return storage.getReport(symbol, strategyName, columns);
|
|
16936
18153
|
};
|
|
@@ -16967,6 +18184,9 @@ class BreakevenMarkdownService {
|
|
|
16967
18184
|
backtest,
|
|
16968
18185
|
path,
|
|
16969
18186
|
});
|
|
18187
|
+
if (!this.subscribe.hasValue()) {
|
|
18188
|
+
throw new Error("BreakevenMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
18189
|
+
}
|
|
16970
18190
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
16971
18191
|
await storage.dump(symbol, strategyName, path, columns);
|
|
16972
18192
|
};
|
|
@@ -17000,22 +18220,6 @@ class BreakevenMarkdownService {
|
|
|
17000
18220
|
this.getStorage.clear();
|
|
17001
18221
|
}
|
|
17002
18222
|
};
|
|
17003
|
-
/**
|
|
17004
|
-
* Initializes the service by subscribing to breakeven events.
|
|
17005
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
17006
|
-
* Automatically called on first use.
|
|
17007
|
-
*
|
|
17008
|
-
* @example
|
|
17009
|
-
* ```typescript
|
|
17010
|
-
* const service = new BreakevenMarkdownService();
|
|
17011
|
-
* await service.init(); // Subscribe to breakeven events
|
|
17012
|
-
* ```
|
|
17013
|
-
*/
|
|
17014
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
17015
|
-
this.loggerService.log("breakevenMarkdownService init");
|
|
17016
|
-
const unBreakeven = breakevenSubject.subscribe(this.tickBreakeven);
|
|
17017
|
-
this.unsubscribe = functoolsKit.compose(() => unBreakeven());
|
|
17018
|
-
});
|
|
17019
18223
|
}
|
|
17020
18224
|
}
|
|
17021
18225
|
|
|
@@ -17169,18 +18373,12 @@ const WARN_KB = 30;
|
|
|
17169
18373
|
* @param outputDir - Output directory path (default: "./dump/strategy")
|
|
17170
18374
|
* @returns Promise that resolves when all files are written
|
|
17171
18375
|
*/
|
|
17172
|
-
const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/
|
|
18376
|
+
const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/outline") => {
|
|
17173
18377
|
// Extract system messages and system reminders from existing data
|
|
17174
18378
|
const systemMessages = history.filter((m) => m.role === "system");
|
|
17175
18379
|
const userMessages = history.filter((m) => m.role === "user");
|
|
17176
18380
|
const subfolderPath = path.join(outputDir, String(signalId));
|
|
17177
|
-
|
|
17178
|
-
await fs$1.promises.access(subfolderPath);
|
|
17179
|
-
return;
|
|
17180
|
-
}
|
|
17181
|
-
catch {
|
|
17182
|
-
await fs$1.promises.mkdir(subfolderPath, { recursive: true });
|
|
17183
|
-
}
|
|
18381
|
+
// Generate system prompt markdown
|
|
17184
18382
|
{
|
|
17185
18383
|
let summary = "# Outline Result Summary\n";
|
|
17186
18384
|
{
|
|
@@ -17203,14 +18401,21 @@ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/str
|
|
|
17203
18401
|
summary += "\n";
|
|
17204
18402
|
});
|
|
17205
18403
|
}
|
|
17206
|
-
|
|
17207
|
-
|
|
18404
|
+
await Markdown.writeData("outline", summary, {
|
|
18405
|
+
path: subfolderPath,
|
|
18406
|
+
file: "00_system_prompt.md",
|
|
18407
|
+
symbol: "",
|
|
18408
|
+
signalId: String(signalId),
|
|
18409
|
+
strategyName: "",
|
|
18410
|
+
exchangeName: "",
|
|
18411
|
+
frameName: ""
|
|
18412
|
+
});
|
|
17208
18413
|
}
|
|
18414
|
+
// Generate user messages
|
|
17209
18415
|
{
|
|
17210
18416
|
await Promise.all(Array.from(userMessages.entries()).map(async ([idx, message]) => {
|
|
17211
18417
|
const messageNum = String(idx + 1).padStart(2, "0");
|
|
17212
18418
|
const contentFileName = `${messageNum}_user_message.md`;
|
|
17213
|
-
const contentFilePath = path.join(subfolderPath, contentFileName);
|
|
17214
18419
|
{
|
|
17215
18420
|
const messageSizeBytes = Buffer.byteLength(message.content, "utf8");
|
|
17216
18421
|
const messageSizeKb = Math.floor(messageSizeBytes / 1024);
|
|
@@ -17222,13 +18427,21 @@ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/str
|
|
|
17222
18427
|
content += `**ResultId**: ${String(signalId)}\n\n`;
|
|
17223
18428
|
content += message.content;
|
|
17224
18429
|
content += "\n";
|
|
17225
|
-
await
|
|
18430
|
+
await Markdown.writeData("outline", content, {
|
|
18431
|
+
path: subfolderPath,
|
|
18432
|
+
file: contentFileName,
|
|
18433
|
+
signalId: String(signalId),
|
|
18434
|
+
symbol: "",
|
|
18435
|
+
strategyName: "",
|
|
18436
|
+
exchangeName: "",
|
|
18437
|
+
frameName: ""
|
|
18438
|
+
});
|
|
17226
18439
|
}));
|
|
17227
18440
|
}
|
|
18441
|
+
// Generate LLM output
|
|
17228
18442
|
{
|
|
17229
18443
|
const messageNum = String(userMessages.length + 1).padStart(2, "0");
|
|
17230
18444
|
const contentFileName = `${messageNum}_llm_output.md`;
|
|
17231
|
-
const contentFilePath = path.join(subfolderPath, contentFileName);
|
|
17232
18445
|
let content = "# Full Outline Result\n\n";
|
|
17233
18446
|
content += `**ResultId**: ${String(signalId)}\n\n`;
|
|
17234
18447
|
if (signal) {
|
|
@@ -17237,7 +18450,15 @@ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/str
|
|
|
17237
18450
|
content += JSON.stringify(signal, null, 2);
|
|
17238
18451
|
content += "\n```\n";
|
|
17239
18452
|
}
|
|
17240
|
-
await
|
|
18453
|
+
await Markdown.writeData("outline", content, {
|
|
18454
|
+
path: subfolderPath,
|
|
18455
|
+
file: contentFileName,
|
|
18456
|
+
symbol: "",
|
|
18457
|
+
signalId: String(signalId),
|
|
18458
|
+
strategyName: "",
|
|
18459
|
+
exchangeName: "",
|
|
18460
|
+
frameName: ""
|
|
18461
|
+
});
|
|
17241
18462
|
}
|
|
17242
18463
|
};
|
|
17243
18464
|
/**
|
|
@@ -17303,7 +18524,7 @@ class OutlineMarkdownService {
|
|
|
17303
18524
|
* (slippage + fees) to guarantee profitable trades when TakeProfit is hit
|
|
17304
18525
|
* - **Range constraints**: Validates MIN < MAX relationships (e.g., StopLoss distances)
|
|
17305
18526
|
* - **Time-based parameters**: Ensures positive integer values for timeouts and lifetimes
|
|
17306
|
-
* - **Candle parameters**: Validates retry counts, delays,
|
|
18527
|
+
* - **Candle parameters**: Validates retry counts, delays, anomaly detection thresholds, and max candles per request
|
|
17307
18528
|
*
|
|
17308
18529
|
* @throws {Error} If any validation fails, throws with detailed breakdown of all errors
|
|
17309
18530
|
*
|
|
@@ -17412,6 +18633,9 @@ class ConfigValidationService {
|
|
|
17412
18633
|
if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN) || GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN <= 0) {
|
|
17413
18634
|
errors.push(`CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN must be a positive integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN}`);
|
|
17414
18635
|
}
|
|
18636
|
+
if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) || GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST <= 0) {
|
|
18637
|
+
errors.push(`CC_MAX_CANDLES_PER_REQUEST must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST}`);
|
|
18638
|
+
}
|
|
17415
18639
|
// Throw aggregated errors if any
|
|
17416
18640
|
if (errors.length > 0) {
|
|
17417
18641
|
const errorMessage = `GLOBAL_CONFIG validation failed:\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`;
|
|
@@ -17440,6 +18664,20 @@ const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
17440
18664
|
parts.push(backtest ? "backtest" : "live");
|
|
17441
18665
|
return parts.join(":");
|
|
17442
18666
|
};
|
|
18667
|
+
/**
|
|
18668
|
+
* Creates a filename for markdown report based on memoization key components.
|
|
18669
|
+
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
18670
|
+
*/
|
|
18671
|
+
const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
18672
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
18673
|
+
if (frameName) {
|
|
18674
|
+
parts.push(frameName);
|
|
18675
|
+
parts.push("backtest");
|
|
18676
|
+
}
|
|
18677
|
+
else
|
|
18678
|
+
parts.push("live");
|
|
18679
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
18680
|
+
};
|
|
17443
18681
|
/** Maximum number of events to store in risk reports */
|
|
17444
18682
|
const MAX_EVENTS = 250;
|
|
17445
18683
|
/**
|
|
@@ -17447,7 +18685,11 @@ const MAX_EVENTS = 250;
|
|
|
17447
18685
|
* Maintains a chronological list of rejected signals due to risk limits.
|
|
17448
18686
|
*/
|
|
17449
18687
|
class ReportStorage {
|
|
17450
|
-
constructor() {
|
|
18688
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
18689
|
+
this.symbol = symbol;
|
|
18690
|
+
this.strategyName = strategyName;
|
|
18691
|
+
this.exchangeName = exchangeName;
|
|
18692
|
+
this.frameName = frameName;
|
|
17451
18693
|
/** Internal list of all risk rejection events for this symbol */
|
|
17452
18694
|
this._eventList = [];
|
|
17453
18695
|
}
|
|
@@ -17540,19 +18782,19 @@ class ReportStorage {
|
|
|
17540
18782
|
* @param path - Directory path to save report (default: "./dump/risk")
|
|
17541
18783
|
* @param columns - Column configuration for formatting the table
|
|
17542
18784
|
*/
|
|
17543
|
-
async dump(symbol, strategyName, path
|
|
18785
|
+
async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
|
|
17544
18786
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
17548
|
-
|
|
17549
|
-
|
|
17550
|
-
|
|
17551
|
-
|
|
17552
|
-
|
|
17553
|
-
|
|
17554
|
-
|
|
17555
|
-
}
|
|
18787
|
+
const timestamp = Date.now();
|
|
18788
|
+
const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
18789
|
+
await Markdown.writeData("risk", markdown, {
|
|
18790
|
+
path,
|
|
18791
|
+
file: filename,
|
|
18792
|
+
symbol: this.symbol,
|
|
18793
|
+
signalId: "",
|
|
18794
|
+
strategyName: this.strategyName,
|
|
18795
|
+
exchangeName: this.exchangeName,
|
|
18796
|
+
frameName: this.frameName
|
|
18797
|
+
});
|
|
17556
18798
|
}
|
|
17557
18799
|
}
|
|
17558
18800
|
/**
|
|
@@ -17584,7 +18826,49 @@ class RiskMarkdownService {
|
|
|
17584
18826
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
17585
18827
|
* Each combination gets its own isolated storage instance.
|
|
17586
18828
|
*/
|
|
17587
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage());
|
|
18829
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
|
|
18830
|
+
/**
|
|
18831
|
+
* Subscribes to risk rejection emitter to receive rejection events.
|
|
18832
|
+
* Protected against multiple subscriptions.
|
|
18833
|
+
* Returns an unsubscribe function to stop receiving events.
|
|
18834
|
+
*
|
|
18835
|
+
* @example
|
|
18836
|
+
* ```typescript
|
|
18837
|
+
* const service = new RiskMarkdownService();
|
|
18838
|
+
* const unsubscribe = service.subscribe();
|
|
18839
|
+
* // ... later
|
|
18840
|
+
* unsubscribe();
|
|
18841
|
+
* ```
|
|
18842
|
+
*/
|
|
18843
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
18844
|
+
this.loggerService.log("riskMarkdownService init");
|
|
18845
|
+
const unsubscribe = riskSubject.subscribe(this.tickRejection);
|
|
18846
|
+
return () => {
|
|
18847
|
+
this.subscribe.clear();
|
|
18848
|
+
this.clear();
|
|
18849
|
+
unsubscribe();
|
|
18850
|
+
};
|
|
18851
|
+
});
|
|
18852
|
+
/**
|
|
18853
|
+
* Unsubscribes from risk rejection emitter to stop receiving events.
|
|
18854
|
+
* Calls the unsubscribe function returned by subscribe().
|
|
18855
|
+
* If not subscribed, does nothing.
|
|
18856
|
+
*
|
|
18857
|
+
* @example
|
|
18858
|
+
* ```typescript
|
|
18859
|
+
* const service = new RiskMarkdownService();
|
|
18860
|
+
* service.subscribe();
|
|
18861
|
+
* // ... later
|
|
18862
|
+
* service.unsubscribe();
|
|
18863
|
+
* ```
|
|
18864
|
+
*/
|
|
18865
|
+
this.unsubscribe = async () => {
|
|
18866
|
+
this.loggerService.log("riskMarkdownService unsubscribe");
|
|
18867
|
+
if (this.subscribe.hasValue()) {
|
|
18868
|
+
const lastSubscription = this.subscribe();
|
|
18869
|
+
lastSubscription();
|
|
18870
|
+
}
|
|
18871
|
+
};
|
|
17588
18872
|
/**
|
|
17589
18873
|
* Processes risk rejection events and accumulates them.
|
|
17590
18874
|
* Should be called from riskSubject subscription.
|
|
@@ -17630,6 +18914,9 @@ class RiskMarkdownService {
|
|
|
17630
18914
|
frameName,
|
|
17631
18915
|
backtest,
|
|
17632
18916
|
});
|
|
18917
|
+
if (!this.subscribe.hasValue()) {
|
|
18918
|
+
throw new Error("RiskMarkdownService not initialized. Call subscribe() before getting data.");
|
|
18919
|
+
}
|
|
17633
18920
|
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
17634
18921
|
return storage.getData();
|
|
17635
18922
|
};
|
|
@@ -17660,201 +18947,1219 @@ class RiskMarkdownService {
|
|
|
17660
18947
|
frameName,
|
|
17661
18948
|
backtest,
|
|
17662
18949
|
});
|
|
17663
|
-
|
|
17664
|
-
|
|
18950
|
+
if (!this.subscribe.hasValue()) {
|
|
18951
|
+
throw new Error("RiskMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
18952
|
+
}
|
|
18953
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
18954
|
+
return storage.getReport(symbol, strategyName, columns);
|
|
18955
|
+
};
|
|
18956
|
+
/**
|
|
18957
|
+
* Saves symbol-strategy report to disk.
|
|
18958
|
+
* Creates directory if it doesn't exist.
|
|
18959
|
+
* Delegates to ReportStorage.dump().
|
|
18960
|
+
*
|
|
18961
|
+
* @param symbol - Trading pair symbol to save report for
|
|
18962
|
+
* @param strategyName - Strategy name to save report for
|
|
18963
|
+
* @param exchangeName - Exchange name
|
|
18964
|
+
* @param frameName - Frame name
|
|
18965
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
18966
|
+
* @param path - Directory path to save report (default: "./dump/risk")
|
|
18967
|
+
* @param columns - Column configuration for formatting the table
|
|
18968
|
+
*
|
|
18969
|
+
* @example
|
|
18970
|
+
* ```typescript
|
|
18971
|
+
* const service = new RiskMarkdownService();
|
|
18972
|
+
*
|
|
18973
|
+
* // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
|
|
18974
|
+
* await service.dump("BTCUSDT", "my-strategy", "binance", "1h", false);
|
|
18975
|
+
*
|
|
18976
|
+
* // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
|
|
18977
|
+
* await service.dump("BTCUSDT", "my-strategy", "binance", "1h", false, "./custom/path");
|
|
18978
|
+
* ```
|
|
18979
|
+
*/
|
|
18980
|
+
this.dump = async (symbol, strategyName, exchangeName, frameName, backtest, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) => {
|
|
18981
|
+
this.loggerService.log("riskMarkdownService dump", {
|
|
18982
|
+
symbol,
|
|
18983
|
+
strategyName,
|
|
18984
|
+
exchangeName,
|
|
18985
|
+
frameName,
|
|
18986
|
+
backtest,
|
|
18987
|
+
path,
|
|
18988
|
+
});
|
|
18989
|
+
if (!this.subscribe.hasValue()) {
|
|
18990
|
+
throw new Error("RiskMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
18991
|
+
}
|
|
18992
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
18993
|
+
await storage.dump(symbol, strategyName, path, columns);
|
|
18994
|
+
};
|
|
18995
|
+
/**
|
|
18996
|
+
* Clears accumulated event data from storage.
|
|
18997
|
+
* If payload is provided, clears only that specific symbol-strategy-exchange-frame-backtest combination's data.
|
|
18998
|
+
* If nothing is provided, clears all data.
|
|
18999
|
+
*
|
|
19000
|
+
* @param payload - Optional payload with symbol, strategyName, exchangeName, frameName, backtest
|
|
19001
|
+
*
|
|
19002
|
+
* @example
|
|
19003
|
+
* ```typescript
|
|
19004
|
+
* const service = new RiskMarkdownService();
|
|
19005
|
+
*
|
|
19006
|
+
* // Clear specific combination
|
|
19007
|
+
* await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy", exchangeName: "binance", frameName: "1h", backtest: false });
|
|
19008
|
+
*
|
|
19009
|
+
* // Clear all data
|
|
19010
|
+
* await service.clear();
|
|
19011
|
+
* ```
|
|
19012
|
+
*/
|
|
19013
|
+
this.clear = async (payload) => {
|
|
19014
|
+
this.loggerService.log("riskMarkdownService clear", {
|
|
19015
|
+
payload,
|
|
19016
|
+
});
|
|
19017
|
+
if (payload) {
|
|
19018
|
+
const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
19019
|
+
this.getStorage.clear(key);
|
|
19020
|
+
}
|
|
19021
|
+
else {
|
|
19022
|
+
this.getStorage.clear();
|
|
19023
|
+
}
|
|
19024
|
+
};
|
|
19025
|
+
}
|
|
19026
|
+
}
|
|
19027
|
+
|
|
19028
|
+
/**
|
|
19029
|
+
* Service for validating column configurations to ensure consistency with ColumnModel interface
|
|
19030
|
+
* and prevent invalid column definitions.
|
|
19031
|
+
*
|
|
19032
|
+
* Performs comprehensive validation on all column definitions in COLUMN_CONFIG:
|
|
19033
|
+
* - **Required fields**: All columns must have key, label, format, and isVisible properties
|
|
19034
|
+
* - **Unique keys**: All key values must be unique within each column collection
|
|
19035
|
+
* - **Function validation**: format and isVisible must be callable functions
|
|
19036
|
+
* - **Data types**: key and label must be non-empty strings
|
|
19037
|
+
*
|
|
19038
|
+
* @throws {Error} If any validation fails, throws with detailed breakdown of all errors
|
|
19039
|
+
*
|
|
19040
|
+
* @example
|
|
19041
|
+
* ```typescript
|
|
19042
|
+
* const validator = new ColumnValidationService();
|
|
19043
|
+
* validator.validate(); // Throws if column configuration is invalid
|
|
19044
|
+
* ```
|
|
19045
|
+
*
|
|
19046
|
+
* @example Validation failure output:
|
|
19047
|
+
* ```
|
|
19048
|
+
* Column configuration validation failed:
|
|
19049
|
+
* 1. backtest_columns[0]: Missing required field "format"
|
|
19050
|
+
* 2. heat_columns: Duplicate key "symbol" at indexes 1, 5
|
|
19051
|
+
* 3. live_columns[3].isVisible must be a function, got "boolean"
|
|
19052
|
+
* ```
|
|
19053
|
+
*/
|
|
19054
|
+
class ColumnValidationService {
|
|
19055
|
+
constructor() {
|
|
19056
|
+
/**
|
|
19057
|
+
* @private
|
|
19058
|
+
* @readonly
|
|
19059
|
+
* Injected logger service instance
|
|
19060
|
+
*/
|
|
19061
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19062
|
+
/**
|
|
19063
|
+
* Validates all column configurations in COLUMN_CONFIG for structural correctness.
|
|
19064
|
+
*
|
|
19065
|
+
* Checks:
|
|
19066
|
+
* 1. All required fields (key, label, format, isVisible) are present in each column
|
|
19067
|
+
* 2. key and label are non-empty strings
|
|
19068
|
+
* 3. format and isVisible are functions (not other types)
|
|
19069
|
+
* 4. All keys are unique within each column collection
|
|
19070
|
+
*
|
|
19071
|
+
* @throws Error if configuration is invalid
|
|
19072
|
+
*/
|
|
19073
|
+
this.validate = () => {
|
|
19074
|
+
this.loggerService.log("columnValidationService validate");
|
|
19075
|
+
const errors = [];
|
|
19076
|
+
// Iterate through all column collections in COLUMN_CONFIG
|
|
19077
|
+
for (const [configKey, columns] of Object.entries(COLUMN_CONFIG)) {
|
|
19078
|
+
if (!Array.isArray(columns)) {
|
|
19079
|
+
errors.push(`${configKey} is not an array, got ${typeof columns}`);
|
|
19080
|
+
continue;
|
|
19081
|
+
}
|
|
19082
|
+
// Track keys for uniqueness check
|
|
19083
|
+
const keyMap = new Map();
|
|
19084
|
+
// Validate each column in the collection
|
|
19085
|
+
columns.forEach((column, index) => {
|
|
19086
|
+
if (!column || typeof column !== "object") {
|
|
19087
|
+
errors.push(`${configKey}[${index}]: Column must be an object, got ${typeof column}`);
|
|
19088
|
+
return;
|
|
19089
|
+
}
|
|
19090
|
+
// Check for all required fields
|
|
19091
|
+
const requiredFields = ["key", "label", "format", "isVisible"];
|
|
19092
|
+
for (const field of requiredFields) {
|
|
19093
|
+
if (!(field in column)) {
|
|
19094
|
+
errors.push(`${configKey}[${index}]: Missing required field "${field}"`);
|
|
19095
|
+
}
|
|
19096
|
+
}
|
|
19097
|
+
// Validate key and label are non-empty strings
|
|
19098
|
+
if (typeof column.key !== "string" || column.key.trim() === "") {
|
|
19099
|
+
errors.push(`${configKey}[${index}].key must be a non-empty string, got ${typeof column.key === "string" ? `"${column.key}"` : typeof column.key}`);
|
|
19100
|
+
}
|
|
19101
|
+
else {
|
|
19102
|
+
// Track key for uniqueness check
|
|
19103
|
+
if (!keyMap.has(column.key)) {
|
|
19104
|
+
keyMap.set(column.key, []);
|
|
19105
|
+
}
|
|
19106
|
+
keyMap.get(column.key).push(index);
|
|
19107
|
+
}
|
|
19108
|
+
if (typeof column.label !== "string" || column.label.trim() === "") {
|
|
19109
|
+
errors.push(`${configKey}[${index}].label must be a non-empty string, got ${typeof column.label === "string" ? `"${column.label}"` : typeof column.label}`);
|
|
19110
|
+
}
|
|
19111
|
+
// Validate format is a function
|
|
19112
|
+
if (typeof column.format !== "function") {
|
|
19113
|
+
errors.push(`${configKey}[${index}].format must be a function, got "${typeof column.format}"`);
|
|
19114
|
+
}
|
|
19115
|
+
// Validate isVisible is a function
|
|
19116
|
+
if (typeof column.isVisible !== "function") {
|
|
19117
|
+
errors.push(`${configKey}[${index}].isVisible must be a function, got "${typeof column.isVisible}"`);
|
|
19118
|
+
}
|
|
19119
|
+
});
|
|
19120
|
+
// Check for duplicate keys
|
|
19121
|
+
for (const [key, indexes] of keyMap.entries()) {
|
|
19122
|
+
if (indexes.length > 1) {
|
|
19123
|
+
errors.push(`${configKey}: Duplicate key "${key}" at indexes ${indexes.join(", ")}`);
|
|
19124
|
+
}
|
|
19125
|
+
}
|
|
19126
|
+
}
|
|
19127
|
+
// Throw aggregated errors if any
|
|
19128
|
+
if (errors.length > 0) {
|
|
19129
|
+
const errorMessage = `Column configuration validation failed:\n${errors
|
|
19130
|
+
.map((e, i) => ` ${i + 1}. ${e}`)
|
|
19131
|
+
.join("\n")}`;
|
|
19132
|
+
this.loggerService.warn(errorMessage);
|
|
19133
|
+
throw new Error(errorMessage);
|
|
19134
|
+
}
|
|
19135
|
+
this.loggerService.log("columnValidationService validation passed");
|
|
19136
|
+
};
|
|
19137
|
+
}
|
|
19138
|
+
}
|
|
19139
|
+
|
|
19140
|
+
var _a, _b, _c;
|
|
19141
|
+
const REPORT_BASE_METHOD_NAME_CTOR = "ReportBase.CTOR";
|
|
19142
|
+
const REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT = "ReportBase.waitForInit";
|
|
19143
|
+
const REPORT_BASE_METHOD_NAME_WRITE = "ReportBase.write";
|
|
19144
|
+
const REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER = "ReportUtils.useReportAdapter";
|
|
19145
|
+
const REPORT_UTILS_METHOD_NAME_WRITE_DATA = "ReportUtils.writeReportData";
|
|
19146
|
+
const REPORT_UTILS_METHOD_NAME_ENABLE = "ReportUtils.enable";
|
|
19147
|
+
const REPORT_UTILS_METHOD_NAME_USE_DUMMY = "ReportUtils.useDummy";
|
|
19148
|
+
const REPORT_UTILS_METHOD_NAME_USE_JSONL = "ReportUtils.useJsonl";
|
|
19149
|
+
const WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
19150
|
+
const WRITE_SAFE_SYMBOL = Symbol("write-safe");
|
|
19151
|
+
/**
|
|
19152
|
+
* JSONL-based report adapter with append-only writes.
|
|
19153
|
+
*
|
|
19154
|
+
* Features:
|
|
19155
|
+
* - Writes events as JSONL entries to a single file per report type
|
|
19156
|
+
* - Stream-based writes with backpressure handling
|
|
19157
|
+
* - 15-second timeout protection for write operations
|
|
19158
|
+
* - Automatic directory creation
|
|
19159
|
+
* - Error handling via exitEmitter
|
|
19160
|
+
* - Search metadata for filtering (symbol, strategy, exchange, frame, signalId, walkerName)
|
|
19161
|
+
*
|
|
19162
|
+
* File format: ./dump/report/{reportName}.jsonl
|
|
19163
|
+
* Each line contains: reportName, data, metadata, timestamp
|
|
19164
|
+
*
|
|
19165
|
+
* Use this adapter for event logging and post-processing analytics.
|
|
19166
|
+
*/
|
|
19167
|
+
const ReportBase = functoolsKit.makeExtendable((_c = class {
|
|
19168
|
+
/**
|
|
19169
|
+
* Creates a new JSONL report adapter instance.
|
|
19170
|
+
*
|
|
19171
|
+
* @param reportName - Type of report (backtest, live, walker, etc.)
|
|
19172
|
+
* @param baseDir - Base directory for report files, defaults to ./dump/report
|
|
19173
|
+
*/
|
|
19174
|
+
constructor(reportName, baseDir = path.join(process.cwd(), "./dump/report")) {
|
|
19175
|
+
this.reportName = reportName;
|
|
19176
|
+
this.baseDir = baseDir;
|
|
19177
|
+
/** WriteStream instance for append-only writes, null until initialized */
|
|
19178
|
+
this._stream = null;
|
|
19179
|
+
/**
|
|
19180
|
+
* Singleshot initialization function that creates directory and stream.
|
|
19181
|
+
* Protected by singleshot to ensure one-time execution.
|
|
19182
|
+
* Sets up error handler that emits to exitEmitter.
|
|
19183
|
+
*/
|
|
19184
|
+
this[_a] = functoolsKit.singleshot(async () => {
|
|
19185
|
+
await fs__namespace.mkdir(this.baseDir, { recursive: true });
|
|
19186
|
+
this._stream = fs$1.createWriteStream(this._filePath, { flags: "a" });
|
|
19187
|
+
this._stream.on('error', (err) => {
|
|
19188
|
+
exitEmitter.next(new Error(`ReportBase stream error for reportName=${this.reportName} message=${functoolsKit.getErrorMessage(err)}`));
|
|
19189
|
+
});
|
|
19190
|
+
});
|
|
19191
|
+
/**
|
|
19192
|
+
* Timeout-protected write function with backpressure handling.
|
|
19193
|
+
* Waits for drain event if write buffer is full.
|
|
19194
|
+
* Times out after 15 seconds and returns TIMEOUT_SYMBOL.
|
|
19195
|
+
*/
|
|
19196
|
+
this[_b] = functoolsKit.timeout(async (line) => {
|
|
19197
|
+
if (!this._stream.write(line)) {
|
|
19198
|
+
await new Promise((resolve) => {
|
|
19199
|
+
this._stream.once('drain', resolve);
|
|
19200
|
+
});
|
|
19201
|
+
}
|
|
19202
|
+
}, 15000);
|
|
19203
|
+
bt.loggerService.debug(REPORT_BASE_METHOD_NAME_CTOR, {
|
|
19204
|
+
reportName: this.reportName,
|
|
19205
|
+
baseDir,
|
|
19206
|
+
});
|
|
19207
|
+
this._filePath = path.join(this.baseDir, `${this.reportName}.jsonl`);
|
|
19208
|
+
}
|
|
19209
|
+
/**
|
|
19210
|
+
* Initializes the JSONL file and write stream.
|
|
19211
|
+
* Safe to call multiple times - singleshot ensures one-time execution.
|
|
19212
|
+
*
|
|
19213
|
+
* @param initial - Whether this is the first initialization (informational only)
|
|
19214
|
+
* @returns Promise that resolves when initialization is complete
|
|
19215
|
+
*/
|
|
19216
|
+
async waitForInit(initial) {
|
|
19217
|
+
bt.loggerService.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
|
|
19218
|
+
reportName: this.reportName,
|
|
19219
|
+
initial,
|
|
19220
|
+
});
|
|
19221
|
+
await this[WAIT_FOR_INIT_SYMBOL]();
|
|
19222
|
+
}
|
|
19223
|
+
/**
|
|
19224
|
+
* Writes event data to JSONL file with metadata.
|
|
19225
|
+
* Appends a single line with JSON object containing:
|
|
19226
|
+
* - reportName: Type of report
|
|
19227
|
+
* - data: Event data object
|
|
19228
|
+
* - Search flags: symbol, strategyName, exchangeName, frameName, signalId, walkerName
|
|
19229
|
+
* - timestamp: Current timestamp in milliseconds
|
|
19230
|
+
*
|
|
19231
|
+
* @param data - Event data object to write
|
|
19232
|
+
* @param options - Metadata options for filtering and search
|
|
19233
|
+
* @throws Error if stream not initialized or write timeout exceeded
|
|
19234
|
+
*/
|
|
19235
|
+
async write(data, options) {
|
|
19236
|
+
bt.loggerService.debug(REPORT_BASE_METHOD_NAME_WRITE, {
|
|
19237
|
+
reportName: this.reportName,
|
|
19238
|
+
options,
|
|
19239
|
+
});
|
|
19240
|
+
if (!this._stream) {
|
|
19241
|
+
throw new Error(`Stream not initialized for report ${this.reportName}. Call waitForInit() first.`);
|
|
19242
|
+
}
|
|
19243
|
+
const searchFlags = {};
|
|
19244
|
+
if (options.symbol) {
|
|
19245
|
+
searchFlags.symbol = options.symbol;
|
|
19246
|
+
}
|
|
19247
|
+
if (options.strategyName) {
|
|
19248
|
+
searchFlags.strategyName = options.strategyName;
|
|
19249
|
+
}
|
|
19250
|
+
if (options.exchangeName) {
|
|
19251
|
+
searchFlags.exchangeName = options.exchangeName;
|
|
19252
|
+
}
|
|
19253
|
+
if (options.frameName) {
|
|
19254
|
+
searchFlags.frameName = options.frameName;
|
|
19255
|
+
}
|
|
19256
|
+
if (options.signalId) {
|
|
19257
|
+
searchFlags.signalId = options.signalId;
|
|
19258
|
+
}
|
|
19259
|
+
if (options.walkerName) {
|
|
19260
|
+
searchFlags.walkerName = options.walkerName;
|
|
19261
|
+
}
|
|
19262
|
+
const line = JSON.stringify({
|
|
19263
|
+
reportName: this.reportName,
|
|
19264
|
+
data,
|
|
19265
|
+
...searchFlags,
|
|
19266
|
+
timestamp: Date.now(),
|
|
19267
|
+
}) + "\n";
|
|
19268
|
+
const status = await this[WRITE_SAFE_SYMBOL](line);
|
|
19269
|
+
if (status === functoolsKit.TIMEOUT_SYMBOL) {
|
|
19270
|
+
throw new Error(`Timeout writing to report ${this.reportName}`);
|
|
19271
|
+
}
|
|
19272
|
+
}
|
|
19273
|
+
},
|
|
19274
|
+
_a = WAIT_FOR_INIT_SYMBOL,
|
|
19275
|
+
_b = WRITE_SAFE_SYMBOL,
|
|
19276
|
+
_c));
|
|
19277
|
+
/**
|
|
19278
|
+
* Dummy report adapter that discards all writes.
|
|
19279
|
+
* Used for disabling report logging.
|
|
19280
|
+
*/
|
|
19281
|
+
class ReportDummy {
|
|
19282
|
+
/**
|
|
19283
|
+
* No-op initialization function.
|
|
19284
|
+
* @returns Promise that resolves immediately
|
|
19285
|
+
*/
|
|
19286
|
+
async waitForInit() {
|
|
19287
|
+
}
|
|
19288
|
+
/**
|
|
19289
|
+
* No-op write function.
|
|
19290
|
+
* @returns Promise that resolves immediately
|
|
19291
|
+
*/
|
|
19292
|
+
async write() {
|
|
19293
|
+
}
|
|
19294
|
+
}
|
|
19295
|
+
/**
|
|
19296
|
+
* Default configuration that enables all report services.
|
|
19297
|
+
* Used when no specific configuration is provided to enable().
|
|
19298
|
+
*/
|
|
19299
|
+
const WILDCARD_TARGET = {
|
|
19300
|
+
backtest: true,
|
|
19301
|
+
breakeven: true,
|
|
19302
|
+
heat: true,
|
|
19303
|
+
live: true,
|
|
19304
|
+
partial: true,
|
|
19305
|
+
performance: true,
|
|
19306
|
+
risk: true,
|
|
19307
|
+
schedule: true,
|
|
19308
|
+
walker: true,
|
|
19309
|
+
};
|
|
19310
|
+
/**
|
|
19311
|
+
* Utility class for managing report services.
|
|
19312
|
+
*
|
|
19313
|
+
* Provides methods to enable/disable JSONL event logging across
|
|
19314
|
+
* different service types (backtest, live, walker, performance, etc.).
|
|
19315
|
+
*
|
|
19316
|
+
* Typically extended by ReportAdapter for additional functionality.
|
|
19317
|
+
*/
|
|
19318
|
+
class ReportUtils {
|
|
19319
|
+
constructor() {
|
|
19320
|
+
/**
|
|
19321
|
+
* Enables report services selectively.
|
|
19322
|
+
*
|
|
19323
|
+
* Subscribes to specified report services and returns a cleanup function
|
|
19324
|
+
* that unsubscribes from all enabled services at once.
|
|
19325
|
+
*
|
|
19326
|
+
* Each enabled service will:
|
|
19327
|
+
* - Start listening to relevant events
|
|
19328
|
+
* - Write events to JSONL files in real-time
|
|
19329
|
+
* - Include metadata for filtering and analytics
|
|
19330
|
+
*
|
|
19331
|
+
* IMPORTANT: Always call the returned unsubscribe function to prevent memory leaks.
|
|
19332
|
+
*
|
|
19333
|
+
* @param config - Service configuration object. Defaults to enabling all services.
|
|
19334
|
+
* @param config.backtest - Enable backtest closed signal logging
|
|
19335
|
+
* @param config.breakeven - Enable breakeven event logging
|
|
19336
|
+
* @param config.partial - Enable partial close event logging
|
|
19337
|
+
* @param config.heat - Enable heatmap data logging
|
|
19338
|
+
* @param config.walker - Enable walker iteration logging
|
|
19339
|
+
* @param config.performance - Enable performance metrics logging
|
|
19340
|
+
* @param config.risk - Enable risk rejection logging
|
|
19341
|
+
* @param config.schedule - Enable scheduled signal logging
|
|
19342
|
+
* @param config.live - Enable live trading event logging
|
|
19343
|
+
*
|
|
19344
|
+
* @returns Cleanup function that unsubscribes from all enabled services
|
|
19345
|
+
*/
|
|
19346
|
+
this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET) => {
|
|
19347
|
+
bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
|
|
19348
|
+
backtest: bt$1,
|
|
19349
|
+
breakeven,
|
|
19350
|
+
heat,
|
|
19351
|
+
live,
|
|
19352
|
+
partial,
|
|
19353
|
+
performance,
|
|
19354
|
+
risk,
|
|
19355
|
+
schedule,
|
|
19356
|
+
walker,
|
|
19357
|
+
});
|
|
19358
|
+
const unList = [];
|
|
19359
|
+
if (bt$1) {
|
|
19360
|
+
unList.push(bt.backtestReportService.subscribe());
|
|
19361
|
+
}
|
|
19362
|
+
if (breakeven) {
|
|
19363
|
+
unList.push(bt.breakevenReportService.subscribe());
|
|
19364
|
+
}
|
|
19365
|
+
if (heat) {
|
|
19366
|
+
unList.push(bt.heatReportService.subscribe());
|
|
19367
|
+
}
|
|
19368
|
+
if (live) {
|
|
19369
|
+
unList.push(bt.liveReportService.subscribe());
|
|
19370
|
+
}
|
|
19371
|
+
if (partial) {
|
|
19372
|
+
unList.push(bt.partialReportService.subscribe());
|
|
19373
|
+
}
|
|
19374
|
+
if (performance) {
|
|
19375
|
+
unList.push(bt.performanceReportService.subscribe());
|
|
19376
|
+
}
|
|
19377
|
+
if (risk) {
|
|
19378
|
+
unList.push(bt.riskReportService.subscribe());
|
|
19379
|
+
}
|
|
19380
|
+
if (schedule) {
|
|
19381
|
+
unList.push(bt.scheduleReportService.subscribe());
|
|
19382
|
+
}
|
|
19383
|
+
if (walker) {
|
|
19384
|
+
unList.push(bt.walkerReportService.subscribe());
|
|
19385
|
+
}
|
|
19386
|
+
return functoolsKit.compose(...unList.map((un) => () => void un()));
|
|
19387
|
+
};
|
|
19388
|
+
}
|
|
19389
|
+
}
|
|
19390
|
+
/**
|
|
19391
|
+
* Report adapter with pluggable storage backend and instance memoization.
|
|
19392
|
+
*
|
|
19393
|
+
* Features:
|
|
19394
|
+
* - Adapter pattern for swappable storage implementations
|
|
19395
|
+
* - Memoized storage instances (one per report type)
|
|
19396
|
+
* - Default adapter: ReportBase (JSONL append)
|
|
19397
|
+
* - Lazy initialization on first write
|
|
19398
|
+
* - Real-time event logging to JSONL files
|
|
19399
|
+
*
|
|
19400
|
+
* Used for structured event logging and analytics pipelines.
|
|
19401
|
+
*/
|
|
19402
|
+
class ReportAdapter extends ReportUtils {
|
|
19403
|
+
constructor() {
|
|
19404
|
+
super(...arguments);
|
|
19405
|
+
/**
|
|
19406
|
+
* Current report storage adapter constructor.
|
|
19407
|
+
* Defaults to ReportBase for JSONL storage.
|
|
19408
|
+
* Can be changed via useReportAdapter().
|
|
19409
|
+
*/
|
|
19410
|
+
this.ReportFactory = ReportBase;
|
|
19411
|
+
/**
|
|
19412
|
+
* Memoized storage instances cache.
|
|
19413
|
+
* Key: reportName (backtest, live, walker, etc.)
|
|
19414
|
+
* Value: TReportBase instance created with current ReportFactory.
|
|
19415
|
+
* Ensures single instance per report type for the lifetime of the application.
|
|
19416
|
+
*/
|
|
19417
|
+
this.getReportStorage = functoolsKit.memoize(([reportName]) => reportName, (reportName) => Reflect.construct(this.ReportFactory, [reportName, "./dump/report"]));
|
|
19418
|
+
/**
|
|
19419
|
+
* Writes report data to storage using the configured adapter.
|
|
19420
|
+
* Automatically initializes storage on first write for each report type.
|
|
19421
|
+
*
|
|
19422
|
+
* @param reportName - Type of report (backtest, live, walker, etc.)
|
|
19423
|
+
* @param data - Event data object to write
|
|
19424
|
+
* @param options - Metadata options for filtering and search
|
|
19425
|
+
* @returns Promise that resolves when write is complete
|
|
19426
|
+
* @throws Error if write fails or storage initialization fails
|
|
19427
|
+
*
|
|
19428
|
+
* @internal - Automatically called by report services, not for direct use
|
|
19429
|
+
*/
|
|
19430
|
+
this.writeData = async (reportName, data, options) => {
|
|
19431
|
+
bt.loggerService.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
|
|
19432
|
+
reportName,
|
|
19433
|
+
options,
|
|
19434
|
+
});
|
|
19435
|
+
const isInitial = !this.getReportStorage.has(reportName);
|
|
19436
|
+
const reportStorage = this.getReportStorage(reportName);
|
|
19437
|
+
await reportStorage.waitForInit(isInitial);
|
|
19438
|
+
await reportStorage.write(data, options);
|
|
19439
|
+
};
|
|
19440
|
+
}
|
|
19441
|
+
/**
|
|
19442
|
+
* Sets the report storage adapter constructor.
|
|
19443
|
+
* All future report instances will use this adapter.
|
|
19444
|
+
*
|
|
19445
|
+
* @param Ctor - Constructor for report storage adapter
|
|
19446
|
+
*/
|
|
19447
|
+
useReportAdapter(Ctor) {
|
|
19448
|
+
bt.loggerService.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
|
|
19449
|
+
this.ReportFactory = Ctor;
|
|
19450
|
+
}
|
|
19451
|
+
/**
|
|
19452
|
+
* Switches to a dummy report adapter that discards all writes.
|
|
19453
|
+
* All future report writes will be no-ops.
|
|
19454
|
+
*/
|
|
19455
|
+
useDummy() {
|
|
19456
|
+
bt.loggerService.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
|
|
19457
|
+
this.useReportAdapter(ReportDummy);
|
|
19458
|
+
}
|
|
19459
|
+
/**
|
|
19460
|
+
* Switches to the default JSONL report adapter.
|
|
19461
|
+
* All future report writes will use JSONL storage.
|
|
19462
|
+
*/
|
|
19463
|
+
useJsonl() {
|
|
19464
|
+
bt.loggerService.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
|
|
19465
|
+
this.useReportAdapter(ReportBase);
|
|
19466
|
+
}
|
|
19467
|
+
}
|
|
19468
|
+
/**
|
|
19469
|
+
* Global singleton instance of ReportAdapter.
|
|
19470
|
+
* Provides JSONL event logging with pluggable storage backends.
|
|
19471
|
+
*/
|
|
19472
|
+
const Report = new ReportAdapter();
|
|
19473
|
+
|
|
19474
|
+
const BACKTEST_REPORT_METHOD_NAME_SUBSCRIBE = "BacktestReportService.subscribe";
|
|
19475
|
+
const BACKTEST_REPORT_METHOD_NAME_UNSUBSCRIBE = "BacktestReportService.unsubscribe";
|
|
19476
|
+
const BACKTEST_REPORT_METHOD_NAME_TICK = "BacktestReportService.tick";
|
|
19477
|
+
class BacktestReportService {
|
|
19478
|
+
constructor() {
|
|
19479
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19480
|
+
this.tick = async (data) => {
|
|
19481
|
+
this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_TICK, { data });
|
|
19482
|
+
const baseEvent = {
|
|
19483
|
+
timestamp: Date.now(),
|
|
19484
|
+
action: data.action,
|
|
19485
|
+
symbol: data.symbol,
|
|
19486
|
+
strategyName: data.strategyName,
|
|
19487
|
+
exchangeName: data.exchangeName,
|
|
19488
|
+
frameName: data.frameName,
|
|
19489
|
+
backtest: true,
|
|
19490
|
+
currentPrice: data.currentPrice,
|
|
19491
|
+
};
|
|
19492
|
+
const searchOptions = {
|
|
19493
|
+
symbol: data.symbol,
|
|
19494
|
+
strategyName: data.strategyName,
|
|
19495
|
+
exchangeName: data.exchangeName,
|
|
19496
|
+
frameName: data.frameName,
|
|
19497
|
+
signalId: data.action === "idle" ? "" : data.signal?.id,
|
|
19498
|
+
walkerName: "",
|
|
19499
|
+
};
|
|
19500
|
+
if (data.action === "idle") {
|
|
19501
|
+
await Report.writeData("backtest", baseEvent, searchOptions);
|
|
19502
|
+
}
|
|
19503
|
+
else if (data.action === "opened") {
|
|
19504
|
+
await Report.writeData("backtest", {
|
|
19505
|
+
...baseEvent,
|
|
19506
|
+
signalId: data.signal?.id,
|
|
19507
|
+
position: data.signal?.position,
|
|
19508
|
+
note: data.signal?.note,
|
|
19509
|
+
priceOpen: data.signal?.priceOpen,
|
|
19510
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19511
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19512
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19513
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19514
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19515
|
+
openTime: data.signal?.pendingAt,
|
|
19516
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19517
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19518
|
+
}, { ...searchOptions, signalId: data.signal?.id });
|
|
19519
|
+
}
|
|
19520
|
+
else if (data.action === "active") {
|
|
19521
|
+
await Report.writeData("backtest", {
|
|
19522
|
+
...baseEvent,
|
|
19523
|
+
signalId: data.signal?.id,
|
|
19524
|
+
position: data.signal?.position,
|
|
19525
|
+
note: data.signal?.note,
|
|
19526
|
+
priceOpen: data.signal?.priceOpen,
|
|
19527
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19528
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19529
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19530
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19531
|
+
_partial: data.signal?._partial,
|
|
19532
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19533
|
+
openTime: data.signal?.pendingAt,
|
|
19534
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19535
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19536
|
+
percentTp: data.percentTp,
|
|
19537
|
+
percentSl: data.percentSl,
|
|
19538
|
+
pnl: data.pnl.pnlPercentage,
|
|
19539
|
+
pnlPriceOpen: data.pnl.priceOpen,
|
|
19540
|
+
pnlPriceClose: data.pnl.priceClose,
|
|
19541
|
+
}, { ...searchOptions, signalId: data.signal?.id });
|
|
19542
|
+
}
|
|
19543
|
+
else if (data.action === "closed") {
|
|
19544
|
+
const durationMs = data.closeTimestamp - data.signal?.pendingAt;
|
|
19545
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
19546
|
+
await Report.writeData("backtest", {
|
|
19547
|
+
...baseEvent,
|
|
19548
|
+
signalId: data.signal?.id,
|
|
19549
|
+
position: data.signal?.position,
|
|
19550
|
+
note: data.signal?.note,
|
|
19551
|
+
priceOpen: data.signal?.priceOpen,
|
|
19552
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19553
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19554
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19555
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19556
|
+
_partial: data.signal?._partial,
|
|
19557
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19558
|
+
openTime: data.signal?.pendingAt,
|
|
19559
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19560
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19561
|
+
pnl: data.pnl.pnlPercentage,
|
|
19562
|
+
pnlPriceOpen: data.pnl.priceOpen,
|
|
19563
|
+
pnlPriceClose: data.pnl.priceClose,
|
|
19564
|
+
closeReason: data.closeReason,
|
|
19565
|
+
closeTime: data.closeTimestamp,
|
|
19566
|
+
duration: durationMin,
|
|
19567
|
+
}, { ...searchOptions, signalId: data.signal?.id });
|
|
19568
|
+
}
|
|
19569
|
+
};
|
|
19570
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
19571
|
+
this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
19572
|
+
const unsubscribe = signalBacktestEmitter.subscribe(this.tick);
|
|
19573
|
+
return () => {
|
|
19574
|
+
this.subscribe.clear();
|
|
19575
|
+
unsubscribe();
|
|
19576
|
+
};
|
|
19577
|
+
});
|
|
19578
|
+
this.unsubscribe = async () => {
|
|
19579
|
+
this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
19580
|
+
if (this.subscribe.hasValue()) {
|
|
19581
|
+
const lastSubscription = this.subscribe();
|
|
19582
|
+
lastSubscription();
|
|
19583
|
+
}
|
|
19584
|
+
};
|
|
19585
|
+
}
|
|
19586
|
+
}
|
|
19587
|
+
|
|
19588
|
+
const LIVE_REPORT_METHOD_NAME_SUBSCRIBE = "LiveReportService.subscribe";
|
|
19589
|
+
const LIVE_REPORT_METHOD_NAME_UNSUBSCRIBE = "LiveReportService.unsubscribe";
|
|
19590
|
+
const LIVE_REPORT_METHOD_NAME_TICK = "LiveReportService.tick";
|
|
19591
|
+
class LiveReportService {
|
|
19592
|
+
constructor() {
|
|
19593
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19594
|
+
this.tick = async (data) => {
|
|
19595
|
+
this.loggerService.log(LIVE_REPORT_METHOD_NAME_TICK, { data });
|
|
19596
|
+
const baseEvent = {
|
|
19597
|
+
timestamp: Date.now(),
|
|
19598
|
+
action: data.action,
|
|
19599
|
+
symbol: data.symbol,
|
|
19600
|
+
strategyName: data.strategyName,
|
|
19601
|
+
exchangeName: data.exchangeName,
|
|
19602
|
+
frameName: data.frameName,
|
|
19603
|
+
backtest: false,
|
|
19604
|
+
currentPrice: data.currentPrice,
|
|
19605
|
+
};
|
|
19606
|
+
const searchOptions = {
|
|
19607
|
+
symbol: data.symbol,
|
|
19608
|
+
strategyName: data.strategyName,
|
|
19609
|
+
exchangeName: data.exchangeName,
|
|
19610
|
+
frameName: data.frameName,
|
|
19611
|
+
signalId: data.action === "idle" ? "" : data.signal?.id,
|
|
19612
|
+
walkerName: "",
|
|
19613
|
+
};
|
|
19614
|
+
if (data.action === "idle") {
|
|
19615
|
+
await Report.writeData("live", baseEvent, searchOptions);
|
|
19616
|
+
}
|
|
19617
|
+
else if (data.action === "opened") {
|
|
19618
|
+
await Report.writeData("live", {
|
|
19619
|
+
...baseEvent,
|
|
19620
|
+
signalId: data.signal?.id,
|
|
19621
|
+
position: data.signal?.position,
|
|
19622
|
+
note: data.signal?.note,
|
|
19623
|
+
priceOpen: data.signal?.priceOpen,
|
|
19624
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19625
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19626
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19627
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19628
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19629
|
+
openTime: data.signal?.pendingAt,
|
|
19630
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19631
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19632
|
+
}, { ...searchOptions, signalId: data.signal?.id });
|
|
19633
|
+
}
|
|
19634
|
+
else if (data.action === "active") {
|
|
19635
|
+
await Report.writeData("live", {
|
|
19636
|
+
...baseEvent,
|
|
19637
|
+
signalId: data.signal?.id,
|
|
19638
|
+
position: data.signal?.position,
|
|
19639
|
+
note: data.signal?.note,
|
|
19640
|
+
priceOpen: data.signal?.priceOpen,
|
|
19641
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19642
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19643
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19644
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19645
|
+
_partial: data.signal?._partial,
|
|
19646
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19647
|
+
openTime: data.signal?.pendingAt,
|
|
19648
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19649
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19650
|
+
percentTp: data.percentTp,
|
|
19651
|
+
percentSl: data.percentSl,
|
|
19652
|
+
pnl: data.pnl.pnlPercentage,
|
|
19653
|
+
pnlPriceOpen: data.pnl.priceOpen,
|
|
19654
|
+
pnlPriceClose: data.pnl.priceClose,
|
|
19655
|
+
}, { ...searchOptions, signalId: data.signal?.id });
|
|
19656
|
+
}
|
|
19657
|
+
else if (data.action === "closed") {
|
|
19658
|
+
const durationMs = data.closeTimestamp - data.signal?.pendingAt;
|
|
19659
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
19660
|
+
await Report.writeData("live", {
|
|
19661
|
+
...baseEvent,
|
|
19662
|
+
signalId: data.signal?.id,
|
|
19663
|
+
position: data.signal?.position,
|
|
19664
|
+
note: data.signal?.note,
|
|
19665
|
+
priceOpen: data.signal?.priceOpen,
|
|
19666
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19667
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19668
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19669
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19670
|
+
_partial: data.signal?._partial,
|
|
19671
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19672
|
+
openTime: data.signal?.pendingAt,
|
|
19673
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19674
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19675
|
+
pnl: data.pnl.pnlPercentage,
|
|
19676
|
+
pnlPriceOpen: data.pnl.priceOpen,
|
|
19677
|
+
pnlPriceClose: data.pnl.priceClose,
|
|
19678
|
+
closeReason: data.closeReason,
|
|
19679
|
+
duration: durationMin,
|
|
19680
|
+
closeTime: data.closeTimestamp,
|
|
19681
|
+
}, { ...searchOptions, signalId: data.signal?.id });
|
|
19682
|
+
}
|
|
19683
|
+
};
|
|
19684
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
19685
|
+
this.loggerService.log(LIVE_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
19686
|
+
const unsubscribe = signalLiveEmitter.subscribe(this.tick);
|
|
19687
|
+
return () => {
|
|
19688
|
+
this.subscribe.clear();
|
|
19689
|
+
unsubscribe();
|
|
19690
|
+
};
|
|
19691
|
+
});
|
|
19692
|
+
this.unsubscribe = async () => {
|
|
19693
|
+
this.loggerService.log(LIVE_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
19694
|
+
if (this.subscribe.hasValue()) {
|
|
19695
|
+
const lastSubscription = this.subscribe();
|
|
19696
|
+
lastSubscription();
|
|
19697
|
+
}
|
|
19698
|
+
};
|
|
19699
|
+
}
|
|
19700
|
+
}
|
|
19701
|
+
|
|
19702
|
+
const SCHEDULE_REPORT_METHOD_NAME_SUBSCRIBE = "ScheduleReportService.subscribe";
|
|
19703
|
+
const SCHEDULE_REPORT_METHOD_NAME_UNSUBSCRIBE = "ScheduleReportService.unsubscribe";
|
|
19704
|
+
const SCHEDULE_REPORT_METHOD_NAME_TICK = "ScheduleReportService.tick";
|
|
19705
|
+
class ScheduleReportService {
|
|
19706
|
+
constructor() {
|
|
19707
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19708
|
+
this.tick = async (data) => {
|
|
19709
|
+
this.loggerService.log(SCHEDULE_REPORT_METHOD_NAME_TICK, { data });
|
|
19710
|
+
const baseEvent = {
|
|
19711
|
+
symbol: data.symbol,
|
|
19712
|
+
strategyName: data.strategyName,
|
|
19713
|
+
exchangeName: data.exchangeName,
|
|
19714
|
+
frameName: data.frameName,
|
|
19715
|
+
backtest: data.backtest,
|
|
19716
|
+
currentPrice: data.currentPrice,
|
|
19717
|
+
};
|
|
19718
|
+
const searchOptions = {
|
|
19719
|
+
symbol: data.symbol,
|
|
19720
|
+
strategyName: data.strategyName,
|
|
19721
|
+
exchangeName: data.exchangeName,
|
|
19722
|
+
frameName: data.frameName,
|
|
19723
|
+
signalId: data.signal?.id,
|
|
19724
|
+
walkerName: "",
|
|
19725
|
+
};
|
|
19726
|
+
if (data.action === "scheduled") {
|
|
19727
|
+
await Report.writeData("schedule", {
|
|
19728
|
+
timestamp: data.signal?.scheduledAt,
|
|
19729
|
+
action: "scheduled",
|
|
19730
|
+
...baseEvent,
|
|
19731
|
+
signalId: data.signal?.id,
|
|
19732
|
+
position: data.signal?.position,
|
|
19733
|
+
note: data.signal?.note,
|
|
19734
|
+
priceOpen: data.signal?.priceOpen,
|
|
19735
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19736
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19737
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19738
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19739
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19740
|
+
pendingAt: data.signal?.pendingAt,
|
|
19741
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19742
|
+
}, searchOptions);
|
|
19743
|
+
}
|
|
19744
|
+
else if (data.action === "opened") {
|
|
19745
|
+
if (data.signal?.scheduledAt !== data.signal?.pendingAt) {
|
|
19746
|
+
const durationMs = data.signal?.pendingAt - data.signal?.scheduledAt;
|
|
19747
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
19748
|
+
await Report.writeData("schedule", {
|
|
19749
|
+
timestamp: data.signal?.pendingAt,
|
|
19750
|
+
action: "opened",
|
|
19751
|
+
...baseEvent,
|
|
19752
|
+
signalId: data.signal?.id,
|
|
19753
|
+
position: data.signal?.position,
|
|
19754
|
+
note: data.signal?.note,
|
|
19755
|
+
priceOpen: data.signal?.priceOpen,
|
|
19756
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19757
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19758
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19759
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19760
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19761
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19762
|
+
pendingAt: data.signal?.pendingAt,
|
|
19763
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19764
|
+
duration: durationMin,
|
|
19765
|
+
}, searchOptions);
|
|
19766
|
+
}
|
|
19767
|
+
}
|
|
19768
|
+
else if (data.action === "cancelled") {
|
|
19769
|
+
const durationMs = data.closeTimestamp - data.signal?.scheduledAt;
|
|
19770
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
19771
|
+
await Report.writeData("schedule", {
|
|
19772
|
+
timestamp: data.closeTimestamp,
|
|
19773
|
+
action: "cancelled",
|
|
19774
|
+
...baseEvent,
|
|
19775
|
+
signalId: data.signal?.id,
|
|
19776
|
+
position: data.signal?.position,
|
|
19777
|
+
note: data.signal?.note,
|
|
19778
|
+
priceOpen: data.signal?.priceOpen,
|
|
19779
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
19780
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
19781
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
19782
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
19783
|
+
totalExecuted: data.signal?.totalExecuted,
|
|
19784
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
19785
|
+
pendingAt: data.signal?.pendingAt,
|
|
19786
|
+
minuteEstimatedTime: data.signal?.minuteEstimatedTime,
|
|
19787
|
+
closeTime: data.closeTimestamp,
|
|
19788
|
+
duration: durationMin,
|
|
19789
|
+
cancelReason: data.reason,
|
|
19790
|
+
cancelId: data.cancelId,
|
|
19791
|
+
}, searchOptions);
|
|
19792
|
+
}
|
|
19793
|
+
};
|
|
19794
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
19795
|
+
this.loggerService.log(SCHEDULE_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
19796
|
+
const unsubscribe = signalEmitter.subscribe(this.tick);
|
|
19797
|
+
return () => {
|
|
19798
|
+
this.subscribe.clear();
|
|
19799
|
+
unsubscribe();
|
|
19800
|
+
};
|
|
19801
|
+
});
|
|
19802
|
+
this.unsubscribe = async () => {
|
|
19803
|
+
this.loggerService.log(SCHEDULE_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
19804
|
+
if (this.subscribe.hasValue()) {
|
|
19805
|
+
const lastSubscription = this.subscribe();
|
|
19806
|
+
lastSubscription();
|
|
19807
|
+
}
|
|
19808
|
+
};
|
|
19809
|
+
}
|
|
19810
|
+
}
|
|
19811
|
+
|
|
19812
|
+
const PERFORMANCE_REPORT_METHOD_NAME_SUBSCRIBE = "PerformanceReportService.subscribe";
|
|
19813
|
+
const PERFORMANCE_REPORT_METHOD_NAME_UNSUBSCRIBE = "PerformanceReportService.unsubscribe";
|
|
19814
|
+
const PERFORMANCE_REPORT_METHOD_NAME_TRACK = "PerformanceReportService.track";
|
|
19815
|
+
class PerformanceReportService {
|
|
19816
|
+
constructor() {
|
|
19817
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19818
|
+
this.track = async (event) => {
|
|
19819
|
+
this.loggerService.log(PERFORMANCE_REPORT_METHOD_NAME_TRACK, { event });
|
|
19820
|
+
await Report.writeData("performance", {
|
|
19821
|
+
timestamp: event.timestamp,
|
|
19822
|
+
metricType: event.metricType,
|
|
19823
|
+
duration: event.duration,
|
|
19824
|
+
symbol: event.symbol,
|
|
19825
|
+
strategyName: event.strategyName,
|
|
19826
|
+
exchangeName: event.exchangeName,
|
|
19827
|
+
frameName: event.frameName,
|
|
19828
|
+
backtest: event.backtest,
|
|
19829
|
+
previousTimestamp: event.previousTimestamp,
|
|
19830
|
+
}, {
|
|
19831
|
+
symbol: event.symbol,
|
|
19832
|
+
strategyName: event.strategyName,
|
|
19833
|
+
exchangeName: event.exchangeName,
|
|
19834
|
+
frameName: event.frameName,
|
|
19835
|
+
signalId: "",
|
|
19836
|
+
walkerName: "",
|
|
19837
|
+
});
|
|
19838
|
+
};
|
|
19839
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
19840
|
+
this.loggerService.log(PERFORMANCE_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
19841
|
+
const unsubscribe = performanceEmitter.subscribe(this.track);
|
|
19842
|
+
return () => {
|
|
19843
|
+
this.subscribe.clear();
|
|
19844
|
+
unsubscribe();
|
|
19845
|
+
};
|
|
19846
|
+
});
|
|
19847
|
+
this.unsubscribe = async () => {
|
|
19848
|
+
this.loggerService.log(PERFORMANCE_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
19849
|
+
if (this.subscribe.hasValue()) {
|
|
19850
|
+
const lastSubscription = this.subscribe();
|
|
19851
|
+
lastSubscription();
|
|
19852
|
+
}
|
|
19853
|
+
};
|
|
19854
|
+
}
|
|
19855
|
+
}
|
|
19856
|
+
|
|
19857
|
+
const WALKER_REPORT_METHOD_NAME_SUBSCRIBE = "WalkerReportService.subscribe";
|
|
19858
|
+
const WALKER_REPORT_METHOD_NAME_UNSUBSCRIBE = "WalkerReportService.unsubscribe";
|
|
19859
|
+
const WALKER_REPORT_METHOD_NAME_TICK = "WalkerReportService.tick";
|
|
19860
|
+
class WalkerReportService {
|
|
19861
|
+
constructor() {
|
|
19862
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19863
|
+
this.tick = async (data) => {
|
|
19864
|
+
this.loggerService.log(WALKER_REPORT_METHOD_NAME_TICK, { data });
|
|
19865
|
+
await Report.writeData("walker", {
|
|
19866
|
+
timestamp: Date.now(),
|
|
19867
|
+
walkerName: data.walkerName,
|
|
19868
|
+
symbol: data.symbol,
|
|
19869
|
+
exchangeName: data.exchangeName,
|
|
19870
|
+
frameName: data.frameName,
|
|
19871
|
+
strategyName: data.strategyName,
|
|
19872
|
+
metric: data.metric,
|
|
19873
|
+
metricValue: data.metricValue,
|
|
19874
|
+
strategiesTested: data.strategiesTested,
|
|
19875
|
+
totalStrategies: data.totalStrategies,
|
|
19876
|
+
bestStrategy: data.bestStrategy,
|
|
19877
|
+
bestMetric: data.bestMetric,
|
|
19878
|
+
totalSignals: data.stats.totalSignals,
|
|
19879
|
+
winCount: data.stats.winCount,
|
|
19880
|
+
lossCount: data.stats.lossCount,
|
|
19881
|
+
winRate: data.stats.winRate,
|
|
19882
|
+
avgPnl: data.stats.avgPnl,
|
|
19883
|
+
totalPnl: data.stats.totalPnl,
|
|
19884
|
+
stdDev: data.stats.stdDev,
|
|
19885
|
+
sharpeRatio: data.stats.sharpeRatio,
|
|
19886
|
+
annualizedSharpeRatio: data.stats.annualizedSharpeRatio,
|
|
19887
|
+
certaintyRatio: data.stats.certaintyRatio,
|
|
19888
|
+
expectedYearlyReturns: data.stats.expectedYearlyReturns,
|
|
19889
|
+
}, {
|
|
19890
|
+
symbol: data.symbol,
|
|
19891
|
+
strategyName: data.strategyName,
|
|
19892
|
+
exchangeName: data.exchangeName,
|
|
19893
|
+
frameName: data.frameName,
|
|
19894
|
+
signalId: "",
|
|
19895
|
+
walkerName: data.walkerName,
|
|
19896
|
+
});
|
|
19897
|
+
};
|
|
19898
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
19899
|
+
this.loggerService.log(WALKER_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
19900
|
+
const unsubscribe = walkerEmitter.subscribe(this.tick);
|
|
19901
|
+
return () => {
|
|
19902
|
+
this.subscribe.clear();
|
|
19903
|
+
unsubscribe();
|
|
19904
|
+
};
|
|
19905
|
+
});
|
|
19906
|
+
this.unsubscribe = async () => {
|
|
19907
|
+
this.loggerService.log(WALKER_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
19908
|
+
if (this.subscribe.hasValue()) {
|
|
19909
|
+
const lastSubscription = this.subscribe();
|
|
19910
|
+
lastSubscription();
|
|
19911
|
+
}
|
|
19912
|
+
};
|
|
19913
|
+
}
|
|
19914
|
+
}
|
|
19915
|
+
|
|
19916
|
+
const HEAT_REPORT_METHOD_NAME_SUBSCRIBE = "HeatReportService.subscribe";
|
|
19917
|
+
const HEAT_REPORT_METHOD_NAME_UNSUBSCRIBE = "HeatReportService.unsubscribe";
|
|
19918
|
+
const HEAT_REPORT_METHOD_NAME_TICK = "HeatReportService.tick";
|
|
19919
|
+
class HeatReportService {
|
|
19920
|
+
constructor() {
|
|
19921
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19922
|
+
this.tick = async (data) => {
|
|
19923
|
+
this.loggerService.log(HEAT_REPORT_METHOD_NAME_TICK, { data });
|
|
19924
|
+
if (data.action !== "closed") {
|
|
19925
|
+
return;
|
|
19926
|
+
}
|
|
19927
|
+
await Report.writeData("heat", {
|
|
19928
|
+
timestamp: Date.now(),
|
|
19929
|
+
action: data.action,
|
|
19930
|
+
symbol: data.symbol,
|
|
19931
|
+
strategyName: data.strategyName,
|
|
19932
|
+
exchangeName: data.exchangeName,
|
|
19933
|
+
frameName: data.frameName,
|
|
19934
|
+
backtest: data.backtest,
|
|
19935
|
+
signalId: data.signal?.id,
|
|
19936
|
+
position: data.signal?.position,
|
|
19937
|
+
pnl: data.pnl.pnlPercentage,
|
|
19938
|
+
closeReason: data.closeReason,
|
|
19939
|
+
openTime: data.signal?.pendingAt,
|
|
19940
|
+
closeTime: data.closeTimestamp,
|
|
19941
|
+
}, {
|
|
19942
|
+
symbol: data.symbol,
|
|
19943
|
+
strategyName: data.strategyName,
|
|
19944
|
+
exchangeName: data.exchangeName,
|
|
19945
|
+
frameName: data.frameName,
|
|
19946
|
+
signalId: data.signal?.id,
|
|
19947
|
+
walkerName: "",
|
|
19948
|
+
});
|
|
19949
|
+
};
|
|
19950
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
19951
|
+
this.loggerService.log(HEAT_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
19952
|
+
const unsubscribe = signalEmitter.subscribe(this.tick);
|
|
19953
|
+
return () => {
|
|
19954
|
+
this.subscribe.clear();
|
|
19955
|
+
unsubscribe();
|
|
19956
|
+
};
|
|
19957
|
+
});
|
|
19958
|
+
this.unsubscribe = async () => {
|
|
19959
|
+
this.loggerService.log(HEAT_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
19960
|
+
if (this.subscribe.hasValue()) {
|
|
19961
|
+
const lastSubscription = this.subscribe();
|
|
19962
|
+
lastSubscription();
|
|
19963
|
+
}
|
|
19964
|
+
};
|
|
19965
|
+
}
|
|
19966
|
+
}
|
|
19967
|
+
|
|
19968
|
+
const PARTIAL_REPORT_METHOD_NAME_SUBSCRIBE = "PartialReportService.subscribe";
|
|
19969
|
+
const PARTIAL_REPORT_METHOD_NAME_UNSUBSCRIBE = "PartialReportService.unsubscribe";
|
|
19970
|
+
const PARTIAL_REPORT_METHOD_NAME_TICK_PROFIT = "PartialReportService.tickProfit";
|
|
19971
|
+
const PARTIAL_REPORT_METHOD_NAME_TICK_LOSS = "PartialReportService.tickLoss";
|
|
19972
|
+
class PartialReportService {
|
|
19973
|
+
constructor() {
|
|
19974
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
19975
|
+
this.tickProfit = async (data) => {
|
|
19976
|
+
this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_TICK_PROFIT, { data });
|
|
19977
|
+
await Report.writeData("partial", {
|
|
19978
|
+
timestamp: data.timestamp,
|
|
19979
|
+
action: "profit",
|
|
19980
|
+
symbol: data.symbol,
|
|
19981
|
+
strategyName: data.data.strategyName,
|
|
19982
|
+
exchangeName: data.exchangeName,
|
|
19983
|
+
frameName: data.frameName,
|
|
19984
|
+
backtest: data.backtest,
|
|
19985
|
+
signalId: data.data.id,
|
|
19986
|
+
position: data.data.position,
|
|
19987
|
+
currentPrice: data.currentPrice,
|
|
19988
|
+
level: data.level,
|
|
19989
|
+
priceOpen: data.data.priceOpen,
|
|
19990
|
+
priceTakeProfit: data.data.priceTakeProfit,
|
|
19991
|
+
priceStopLoss: data.data.priceStopLoss,
|
|
19992
|
+
_partial: data.data._partial,
|
|
19993
|
+
note: data.data.note,
|
|
19994
|
+
pendingAt: data.data.pendingAt,
|
|
19995
|
+
scheduledAt: data.data.scheduledAt,
|
|
19996
|
+
minuteEstimatedTime: data.data.minuteEstimatedTime,
|
|
19997
|
+
}, {
|
|
19998
|
+
symbol: data.symbol,
|
|
19999
|
+
strategyName: data.data.strategyName,
|
|
20000
|
+
exchangeName: data.exchangeName,
|
|
20001
|
+
frameName: data.frameName,
|
|
20002
|
+
signalId: data.data.id,
|
|
20003
|
+
walkerName: "",
|
|
20004
|
+
});
|
|
17665
20005
|
};
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
|
|
17672
|
-
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
frameName,
|
|
17696
|
-
backtest,
|
|
17697
|
-
path,
|
|
20006
|
+
this.tickLoss = async (data) => {
|
|
20007
|
+
this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_TICK_LOSS, { data });
|
|
20008
|
+
await Report.writeData("partial", {
|
|
20009
|
+
timestamp: data.timestamp,
|
|
20010
|
+
action: "loss",
|
|
20011
|
+
symbol: data.symbol,
|
|
20012
|
+
strategyName: data.data.strategyName,
|
|
20013
|
+
exchangeName: data.exchangeName,
|
|
20014
|
+
frameName: data.frameName,
|
|
20015
|
+
backtest: data.backtest,
|
|
20016
|
+
signalId: data.data.id,
|
|
20017
|
+
position: data.data.position,
|
|
20018
|
+
currentPrice: data.currentPrice,
|
|
20019
|
+
level: data.level,
|
|
20020
|
+
priceOpen: data.data.priceOpen,
|
|
20021
|
+
priceTakeProfit: data.data.priceTakeProfit,
|
|
20022
|
+
priceStopLoss: data.data.priceStopLoss,
|
|
20023
|
+
_partial: data.data._partial,
|
|
20024
|
+
note: data.data.note,
|
|
20025
|
+
pendingAt: data.data.pendingAt,
|
|
20026
|
+
scheduledAt: data.data.scheduledAt,
|
|
20027
|
+
minuteEstimatedTime: data.data.minuteEstimatedTime,
|
|
20028
|
+
}, {
|
|
20029
|
+
symbol: data.symbol,
|
|
20030
|
+
strategyName: data.data.strategyName,
|
|
20031
|
+
exchangeName: data.exchangeName,
|
|
20032
|
+
frameName: data.frameName,
|
|
20033
|
+
signalId: data.data.id,
|
|
20034
|
+
walkerName: "",
|
|
17698
20035
|
});
|
|
17699
|
-
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
17700
|
-
await storage.dump(symbol, strategyName, path, columns);
|
|
17701
20036
|
};
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
* await service.clear();
|
|
17718
|
-
* ```
|
|
17719
|
-
*/
|
|
17720
|
-
this.clear = async (payload) => {
|
|
17721
|
-
this.loggerService.log("riskMarkdownService clear", {
|
|
17722
|
-
payload,
|
|
17723
|
-
});
|
|
17724
|
-
if (payload) {
|
|
17725
|
-
const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
17726
|
-
this.getStorage.clear(key);
|
|
17727
|
-
}
|
|
17728
|
-
else {
|
|
17729
|
-
this.getStorage.clear();
|
|
20037
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
20038
|
+
this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
20039
|
+
const unProfit = partialProfitSubject.subscribe(this.tickProfit);
|
|
20040
|
+
const unLoss = partialLossSubject.subscribe(this.tickLoss);
|
|
20041
|
+
return () => {
|
|
20042
|
+
this.subscribe.clear();
|
|
20043
|
+
unProfit();
|
|
20044
|
+
unLoss();
|
|
20045
|
+
};
|
|
20046
|
+
});
|
|
20047
|
+
this.unsubscribe = async () => {
|
|
20048
|
+
this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
20049
|
+
if (this.subscribe.hasValue()) {
|
|
20050
|
+
const lastSubscription = this.subscribe();
|
|
20051
|
+
lastSubscription();
|
|
17730
20052
|
}
|
|
17731
20053
|
};
|
|
17732
|
-
/**
|
|
17733
|
-
* Initializes the service by subscribing to risk rejection events.
|
|
17734
|
-
* Uses singleshot to ensure initialization happens only once.
|
|
17735
|
-
* Automatically called on first use.
|
|
17736
|
-
*
|
|
17737
|
-
* @example
|
|
17738
|
-
* ```typescript
|
|
17739
|
-
* const service = new RiskMarkdownService();
|
|
17740
|
-
* await service.init(); // Subscribe to rejection events
|
|
17741
|
-
* ```
|
|
17742
|
-
*/
|
|
17743
|
-
this.init = functoolsKit.singleshot(async () => {
|
|
17744
|
-
this.loggerService.log("riskMarkdownService init");
|
|
17745
|
-
this.unsubscribe = riskSubject.subscribe(this.tickRejection);
|
|
17746
|
-
});
|
|
17747
20054
|
}
|
|
17748
20055
|
}
|
|
17749
20056
|
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
* Performs comprehensive validation on all column definitions in COLUMN_CONFIG:
|
|
17755
|
-
* - **Required fields**: All columns must have key, label, format, and isVisible properties
|
|
17756
|
-
* - **Unique keys**: All key values must be unique within each column collection
|
|
17757
|
-
* - **Function validation**: format and isVisible must be callable functions
|
|
17758
|
-
* - **Data types**: key and label must be non-empty strings
|
|
17759
|
-
*
|
|
17760
|
-
* @throws {Error} If any validation fails, throws with detailed breakdown of all errors
|
|
17761
|
-
*
|
|
17762
|
-
* @example
|
|
17763
|
-
* ```typescript
|
|
17764
|
-
* const validator = new ColumnValidationService();
|
|
17765
|
-
* validator.validate(); // Throws if column configuration is invalid
|
|
17766
|
-
* ```
|
|
17767
|
-
*
|
|
17768
|
-
* @example Validation failure output:
|
|
17769
|
-
* ```
|
|
17770
|
-
* Column configuration validation failed:
|
|
17771
|
-
* 1. backtest_columns[0]: Missing required field "format"
|
|
17772
|
-
* 2. heat_columns: Duplicate key "symbol" at indexes 1, 5
|
|
17773
|
-
* 3. live_columns[3].isVisible must be a function, got "boolean"
|
|
17774
|
-
* ```
|
|
17775
|
-
*/
|
|
17776
|
-
class ColumnValidationService {
|
|
20057
|
+
const BREAKEVEN_REPORT_METHOD_NAME_SUBSCRIBE = "BreakevenReportService.subscribe";
|
|
20058
|
+
const BREAKEVEN_REPORT_METHOD_NAME_UNSUBSCRIBE = "BreakevenReportService.unsubscribe";
|
|
20059
|
+
const BREAKEVEN_REPORT_METHOD_NAME_TICK = "BreakevenReportService.tickBreakeven";
|
|
20060
|
+
class BreakevenReportService {
|
|
17777
20061
|
constructor() {
|
|
17778
|
-
/**
|
|
17779
|
-
* @private
|
|
17780
|
-
* @readonly
|
|
17781
|
-
* Injected logger service instance
|
|
17782
|
-
*/
|
|
17783
20062
|
this.loggerService = inject(TYPES.loggerService);
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
|
|
17824
|
-
|
|
17825
|
-
|
|
17826
|
-
keyMap.set(column.key, []);
|
|
17827
|
-
}
|
|
17828
|
-
keyMap.get(column.key).push(index);
|
|
17829
|
-
}
|
|
17830
|
-
if (typeof column.label !== "string" || column.label.trim() === "") {
|
|
17831
|
-
errors.push(`${configKey}[${index}].label must be a non-empty string, got ${typeof column.label === "string" ? `"${column.label}"` : typeof column.label}`);
|
|
17832
|
-
}
|
|
17833
|
-
// Validate format is a function
|
|
17834
|
-
if (typeof column.format !== "function") {
|
|
17835
|
-
errors.push(`${configKey}[${index}].format must be a function, got "${typeof column.format}"`);
|
|
17836
|
-
}
|
|
17837
|
-
// Validate isVisible is a function
|
|
17838
|
-
if (typeof column.isVisible !== "function") {
|
|
17839
|
-
errors.push(`${configKey}[${index}].isVisible must be a function, got "${typeof column.isVisible}"`);
|
|
17840
|
-
}
|
|
17841
|
-
});
|
|
17842
|
-
// Check for duplicate keys
|
|
17843
|
-
for (const [key, indexes] of keyMap.entries()) {
|
|
17844
|
-
if (indexes.length > 1) {
|
|
17845
|
-
errors.push(`${configKey}: Duplicate key "${key}" at indexes ${indexes.join(", ")}`);
|
|
17846
|
-
}
|
|
17847
|
-
}
|
|
20063
|
+
this.tickBreakeven = async (data) => {
|
|
20064
|
+
this.loggerService.log(BREAKEVEN_REPORT_METHOD_NAME_TICK, { data });
|
|
20065
|
+
await Report.writeData("breakeven", {
|
|
20066
|
+
timestamp: data.timestamp,
|
|
20067
|
+
symbol: data.symbol,
|
|
20068
|
+
strategyName: data.data.strategyName,
|
|
20069
|
+
exchangeName: data.exchangeName,
|
|
20070
|
+
frameName: data.frameName,
|
|
20071
|
+
backtest: data.backtest,
|
|
20072
|
+
signalId: data.data.id,
|
|
20073
|
+
position: data.data.position,
|
|
20074
|
+
currentPrice: data.currentPrice,
|
|
20075
|
+
priceOpen: data.data.priceOpen,
|
|
20076
|
+
priceTakeProfit: data.data.priceTakeProfit,
|
|
20077
|
+
priceStopLoss: data.data.priceStopLoss,
|
|
20078
|
+
_partial: data.data._partial,
|
|
20079
|
+
note: data.data.note,
|
|
20080
|
+
pendingAt: data.data.pendingAt,
|
|
20081
|
+
scheduledAt: data.data.scheduledAt,
|
|
20082
|
+
minuteEstimatedTime: data.data.minuteEstimatedTime,
|
|
20083
|
+
}, {
|
|
20084
|
+
symbol: data.symbol,
|
|
20085
|
+
strategyName: data.data.strategyName,
|
|
20086
|
+
exchangeName: data.exchangeName,
|
|
20087
|
+
frameName: data.frameName,
|
|
20088
|
+
signalId: data.data.id,
|
|
20089
|
+
walkerName: "",
|
|
20090
|
+
});
|
|
20091
|
+
};
|
|
20092
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
20093
|
+
this.loggerService.log(BREAKEVEN_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
20094
|
+
const unsubscribe = breakevenSubject.subscribe(this.tickBreakeven);
|
|
20095
|
+
return () => {
|
|
20096
|
+
this.subscribe.clear();
|
|
20097
|
+
unsubscribe();
|
|
20098
|
+
};
|
|
20099
|
+
});
|
|
20100
|
+
this.unsubscribe = async () => {
|
|
20101
|
+
this.loggerService.log(BREAKEVEN_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
20102
|
+
if (this.subscribe.hasValue()) {
|
|
20103
|
+
const lastSubscription = this.subscribe();
|
|
20104
|
+
lastSubscription();
|
|
17848
20105
|
}
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
20106
|
+
};
|
|
20107
|
+
}
|
|
20108
|
+
}
|
|
20109
|
+
|
|
20110
|
+
const RISK_REPORT_METHOD_NAME_SUBSCRIBE = "RiskReportService.subscribe";
|
|
20111
|
+
const RISK_REPORT_METHOD_NAME_UNSUBSCRIBE = "RiskReportService.unsubscribe";
|
|
20112
|
+
const RISK_REPORT_METHOD_NAME_TICK = "RiskReportService.tickRejection";
|
|
20113
|
+
class RiskReportService {
|
|
20114
|
+
constructor() {
|
|
20115
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
20116
|
+
this.tickRejection = async (data) => {
|
|
20117
|
+
this.loggerService.log(RISK_REPORT_METHOD_NAME_TICK, { data });
|
|
20118
|
+
await Report.writeData("risk", {
|
|
20119
|
+
timestamp: data.timestamp,
|
|
20120
|
+
symbol: data.symbol,
|
|
20121
|
+
strategyName: data.strategyName,
|
|
20122
|
+
exchangeName: data.exchangeName,
|
|
20123
|
+
frameName: data.frameName,
|
|
20124
|
+
backtest: data.backtest,
|
|
20125
|
+
currentPrice: data.currentPrice,
|
|
20126
|
+
activePositionCount: data.activePositionCount,
|
|
20127
|
+
rejectionId: data.rejectionId,
|
|
20128
|
+
rejectionNote: data.rejectionNote,
|
|
20129
|
+
pendingSignal: data.pendingSignal,
|
|
20130
|
+
signalId: data.pendingSignal?.id,
|
|
20131
|
+
position: data.pendingSignal?.position,
|
|
20132
|
+
priceOpen: data.pendingSignal?.priceOpen,
|
|
20133
|
+
priceTakeProfit: data.pendingSignal?.priceTakeProfit,
|
|
20134
|
+
priceStopLoss: data.pendingSignal?.priceStopLoss,
|
|
20135
|
+
originalPriceTakeProfit: data.pendingSignal?.originalPriceTakeProfit,
|
|
20136
|
+
originalPriceStopLoss: data.pendingSignal?.originalPriceStopLoss,
|
|
20137
|
+
totalExecuted: data.pendingSignal?.totalExecuted,
|
|
20138
|
+
note: data.pendingSignal?.note,
|
|
20139
|
+
minuteEstimatedTime: data.pendingSignal?.minuteEstimatedTime,
|
|
20140
|
+
}, {
|
|
20141
|
+
symbol: data.symbol,
|
|
20142
|
+
strategyName: data.strategyName,
|
|
20143
|
+
exchangeName: data.exchangeName,
|
|
20144
|
+
frameName: data.frameName,
|
|
20145
|
+
signalId: "",
|
|
20146
|
+
walkerName: "",
|
|
20147
|
+
});
|
|
20148
|
+
};
|
|
20149
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
20150
|
+
this.loggerService.log(RISK_REPORT_METHOD_NAME_SUBSCRIBE);
|
|
20151
|
+
const unsubscribe = riskSubject.subscribe(this.tickRejection);
|
|
20152
|
+
return () => {
|
|
20153
|
+
this.subscribe.clear();
|
|
20154
|
+
unsubscribe();
|
|
20155
|
+
};
|
|
20156
|
+
});
|
|
20157
|
+
this.unsubscribe = async () => {
|
|
20158
|
+
this.loggerService.log(RISK_REPORT_METHOD_NAME_UNSUBSCRIBE);
|
|
20159
|
+
if (this.subscribe.hasValue()) {
|
|
20160
|
+
const lastSubscription = this.subscribe();
|
|
20161
|
+
lastSubscription();
|
|
17856
20162
|
}
|
|
17857
|
-
this.loggerService.log("columnValidationService validation passed");
|
|
17858
20163
|
};
|
|
17859
20164
|
}
|
|
17860
20165
|
}
|
|
@@ -17924,6 +20229,17 @@ class ColumnValidationService {
|
|
|
17924
20229
|
provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
|
|
17925
20230
|
provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
|
|
17926
20231
|
}
|
|
20232
|
+
{
|
|
20233
|
+
provide(TYPES.backtestReportService, () => new BacktestReportService());
|
|
20234
|
+
provide(TYPES.liveReportService, () => new LiveReportService());
|
|
20235
|
+
provide(TYPES.scheduleReportService, () => new ScheduleReportService());
|
|
20236
|
+
provide(TYPES.performanceReportService, () => new PerformanceReportService());
|
|
20237
|
+
provide(TYPES.walkerReportService, () => new WalkerReportService());
|
|
20238
|
+
provide(TYPES.heatReportService, () => new HeatReportService());
|
|
20239
|
+
provide(TYPES.partialReportService, () => new PartialReportService());
|
|
20240
|
+
provide(TYPES.breakevenReportService, () => new BreakevenReportService());
|
|
20241
|
+
provide(TYPES.riskReportService, () => new RiskReportService());
|
|
20242
|
+
}
|
|
17927
20243
|
{
|
|
17928
20244
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
17929
20245
|
provide(TYPES.strategyValidationService, () => new StrategyValidationService());
|
|
@@ -18004,6 +20320,17 @@ const markdownServices = {
|
|
|
18004
20320
|
outlineMarkdownService: inject(TYPES.outlineMarkdownService),
|
|
18005
20321
|
riskMarkdownService: inject(TYPES.riskMarkdownService),
|
|
18006
20322
|
};
|
|
20323
|
+
const reportServices = {
|
|
20324
|
+
backtestReportService: inject(TYPES.backtestReportService),
|
|
20325
|
+
liveReportService: inject(TYPES.liveReportService),
|
|
20326
|
+
scheduleReportService: inject(TYPES.scheduleReportService),
|
|
20327
|
+
performanceReportService: inject(TYPES.performanceReportService),
|
|
20328
|
+
walkerReportService: inject(TYPES.walkerReportService),
|
|
20329
|
+
heatReportService: inject(TYPES.heatReportService),
|
|
20330
|
+
partialReportService: inject(TYPES.partialReportService),
|
|
20331
|
+
breakevenReportService: inject(TYPES.breakevenReportService),
|
|
20332
|
+
riskReportService: inject(TYPES.riskReportService),
|
|
20333
|
+
};
|
|
18007
20334
|
const validationServices = {
|
|
18008
20335
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
18009
20336
|
strategyValidationService: inject(TYPES.strategyValidationService),
|
|
@@ -18029,6 +20356,7 @@ const backtest = {
|
|
|
18029
20356
|
...logicPrivateServices,
|
|
18030
20357
|
...logicPublicServices,
|
|
18031
20358
|
...markdownServices,
|
|
20359
|
+
...reportServices,
|
|
18032
20360
|
...validationServices,
|
|
18033
20361
|
...templateServices,
|
|
18034
20362
|
};
|
|
@@ -18496,7 +20824,7 @@ async function cancel(symbol, cancelId) {
|
|
|
18496
20824
|
*
|
|
18497
20825
|
* @param symbol - Trading pair symbol
|
|
18498
20826
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
18499
|
-
* @returns Promise
|
|
20827
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
18500
20828
|
*
|
|
18501
20829
|
* @throws Error if currentPrice is not in profit direction:
|
|
18502
20830
|
* - LONG: currentPrice must be > priceOpen
|
|
@@ -18507,7 +20835,10 @@ async function cancel(symbol, cancelId) {
|
|
|
18507
20835
|
* import { partialProfit } from "backtest-kit";
|
|
18508
20836
|
*
|
|
18509
20837
|
* // Close 30% of LONG position at profit
|
|
18510
|
-
* await partialProfit("BTCUSDT", 30
|
|
20838
|
+
* const success = await partialProfit("BTCUSDT", 30);
|
|
20839
|
+
* if (success) {
|
|
20840
|
+
* console.log('Partial profit executed');
|
|
20841
|
+
* }
|
|
18511
20842
|
* ```
|
|
18512
20843
|
*/
|
|
18513
20844
|
async function partialProfit(symbol, percentToClose) {
|
|
@@ -18524,7 +20855,7 @@ async function partialProfit(symbol, percentToClose) {
|
|
|
18524
20855
|
const currentPrice = await getAveragePrice(symbol);
|
|
18525
20856
|
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
18526
20857
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
18527
|
-
await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
20858
|
+
return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
18528
20859
|
}
|
|
18529
20860
|
/**
|
|
18530
20861
|
* Executes partial close at loss level (moving toward SL).
|
|
@@ -18536,7 +20867,7 @@ async function partialProfit(symbol, percentToClose) {
|
|
|
18536
20867
|
*
|
|
18537
20868
|
* @param symbol - Trading pair symbol
|
|
18538
20869
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
18539
|
-
* @returns Promise
|
|
20870
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
18540
20871
|
*
|
|
18541
20872
|
* @throws Error if currentPrice is not in loss direction:
|
|
18542
20873
|
* - LONG: currentPrice must be < priceOpen
|
|
@@ -18547,7 +20878,10 @@ async function partialProfit(symbol, percentToClose) {
|
|
|
18547
20878
|
* import { partialLoss } from "backtest-kit";
|
|
18548
20879
|
*
|
|
18549
20880
|
* // Close 40% of LONG position at loss
|
|
18550
|
-
* await partialLoss("BTCUSDT", 40
|
|
20881
|
+
* const success = await partialLoss("BTCUSDT", 40);
|
|
20882
|
+
* if (success) {
|
|
20883
|
+
* console.log('Partial loss executed');
|
|
20884
|
+
* }
|
|
18551
20885
|
* ```
|
|
18552
20886
|
*/
|
|
18553
20887
|
async function partialLoss(symbol, percentToClose) {
|
|
@@ -18564,7 +20898,7 @@ async function partialLoss(symbol, percentToClose) {
|
|
|
18564
20898
|
const currentPrice = await getAveragePrice(symbol);
|
|
18565
20899
|
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
18566
20900
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
18567
|
-
await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
20901
|
+
return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
18568
20902
|
}
|
|
18569
20903
|
/**
|
|
18570
20904
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
@@ -21167,7 +23501,7 @@ class BacktestUtils {
|
|
|
21167
23501
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
21168
23502
|
* @param currentPrice - Current market price for this partial close
|
|
21169
23503
|
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
21170
|
-
* @returns Promise
|
|
23504
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
21171
23505
|
*
|
|
21172
23506
|
* @throws Error if currentPrice is not in profit direction:
|
|
21173
23507
|
* - LONG: currentPrice must be > priceOpen
|
|
@@ -21176,11 +23510,14 @@ class BacktestUtils {
|
|
|
21176
23510
|
* @example
|
|
21177
23511
|
* ```typescript
|
|
21178
23512
|
* // Close 30% of LONG position at profit
|
|
21179
|
-
* await Backtest.partialProfit("BTCUSDT", 30, 45000, {
|
|
23513
|
+
* const success = await Backtest.partialProfit("BTCUSDT", 30, 45000, {
|
|
21180
23514
|
* exchangeName: "binance",
|
|
21181
23515
|
* frameName: "frame1",
|
|
21182
23516
|
* strategyName: "my-strategy"
|
|
21183
23517
|
* });
|
|
23518
|
+
* if (success) {
|
|
23519
|
+
* console.log('Partial profit executed');
|
|
23520
|
+
* }
|
|
21184
23521
|
* ```
|
|
21185
23522
|
*/
|
|
21186
23523
|
this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
|
|
@@ -21199,7 +23536,7 @@ class BacktestUtils {
|
|
|
21199
23536
|
riskList &&
|
|
21200
23537
|
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT));
|
|
21201
23538
|
}
|
|
21202
|
-
await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
|
|
23539
|
+
return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
|
|
21203
23540
|
};
|
|
21204
23541
|
/**
|
|
21205
23542
|
* Executes partial close at loss level (moving toward SL).
|
|
@@ -21211,7 +23548,7 @@ class BacktestUtils {
|
|
|
21211
23548
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
21212
23549
|
* @param currentPrice - Current market price for this partial close
|
|
21213
23550
|
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
21214
|
-
* @returns Promise
|
|
23551
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
21215
23552
|
*
|
|
21216
23553
|
* @throws Error if currentPrice is not in loss direction:
|
|
21217
23554
|
* - LONG: currentPrice must be < priceOpen
|
|
@@ -21220,11 +23557,14 @@ class BacktestUtils {
|
|
|
21220
23557
|
* @example
|
|
21221
23558
|
* ```typescript
|
|
21222
23559
|
* // Close 40% of LONG position at loss
|
|
21223
|
-
* await Backtest.partialLoss("BTCUSDT", 40, 38000, {
|
|
23560
|
+
* const success = await Backtest.partialLoss("BTCUSDT", 40, 38000, {
|
|
21224
23561
|
* exchangeName: "binance",
|
|
21225
23562
|
* frameName: "frame1",
|
|
21226
23563
|
* strategyName: "my-strategy"
|
|
21227
23564
|
* });
|
|
23565
|
+
* if (success) {
|
|
23566
|
+
* console.log('Partial loss executed');
|
|
23567
|
+
* }
|
|
21228
23568
|
* ```
|
|
21229
23569
|
*/
|
|
21230
23570
|
this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
|
|
@@ -21243,7 +23583,7 @@ class BacktestUtils {
|
|
|
21243
23583
|
riskList &&
|
|
21244
23584
|
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS));
|
|
21245
23585
|
}
|
|
21246
|
-
await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
|
|
23586
|
+
return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
|
|
21247
23587
|
};
|
|
21248
23588
|
/**
|
|
21249
23589
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
@@ -22086,7 +24426,7 @@ class LiveUtils {
|
|
|
22086
24426
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
22087
24427
|
* @param currentPrice - Current market price for this partial close
|
|
22088
24428
|
* @param context - Execution context with strategyName and exchangeName
|
|
22089
|
-
* @returns Promise
|
|
24429
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
22090
24430
|
*
|
|
22091
24431
|
* @throws Error if currentPrice is not in profit direction:
|
|
22092
24432
|
* - LONG: currentPrice must be > priceOpen
|
|
@@ -22095,10 +24435,13 @@ class LiveUtils {
|
|
|
22095
24435
|
* @example
|
|
22096
24436
|
* ```typescript
|
|
22097
24437
|
* // Close 30% of LONG position at profit
|
|
22098
|
-
* await Live.partialProfit("BTCUSDT", 30, 45000, {
|
|
24438
|
+
* const success = await Live.partialProfit("BTCUSDT", 30, 45000, {
|
|
22099
24439
|
* exchangeName: "binance",
|
|
22100
24440
|
* strategyName: "my-strategy"
|
|
22101
24441
|
* });
|
|
24442
|
+
* if (success) {
|
|
24443
|
+
* console.log('Partial profit executed');
|
|
24444
|
+
* }
|
|
22102
24445
|
* ```
|
|
22103
24446
|
*/
|
|
22104
24447
|
this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
|
|
@@ -22115,7 +24458,7 @@ class LiveUtils {
|
|
|
22115
24458
|
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
|
|
22116
24459
|
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
|
|
22117
24460
|
}
|
|
22118
|
-
await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
|
|
24461
|
+
return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
|
|
22119
24462
|
strategyName: context.strategyName,
|
|
22120
24463
|
exchangeName: context.exchangeName,
|
|
22121
24464
|
frameName: "",
|
|
@@ -22131,7 +24474,7 @@ class LiveUtils {
|
|
|
22131
24474
|
* @param percentToClose - Percentage of position to close (0-100, absolute value)
|
|
22132
24475
|
* @param currentPrice - Current market price for this partial close
|
|
22133
24476
|
* @param context - Execution context with strategyName and exchangeName
|
|
22134
|
-
* @returns Promise
|
|
24477
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped
|
|
22135
24478
|
*
|
|
22136
24479
|
* @throws Error if currentPrice is not in loss direction:
|
|
22137
24480
|
* - LONG: currentPrice must be < priceOpen
|
|
@@ -22140,10 +24483,13 @@ class LiveUtils {
|
|
|
22140
24483
|
* @example
|
|
22141
24484
|
* ```typescript
|
|
22142
24485
|
* // Close 40% of LONG position at loss
|
|
22143
|
-
* await Live.partialLoss("BTCUSDT", 40, 38000, {
|
|
24486
|
+
* const success = await Live.partialLoss("BTCUSDT", 40, 38000, {
|
|
22144
24487
|
* exchangeName: "binance",
|
|
22145
24488
|
* strategyName: "my-strategy"
|
|
22146
24489
|
* });
|
|
24490
|
+
* if (success) {
|
|
24491
|
+
* console.log('Partial loss executed');
|
|
24492
|
+
* }
|
|
22147
24493
|
* ```
|
|
22148
24494
|
*/
|
|
22149
24495
|
this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
|
|
@@ -22160,7 +24506,7 @@ class LiveUtils {
|
|
|
22160
24506
|
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
|
|
22161
24507
|
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
|
|
22162
24508
|
}
|
|
22163
|
-
await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
|
|
24509
|
+
return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
|
|
22164
24510
|
strategyName: context.strategyName,
|
|
22165
24511
|
exchangeName: context.exchangeName,
|
|
22166
24512
|
frameName: "",
|
|
@@ -24027,11 +26373,29 @@ class ExchangeInstance {
|
|
|
24027
26373
|
}
|
|
24028
26374
|
const when = new Date(Date.now());
|
|
24029
26375
|
const since = new Date(when.getTime() - adjust * 60 * 1000);
|
|
24030
|
-
|
|
26376
|
+
let allData = [];
|
|
26377
|
+
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
26378
|
+
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
26379
|
+
let remaining = limit;
|
|
26380
|
+
let currentSince = new Date(since.getTime());
|
|
26381
|
+
while (remaining > 0) {
|
|
26382
|
+
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
26383
|
+
const chunkData = await this._schema.getCandles(symbol, interval, currentSince, chunkLimit);
|
|
26384
|
+
allData.push(...chunkData);
|
|
26385
|
+
remaining -= chunkLimit;
|
|
26386
|
+
if (remaining > 0) {
|
|
26387
|
+
// Move currentSince forward by the number of candles fetched
|
|
26388
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
|
|
26389
|
+
}
|
|
26390
|
+
}
|
|
26391
|
+
}
|
|
26392
|
+
else {
|
|
26393
|
+
allData = await this._schema.getCandles(symbol, interval, since, limit);
|
|
26394
|
+
}
|
|
24031
26395
|
// Filter candles to strictly match the requested range
|
|
24032
26396
|
const whenTimestamp = when.getTime();
|
|
24033
26397
|
const sinceTimestamp = since.getTime();
|
|
24034
|
-
const filteredData =
|
|
26398
|
+
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
|
|
24035
26399
|
if (filteredData.length < limit) {
|
|
24036
26400
|
bt.loggerService.warn(`ExchangeInstance Expected ${limit} candles, got ${filteredData.length}`);
|
|
24037
26401
|
}
|
|
@@ -25164,6 +27528,9 @@ exports.Exchange = Exchange;
|
|
|
25164
27528
|
exports.ExecutionContextService = ExecutionContextService;
|
|
25165
27529
|
exports.Heat = Heat;
|
|
25166
27530
|
exports.Live = Live;
|
|
27531
|
+
exports.Markdown = Markdown;
|
|
27532
|
+
exports.MarkdownFileBase = MarkdownFileBase;
|
|
27533
|
+
exports.MarkdownFolderBase = MarkdownFolderBase;
|
|
25167
27534
|
exports.MethodContextService = MethodContextService;
|
|
25168
27535
|
exports.Notification = Notification;
|
|
25169
27536
|
exports.Optimizer = Optimizer;
|
|
@@ -25176,6 +27543,8 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
|
25176
27543
|
exports.PersistScheduleAdapter = PersistScheduleAdapter;
|
|
25177
27544
|
exports.PersistSignalAdapter = PersistSignalAdapter;
|
|
25178
27545
|
exports.PositionSize = PositionSize;
|
|
27546
|
+
exports.Report = Report;
|
|
27547
|
+
exports.ReportBase = ReportBase;
|
|
25179
27548
|
exports.Risk = Risk;
|
|
25180
27549
|
exports.Schedule = Schedule;
|
|
25181
27550
|
exports.Walker = Walker;
|