backtest-kit 11.9.0 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -21061,6 +21061,24 @@ class WalkerCommandService {
21061
21061
  }
21062
21062
  }
21063
21063
 
21064
+ /**
21065
+ * Derives the number of decimal places to show for a price based on the
21066
+ * magnitude of its integer part. Larger prices need fewer decimals; sub-dollar
21067
+ * prices keep full precision.
21068
+ * @param value - The price to derive the decimal scale for
21069
+ * @returns The number of digits after the decimal point
21070
+ */
21071
+ const getPriceScale = (value) => {
21072
+ const abs = Math.abs(value);
21073
+ if (abs >= 1) {
21074
+ // 1..9 -> 4, 10..99 -> 3, 100..999 -> 2, 1000+ -> 2 (floor), capped at 2
21075
+ const digits = Math.floor(Math.log10(abs)) + 1;
21076
+ return Math.max(2, 6 - digits);
21077
+ }
21078
+ // Sub-dollar prices need more precision.
21079
+ return 8;
21080
+ };
21081
+
21064
21082
  /**
21065
21083
  * Converts markdown content to plain text with minimal formatting
21066
21084
  * @param content - Markdown string to convert
@@ -21169,43 +21187,43 @@ const backtest_columns = [
21169
21187
  {
21170
21188
  key: "openPrice",
21171
21189
  label: "Open Price",
21172
- format: (data) => `${data.signal.priceOpen.toFixed(8)} USD`,
21190
+ format: (data) => `${data.signal.priceOpen.toFixed(getPriceScale(data.signal.priceOpen))} USD`,
21173
21191
  isVisible: () => true,
21174
21192
  },
21175
21193
  {
21176
21194
  key: "closePrice",
21177
21195
  label: "Close Price",
21178
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
21196
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21179
21197
  isVisible: () => true,
21180
21198
  },
21181
21199
  {
21182
21200
  key: "takeProfit",
21183
21201
  label: "Take Profit",
21184
- format: (data) => `${data.signal.priceTakeProfit.toFixed(8)} USD`,
21202
+ format: (data) => `${data.signal.priceTakeProfit.toFixed(getPriceScale(data.signal.priceTakeProfit))} USD`,
21185
21203
  isVisible: () => true,
21186
21204
  },
21187
21205
  {
21188
21206
  key: "stopLoss",
21189
21207
  label: "Stop Loss",
21190
- format: (data) => `${data.signal.priceStopLoss.toFixed(8)} USD`,
21208
+ format: (data) => `${data.signal.priceStopLoss.toFixed(getPriceScale(data.signal.priceStopLoss))} USD`,
21191
21209
  isVisible: () => true,
21192
21210
  },
21193
21211
  {
21194
21212
  key: "originalPriceTakeProfit",
21195
21213
  label: "Original TP",
21196
- format: (data) => `${data.signal.originalPriceTakeProfit.toFixed(8)} USD`,
21214
+ format: (data) => `${data.signal.originalPriceTakeProfit.toFixed(getPriceScale(data.signal.originalPriceTakeProfit))} USD`,
21197
21215
  isVisible: () => true,
21198
21216
  },
21199
21217
  {
21200
21218
  key: "originalPriceStopLoss",
21201
21219
  label: "Original SL",
21202
- format: (data) => `${data.signal.originalPriceStopLoss.toFixed(8)} USD`,
21220
+ format: (data) => `${data.signal.originalPriceStopLoss.toFixed(getPriceScale(data.signal.originalPriceStopLoss))} USD`,
21203
21221
  isVisible: () => true,
21204
21222
  },
21205
21223
  {
21206
21224
  key: "originalPriceOpen",
21207
21225
  label: "Original Entry",
21208
- format: (data) => `${data.signal.originalPriceOpen.toFixed(8)} USD`,
21226
+ format: (data) => `${data.signal.originalPriceOpen.toFixed(getPriceScale(data.signal.originalPriceOpen))} USD`,
21209
21227
  isVisible: () => true,
21210
21228
  },
21211
21229
  {
@@ -21508,6 +21526,71 @@ const heat_columns = [
21508
21526
  : "N/A",
21509
21527
  isVisible: () => true,
21510
21528
  },
21529
+ {
21530
+ key: "trend",
21531
+ label: "Trend",
21532
+ format: (data) => data.trend ?? "N/A",
21533
+ isVisible: () => true,
21534
+ },
21535
+ {
21536
+ key: "trendStrength",
21537
+ label: "Trend %/d",
21538
+ format: (data) => data.trendStrength !== null ? functoolsKit.str(data.trendStrength, "%") : "N/A",
21539
+ isVisible: () => true,
21540
+ },
21541
+ {
21542
+ key: "trendConfidence",
21543
+ label: "Trend R²",
21544
+ format: (data) => data.trendConfidence !== null ? data.trendConfidence.toFixed(3) : "N/A",
21545
+ isVisible: () => true,
21546
+ },
21547
+ {
21548
+ key: "buyerPressure",
21549
+ label: "Buyer Pres",
21550
+ format: (data) => data.buyerPressure !== null
21551
+ ? (data.buyerPressure * 100).toFixed(1) + "%"
21552
+ : "N/A",
21553
+ isVisible: () => true,
21554
+ },
21555
+ {
21556
+ key: "sellerPressure",
21557
+ label: "Seller Pres",
21558
+ format: (data) => data.sellerPressure !== null
21559
+ ? (data.sellerPressure * 100).toFixed(1) + "%"
21560
+ : "N/A",
21561
+ isVisible: () => true,
21562
+ },
21563
+ {
21564
+ key: "buyerStrength",
21565
+ label: "Buyer Str",
21566
+ format: (data) => data.buyerStrength !== null
21567
+ ? (data.buyerStrength * 100).toFixed(1) + "%"
21568
+ : "N/A",
21569
+ isVisible: () => true,
21570
+ },
21571
+ {
21572
+ key: "sellerStrength",
21573
+ label: "Seller Str",
21574
+ format: (data) => data.sellerStrength !== null
21575
+ ? (data.sellerStrength * 100).toFixed(1) + "%"
21576
+ : "N/A",
21577
+ isVisible: () => true,
21578
+ },
21579
+ {
21580
+ key: "pressureImbalance",
21581
+ label: "Pres Imb",
21582
+ format: (data) => data.pressureImbalance !== null
21583
+ ? (data.pressureImbalance > 0 ? "+" : "") +
21584
+ data.pressureImbalance.toFixed(3)
21585
+ : "N/A",
21586
+ isVisible: () => true,
21587
+ },
21588
+ {
21589
+ key: "medianStepSize",
21590
+ label: "Median Step",
21591
+ format: (data) => data.medianStepSize !== null ? functoolsKit.str(data.medianStepSize, "%") : "N/A",
21592
+ isVisible: () => true,
21593
+ },
21511
21594
  {
21512
21595
  key: "sortinoRatio",
21513
21596
  label: "Sortino",
@@ -21604,34 +21687,34 @@ const live_columns = [
21604
21687
  {
21605
21688
  key: "currentPrice",
21606
21689
  label: "Current Price",
21607
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
21690
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21608
21691
  isVisible: () => true,
21609
21692
  },
21610
21693
  {
21611
21694
  key: "openPrice",
21612
21695
  label: "Open Price",
21613
- format: (data) => data.priceOpen !== undefined ? `${data.priceOpen.toFixed(8)} USD` : "N/A",
21696
+ format: (data) => data.priceOpen !== undefined ? `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD` : "N/A",
21614
21697
  isVisible: () => true,
21615
21698
  },
21616
21699
  {
21617
21700
  key: "takeProfit",
21618
21701
  label: "Take Profit",
21619
21702
  format: (data) => data.priceTakeProfit !== undefined
21620
- ? `${data.priceTakeProfit.toFixed(8)} USD`
21703
+ ? `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`
21621
21704
  : "N/A",
21622
21705
  isVisible: () => true,
21623
21706
  },
21624
21707
  {
21625
21708
  key: "stopLoss",
21626
21709
  label: "Stop Loss",
21627
- format: (data) => data.priceStopLoss !== undefined ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A",
21710
+ format: (data) => data.priceStopLoss !== undefined ? `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD` : "N/A",
21628
21711
  isVisible: () => true,
21629
21712
  },
21630
21713
  {
21631
21714
  key: "originalPriceTakeProfit",
21632
21715
  label: "Original TP",
21633
21716
  format: (data) => data.originalPriceTakeProfit !== undefined
21634
- ? `${data.originalPriceTakeProfit.toFixed(8)} USD`
21717
+ ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD`
21635
21718
  : "N/A",
21636
21719
  isVisible: () => true,
21637
21720
  },
@@ -21639,7 +21722,7 @@ const live_columns = [
21639
21722
  key: "originalPriceStopLoss",
21640
21723
  label: "Original SL",
21641
21724
  format: (data) => data.originalPriceStopLoss !== undefined
21642
- ? `${data.originalPriceStopLoss.toFixed(8)} USD`
21725
+ ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD`
21643
21726
  : "N/A",
21644
21727
  isVisible: () => true,
21645
21728
  },
@@ -21647,7 +21730,7 @@ const live_columns = [
21647
21730
  key: "originalPriceOpen",
21648
21731
  label: "Original Entry",
21649
21732
  format: (data) => data.originalPriceOpen !== undefined
21650
- ? `${data.originalPriceOpen.toFixed(8)} USD`
21733
+ ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD`
21651
21734
  : "N/A",
21652
21735
  isVisible: () => true,
21653
21736
  },
@@ -21818,43 +21901,43 @@ const partial_columns = [
21818
21901
  {
21819
21902
  key: "currentPrice",
21820
21903
  label: "Current Price",
21821
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
21904
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21822
21905
  isVisible: () => true,
21823
21906
  },
21824
21907
  {
21825
21908
  key: "priceOpen",
21826
21909
  label: "Entry Price",
21827
- format: (data) => (data.priceOpen ? `${data.priceOpen.toFixed(8)} USD` : "N/A"),
21910
+ format: (data) => (data.priceOpen ? `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD` : "N/A"),
21828
21911
  isVisible: () => true,
21829
21912
  },
21830
21913
  {
21831
21914
  key: "priceTakeProfit",
21832
21915
  label: "Take Profit",
21833
- format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(8)} USD` : "N/A"),
21916
+ format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD` : "N/A"),
21834
21917
  isVisible: () => true,
21835
21918
  },
21836
21919
  {
21837
21920
  key: "priceStopLoss",
21838
21921
  label: "Stop Loss",
21839
- format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A"),
21922
+ format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD` : "N/A"),
21840
21923
  isVisible: () => true,
21841
21924
  },
21842
21925
  {
21843
21926
  key: "originalPriceTakeProfit",
21844
21927
  label: "Original TP",
21845
- format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(8)} USD` : "N/A"),
21928
+ format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD` : "N/A"),
21846
21929
  isVisible: () => true,
21847
21930
  },
21848
21931
  {
21849
21932
  key: "originalPriceStopLoss",
21850
21933
  label: "Original SL",
21851
- format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
21934
+ format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD` : "N/A"),
21852
21935
  isVisible: () => true,
21853
21936
  },
21854
21937
  {
21855
21938
  key: "originalPriceOpen",
21856
21939
  label: "Original Entry",
21857
- format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
21940
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD` : "N/A"),
21858
21941
  isVisible: () => true,
21859
21942
  },
21860
21943
  {
@@ -21978,43 +22061,43 @@ const breakeven_columns = [
21978
22061
  {
21979
22062
  key: "priceOpen",
21980
22063
  label: "Entry Price",
21981
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22064
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
21982
22065
  isVisible: () => true,
21983
22066
  },
21984
22067
  {
21985
22068
  key: "currentPrice",
21986
22069
  label: "Breakeven Price",
21987
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22070
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
21988
22071
  isVisible: () => true,
21989
22072
  },
21990
22073
  {
21991
22074
  key: "priceTakeProfit",
21992
22075
  label: "Take Profit",
21993
- format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(8)} USD` : "N/A"),
22076
+ format: (data) => (data.priceTakeProfit ? `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD` : "N/A"),
21994
22077
  isVisible: () => true,
21995
22078
  },
21996
22079
  {
21997
22080
  key: "priceStopLoss",
21998
22081
  label: "Stop Loss",
21999
- format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A"),
22082
+ format: (data) => (data.priceStopLoss ? `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD` : "N/A"),
22000
22083
  isVisible: () => true,
22001
22084
  },
22002
22085
  {
22003
22086
  key: "originalPriceTakeProfit",
22004
22087
  label: "Original TP",
22005
- format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(8)} USD` : "N/A"),
22088
+ format: (data) => (data.originalPriceTakeProfit ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD` : "N/A"),
22006
22089
  isVisible: () => true,
22007
22090
  },
22008
22091
  {
22009
22092
  key: "originalPriceStopLoss",
22010
22093
  label: "Original SL",
22011
- format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
22094
+ format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD` : "N/A"),
22012
22095
  isVisible: () => true,
22013
22096
  },
22014
22097
  {
22015
22098
  key: "originalPriceOpen",
22016
22099
  label: "Original Entry",
22017
- format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
22100
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD` : "N/A"),
22018
22101
  isVisible: () => true,
22019
22102
  },
22020
22103
  {
@@ -22271,7 +22354,7 @@ const risk_columns = [
22271
22354
  key: "openPrice",
22272
22355
  label: "Open Price",
22273
22356
  format: (data) => data.currentSignal.priceOpen !== undefined
22274
- ? `${data.currentSignal.priceOpen.toFixed(8)} USD`
22357
+ ? `${data.currentSignal.priceOpen.toFixed(getPriceScale(data.currentSignal.priceOpen))} USD`
22275
22358
  : "N/A",
22276
22359
  isVisible: () => true,
22277
22360
  },
@@ -22279,7 +22362,7 @@ const risk_columns = [
22279
22362
  key: "takeProfit",
22280
22363
  label: "Take Profit",
22281
22364
  format: (data) => data.currentSignal.priceTakeProfit !== undefined
22282
- ? `${data.currentSignal.priceTakeProfit.toFixed(8)} USD`
22365
+ ? `${data.currentSignal.priceTakeProfit.toFixed(getPriceScale(data.currentSignal.priceTakeProfit))} USD`
22283
22366
  : "N/A",
22284
22367
  isVisible: () => true,
22285
22368
  },
@@ -22287,7 +22370,7 @@ const risk_columns = [
22287
22370
  key: "stopLoss",
22288
22371
  label: "Stop Loss",
22289
22372
  format: (data) => data.currentSignal.priceStopLoss !== undefined
22290
- ? `${data.currentSignal.priceStopLoss.toFixed(8)} USD`
22373
+ ? `${data.currentSignal.priceStopLoss.toFixed(getPriceScale(data.currentSignal.priceStopLoss))} USD`
22291
22374
  : "N/A",
22292
22375
  isVisible: () => true,
22293
22376
  },
@@ -22295,7 +22378,7 @@ const risk_columns = [
22295
22378
  key: "originalPriceTakeProfit",
22296
22379
  label: "Original TP",
22297
22380
  format: (data) => data.currentSignal.originalPriceTakeProfit !== undefined
22298
- ? `${data.currentSignal.originalPriceTakeProfit.toFixed(8)} USD`
22381
+ ? `${data.currentSignal.originalPriceTakeProfit.toFixed(getPriceScale(data.currentSignal.originalPriceTakeProfit))} USD`
22299
22382
  : "N/A",
22300
22383
  isVisible: () => true,
22301
22384
  },
@@ -22303,7 +22386,7 @@ const risk_columns = [
22303
22386
  key: "originalPriceStopLoss",
22304
22387
  label: "Original SL",
22305
22388
  format: (data) => data.currentSignal.originalPriceStopLoss !== undefined
22306
- ? `${data.currentSignal.originalPriceStopLoss.toFixed(8)} USD`
22389
+ ? `${data.currentSignal.originalPriceStopLoss.toFixed(getPriceScale(data.currentSignal.originalPriceStopLoss))} USD`
22307
22390
  : "N/A",
22308
22391
  isVisible: () => true,
22309
22392
  },
@@ -22311,7 +22394,7 @@ const risk_columns = [
22311
22394
  key: "originalPriceOpen",
22312
22395
  label: "Original Entry",
22313
22396
  format: (data) => data.currentSignal.originalPriceOpen !== undefined
22314
- ? `${data.currentSignal.originalPriceOpen.toFixed(8)} USD`
22397
+ ? `${data.currentSignal.originalPriceOpen.toFixed(getPriceScale(data.currentSignal.originalPriceOpen))} USD`
22315
22398
  : "N/A",
22316
22399
  isVisible: () => true,
22317
22400
  },
@@ -22342,7 +22425,7 @@ const risk_columns = [
22342
22425
  {
22343
22426
  key: "currentPrice",
22344
22427
  label: "Current Price",
22345
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22428
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22346
22429
  isVisible: () => true,
22347
22430
  },
22348
22431
  {
@@ -22468,32 +22551,32 @@ const schedule_columns = [
22468
22551
  {
22469
22552
  key: "currentPrice",
22470
22553
  label: "Current Price",
22471
- format: (data) => data.currentPrice ? `${data.currentPrice.toFixed(8)} USD` : "N/A",
22554
+ format: (data) => data.currentPrice ? `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD` : "N/A",
22472
22555
  isVisible: () => true,
22473
22556
  },
22474
22557
  {
22475
22558
  key: "priceOpen",
22476
22559
  label: "Entry Price",
22477
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22560
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22478
22561
  isVisible: () => true,
22479
22562
  },
22480
22563
  {
22481
22564
  key: "takeProfit",
22482
22565
  label: "Take Profit",
22483
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
22566
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22484
22567
  isVisible: () => true,
22485
22568
  },
22486
22569
  {
22487
22570
  key: "stopLoss",
22488
22571
  label: "Stop Loss",
22489
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
22572
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22490
22573
  isVisible: () => true,
22491
22574
  },
22492
22575
  {
22493
22576
  key: "originalPriceTakeProfit",
22494
22577
  label: "Original TP",
22495
22578
  format: (data) => data.originalPriceTakeProfit !== undefined
22496
- ? `${data.originalPriceTakeProfit.toFixed(8)} USD`
22579
+ ? `${data.originalPriceTakeProfit.toFixed(getPriceScale(data.originalPriceTakeProfit))} USD`
22497
22580
  : "N/A",
22498
22581
  isVisible: () => true,
22499
22582
  },
@@ -22501,7 +22584,7 @@ const schedule_columns = [
22501
22584
  key: "originalPriceStopLoss",
22502
22585
  label: "Original SL",
22503
22586
  format: (data) => data.originalPriceStopLoss !== undefined
22504
- ? `${data.originalPriceStopLoss.toFixed(8)} USD`
22587
+ ? `${data.originalPriceStopLoss.toFixed(getPriceScale(data.originalPriceStopLoss))} USD`
22505
22588
  : "N/A",
22506
22589
  isVisible: () => true,
22507
22590
  },
@@ -22509,7 +22592,7 @@ const schedule_columns = [
22509
22592
  key: "originalPriceOpen",
22510
22593
  label: "Original Entry",
22511
22594
  format: (data) => data.originalPriceOpen !== undefined
22512
- ? `${data.originalPriceOpen.toFixed(8)} USD`
22595
+ ? `${data.originalPriceOpen.toFixed(getPriceScale(data.originalPriceOpen))} USD`
22513
22596
  : "N/A",
22514
22597
  isVisible: () => true,
22515
22598
  },
@@ -22634,7 +22717,7 @@ const strategy_columns = [
22634
22717
  {
22635
22718
  key: "currentPrice",
22636
22719
  label: "Price",
22637
- format: (data) => (data.currentPrice !== undefined ? `${data.currentPrice.toFixed(8)} USD` : "N/A"),
22720
+ format: (data) => (data.currentPrice !== undefined ? `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD` : "N/A"),
22638
22721
  isVisible: () => true,
22639
22722
  },
22640
22723
  {
@@ -22754,25 +22837,25 @@ const sync_columns = [
22754
22837
  {
22755
22838
  key: "currentPrice",
22756
22839
  label: "Current Price",
22757
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22840
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22758
22841
  isVisible: () => true,
22759
22842
  },
22760
22843
  {
22761
22844
  key: "priceOpen",
22762
22845
  label: "Entry Price",
22763
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22846
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22764
22847
  isVisible: () => true,
22765
22848
  },
22766
22849
  {
22767
22850
  key: "priceTakeProfit",
22768
22851
  label: "Take Profit",
22769
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
22852
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22770
22853
  isVisible: () => true,
22771
22854
  },
22772
22855
  {
22773
22856
  key: "priceStopLoss",
22774
22857
  label: "Stop Loss",
22775
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
22858
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22776
22859
  isVisible: () => true,
22777
22860
  },
22778
22861
  {
@@ -22865,25 +22948,25 @@ const highest_profit_columns = [
22865
22948
  {
22866
22949
  key: "currentPrice",
22867
22950
  label: "Peak Price",
22868
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
22951
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22869
22952
  isVisible: () => true,
22870
22953
  },
22871
22954
  {
22872
22955
  key: "priceOpen",
22873
22956
  label: "Entry Price",
22874
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
22957
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22875
22958
  isVisible: () => true,
22876
22959
  },
22877
22960
  {
22878
22961
  key: "priceTakeProfit",
22879
22962
  label: "Take Profit",
22880
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
22963
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22881
22964
  isVisible: () => true,
22882
22965
  },
22883
22966
  {
22884
22967
  key: "priceStopLoss",
22885
22968
  label: "Stop Loss",
22886
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
22969
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22887
22970
  isVisible: () => true,
22888
22971
  },
22889
22972
  {
@@ -22952,25 +23035,25 @@ const max_drawdown_columns = [
22952
23035
  {
22953
23036
  key: "currentPrice",
22954
23037
  label: "DD Price",
22955
- format: (data) => `${data.currentPrice.toFixed(8)} USD`,
23038
+ format: (data) => `${data.currentPrice.toFixed(getPriceScale(data.currentPrice))} USD`,
22956
23039
  isVisible: () => true,
22957
23040
  },
22958
23041
  {
22959
23042
  key: "priceOpen",
22960
23043
  label: "Entry Price",
22961
- format: (data) => `${data.priceOpen.toFixed(8)} USD`,
23044
+ format: (data) => `${data.priceOpen.toFixed(getPriceScale(data.priceOpen))} USD`,
22962
23045
  isVisible: () => true,
22963
23046
  },
22964
23047
  {
22965
23048
  key: "priceTakeProfit",
22966
23049
  label: "Take Profit",
22967
- format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
23050
+ format: (data) => `${data.priceTakeProfit.toFixed(getPriceScale(data.priceTakeProfit))} USD`,
22968
23051
  isVisible: () => true,
22969
23052
  },
22970
23053
  {
22971
23054
  key: "priceStopLoss",
22972
23055
  label: "Stop Loss",
22973
- format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
23056
+ format: (data) => `${data.priceStopLoss.toFixed(getPriceScale(data.priceStopLoss))} USD`,
22974
23057
  isVisible: () => true,
22975
23058
  },
22976
23059
  {
@@ -23852,6 +23935,165 @@ MarkdownFileBase = functoolsKit.makeExtendable(MarkdownFileBase);
23852
23935
  ReportBase = functoolsKit.makeExtendable(ReportBase);
23853
23936
  const ReportWriter = new ReportWriterAdapter();
23854
23937
 
23938
+ /** Minimum samples to surface any price-profile metric. Below this the
23939
+ * per-trade step-distribution and the regression are statistically noisy. */
23940
+ const MIN_SIGNALS = 10;
23941
+ /** R² gate for declaring any trend at all. Below this the regression is
23942
+ * too weak to claim a direction — treat the series as sideways even if
23943
+ * the slope is large. 0.30 is the conventional weak-to-moderate-fit
23944
+ * boundary in econometrics (Cohen's f² ≈ 0.43). */
23945
+ const R2_TREND_GATE = 0.30;
23946
+ /** Slope-magnitude threshold relative to medianStepSize for declaring the
23947
+ * trend strong enough to call bullish/bearish. Below this the regression
23948
+ * fits but the actual drift is weaker than the typical daily step, so we
23949
+ * downgrade to "neutral" (a real but uninteresting tilt). */
23950
+ const SLOPE_VS_STEP_GATE = 0.25;
23951
+ const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
23952
+ const median = (values) => {
23953
+ const sorted = values.slice().sort((a, b) => a - b);
23954
+ const mid = sorted.length >> 1;
23955
+ return sorted.length % 2 === 0
23956
+ ? (sorted[mid - 1] + sorted[mid]) / 2
23957
+ : sorted[mid];
23958
+ };
23959
+ const emptyProfile = () => ({
23960
+ medianStepSize: null,
23961
+ buyerPressure: null,
23962
+ sellerPressure: null,
23963
+ buyerStrength: null,
23964
+ sellerStrength: null,
23965
+ pressureImbalance: null,
23966
+ trend: null,
23967
+ trendStrength: null,
23968
+ trendConfidence: null,
23969
+ });
23970
+ /**
23971
+ * Computes the price-profile bundle for a chronological series of trade
23972
+ * closes. The input is expected to be sorted by `closeAt` ascending; the
23973
+ * function does not sort defensively (the markdown services already sort).
23974
+ *
23975
+ * @param series - One point per closed trade: timestamp (ms) and close price.
23976
+ * @returns A bundle of nine metrics, each `null` when the input is too small
23977
+ * or numerically unsafe.
23978
+ */
23979
+ const getPriceProfile = (series) => {
23980
+ const valid = series.filter((p) => isFiniteNumber(p.closeAt) &&
23981
+ p.closeAt > 0 &&
23982
+ isFiniteNumber(p.close) &&
23983
+ p.close > 0);
23984
+ if (valid.length < MIN_SIGNALS)
23985
+ return emptyProfile();
23986
+ const n = valid.length;
23987
+ const closes = valid.map((p) => p.close);
23988
+ const times = valid.map((p) => p.closeAt);
23989
+ const absSteps = [];
23990
+ let upMoves = 0;
23991
+ let downMoves = 0;
23992
+ let upMagnitude = 0;
23993
+ let downMagnitude = 0;
23994
+ for (let i = 1; i < n; i++) {
23995
+ const prev = closes[i - 1];
23996
+ const cur = closes[i];
23997
+ const ret = (cur - prev) / prev;
23998
+ const abs = Math.abs(ret);
23999
+ absSteps.push(abs);
24000
+ if (ret > 0) {
24001
+ upMoves++;
24002
+ upMagnitude += abs;
24003
+ }
24004
+ else if (ret < 0) {
24005
+ downMoves++;
24006
+ downMagnitude += abs;
24007
+ }
24008
+ }
24009
+ if (absSteps.length === 0)
24010
+ return emptyProfile();
24011
+ const medianStepSize = median(absSteps) * 100; // percent
24012
+ // --- Pressure / strength ---
24013
+ const decisiveMoves = upMoves + downMoves;
24014
+ const totalMagnitude = upMagnitude + downMagnitude;
24015
+ const buyerPressure = decisiveMoves > 0 ? upMoves / decisiveMoves : null;
24016
+ const sellerPressure = decisiveMoves > 0 ? downMoves / decisiveMoves : null;
24017
+ const buyerStrength = totalMagnitude > 0 ? upMagnitude / totalMagnitude : null;
24018
+ const sellerStrength = totalMagnitude > 0 ? downMagnitude / totalMagnitude : null;
24019
+ const pressureImbalance = buyerStrength !== null && sellerStrength !== null
24020
+ ? buyerStrength - sellerStrength
24021
+ : null;
24022
+ // --- Trend: linear regression of log(close) vs days ---
24023
+ // log-price slope is scale-invariant: 1%/day means the same whether the
24024
+ // asset trades at $0.01 or $10000. Use `closeAt[0]` as time origin so x_i
24025
+ // starts at zero — keeps numerical conditioning sane on long horizons.
24026
+ const t0 = times[0];
24027
+ const xs = new Array(n);
24028
+ const ys = new Array(n);
24029
+ for (let i = 0; i < n; i++) {
24030
+ xs[i] = (times[i] - t0) / (1000 * 60 * 60 * 24); // days
24031
+ ys[i] = Math.log(closes[i]);
24032
+ }
24033
+ // Calendar span must be non-degenerate for the slope to mean anything.
24034
+ const xRange = xs[n - 1] - xs[0];
24035
+ let trend = null;
24036
+ let trendStrength = null;
24037
+ let trendConfidence = null;
24038
+ if (xRange > 0) {
24039
+ let sumX = 0;
24040
+ let sumY = 0;
24041
+ for (let i = 0; i < n; i++) {
24042
+ sumX += xs[i];
24043
+ sumY += ys[i];
24044
+ }
24045
+ const meanX = sumX / n;
24046
+ const meanY = sumY / n;
24047
+ let ssXX = 0;
24048
+ let ssXY = 0;
24049
+ let ssYY = 0;
24050
+ for (let i = 0; i < n; i++) {
24051
+ const dx = xs[i] - meanX;
24052
+ const dy = ys[i] - meanY;
24053
+ ssXX += dx * dx;
24054
+ ssXY += dx * dy;
24055
+ ssYY += dy * dy;
24056
+ }
24057
+ if (ssXX > 0) {
24058
+ const slopeLog = ssXY / ssXX; // log-return per day
24059
+ const slopePct = slopeLog * 100; // %/day (small-slope approx)
24060
+ // R² = 1 - SS_res / SS_tot. With a single explanatory variable this
24061
+ // equals (ssXY)² / (ssXX * ssYY) when ssYY > 0.
24062
+ const r2 = ssYY > 0 ? (ssXY * ssXY) / (ssXX * ssYY) : 0;
24063
+ trendStrength = slopePct;
24064
+ trendConfidence = Math.max(0, Math.min(1, r2));
24065
+ if (trendConfidence < R2_TREND_GATE) {
24066
+ trend = "sideways";
24067
+ }
24068
+ else {
24069
+ const slopeMagnitude = Math.abs(slopePct);
24070
+ const stepScale = medianStepSize * SLOPE_VS_STEP_GATE;
24071
+ if (slopeMagnitude < stepScale) {
24072
+ trend = "neutral";
24073
+ }
24074
+ else if (slopePct > 0) {
24075
+ trend = "bullish";
24076
+ }
24077
+ else {
24078
+ trend = "bearish";
24079
+ }
24080
+ }
24081
+ }
24082
+ }
24083
+ const safe = (v) => v === null || !Number.isFinite(v) ? null : v;
24084
+ return {
24085
+ medianStepSize: safe(medianStepSize),
24086
+ buyerPressure: safe(buyerPressure),
24087
+ sellerPressure: safe(sellerPressure),
24088
+ buyerStrength: safe(buyerStrength),
24089
+ sellerStrength: safe(sellerStrength),
24090
+ pressureImbalance: safe(pressureImbalance),
24091
+ trend,
24092
+ trendStrength: safe(trendStrength),
24093
+ trendConfidence: safe(trendConfidence),
24094
+ };
24095
+ };
24096
+
23855
24097
  /**
23856
24098
  * Creates a unique key for memoizing ReportStorage instances.
23857
24099
  * Key format: "symbol:strategyName:exchangeName:frameName:backtest" or "symbol:strategyName:exchangeName:live"
@@ -23984,6 +24226,15 @@ let ReportStorage$a = class ReportStorage {
23984
24226
  avgConsecutiveLossPnl: null,
23985
24227
  avgWinDuration: null,
23986
24228
  avgLossDuration: null,
24229
+ medianStepSize: null,
24230
+ buyerPressure: null,
24231
+ sellerPressure: null,
24232
+ buyerStrength: null,
24233
+ sellerStrength: null,
24234
+ pressureImbalance: null,
24235
+ trend: null,
24236
+ trendStrength: null,
24237
+ trendConfidence: null,
23987
24238
  };
23988
24239
  }
23989
24240
  // Valid signal set — those with usable pendingAt AND closeTimestamp. Single source
@@ -24031,16 +24282,18 @@ let ReportStorage$a = class ReportStorage {
24031
24282
  const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
24032
24283
  // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1) for unbiased estimate.
24033
24284
  // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
24034
- // are too noisy to publish (high chance of spurious ±Sharpe).
24285
+ // are too noisy to publish (high chance of spurious ±Sharpe). When the gate fails the
24286
+ // standard deviation itself is reported as null (NOT 0) so the report doesn't suggest
24287
+ // a flat distribution for a small but variable sample.
24035
24288
  const returns = validSignals.map((s) => s.pnl.pnlPercentage);
24036
24289
  const canComputeRatios = totalSignals >= MIN_SIGNALS_FOR_RATIOS$2;
24037
24290
  const stdDev = canComputeRatios
24038
24291
  ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (totalSignals - 1))
24039
- : 0;
24292
+ : null;
24040
24293
  // Use STDDEV_EPSILON gate (not stdDev > 0) — identical-returns series produce
24041
24294
  // float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously
24042
24295
  // inflates sharpe to astronomical magnitudes (avgPnl / epsilon).
24043
- const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$2
24296
+ const sharpeRatio = canComputeRatios && stdDev !== null && stdDev > STDDEV_EPSILON$2
24044
24297
  ? avgPnl / stdDev
24045
24298
  : null;
24046
24299
  // Annualize only when gate passes; otherwise null.
@@ -24280,6 +24533,13 @@ let ReportStorage$a = class ReportStorage {
24280
24533
  const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
24281
24534
  ? null
24282
24535
  : Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, ((equityFinal - 1) * 100) / equityMaxDrawdown));
24536
+ // Price profile — buyer/seller pressure, trend classification. Walks the
24537
+ // chronological close series (`orderedSignals` is already sorted by
24538
+ // closeTimestamp). N-gated internally by the helper (MIN_SIGNALS = 10).
24539
+ const priceProfile = getPriceProfile(orderedSignals.map((s) => ({
24540
+ closeAt: s.closeTimestamp,
24541
+ close: s.currentPrice,
24542
+ })));
24283
24543
  return {
24284
24544
  signalList: this._signalList,
24285
24545
  totalSignals,
@@ -24305,6 +24565,15 @@ let ReportStorage$a = class ReportStorage {
24305
24565
  avgConsecutiveLossPnl: isUnsafe$4(avgConsecutiveLossPnl) ? null : avgConsecutiveLossPnl,
24306
24566
  avgWinDuration: isUnsafe$4(avgWinDuration) ? null : avgWinDuration,
24307
24567
  avgLossDuration: isUnsafe$4(avgLossDuration) ? null : avgLossDuration,
24568
+ medianStepSize: priceProfile.medianStepSize,
24569
+ buyerPressure: priceProfile.buyerPressure,
24570
+ sellerPressure: priceProfile.sellerPressure,
24571
+ buyerStrength: priceProfile.buyerStrength,
24572
+ sellerStrength: priceProfile.sellerStrength,
24573
+ pressureImbalance: priceProfile.pressureImbalance,
24574
+ trend: priceProfile.trend,
24575
+ trendStrength: priceProfile.trendStrength,
24576
+ trendConfidence: priceProfile.trendConfidence,
24308
24577
  };
24309
24578
  }
24310
24579
  /**
@@ -24361,20 +24630,48 @@ let ReportStorage$a = class ReportStorage {
24361
24630
  `**Avg Loss Duration:** ${stats.avgLossDuration === null ? "N/A" : `${stats.avgLossDuration.toFixed(1)} min`}`,
24362
24631
  `**Avg Consecutive Win PNL:** ${stats.avgConsecutiveWinPnl === null ? "N/A" : `${stats.avgConsecutiveWinPnl > 0 ? "+" : ""}${stats.avgConsecutiveWinPnl.toFixed(3)}% (higher is better)`}`,
24363
24632
  `**Avg Consecutive Loss PNL:** ${stats.avgConsecutiveLossPnl === null ? "N/A" : `${stats.avgConsecutiveLossPnl.toFixed(3)}% (closer to 0 is better)`}`,
24633
+ `**Trend:** ${stats.trend === null ? "N/A" : stats.trend}`,
24634
+ `**Trend Strength:** ${stats.trendStrength === null ? "N/A" : `${stats.trendStrength > 0 ? "+" : ""}${stats.trendStrength.toFixed(3)}%/day`}`,
24635
+ `**Trend Confidence (R²):** ${stats.trendConfidence === null ? "N/A" : stats.trendConfidence.toFixed(3)}`,
24636
+ `**Buyer Pressure:** ${stats.buyerPressure === null ? "N/A" : `${(stats.buyerPressure * 100).toFixed(1)}%`}`,
24637
+ `**Seller Pressure:** ${stats.sellerPressure === null ? "N/A" : `${(stats.sellerPressure * 100).toFixed(1)}%`}`,
24638
+ `**Buyer Strength:** ${stats.buyerStrength === null ? "N/A" : `${(stats.buyerStrength * 100).toFixed(1)}%`}`,
24639
+ `**Seller Strength:** ${stats.sellerStrength === null ? "N/A" : `${(stats.sellerStrength * 100).toFixed(1)}%`}`,
24640
+ `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
24641
+ `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
24364
24642
  "",
24365
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
24366
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24367
- `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$2} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$2} days. Assumes returns are iid autocorrelated strategies are overstated.*`,
24368
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
24369
- `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
24370
- `*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS$2}% values above the cap return N/A.*`,
24371
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). Capped at ±${MAX_CALMAR_RATIO$2}.*`,
24372
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator and the mark-to-market max drawdown as denominator.*`,
24373
- `*Max Drawdown: mark-to-market the compounded equity curve applies each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery.*`,
24374
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
24375
- `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24376
- `*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures these metrics represent a theoretical upper bound under full allocation.*`,
24377
- `*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies closer to zero is less bad, positive is profitable.*`,
24643
+ `*Win Rate: percent of closed signals that ended with per-trade PNL > 0, computed as winning-trade count / (winning-trade count + losing-trade count) × 100 — break-even trades (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistical reliability: noisy below 30 signals (a single streak can shift it 1020 points); stable above 200 signals.*`,
24644
+ `*Average PNL: arithmetic mean of per-trade PNL across every closed signal, computed as Σ per-trade PNL / closed signal count. UNITS: percent per trade. Sign mirrors strategy edge — positive = profitable on average per trade. Sensitive to one whale trade; cross-check with Median PNL to detect skew.*`,
24645
+ `*Total PNL: arithmetic sum of per-trade PNL across every closed signal. UNITS: percent. This is the additive total, NOT the compounded equity return — for the geometrically-compounded variant see Recovery Factor's numerator and Expected Yearly Returns. Useful as a quick scoreboard but ignores volatility drag.*`,
24646
+ `*Standard Deviation Per Trade: sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL. UNITS: percent. Measures volatility of returns. Denominator for per-trade Sharpe Ratio. Below STDDEV_EPSILON = 1e-9 it is treated as zero (identical-returns guard). Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} (variance too noisy on small samples).*`,
24647
+ `*Sharpe Ratio: per-trade Sharpe = Average PNL / Standard Deviation Per Trade (risk-free rate = 0). UNITS: dimensionless ratio. Higher = better risk-adjusted return per trade. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
24648
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √(trades per year), where trades per year = closed signal count × 365 / calendar span in days. UNITS: dimensionless. Null when the closed signal count < ${MIN_SIGNALS_FOR_ANNUALIZATION$2}, OR calendar span < ${MIN_CALENDAR_SPAN_DAYS$2} days, OR raw frequency > ${MAX_TRADES_PER_YEAR$2} (clustered sample, annualisation unreliable). Assumes returns are iid — autocorrelated strategies are overstated.*`,
24649
+ `*Certainty Ratio: mean per-trade PNL over winning trades, divided by the absolute value of the mean per-trade PNL over losing trades. UNITS: dimensionless ratio. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, OR there are no losing trades, OR the absolute mean losing PNL < 1e-9 (float-artifact loss guard).*`,
24650
+ `*Expected Yearly Returns: geometric annualisation of the equity curve: (final equity ^ (trades per year / closed signal count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over all closed signals in chronological close order. UNITS: percent per year. Accounts for volatility drag (unlike a simple Σ × 365 / calendar-span projection). Null under the same closed-signal-count / calendar-span / frequency gates as Annualized Sharpe Ratio, AND null when |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS$2}% (capped numbers mislead). −100% when the equity curve hits ≤ 0 (account blown).*`,
24651
+ `*Avg Peak PNL: arithmetic mean of each closed signal's peak-PNL snapshot — the best mark-to-market PNL recorded while the position was open. Signals that never recorded such a snapshot are excluded from both numerator and denominator (no zero dilution). UNITS: percent. Describes the typical best-case unrealised excursion during the position's lifetime, not the realised close. NOT gated by MIN_SIGNALS computed whenever at least one signal carries the snapshot; null only if no signal carries it.*`,
24652
+ `*Avg Max Drawdown PNL: arithmetic mean of each closed signal's trough-PNL snapshot the worst mark-to-market PNL recorded while the position was open. Signals that never recorded such a snapshot are excluded (no zero dilution). UNITS: percent (negative for losing excursions). Describes the typical worst-case unrealised PNL during the position's lifetime; closer to 0 is better. NOT gated by MIN_SIGNALS — computed whenever at least one signal carries the snapshot.*`,
24653
+ `*Sortino Ratio: Average PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / closed signal count ) (canonical Sortino 1991: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, OR there are no losing trades (downside is undefined — flawless ≠ infinitely good), OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
24654
+ `*Calmar Ratio: Expected Yearly Returns divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$2}. The denominator is the max drawdown of the compounded equity curve: each trade's worst intra-trade excursion (the trough-PNL snapshot, 0) is applied before booking its realised close, so a position that dipped to −18% and recovered to +2% contributes a real drawdown rather than zero. UNITS: dimensionless. Rule of thumb: below 0.5 poor, 0.5–1.0 acceptable, above 1.0 strong. Null when Expected Yearly Returns is null OR the equity max drawdown 0.*`,
24655
+ `*Recovery Factor: (final equity 1) × 100 divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$2}. The numerator is the compounded total return — NOT the arithmetic Total PNL shown above; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = profit doesn't cover drawdown; above 3.0 = good. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, the equity curve blew up (account ≤ 0), or the equity max drawdown ≤ 0.*`,
24656
+ `*Expectancy: per-trade expected value = (winning-trade count / closed signal count) × mean winning PNL + (losing-trade count / closed signal count) × mean losing PNL. Break-even trades contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24657
+ `*Median PNL: median of per-trade PNL across all closed signals (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers — comparing it with Average PNL reveals distribution skew. A large gap (e.g. avg +1.5%, median +0.2%) means one or two trades carry the arithmetic mean. NOT gated by MIN_SIGNALS — computed whenever the closed signal count ≥ 1.*`,
24658
+ `*Avg Duration: arithmetic mean of (close-time − pending-time) / 60_000 over all closed signals. UNITS: minutes (synchronised with the strategy's estimated-minutes setting). Describes the typical position hold time. NOT gated by MIN_SIGNALS — computed whenever the closed signal count ≥ 1.*`,
24659
+ `*Avg Win Duration: arithmetic mean of (close-time − pending-time) / 60_000 restricted to winning trades (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning trades (NOT gated by MIN_SIGNALS — computed at any winning-trade count ≥ 1). Pair with Avg Loss Duration to detect the classic asymmetry "let winners run, cut losers short" (Win Duration > Loss Duration is healthy) versus the inverse red flag "cut winners short, let losers run".*`,
24660
+ `*Avg Loss Duration: arithmetic mean of (close-time − pending-time) / 60_000 restricted to losing trades (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing trades (NOT gated by MIN_SIGNALS).*`,
24661
+ `*Avg Consecutive Win PNL: a "win streak" is a run of consecutive trades with per-trade PNL > 0 bounded by either a losing or break-even trade; this metric sums per-trade PNL within each streak and then averages those sums. Trades are walked in chronological close order. UNITS: percent per streak. Higher = winning streaks are typically bigger. Null only when there is not a single complete win streak in the stored history (NOT gated by MIN_SIGNALS).*`,
24662
+ `*Avg Consecutive Loss PNL: a "loss streak" is a run of consecutive trades with per-trade PNL < 0 bounded by either a winning or break-even trade; this metric sums per-trade PNL within each streak and then averages those sums. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. Null only when there is not a single complete loss streak (NOT gated by MIN_SIGNALS).*`,
24663
+ `*Trend: classification computed FROM CLOSING PRICES OF CLOSED SIGNALS (one point per closed trade = the price at which it closed, with its close-timestamp — no candles, no order book, no tick stream). Bivariate gate: "sideways" when R² < 0.30 (regression too weak to call any direction). Otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size (slope detectable but smaller than the typical daily step). Otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate — never a single-axis if on a magic-constant slope magnitude. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS$2}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected.*`,
24664
+ `*Trend Strength: ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED SIGNALS (one point per closed trade in chronological order — no candles). UNITS: percent per day (small-slope approximation: log-return ≈ percent-return). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator at signal entry — a static fit over the entire stored history at report time. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} or the calendar span is degenerate.*`,
24665
+ `*Trend Confidence (R²): coefficient of determination of the same log-price regression that produces Trend Strength, computed on log(close) — NOT on absolute close. UNITS: dimensionless in [0, 1]. High R² = log-price moves linearly with time (clean trend); low R² = the series is noisy regardless of slope sign. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} or the calendar span is degenerate.*`,
24666
+ `*Buyer Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. (count of i where close[i] > close[i−1]) / (count of decisive moves), where "decisive" excludes flats (close[i] == close[i−1]). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — backtest data has no order book; "buyer" labels only the SIGN of the close-to-close return. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24667
+ `*Seller Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Seller Pressure = 1 − Buyer Pressure (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — labels only the SIGN of the close-to-close return. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24668
+ `*Buyer Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Σ |close[i] − close[i−1]| / close[i−1] over up-moves (close[i] > close[i−1]), divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pressure (count of up-moves) and Buyer Strength (sum of up-magnitudes) use the SAME close-to-close return series; only count vs magnitude differs. A divergence between them (e.g. Pressure 0.70 with Strength 0.45) means "many small up-moves, fewer but larger down-moves" — a regime asymmetry frequency alone hides. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24669
+ `*Seller Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Σ |close[i] − close[i−1]| / close[i−1] over down-moves (close[i] < close[i−1]), divided by the same sum over ALL decisive moves. By construction Seller Strength = 1 − Buyer Strength (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24670
+ `*Pressure Imbalance: DIFFERENCE, not ratio: Buyer Strength − Seller Strength, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Equivalent to (2 × Buyer Strength − 1). UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias (positive = bullish bias on magnitude); absolute value = how lopsided. Single signed scalar that compresses the strength pair into one number. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24671
+ `*Median Step Size: median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED SIGNALS — "step" = consecutive closes of CLOSED TRADES (NOT ticks, NOT bars of any timeframe — the report has no candle access). UNITS: percent, normalised by price → directly comparable across symbols at any price level. Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24672
+ `*General reliability note: per-trade ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS$2} closed signals because the underlying variance estimates are too noisy. Annualised metrics (Annualized Sharpe, Expected Yearly Returns, Calmar) additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS$2} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR$2} per year. 100+ signals are needed for statistical reliability of the ratios; annualised metrics assume the observed frequency and market regime persist year-round.*`,
24673
+ `*IMPORTANT: Total PNL and the equity-curve metrics (Expected Yearly Returns, Calmar, Recovery, and the equity max drawdown that feeds them) all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem (PositionSize / Kelly / ATR): per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging, the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position. If your strategy risks X% of capital per trade, the realised portfolio return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
24674
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing strategy (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
24378
24675
  ].join("\n");
24379
24676
  }
24380
24677
  /**
@@ -25004,6 +25301,15 @@ let ReportStorage$9 = class ReportStorage {
25004
25301
  avgConsecutiveLossPnl: null,
25005
25302
  avgWinDuration: null,
25006
25303
  avgLossDuration: null,
25304
+ medianStepSize: null,
25305
+ buyerPressure: null,
25306
+ sellerPressure: null,
25307
+ buyerStrength: null,
25308
+ sellerStrength: null,
25309
+ pressureImbalance: null,
25310
+ trend: null,
25311
+ trendStrength: null,
25312
+ trendConfidence: null,
25007
25313
  };
25008
25314
  }
25009
25315
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
@@ -25053,14 +25359,16 @@ let ReportStorage$9 = class ReportStorage {
25053
25359
  const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
25054
25360
  // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1).
25055
25361
  // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
25056
- // are too noisy to publish (high chance of spurious ±Sharpe).
25362
+ // are too noisy to publish (high chance of spurious ±Sharpe). When the gate fails the
25363
+ // standard deviation itself is reported as null (NOT 0) so the report doesn't suggest
25364
+ // a flat distribution for a small but variable sample.
25057
25365
  const canComputeRatios = returns.length >= MIN_SIGNALS_FOR_RATIOS$1;
25058
25366
  const stdDev = canComputeRatios
25059
25367
  ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (returns.length - 1))
25060
- : 0;
25368
+ : null;
25061
25369
  // STDDEV_EPSILON guard — protects against float-artifact stdDev from identical
25062
25370
  // returns producing spuriously astronomical sharpe.
25063
- const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$1
25371
+ const sharpeRatio = canComputeRatios && stdDev !== null && stdDev > STDDEV_EPSILON$1
25064
25372
  ? avgPnl / stdDev
25065
25373
  : null;
25066
25374
  // Annualize only when gate passes; otherwise null.
@@ -25302,6 +25610,14 @@ let ReportStorage$9 = class ReportStorage {
25302
25610
  const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
25303
25611
  ? null
25304
25612
  : Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, ((equityFinal - 1) * 100) / equityMaxDrawdown));
25613
+ // Price profile — buyer/seller pressure, trend classification. Built only
25614
+ // from closed events' (timestamp, currentPrice) pairs in chronological
25615
+ // close order — the same data the equity curve uses. N-gated internally
25616
+ // by the helper (MIN_SIGNALS = 10).
25617
+ const priceProfile = getPriceProfile(validClosed
25618
+ .slice()
25619
+ .sort((a, b) => a.timestamp - b.timestamp)
25620
+ .map((e) => ({ closeAt: e.timestamp, close: e.currentPrice })));
25305
25621
  return {
25306
25622
  eventList: this._eventList,
25307
25623
  totalEvents: this._eventList.length,
@@ -25328,6 +25644,15 @@ let ReportStorage$9 = class ReportStorage {
25328
25644
  avgConsecutiveLossPnl: isUnsafe$3(avgConsecutiveLossPnl) ? null : avgConsecutiveLossPnl,
25329
25645
  avgWinDuration: isUnsafe$3(avgWinDuration) ? null : avgWinDuration,
25330
25646
  avgLossDuration: isUnsafe$3(avgLossDuration) ? null : avgLossDuration,
25647
+ medianStepSize: priceProfile.medianStepSize,
25648
+ buyerPressure: priceProfile.buyerPressure,
25649
+ sellerPressure: priceProfile.sellerPressure,
25650
+ buyerStrength: priceProfile.buyerStrength,
25651
+ sellerStrength: priceProfile.sellerStrength,
25652
+ pressureImbalance: priceProfile.pressureImbalance,
25653
+ trend: priceProfile.trend,
25654
+ trendStrength: priceProfile.trendStrength,
25655
+ trendConfidence: priceProfile.trendConfidence,
25331
25656
  };
25332
25657
  }
25333
25658
  /**
@@ -25384,20 +25709,48 @@ let ReportStorage$9 = class ReportStorage {
25384
25709
  `**Avg Loss Duration:** ${stats.avgLossDuration === null ? "N/A" : `${stats.avgLossDuration.toFixed(1)} min`}`,
25385
25710
  `**Avg Consecutive Win PNL:** ${stats.avgConsecutiveWinPnl === null ? "N/A" : `${stats.avgConsecutiveWinPnl > 0 ? "+" : ""}${stats.avgConsecutiveWinPnl.toFixed(3)}% (higher is better)`}`,
25386
25711
  `**Avg Consecutive Loss PNL:** ${stats.avgConsecutiveLossPnl === null ? "N/A" : `${stats.avgConsecutiveLossPnl.toFixed(3)}% (closer to 0 is better)`}`,
25712
+ `**Trend:** ${stats.trend === null ? "N/A" : stats.trend}`,
25713
+ `**Trend Strength:** ${stats.trendStrength === null ? "N/A" : `${stats.trendStrength > 0 ? "+" : ""}${stats.trendStrength.toFixed(3)}%/day`}`,
25714
+ `**Trend Confidence (R²):** ${stats.trendConfidence === null ? "N/A" : stats.trendConfidence.toFixed(3)}`,
25715
+ `**Buyer Pressure:** ${stats.buyerPressure === null ? "N/A" : `${(stats.buyerPressure * 100).toFixed(1)}%`}`,
25716
+ `**Seller Pressure:** ${stats.sellerPressure === null ? "N/A" : `${(stats.sellerPressure * 100).toFixed(1)}%`}`,
25717
+ `**Buyer Strength:** ${stats.buyerStrength === null ? "N/A" : `${(stats.buyerStrength * 100).toFixed(1)}%`}`,
25718
+ `**Seller Strength:** ${stats.sellerStrength === null ? "N/A" : `${(stats.sellerStrength * 100).toFixed(1)}%`}`,
25719
+ `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
25720
+ `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
25387
25721
  "",
25388
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
25389
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
25390
- `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$1} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$1} days. Assumes returns are iid autocorrelated strategies are overstated.*`,
25391
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
25392
- `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
25393
- `*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS$1}% values above the cap return N/A.*`,
25394
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). Capped at ±${MAX_CALMAR_RATIO$1}.*`,
25395
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator and the mark-to-market max drawdown as denominator.*`,
25396
- `*Max Drawdown: mark-to-market the compounded equity curve applies each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery.*`,
25397
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
25398
- `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
25399
- `*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures these metrics represent a theoretical upper bound under full allocation.*`,
25400
- `*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies closer to zero is less bad, positive is profitable.*`,
25722
+ `*Win Rate: percent of closed events that ended with per-trade PNL > 0, computed as winning-event count / (winning-event count + losing-event count) × 100 — break-even closed events (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistical reliability: noisy below 30 closed events (a single streak can shift it 1020 points); stable above 200.*`,
25723
+ `*Average PNL: arithmetic mean of per-trade PNL across every closed event, computed as Σ per-trade PNL / closed event count. UNITS: percent per trade. Sign mirrors strategy edge — positive = profitable on average per trade. Sensitive to one whale trade; cross-check with Median PNL to detect skew.*`,
25724
+ `*Total PNL: arithmetic sum of per-trade PNL across every closed event. UNITS: percent. This is the additive total, NOT the compounded equity return — for the geometrically-compounded variant see Recovery Factor's numerator and Expected Yearly Returns. Useful as a quick scoreboard but ignores volatility drag.*`,
25725
+ `*Standard Deviation Per Trade: sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL across closed events. UNITS: percent. Measures volatility of realised returns. Denominator for per-trade Sharpe Ratio. Below STDDEV_EPSILON = 1e-9 it is treated as zero (identical-returns guard). Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25726
+ `*Sharpe Ratio: per-trade Sharpe = Average PNL / Standard Deviation Per Trade (risk-free rate = 0). UNITS: dimensionless ratio. Higher = better risk-adjusted return per trade. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
25727
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √(trades per year), where trades per year = closed event count × 365 / calendar span in days. UNITS: dimensionless. Null when the closed event count < ${MIN_SIGNALS_FOR_ANNUALIZATION$1}, OR calendar span < ${MIN_CALENDAR_SPAN_DAYS$1} days, OR raw frequency > ${MAX_TRADES_PER_YEAR$1}. Assumes returns are iid autocorrelated strategies are overstated.*`,
25728
+ `*Certainty Ratio: mean per-trade PNL over winning closed events, divided by the absolute value of the mean per-trade PNL over losing closed events. UNITS: dimensionless ratio. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, OR there are no losing events, OR the absolute mean losing PNL < 1e-9 (float-artifact loss guard).*`,
25729
+ `*Expected Yearly Returns: geometric annualisation of the equity curve: (final equity ^ (trades per year / closed event count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over all closed events in chronological close order. UNITS: percent per year. Accounts for volatility drag. Null under the same closed-event-count / calendar-span / frequency gates as Annualized Sharpe Ratio, AND null when |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS$1}%. −100% when the equity curve hits ≤ 0 (account blown).*`,
25730
+ `*Avg Peak PNL: arithmetic mean of each closed event's peak-PNL snapshot — the best mark-to-market PNL recorded while the position was open. Closed events that never recorded such a snapshot are excluded from both numerator and denominator (no zero dilution). UNITS: percent. Describes the typical best-case unrealised PNL during the position's lifetime, not the realised close. NOT gated by MIN_SIGNALS computed whenever at least one closed event carries the snapshot.*`,
25731
+ `*Avg Max Drawdown PNL: arithmetic mean of each closed event's trough-PNL snapshot the worst mark-to-market PNL recorded while the position was open. Closed events that never recorded such a snapshot are excluded (no zero dilution). UNITS: percent (negative for losing excursions). Describes the typical worst-case unrealised PNL during the position's lifetime; closer to 0 is better. NOT gated by MIN_SIGNALS — computed whenever at least one closed event carries the snapshot.*`,
25732
+ `*Sortino Ratio: Average PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / closed event count ) (canonical Sortino 1991: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, OR there are no losing events, OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
25733
+ `*Calmar Ratio: Expected Yearly Returns divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$1}. The denominator is the max drawdown of the compounded equity curve: each closed event's worst intra-trade excursion (the trough-PNL snapshot, 0) is applied before booking its realised close, so a position that dipped to −18% and recovered to +2% contributes a real drawdown rather than zero. UNITS: dimensionless. Rule of thumb: below 0.5 poor, 0.5–1.0 acceptable, above 1.0 strong. Null when Expected Yearly Returns is null OR the equity max drawdown 0.*`,
25734
+ `*Recovery Factor: (final equity 1) × 100 divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$1}. The numerator is the compounded total return — NOT the arithmetic Total PNL shown above; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = profit doesn't cover drawdown; above 3.0 = good. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, the equity curve blew up (account ≤ 0), or the equity max drawdown ≤ 0.*`,
25735
+ `*Expectancy: per-trade expected value = (winning-event count / closed event count) × mean winning PNL + (losing-event count / closed event count) × mean losing PNL. Break-even events contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25736
+ `*Median PNL: median of per-trade PNL across all closed events (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers — comparing it with Average PNL reveals distribution skew. A large gap (e.g. avg +1.5%, median +0.2%) means one or two events carry the arithmetic mean. NOT gated by MIN_SIGNALS — computed whenever the closed event count ≥ 1.*`,
25737
+ `*Avg Duration: arithmetic mean of (close-time − pending-time) / 60_000 over all closed events. UNITS: minutes. Describes the typical position hold time. Closed events that have no pending timestamp fall back to using their own close timestamp (yielding a zero-duration contribution). NOT gated by MIN_SIGNALS — computed whenever the closed event count ≥ 1.*`,
25738
+ `*Avg Win Duration: arithmetic mean of (close-time − pending-time) / 60_000 restricted to winning closed events (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning events (NOT gated by MIN_SIGNALS). Pair with Avg Loss Duration to detect the classic asymmetry "let winners run, cut losers short" versus the inverse "cut winners short, let losers run".*`,
25739
+ `*Avg Loss Duration: arithmetic mean of (close-time − pending-time) / 60_000 restricted to losing closed events (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing events (NOT gated by MIN_SIGNALS).*`,
25740
+ `*Avg Consecutive Win PNL: a "win streak" is a run of consecutive closed events with per-trade PNL > 0 bounded by either a losing or break-even event; this metric sums per-trade PNL within each streak and then averages those sums. Events are walked in chronological close order. UNITS: percent per streak. Higher = winning streaks are typically bigger. Null only when there is not a single complete win streak (NOT gated by MIN_SIGNALS).*`,
25741
+ `*Avg Consecutive Loss PNL: a "loss streak" is a run of consecutive closed events with per-trade PNL < 0 bounded by either a winning or break-even event; this metric sums per-trade PNL within each streak and then averages those sums. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. Null only when there is not a single complete loss streak (NOT gated by MIN_SIGNALS).*`,
25742
+ `*Trend: classification computed FROM CLOSING PRICES OF CLOSED EVENTS (one point per closed event = the price at which it closed, with its close-timestamp; the live mode ingests sub-minute events of other kinds but the price profile uses ONLY closed-event prices — no candles, no order book, no tick stream). Bivariate gate: "sideways" when R² < 0.30. Otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size. Otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate — never a single-axis if on a magic-constant slope magnitude. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS$1}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected.*`,
25743
+ `*Trend Strength: ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED EVENTS in chronological order. UNITS: percent per day (small-slope approximation). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator at signal entry — a static fit over the entire stored history at report time. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} or the calendar span is degenerate.*`,
25744
+ `*Trend Confidence (R²): coefficient of determination of the same log-price regression that produces Trend Strength, computed on log(close) — NOT on absolute close. UNITS: dimensionless in [0, 1]. High R² = log-price moves linearly with time (clean trend); low R² = the series is noisy regardless of slope sign. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} or the calendar span is degenerate.*`,
25745
+ `*Buyer Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. (count of i where close[i] > close[i−1]) / (count of decisive moves), where "decisive" excludes flats. UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — the live ingest does not separate buy/sell side; "buyer" labels only the SIGN of the close-to-close return. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25746
+ `*Seller Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Seller Pressure = 1 − Buyer Pressure (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25747
+ `*Buyer Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. Σ |close[i] − close[i−1]| / close[i−1] over up-moves, divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pressure (count of up-moves) and Buyer Strength (sum of up-magnitudes) use the SAME close-to-close return series; only count vs magnitude differs. A divergence between them (e.g. Pressure 0.70 with Strength 0.45) means "many small up-moves, fewer but larger down-moves". Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25748
+ `*Seller Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. Σ |close[i] − close[i−1]| / close[i−1] over down-moves, divided by the same sum over ALL decisive moves. By construction Seller Strength = 1 − Buyer Strength (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25749
+ `*Pressure Imbalance: DIFFERENCE, not ratio: Buyer Strength − Seller Strength, computed FROM CLOSING PRICES OF CLOSED EVENTS. Equivalent to (2 × Buyer Strength − 1). UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias (positive = bullish bias on magnitude); absolute value = how lopsided. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25750
+ `*Median Step Size: median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED EVENTS — "step" = consecutive closed-event prices (NOT ticks, NOT bars; the report has no candle access even though the live mode ingests sub-minute events — they are not used here). UNITS: percent, normalised by price → directly comparable across symbols at any price level. Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25751
+ `*General reliability note: per-trade ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS$1} closed events. Annualised metrics additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS$1} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR$1} per year. 100+ closed events are needed for statistical reliability of the ratios.*`,
25752
+ `*IMPORTANT: Total PNL and the equity-curve metrics (Expected Yearly Returns, Calmar, Recovery, and the equity max drawdown that feeds them) all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem: per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging the cost basis is the sum of all entries and the entry price is dollar-cost-weighted. If your strategy risks X% of capital per trade, the realised portfolio return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
25753
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing strategy (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
25401
25754
  ].join("\n");
25402
25755
  }
25403
25756
  /**
@@ -25952,7 +26305,14 @@ let ReportStorage$8 = class ReportStorage {
25952
26305
  `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
25953
26306
  `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`,
25954
26307
  "",
25955
- `*Activation / Cancellation rates are computed over scheduled signals whose outcome (opened or cancelled) is also in the buffer matched by signalId. "Scheduled signals (raw)" above is the unmatched count and may include records whose outcome has not yet arrived or was evicted from the buffer.*`
26308
+ `*Total events: integer count of every scheduled / opened / cancelled event in the ring buffer (capped at CC_MAX_SCHEDULE_MARKDOWN_ROWS, newest-first; oldest entries are dropped past the cap). UNITS: count.*`,
26309
+ `*Scheduled signals (raw): count of events whose action is "scheduled" in the buffer. UNITS: count. NOTE: this is the unmatched raw count — it may include records whose outcome (opened/cancelled) has not yet arrived or was evicted from the buffer. Activation / Cancellation rates do NOT use this denominator (see below).*`,
26310
+ `*Opened signals: count of events whose action is "opened" in the buffer. UNITS: count.*`,
26311
+ `*Cancelled signals: count of events whose action is "cancelled" in the buffer. UNITS: count.*`,
26312
+ `*Activation rate: matched-opened count / matched-resolved count × 100, where matched-opened = number of "opened" events whose signal id also appears among the buffer's "scheduled" events, matched-cancelled = same for "cancelled" events, and matched-resolved = matched-opened + matched-cancelled. The signal-id match guarantees the numerator and denominator come from the SAME population — a sliding window of CC_MAX_SCHEDULE_MARKDOWN_ROWS can otherwise drop a "scheduled" record before its outcome arrives, inflating rates above 100% or firing one rate without the other. UNITS: percent in [0, 100]. Null when matched-resolved = 0 (no matched outcomes in the buffer).*`,
26313
+ `*Cancellation rate: matched-cancelled count / matched-resolved count × 100 — same signal-id-matched denominator as Activation rate. UNITS: percent in [0, 100]. Null when matched-resolved = 0. By construction Activation rate + Cancellation rate = 100% (or both null together).*`,
26314
+ `*Average activation time: arithmetic mean of duration over events whose action is "opened" AND whose duration is a number. Events without a numeric duration are excluded from both numerator and denominator (no zero dilution). UNITS: minutes. Null when no "opened" event carries a numeric duration. The duration on an opened event represents wait time from the moment the signal was scheduled until the moment it activated (became pending).*`,
26315
+ `*Average wait time (cancelled): arithmetic mean of duration over events whose action is "cancelled" AND whose duration is a number. Same exclusion rule as Average activation time. UNITS: minutes. Null when no "cancelled" event carries a numeric duration. The duration on a cancelled event represents wait time from the moment the signal was scheduled until cancellation.*`,
25956
26316
  ].join("\n");
25957
26317
  }
25958
26318
  /**
@@ -26435,8 +26795,6 @@ class PerformanceStorage {
26435
26795
  "",
26436
26796
  summaryTable,
26437
26797
  "",
26438
- "**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.",
26439
- "",
26440
26798
  `**Total events:** ${stats.totalEvents}`,
26441
26799
  `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
26442
26800
  `**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
@@ -26445,6 +26803,17 @@ class PerformanceStorage {
26445
26803
  "",
26446
26804
  percentages.join("\n"),
26447
26805
  "",
26806
+ `*Total events: integer count of every performance event in the ring buffer (capped at CC_MAX_PERFORMANCE_MARKDOWN_ROWS, newest-first; oldest entries are dropped past the cap). UNITS: count.*`,
26807
+ `*Total execution time: arithmetic sum of every event's duration in the buffer. UNITS: milliseconds.*`,
26808
+ `*Number of metric types: count of distinct metric-type labels present in the buffer. UNITS: count.*`,
26809
+ `*Time Distribution (per metric): the metric's Total Duration divided by overall Total Execution Time × 100; rendered as a percent next to that metric's total duration in milliseconds. Guard: when the overall execution time is 0 (all-instant operations) the percent is forced to 0 instead of NaN. UNITS: percent of total execution time.*`,
26810
+ `*Count (table column): integer number of events of this metric type in the buffer.*`,
26811
+ `*Total Duration (table column): sum of every event's duration for that metric type. UNITS: milliseconds.*`,
26812
+ `*Avg Duration (table column): Total Duration divided by Count for that metric type. UNITS: milliseconds.*`,
26813
+ `*Min / Max Duration (table columns): smallest / largest single-event duration in the metric-type bucket. UNITS: milliseconds.*`,
26814
+ `*Std Dev (table column): sample standard deviation (Bessel-corrected, N−1 denominator) of single-event durations for that metric type. UNITS: milliseconds. Zero when the bucket has only one event (no variance defined).*`,
26815
+ `*Median / P95 / P99 (table columns): percentiles of the sorted single-event durations using linear interpolation between adjacent ranks — equivalent to numpy.percentile with the default linear method. P95 / P99 represent the 95th / 99th percentile response times (tail latency). UNITS: milliseconds. Buckets of size 0 fall back to 0; size 1 returns the single value for every percentile.*`,
26816
+ `*Avg / Min / Max Wait Time (table columns): wait time = (event's timestamp − the previous event's timestamp) for events where a previous-timestamp value is recorded; this captures the interval between consecutive events of the same metric type. Mean / smallest / largest of those gaps. UNITS: milliseconds. All zero when no event in the bucket carries a previous-timestamp value.*`,
26448
26817
  ].join("\n");
26449
26818
  }
26450
26819
  /**
@@ -26904,7 +27273,17 @@ let ReportStorage$7 = class ReportStorage {
26904
27273
  "",
26905
27274
  await this.getPnlTable(pnlColumns),
26906
27275
  "",
26907
- "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
27276
+ "",
27277
+ `*Optimization Metric: the name of the per-strategy statistic that this walker run ranked strategies by. The "Best ${results.metric}" value below is the largest value of that statistic among all tested strategies (or, if the statistic is one for which "lower is better", the smallest); the strategy that produced it is reported as Best Strategy. Strategies whose ${results.metric} is N/A are skipped from "best" selection — a missing value never wins.*`,
27278
+ `*Strategies Tested: integer count of distinct strategies that were run during this walker comparison. UNITS: count.*`,
27279
+ `*Total Signals (best strategy): integer count of closed signals produced by the best strategy during this walker run.*`,
27280
+ `*Sortino Ratio (best strategy summary): the best strategy's Average PNL divided by downside deviation (risk-free rate = 0). Downside deviation = √( Σ min(0, per-trade PNL)² / closed signal count ) — squared sums computed over the strategy's closed signals and divided by the total signal count (canonical Sortino 1991, MAR = 0). UNITS: dimensionless. N/A when the best strategy has fewer than 10 closed signals, when it has no losing trades, or when downside deviation is below a float-artifact threshold (~1e-9). Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong.*`,
27281
+ `*Calmar Ratio (best strategy summary): the best strategy's Expected Yearly Returns divided by its equity max drawdown, clamped to ±1000. Expected Yearly Returns is the geometric annualisation of the strategy's compounded equity curve; the equity max drawdown is the mark-to-market max drawdown of that same curve (each trade's worst intra-trade excursion is applied as a trough before booking the realised close, so deep round-trip dips count rather than only close-to-close drops). UNITS: dimensionless. N/A when Expected Yearly Returns is N/A (e.g. too few signals, calendar span too short, or trade frequency too clustered to extrapolate) or when the equity max drawdown ≤ 0. Rule of thumb: below 0.5 poor, above 1.0 strong.*`,
27282
+ `*Recovery Factor (best strategy summary): the best strategy's (final equity − 1) × 100 divided by its equity max drawdown, clamped to ±1000. The numerator is the strategy's compounded total return (the equity curve's end value minus its start, in percent) — NOT the arithmetic sum of per-trade PNLs; mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown described under Calmar Ratio. UNITS: dimensionless. N/A when the best strategy has fewer than 10 closed signals, the strategy blew its account (equity ≤ 0), or the equity max drawdown ≤ 0. Rule of thumb: below 1.0 means profit doesn't cover drawdown, above 3.0 is good.*`,
27283
+ `*Top Strategies Comparison (table): up to N strategies (newest-best first) ranked by the optimization metric, where N is the configured top-N limit for this report. Each row shows that strategy's full statistical summary through the configured comparison columns.*`,
27284
+ `*All Signals (PNL Table): every closed signal produced by every tested strategy during the run, in storage order (newest first). Each row shows one closed trade — entry / exit prices, realised PNL percentage and cost, peak / trough excursions, close reason, durations — through the configured PNL columns.*`,
27285
+ "",
27286
+ "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better). The per-strategy ratios shown above (Sortino, Calmar, Recovery) are forwarded verbatim from each strategy's own backtest statistics — Walker only ranks and surfaces those values, it does not recompute them. Their formulas, null-conditions and statistical gates are spelled out in the individual legend entries above."
26908
27287
  ].join("\n");
26909
27288
  }
26910
27289
  /**
@@ -27679,6 +28058,12 @@ class HeatmapStorage {
27679
28058
  calmarRatio = null;
27680
28059
  if (isUnsafe(recoveryFactor))
27681
28060
  recoveryFactor = null;
28061
+ // Price profile — buyer/seller pressure, trend classification. Built from
28062
+ // the chronological close-price series of this symbol's closed signals.
28063
+ // N-gated internally by the helper (MIN_SIGNALS = 10).
28064
+ const priceProfile = getPriceProfile([...signals]
28065
+ .sort((a, b) => a.closeTimestamp - b.closeTimestamp)
28066
+ .map((s) => ({ closeAt: s.closeTimestamp, close: s.currentPrice })));
27682
28067
  return {
27683
28068
  symbol,
27684
28069
  totalPnl,
@@ -27713,6 +28098,15 @@ class HeatmapStorage {
27713
28098
  certaintyRatio,
27714
28099
  expectedYearlyReturns,
27715
28100
  tradesPerYear,
28101
+ medianStepSize: priceProfile.medianStepSize,
28102
+ buyerPressure: priceProfile.buyerPressure,
28103
+ sellerPressure: priceProfile.sellerPressure,
28104
+ buyerStrength: priceProfile.buyerStrength,
28105
+ sellerStrength: priceProfile.sellerStrength,
28106
+ pressureImbalance: priceProfile.pressureImbalance,
28107
+ trend: priceProfile.trend,
28108
+ trendStrength: priceProfile.trendStrength,
28109
+ trendConfidence: priceProfile.trendConfidence,
27716
28110
  };
27717
28111
  }
27718
28112
  /**
@@ -28156,27 +28550,69 @@ class HeatmapStorage {
28156
28550
  "",
28157
28551
  table,
28158
28552
  "",
28159
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
28160
- `*Pooled Sharpe: Sharpe computed over all trades across symbols treated as one sample. NOT a Markowitz portfolio Sharpe ignores cross-symbol correlations and capital allocation. N/A unless ≥${MIN_SIGNALS_FOR_RATIOS} pooled trades.*`,
28161
- `*Annualized Sharpe: per-trade Sharpe × √tradesPerYear. N/A unless the underlying Sharpe and tradesPerYear are both available (≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals, span ≥${MIN_CALENDAR_SPAN_DAYS} days, raw frequency ≤${MAX_TRADES_PER_YEAR}). Assumes returns are iid autocorrelated strategies are overstated.*`,
28162
- `*Certainty Ratio: avgWin / |avgLoss|. Below 1.0 means average loss exceeds average win. Above 1.5 is considered good. N/A when no losing trades or |avgLoss| is sub-epsilon.*`,
28163
- `*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS}% values above the cap return N/A.*`,
28164
- `*Trades Per Year: observed trade frequency extrapolated to one year (signals × 365 / calendarSpanDays). N/A when too few signals or too short a calendar span; also null when the raw frequency exceeds ${MAX_TRADES_PER_YEAR} (too clustered for reliable annualization).*`,
28165
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals per symbol.*`,
28166
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
28167
- `*Profit Factor: below 1.0 means strategy is losing overall. Above 1.5 is considered good.*`,
28168
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals per symbol and span ≥${MIN_CALENDAR_SPAN_DAYS} days. Capped at ±${MAX_CALMAR_RATIO}.*`,
28169
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator and the mark-to-market max drawdown as denominator.*`,
28170
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
28171
- `*Median PNL: middle value of the pnl distribution. Robust to outliers; compare to Average PNL a large gap signals a skewed distribution (e.g. one whale trade dragging the mean).*`,
28172
- `*Avg Peak PNL / Avg Max Drawdown PNL: mean of per-trade _peak.pnlPercentage / _fall.pnlPercentage. Higher avg-peak with deeper avg-drawdown means strategy needs to tolerate bigger swings to capture the upside.*`,
28173
- `*Peak Profit PNL / Max Drawdown PNL: extremes the best best-case and worst worst-case observed across all trades. Tail behaviour the averages hide.*`,
28174
- `*Avg Duration / Avg Win Duration / Avg Loss Duration: mean hold time in minutes (closeTimestamp - pendingAt). Winner-shorter-than-loser is a red flag ("cut winners short, let losers run").*`,
28175
- `*Avg Consecutive Win/Loss PNL: average sum of pnlPercentage across consecutive streaks. Pairs with max streak length to show the typical (not worst-case) streak magnitude. Portfolio uses trade-count-weighted mean of per-symbol streak averages — concatenating streaks across symbols would be meaningless (different markets, different timeframes).*`,
28176
- `*Max Drawdown: mark-to-market both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery. The pooled curve walks trades chronologically by closeTimestamp; simultaneous cross-symbol drawdowns within the same minute are still serialised (one trade applied at a time), so genuine same-instant tail correlation is not modelled.*`,
28177
- `*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
28178
- `*IMPORTANT: Per-symbol equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized return / drawdown will be roughly X/100 of the reported figures these metrics represent a theoretical upper bound under full allocation.*`,
28179
- `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies closer to zero is less bad, positive is profitable.*`,
28553
+ `*Total Symbols: count of distinct symbol buckets currently tracked in this storage. Each bucket holds at most CC_MAX_HEATMAP_MARKDOWN_ROWS closed signals (newest-first ring buffer); when capacity is reached the oldest signal is dropped. UNITS: integer count of symbols.*`,
28554
+ `*Portfolio PNL: arithmetic sum of per-trade PNL across every closed signal of every tracked symbol (Σ per-symbol Total PNL over symbols whose Total PNL is non-null). UNITS: percent. Additive total — NOT the compounded equity return. Useful as a quick scoreboard; ignores volatility drag and cross-symbol diversification.*`,
28555
+ `*Pooled Sharpe: pooled mean per-trade PNL divided by the pooled sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL, computed over the POOLED set of every closed signal of every symbol treated as one sample. UNITS: dimensionless ratio. NOT a Markowitz portfolio Sharpe — ignores cross-symbol correlations and capital allocation. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS} OR pooled standard deviation ≤ 1e-9. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong.*`,
28556
+ `*Annualized Sharpe (portfolio): Pooled Sharpe × √(portfolio Trades Per Year). UNITS: dimensionless. Null when Pooled Sharpe is null OR portfolio Trades Per Year is null OR ≤ 0. Assumes returns are iid autocorrelated strategies are overstated.*`,
28557
+ `*Certainty Ratio (portfolio): pooledAvgWin / |pooledAvgLoss| where pooledAvgWin = mean over pooled winning closed signals and pooledAvgLoss = mean over pooled losing closed signals. UNITS: dimensionless. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when pooled N < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades in the pool, OR |pooledAvgLoss| < 1e-9.*`,
28558
+ `*Expected Yearly Returns (portfolio): geometric annualisation of the pooled equity curve: (pooled final equity ^ (portfolio Trades Per Year / pooled trade count) 1) × 100, where the pooled equity curve walks every closed signal of every symbol chronologically by close-time and compounds (1 + per-trade PNL / 100). UNITS: percent per year. Null when the pooled calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, raw portfolio Trades Per Year > ${MAX_TRADES_PER_YEAR}, or |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS}%. −100% if the pooled equity curve hits ≤ 0.*`,
28559
+ `*Trades Per Year (portfolio): pooled trade count × 365 / pooled span in days, where pooled span = (latest close-time − earliest pending-time across the whole pool) / day. UNITS: trades / year. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR pooled span < ${MIN_CALENDAR_SPAN_DAYS} days, OR raw value > ${MAX_TRADES_PER_YEAR} (sample too clustered to extrapolate reliably). NOTE: portfolio Trades Per Year inherits the ratios-block sample-size gate (MIN_SIGNALS_FOR_RATIOS) on the pooled count, whereas the per-symbol Trades Per Year column uses the annualisation-block gate (MIN_SIGNALS_FOR_ANNUALIZATION) on the per-symbol count. The two constants are equal (${MIN_SIGNALS_FOR_RATIOS}) in this build, so this never produces a discrepancy in practice; should they ever diverge, portfolio Trades Per Year would surface earlier than per-symbol Trades Per Year, which is consistent with the rest of the portfolio block already being scoped under the ratios gate.*`,
28560
+ `*Total Trades (portfolio = portfolioTotalTrades): sum of per-symbol totalTrades over all tracked symbols. UNITS: integer count.*`,
28561
+ `*Avg Peak PNL (portfolio): trade-count-weighted mean of per-symbol Avg Peak PNL over symbols whose value is non-null. Weights are per-symbol Total Trades; symbols without any peak-snapshot signals don't contribute and don't dilute. UNITS: percent. Describes the portfolio's typical best-case unrealised excursion. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Avg Peak PNL is null.*`,
28562
+ `*Avg Max Drawdown PNL (portfolio): trade-count-weighted mean of per-symbol Avg Max Drawdown PNL over symbols whose value is non-null. UNITS: percent (negative for losing excursions). Closer to 0 is better describes the portfolio's typical worst-case unrealised excursion. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Avg Max Drawdown PNL is null.*`,
28563
+ `*Peak Profit PNL (portfolio): MAX of per-symbol Peak Profit PNL over all symbols whose value is non-null. UNITS: percent. The single best best-case excursion observed anywhere across the portfolio (tail behaviour the average hides). NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Peak Profit PNL is null.*`,
28564
+ `*Max Drawdown PNL (portfolio): MIN of per-symbol Max Drawdown PNL over all symbols whose value is non-null. UNITS: percent (negative). The single deepest unrealised dip observed anywhere across the portfolio. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Max Drawdown PNL is null.*`,
28565
+ `*Median PNL (portfolio): median of per-trade PNL over the POOLED set of every closed signal of every symbol (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers; compare with the pooled mean per-trade PNL that underlies Pooled Sharpe to detect distribution skew (a large gap means one or two trades drag the arithmetic mean). NOT gated by MIN_SIGNALS — computed whenever pooled trade count 1.*`,
28566
+ `*Avg Duration (portfolio): arithmetic mean of (close-time − pending-time) / 60_000 over the pooled set of every closed signal of every symbol that has both timestamps. UNITS: minutes. NOT gated by MIN_SIGNALS computed whenever at least one closed signal across the pool carries valid timestamps.*`,
28567
+ `*Avg Win Duration (portfolio): mean hold time in minutes restricted to pooled winning closed signals (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning trades across the pool (NOT gated by MIN_SIGNALS).*`,
28568
+ `*Avg Loss Duration (portfolio): mean hold time in minutes restricted to pooled losing closed signals (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing trades across the pool (NOT gated by MIN_SIGNALS). Pair with Avg Win Duration to detect the asymmetry "winner-shorter-than-loser" (a red flag: cut winners short, let losers run).*`,
28569
+ `*Avg Consecutive Win PNL (portfolio): trade-count-weighted mean of per-symbol Avg Consecutive Win PNL over symbols whose value is non-null. A per-symbol "win streak" is a run of consecutive same-symbol closed signals with per-trade PNL > 0 bounded by a losing or break-even trade; the per-symbol metric averages the SUM of per-trade PNL within each streak. The portfolio aggregate weights by per-symbol Total Trades — concatenating streaks ACROSS symbols would be meaningless (different markets, different timeframes). UNITS: percent per streak. NOT gated by MIN_SIGNALS at portfolio level — null only when every per-symbol Avg Consecutive Win PNL is null (i.e. no symbol has a single complete win streak).*`,
28570
+ `*Avg Consecutive Loss PNL (portfolio): trade-count-weighted mean of per-symbol Avg Consecutive Loss PNL over symbols whose value is non-null. Same weighting rationale as the Win-streak counterpart above. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. NOT gated by MIN_SIGNALS at portfolio level null only when every per-symbol Avg Consecutive Loss PNL is null.*`,
28571
+ `*Standard Deviation Per Trade (portfolio): sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL over the pooled set of every closed signal of every symbol. UNITS: percent. Denominator for Pooled Sharpe. Below 1e-9 it is treated as zero (identical-returns guard). Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}.*`,
28572
+ `*Sortino Ratio (portfolio): pooled-mean per-trade PNL divided by pooled downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / pooled trade count ) over the pool (canonical Sortino: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades in the pool, OR pooled downside deviation 1e-9 (float-artifact guard).*`,
28573
+ `*Calmar Ratio (portfolio): portfolio Expected Yearly Returns divided by the pooled mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO}. The denominator is the max drawdown of the POOLED equity curve walked chronologically by close-timestamp across every symbol — each closed signal's worst intra-trade excursion (its trough-PNL snapshot, ≤ 0) is applied before booking its realised close, so deep round-trip dips count. Cross-symbol simultaneous drawdowns within the same minute are still serialised (one signal applied at a time), so genuine same-instant tail correlation is not modelled. UNITS: dimensionless. Null when the portfolio Expected Yearly Returns is null OR the pooled equity max drawdown ≤ 0.*`,
28574
+ `*Recovery Factor (portfolio): (pooled final equity − 1) × 100 divided by the pooled equity max drawdown, clamped to ±${MAX_CALMAR_RATIO}. The numerator is the compounded total return of the pooled equity curve — NOT the arithmetic Portfolio PNL shown at the top of this report; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same pooled mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = compounded profit doesn't cover max drawdown; above 3.0 = good. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, the pooled equity curve blew up (reached ≤ 0), or the pooled equity max drawdown ≤ 0.*`,
28575
+ `*Expectancy (portfolio): (pooled winning-trade count / pooled trade count) × pooled mean of winning PNLs + (pooled losing-trade count / pooled trade count) × pooled mean of losing PNLs. Break-even closed signals contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade across the portfolio. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR when the entire pool is break-even (no wins AND no losses).*`,
28576
+ ``,
28577
+ `*--- Per-symbol table columns ---*`,
28578
+ `*Total PNL (column): arithmetic sum of per-trade PNL across that symbol's stored closed signals. UNITS: percent. Additive total — NOT a compounded equity return. Null only when that symbol has zero stored signals.*`,
28579
+ `*Total Trades (column): integer count of that symbol's stored closed signals. UNITS: count.*`,
28580
+ `*Win Rate (column): per-symbol winning-trade count / (winning-trade count + losing-trade count) × 100; break-even trades (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistically noisy below 30 per-symbol signals; stable above 200. NOT gated by MIN_SIGNALS — null only when there are no decisive trades for that symbol.*`,
28581
+ `*Avg PNL (column): arithmetic mean of per-trade PNL across that symbol's signals — Σ per-trade PNL / Total Trades. UNITS: percent per trade. Null only when Total Trades = 0.*`,
28582
+ `*Standard Deviation (column): per-symbol sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL. UNITS: percent. Denominator for per-symbol Sharpe Ratio. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}.*`,
28583
+ `*Avg Win (column): arithmetic mean of per-trade PNL restricted to that symbol's winning trades (per-trade PNL > 0). UNITS: percent. NOT gated by MIN_SIGNALS — null only when that symbol has no winning trades.*`,
28584
+ `*Avg Loss (column): arithmetic mean of per-trade PNL restricted to that symbol's losing trades (per-trade PNL < 0). UNITS: percent (negative). NOT gated by MIN_SIGNALS — null only when that symbol has no losing trades.*`,
28585
+ `*Max Win Streak (column): longest run of consecutive same-symbol closed signals with per-trade PNL > 0, bounded by an opposite-sign or break-even trade. UNITS: integer count of consecutive wins. The streak structure is sign-invariant under reversal, so the count is identical whether signals are walked in chronological or reverse-chronological order. NOT gated by MIN_SIGNALS — 0 when that symbol has no winning trades.*`,
28586
+ `*Max Loss Streak (column): longest run of consecutive same-symbol closed signals with per-trade PNL < 0. UNITS: integer count. Same iteration rules as Max Win Streak. NOT gated by MIN_SIGNALS — 0 when that symbol has no losing trades.*`,
28587
+ `*Median PNL (column): per-symbol median of per-trade PNL (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers; compare with Avg PNL to detect distribution skew. NOT gated by MIN_SIGNALS — null only when Total Trades = 0.*`,
28588
+ `*Sharpe Ratio (column): per-symbol Avg PNL / Standard Deviation (risk-free rate = 0). UNITS: dimensionless. Below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
28589
+ `*Annualized Sharpe (column): per-symbol Sharpe Ratio × √(per-symbol Trades Per Year), where Trades Per Year for that symbol = its trade count × 365 / its calendar span in days. UNITS: dimensionless. Null when per-symbol Sharpe Ratio is null, OR that symbol's Trades Per Year is null (requires trade count ≥ ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days, raw frequency ≤ ${MAX_TRADES_PER_YEAR}). Assumes iid returns.*`,
28590
+ `*Certainty Ratio (column): per-symbol Avg Win / |Avg Loss|. UNITS: dimensionless. Below 1.0 = typical loss exceeds typical win; above 1.5 generally good. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR Avg Win / Avg Loss missing, OR Avg Loss ≥ 0, OR |Avg Loss| < 1e-9 (float-artifact loss guard).*`,
28591
+ `*Expected Yearly Returns (column): per-symbol geometric annualisation of that symbol's equity curve — (final equity ^ (Trades Per Year / trade count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over that symbol's signals in chronological close order. UNITS: percent per year. Null when trade count < ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, raw frequency > ${MAX_TRADES_PER_YEAR}, or |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS}%. −100% when the symbol's equity curve hits ≤ 0.*`,
28592
+ `*Trades Per Year (column): per-symbol trade count × 365 / calendar span in days, where calendar span = (latest close-time − earliest pending-time for that symbol) / day. UNITS: trades / year. Null when trade count < ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, or raw value > ${MAX_TRADES_PER_YEAR}.*`,
28593
+ `*Profit Factor (column): per-symbol Σ winning per-trade PNL / |Σ losing per-trade PNL|. UNITS: dimensionless. Below 1.0 means the symbol is net losing; above 1.5 is generally good. Null when there are no winning or no losing trades for that symbol, or when Σ|losing PNL| < 1e-9 (float-artifact guard).*`,
28594
+ `*Sortino Ratio (column): per-symbol Avg PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / total trade count ) (canonical Sortino: MAR = 0, divide by N_total). UNITS: dimensionless. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades, OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
28595
+ `*Calmar Ratio (column): per-symbol Expected Yearly Returns / Max Drawdown, clamped to ±${MAX_CALMAR_RATIO}. Denominator is the mark-to-market max drawdown of that symbol's compounded equity curve. Null when Expected Yearly Returns is null (requires ≥ ${MIN_SIGNALS_FOR_ANNUALIZATION} signals and a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days for that symbol) OR Max Drawdown ≤ 0.*`,
28596
+ `*Recovery Factor (column): per-symbol (final equity − 1) × 100 / Max Drawdown, clamped to ±${MAX_CALMAR_RATIO}. The numerator is the compounded total return of that symbol's equity curve. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, the equity curve blew up (reached ≤ 0), or Max Drawdown ≤ 0.*`,
28597
+ `*Expectancy (column): per-symbol expected value per trade. Three cases depending on what kinds of trades exist for that symbol: (a) BOTH winning and losing trades present → (winning-trade count / Total Trades) × Avg Win + (losing-trade count / Total Trades) × Avg Loss; (b) only winning trades present → (winning-trade count / Total Trades) × Avg Win (zero contribution from non-existent losses); (c) only losing trades present → (losing-trade count / Total Trades) × Avg Loss. Break-even trades contribute 0 (excluded from both probabilities). UNITS: percent per trade. NOT gated by MIN_SIGNALS — computed whenever Total Trades ≥ 1 and at least one decisive trade exists. (Note: the portfolio-level Expectancy further up in this report uses a single combined formula and IS gated by MIN_SIGNALS_FOR_RATIOS over the pooled count; per-symbol Expectancy is intentionally looser to populate the row early.)*`,
28598
+ `*Max Drawdown (column): per-symbol mark-to-market max drawdown — the symbol's compounded equity curve applies each closed signal's worst intra-trade excursion (its trough-PNL snapshot, ≤ 0) before booking the realised close, so deep round-trip dips count rather than only realised close-to-close drops. UNITS: percent. NOT realised-only.*`,
28599
+ `*Avg Peak PNL / Avg Max Drawdown PNL (columns): per-symbol arithmetic means of each closed signal's peak-PNL / trough-PNL snapshot — the best / worst mark-to-market PNL recorded while the position was open. Signals that never recorded the snapshot are excluded — no zero dilution. UNITS: percent. NOT gated by MIN_SIGNALS — each is null only if no signal for that symbol carries the corresponding snapshot.*`,
28600
+ `*Peak Profit PNL / Max Drawdown PNL (columns): per-symbol MAX of the peak-PNL snapshot / MIN of the trough-PNL snapshot across the symbol's stored closed signals. UNITS: percent. The single best best-case and worst worst-case excursions for that symbol — tail behaviour the averages hide. NOT gated by MIN_SIGNALS — each is null only if no signal for that symbol carries the corresponding snapshot.*`,
28601
+ `*Avg Duration / Avg Win Duration / Avg Loss Duration (columns): per-symbol mean hold time in minutes — (close-time − pending-time) / 60_000 — over all signals / winning signals / losing signals respectively. UNITS: minutes. NOT gated by MIN_SIGNALS — each is null only if the corresponding bucket (all / wins / losses) is empty for that symbol. Pair Avg Win vs Avg Loss durations to detect "let winners run, cut losers short" (Avg Win Duration > Avg Loss Duration is healthy).*`,
28602
+ `*Avg Consecutive Win PNL / Avg Consecutive Loss PNL (columns): a per-symbol "streak" is a run of consecutive same-symbol closed signals with same-sign per-trade PNL bounded by an opposite-sign or break-even trade; each metric sums per-trade PNL within each streak and averages those sums. The streak structure is sign-invariant under iteration order, so the resulting average is the same whether signals are walked chronologically or in reverse. UNITS: percent per streak. NOT gated by MIN_SIGNALS — each is null only if not a single complete streak of the corresponding sign exists in that symbol's history. Pair with Max Win Streak / Max Loss Streak to compare typical vs worst-case streak magnitude.*`,
28603
+ `*Trend (column, per-symbol only): bivariate classification computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol (one point per closed trade = the price at which it closed, with its close-timestamp — no candles, no order book, no tick stream). "sideways" when R² < 0.30; otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size; otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate, never a single magic constant on slope magnitude. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected. Not aggregated portfolio-wide because price series across symbols are not comparable.*`,
28604
+ `*Trend %/d (column, per-symbol only): ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED SIGNALS of that symbol. UNITS: percent per day (small-slope approximation). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator — static fit at report time. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} or the calendar span is degenerate.*`,
28605
+ `*Trend R² (column, per-symbol only): coefficient of determination of the same log-price regression that produces Trend %/d, computed on log(close) (NOT on absolute close). UNITS: dimensionless in [0, 1]. High R² = clean trend; low R² = noisy regardless of slope sign. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} or the calendar span is degenerate.*`,
28606
+ `*Buyer Pres (column, per-symbol only): frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. (count of i where close[i] > close[i−1]) / (count of decisive moves); flats (close[i] == close[i−1]) excluded from numerator and denominator. UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — the report has no orderbook access; "buyer" labels only the SIGN of the close-to-close return.*`,
28607
+ `*Seller Pres (column, per-symbol only): frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Buyer Pres + Seller Pres = 1. UNITS: dimensionless fraction in [0, 1].*`,
28608
+ `*Buyer Str (column, per-symbol only): magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. Σ |close[i] − close[i−1]| / close[i−1] over up-moves, divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pres (count) and Buyer Str (Σ|Δ|) use the SAME return series; divergence between them (e.g. Pres 0.70 with Str 0.45) reveals regime asymmetry: many small up-moves vs fewer but larger down-moves.*`,
28609
+ `*Seller Str (column, per-symbol only): magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. Σ |close[i] − close[i−1]| / close[i−1] over down-moves, divided by the same sum over ALL decisive moves. By construction Buyer Str + Seller Str = 1.*`,
28610
+ `*Pres Imb (column, per-symbol only): DIFFERENCE, not ratio: Buyer Str − Seller Str = (2 × Buyer Str − 1), computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias.*`,
28611
+ `*Median Step (column, per-symbol only): median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol — "step" = consecutive trade closes (NOT ticks, NOT bars of any timeframe; the report has no candle access). UNITS: percent (normalised by price → directly comparable between symbols at any price level). Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE.*`,
28612
+ ``,
28613
+ `*General reliability note: per-symbol ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS} per-symbol signals. Annualised metrics (Annualized Sharpe, Expected Yearly Returns, Calmar) additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR} per year. The same gates apply to portfolio-level pooled metrics, with the pooled trade count replacing per-symbol trade count. 100+ per-symbol signals are needed for statistical reliability of the ratios; annualised metrics assume the observed frequency and market regime persist year-round.*`,
28614
+ `*IMPORTANT: per-symbol AND pooled equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem (PositionSize / Kelly / ATR): per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging, the cost basis is the sum of all entries and the entry price is dollar-cost-weighted. If your strategy risks X% of capital per trade, the realised return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
28615
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol or portfolio (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
28180
28616
  ].join("\n");
28181
28617
  }
28182
28618
  /**
@@ -66976,6 +67412,7 @@ exports.getPositionPartials = getPositionPartials;
66976
67412
  exports.getPositionPnlCost = getPositionPnlCost;
66977
67413
  exports.getPositionPnlPercent = getPositionPnlPercent;
66978
67414
  exports.getPositionWaitingMinutes = getPositionWaitingMinutes;
67415
+ exports.getPriceScale = getPriceScale;
66979
67416
  exports.getRawCandles = getRawCandles;
66980
67417
  exports.getRiskSchema = getRiskSchema;
66981
67418
  exports.getRuntimeInfo = getRuntimeInfo;