backtest-kit 1.5.0 → 1.5.1
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/build/index.cjs +188 -30
- package/build/index.mjs +188 -30
- package/package.json +1 -1
- package/types.d.ts +17 -0
package/build/index.cjs
CHANGED
|
@@ -38,6 +38,15 @@ const GLOBAL_CONFIG = {
|
|
|
38
38
|
* Default: 1440 minutes (1 day)
|
|
39
39
|
*/
|
|
40
40
|
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
41
|
+
/**
|
|
42
|
+
* Maximum time allowed for signal generation (in seconds).
|
|
43
|
+
* Prevents long-running or stuck signal generation routines from blocking
|
|
44
|
+
* execution or consuming resources indefinitely. If generation exceeds this
|
|
45
|
+
* threshold the attempt should be aborted, logged and optionally retried.
|
|
46
|
+
*
|
|
47
|
+
* Default: 180 seconds (3 minutes)
|
|
48
|
+
*/
|
|
49
|
+
CC_MAX_SIGNAL_GENERATION_SECONDS: 180,
|
|
41
50
|
/**
|
|
42
51
|
* Number of retries for getCandles function
|
|
43
52
|
* Default: 3 retries
|
|
@@ -1755,6 +1764,7 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1755
1764
|
"30m": 30,
|
|
1756
1765
|
"1h": 60,
|
|
1757
1766
|
};
|
|
1767
|
+
const TIMEOUT_SYMBOL = Symbol('timeout');
|
|
1758
1768
|
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
1759
1769
|
const errors = [];
|
|
1760
1770
|
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
@@ -1938,7 +1948,14 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
1938
1948
|
}))) {
|
|
1939
1949
|
return null;
|
|
1940
1950
|
}
|
|
1941
|
-
const
|
|
1951
|
+
const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
|
|
1952
|
+
const signal = await Promise.race([
|
|
1953
|
+
self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
|
|
1954
|
+
functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
|
|
1955
|
+
]);
|
|
1956
|
+
if (typeof signal === "symbol") {
|
|
1957
|
+
throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
|
|
1958
|
+
}
|
|
1942
1959
|
if (!signal) {
|
|
1943
1960
|
return null;
|
|
1944
1961
|
}
|
|
@@ -2224,6 +2241,8 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
|
|
|
2224
2241
|
strategyName: self.params.method.context.strategyName,
|
|
2225
2242
|
exchangeName: self.params.method.context.exchangeName,
|
|
2226
2243
|
symbol: self.params.execution.context.symbol,
|
|
2244
|
+
percentTp: 0,
|
|
2245
|
+
percentSl: 0,
|
|
2227
2246
|
};
|
|
2228
2247
|
if (self.params.callbacks?.onTick) {
|
|
2229
2248
|
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
@@ -2350,6 +2369,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
2350
2369
|
return result;
|
|
2351
2370
|
};
|
|
2352
2371
|
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
2372
|
+
let percentTp = 0;
|
|
2373
|
+
let percentSl = 0;
|
|
2353
2374
|
// Calculate percentage of path to TP/SL for partial fill/loss callbacks
|
|
2354
2375
|
{
|
|
2355
2376
|
if (signal.position === "long") {
|
|
@@ -2359,18 +2380,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2359
2380
|
// Moving towards TP
|
|
2360
2381
|
const tpDistance = signal.priceTakeProfit - signal.priceOpen;
|
|
2361
2382
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2362
|
-
|
|
2383
|
+
percentTp = Math.min(progressPercent, 100);
|
|
2384
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2363
2385
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2364
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice,
|
|
2386
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2365
2387
|
}
|
|
2366
2388
|
}
|
|
2367
2389
|
else if (currentDistance < 0) {
|
|
2368
2390
|
// Moving towards SL
|
|
2369
2391
|
const slDistance = signal.priceOpen - signal.priceStopLoss;
|
|
2370
2392
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2371
|
-
|
|
2393
|
+
percentSl = Math.min(progressPercent, 100);
|
|
2394
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2372
2395
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2373
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice,
|
|
2396
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2374
2397
|
}
|
|
2375
2398
|
}
|
|
2376
2399
|
}
|
|
@@ -2381,18 +2404,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2381
2404
|
// Moving towards TP
|
|
2382
2405
|
const tpDistance = signal.priceOpen - signal.priceTakeProfit;
|
|
2383
2406
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2384
|
-
|
|
2407
|
+
percentTp = Math.min(progressPercent, 100);
|
|
2408
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2385
2409
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2386
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice,
|
|
2410
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2387
2411
|
}
|
|
2388
2412
|
}
|
|
2389
2413
|
if (currentDistance < 0) {
|
|
2390
2414
|
// Moving towards SL
|
|
2391
2415
|
const slDistance = signal.priceStopLoss - signal.priceOpen;
|
|
2392
2416
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2393
|
-
|
|
2417
|
+
percentSl = Math.min(progressPercent, 100);
|
|
2418
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2394
2419
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2395
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice,
|
|
2420
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2396
2421
|
}
|
|
2397
2422
|
}
|
|
2398
2423
|
}
|
|
@@ -2404,6 +2429,8 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2404
2429
|
strategyName: self.params.method.context.strategyName,
|
|
2405
2430
|
exchangeName: self.params.method.context.exchangeName,
|
|
2406
2431
|
symbol: self.params.execution.context.symbol,
|
|
2432
|
+
percentTp,
|
|
2433
|
+
percentSl,
|
|
2407
2434
|
};
|
|
2408
2435
|
if (self.params.callbacks?.onTick) {
|
|
2409
2436
|
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
@@ -3000,6 +3027,8 @@ class ClientStrategy {
|
|
|
3000
3027
|
action: "active",
|
|
3001
3028
|
signal: scheduled,
|
|
3002
3029
|
currentPrice: lastPrice,
|
|
3030
|
+
percentSl: 0,
|
|
3031
|
+
percentTp: 0,
|
|
3003
3032
|
strategyName: this.params.method.context.strategyName,
|
|
3004
3033
|
exchangeName: this.params.method.context.exchangeName,
|
|
3005
3034
|
symbol: this.params.execution.context.symbol,
|
|
@@ -5848,14 +5877,33 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
5848
5877
|
async getReport(strategyName) {
|
|
5849
5878
|
const stats = await this.getData();
|
|
5850
5879
|
if (stats.totalSignals === 0) {
|
|
5851
|
-
return
|
|
5880
|
+
return [
|
|
5881
|
+
`# Backtest Report: ${strategyName}`,
|
|
5882
|
+
"",
|
|
5883
|
+
"No signals closed yet."
|
|
5884
|
+
].join("\n");
|
|
5852
5885
|
}
|
|
5853
5886
|
const header = columns$4.map((col) => col.label);
|
|
5854
5887
|
const separator = columns$4.map(() => "---");
|
|
5855
5888
|
const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
|
|
5856
5889
|
const tableData = [header, separator, ...rows];
|
|
5857
|
-
const table =
|
|
5858
|
-
return
|
|
5890
|
+
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
5891
|
+
return [
|
|
5892
|
+
`# Backtest Report: ${strategyName}`,
|
|
5893
|
+
"",
|
|
5894
|
+
table,
|
|
5895
|
+
"",
|
|
5896
|
+
`**Total signals:** ${stats.totalSignals}`,
|
|
5897
|
+
`**Closed signals:** ${stats.totalSignals}`,
|
|
5898
|
+
`**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
|
|
5899
|
+
`**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
|
|
5900
|
+
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
5901
|
+
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
5902
|
+
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
5903
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
5904
|
+
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
5905
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
5906
|
+
].join("\n");
|
|
5859
5907
|
}
|
|
5860
5908
|
/**
|
|
5861
5909
|
* Saves strategy report to disk.
|
|
@@ -6137,6 +6185,16 @@ const columns$3 = [
|
|
|
6137
6185
|
label: "Stop Loss",
|
|
6138
6186
|
format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
|
|
6139
6187
|
},
|
|
6188
|
+
{
|
|
6189
|
+
key: "percentTp",
|
|
6190
|
+
label: "% to TP",
|
|
6191
|
+
format: (data) => data.percentTp !== undefined ? `${data.percentTp.toFixed(2)}%` : "N/A",
|
|
6192
|
+
},
|
|
6193
|
+
{
|
|
6194
|
+
key: "percentSl",
|
|
6195
|
+
label: "% to SL",
|
|
6196
|
+
format: (data) => data.percentSl !== undefined ? `${data.percentSl.toFixed(2)}%` : "N/A",
|
|
6197
|
+
},
|
|
6140
6198
|
{
|
|
6141
6199
|
key: "pnl",
|
|
6142
6200
|
label: "PNL (net)",
|
|
@@ -6239,6 +6297,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6239
6297
|
openPrice: data.signal.priceOpen,
|
|
6240
6298
|
takeProfit: data.signal.priceTakeProfit,
|
|
6241
6299
|
stopLoss: data.signal.priceStopLoss,
|
|
6300
|
+
percentTp: data.percentTp,
|
|
6301
|
+
percentSl: data.percentSl,
|
|
6242
6302
|
};
|
|
6243
6303
|
// Replace existing event or add new one
|
|
6244
6304
|
if (existingIndex !== -1) {
|
|
@@ -6380,14 +6440,33 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6380
6440
|
async getReport(strategyName) {
|
|
6381
6441
|
const stats = await this.getData();
|
|
6382
6442
|
if (stats.totalEvents === 0) {
|
|
6383
|
-
return
|
|
6443
|
+
return [
|
|
6444
|
+
`# Live Trading Report: ${strategyName}`,
|
|
6445
|
+
"",
|
|
6446
|
+
"No events recorded yet."
|
|
6447
|
+
].join("\n");
|
|
6384
6448
|
}
|
|
6385
6449
|
const header = columns$3.map((col) => col.label);
|
|
6386
6450
|
const separator = columns$3.map(() => "---");
|
|
6387
6451
|
const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
|
|
6388
6452
|
const tableData = [header, separator, ...rows];
|
|
6389
|
-
const table =
|
|
6390
|
-
return
|
|
6453
|
+
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
6454
|
+
return [
|
|
6455
|
+
`# Live Trading Report: ${strategyName}`,
|
|
6456
|
+
"",
|
|
6457
|
+
table,
|
|
6458
|
+
"",
|
|
6459
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
6460
|
+
`**Closed signals:** ${stats.totalClosed}`,
|
|
6461
|
+
`**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
|
|
6462
|
+
`**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
|
|
6463
|
+
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
6464
|
+
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
6465
|
+
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
6466
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
6467
|
+
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
6468
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
6469
|
+
].join("\n");
|
|
6391
6470
|
}
|
|
6392
6471
|
/**
|
|
6393
6472
|
* Saves strategy report to disk.
|
|
@@ -6784,14 +6863,28 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
6784
6863
|
async getReport(strategyName) {
|
|
6785
6864
|
const stats = await this.getData();
|
|
6786
6865
|
if (stats.totalEvents === 0) {
|
|
6787
|
-
return
|
|
6866
|
+
return [
|
|
6867
|
+
`# Scheduled Signals Report: ${strategyName}`,
|
|
6868
|
+
"",
|
|
6869
|
+
"No scheduled signals recorded yet."
|
|
6870
|
+
].join("\n");
|
|
6788
6871
|
}
|
|
6789
6872
|
const header = columns$2.map((col) => col.label);
|
|
6790
6873
|
const separator = columns$2.map(() => "---");
|
|
6791
6874
|
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
6792
6875
|
const tableData = [header, separator, ...rows];
|
|
6793
|
-
const table =
|
|
6794
|
-
return
|
|
6876
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
6877
|
+
return [
|
|
6878
|
+
`# Scheduled Signals Report: ${strategyName}`,
|
|
6879
|
+
"",
|
|
6880
|
+
table,
|
|
6881
|
+
"",
|
|
6882
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
6883
|
+
`**Scheduled signals:** ${stats.totalScheduled}`,
|
|
6884
|
+
`**Cancelled signals:** ${stats.totalCancelled}`,
|
|
6885
|
+
`**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
|
|
6886
|
+
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
|
|
6887
|
+
].join("\n");
|
|
6795
6888
|
}
|
|
6796
6889
|
/**
|
|
6797
6890
|
* Saves strategy report to disk.
|
|
@@ -7109,7 +7202,11 @@ class PerformanceStorage {
|
|
|
7109
7202
|
async getReport(strategyName) {
|
|
7110
7203
|
const stats = await this.getData(strategyName);
|
|
7111
7204
|
if (stats.totalEvents === 0) {
|
|
7112
|
-
return
|
|
7205
|
+
return [
|
|
7206
|
+
`# Performance Report: ${strategyName}`,
|
|
7207
|
+
"",
|
|
7208
|
+
"No performance metrics recorded yet."
|
|
7209
|
+
].join("\n");
|
|
7113
7210
|
}
|
|
7114
7211
|
// Sort metrics by total duration (descending) to show bottlenecks first
|
|
7115
7212
|
const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
|
|
@@ -7146,13 +7243,29 @@ class PerformanceStorage {
|
|
|
7146
7243
|
metric.maxWaitTime.toFixed(2),
|
|
7147
7244
|
]);
|
|
7148
7245
|
const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
|
|
7149
|
-
const summaryTable =
|
|
7246
|
+
const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7150
7247
|
// Calculate percentage of total time for each metric
|
|
7151
7248
|
const percentages = sortedMetrics.map((metric) => {
|
|
7152
7249
|
const pct = (metric.totalDuration / stats.totalDuration) * 100;
|
|
7153
7250
|
return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
|
|
7154
7251
|
});
|
|
7155
|
-
return
|
|
7252
|
+
return [
|
|
7253
|
+
`# Performance Report: ${strategyName}`,
|
|
7254
|
+
"",
|
|
7255
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
7256
|
+
`**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
|
|
7257
|
+
`**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
|
|
7258
|
+
"",
|
|
7259
|
+
"## Time Distribution",
|
|
7260
|
+
"",
|
|
7261
|
+
percentages.join("\n"),
|
|
7262
|
+
"",
|
|
7263
|
+
"## Detailed Metrics",
|
|
7264
|
+
"",
|
|
7265
|
+
summaryTable,
|
|
7266
|
+
"",
|
|
7267
|
+
"**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type."
|
|
7268
|
+
].join("\n");
|
|
7156
7269
|
}
|
|
7157
7270
|
/**
|
|
7158
7271
|
* Saves performance report to disk.
|
|
@@ -7556,7 +7669,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7556
7669
|
// Build table rows
|
|
7557
7670
|
const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
|
|
7558
7671
|
const tableData = [header, separator, ...rows];
|
|
7559
|
-
return
|
|
7672
|
+
return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7560
7673
|
}
|
|
7561
7674
|
/**
|
|
7562
7675
|
* Generates PNL table showing all closed signals across all strategies (View).
|
|
@@ -7593,7 +7706,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7593
7706
|
// Build table rows
|
|
7594
7707
|
const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
|
|
7595
7708
|
const tableData = [header, separator, ...rows];
|
|
7596
|
-
return
|
|
7709
|
+
return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7597
7710
|
}
|
|
7598
7711
|
/**
|
|
7599
7712
|
* Generates markdown report with all strategy results (View).
|
|
@@ -7608,7 +7721,30 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7608
7721
|
const results = await this.getData(symbol, metric, context);
|
|
7609
7722
|
// Get total signals for best strategy
|
|
7610
7723
|
const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
|
|
7611
|
-
return
|
|
7724
|
+
return [
|
|
7725
|
+
`# Walker Comparison Report: ${results.walkerName}`,
|
|
7726
|
+
"",
|
|
7727
|
+
`**Symbol:** ${results.symbol}`,
|
|
7728
|
+
`**Exchange:** ${results.exchangeName}`,
|
|
7729
|
+
`**Frame:** ${results.frameName}`,
|
|
7730
|
+
`**Optimization Metric:** ${results.metric}`,
|
|
7731
|
+
`**Strategies Tested:** ${results.totalStrategies}`,
|
|
7732
|
+
"",
|
|
7733
|
+
`## Best Strategy: ${results.bestStrategy}`,
|
|
7734
|
+
"",
|
|
7735
|
+
`**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
|
|
7736
|
+
`**Total Signals:** ${bestStrategySignals}`,
|
|
7737
|
+
"",
|
|
7738
|
+
"## Top Strategies Comparison",
|
|
7739
|
+
"",
|
|
7740
|
+
this.getComparisonTable(metric, 10),
|
|
7741
|
+
"",
|
|
7742
|
+
"## All Signals (PNL Table)",
|
|
7743
|
+
"",
|
|
7744
|
+
this.getPnlTable(),
|
|
7745
|
+
"",
|
|
7746
|
+
"**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
|
|
7747
|
+
].join("\n");
|
|
7612
7748
|
}
|
|
7613
7749
|
/**
|
|
7614
7750
|
* Saves walker report to disk.
|
|
@@ -8122,14 +8258,24 @@ class HeatmapStorage {
|
|
|
8122
8258
|
async getReport(strategyName) {
|
|
8123
8259
|
const data = await this.getData();
|
|
8124
8260
|
if (data.symbols.length === 0) {
|
|
8125
|
-
return
|
|
8261
|
+
return [
|
|
8262
|
+
`# Portfolio Heatmap: ${strategyName}`,
|
|
8263
|
+
"",
|
|
8264
|
+
"*No data available*"
|
|
8265
|
+
].join("\n");
|
|
8126
8266
|
}
|
|
8127
8267
|
const header = columns$1.map((col) => col.label);
|
|
8128
8268
|
const separator = columns$1.map(() => "---");
|
|
8129
8269
|
const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
|
|
8130
8270
|
const tableData = [header, separator, ...rows];
|
|
8131
|
-
const table =
|
|
8132
|
-
return
|
|
8271
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
8272
|
+
return [
|
|
8273
|
+
`# Portfolio Heatmap: ${strategyName}`,
|
|
8274
|
+
"",
|
|
8275
|
+
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
|
|
8276
|
+
"",
|
|
8277
|
+
table
|
|
8278
|
+
].join("\n");
|
|
8133
8279
|
}
|
|
8134
8280
|
/**
|
|
8135
8281
|
* Saves heatmap report to disk.
|
|
@@ -10687,14 +10833,26 @@ class ReportStorage {
|
|
|
10687
10833
|
async getReport(symbol, strategyName) {
|
|
10688
10834
|
const stats = await this.getData();
|
|
10689
10835
|
if (stats.totalEvents === 0) {
|
|
10690
|
-
return
|
|
10836
|
+
return [
|
|
10837
|
+
`# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
|
|
10838
|
+
"",
|
|
10839
|
+
"No partial profit/loss events recorded yet."
|
|
10840
|
+
].join("\n");
|
|
10691
10841
|
}
|
|
10692
10842
|
const header = columns.map((col) => col.label);
|
|
10693
10843
|
const separator = columns.map(() => "---");
|
|
10694
10844
|
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
10695
10845
|
const tableData = [header, separator, ...rows];
|
|
10696
|
-
const table =
|
|
10697
|
-
return
|
|
10846
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
10847
|
+
return [
|
|
10848
|
+
`# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
|
|
10849
|
+
"",
|
|
10850
|
+
table,
|
|
10851
|
+
"",
|
|
10852
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
10853
|
+
`**Profit events:** ${stats.totalProfit}`,
|
|
10854
|
+
`**Loss events:** ${stats.totalLoss}`
|
|
10855
|
+
].join("\n");
|
|
10698
10856
|
}
|
|
10699
10857
|
/**
|
|
10700
10858
|
* Saves symbol-strategy report to disk.
|
package/build/index.mjs
CHANGED
|
@@ -36,6 +36,15 @@ const GLOBAL_CONFIG = {
|
|
|
36
36
|
* Default: 1440 minutes (1 day)
|
|
37
37
|
*/
|
|
38
38
|
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
39
|
+
/**
|
|
40
|
+
* Maximum time allowed for signal generation (in seconds).
|
|
41
|
+
* Prevents long-running or stuck signal generation routines from blocking
|
|
42
|
+
* execution or consuming resources indefinitely. If generation exceeds this
|
|
43
|
+
* threshold the attempt should be aborted, logged and optionally retried.
|
|
44
|
+
*
|
|
45
|
+
* Default: 180 seconds (3 minutes)
|
|
46
|
+
*/
|
|
47
|
+
CC_MAX_SIGNAL_GENERATION_SECONDS: 180,
|
|
39
48
|
/**
|
|
40
49
|
* Number of retries for getCandles function
|
|
41
50
|
* Default: 3 retries
|
|
@@ -1753,6 +1762,7 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1753
1762
|
"30m": 30,
|
|
1754
1763
|
"1h": 60,
|
|
1755
1764
|
};
|
|
1765
|
+
const TIMEOUT_SYMBOL = Symbol('timeout');
|
|
1756
1766
|
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
1757
1767
|
const errors = [];
|
|
1758
1768
|
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
@@ -1936,7 +1946,14 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
1936
1946
|
}))) {
|
|
1937
1947
|
return null;
|
|
1938
1948
|
}
|
|
1939
|
-
const
|
|
1949
|
+
const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
|
|
1950
|
+
const signal = await Promise.race([
|
|
1951
|
+
self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
|
|
1952
|
+
sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
|
|
1953
|
+
]);
|
|
1954
|
+
if (typeof signal === "symbol") {
|
|
1955
|
+
throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
|
|
1956
|
+
}
|
|
1940
1957
|
if (!signal) {
|
|
1941
1958
|
return null;
|
|
1942
1959
|
}
|
|
@@ -2222,6 +2239,8 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
|
|
|
2222
2239
|
strategyName: self.params.method.context.strategyName,
|
|
2223
2240
|
exchangeName: self.params.method.context.exchangeName,
|
|
2224
2241
|
symbol: self.params.execution.context.symbol,
|
|
2242
|
+
percentTp: 0,
|
|
2243
|
+
percentSl: 0,
|
|
2225
2244
|
};
|
|
2226
2245
|
if (self.params.callbacks?.onTick) {
|
|
2227
2246
|
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
@@ -2348,6 +2367,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
2348
2367
|
return result;
|
|
2349
2368
|
};
|
|
2350
2369
|
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
2370
|
+
let percentTp = 0;
|
|
2371
|
+
let percentSl = 0;
|
|
2351
2372
|
// Calculate percentage of path to TP/SL for partial fill/loss callbacks
|
|
2352
2373
|
{
|
|
2353
2374
|
if (signal.position === "long") {
|
|
@@ -2357,18 +2378,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2357
2378
|
// Moving towards TP
|
|
2358
2379
|
const tpDistance = signal.priceTakeProfit - signal.priceOpen;
|
|
2359
2380
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2360
|
-
|
|
2381
|
+
percentTp = Math.min(progressPercent, 100);
|
|
2382
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2361
2383
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2362
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice,
|
|
2384
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2363
2385
|
}
|
|
2364
2386
|
}
|
|
2365
2387
|
else if (currentDistance < 0) {
|
|
2366
2388
|
// Moving towards SL
|
|
2367
2389
|
const slDistance = signal.priceOpen - signal.priceStopLoss;
|
|
2368
2390
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2369
|
-
|
|
2391
|
+
percentSl = Math.min(progressPercent, 100);
|
|
2392
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2370
2393
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2371
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice,
|
|
2394
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2372
2395
|
}
|
|
2373
2396
|
}
|
|
2374
2397
|
}
|
|
@@ -2379,18 +2402,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2379
2402
|
// Moving towards TP
|
|
2380
2403
|
const tpDistance = signal.priceOpen - signal.priceTakeProfit;
|
|
2381
2404
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2382
|
-
|
|
2405
|
+
percentTp = Math.min(progressPercent, 100);
|
|
2406
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2383
2407
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2384
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice,
|
|
2408
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2385
2409
|
}
|
|
2386
2410
|
}
|
|
2387
2411
|
if (currentDistance < 0) {
|
|
2388
2412
|
// Moving towards SL
|
|
2389
2413
|
const slDistance = signal.priceStopLoss - signal.priceOpen;
|
|
2390
2414
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2391
|
-
|
|
2415
|
+
percentSl = Math.min(progressPercent, 100);
|
|
2416
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2392
2417
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2393
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice,
|
|
2418
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2394
2419
|
}
|
|
2395
2420
|
}
|
|
2396
2421
|
}
|
|
@@ -2402,6 +2427,8 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2402
2427
|
strategyName: self.params.method.context.strategyName,
|
|
2403
2428
|
exchangeName: self.params.method.context.exchangeName,
|
|
2404
2429
|
symbol: self.params.execution.context.symbol,
|
|
2430
|
+
percentTp,
|
|
2431
|
+
percentSl,
|
|
2405
2432
|
};
|
|
2406
2433
|
if (self.params.callbacks?.onTick) {
|
|
2407
2434
|
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
@@ -2998,6 +3025,8 @@ class ClientStrategy {
|
|
|
2998
3025
|
action: "active",
|
|
2999
3026
|
signal: scheduled,
|
|
3000
3027
|
currentPrice: lastPrice,
|
|
3028
|
+
percentSl: 0,
|
|
3029
|
+
percentTp: 0,
|
|
3001
3030
|
strategyName: this.params.method.context.strategyName,
|
|
3002
3031
|
exchangeName: this.params.method.context.exchangeName,
|
|
3003
3032
|
symbol: this.params.execution.context.symbol,
|
|
@@ -5846,14 +5875,33 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
5846
5875
|
async getReport(strategyName) {
|
|
5847
5876
|
const stats = await this.getData();
|
|
5848
5877
|
if (stats.totalSignals === 0) {
|
|
5849
|
-
return
|
|
5878
|
+
return [
|
|
5879
|
+
`# Backtest Report: ${strategyName}`,
|
|
5880
|
+
"",
|
|
5881
|
+
"No signals closed yet."
|
|
5882
|
+
].join("\n");
|
|
5850
5883
|
}
|
|
5851
5884
|
const header = columns$4.map((col) => col.label);
|
|
5852
5885
|
const separator = columns$4.map(() => "---");
|
|
5853
5886
|
const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
|
|
5854
5887
|
const tableData = [header, separator, ...rows];
|
|
5855
|
-
const table =
|
|
5856
|
-
return
|
|
5888
|
+
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
5889
|
+
return [
|
|
5890
|
+
`# Backtest Report: ${strategyName}`,
|
|
5891
|
+
"",
|
|
5892
|
+
table,
|
|
5893
|
+
"",
|
|
5894
|
+
`**Total signals:** ${stats.totalSignals}`,
|
|
5895
|
+
`**Closed signals:** ${stats.totalSignals}`,
|
|
5896
|
+
`**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
|
|
5897
|
+
`**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
|
|
5898
|
+
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
5899
|
+
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
5900
|
+
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
5901
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
5902
|
+
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
5903
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
5904
|
+
].join("\n");
|
|
5857
5905
|
}
|
|
5858
5906
|
/**
|
|
5859
5907
|
* Saves strategy report to disk.
|
|
@@ -6135,6 +6183,16 @@ const columns$3 = [
|
|
|
6135
6183
|
label: "Stop Loss",
|
|
6136
6184
|
format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
|
|
6137
6185
|
},
|
|
6186
|
+
{
|
|
6187
|
+
key: "percentTp",
|
|
6188
|
+
label: "% to TP",
|
|
6189
|
+
format: (data) => data.percentTp !== undefined ? `${data.percentTp.toFixed(2)}%` : "N/A",
|
|
6190
|
+
},
|
|
6191
|
+
{
|
|
6192
|
+
key: "percentSl",
|
|
6193
|
+
label: "% to SL",
|
|
6194
|
+
format: (data) => data.percentSl !== undefined ? `${data.percentSl.toFixed(2)}%` : "N/A",
|
|
6195
|
+
},
|
|
6138
6196
|
{
|
|
6139
6197
|
key: "pnl",
|
|
6140
6198
|
label: "PNL (net)",
|
|
@@ -6237,6 +6295,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6237
6295
|
openPrice: data.signal.priceOpen,
|
|
6238
6296
|
takeProfit: data.signal.priceTakeProfit,
|
|
6239
6297
|
stopLoss: data.signal.priceStopLoss,
|
|
6298
|
+
percentTp: data.percentTp,
|
|
6299
|
+
percentSl: data.percentSl,
|
|
6240
6300
|
};
|
|
6241
6301
|
// Replace existing event or add new one
|
|
6242
6302
|
if (existingIndex !== -1) {
|
|
@@ -6378,14 +6438,33 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6378
6438
|
async getReport(strategyName) {
|
|
6379
6439
|
const stats = await this.getData();
|
|
6380
6440
|
if (stats.totalEvents === 0) {
|
|
6381
|
-
return
|
|
6441
|
+
return [
|
|
6442
|
+
`# Live Trading Report: ${strategyName}`,
|
|
6443
|
+
"",
|
|
6444
|
+
"No events recorded yet."
|
|
6445
|
+
].join("\n");
|
|
6382
6446
|
}
|
|
6383
6447
|
const header = columns$3.map((col) => col.label);
|
|
6384
6448
|
const separator = columns$3.map(() => "---");
|
|
6385
6449
|
const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
|
|
6386
6450
|
const tableData = [header, separator, ...rows];
|
|
6387
|
-
const table =
|
|
6388
|
-
return
|
|
6451
|
+
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
6452
|
+
return [
|
|
6453
|
+
`# Live Trading Report: ${strategyName}`,
|
|
6454
|
+
"",
|
|
6455
|
+
table,
|
|
6456
|
+
"",
|
|
6457
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
6458
|
+
`**Closed signals:** ${stats.totalClosed}`,
|
|
6459
|
+
`**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
|
|
6460
|
+
`**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
|
|
6461
|
+
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
6462
|
+
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
6463
|
+
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
6464
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
6465
|
+
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
6466
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
6467
|
+
].join("\n");
|
|
6389
6468
|
}
|
|
6390
6469
|
/**
|
|
6391
6470
|
* Saves strategy report to disk.
|
|
@@ -6782,14 +6861,28 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
6782
6861
|
async getReport(strategyName) {
|
|
6783
6862
|
const stats = await this.getData();
|
|
6784
6863
|
if (stats.totalEvents === 0) {
|
|
6785
|
-
return
|
|
6864
|
+
return [
|
|
6865
|
+
`# Scheduled Signals Report: ${strategyName}`,
|
|
6866
|
+
"",
|
|
6867
|
+
"No scheduled signals recorded yet."
|
|
6868
|
+
].join("\n");
|
|
6786
6869
|
}
|
|
6787
6870
|
const header = columns$2.map((col) => col.label);
|
|
6788
6871
|
const separator = columns$2.map(() => "---");
|
|
6789
6872
|
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
6790
6873
|
const tableData = [header, separator, ...rows];
|
|
6791
|
-
const table =
|
|
6792
|
-
return
|
|
6874
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
6875
|
+
return [
|
|
6876
|
+
`# Scheduled Signals Report: ${strategyName}`,
|
|
6877
|
+
"",
|
|
6878
|
+
table,
|
|
6879
|
+
"",
|
|
6880
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
6881
|
+
`**Scheduled signals:** ${stats.totalScheduled}`,
|
|
6882
|
+
`**Cancelled signals:** ${stats.totalCancelled}`,
|
|
6883
|
+
`**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
|
|
6884
|
+
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
|
|
6885
|
+
].join("\n");
|
|
6793
6886
|
}
|
|
6794
6887
|
/**
|
|
6795
6888
|
* Saves strategy report to disk.
|
|
@@ -7107,7 +7200,11 @@ class PerformanceStorage {
|
|
|
7107
7200
|
async getReport(strategyName) {
|
|
7108
7201
|
const stats = await this.getData(strategyName);
|
|
7109
7202
|
if (stats.totalEvents === 0) {
|
|
7110
|
-
return
|
|
7203
|
+
return [
|
|
7204
|
+
`# Performance Report: ${strategyName}`,
|
|
7205
|
+
"",
|
|
7206
|
+
"No performance metrics recorded yet."
|
|
7207
|
+
].join("\n");
|
|
7111
7208
|
}
|
|
7112
7209
|
// Sort metrics by total duration (descending) to show bottlenecks first
|
|
7113
7210
|
const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
|
|
@@ -7144,13 +7241,29 @@ class PerformanceStorage {
|
|
|
7144
7241
|
metric.maxWaitTime.toFixed(2),
|
|
7145
7242
|
]);
|
|
7146
7243
|
const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
|
|
7147
|
-
const summaryTable =
|
|
7244
|
+
const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7148
7245
|
// Calculate percentage of total time for each metric
|
|
7149
7246
|
const percentages = sortedMetrics.map((metric) => {
|
|
7150
7247
|
const pct = (metric.totalDuration / stats.totalDuration) * 100;
|
|
7151
7248
|
return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
|
|
7152
7249
|
});
|
|
7153
|
-
return
|
|
7250
|
+
return [
|
|
7251
|
+
`# Performance Report: ${strategyName}`,
|
|
7252
|
+
"",
|
|
7253
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
7254
|
+
`**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
|
|
7255
|
+
`**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
|
|
7256
|
+
"",
|
|
7257
|
+
"## Time Distribution",
|
|
7258
|
+
"",
|
|
7259
|
+
percentages.join("\n"),
|
|
7260
|
+
"",
|
|
7261
|
+
"## Detailed Metrics",
|
|
7262
|
+
"",
|
|
7263
|
+
summaryTable,
|
|
7264
|
+
"",
|
|
7265
|
+
"**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type."
|
|
7266
|
+
].join("\n");
|
|
7154
7267
|
}
|
|
7155
7268
|
/**
|
|
7156
7269
|
* Saves performance report to disk.
|
|
@@ -7554,7 +7667,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7554
7667
|
// Build table rows
|
|
7555
7668
|
const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
|
|
7556
7669
|
const tableData = [header, separator, ...rows];
|
|
7557
|
-
return
|
|
7670
|
+
return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7558
7671
|
}
|
|
7559
7672
|
/**
|
|
7560
7673
|
* Generates PNL table showing all closed signals across all strategies (View).
|
|
@@ -7591,7 +7704,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7591
7704
|
// Build table rows
|
|
7592
7705
|
const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
|
|
7593
7706
|
const tableData = [header, separator, ...rows];
|
|
7594
|
-
return
|
|
7707
|
+
return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7595
7708
|
}
|
|
7596
7709
|
/**
|
|
7597
7710
|
* Generates markdown report with all strategy results (View).
|
|
@@ -7606,7 +7719,30 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7606
7719
|
const results = await this.getData(symbol, metric, context);
|
|
7607
7720
|
// Get total signals for best strategy
|
|
7608
7721
|
const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
|
|
7609
|
-
return
|
|
7722
|
+
return [
|
|
7723
|
+
`# Walker Comparison Report: ${results.walkerName}`,
|
|
7724
|
+
"",
|
|
7725
|
+
`**Symbol:** ${results.symbol}`,
|
|
7726
|
+
`**Exchange:** ${results.exchangeName}`,
|
|
7727
|
+
`**Frame:** ${results.frameName}`,
|
|
7728
|
+
`**Optimization Metric:** ${results.metric}`,
|
|
7729
|
+
`**Strategies Tested:** ${results.totalStrategies}`,
|
|
7730
|
+
"",
|
|
7731
|
+
`## Best Strategy: ${results.bestStrategy}`,
|
|
7732
|
+
"",
|
|
7733
|
+
`**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
|
|
7734
|
+
`**Total Signals:** ${bestStrategySignals}`,
|
|
7735
|
+
"",
|
|
7736
|
+
"## Top Strategies Comparison",
|
|
7737
|
+
"",
|
|
7738
|
+
this.getComparisonTable(metric, 10),
|
|
7739
|
+
"",
|
|
7740
|
+
"## All Signals (PNL Table)",
|
|
7741
|
+
"",
|
|
7742
|
+
this.getPnlTable(),
|
|
7743
|
+
"",
|
|
7744
|
+
"**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
|
|
7745
|
+
].join("\n");
|
|
7610
7746
|
}
|
|
7611
7747
|
/**
|
|
7612
7748
|
* Saves walker report to disk.
|
|
@@ -8120,14 +8256,24 @@ class HeatmapStorage {
|
|
|
8120
8256
|
async getReport(strategyName) {
|
|
8121
8257
|
const data = await this.getData();
|
|
8122
8258
|
if (data.symbols.length === 0) {
|
|
8123
|
-
return
|
|
8259
|
+
return [
|
|
8260
|
+
`# Portfolio Heatmap: ${strategyName}`,
|
|
8261
|
+
"",
|
|
8262
|
+
"*No data available*"
|
|
8263
|
+
].join("\n");
|
|
8124
8264
|
}
|
|
8125
8265
|
const header = columns$1.map((col) => col.label);
|
|
8126
8266
|
const separator = columns$1.map(() => "---");
|
|
8127
8267
|
const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
|
|
8128
8268
|
const tableData = [header, separator, ...rows];
|
|
8129
|
-
const table =
|
|
8130
|
-
return
|
|
8269
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
8270
|
+
return [
|
|
8271
|
+
`# Portfolio Heatmap: ${strategyName}`,
|
|
8272
|
+
"",
|
|
8273
|
+
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
|
|
8274
|
+
"",
|
|
8275
|
+
table
|
|
8276
|
+
].join("\n");
|
|
8131
8277
|
}
|
|
8132
8278
|
/**
|
|
8133
8279
|
* Saves heatmap report to disk.
|
|
@@ -10685,14 +10831,26 @@ class ReportStorage {
|
|
|
10685
10831
|
async getReport(symbol, strategyName) {
|
|
10686
10832
|
const stats = await this.getData();
|
|
10687
10833
|
if (stats.totalEvents === 0) {
|
|
10688
|
-
return
|
|
10834
|
+
return [
|
|
10835
|
+
`# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
|
|
10836
|
+
"",
|
|
10837
|
+
"No partial profit/loss events recorded yet."
|
|
10838
|
+
].join("\n");
|
|
10689
10839
|
}
|
|
10690
10840
|
const header = columns.map((col) => col.label);
|
|
10691
10841
|
const separator = columns.map(() => "---");
|
|
10692
10842
|
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
10693
10843
|
const tableData = [header, separator, ...rows];
|
|
10694
|
-
const table =
|
|
10695
|
-
return
|
|
10844
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
10845
|
+
return [
|
|
10846
|
+
`# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
|
|
10847
|
+
"",
|
|
10848
|
+
table,
|
|
10849
|
+
"",
|
|
10850
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
10851
|
+
`**Profit events:** ${stats.totalProfit}`,
|
|
10852
|
+
`**Loss events:** ${stats.totalLoss}`
|
|
10853
|
+
].join("\n");
|
|
10696
10854
|
}
|
|
10697
10855
|
/**
|
|
10698
10856
|
* Saves symbol-strategy report to disk.
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -31,6 +31,15 @@ declare const GLOBAL_CONFIG: {
|
|
|
31
31
|
* Default: 1440 minutes (1 day)
|
|
32
32
|
*/
|
|
33
33
|
CC_MAX_SIGNAL_LIFETIME_MINUTES: number;
|
|
34
|
+
/**
|
|
35
|
+
* Maximum time allowed for signal generation (in seconds).
|
|
36
|
+
* Prevents long-running or stuck signal generation routines from blocking
|
|
37
|
+
* execution or consuming resources indefinitely. If generation exceeds this
|
|
38
|
+
* threshold the attempt should be aborted, logged and optionally retried.
|
|
39
|
+
*
|
|
40
|
+
* Default: 180 seconds (3 minutes)
|
|
41
|
+
*/
|
|
42
|
+
CC_MAX_SIGNAL_GENERATION_SECONDS: number;
|
|
34
43
|
/**
|
|
35
44
|
* Number of retries for getCandles function
|
|
36
45
|
* Default: 3 retries
|
|
@@ -918,6 +927,10 @@ interface IStrategyTickResultActive {
|
|
|
918
927
|
exchangeName: ExchangeName;
|
|
919
928
|
/** Trading pair symbol (e.g., "BTCUSDT") */
|
|
920
929
|
symbol: string;
|
|
930
|
+
/** Percentage progress towards take profit (0-100%, 0 if moving towards SL) */
|
|
931
|
+
percentTp: number;
|
|
932
|
+
/** Percentage progress towards stop loss (0-100%, 0 if moving towards TP) */
|
|
933
|
+
percentSl: number;
|
|
921
934
|
}
|
|
922
935
|
/**
|
|
923
936
|
* Tick result: signal closed with PNL.
|
|
@@ -3764,6 +3777,10 @@ interface TickEvent {
|
|
|
3764
3777
|
takeProfit?: number;
|
|
3765
3778
|
/** Stop loss price (only for opened/active/closed) */
|
|
3766
3779
|
stopLoss?: number;
|
|
3780
|
+
/** Percentage progress towards take profit (only for active) */
|
|
3781
|
+
percentTp?: number;
|
|
3782
|
+
/** Percentage progress towards stop loss (only for active) */
|
|
3783
|
+
percentSl?: number;
|
|
3767
3784
|
/** PNL percentage (only for closed) */
|
|
3768
3785
|
pnl?: number;
|
|
3769
3786
|
/** Close reason (only for closed) */
|