backtest-kit 1.1.6 โ†’ 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Backtest Kit
1
+ # ๐Ÿงฟ Backtest Kit
2
2
 
3
3
  > A production-ready TypeScript framework for backtesting and live trading strategies with crash-safe state persistence, signal validation, and memory-optimized architecture.
4
4
 
@@ -19,10 +19,13 @@
19
19
  - โšก **Memory Optimized** - Prototype methods + memoization + streaming
20
20
  - ๐Ÿ”Œ **Flexible Architecture** - Plug your own exchanges and strategies
21
21
  - ๐Ÿ“ **Markdown Reports** - Auto-generated trading reports with statistics (win rate, avg PNL, Sharpe Ratio, Standard Deviation, Certainty Ratio, Expected Yearly Returns, Risk-Adjusted Returns)
22
+ - ๐Ÿ“Š **Performance Profiling** - Built-in performance tracking with aggregated statistics (avg, min, max, stdDev, P95, P99) for bottleneck analysis
22
23
  - ๐Ÿ›‘ **Graceful Shutdown** - Live.background() waits for open positions to close before stopping
23
24
  - ๐Ÿ’‰ **Strategy Dependency Injection** - addStrategy() enables DI pattern for trading strategies
24
25
  - ๐Ÿ” **Schema Reflection API** - listExchanges(), listStrategies(), listFrames() for runtime introspection
25
- - ๐Ÿงช **Comprehensive Test Coverage** - 45+ unit tests covering validation, PNL, callbacks, reports, and event system
26
+ - ๐Ÿงช **Comprehensive Test Coverage** - 61 unit tests covering validation, PNL, callbacks, reports, performance tracking, and event system
27
+ - ๐Ÿ’พ **Zero Data Download** - Unlike Freqtrade, no need to download gigabytes of historical data - plug any data source (CCXT, database, API)
28
+ - ๐Ÿ”’ **Safe Math & Robustness** - All metrics protected against NaN/Infinity with unsafe numeric checks, returns N/A for invalid calculations
26
29
 
27
30
  ## Installation
28
31
 
@@ -106,7 +109,7 @@ addStrategy({
106
109
  interval: "5m", // Throttling: signals generated max once per 5 minutes
107
110
  getSignal: async (symbol) => {
108
111
  // Your signal generation logic
109
- // Validation happens automatically (prices, TP/SL logic, timestamps)
112
+ // Validation happens automatically (prices, TP/SL logic)
110
113
  return {
111
114
  position: "long",
112
115
  note: "BTC breakout",
@@ -114,14 +117,13 @@ addStrategy({
114
117
  priceTakeProfit: 51000, // Must be > priceOpen for long
115
118
  priceStopLoss: 49000, // Must be < priceOpen for long
116
119
  minuteEstimatedTime: 60, // Signal duration in minutes
117
- timestamp: Date.now(),
118
120
  };
119
121
  },
120
122
  callbacks: {
121
- onOpen: (backtest, symbol, signal) => {
123
+ onOpen: (symbol, signal, currentPrice, backtest) => {
122
124
  console.log(`[${backtest ? "BT" : "LIVE"}] Signal opened:`, signal.id);
123
125
  },
124
- onClose: (backtest, symbol, priceClose, signal) => {
126
+ onClose: (symbol, signal, priceClose, backtest) => {
125
127
  console.log(`[${backtest ? "BT" : "LIVE"}] Signal closed:`, priceClose);
126
128
  },
127
129
  },
@@ -348,7 +350,6 @@ All signals are validated automatically before execution:
348
350
  priceTakeProfit: 51000, // โœ… 51000 > 50000
349
351
  priceStopLoss: 49000, // โœ… 49000 < 50000
350
352
  minuteEstimatedTime: 60, // โœ… positive
351
- timestamp: Date.now(), // โœ… positive
352
353
  }
353
354
 
354
355
  // โŒ Invalid long signal - throws error
@@ -465,7 +466,26 @@ const stopBacktest = Backtest.background("BTCUSDT", {
465
466
  frameName: "1d-backtest"
466
467
  });
467
468
 
468
- // Generate markdown report
469
+ // Get raw statistical data (Controller)
470
+ const stats = await Backtest.getData("my-strategy");
471
+ console.log(stats);
472
+ // Returns:
473
+ // {
474
+ // signalList: [...], // All closed signals
475
+ // totalSignals: 10,
476
+ // winCount: 7,
477
+ // lossCount: 3,
478
+ // winRate: 70.0, // Percentage (higher is better)
479
+ // avgPnl: 1.23, // Average PNL % (higher is better)
480
+ // totalPnl: 12.30, // Total PNL % (higher is better)
481
+ // stdDev: 2.45, // Standard deviation (lower is better)
482
+ // sharpeRatio: 0.50, // Risk-adjusted return (higher is better)
483
+ // annualizedSharpeRatio: 9.55, // Sharpe ร— โˆš365 (higher is better)
484
+ // certaintyRatio: 1.75, // avgWin / |avgLoss| (higher is better)
485
+ // expectedYearlyReturns: 156 // Estimated yearly trades (higher is better)
486
+ // }
487
+
488
+ // Generate markdown report (View)
469
489
  const markdown = await Backtest.getReport("my-strategy");
470
490
  console.log(markdown);
471
491
 
@@ -476,29 +496,75 @@ await Backtest.dump("my-strategy");
476
496
  await Backtest.dump("my-strategy", "./custom/path");
477
497
  ```
478
498
 
479
- **Report includes:**
480
- - Total closed signals
499
+ **getData() returns BacktestStatistics:**
500
+ - `signalList` - Array of all closed signals
501
+ - `totalSignals` - Total number of closed signals
502
+ - `winCount` / `lossCount` - Number of winning/losing trades
503
+ - `winRate` - Win percentage (higher is better)
504
+ - `avgPnl` - Average PNL percentage (higher is better)
505
+ - `totalPnl` - Total PNL percentage (higher is better)
506
+ - `stdDev` - Standard deviation / volatility (lower is better)
507
+ - `sharpeRatio` - Risk-adjusted return (higher is better)
508
+ - `annualizedSharpeRatio` - Sharpe Ratio ร— โˆš365 (higher is better)
509
+ - `certaintyRatio` - avgWin / |avgLoss| (higher is better)
510
+ - `expectedYearlyReturns` - Estimated number of trades per year (higher is better)
511
+
512
+ **getReport() includes:**
513
+ - All metrics from getData() formatted as markdown
481
514
  - All signal details (prices, TP/SL, PNL, duration, close reason)
482
515
  - Timestamps for each signal
516
+ - "Higher is better" / "Lower is better" annotations
483
517
 
484
518
  ### Live Trading Reports
485
519
 
486
520
  ```typescript
487
521
  import { Live } from "backtest-kit";
488
522
 
489
- // Generate live trading report
523
+ // Get raw statistical data (Controller)
524
+ const stats = await Live.getData("my-strategy");
525
+ console.log(stats);
526
+ // Returns:
527
+ // {
528
+ // eventList: [...], // All events (idle, opened, active, closed)
529
+ // totalEvents: 15,
530
+ // totalClosed: 5,
531
+ // winCount: 3,
532
+ // lossCount: 2,
533
+ // winRate: 60.0, // Percentage (higher is better)
534
+ // avgPnl: 1.23, // Average PNL % (higher is better)
535
+ // totalPnl: 6.15, // Total PNL % (higher is better)
536
+ // stdDev: 1.85, // Standard deviation (lower is better)
537
+ // sharpeRatio: 0.66, // Risk-adjusted return (higher is better)
538
+ // annualizedSharpeRatio: 12.61,// Sharpe ร— โˆš365 (higher is better)
539
+ // certaintyRatio: 2.10, // avgWin / |avgLoss| (higher is better)
540
+ // expectedYearlyReturns: 365 // Estimated yearly trades (higher is better)
541
+ // }
542
+
543
+ // Generate markdown report (View)
490
544
  const markdown = await Live.getReport("my-strategy");
491
545
 
492
546
  // Save to disk (default: ./logs/live/my-strategy.md)
493
547
  await Live.dump("my-strategy");
494
548
  ```
495
549
 
496
- **Report includes:**
497
- - Total events (idle, opened, active, closed)
498
- - Closed signals count
499
- - Win rate (% wins, wins/losses)
500
- - Average PNL percentage
550
+ **getData() returns LiveStatistics:**
551
+ - `eventList` - Array of all events (idle, opened, active, closed)
552
+ - `totalEvents` - Total number of events
553
+ - `totalClosed` - Total number of closed signals
554
+ - `winCount` / `lossCount` - Number of winning/losing trades
555
+ - `winRate` - Win percentage (higher is better)
556
+ - `avgPnl` - Average PNL percentage (higher is better)
557
+ - `totalPnl` - Total PNL percentage (higher is better)
558
+ - `stdDev` - Standard deviation / volatility (lower is better)
559
+ - `sharpeRatio` - Risk-adjusted return (higher is better)
560
+ - `annualizedSharpeRatio` - Sharpe Ratio ร— โˆš365 (higher is better)
561
+ - `certaintyRatio` - avgWin / |avgLoss| (higher is better)
562
+ - `expectedYearlyReturns` - Estimated number of trades per year (higher is better)
563
+
564
+ **getReport() includes:**
565
+ - All metrics from getData() formatted as markdown
501
566
  - Signal-by-signal details with current state
567
+ - "Higher is better" / "Lower is better" annotations
502
568
 
503
569
  **Report example:**
504
570
  ```markdown
@@ -506,8 +572,14 @@ await Live.dump("my-strategy");
506
572
 
507
573
  Total events: 15
508
574
  Closed signals: 5
509
- Win rate: 60.00% (3W / 2L)
510
- Average PNL: +1.23%
575
+ Win rate: 60.00% (3W / 2L) (higher is better)
576
+ Average PNL: +1.23% (higher is better)
577
+ Total PNL: +6.15% (higher is better)
578
+ Standard Deviation: 1.85% (lower is better)
579
+ Sharpe Ratio: 0.66 (higher is better)
580
+ Annualized Sharpe Ratio: 12.61 (higher is better)
581
+ Certainty Ratio: 2.10 (higher is better)
582
+ Expected Yearly Returns: 365 trades (higher is better)
511
583
 
512
584
  | Timestamp | Action | Symbol | Signal ID | Position | ... | PNL (net) | Close Reason |
513
585
  |-----------|--------|--------|-----------|----------|-----|-----------|--------------|
@@ -639,6 +711,8 @@ Live.background("BTCUSDT", {
639
711
  - `listenSignalBacktestOnce(filter, callback)` - Subscribe to backtest signals once
640
712
  - `listenSignalLive(callback)` - Subscribe to live signals only
641
713
  - `listenSignalLiveOnce(filter, callback)` - Subscribe to live signals once
714
+ - `listenPerformance(callback)` - Subscribe to performance metrics (backtest + live)
715
+ - `listenProgress(callback)` - Subscribe to backtest progress events
642
716
  - `listenError(callback)` - Subscribe to background execution errors
643
717
  - `listenDone(callback)` - Subscribe to background completion events
644
718
  - `listenDoneOnce(filter, callback)` - Subscribe to background completion once
@@ -744,7 +818,7 @@ const quantity = await formatQuantity("BTCUSDT", 0.123456789);
744
818
  #### Backtest API
745
819
 
746
820
  ```typescript
747
- import { Backtest } from "backtest-kit";
821
+ import { Backtest, BacktestStatistics } from "backtest-kit";
748
822
 
749
823
  // Stream backtest results
750
824
  Backtest.run(
@@ -762,7 +836,10 @@ Backtest.background(
762
836
  context: { strategyName, exchangeName, frameName }
763
837
  ): Promise<() => void> // Returns cancellation function
764
838
 
765
- // Generate markdown report
839
+ // Get raw statistical data (Controller)
840
+ Backtest.getData(strategyName: string): Promise<BacktestStatistics>
841
+
842
+ // Generate markdown report (View)
766
843
  Backtest.getReport(strategyName: string): Promise<string>
767
844
 
768
845
  // Save report to disk
@@ -772,7 +849,7 @@ Backtest.dump(strategyName: string, path?: string): Promise<void>
772
849
  #### Live Trading API
773
850
 
774
851
  ```typescript
775
- import { Live } from "backtest-kit";
852
+ import { Live, LiveStatistics } from "backtest-kit";
776
853
 
777
854
  // Stream live results (infinite)
778
855
  Live.run(
@@ -789,27 +866,133 @@ Live.background(
789
866
  context: { strategyName, exchangeName }
790
867
  ): Promise<() => void> // Returns cancellation function
791
868
 
792
- // Generate markdown report
869
+ // Get raw statistical data (Controller)
870
+ Live.getData(strategyName: string): Promise<LiveStatistics>
871
+
872
+ // Generate markdown report (View)
793
873
  Live.getReport(strategyName: string): Promise<string>
794
874
 
795
875
  // Save report to disk
796
876
  Live.dump(strategyName: string, path?: string): Promise<void>
797
877
  ```
798
878
 
879
+ #### Performance Profiling API
880
+
881
+ ```typescript
882
+ import { Performance, PerformanceStatistics, listenPerformance } from "backtest-kit";
883
+
884
+ // Get raw performance statistics (Controller)
885
+ Performance.getData(strategyName: string): Promise<PerformanceStatistics>
886
+
887
+ // Generate markdown report with bottleneck analysis (View)
888
+ Performance.getReport(strategyName: string): Promise<string>
889
+
890
+ // Save performance report to disk (default: ./logs/performance)
891
+ Performance.dump(strategyName: string, path?: string): Promise<void>
892
+
893
+ // Clear accumulated performance data
894
+ Performance.clear(strategyName?: string): Promise<void>
895
+
896
+ // Listen to real-time performance events
897
+ listenPerformance((event) => {
898
+ console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
899
+ console.log(`Strategy: ${event.strategyName} @ ${event.exchangeName}`);
900
+ console.log(`Symbol: ${event.symbol}, Backtest: ${event.backtest}`);
901
+ });
902
+ ```
903
+
799
904
  ## Type Definitions
800
905
 
906
+ ### Statistics Types
907
+
908
+ ```typescript
909
+ // Backtest statistics (exported from "backtest-kit")
910
+ interface BacktestStatistics {
911
+ signalList: IStrategyTickResultClosed[]; // All closed signals
912
+ totalSignals: number;
913
+ winCount: number;
914
+ lossCount: number;
915
+ winRate: number | null; // Win percentage (higher is better)
916
+ avgPnl: number | null; // Average PNL % (higher is better)
917
+ totalPnl: number | null; // Total PNL % (higher is better)
918
+ stdDev: number | null; // Standard deviation (lower is better)
919
+ sharpeRatio: number | null; // Risk-adjusted return (higher is better)
920
+ annualizedSharpeRatio: number | null; // Sharpe ร— โˆš365 (higher is better)
921
+ certaintyRatio: number | null; // avgWin / |avgLoss| (higher is better)
922
+ expectedYearlyReturns: number | null; // Estimated yearly trades (higher is better)
923
+ }
924
+
925
+ // Live statistics (exported from "backtest-kit")
926
+ interface LiveStatistics {
927
+ eventList: TickEvent[]; // All events (idle, opened, active, closed)
928
+ totalEvents: number;
929
+ totalClosed: number;
930
+ winCount: number;
931
+ lossCount: number;
932
+ winRate: number | null; // Win percentage (higher is better)
933
+ avgPnl: number | null; // Average PNL % (higher is better)
934
+ totalPnl: number | null; // Total PNL % (higher is better)
935
+ stdDev: number | null; // Standard deviation (lower is better)
936
+ sharpeRatio: number | null; // Risk-adjusted return (higher is better)
937
+ annualizedSharpeRatio: number | null; // Sharpe ร— โˆš365 (higher is better)
938
+ certaintyRatio: number | null; // avgWin / |avgLoss| (higher is better)
939
+ expectedYearlyReturns: number | null; // Estimated yearly trades (higher is better)
940
+ }
941
+
942
+ // Performance statistics (exported from "backtest-kit")
943
+ interface PerformanceStatistics {
944
+ strategyName: string; // Strategy name
945
+ totalEvents: number; // Total number of performance events
946
+ totalDuration: number; // Total execution time (ms)
947
+ metricStats: Record<string, { // Statistics by metric type
948
+ metricType: PerformanceMetricType; // backtest_total | backtest_timeframe | backtest_signal | live_tick
949
+ count: number; // Number of samples
950
+ totalDuration: number; // Total duration (ms)
951
+ avgDuration: number; // Average duration (ms)
952
+ minDuration: number; // Minimum duration (ms)
953
+ maxDuration: number; // Maximum duration (ms)
954
+ stdDev: number; // Standard deviation (ms)
955
+ median: number; // Median duration (ms)
956
+ p95: number; // 95th percentile (ms)
957
+ p99: number; // 99th percentile (ms)
958
+ }>;
959
+ events: PerformanceContract[]; // All raw performance events
960
+ }
961
+
962
+ // Performance event (exported from "backtest-kit")
963
+ interface PerformanceContract {
964
+ timestamp: number; // When metric was recorded (epoch ms)
965
+ metricType: PerformanceMetricType; // Type of operation measured
966
+ duration: number; // Operation duration (ms)
967
+ strategyName: string; // Strategy name
968
+ exchangeName: string; // Exchange name
969
+ symbol: string; // Trading symbol
970
+ backtest: boolean; // true = backtest, false = live
971
+ }
972
+
973
+ // Performance metric types (exported from "backtest-kit")
974
+ type PerformanceMetricType =
975
+ | "backtest_total" // Total backtest duration
976
+ | "backtest_timeframe" // Single timeframe processing
977
+ | "backtest_signal" // Signal processing (tick + getNextCandles + backtest)
978
+ | "live_tick"; // Single live tick duration
979
+ ```
980
+
801
981
  ### Signal Data
802
982
 
803
983
  ```typescript
804
984
  interface ISignalRow {
805
- id: string; // Auto-generated
985
+ id: string; // UUID v4 auto-generated
806
986
  position: "long" | "short";
807
- note: string;
987
+ note?: string;
808
988
  priceOpen: number;
809
989
  priceTakeProfit: number;
810
990
  priceStopLoss: number;
811
991
  minuteEstimatedTime: number;
812
- timestamp: number;
992
+ exchangeName: string;
993
+ strategyName: string;
994
+ timestamp: number; // Signal creation timestamp
995
+ symbol: string; // Trading pair (e.g., "BTCUSDT")
813
996
  }
814
997
  ```
815
998
 
@@ -817,9 +1000,27 @@ interface ISignalRow {
817
1000
 
818
1001
  ```typescript
819
1002
  type IStrategyTickResult =
820
- | { action: "idle"; signal: null }
821
- | { action: "opened"; signal: ISignalRow }
822
- | { action: "active"; signal: ISignalRow; currentPrice: number }
1003
+ | {
1004
+ action: "idle";
1005
+ signal: null;
1006
+ strategyName: string;
1007
+ exchangeName: string;
1008
+ currentPrice: number;
1009
+ }
1010
+ | {
1011
+ action: "opened";
1012
+ signal: ISignalRow;
1013
+ strategyName: string;
1014
+ exchangeName: string;
1015
+ currentPrice: number;
1016
+ }
1017
+ | {
1018
+ action: "active";
1019
+ signal: ISignalRow;
1020
+ currentPrice: number;
1021
+ strategyName: string;
1022
+ exchangeName: string;
1023
+ }
823
1024
  | {
824
1025
  action: "closed";
825
1026
  signal: ISignalRow;
@@ -827,10 +1028,12 @@ type IStrategyTickResult =
827
1028
  closeReason: "take_profit" | "stop_loss" | "time_expired";
828
1029
  closeTimestamp: number;
829
1030
  pnl: {
830
- priceOpenWithCosts: number;
831
- priceCloseWithCosts: number;
832
1031
  pnlPercentage: number;
1032
+ priceOpen: number; // Entry price adjusted with slippage and fees
1033
+ priceClose: number; // Exit price adjusted with slippage and fees
833
1034
  };
1035
+ strategyName: string;
1036
+ exchangeName: string;
834
1037
  };
835
1038
  ```
836
1039
 
@@ -909,6 +1112,72 @@ Backtest.background("BTCUSDT", {
909
1112
  });
910
1113
  ```
911
1114
 
1115
+ ### Performance Profiling
1116
+
1117
+ ```typescript
1118
+ import { Performance, listenPerformance, Backtest } from "backtest-kit";
1119
+
1120
+ // Listen to real-time performance metrics
1121
+ listenPerformance((event) => {
1122
+ console.log(`[${event.metricType}] ${event.duration.toFixed(2)}ms`);
1123
+ console.log(` Strategy: ${event.strategyName}`);
1124
+ console.log(` Symbol: ${event.symbol}, Backtest: ${event.backtest}`);
1125
+ });
1126
+
1127
+ // Run backtest
1128
+ await Backtest.background("BTCUSDT", {
1129
+ strategyName: "my-strategy",
1130
+ exchangeName: "binance",
1131
+ frameName: "1d-backtest"
1132
+ });
1133
+
1134
+ // Get aggregated performance statistics
1135
+ const perfStats = await Performance.getData("my-strategy");
1136
+ console.log("Performance Statistics:");
1137
+ console.log(` Total events: ${perfStats.totalEvents}`);
1138
+ console.log(` Total duration: ${perfStats.totalDuration.toFixed(2)}ms`);
1139
+ console.log(` Metrics tracked: ${Object.keys(perfStats.metricStats).join(", ")}`);
1140
+
1141
+ // Analyze bottlenecks
1142
+ for (const [type, stats] of Object.entries(perfStats.metricStats)) {
1143
+ console.log(`\n${type}:`);
1144
+ console.log(` Count: ${stats.count}`);
1145
+ console.log(` Average: ${stats.avgDuration.toFixed(2)}ms`);
1146
+ console.log(` Min/Max: ${stats.minDuration.toFixed(2)}ms / ${stats.maxDuration.toFixed(2)}ms`);
1147
+ console.log(` P95/P99: ${stats.p95.toFixed(2)}ms / ${stats.p99.toFixed(2)}ms`);
1148
+ console.log(` Std Dev: ${stats.stdDev.toFixed(2)}ms`);
1149
+ }
1150
+
1151
+ // Generate and save performance report
1152
+ const markdown = await Performance.getReport("my-strategy");
1153
+ await Performance.dump("my-strategy"); // Saves to ./logs/performance/my-strategy.md
1154
+ ```
1155
+
1156
+ **Performance Report Example:**
1157
+ ```markdown
1158
+ # Performance Report: my-strategy
1159
+
1160
+ **Total events:** 1440
1161
+ **Total execution time:** 12345.67ms
1162
+ **Number of metric types:** 3
1163
+
1164
+ ## Time Distribution
1165
+
1166
+ - **backtest_timeframe**: 65.4% (8074.32ms total)
1167
+ - **backtest_signal**: 28.3% (3493.85ms total)
1168
+ - **backtest_total**: 6.3% (777.50ms total)
1169
+
1170
+ ## Detailed Metrics
1171
+
1172
+ | Metric Type | Count | Total (ms) | Avg (ms) | Min (ms) | Max (ms) | Std Dev (ms) | Median (ms) | P95 (ms) | P99 (ms) |
1173
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
1174
+ | backtest_timeframe | 1440 | 8074.32 | 5.61 | 2.10 | 12.45 | 1.85 | 5.20 | 8.90 | 10.50 |
1175
+ | backtest_signal | 45 | 3493.85 | 77.64 | 45.20 | 125.80 | 18.32 | 75.10 | 110.20 | 120.15 |
1176
+ | backtest_total | 1 | 777.50 | 777.50 | 777.50 | 777.50 | 0.00 | 777.50 | 777.50 | 777.50 |
1177
+
1178
+ **Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times.
1179
+ ```
1180
+
912
1181
  ### Early Termination
913
1182
 
914
1183
  **Using async generator with break:**