backtest-kit 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/build/index.cjs +2116 -947
  2. package/build/index.mjs +2114 -948
  3. package/package.json +1 -1
  4. package/types.d.ts +396 -117
package/build/index.mjs CHANGED
@@ -730,6 +730,11 @@ const PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA = "PersistStorageUtils.writeS
730
730
  const PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON = "PersistStorageUtils.useJson";
731
731
  const PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY = "PersistStorageUtils.useDummy";
732
732
  const PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER = "PersistStorageUtils.usePersistStorageAdapter";
733
+ const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA = "PersistNotificationUtils.readNotificationData";
734
+ const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA = "PersistNotificationUtils.writeNotificationData";
735
+ const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON = "PersistNotificationUtils.useJson";
736
+ const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY = "PersistNotificationUtils.useDummy";
737
+ const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER = "PersistNotificationUtils.usePersistNotificationAdapter";
733
738
  const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
734
739
  const BASE_UNLINK_RETRY_COUNT = 5;
735
740
  const BASE_UNLINK_RETRY_DELAY = 1000;
@@ -1803,6 +1808,101 @@ class PersistStorageUtils {
1803
1808
  * Used by SignalLiveUtils for signal storage persistence.
1804
1809
  */
1805
1810
  const PersistStorageAdapter = new PersistStorageUtils();
1811
+ /**
1812
+ * Utility class for managing notification persistence.
1813
+ *
1814
+ * Features:
1815
+ * - Memoized storage instances
1816
+ * - Custom adapter support
1817
+ * - Atomic read/write operations for NotificationData
1818
+ * - Each notification stored as separate file keyed by id
1819
+ * - Crash-safe notification state management
1820
+ *
1821
+ * Used by NotificationPersistLiveUtils/NotificationPersistBacktestUtils for persistence.
1822
+ */
1823
+ class PersistNotificationUtils {
1824
+ constructor() {
1825
+ this.PersistNotificationFactory = PersistBase;
1826
+ this.getNotificationStorage = memoize(([backtest]) => backtest ? `backtest` : `live`, (backtest) => Reflect.construct(this.PersistNotificationFactory, [
1827
+ backtest ? `backtest` : `live`,
1828
+ `./dump/data/notification/`,
1829
+ ]));
1830
+ /**
1831
+ * Reads persisted notifications data.
1832
+ *
1833
+ * Called by NotificationPersistLiveUtils/NotificationPersistBacktestUtils.waitForInit() to restore state.
1834
+ * Uses keys() from PersistBase to iterate over all stored notifications.
1835
+ * Returns empty array if no notifications exist.
1836
+ *
1837
+ * @param backtest - If true, reads from backtest storage; otherwise from live storage
1838
+ * @returns Promise resolving to array of notification entries
1839
+ */
1840
+ this.readNotificationData = async (backtest) => {
1841
+ bt.loggerService.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
1842
+ const key = backtest ? `backtest` : `live`;
1843
+ const isInitial = !this.getNotificationStorage.has(key);
1844
+ const stateStorage = this.getNotificationStorage(backtest);
1845
+ await stateStorage.waitForInit(isInitial);
1846
+ const notifications = [];
1847
+ for await (const notificationId of stateStorage.keys()) {
1848
+ const notification = await stateStorage.readValue(notificationId);
1849
+ notifications.push(notification);
1850
+ }
1851
+ return notifications;
1852
+ };
1853
+ /**
1854
+ * Writes notification data to disk with atomic file writes.
1855
+ *
1856
+ * Called by NotificationPersistLiveUtils/NotificationPersistBacktestUtils after notification changes to persist state.
1857
+ * Uses notification.id as the storage key for individual file storage.
1858
+ * Uses atomic writes to prevent corruption on crashes.
1859
+ *
1860
+ * @param notificationData - Notification entries to persist
1861
+ * @param backtest - If true, writes to backtest storage; otherwise to live storage
1862
+ * @returns Promise that resolves when write is complete
1863
+ */
1864
+ this.writeNotificationData = async (notificationData, backtest) => {
1865
+ bt.loggerService.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
1866
+ const key = backtest ? `backtest` : `live`;
1867
+ const isInitial = !this.getNotificationStorage.has(key);
1868
+ const stateStorage = this.getNotificationStorage(backtest);
1869
+ await stateStorage.waitForInit(isInitial);
1870
+ for (const notification of notificationData) {
1871
+ await stateStorage.writeValue(notification.id, notification);
1872
+ }
1873
+ };
1874
+ }
1875
+ /**
1876
+ * Registers a custom persistence adapter.
1877
+ *
1878
+ * @param Ctor - Custom PersistBase constructor
1879
+ */
1880
+ usePersistNotificationAdapter(Ctor) {
1881
+ bt.loggerService.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
1882
+ this.PersistNotificationFactory = Ctor;
1883
+ }
1884
+ /**
1885
+ * Switches to the default JSON persist adapter.
1886
+ * All future persistence writes will use JSON storage.
1887
+ */
1888
+ useJson() {
1889
+ bt.loggerService.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
1890
+ this.usePersistNotificationAdapter(PersistBase);
1891
+ }
1892
+ /**
1893
+ * Switches to a dummy persist adapter that discards all writes.
1894
+ * All future persistence writes will be no-ops.
1895
+ */
1896
+ useDummy() {
1897
+ bt.loggerService.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
1898
+ this.usePersistNotificationAdapter(PersistDummy);
1899
+ }
1900
+ }
1901
+ /**
1902
+ * Global singleton instance of PersistNotificationUtils.
1903
+ * Used by NotificationPersistLiveUtils/NotificationPersistBacktestUtils for notification persistence.
1904
+ */
1905
+ const PersistNotificationAdapter = new PersistNotificationUtils();
1806
1906
 
1807
1907
  const MS_PER_MINUTE$1 = 60000;
1808
1908
  const INTERVAL_MINUTES$4 = {
@@ -33620,325 +33720,2048 @@ const StorageLive = new StorageLiveAdapter();
33620
33720
  */
33621
33721
  const StorageBacktest = new StorageBacktestAdapter();
33622
33722
 
33623
- const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
33624
- const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
33625
- const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
33626
- const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
33627
- const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
33628
- const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
33629
- const MS_PER_MINUTE = 60000;
33630
33723
  /**
33631
- * Gets current timestamp from execution context if available.
33632
- * Returns current Date() if no execution context exists (non-trading GUI).
33724
+ * Maximum number of notifications to keep in storage.
33725
+ * Older notifications are removed when this limit is exceeded.
33633
33726
  */
33634
- const GET_TIMESTAMP_FN = async () => {
33635
- if (ExecutionContextService.hasContext()) {
33636
- return new Date(bt.executionContextService.context.when);
33637
- }
33638
- return new Date();
33639
- };
33727
+ const MAX_NOTIFICATIONS = 250;
33640
33728
  /**
33641
- * Gets backtest mode flag from execution context if available.
33642
- * Returns false if no execution context exists (live mode).
33729
+ * Generates a unique key for notification identification.
33730
+ * @returns Random string identifier
33643
33731
  */
33644
- const GET_BACKTEST_FN = async () => {
33645
- if (ExecutionContextService.hasContext()) {
33646
- return bt.executionContextService.context.backtest;
33647
- }
33648
- return false;
33649
- };
33732
+ const CREATE_KEY_FN$1 = () => randomString();
33650
33733
  /**
33651
- * Default implementation for getCandles.
33652
- * Throws an error indicating the method is not implemented.
33734
+ * Creates a notification model from signal tick result.
33735
+ * Handles opened, closed, scheduled, and cancelled signal actions.
33736
+ * @param data - The strategy tick result data
33737
+ * @returns NotificationModel or null if action is not recognized
33653
33738
  */
33654
- const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
33655
- throw new Error(`getCandles is not implemented for this exchange`);
33739
+ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
33740
+ if (data.action === "opened") {
33741
+ return {
33742
+ type: "signal.opened",
33743
+ id: CREATE_KEY_FN$1(),
33744
+ timestamp: data.signal.pendingAt,
33745
+ backtest: data.backtest,
33746
+ symbol: data.symbol,
33747
+ strategyName: data.strategyName,
33748
+ exchangeName: data.exchangeName,
33749
+ signalId: data.signal.id,
33750
+ position: data.signal.position,
33751
+ priceOpen: data.signal.priceOpen,
33752
+ priceTakeProfit: data.signal.priceTakeProfit,
33753
+ priceStopLoss: data.signal.priceStopLoss,
33754
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
33755
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
33756
+ note: data.signal.note,
33757
+ scheduledAt: data.signal.scheduledAt,
33758
+ pendingAt: data.signal.pendingAt,
33759
+ createdAt: data.createdAt,
33760
+ };
33761
+ }
33762
+ if (data.action === "closed") {
33763
+ const durationMs = data.closeTimestamp - data.signal.pendingAt;
33764
+ const durationMin = Math.round(durationMs / 60000);
33765
+ return {
33766
+ type: "signal.closed",
33767
+ id: CREATE_KEY_FN$1(),
33768
+ timestamp: data.closeTimestamp,
33769
+ backtest: data.backtest,
33770
+ symbol: data.symbol,
33771
+ strategyName: data.strategyName,
33772
+ exchangeName: data.exchangeName,
33773
+ signalId: data.signal.id,
33774
+ position: data.signal.position,
33775
+ priceOpen: data.signal.priceOpen,
33776
+ priceClose: data.currentPrice,
33777
+ priceTakeProfit: data.signal.priceTakeProfit,
33778
+ priceStopLoss: data.signal.priceStopLoss,
33779
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
33780
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
33781
+ pnlPercentage: data.pnl.pnlPercentage,
33782
+ closeReason: data.closeReason,
33783
+ duration: durationMin,
33784
+ note: data.signal.note,
33785
+ scheduledAt: data.signal.scheduledAt,
33786
+ pendingAt: data.signal.pendingAt,
33787
+ createdAt: data.createdAt,
33788
+ };
33789
+ }
33790
+ if (data.action === "scheduled") {
33791
+ return {
33792
+ type: "signal.scheduled",
33793
+ id: CREATE_KEY_FN$1(),
33794
+ timestamp: data.signal.scheduledAt,
33795
+ backtest: data.backtest,
33796
+ symbol: data.symbol,
33797
+ strategyName: data.strategyName,
33798
+ exchangeName: data.exchangeName,
33799
+ signalId: data.signal.id,
33800
+ position: data.signal.position,
33801
+ priceOpen: data.signal.priceOpen,
33802
+ priceTakeProfit: data.signal.priceTakeProfit,
33803
+ priceStopLoss: data.signal.priceStopLoss,
33804
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
33805
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
33806
+ scheduledAt: data.signal.scheduledAt,
33807
+ currentPrice: data.currentPrice,
33808
+ createdAt: data.createdAt,
33809
+ };
33810
+ }
33811
+ if (data.action === "cancelled") {
33812
+ const durationMs = data.closeTimestamp - data.signal.scheduledAt;
33813
+ const durationMin = Math.round(durationMs / 60000);
33814
+ return {
33815
+ type: "signal.cancelled",
33816
+ id: CREATE_KEY_FN$1(),
33817
+ timestamp: data.closeTimestamp,
33818
+ backtest: data.backtest,
33819
+ symbol: data.symbol,
33820
+ strategyName: data.strategyName,
33821
+ exchangeName: data.exchangeName,
33822
+ signalId: data.signal.id,
33823
+ position: data.signal.position,
33824
+ priceOpen: data.signal.priceOpen,
33825
+ priceTakeProfit: data.signal.priceTakeProfit,
33826
+ priceStopLoss: data.signal.priceStopLoss,
33827
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
33828
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
33829
+ cancelReason: data.reason,
33830
+ cancelId: data.cancelId,
33831
+ duration: durationMin,
33832
+ scheduledAt: data.signal.scheduledAt,
33833
+ pendingAt: data.signal.pendingAt,
33834
+ createdAt: data.createdAt,
33835
+ };
33836
+ }
33837
+ return null;
33656
33838
  };
33657
33839
  /**
33658
- * Default implementation for formatQuantity.
33659
- * Returns Bitcoin precision on Binance (8 decimal places).
33660
- */
33661
- const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
33662
- return quantity.toFixed(8);
33663
- };
33840
+ * Creates a notification model for partial profit availability.
33841
+ * @param data - The partial profit contract data
33842
+ * @returns NotificationModel for partial profit event
33843
+ */
33844
+ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
33845
+ type: "partial_profit.available",
33846
+ id: CREATE_KEY_FN$1(),
33847
+ timestamp: data.timestamp,
33848
+ backtest: data.backtest,
33849
+ symbol: data.symbol,
33850
+ strategyName: data.strategyName,
33851
+ exchangeName: data.exchangeName,
33852
+ signalId: data.data.id,
33853
+ level: data.level,
33854
+ currentPrice: data.currentPrice,
33855
+ priceOpen: data.data.priceOpen,
33856
+ position: data.data.position,
33857
+ priceTakeProfit: data.data.priceTakeProfit,
33858
+ priceStopLoss: data.data.priceStopLoss,
33859
+ originalPriceTakeProfit: data.data.originalPriceTakeProfit,
33860
+ originalPriceStopLoss: data.data.originalPriceStopLoss,
33861
+ scheduledAt: data.data.scheduledAt,
33862
+ pendingAt: data.data.pendingAt,
33863
+ createdAt: data.timestamp,
33864
+ });
33664
33865
  /**
33665
- * Default implementation for formatPrice.
33666
- * Returns Bitcoin precision on Binance (2 decimal places).
33667
- */
33668
- const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
33669
- return price.toFixed(2);
33670
- };
33866
+ * Creates a notification model for partial loss availability.
33867
+ * @param data - The partial loss contract data
33868
+ * @returns NotificationModel for partial loss event
33869
+ */
33870
+ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
33871
+ type: "partial_loss.available",
33872
+ id: CREATE_KEY_FN$1(),
33873
+ timestamp: data.timestamp,
33874
+ backtest: data.backtest,
33875
+ symbol: data.symbol,
33876
+ strategyName: data.strategyName,
33877
+ exchangeName: data.exchangeName,
33878
+ signalId: data.data.id,
33879
+ level: data.level,
33880
+ currentPrice: data.currentPrice,
33881
+ priceOpen: data.data.priceOpen,
33882
+ position: data.data.position,
33883
+ priceTakeProfit: data.data.priceTakeProfit,
33884
+ priceStopLoss: data.data.priceStopLoss,
33885
+ originalPriceTakeProfit: data.data.originalPriceTakeProfit,
33886
+ originalPriceStopLoss: data.data.originalPriceStopLoss,
33887
+ scheduledAt: data.data.scheduledAt,
33888
+ pendingAt: data.data.pendingAt,
33889
+ createdAt: data.timestamp,
33890
+ });
33671
33891
  /**
33672
- * Default implementation for getOrderBook.
33673
- * Throws an error indicating the method is not implemented.
33674
- *
33675
- * @param _symbol - Trading pair symbol (unused)
33676
- * @param _depth - Maximum depth levels (unused)
33677
- * @param _from - Start of time range (unused - can be ignored in live implementations)
33678
- * @param _to - End of time range (unused - can be ignored in live implementations)
33679
- * @param _backtest - Whether running in backtest mode (unused)
33680
- */
33681
- const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
33682
- throw new Error(`getOrderBook is not implemented for this exchange`);
33683
- };
33684
- const INTERVAL_MINUTES$1 = {
33685
- "1m": 1,
33686
- "3m": 3,
33687
- "5m": 5,
33688
- "15m": 15,
33689
- "30m": 30,
33690
- "1h": 60,
33691
- "2h": 120,
33692
- "4h": 240,
33693
- "6h": 360,
33694
- "8h": 480,
33695
- };
33892
+ * Creates a notification model for breakeven availability.
33893
+ * @param data - The breakeven contract data
33894
+ * @returns NotificationModel for breakeven event
33895
+ */
33896
+ const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
33897
+ type: "breakeven.available",
33898
+ id: CREATE_KEY_FN$1(),
33899
+ timestamp: data.timestamp,
33900
+ backtest: data.backtest,
33901
+ symbol: data.symbol,
33902
+ strategyName: data.strategyName,
33903
+ exchangeName: data.exchangeName,
33904
+ signalId: data.data.id,
33905
+ currentPrice: data.currentPrice,
33906
+ priceOpen: data.data.priceOpen,
33907
+ position: data.data.position,
33908
+ priceTakeProfit: data.data.priceTakeProfit,
33909
+ priceStopLoss: data.data.priceStopLoss,
33910
+ originalPriceTakeProfit: data.data.originalPriceTakeProfit,
33911
+ originalPriceStopLoss: data.data.originalPriceStopLoss,
33912
+ scheduledAt: data.data.scheduledAt,
33913
+ pendingAt: data.data.pendingAt,
33914
+ createdAt: data.timestamp,
33915
+ });
33696
33916
  /**
33697
- * Aligns timestamp down to the nearest interval boundary.
33698
- * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
33699
- *
33700
- * Candle timestamp convention:
33701
- * - Candle timestamp = openTime (when candle opens)
33702
- * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
33703
- *
33704
- * Adapter contract:
33705
- * - Adapter must return candles with timestamp = openTime
33706
- * - First returned candle.timestamp must equal aligned since
33707
- * - Adapter must return exactly `limit` candles
33708
- *
33709
- * @param timestamp - Timestamp in milliseconds
33710
- * @param intervalMinutes - Interval in minutes
33711
- * @returns Aligned timestamp rounded down to interval boundary
33917
+ * Creates a notification model for strategy commit events.
33918
+ * Handles partial-profit, partial-loss, breakeven, trailing-stop,
33919
+ * trailing-take, and activate-scheduled actions.
33920
+ * @param data - The strategy commit contract data
33921
+ * @returns NotificationModel or null if action is not recognized
33712
33922
  */
33713
- const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
33714
- const intervalMs = intervalMinutes * MS_PER_MINUTE;
33715
- return Math.floor(timestamp / intervalMs) * intervalMs;
33923
+ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
33924
+ if (data.action === "partial-profit") {
33925
+ return {
33926
+ type: "partial_profit.commit",
33927
+ id: CREATE_KEY_FN$1(),
33928
+ timestamp: data.timestamp,
33929
+ backtest: data.backtest,
33930
+ symbol: data.symbol,
33931
+ strategyName: data.strategyName,
33932
+ exchangeName: data.exchangeName,
33933
+ signalId: data.signalId,
33934
+ percentToClose: data.percentToClose,
33935
+ currentPrice: data.currentPrice,
33936
+ position: data.position,
33937
+ priceOpen: data.priceOpen,
33938
+ priceTakeProfit: data.priceTakeProfit,
33939
+ priceStopLoss: data.priceStopLoss,
33940
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
33941
+ originalPriceStopLoss: data.originalPriceStopLoss,
33942
+ scheduledAt: data.scheduledAt,
33943
+ pendingAt: data.pendingAt,
33944
+ createdAt: data.timestamp,
33945
+ };
33946
+ }
33947
+ if (data.action === "partial-loss") {
33948
+ return {
33949
+ type: "partial_loss.commit",
33950
+ id: CREATE_KEY_FN$1(),
33951
+ timestamp: data.timestamp,
33952
+ backtest: data.backtest,
33953
+ symbol: data.symbol,
33954
+ strategyName: data.strategyName,
33955
+ exchangeName: data.exchangeName,
33956
+ signalId: data.signalId,
33957
+ percentToClose: data.percentToClose,
33958
+ currentPrice: data.currentPrice,
33959
+ position: data.position,
33960
+ priceOpen: data.priceOpen,
33961
+ priceTakeProfit: data.priceTakeProfit,
33962
+ priceStopLoss: data.priceStopLoss,
33963
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
33964
+ originalPriceStopLoss: data.originalPriceStopLoss,
33965
+ scheduledAt: data.scheduledAt,
33966
+ pendingAt: data.pendingAt,
33967
+ createdAt: data.timestamp,
33968
+ };
33969
+ }
33970
+ if (data.action === "breakeven") {
33971
+ return {
33972
+ type: "breakeven.commit",
33973
+ id: CREATE_KEY_FN$1(),
33974
+ timestamp: data.timestamp,
33975
+ backtest: data.backtest,
33976
+ symbol: data.symbol,
33977
+ strategyName: data.strategyName,
33978
+ exchangeName: data.exchangeName,
33979
+ signalId: data.signalId,
33980
+ currentPrice: data.currentPrice,
33981
+ position: data.position,
33982
+ priceOpen: data.priceOpen,
33983
+ priceTakeProfit: data.priceTakeProfit,
33984
+ priceStopLoss: data.priceStopLoss,
33985
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
33986
+ originalPriceStopLoss: data.originalPriceStopLoss,
33987
+ scheduledAt: data.scheduledAt,
33988
+ pendingAt: data.pendingAt,
33989
+ createdAt: data.timestamp,
33990
+ };
33991
+ }
33992
+ if (data.action === "trailing-stop") {
33993
+ return {
33994
+ type: "trailing_stop.commit",
33995
+ id: CREATE_KEY_FN$1(),
33996
+ timestamp: data.timestamp,
33997
+ backtest: data.backtest,
33998
+ symbol: data.symbol,
33999
+ strategyName: data.strategyName,
34000
+ exchangeName: data.exchangeName,
34001
+ signalId: data.signalId,
34002
+ percentShift: data.percentShift,
34003
+ currentPrice: data.currentPrice,
34004
+ position: data.position,
34005
+ priceOpen: data.priceOpen,
34006
+ priceTakeProfit: data.priceTakeProfit,
34007
+ priceStopLoss: data.priceStopLoss,
34008
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
34009
+ originalPriceStopLoss: data.originalPriceStopLoss,
34010
+ scheduledAt: data.scheduledAt,
34011
+ pendingAt: data.pendingAt,
34012
+ createdAt: data.timestamp,
34013
+ };
34014
+ }
34015
+ if (data.action === "trailing-take") {
34016
+ return {
34017
+ type: "trailing_take.commit",
34018
+ id: CREATE_KEY_FN$1(),
34019
+ timestamp: data.timestamp,
34020
+ backtest: data.backtest,
34021
+ symbol: data.symbol,
34022
+ strategyName: data.strategyName,
34023
+ exchangeName: data.exchangeName,
34024
+ signalId: data.signalId,
34025
+ percentShift: data.percentShift,
34026
+ currentPrice: data.currentPrice,
34027
+ position: data.position,
34028
+ priceOpen: data.priceOpen,
34029
+ priceTakeProfit: data.priceTakeProfit,
34030
+ priceStopLoss: data.priceStopLoss,
34031
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
34032
+ originalPriceStopLoss: data.originalPriceStopLoss,
34033
+ scheduledAt: data.scheduledAt,
34034
+ pendingAt: data.pendingAt,
34035
+ createdAt: data.timestamp,
34036
+ };
34037
+ }
34038
+ if (data.action === "activate-scheduled") {
34039
+ return {
34040
+ type: "activate_scheduled.commit",
34041
+ id: CREATE_KEY_FN$1(),
34042
+ timestamp: data.timestamp,
34043
+ backtest: data.backtest,
34044
+ symbol: data.symbol,
34045
+ strategyName: data.strategyName,
34046
+ exchangeName: data.exchangeName,
34047
+ signalId: data.signalId,
34048
+ activateId: data.activateId,
34049
+ currentPrice: data.currentPrice,
34050
+ position: data.position,
34051
+ priceOpen: data.priceOpen,
34052
+ priceTakeProfit: data.priceTakeProfit,
34053
+ priceStopLoss: data.priceStopLoss,
34054
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
34055
+ originalPriceStopLoss: data.originalPriceStopLoss,
34056
+ scheduledAt: data.scheduledAt,
34057
+ pendingAt: data.pendingAt,
34058
+ createdAt: data.timestamp,
34059
+ };
34060
+ }
34061
+ return null;
33716
34062
  };
33717
34063
  /**
33718
- * Creates exchange instance with methods resolved once during construction.
33719
- * Applies default implementations where schema methods are not provided.
33720
- *
33721
- * @param schema - Exchange schema from registry
33722
- * @returns Object with resolved exchange methods
33723
- */
33724
- const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
33725
- const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
33726
- const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
33727
- const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
33728
- const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
33729
- return {
33730
- getCandles,
33731
- formatQuantity,
33732
- formatPrice,
33733
- getOrderBook,
33734
- };
33735
- };
34064
+ * Creates a notification model for risk rejection events.
34065
+ * @param data - The risk contract data
34066
+ * @returns NotificationModel for risk rejection event
34067
+ */
34068
+ const CREATE_RISK_NOTIFICATION_FN = (data) => ({
34069
+ type: "risk.rejection",
34070
+ id: CREATE_KEY_FN$1(),
34071
+ timestamp: data.timestamp,
34072
+ backtest: data.backtest,
34073
+ symbol: data.symbol,
34074
+ strategyName: data.strategyName,
34075
+ exchangeName: data.exchangeName,
34076
+ rejectionNote: data.rejectionNote,
34077
+ rejectionId: data.rejectionId,
34078
+ activePositionCount: data.activePositionCount,
34079
+ currentPrice: data.currentPrice,
34080
+ signalId: data.currentSignal.id,
34081
+ position: data.currentSignal.position,
34082
+ priceOpen: data.currentSignal.priceOpen,
34083
+ priceTakeProfit: data.currentSignal.priceTakeProfit,
34084
+ priceStopLoss: data.currentSignal.priceStopLoss,
34085
+ minuteEstimatedTime: data.currentSignal.minuteEstimatedTime,
34086
+ signalNote: data.currentSignal.note,
34087
+ createdAt: data.timestamp,
34088
+ });
33736
34089
  /**
33737
- * Attempts to read candles from cache.
33738
- *
33739
- * Cache lookup calculates expected timestamps:
33740
- * sinceTimestamp + i * stepMs for i = 0..limit-1
33741
- * Returns all candles if found, null if any missing.
33742
- *
33743
- * @param dto - Data transfer object containing symbol, interval, and limit
33744
- * @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
33745
- * @param untilTimestamp - Unused, kept for API compatibility
33746
- * @param exchangeName - Exchange name
33747
- * @returns Cached candles array (exactly limit) or null if cache miss
34090
+ * Creates a notification model for error events.
34091
+ * @param error - The error object
34092
+ * @returns NotificationModel for error event
33748
34093
  */
33749
- const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
33750
- // PersistCandleAdapter.readCandlesData calculates expected timestamps:
33751
- // sinceTimestamp + i * stepMs for i = 0..limit-1
33752
- // Returns all candles if found, null if any missing
33753
- const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
33754
- // Return cached data only if we have exactly the requested limit
33755
- if (cachedCandles?.length === dto.limit) {
33756
- bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
33757
- return cachedCandles;
33758
- }
33759
- bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
33760
- return null;
33761
- }, {
33762
- fallback: async (error) => {
33763
- const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
33764
- const payload = {
33765
- error: errorData(error),
33766
- message: getErrorMessage(error),
33767
- };
33768
- bt.loggerService.warn(message, payload);
33769
- console.warn(message, payload);
33770
- errorEmitter.next(error);
33771
- },
33772
- defaultValue: null,
34094
+ const CREATE_ERROR_NOTIFICATION_FN = (error) => ({
34095
+ type: "error.info",
34096
+ id: CREATE_KEY_FN$1(),
34097
+ error: errorData(error),
34098
+ message: getErrorMessage(error),
34099
+ backtest: false,
33773
34100
  });
33774
34101
  /**
33775
- * Writes candles to cache with error handling.
33776
- *
33777
- * The candles passed to this function should be validated:
33778
- * - First candle.timestamp equals aligned sinceTimestamp (openTime)
33779
- * - Exact number of candles as requested (limit)
33780
- * - Sequential timestamps: sinceTimestamp + i * stepMs
33781
- *
33782
- * @param candles - Array of validated candle data to cache
33783
- * @param dto - Data transfer object containing symbol, interval, and limit
33784
- * @param exchangeName - Exchange name
34102
+ * Creates a notification model for critical error events.
34103
+ * @param error - The error object
34104
+ * @returns NotificationModel for critical error event
33785
34105
  */
33786
- const WRITE_CANDLES_CACHE_FN = trycatch(queued(async (candles, dto, exchangeName) => {
33787
- await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
33788
- bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
33789
- }), {
33790
- fallback: async (error) => {
33791
- const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
33792
- const payload = {
33793
- error: errorData(error),
33794
- message: getErrorMessage(error),
33795
- };
33796
- bt.loggerService.warn(message, payload);
33797
- console.warn(message, payload);
33798
- errorEmitter.next(error);
33799
- },
33800
- defaultValue: null,
34106
+ const CREATE_CRITICAL_ERROR_NOTIFICATION_FN = (error) => ({
34107
+ type: "error.critical",
34108
+ id: CREATE_KEY_FN$1(),
34109
+ error: errorData(error),
34110
+ message: getErrorMessage(error),
34111
+ backtest: false,
33801
34112
  });
33802
34113
  /**
33803
- * Instance class for exchange operations on a specific exchange.
33804
- *
33805
- * Provides isolated exchange operations for a single exchange.
33806
- * Each instance maintains its own context and exposes IExchangeSchema methods.
33807
- * The schema is retrieved once during construction for better performance.
34114
+ * Creates a notification model for validation error events.
34115
+ * @param error - The error object
34116
+ * @returns NotificationModel for validation error event
34117
+ */
34118
+ const CREATE_VALIDATION_ERROR_NOTIFICATION_FN = (error) => ({
34119
+ type: "error.validation",
34120
+ id: CREATE_KEY_FN$1(),
34121
+ error: errorData(error),
34122
+ message: getErrorMessage(error),
34123
+ backtest: false,
34124
+ });
34125
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_SIGNAL = "NotificationMemoryBacktestUtils.handleSignal";
34126
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_PROFIT = "NotificationMemoryBacktestUtils.handlePartialProfit";
34127
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationMemoryBacktestUtils.handlePartialLoss";
34128
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationMemoryBacktestUtils.handleBreakeven";
34129
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationMemoryBacktestUtils.handleStrategyCommit";
34130
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_RISK = "NotificationMemoryBacktestUtils.handleRisk";
34131
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_ERROR = "NotificationMemoryBacktestUtils.handleError";
34132
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationMemoryBacktestUtils.handleCriticalError";
34133
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_VALIDATION_ERROR = "NotificationMemoryBacktestUtils.handleValidationError";
34134
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_GET_DATA = "NotificationMemoryBacktestUtils.getData";
34135
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_CLEAR = "NotificationMemoryBacktestUtils.clear";
34136
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_SIGNAL = "NotificationMemoryLiveUtils.handleSignal";
34137
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_PARTIAL_PROFIT = "NotificationMemoryLiveUtils.handlePartialProfit";
34138
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationMemoryLiveUtils.handlePartialLoss";
34139
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationMemoryLiveUtils.handleBreakeven";
34140
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationMemoryLiveUtils.handleStrategyCommit";
34141
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_RISK = "NotificationMemoryLiveUtils.handleRisk";
34142
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_ERROR = "NotificationMemoryLiveUtils.handleError";
34143
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationMemoryLiveUtils.handleCriticalError";
34144
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_VALIDATION_ERROR = "NotificationMemoryLiveUtils.handleValidationError";
34145
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_GET_DATA = "NotificationMemoryLiveUtils.getData";
34146
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_CLEAR = "NotificationMemoryLiveUtils.clear";
34147
+ const NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE = "NotificationAdapter.enable";
34148
+ const NOTIFICATION_ADAPTER_METHOD_NAME_DISABLE = "NotificationAdapter.disable";
34149
+ const NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_BACKTEST = "NotificationAdapter.getDataBacktest";
34150
+ const NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_LIVE = "NotificationAdapter.getDataLive";
34151
+ const NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_BACKTEST = "NotificationAdapter.clearBacktest";
34152
+ const NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_LIVE = "NotificationAdapter.clearLive";
34153
+ const NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER = "NotificationBacktestAdapter.useNotificationAdapter";
34154
+ const NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY = "NotificationBacktestAdapter.useDummy";
34155
+ const NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY = "NotificationBacktestAdapter.useMemory";
34156
+ const NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST = "NotificationBacktestAdapter.usePersist";
34157
+ const NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_ADAPTER = "NotificationLiveAdapter.useNotificationAdapter";
34158
+ const NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_DUMMY = "NotificationLiveAdapter.useDummy";
34159
+ const NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_MEMORY = "NotificationLiveAdapter.useMemory";
34160
+ const NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_PERSIST = "NotificationLiveAdapter.usePersist";
34161
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_WAIT_FOR_INIT = "NotificationPersistBacktestUtils.waitForInit";
34162
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_UPDATE_NOTIFICATIONS = "NotificationPersistBacktestUtils._updateNotifications";
34163
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_SIGNAL = "NotificationPersistBacktestUtils.handleSignal";
34164
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_PROFIT = "NotificationPersistBacktestUtils.handlePartialProfit";
34165
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationPersistBacktestUtils.handlePartialLoss";
34166
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationPersistBacktestUtils.handleBreakeven";
34167
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationPersistBacktestUtils.handleStrategyCommit";
34168
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_RISK = "NotificationPersistBacktestUtils.handleRisk";
34169
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ERROR = "NotificationPersistBacktestUtils.handleError";
34170
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationPersistBacktestUtils.handleCriticalError";
34171
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_VALIDATION_ERROR = "NotificationPersistBacktestUtils.handleValidationError";
34172
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_GET_DATA = "NotificationPersistBacktestUtils.getData";
34173
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_CLEAR = "NotificationPersistBacktestUtils.clear";
34174
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_WAIT_FOR_INIT = "NotificationPersistLiveUtils.waitForInit";
34175
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_UPDATE_NOTIFICATIONS = "NotificationPersistLiveUtils._updateNotifications";
34176
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_SIGNAL = "NotificationPersistLiveUtils.handleSignal";
34177
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_PARTIAL_PROFIT = "NotificationPersistLiveUtils.handlePartialProfit";
34178
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationPersistLiveUtils.handlePartialLoss";
34179
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationPersistLiveUtils.handleBreakeven";
34180
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationPersistLiveUtils.handleStrategyCommit";
34181
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_RISK = "NotificationPersistLiveUtils.handleRisk";
34182
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_ERROR = "NotificationPersistLiveUtils.handleError";
34183
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationPersistLiveUtils.handleCriticalError";
34184
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_VALIDATION_ERROR = "NotificationPersistLiveUtils.handleValidationError";
34185
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_GET_DATA = "NotificationPersistLiveUtils.getData";
34186
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_CLEAR = "NotificationPersistLiveUtils.clear";
34187
+ /**
34188
+ * In-memory notification adapter for backtest signals.
33808
34189
  *
33809
- * @example
33810
- * ```typescript
33811
- * const instance = new ExchangeInstance("binance");
34190
+ * Features:
34191
+ * - Stores notifications in memory only (no persistence)
34192
+ * - Fast read/write operations
34193
+ * - Data is lost when application restarts
34194
+ * - Maintains up to MAX_NOTIFICATIONS (250) most recent notifications
34195
+ * - Handles all notification types: signals, partial profit/loss, breakeven, risk, errors
33812
34196
  *
33813
- * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
33814
- * const vwap = await instance.getAveragePrice("BTCUSDT");
33815
- * const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
33816
- * const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
33817
- * ```
34197
+ * Use this adapter for testing or when persistence is not required.
33818
34198
  */
33819
- class ExchangeInstance {
33820
- /**
33821
- * Creates a new ExchangeInstance for a specific exchange.
33822
- *
33823
- * @param exchangeName - Exchange name (e.g., "binance")
33824
- */
33825
- constructor(exchangeName) {
33826
- this.exchangeName = exchangeName;
34199
+ class NotificationMemoryBacktestUtils {
34200
+ constructor() {
34201
+ /** Array of notification models */
34202
+ this._notifications = [];
33827
34203
  /**
33828
- * Fetch candles from data source (API or database).
33829
- *
33830
- * Automatically calculates the start date based on Date.now() and the requested interval/limit.
33831
- * Uses the same logic as ClientExchange to ensure backwards compatibility.
33832
- *
33833
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
33834
- * @param interval - Candle time interval (e.g., "1m", "1h")
33835
- * @param limit - Maximum number of candles to fetch
33836
- * @returns Promise resolving to array of OHLCV candle data
33837
- *
33838
- * @example
33839
- * ```typescript
33840
- * const instance = new ExchangeInstance("binance");
33841
- * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
33842
- * ```
34204
+ * Handles signal events.
34205
+ * Creates and stores notification for opened, closed, scheduled, cancelled signals.
34206
+ * @param data - The strategy tick result data
33843
34207
  */
33844
- this.getCandles = async (symbol, interval, limit) => {
33845
- bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_CANDLES, {
33846
- exchangeName: this.exchangeName,
33847
- symbol,
33848
- interval,
33849
- limit,
34208
+ this.handleSignal = async (data) => {
34209
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_SIGNAL, {
34210
+ signalId: data.signal.id,
34211
+ action: data.action,
33850
34212
  });
33851
- const getCandles = this._methods.getCandles;
33852
- const step = INTERVAL_MINUTES$1[interval];
33853
- if (!step) {
33854
- throw new Error(`ExchangeInstance unknown interval=${interval}`);
33855
- }
33856
- const stepMs = step * MS_PER_MINUTE;
33857
- // Align when down to interval boundary
33858
- const when = await GET_TIMESTAMP_FN();
33859
- const whenTimestamp = when.getTime();
33860
- const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
33861
- // Calculate since: go back limit candles from aligned when
33862
- const sinceTimestamp = alignedWhen - limit * stepMs;
33863
- const since = new Date(sinceTimestamp);
33864
- const untilTimestamp = alignedWhen;
33865
- // Try to read from cache first
33866
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
33867
- if (cachedCandles !== null) {
33868
- return cachedCandles;
33869
- }
33870
- let allData = [];
33871
- // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
33872
- if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
33873
- let remaining = limit;
33874
- let currentSince = new Date(since.getTime());
33875
- const isBacktest = await GET_BACKTEST_FN();
33876
- while (remaining > 0) {
33877
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
33878
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
33879
- allData.push(...chunkData);
33880
- remaining -= chunkLimit;
33881
- if (remaining > 0) {
33882
- // Move currentSince forward by the number of candles fetched
33883
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
33884
- }
33885
- }
33886
- }
33887
- else {
33888
- const isBacktest = await GET_BACKTEST_FN();
33889
- allData = await getCandles(symbol, interval, since, limit, isBacktest);
33890
- }
33891
- // Apply distinct by timestamp to remove duplicates
33892
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
33893
- if (allData.length !== uniqueData.length) {
33894
- bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
33895
- }
33896
- // Validate adapter returned data
33897
- if (uniqueData.length === 0) {
33898
- throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
33899
- `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
33900
- }
33901
- if (uniqueData[0].timestamp !== sinceTimestamp) {
33902
- throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
33903
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
33904
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
33905
- }
33906
- if (uniqueData.length !== limit) {
33907
- throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
33908
- `Expected ${limit} candles, got ${uniqueData.length}. ` +
33909
- `Adapter must return exact number of candles requested.`);
34213
+ const notification = CREATE_SIGNAL_NOTIFICATION_FN(data);
34214
+ if (notification) {
34215
+ this._addNotification(notification);
33910
34216
  }
33911
- // Write to cache after successful fetch
33912
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
33913
- return uniqueData;
33914
34217
  };
33915
34218
  /**
33916
- * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
33917
- * The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
33918
- *
33919
- * Formula:
33920
- * - Typical Price = (high + low + close) / 3
33921
- * - VWAP = sum(typical_price * volume) / sum(volume)
33922
- *
33923
- * If volume is zero, returns simple average of close prices.
33924
- *
33925
- * @param symbol - Trading pair symbol
33926
- * @returns Promise resolving to VWAP price
33927
- * @throws Error if no candles available
33928
- *
33929
- * @example
33930
- * ```typescript
33931
- * const instance = new ExchangeInstance("binance");
33932
- * const vwap = await instance.getAveragePrice("BTCUSDT");
33933
- * console.log(vwap); // 50125.43
33934
- * ```
34219
+ * Handles partial profit availability event.
34220
+ * @param data - The partial profit contract data
33935
34221
  */
33936
- this.getAveragePrice = async (symbol) => {
33937
- bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
33938
- exchangeName: this.exchangeName,
33939
- symbol,
34222
+ this.handlePartialProfit = async (data) => {
34223
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_PROFIT, {
34224
+ signalId: data.data.id,
34225
+ level: data.level,
33940
34226
  });
33941
- const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
34227
+ this._addNotification(CREATE_PARTIAL_PROFIT_NOTIFICATION_FN(data));
34228
+ };
34229
+ /**
34230
+ * Handles partial loss availability event.
34231
+ * @param data - The partial loss contract data
34232
+ */
34233
+ this.handlePartialLoss = async (data) => {
34234
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS, {
34235
+ signalId: data.data.id,
34236
+ level: data.level,
34237
+ });
34238
+ this._addNotification(CREATE_PARTIAL_LOSS_NOTIFICATION_FN(data));
34239
+ };
34240
+ /**
34241
+ * Handles breakeven availability event.
34242
+ * @param data - The breakeven contract data
34243
+ */
34244
+ this.handleBreakeven = async (data) => {
34245
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN, {
34246
+ signalId: data.data.id,
34247
+ });
34248
+ this._addNotification(CREATE_BREAKEVEN_NOTIFICATION_FN(data));
34249
+ };
34250
+ /**
34251
+ * Handles strategy commit events.
34252
+ * @param data - The strategy commit contract data
34253
+ */
34254
+ this.handleStrategyCommit = async (data) => {
34255
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT, {
34256
+ signalId: data.signalId,
34257
+ action: data.action,
34258
+ });
34259
+ const notification = CREATE_STRATEGY_COMMIT_NOTIFICATION_FN(data);
34260
+ if (notification) {
34261
+ this._addNotification(notification);
34262
+ }
34263
+ };
34264
+ /**
34265
+ * Handles risk rejection event.
34266
+ * @param data - The risk contract data
34267
+ */
34268
+ this.handleRisk = async (data) => {
34269
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_RISK, {
34270
+ signalId: data.currentSignal.id,
34271
+ rejectionId: data.rejectionId,
34272
+ });
34273
+ this._addNotification(CREATE_RISK_NOTIFICATION_FN(data));
34274
+ };
34275
+ /**
34276
+ * Handles error event.
34277
+ * @param error - The error object
34278
+ */
34279
+ this.handleError = async (error) => {
34280
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_ERROR, {
34281
+ message: getErrorMessage(error),
34282
+ });
34283
+ this._addNotification(CREATE_ERROR_NOTIFICATION_FN(error));
34284
+ };
34285
+ /**
34286
+ * Handles critical error event.
34287
+ * @param error - The error object
34288
+ */
34289
+ this.handleCriticalError = async (error) => {
34290
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR, {
34291
+ message: getErrorMessage(error),
34292
+ });
34293
+ this._addNotification(CREATE_CRITICAL_ERROR_NOTIFICATION_FN(error));
34294
+ };
34295
+ /**
34296
+ * Handles validation error event.
34297
+ * @param error - The error object
34298
+ */
34299
+ this.handleValidationError = async (error) => {
34300
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_VALIDATION_ERROR, {
34301
+ message: getErrorMessage(error),
34302
+ });
34303
+ this._addNotification(CREATE_VALIDATION_ERROR_NOTIFICATION_FN(error));
34304
+ };
34305
+ /**
34306
+ * Gets all stored notifications.
34307
+ * @returns Copy of notifications array
34308
+ */
34309
+ this.getData = async () => {
34310
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_GET_DATA);
34311
+ return [...this._notifications];
34312
+ };
34313
+ /**
34314
+ * Clears all stored notifications.
34315
+ */
34316
+ this.clear = async () => {
34317
+ bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_CLEAR);
34318
+ this._notifications = [];
34319
+ };
34320
+ }
34321
+ /**
34322
+ * Adds a notification to the beginning of the list.
34323
+ * Removes oldest notification if limit is exceeded.
34324
+ * @param notification - The notification model to add
34325
+ */
34326
+ _addNotification(notification) {
34327
+ this._notifications.unshift(notification);
34328
+ if (this._notifications.length > MAX_NOTIFICATIONS) {
34329
+ this._notifications.pop();
34330
+ }
34331
+ }
34332
+ }
34333
+ /**
34334
+ * Dummy notification adapter for backtest signals that discards all writes.
34335
+ *
34336
+ * Features:
34337
+ * - No-op implementation for all methods
34338
+ * - getData always returns empty array
34339
+ *
34340
+ * Use this adapter to disable backtest notification storage completely.
34341
+ */
34342
+ class NotificationDummyBacktestUtils {
34343
+ constructor() {
34344
+ /**
34345
+ * No-op handler for signal events.
34346
+ */
34347
+ this.handleSignal = async () => {
34348
+ };
34349
+ /**
34350
+ * No-op handler for partial profit event.
34351
+ */
34352
+ this.handlePartialProfit = async () => {
34353
+ };
34354
+ /**
34355
+ * No-op handler for partial loss event.
34356
+ */
34357
+ this.handlePartialLoss = async () => {
34358
+ };
34359
+ /**
34360
+ * No-op handler for breakeven event.
34361
+ */
34362
+ this.handleBreakeven = async () => {
34363
+ };
34364
+ /**
34365
+ * No-op handler for strategy commit event.
34366
+ */
34367
+ this.handleStrategyCommit = async () => {
34368
+ };
34369
+ /**
34370
+ * No-op handler for risk rejection event.
34371
+ */
34372
+ this.handleRisk = async () => {
34373
+ };
34374
+ /**
34375
+ * No-op handler for error event.
34376
+ */
34377
+ this.handleError = async () => {
34378
+ };
34379
+ /**
34380
+ * No-op handler for critical error event.
34381
+ */
34382
+ this.handleCriticalError = async () => {
34383
+ };
34384
+ /**
34385
+ * No-op handler for validation error event.
34386
+ */
34387
+ this.handleValidationError = async () => {
34388
+ };
34389
+ /**
34390
+ * Always returns empty array (no storage).
34391
+ * @returns Empty array
34392
+ */
34393
+ this.getData = async () => {
34394
+ return [];
34395
+ };
34396
+ /**
34397
+ * No-op clear operation.
34398
+ */
34399
+ this.clear = async () => {
34400
+ };
34401
+ }
34402
+ }
34403
+ /**
34404
+ * Persistent notification adapter for backtest signals.
34405
+ *
34406
+ * Features:
34407
+ * - Persists notifications to disk using PersistNotificationAdapter
34408
+ * - Lazy initialization with singleshot pattern
34409
+ * - Maintains up to MAX_NOTIFICATIONS (250) most recent notifications
34410
+ * - Handles all notification types: signals, partial profit/loss, breakeven, risk, errors
34411
+ *
34412
+ * Use this adapter (default) for backtest notification persistence across sessions.
34413
+ */
34414
+ class NotificationPersistBacktestUtils {
34415
+ constructor() {
34416
+ /**
34417
+ * Singleshot initialization function that loads notifications from disk.
34418
+ * Protected by singleshot to ensure one-time execution.
34419
+ */
34420
+ this.waitForInit = singleshot(async () => {
34421
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_WAIT_FOR_INIT);
34422
+ const notificationList = await PersistNotificationAdapter.readNotificationData(true);
34423
+ notificationList.sort((a, b) => {
34424
+ const aTime = 'createdAt' in a ? a.createdAt : 0;
34425
+ const bTime = 'createdAt' in b ? b.createdAt : 0;
34426
+ return aTime - bTime;
34427
+ });
34428
+ this._notifications = new Map(notificationList
34429
+ .slice(-MAX_NOTIFICATIONS)
34430
+ .map((notification) => [notification.id, notification]));
34431
+ });
34432
+ /**
34433
+ * Handles signal events.
34434
+ * Creates and stores notification for opened, closed, scheduled, cancelled signals.
34435
+ * @param data - The strategy tick result data
34436
+ */
34437
+ this.handleSignal = async (data) => {
34438
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_SIGNAL, {
34439
+ signalId: data.signal.id,
34440
+ action: data.action,
34441
+ });
34442
+ await this.waitForInit();
34443
+ const notification = CREATE_SIGNAL_NOTIFICATION_FN(data);
34444
+ if (notification) {
34445
+ this._addNotification(notification);
34446
+ await this._updateNotifications();
34447
+ }
34448
+ };
34449
+ /**
34450
+ * Handles partial profit availability event.
34451
+ * @param data - The partial profit contract data
34452
+ */
34453
+ this.handlePartialProfit = async (data) => {
34454
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_PROFIT, {
34455
+ signalId: data.data.id,
34456
+ level: data.level,
34457
+ });
34458
+ await this.waitForInit();
34459
+ this._addNotification(CREATE_PARTIAL_PROFIT_NOTIFICATION_FN(data));
34460
+ await this._updateNotifications();
34461
+ };
34462
+ /**
34463
+ * Handles partial loss availability event.
34464
+ * @param data - The partial loss contract data
34465
+ */
34466
+ this.handlePartialLoss = async (data) => {
34467
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS, {
34468
+ signalId: data.data.id,
34469
+ level: data.level,
34470
+ });
34471
+ await this.waitForInit();
34472
+ this._addNotification(CREATE_PARTIAL_LOSS_NOTIFICATION_FN(data));
34473
+ await this._updateNotifications();
34474
+ };
34475
+ /**
34476
+ * Handles breakeven availability event.
34477
+ * @param data - The breakeven contract data
34478
+ */
34479
+ this.handleBreakeven = async (data) => {
34480
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN, {
34481
+ signalId: data.data.id,
34482
+ });
34483
+ await this.waitForInit();
34484
+ this._addNotification(CREATE_BREAKEVEN_NOTIFICATION_FN(data));
34485
+ await this._updateNotifications();
34486
+ };
34487
+ /**
34488
+ * Handles strategy commit events.
34489
+ * @param data - The strategy commit contract data
34490
+ */
34491
+ this.handleStrategyCommit = async (data) => {
34492
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT, {
34493
+ signalId: data.signalId,
34494
+ action: data.action,
34495
+ });
34496
+ await this.waitForInit();
34497
+ const notification = CREATE_STRATEGY_COMMIT_NOTIFICATION_FN(data);
34498
+ if (notification) {
34499
+ this._addNotification(notification);
34500
+ await this._updateNotifications();
34501
+ }
34502
+ };
34503
+ /**
34504
+ * Handles risk rejection event.
34505
+ * @param data - The risk contract data
34506
+ */
34507
+ this.handleRisk = async (data) => {
34508
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_RISK, {
34509
+ signalId: data.currentSignal.id,
34510
+ rejectionId: data.rejectionId,
34511
+ });
34512
+ await this.waitForInit();
34513
+ this._addNotification(CREATE_RISK_NOTIFICATION_FN(data));
34514
+ await this._updateNotifications();
34515
+ };
34516
+ /**
34517
+ * Handles error event.
34518
+ * Note: Error notifications are not persisted to disk.
34519
+ * @param error - The error object
34520
+ */
34521
+ this.handleError = async (error) => {
34522
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ERROR, {
34523
+ message: getErrorMessage(error),
34524
+ });
34525
+ await this.waitForInit();
34526
+ this._addNotification(CREATE_ERROR_NOTIFICATION_FN(error));
34527
+ };
34528
+ /**
34529
+ * Handles critical error event.
34530
+ * Note: Error notifications are not persisted to disk.
34531
+ * @param error - The error object
34532
+ */
34533
+ this.handleCriticalError = async (error) => {
34534
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR, {
34535
+ message: getErrorMessage(error),
34536
+ });
34537
+ await this.waitForInit();
34538
+ this._addNotification(CREATE_CRITICAL_ERROR_NOTIFICATION_FN(error));
34539
+ };
34540
+ /**
34541
+ * Handles validation error event.
34542
+ * Note: Error notifications are not persisted to disk.
34543
+ * @param error - The error object
34544
+ */
34545
+ this.handleValidationError = async (error) => {
34546
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_VALIDATION_ERROR, {
34547
+ message: getErrorMessage(error),
34548
+ });
34549
+ await this.waitForInit();
34550
+ this._addNotification(CREATE_VALIDATION_ERROR_NOTIFICATION_FN(error));
34551
+ };
34552
+ /**
34553
+ * Gets all stored notifications.
34554
+ * @returns Array of all notification models
34555
+ */
34556
+ this.getData = async () => {
34557
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_GET_DATA);
34558
+ await this.waitForInit();
34559
+ return Array.from(this._notifications.values());
34560
+ };
34561
+ /**
34562
+ * Clears all stored notifications.
34563
+ */
34564
+ this.clear = async () => {
34565
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_CLEAR);
34566
+ await this.waitForInit();
34567
+ this._notifications.clear();
34568
+ await this._updateNotifications();
34569
+ };
34570
+ }
34571
+ /**
34572
+ * Persists the current notification map to disk storage.
34573
+ * Sorts notifications by createdAt and keeps only the most recent MAX_NOTIFICATIONS.
34574
+ * @throws Error if not initialized
34575
+ */
34576
+ async _updateNotifications() {
34577
+ bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_UPDATE_NOTIFICATIONS);
34578
+ if (!this._notifications) {
34579
+ throw new Error("NotificationPersistBacktestUtils not initialized. Call waitForInit first.");
34580
+ }
34581
+ const notificationList = Array.from(this._notifications.values());
34582
+ notificationList.sort((a, b) => {
34583
+ const aTime = 'createdAt' in a ? a.createdAt : 0;
34584
+ const bTime = 'createdAt' in b ? b.createdAt : 0;
34585
+ return aTime - bTime;
34586
+ });
34587
+ await PersistNotificationAdapter.writeNotificationData(notificationList.slice(-MAX_NOTIFICATIONS), true);
34588
+ }
34589
+ /**
34590
+ * Adds a notification to the map.
34591
+ * Removes oldest notification if limit is exceeded.
34592
+ * @param notification - The notification model to add
34593
+ */
34594
+ _addNotification(notification) {
34595
+ this._notifications.set(notification.id, notification);
34596
+ if (this._notifications.size > MAX_NOTIFICATIONS) {
34597
+ const firstKey = this._notifications.keys().next().value;
34598
+ if (firstKey) {
34599
+ this._notifications.delete(firstKey);
34600
+ }
34601
+ }
34602
+ }
34603
+ }
34604
+ /**
34605
+ * In-memory notification adapter for live trading signals.
34606
+ *
34607
+ * Features:
34608
+ * - Stores notifications in memory only (no persistence)
34609
+ * - Fast read/write operations
34610
+ * - Data is lost when application restarts
34611
+ * - Maintains up to MAX_NOTIFICATIONS (250) most recent notifications
34612
+ * - Handles all notification types: signals, partial profit/loss, breakeven, risk, errors
34613
+ *
34614
+ * Use this adapter for testing or when persistence is not required.
34615
+ */
34616
+ class NotificationMemoryLiveUtils {
34617
+ constructor() {
34618
+ /** Array of notification models */
34619
+ this._notifications = [];
34620
+ /**
34621
+ * Handles signal events.
34622
+ * Creates and stores notification for opened, closed, scheduled, cancelled signals.
34623
+ * @param data - The strategy tick result data
34624
+ */
34625
+ this.handleSignal = async (data) => {
34626
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_SIGNAL, {
34627
+ signalId: data.signal.id,
34628
+ action: data.action,
34629
+ });
34630
+ const notification = CREATE_SIGNAL_NOTIFICATION_FN(data);
34631
+ if (notification) {
34632
+ this._addNotification(notification);
34633
+ }
34634
+ };
34635
+ /**
34636
+ * Handles partial profit availability event.
34637
+ * @param data - The partial profit contract data
34638
+ */
34639
+ this.handlePartialProfit = async (data) => {
34640
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_PARTIAL_PROFIT, {
34641
+ signalId: data.data.id,
34642
+ level: data.level,
34643
+ });
34644
+ this._addNotification(CREATE_PARTIAL_PROFIT_NOTIFICATION_FN(data));
34645
+ };
34646
+ /**
34647
+ * Handles partial loss availability event.
34648
+ * @param data - The partial loss contract data
34649
+ */
34650
+ this.handlePartialLoss = async (data) => {
34651
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS, {
34652
+ signalId: data.data.id,
34653
+ level: data.level,
34654
+ });
34655
+ this._addNotification(CREATE_PARTIAL_LOSS_NOTIFICATION_FN(data));
34656
+ };
34657
+ /**
34658
+ * Handles breakeven availability event.
34659
+ * @param data - The breakeven contract data
34660
+ */
34661
+ this.handleBreakeven = async (data) => {
34662
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_BREAKEVEN, {
34663
+ signalId: data.data.id,
34664
+ });
34665
+ this._addNotification(CREATE_BREAKEVEN_NOTIFICATION_FN(data));
34666
+ };
34667
+ /**
34668
+ * Handles strategy commit events.
34669
+ * @param data - The strategy commit contract data
34670
+ */
34671
+ this.handleStrategyCommit = async (data) => {
34672
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT, {
34673
+ signalId: data.signalId,
34674
+ action: data.action,
34675
+ });
34676
+ const notification = CREATE_STRATEGY_COMMIT_NOTIFICATION_FN(data);
34677
+ if (notification) {
34678
+ this._addNotification(notification);
34679
+ }
34680
+ };
34681
+ /**
34682
+ * Handles risk rejection event.
34683
+ * @param data - The risk contract data
34684
+ */
34685
+ this.handleRisk = async (data) => {
34686
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_RISK, {
34687
+ signalId: data.currentSignal.id,
34688
+ rejectionId: data.rejectionId,
34689
+ });
34690
+ this._addNotification(CREATE_RISK_NOTIFICATION_FN(data));
34691
+ };
34692
+ /**
34693
+ * Handles error event.
34694
+ * @param error - The error object
34695
+ */
34696
+ this.handleError = async (error) => {
34697
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_ERROR, {
34698
+ message: getErrorMessage(error),
34699
+ });
34700
+ this._addNotification(CREATE_ERROR_NOTIFICATION_FN(error));
34701
+ };
34702
+ /**
34703
+ * Handles critical error event.
34704
+ * @param error - The error object
34705
+ */
34706
+ this.handleCriticalError = async (error) => {
34707
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR, {
34708
+ message: getErrorMessage(error),
34709
+ });
34710
+ this._addNotification(CREATE_CRITICAL_ERROR_NOTIFICATION_FN(error));
34711
+ };
34712
+ /**
34713
+ * Handles validation error event.
34714
+ * @param error - The error object
34715
+ */
34716
+ this.handleValidationError = async (error) => {
34717
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_VALIDATION_ERROR, {
34718
+ message: getErrorMessage(error),
34719
+ });
34720
+ this._addNotification(CREATE_VALIDATION_ERROR_NOTIFICATION_FN(error));
34721
+ };
34722
+ /**
34723
+ * Gets all stored notifications.
34724
+ * @returns Copy of notifications array
34725
+ */
34726
+ this.getData = async () => {
34727
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_GET_DATA);
34728
+ return [...this._notifications];
34729
+ };
34730
+ /**
34731
+ * Clears all stored notifications.
34732
+ */
34733
+ this.clear = async () => {
34734
+ bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_CLEAR);
34735
+ this._notifications = [];
34736
+ };
34737
+ }
34738
+ /**
34739
+ * Adds a notification to the beginning of the list.
34740
+ * Removes oldest notification if limit is exceeded.
34741
+ * @param notification - The notification model to add
34742
+ */
34743
+ _addNotification(notification) {
34744
+ this._notifications.unshift(notification);
34745
+ if (this._notifications.length > MAX_NOTIFICATIONS) {
34746
+ this._notifications.pop();
34747
+ }
34748
+ }
34749
+ }
34750
+ /**
34751
+ * Dummy notification adapter for live trading signals that discards all writes.
34752
+ *
34753
+ * Features:
34754
+ * - No-op implementation for all methods
34755
+ * - getData always returns empty array
34756
+ *
34757
+ * Use this adapter to disable live notification storage completely.
34758
+ */
34759
+ class NotificationDummyLiveUtils {
34760
+ constructor() {
34761
+ /**
34762
+ * No-op handler for signal events.
34763
+ */
34764
+ this.handleSignal = async () => {
34765
+ };
34766
+ /**
34767
+ * No-op handler for partial profit event.
34768
+ */
34769
+ this.handlePartialProfit = async () => {
34770
+ };
34771
+ /**
34772
+ * No-op handler for partial loss event.
34773
+ */
34774
+ this.handlePartialLoss = async () => {
34775
+ };
34776
+ /**
34777
+ * No-op handler for breakeven event.
34778
+ */
34779
+ this.handleBreakeven = async () => {
34780
+ };
34781
+ /**
34782
+ * No-op handler for strategy commit event.
34783
+ */
34784
+ this.handleStrategyCommit = async () => {
34785
+ };
34786
+ /**
34787
+ * No-op handler for risk rejection event.
34788
+ */
34789
+ this.handleRisk = async () => {
34790
+ };
34791
+ /**
34792
+ * No-op handler for error event.
34793
+ */
34794
+ this.handleError = async () => {
34795
+ };
34796
+ /**
34797
+ * No-op handler for critical error event.
34798
+ */
34799
+ this.handleCriticalError = async () => {
34800
+ };
34801
+ /**
34802
+ * No-op handler for validation error event.
34803
+ */
34804
+ this.handleValidationError = async () => {
34805
+ };
34806
+ /**
34807
+ * Always returns empty array (no storage).
34808
+ * @returns Empty array
34809
+ */
34810
+ this.getData = async () => {
34811
+ return [];
34812
+ };
34813
+ /**
34814
+ * No-op clear operation.
34815
+ */
34816
+ this.clear = async () => {
34817
+ };
34818
+ }
34819
+ }
34820
+ /**
34821
+ * Persistent notification adapter for live trading signals.
34822
+ *
34823
+ * Features:
34824
+ * - Persists notifications to disk using PersistNotificationAdapter
34825
+ * - Lazy initialization with singleshot pattern
34826
+ * - Maintains up to MAX_NOTIFICATIONS (250) most recent notifications
34827
+ * - Filters out error notifications when persisting to disk
34828
+ * - Handles all notification types: signals, partial profit/loss, breakeven, risk, errors
34829
+ *
34830
+ * Use this adapter (default) for live notification persistence across sessions.
34831
+ */
34832
+ class NotificationPersistLiveUtils {
34833
+ constructor() {
34834
+ /**
34835
+ * Singleshot initialization function that loads notifications from disk.
34836
+ * Protected by singleshot to ensure one-time execution.
34837
+ */
34838
+ this.waitForInit = singleshot(async () => {
34839
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_WAIT_FOR_INIT);
34840
+ const notificationList = await PersistNotificationAdapter.readNotificationData(false);
34841
+ notificationList.sort((a, b) => {
34842
+ const aTime = 'createdAt' in a ? a.createdAt : 0;
34843
+ const bTime = 'createdAt' in b ? b.createdAt : 0;
34844
+ return aTime - bTime;
34845
+ });
34846
+ this._notifications = new Map(notificationList
34847
+ .slice(-MAX_NOTIFICATIONS)
34848
+ .map((notification) => [notification.id, notification]));
34849
+ });
34850
+ /**
34851
+ * Handles signal events.
34852
+ * Creates and stores notification for opened, closed, scheduled, cancelled signals.
34853
+ * @param data - The strategy tick result data
34854
+ */
34855
+ this.handleSignal = async (data) => {
34856
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_SIGNAL, {
34857
+ signalId: data.signal.id,
34858
+ action: data.action,
34859
+ });
34860
+ await this.waitForInit();
34861
+ const notification = CREATE_SIGNAL_NOTIFICATION_FN(data);
34862
+ if (notification) {
34863
+ this._addNotification(notification);
34864
+ await this._updateNotifications();
34865
+ }
34866
+ };
34867
+ /**
34868
+ * Handles partial profit availability event.
34869
+ * @param data - The partial profit contract data
34870
+ */
34871
+ this.handlePartialProfit = async (data) => {
34872
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_PARTIAL_PROFIT, {
34873
+ signalId: data.data.id,
34874
+ level: data.level,
34875
+ });
34876
+ await this.waitForInit();
34877
+ this._addNotification(CREATE_PARTIAL_PROFIT_NOTIFICATION_FN(data));
34878
+ await this._updateNotifications();
34879
+ };
34880
+ /**
34881
+ * Handles partial loss availability event.
34882
+ * @param data - The partial loss contract data
34883
+ */
34884
+ this.handlePartialLoss = async (data) => {
34885
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS, {
34886
+ signalId: data.data.id,
34887
+ level: data.level,
34888
+ });
34889
+ await this.waitForInit();
34890
+ this._addNotification(CREATE_PARTIAL_LOSS_NOTIFICATION_FN(data));
34891
+ await this._updateNotifications();
34892
+ };
34893
+ /**
34894
+ * Handles breakeven availability event.
34895
+ * @param data - The breakeven contract data
34896
+ */
34897
+ this.handleBreakeven = async (data) => {
34898
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_BREAKEVEN, {
34899
+ signalId: data.data.id,
34900
+ });
34901
+ await this.waitForInit();
34902
+ this._addNotification(CREATE_BREAKEVEN_NOTIFICATION_FN(data));
34903
+ await this._updateNotifications();
34904
+ };
34905
+ /**
34906
+ * Handles strategy commit events.
34907
+ * @param data - The strategy commit contract data
34908
+ */
34909
+ this.handleStrategyCommit = async (data) => {
34910
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT, {
34911
+ signalId: data.signalId,
34912
+ action: data.action,
34913
+ });
34914
+ await this.waitForInit();
34915
+ const notification = CREATE_STRATEGY_COMMIT_NOTIFICATION_FN(data);
34916
+ if (notification) {
34917
+ this._addNotification(notification);
34918
+ await this._updateNotifications();
34919
+ }
34920
+ };
34921
+ /**
34922
+ * Handles risk rejection event.
34923
+ * @param data - The risk contract data
34924
+ */
34925
+ this.handleRisk = async (data) => {
34926
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_RISK, {
34927
+ signalId: data.currentSignal.id,
34928
+ rejectionId: data.rejectionId,
34929
+ });
34930
+ await this.waitForInit();
34931
+ this._addNotification(CREATE_RISK_NOTIFICATION_FN(data));
34932
+ await this._updateNotifications();
34933
+ };
34934
+ /**
34935
+ * Handles error event.
34936
+ * Note: Error notifications are not persisted to disk.
34937
+ * @param error - The error object
34938
+ */
34939
+ this.handleError = async (error) => {
34940
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_ERROR, {
34941
+ message: getErrorMessage(error),
34942
+ });
34943
+ await this.waitForInit();
34944
+ this._addNotification(CREATE_ERROR_NOTIFICATION_FN(error));
34945
+ };
34946
+ /**
34947
+ * Handles critical error event.
34948
+ * Note: Error notifications are not persisted to disk.
34949
+ * @param error - The error object
34950
+ */
34951
+ this.handleCriticalError = async (error) => {
34952
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR, {
34953
+ message: getErrorMessage(error),
34954
+ });
34955
+ await this.waitForInit();
34956
+ this._addNotification(CREATE_CRITICAL_ERROR_NOTIFICATION_FN(error));
34957
+ };
34958
+ /**
34959
+ * Handles validation error event.
34960
+ * Note: Error notifications are not persisted to disk.
34961
+ * @param error - The error object
34962
+ */
34963
+ this.handleValidationError = async (error) => {
34964
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_VALIDATION_ERROR, {
34965
+ message: getErrorMessage(error),
34966
+ });
34967
+ await this.waitForInit();
34968
+ this._addNotification(CREATE_VALIDATION_ERROR_NOTIFICATION_FN(error));
34969
+ };
34970
+ /**
34971
+ * Gets all stored notifications.
34972
+ * @returns Array of all notification models
34973
+ */
34974
+ this.getData = async () => {
34975
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_GET_DATA);
34976
+ await this.waitForInit();
34977
+ return Array.from(this._notifications.values());
34978
+ };
34979
+ /**
34980
+ * Clears all stored notifications.
34981
+ */
34982
+ this.clear = async () => {
34983
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_CLEAR);
34984
+ await this.waitForInit();
34985
+ this._notifications.clear();
34986
+ await this._updateNotifications();
34987
+ };
34988
+ }
34989
+ /**
34990
+ * Persists the current notification map to disk storage.
34991
+ * Filters out error notifications and sorts by createdAt.
34992
+ * Keeps only the most recent MAX_NOTIFICATIONS.
34993
+ * @throws Error if not initialized
34994
+ */
34995
+ async _updateNotifications() {
34996
+ bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_UPDATE_NOTIFICATIONS);
34997
+ if (!this._notifications) {
34998
+ throw new Error("NotificationPersistLiveUtils not initialized. Call waitForInit first.");
34999
+ }
35000
+ const notificationList = Array.from(this._notifications.values())
35001
+ .filter(({ type }) => !type.startsWith("error."));
35002
+ notificationList.sort((a, b) => {
35003
+ const aTime = 'createdAt' in a ? a.createdAt : 0;
35004
+ const bTime = 'createdAt' in b ? b.createdAt : 0;
35005
+ return aTime - bTime;
35006
+ });
35007
+ await PersistNotificationAdapter.writeNotificationData(notificationList.slice(-MAX_NOTIFICATIONS), false);
35008
+ }
35009
+ /**
35010
+ * Adds a notification to the map.
35011
+ * Removes oldest notification if limit is exceeded.
35012
+ * @param notification - The notification model to add
35013
+ */
35014
+ _addNotification(notification) {
35015
+ this._notifications.set(notification.id, notification);
35016
+ if (this._notifications.size > MAX_NOTIFICATIONS) {
35017
+ const firstKey = this._notifications.keys().next().value;
35018
+ if (firstKey) {
35019
+ this._notifications.delete(firstKey);
35020
+ }
35021
+ }
35022
+ }
35023
+ }
35024
+ /**
35025
+ * Backtest notification adapter with pluggable notification backend.
35026
+ *
35027
+ * Features:
35028
+ * - Adapter pattern for swappable notification implementations
35029
+ * - Default adapter: NotificationMemoryBacktestUtils (in-memory storage)
35030
+ * - Alternative adapters: NotificationPersistBacktestUtils, NotificationDummyBacktestUtils
35031
+ * - Convenience methods: usePersist(), useMemory(), useDummy()
35032
+ */
35033
+ class NotificationBacktestAdapter {
35034
+ constructor() {
35035
+ /** Internal notification utils instance */
35036
+ this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
35037
+ /**
35038
+ * Handles signal events.
35039
+ * Proxies call to the underlying notification adapter.
35040
+ * @param data - The strategy tick result data
35041
+ */
35042
+ this.handleSignal = async (data) => {
35043
+ return await this._notificationBacktestUtils.handleSignal(data);
35044
+ };
35045
+ /**
35046
+ * Handles partial profit availability event.
35047
+ * Proxies call to the underlying notification adapter.
35048
+ * @param data - The partial profit contract data
35049
+ */
35050
+ this.handlePartialProfit = async (data) => {
35051
+ return await this._notificationBacktestUtils.handlePartialProfit(data);
35052
+ };
35053
+ /**
35054
+ * Handles partial loss availability event.
35055
+ * Proxies call to the underlying notification adapter.
35056
+ * @param data - The partial loss contract data
35057
+ */
35058
+ this.handlePartialLoss = async (data) => {
35059
+ return await this._notificationBacktestUtils.handlePartialLoss(data);
35060
+ };
35061
+ /**
35062
+ * Handles breakeven availability event.
35063
+ * Proxies call to the underlying notification adapter.
35064
+ * @param data - The breakeven contract data
35065
+ */
35066
+ this.handleBreakeven = async (data) => {
35067
+ return await this._notificationBacktestUtils.handleBreakeven(data);
35068
+ };
35069
+ /**
35070
+ * Handles strategy commit events.
35071
+ * Proxies call to the underlying notification adapter.
35072
+ * @param data - The strategy commit contract data
35073
+ */
35074
+ this.handleStrategyCommit = async (data) => {
35075
+ return await this._notificationBacktestUtils.handleStrategyCommit(data);
35076
+ };
35077
+ /**
35078
+ * Handles risk rejection event.
35079
+ * Proxies call to the underlying notification adapter.
35080
+ * @param data - The risk contract data
35081
+ */
35082
+ this.handleRisk = async (data) => {
35083
+ return await this._notificationBacktestUtils.handleRisk(data);
35084
+ };
35085
+ /**
35086
+ * Handles error event.
35087
+ * Proxies call to the underlying notification adapter.
35088
+ * @param error - The error object
35089
+ */
35090
+ this.handleError = async (error) => {
35091
+ return await this._notificationBacktestUtils.handleError(error);
35092
+ };
35093
+ /**
35094
+ * Handles critical error event.
35095
+ * Proxies call to the underlying notification adapter.
35096
+ * @param error - The error object
35097
+ */
35098
+ this.handleCriticalError = async (error) => {
35099
+ return await this._notificationBacktestUtils.handleCriticalError(error);
35100
+ };
35101
+ /**
35102
+ * Handles validation error event.
35103
+ * Proxies call to the underlying notification adapter.
35104
+ * @param error - The error object
35105
+ */
35106
+ this.handleValidationError = async (error) => {
35107
+ return await this._notificationBacktestUtils.handleValidationError(error);
35108
+ };
35109
+ /**
35110
+ * Gets all stored notifications.
35111
+ * Proxies call to the underlying notification adapter.
35112
+ * @returns Array of all notification models
35113
+ */
35114
+ this.getData = async () => {
35115
+ return await this._notificationBacktestUtils.getData();
35116
+ };
35117
+ /**
35118
+ * Clears all stored notifications.
35119
+ * Proxies call to the underlying notification adapter.
35120
+ */
35121
+ this.clear = async () => {
35122
+ return await this._notificationBacktestUtils.clear();
35123
+ };
35124
+ /**
35125
+ * Sets the notification adapter constructor.
35126
+ * All future notification operations will use this adapter.
35127
+ *
35128
+ * @param Ctor - Constructor for notification adapter
35129
+ */
35130
+ this.useNotificationAdapter = (Ctor) => {
35131
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER);
35132
+ this._notificationBacktestUtils = Reflect.construct(Ctor, []);
35133
+ };
35134
+ /**
35135
+ * Switches to dummy notification adapter.
35136
+ * All future notification writes will be no-ops.
35137
+ */
35138
+ this.useDummy = () => {
35139
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY);
35140
+ this._notificationBacktestUtils = new NotificationDummyBacktestUtils();
35141
+ };
35142
+ /**
35143
+ * Switches to in-memory notification adapter (default).
35144
+ * Notifications will be stored in memory only.
35145
+ */
35146
+ this.useMemory = () => {
35147
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY);
35148
+ this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
35149
+ };
35150
+ /**
35151
+ * Switches to persistent notification adapter.
35152
+ * Notifications will be persisted to disk.
35153
+ */
35154
+ this.usePersist = () => {
35155
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST);
35156
+ this._notificationBacktestUtils = new NotificationPersistBacktestUtils();
35157
+ };
35158
+ }
35159
+ }
35160
+ /**
35161
+ * Live trading notification adapter with pluggable notification backend.
35162
+ *
35163
+ * Features:
35164
+ * - Adapter pattern for swappable notification implementations
35165
+ * - Default adapter: NotificationMemoryLiveUtils (in-memory storage)
35166
+ * - Alternative adapters: NotificationPersistLiveUtils, NotificationDummyLiveUtils
35167
+ * - Convenience methods: usePersist(), useMemory(), useDummy()
35168
+ */
35169
+ class NotificationLiveAdapter {
35170
+ constructor() {
35171
+ /** Internal notification utils instance */
35172
+ this._notificationLiveUtils = new NotificationMemoryLiveUtils();
35173
+ /**
35174
+ * Handles signal events.
35175
+ * Proxies call to the underlying notification adapter.
35176
+ * @param data - The strategy tick result data
35177
+ */
35178
+ this.handleSignal = async (data) => {
35179
+ return await this._notificationLiveUtils.handleSignal(data);
35180
+ };
35181
+ /**
35182
+ * Handles partial profit availability event.
35183
+ * Proxies call to the underlying notification adapter.
35184
+ * @param data - The partial profit contract data
35185
+ */
35186
+ this.handlePartialProfit = async (data) => {
35187
+ return await this._notificationLiveUtils.handlePartialProfit(data);
35188
+ };
35189
+ /**
35190
+ * Handles partial loss availability event.
35191
+ * Proxies call to the underlying notification adapter.
35192
+ * @param data - The partial loss contract data
35193
+ */
35194
+ this.handlePartialLoss = async (data) => {
35195
+ return await this._notificationLiveUtils.handlePartialLoss(data);
35196
+ };
35197
+ /**
35198
+ * Handles breakeven availability event.
35199
+ * Proxies call to the underlying notification adapter.
35200
+ * @param data - The breakeven contract data
35201
+ */
35202
+ this.handleBreakeven = async (data) => {
35203
+ return await this._notificationLiveUtils.handleBreakeven(data);
35204
+ };
35205
+ /**
35206
+ * Handles strategy commit events.
35207
+ * Proxies call to the underlying notification adapter.
35208
+ * @param data - The strategy commit contract data
35209
+ */
35210
+ this.handleStrategyCommit = async (data) => {
35211
+ return await this._notificationLiveUtils.handleStrategyCommit(data);
35212
+ };
35213
+ /**
35214
+ * Handles risk rejection event.
35215
+ * Proxies call to the underlying notification adapter.
35216
+ * @param data - The risk contract data
35217
+ */
35218
+ this.handleRisk = async (data) => {
35219
+ return await this._notificationLiveUtils.handleRisk(data);
35220
+ };
35221
+ /**
35222
+ * Handles error event.
35223
+ * Proxies call to the underlying notification adapter.
35224
+ * @param error - The error object
35225
+ */
35226
+ this.handleError = async (error) => {
35227
+ return await this._notificationLiveUtils.handleError(error);
35228
+ };
35229
+ /**
35230
+ * Handles critical error event.
35231
+ * Proxies call to the underlying notification adapter.
35232
+ * @param error - The error object
35233
+ */
35234
+ this.handleCriticalError = async (error) => {
35235
+ return await this._notificationLiveUtils.handleCriticalError(error);
35236
+ };
35237
+ /**
35238
+ * Handles validation error event.
35239
+ * Proxies call to the underlying notification adapter.
35240
+ * @param error - The error object
35241
+ */
35242
+ this.handleValidationError = async (error) => {
35243
+ return await this._notificationLiveUtils.handleValidationError(error);
35244
+ };
35245
+ /**
35246
+ * Gets all stored notifications.
35247
+ * Proxies call to the underlying notification adapter.
35248
+ * @returns Array of all notification models
35249
+ */
35250
+ this.getData = async () => {
35251
+ return await this._notificationLiveUtils.getData();
35252
+ };
35253
+ /**
35254
+ * Clears all stored notifications.
35255
+ * Proxies call to the underlying notification adapter.
35256
+ */
35257
+ this.clear = async () => {
35258
+ return await this._notificationLiveUtils.clear();
35259
+ };
35260
+ /**
35261
+ * Sets the notification adapter constructor.
35262
+ * All future notification operations will use this adapter.
35263
+ *
35264
+ * @param Ctor - Constructor for notification adapter
35265
+ */
35266
+ this.useNotificationAdapter = (Ctor) => {
35267
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_ADAPTER);
35268
+ this._notificationLiveUtils = Reflect.construct(Ctor, []);
35269
+ };
35270
+ /**
35271
+ * Switches to dummy notification adapter.
35272
+ * All future notification writes will be no-ops.
35273
+ */
35274
+ this.useDummy = () => {
35275
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_DUMMY);
35276
+ this._notificationLiveUtils = new NotificationDummyLiveUtils();
35277
+ };
35278
+ /**
35279
+ * Switches to in-memory notification adapter (default).
35280
+ * Notifications will be stored in memory only.
35281
+ */
35282
+ this.useMemory = () => {
35283
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_MEMORY);
35284
+ this._notificationLiveUtils = new NotificationMemoryLiveUtils();
35285
+ };
35286
+ /**
35287
+ * Switches to persistent notification adapter.
35288
+ * Notifications will be persisted to disk.
35289
+ */
35290
+ this.usePersist = () => {
35291
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_PERSIST);
35292
+ this._notificationLiveUtils = new NotificationPersistLiveUtils();
35293
+ };
35294
+ }
35295
+ }
35296
+ /**
35297
+ * Main notification adapter that manages both backtest and live notification storage.
35298
+ *
35299
+ * Features:
35300
+ * - Subscribes to signal emitters for automatic notification updates
35301
+ * - Provides unified access to both backtest and live notifications
35302
+ * - Singleshot enable pattern prevents duplicate subscriptions
35303
+ * - Cleanup function for proper unsubscription
35304
+ */
35305
+ class NotificationAdapter {
35306
+ constructor() {
35307
+ /**
35308
+ * Enables notification storage by subscribing to signal emitters.
35309
+ * Uses singleshot to ensure one-time subscription.
35310
+ *
35311
+ * @returns Cleanup function that unsubscribes from all emitters
35312
+ */
35313
+ this.enable = singleshot(() => {
35314
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE);
35315
+ let unLive;
35316
+ let unBacktest;
35317
+ {
35318
+ const unBacktestSignal = signalBacktestEmitter.subscribe((data) => NotificationBacktest.handleSignal(data));
35319
+ const unBacktestPartialProfit = partialProfitSubject
35320
+ .filter(({ backtest }) => backtest)
35321
+ .connect((data) => NotificationBacktest.handlePartialProfit(data));
35322
+ const unBacktestPartialLoss = partialLossSubject
35323
+ .filter(({ backtest }) => backtest)
35324
+ .connect((data) => NotificationBacktest.handlePartialLoss(data));
35325
+ const unBacktestBreakeven = breakevenSubject
35326
+ .filter(({ backtest }) => backtest)
35327
+ .connect((data) => NotificationBacktest.handleBreakeven(data));
35328
+ const unBacktestStrategyCommit = strategyCommitSubject
35329
+ .filter(({ backtest }) => backtest)
35330
+ .connect((data) => NotificationBacktest.handleStrategyCommit(data));
35331
+ const unBacktestRisk = riskSubject
35332
+ .filter(({ backtest }) => backtest)
35333
+ .connect((data) => NotificationBacktest.handleRisk(data));
35334
+ const unBacktestError = errorEmitter.subscribe((error) => NotificationBacktest.handleError(error));
35335
+ const unBacktestExit = exitEmitter.subscribe((error) => NotificationBacktest.handleCriticalError(error));
35336
+ const unBacktestValidation = validationSubject.subscribe((error) => NotificationBacktest.handleValidationError(error));
35337
+ unBacktest = compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation());
35338
+ }
35339
+ {
35340
+ const unLiveSignal = signalLiveEmitter.subscribe((data) => NotificationLive.handleSignal(data));
35341
+ const unLivePartialProfit = partialProfitSubject
35342
+ .filter(({ backtest }) => !backtest)
35343
+ .connect((data) => NotificationLive.handlePartialProfit(data));
35344
+ const unLivePartialLoss = partialLossSubject
35345
+ .filter(({ backtest }) => !backtest)
35346
+ .connect((data) => NotificationLive.handlePartialLoss(data));
35347
+ const unLiveBreakeven = breakevenSubject
35348
+ .filter(({ backtest }) => !backtest)
35349
+ .connect((data) => NotificationLive.handleBreakeven(data));
35350
+ const unLiveStrategyCommit = strategyCommitSubject
35351
+ .filter(({ backtest }) => !backtest)
35352
+ .connect((data) => NotificationLive.handleStrategyCommit(data));
35353
+ const unLiveRisk = riskSubject
35354
+ .filter(({ backtest }) => !backtest)
35355
+ .connect((data) => NotificationLive.handleRisk(data));
35356
+ const unLiveError = errorEmitter.subscribe((error) => NotificationLive.handleError(error));
35357
+ const unLiveExit = exitEmitter.subscribe((error) => NotificationLive.handleCriticalError(error));
35358
+ const unLiveValidation = validationSubject.subscribe((error) => NotificationLive.handleValidationError(error));
35359
+ unLive = compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation());
35360
+ }
35361
+ return () => {
35362
+ unLive();
35363
+ unBacktest();
35364
+ this.enable.clear();
35365
+ };
35366
+ });
35367
+ /**
35368
+ * Disables notification storage by unsubscribing from all emitters.
35369
+ * Safe to call multiple times.
35370
+ */
35371
+ this.disable = () => {
35372
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_DISABLE);
35373
+ if (this.enable.hasValue()) {
35374
+ const lastSubscription = this.enable();
35375
+ lastSubscription();
35376
+ }
35377
+ };
35378
+ /**
35379
+ * Gets all backtest notifications from storage.
35380
+ *
35381
+ * @returns Array of all backtest notification models
35382
+ * @throws Error if NotificationAdapter is not enabled
35383
+ */
35384
+ this.getDataBacktest = async () => {
35385
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_BACKTEST);
35386
+ if (!this.enable.hasValue()) {
35387
+ throw new Error("NotificationAdapter is not enabled. Call enable() first.");
35388
+ }
35389
+ return await NotificationBacktest.getData();
35390
+ };
35391
+ /**
35392
+ * Gets all live notifications from storage.
35393
+ *
35394
+ * @returns Array of all live notification models
35395
+ * @throws Error if NotificationAdapter is not enabled
35396
+ */
35397
+ this.getDataLive = async () => {
35398
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_LIVE);
35399
+ if (!this.enable.hasValue()) {
35400
+ throw new Error("NotificationAdapter is not enabled. Call enable() first.");
35401
+ }
35402
+ return await NotificationLive.getData();
35403
+ };
35404
+ /**
35405
+ * Clears all backtest notifications from storage.
35406
+ *
35407
+ * @throws Error if NotificationAdapter is not enabled
35408
+ */
35409
+ this.clearBacktest = async () => {
35410
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_BACKTEST);
35411
+ if (!this.enable.hasValue()) {
35412
+ throw new Error("NotificationAdapter is not enabled. Call enable() first.");
35413
+ }
35414
+ return await NotificationBacktest.clear();
35415
+ };
35416
+ /**
35417
+ * Clears all live notifications from storage.
35418
+ *
35419
+ * @throws Error if NotificationAdapter is not enabled
35420
+ */
35421
+ this.clearLive = async () => {
35422
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_LIVE);
35423
+ if (!this.enable.hasValue()) {
35424
+ throw new Error("NotificationAdapter is not enabled. Call enable() first.");
35425
+ }
35426
+ return await NotificationLive.clear();
35427
+ };
35428
+ }
35429
+ }
35430
+ /**
35431
+ * Global singleton instance of NotificationAdapter.
35432
+ * Provides unified notification management for backtest and live trading.
35433
+ */
35434
+ const Notification = new NotificationAdapter();
35435
+ /**
35436
+ * Global singleton instance of NotificationLiveAdapter.
35437
+ * Provides live trading notification storage with pluggable backends.
35438
+ */
35439
+ const NotificationLive = new NotificationLiveAdapter();
35440
+ /**
35441
+ * Global singleton instance of NotificationBacktestAdapter.
35442
+ * Provides backtest notification storage with pluggable backends.
35443
+ */
35444
+ const NotificationBacktest = new NotificationBacktestAdapter();
35445
+
35446
+ const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
35447
+ const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
35448
+ const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
35449
+ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
35450
+ const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
35451
+ const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
35452
+ const MS_PER_MINUTE = 60000;
35453
+ /**
35454
+ * Gets current timestamp from execution context if available.
35455
+ * Returns current Date() if no execution context exists (non-trading GUI).
35456
+ */
35457
+ const GET_TIMESTAMP_FN = async () => {
35458
+ if (ExecutionContextService.hasContext()) {
35459
+ return new Date(bt.executionContextService.context.when);
35460
+ }
35461
+ return new Date();
35462
+ };
35463
+ /**
35464
+ * Gets backtest mode flag from execution context if available.
35465
+ * Returns false if no execution context exists (live mode).
35466
+ */
35467
+ const GET_BACKTEST_FN = async () => {
35468
+ if (ExecutionContextService.hasContext()) {
35469
+ return bt.executionContextService.context.backtest;
35470
+ }
35471
+ return false;
35472
+ };
35473
+ /**
35474
+ * Default implementation for getCandles.
35475
+ * Throws an error indicating the method is not implemented.
35476
+ */
35477
+ const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
35478
+ throw new Error(`getCandles is not implemented for this exchange`);
35479
+ };
35480
+ /**
35481
+ * Default implementation for formatQuantity.
35482
+ * Returns Bitcoin precision on Binance (8 decimal places).
35483
+ */
35484
+ const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
35485
+ return quantity.toFixed(8);
35486
+ };
35487
+ /**
35488
+ * Default implementation for formatPrice.
35489
+ * Returns Bitcoin precision on Binance (2 decimal places).
35490
+ */
35491
+ const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
35492
+ return price.toFixed(2);
35493
+ };
35494
+ /**
35495
+ * Default implementation for getOrderBook.
35496
+ * Throws an error indicating the method is not implemented.
35497
+ *
35498
+ * @param _symbol - Trading pair symbol (unused)
35499
+ * @param _depth - Maximum depth levels (unused)
35500
+ * @param _from - Start of time range (unused - can be ignored in live implementations)
35501
+ * @param _to - End of time range (unused - can be ignored in live implementations)
35502
+ * @param _backtest - Whether running in backtest mode (unused)
35503
+ */
35504
+ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
35505
+ throw new Error(`getOrderBook is not implemented for this exchange`);
35506
+ };
35507
+ const INTERVAL_MINUTES$1 = {
35508
+ "1m": 1,
35509
+ "3m": 3,
35510
+ "5m": 5,
35511
+ "15m": 15,
35512
+ "30m": 30,
35513
+ "1h": 60,
35514
+ "2h": 120,
35515
+ "4h": 240,
35516
+ "6h": 360,
35517
+ "8h": 480,
35518
+ };
35519
+ /**
35520
+ * Aligns timestamp down to the nearest interval boundary.
35521
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
35522
+ *
35523
+ * Candle timestamp convention:
35524
+ * - Candle timestamp = openTime (when candle opens)
35525
+ * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
35526
+ *
35527
+ * Adapter contract:
35528
+ * - Adapter must return candles with timestamp = openTime
35529
+ * - First returned candle.timestamp must equal aligned since
35530
+ * - Adapter must return exactly `limit` candles
35531
+ *
35532
+ * @param timestamp - Timestamp in milliseconds
35533
+ * @param intervalMinutes - Interval in minutes
35534
+ * @returns Aligned timestamp rounded down to interval boundary
35535
+ */
35536
+ const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
35537
+ const intervalMs = intervalMinutes * MS_PER_MINUTE;
35538
+ return Math.floor(timestamp / intervalMs) * intervalMs;
35539
+ };
35540
+ /**
35541
+ * Creates exchange instance with methods resolved once during construction.
35542
+ * Applies default implementations where schema methods are not provided.
35543
+ *
35544
+ * @param schema - Exchange schema from registry
35545
+ * @returns Object with resolved exchange methods
35546
+ */
35547
+ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
35548
+ const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
35549
+ const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
35550
+ const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
35551
+ const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
35552
+ return {
35553
+ getCandles,
35554
+ formatQuantity,
35555
+ formatPrice,
35556
+ getOrderBook,
35557
+ };
35558
+ };
35559
+ /**
35560
+ * Attempts to read candles from cache.
35561
+ *
35562
+ * Cache lookup calculates expected timestamps:
35563
+ * sinceTimestamp + i * stepMs for i = 0..limit-1
35564
+ * Returns all candles if found, null if any missing.
35565
+ *
35566
+ * @param dto - Data transfer object containing symbol, interval, and limit
35567
+ * @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
35568
+ * @param untilTimestamp - Unused, kept for API compatibility
35569
+ * @param exchangeName - Exchange name
35570
+ * @returns Cached candles array (exactly limit) or null if cache miss
35571
+ */
35572
+ const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
35573
+ // PersistCandleAdapter.readCandlesData calculates expected timestamps:
35574
+ // sinceTimestamp + i * stepMs for i = 0..limit-1
35575
+ // Returns all candles if found, null if any missing
35576
+ const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
35577
+ // Return cached data only if we have exactly the requested limit
35578
+ if (cachedCandles?.length === dto.limit) {
35579
+ bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
35580
+ return cachedCandles;
35581
+ }
35582
+ bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
35583
+ return null;
35584
+ }, {
35585
+ fallback: async (error) => {
35586
+ const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
35587
+ const payload = {
35588
+ error: errorData(error),
35589
+ message: getErrorMessage(error),
35590
+ };
35591
+ bt.loggerService.warn(message, payload);
35592
+ console.warn(message, payload);
35593
+ errorEmitter.next(error);
35594
+ },
35595
+ defaultValue: null,
35596
+ });
35597
+ /**
35598
+ * Writes candles to cache with error handling.
35599
+ *
35600
+ * The candles passed to this function should be validated:
35601
+ * - First candle.timestamp equals aligned sinceTimestamp (openTime)
35602
+ * - Exact number of candles as requested (limit)
35603
+ * - Sequential timestamps: sinceTimestamp + i * stepMs
35604
+ *
35605
+ * @param candles - Array of validated candle data to cache
35606
+ * @param dto - Data transfer object containing symbol, interval, and limit
35607
+ * @param exchangeName - Exchange name
35608
+ */
35609
+ const WRITE_CANDLES_CACHE_FN = trycatch(queued(async (candles, dto, exchangeName) => {
35610
+ await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
35611
+ bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
35612
+ }), {
35613
+ fallback: async (error) => {
35614
+ const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
35615
+ const payload = {
35616
+ error: errorData(error),
35617
+ message: getErrorMessage(error),
35618
+ };
35619
+ bt.loggerService.warn(message, payload);
35620
+ console.warn(message, payload);
35621
+ errorEmitter.next(error);
35622
+ },
35623
+ defaultValue: null,
35624
+ });
35625
+ /**
35626
+ * Instance class for exchange operations on a specific exchange.
35627
+ *
35628
+ * Provides isolated exchange operations for a single exchange.
35629
+ * Each instance maintains its own context and exposes IExchangeSchema methods.
35630
+ * The schema is retrieved once during construction for better performance.
35631
+ *
35632
+ * @example
35633
+ * ```typescript
35634
+ * const instance = new ExchangeInstance("binance");
35635
+ *
35636
+ * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
35637
+ * const vwap = await instance.getAveragePrice("BTCUSDT");
35638
+ * const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
35639
+ * const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
35640
+ * ```
35641
+ */
35642
+ class ExchangeInstance {
35643
+ /**
35644
+ * Creates a new ExchangeInstance for a specific exchange.
35645
+ *
35646
+ * @param exchangeName - Exchange name (e.g., "binance")
35647
+ */
35648
+ constructor(exchangeName) {
35649
+ this.exchangeName = exchangeName;
35650
+ /**
35651
+ * Fetch candles from data source (API or database).
35652
+ *
35653
+ * Automatically calculates the start date based on Date.now() and the requested interval/limit.
35654
+ * Uses the same logic as ClientExchange to ensure backwards compatibility.
35655
+ *
35656
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
35657
+ * @param interval - Candle time interval (e.g., "1m", "1h")
35658
+ * @param limit - Maximum number of candles to fetch
35659
+ * @returns Promise resolving to array of OHLCV candle data
35660
+ *
35661
+ * @example
35662
+ * ```typescript
35663
+ * const instance = new ExchangeInstance("binance");
35664
+ * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
35665
+ * ```
35666
+ */
35667
+ this.getCandles = async (symbol, interval, limit) => {
35668
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_CANDLES, {
35669
+ exchangeName: this.exchangeName,
35670
+ symbol,
35671
+ interval,
35672
+ limit,
35673
+ });
35674
+ const getCandles = this._methods.getCandles;
35675
+ const step = INTERVAL_MINUTES$1[interval];
35676
+ if (!step) {
35677
+ throw new Error(`ExchangeInstance unknown interval=${interval}`);
35678
+ }
35679
+ const stepMs = step * MS_PER_MINUTE;
35680
+ // Align when down to interval boundary
35681
+ const when = await GET_TIMESTAMP_FN();
35682
+ const whenTimestamp = when.getTime();
35683
+ const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
35684
+ // Calculate since: go back limit candles from aligned when
35685
+ const sinceTimestamp = alignedWhen - limit * stepMs;
35686
+ const since = new Date(sinceTimestamp);
35687
+ const untilTimestamp = alignedWhen;
35688
+ // Try to read from cache first
35689
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
35690
+ if (cachedCandles !== null) {
35691
+ return cachedCandles;
35692
+ }
35693
+ let allData = [];
35694
+ // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
35695
+ if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
35696
+ let remaining = limit;
35697
+ let currentSince = new Date(since.getTime());
35698
+ const isBacktest = await GET_BACKTEST_FN();
35699
+ while (remaining > 0) {
35700
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
35701
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
35702
+ allData.push(...chunkData);
35703
+ remaining -= chunkLimit;
35704
+ if (remaining > 0) {
35705
+ // Move currentSince forward by the number of candles fetched
35706
+ currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
35707
+ }
35708
+ }
35709
+ }
35710
+ else {
35711
+ const isBacktest = await GET_BACKTEST_FN();
35712
+ allData = await getCandles(symbol, interval, since, limit, isBacktest);
35713
+ }
35714
+ // Apply distinct by timestamp to remove duplicates
35715
+ const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
35716
+ if (allData.length !== uniqueData.length) {
35717
+ bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
35718
+ }
35719
+ // Validate adapter returned data
35720
+ if (uniqueData.length === 0) {
35721
+ throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
35722
+ `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
35723
+ }
35724
+ if (uniqueData[0].timestamp !== sinceTimestamp) {
35725
+ throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
35726
+ `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
35727
+ `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
35728
+ }
35729
+ if (uniqueData.length !== limit) {
35730
+ throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
35731
+ `Expected ${limit} candles, got ${uniqueData.length}. ` +
35732
+ `Adapter must return exact number of candles requested.`);
35733
+ }
35734
+ // Write to cache after successful fetch
35735
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
35736
+ return uniqueData;
35737
+ };
35738
+ /**
35739
+ * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
35740
+ * The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
35741
+ *
35742
+ * Formula:
35743
+ * - Typical Price = (high + low + close) / 3
35744
+ * - VWAP = sum(typical_price * volume) / sum(volume)
35745
+ *
35746
+ * If volume is zero, returns simple average of close prices.
35747
+ *
35748
+ * @param symbol - Trading pair symbol
35749
+ * @returns Promise resolving to VWAP price
35750
+ * @throws Error if no candles available
35751
+ *
35752
+ * @example
35753
+ * ```typescript
35754
+ * const instance = new ExchangeInstance("binance");
35755
+ * const vwap = await instance.getAveragePrice("BTCUSDT");
35756
+ * console.log(vwap); // 50125.43
35757
+ * ```
35758
+ */
35759
+ this.getAveragePrice = async (symbol) => {
35760
+ bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
35761
+ exchangeName: this.exchangeName,
35762
+ symbol,
35763
+ });
35764
+ const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
33942
35765
  if (candles.length === 0) {
33943
35766
  throw new Error(`ExchangeInstance getAveragePrice: no candles data for symbol=${symbol}`);
33944
35767
  }
@@ -34370,7 +36193,7 @@ const INTERVAL_MINUTES = {
34370
36193
  * @param backtest - Whether running in backtest mode
34371
36194
  * @returns Cache key string
34372
36195
  */
34373
- const CREATE_KEY_FN$1 = (strategyName, exchangeName, frameName, backtest) => {
36196
+ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, backtest) => {
34374
36197
  const parts = [strategyName, exchangeName];
34375
36198
  if (frameName)
34376
36199
  parts.push(frameName);
@@ -34452,7 +36275,7 @@ class CacheInstance {
34452
36275
  throw new Error(`CacheInstance unknown cache ttl interval=${this.interval}`);
34453
36276
  }
34454
36277
  }
34455
- const key = CREATE_KEY_FN$1(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36278
+ const key = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
34456
36279
  const currentWhen = bt.executionContextService.context.when;
34457
36280
  const cached = this._cacheMap.get(key);
34458
36281
  if (cached) {
@@ -34490,7 +36313,7 @@ class CacheInstance {
34490
36313
  * ```
34491
36314
  */
34492
36315
  this.clear = () => {
34493
- const key = CREATE_KEY_FN$1(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36316
+ const key = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
34494
36317
  this._cacheMap.delete(key);
34495
36318
  };
34496
36319
  }
@@ -34638,663 +36461,6 @@ class CacheUtils {
34638
36461
  */
34639
36462
  const Cache = new CacheUtils();
34640
36463
 
34641
- /** Maximum number of notifications to store in history */
34642
- const MAX_NOTIFICATIONS = 250;
34643
- /** Function to create unique notification IDs */
34644
- const CREATE_KEY_FN = () => randomString();
34645
- /**
34646
- * Instance class for notification history management.
34647
- *
34648
- * Contains all business logic for notification collection from emitters/subjects.
34649
- * Stores notifications in chronological order with automatic limit management.
34650
- *
34651
- * @example
34652
- * ```typescript
34653
- * const instance = new NotificationInstance();
34654
- * await instance.waitForInit();
34655
- *
34656
- * // Get all notifications
34657
- * const all = instance.getData();
34658
- *
34659
- * // Process notifications with type discrimination
34660
- * all.forEach(notification => {
34661
- * switch (notification.type) {
34662
- * case "signal.closed":
34663
- * console.log(`Closed: ${notification.pnlPercentage}%`);
34664
- * break;
34665
- * case "partial.loss":
34666
- * if (notification.level >= 30) {
34667
- * alert("High loss!");
34668
- * }
34669
- * break;
34670
- * case "risk.rejection":
34671
- * console.warn(notification.rejectionNote);
34672
- * break;
34673
- * }
34674
- * });
34675
- *
34676
- * // Clear history
34677
- * instance.clear();
34678
- * ```
34679
- */
34680
- class NotificationInstance {
34681
- constructor() {
34682
- /** Internal notification history storage (newest first) */
34683
- this._notifications = [];
34684
- /**
34685
- * Processes signal events and creates appropriate notifications.
34686
- * Sorts signal notifications by createdAt to maintain chronological order.
34687
- */
34688
- this._handleSignal = async (data) => {
34689
- if (data.action === "opened") {
34690
- this._addNotification({
34691
- type: "signal.opened",
34692
- id: CREATE_KEY_FN(),
34693
- timestamp: data.signal.pendingAt,
34694
- backtest: data.backtest,
34695
- symbol: data.symbol,
34696
- strategyName: data.strategyName,
34697
- exchangeName: data.exchangeName,
34698
- signalId: data.signal.id,
34699
- position: data.signal.position,
34700
- priceOpen: data.signal.priceOpen,
34701
- priceTakeProfit: data.signal.priceTakeProfit,
34702
- priceStopLoss: data.signal.priceStopLoss,
34703
- originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34704
- originalPriceStopLoss: data.signal.originalPriceStopLoss,
34705
- note: data.signal.note,
34706
- scheduledAt: data.signal.scheduledAt,
34707
- pendingAt: data.signal.pendingAt,
34708
- createdAt: data.createdAt,
34709
- });
34710
- }
34711
- else if (data.action === "closed") {
34712
- const durationMs = data.closeTimestamp - data.signal.pendingAt;
34713
- const durationMin = Math.round(durationMs / 60000);
34714
- this._addNotification({
34715
- type: "signal.closed",
34716
- id: CREATE_KEY_FN(),
34717
- timestamp: data.closeTimestamp,
34718
- backtest: data.backtest,
34719
- symbol: data.symbol,
34720
- strategyName: data.strategyName,
34721
- exchangeName: data.exchangeName,
34722
- signalId: data.signal.id,
34723
- position: data.signal.position,
34724
- priceOpen: data.signal.priceOpen,
34725
- priceClose: data.currentPrice,
34726
- priceTakeProfit: data.signal.priceTakeProfit,
34727
- priceStopLoss: data.signal.priceStopLoss,
34728
- originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34729
- originalPriceStopLoss: data.signal.originalPriceStopLoss,
34730
- pnlPercentage: data.pnl.pnlPercentage,
34731
- closeReason: data.closeReason,
34732
- duration: durationMin,
34733
- note: data.signal.note,
34734
- scheduledAt: data.signal.scheduledAt,
34735
- pendingAt: data.signal.pendingAt,
34736
- createdAt: data.createdAt,
34737
- });
34738
- }
34739
- else if (data.action === "scheduled") {
34740
- this._addNotification({
34741
- type: "signal.scheduled",
34742
- id: CREATE_KEY_FN(),
34743
- timestamp: data.signal.scheduledAt,
34744
- backtest: data.backtest,
34745
- symbol: data.symbol,
34746
- strategyName: data.strategyName,
34747
- exchangeName: data.exchangeName,
34748
- signalId: data.signal.id,
34749
- position: data.signal.position,
34750
- priceOpen: data.signal.priceOpen,
34751
- priceTakeProfit: data.signal.priceTakeProfit,
34752
- priceStopLoss: data.signal.priceStopLoss,
34753
- originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34754
- originalPriceStopLoss: data.signal.originalPriceStopLoss,
34755
- scheduledAt: data.signal.scheduledAt,
34756
- currentPrice: data.currentPrice,
34757
- createdAt: data.createdAt,
34758
- });
34759
- }
34760
- else if (data.action === "cancelled") {
34761
- const durationMs = data.closeTimestamp - data.signal.scheduledAt;
34762
- const durationMin = Math.round(durationMs / 60000);
34763
- this._addNotification({
34764
- type: "signal.cancelled",
34765
- id: CREATE_KEY_FN(),
34766
- timestamp: data.closeTimestamp,
34767
- backtest: data.backtest,
34768
- symbol: data.symbol,
34769
- strategyName: data.strategyName,
34770
- exchangeName: data.exchangeName,
34771
- signalId: data.signal.id,
34772
- position: data.signal.position,
34773
- priceOpen: data.signal.priceOpen,
34774
- priceTakeProfit: data.signal.priceTakeProfit,
34775
- priceStopLoss: data.signal.priceStopLoss,
34776
- originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34777
- originalPriceStopLoss: data.signal.originalPriceStopLoss,
34778
- cancelReason: data.reason,
34779
- cancelId: data.cancelId,
34780
- duration: durationMin,
34781
- scheduledAt: data.signal.scheduledAt,
34782
- pendingAt: data.signal.pendingAt,
34783
- createdAt: data.createdAt,
34784
- });
34785
- }
34786
- };
34787
- /**
34788
- * Processes partial profit events.
34789
- */
34790
- this._handlePartialProfit = async (data) => {
34791
- this._addNotification({
34792
- type: "partial_profit.available",
34793
- id: CREATE_KEY_FN(),
34794
- timestamp: data.timestamp,
34795
- backtest: data.backtest,
34796
- symbol: data.symbol,
34797
- strategyName: data.strategyName,
34798
- exchangeName: data.exchangeName,
34799
- signalId: data.data.id,
34800
- level: data.level,
34801
- currentPrice: data.currentPrice,
34802
- priceOpen: data.data.priceOpen,
34803
- position: data.data.position,
34804
- priceTakeProfit: data.data.priceTakeProfit,
34805
- priceStopLoss: data.data.priceStopLoss,
34806
- originalPriceTakeProfit: data.data.originalPriceTakeProfit,
34807
- originalPriceStopLoss: data.data.originalPriceStopLoss,
34808
- scheduledAt: data.data.scheduledAt,
34809
- pendingAt: data.data.pendingAt,
34810
- createdAt: data.timestamp,
34811
- });
34812
- };
34813
- /**
34814
- * Processes partial loss events.
34815
- */
34816
- this._handlePartialLoss = async (data) => {
34817
- this._addNotification({
34818
- type: "partial_loss.available",
34819
- id: CREATE_KEY_FN(),
34820
- timestamp: data.timestamp,
34821
- backtest: data.backtest,
34822
- symbol: data.symbol,
34823
- strategyName: data.strategyName,
34824
- exchangeName: data.exchangeName,
34825
- signalId: data.data.id,
34826
- level: data.level,
34827
- currentPrice: data.currentPrice,
34828
- priceOpen: data.data.priceOpen,
34829
- position: data.data.position,
34830
- priceTakeProfit: data.data.priceTakeProfit,
34831
- priceStopLoss: data.data.priceStopLoss,
34832
- originalPriceTakeProfit: data.data.originalPriceTakeProfit,
34833
- originalPriceStopLoss: data.data.originalPriceStopLoss,
34834
- scheduledAt: data.data.scheduledAt,
34835
- pendingAt: data.data.pendingAt,
34836
- createdAt: data.timestamp,
34837
- });
34838
- };
34839
- /**
34840
- * Processes breakeven events.
34841
- */
34842
- this._handleBreakeven = async (data) => {
34843
- this._addNotification({
34844
- type: "breakeven.available",
34845
- id: CREATE_KEY_FN(),
34846
- timestamp: data.timestamp,
34847
- backtest: data.backtest,
34848
- symbol: data.symbol,
34849
- strategyName: data.strategyName,
34850
- exchangeName: data.exchangeName,
34851
- signalId: data.data.id,
34852
- currentPrice: data.currentPrice,
34853
- priceOpen: data.data.priceOpen,
34854
- position: data.data.position,
34855
- priceTakeProfit: data.data.priceTakeProfit,
34856
- priceStopLoss: data.data.priceStopLoss,
34857
- originalPriceTakeProfit: data.data.originalPriceTakeProfit,
34858
- originalPriceStopLoss: data.data.originalPriceStopLoss,
34859
- scheduledAt: data.data.scheduledAt,
34860
- pendingAt: data.data.pendingAt,
34861
- createdAt: data.timestamp,
34862
- });
34863
- };
34864
- /**
34865
- * Processes strategy commit events.
34866
- */
34867
- this._handleStrategyCommit = async (data) => {
34868
- if (data.action === "partial-profit") {
34869
- this._addNotification({
34870
- type: "partial_profit.commit",
34871
- id: CREATE_KEY_FN(),
34872
- timestamp: data.timestamp,
34873
- backtest: data.backtest,
34874
- symbol: data.symbol,
34875
- strategyName: data.strategyName,
34876
- exchangeName: data.exchangeName,
34877
- signalId: data.signalId,
34878
- percentToClose: data.percentToClose,
34879
- currentPrice: data.currentPrice,
34880
- position: data.position,
34881
- priceOpen: data.priceOpen,
34882
- priceTakeProfit: data.priceTakeProfit,
34883
- priceStopLoss: data.priceStopLoss,
34884
- originalPriceTakeProfit: data.originalPriceTakeProfit,
34885
- originalPriceStopLoss: data.originalPriceStopLoss,
34886
- scheduledAt: data.scheduledAt,
34887
- pendingAt: data.pendingAt,
34888
- createdAt: data.timestamp,
34889
- });
34890
- }
34891
- else if (data.action === "partial-loss") {
34892
- this._addNotification({
34893
- type: "partial_loss.commit",
34894
- id: CREATE_KEY_FN(),
34895
- timestamp: data.timestamp,
34896
- backtest: data.backtest,
34897
- symbol: data.symbol,
34898
- strategyName: data.strategyName,
34899
- exchangeName: data.exchangeName,
34900
- signalId: data.signalId,
34901
- percentToClose: data.percentToClose,
34902
- currentPrice: data.currentPrice,
34903
- position: data.position,
34904
- priceOpen: data.priceOpen,
34905
- priceTakeProfit: data.priceTakeProfit,
34906
- priceStopLoss: data.priceStopLoss,
34907
- originalPriceTakeProfit: data.originalPriceTakeProfit,
34908
- originalPriceStopLoss: data.originalPriceStopLoss,
34909
- scheduledAt: data.scheduledAt,
34910
- pendingAt: data.pendingAt,
34911
- createdAt: data.timestamp,
34912
- });
34913
- }
34914
- else if (data.action === "breakeven") {
34915
- this._addNotification({
34916
- type: "breakeven.commit",
34917
- id: CREATE_KEY_FN(),
34918
- timestamp: data.timestamp,
34919
- backtest: data.backtest,
34920
- symbol: data.symbol,
34921
- strategyName: data.strategyName,
34922
- exchangeName: data.exchangeName,
34923
- signalId: data.signalId,
34924
- currentPrice: data.currentPrice,
34925
- position: data.position,
34926
- priceOpen: data.priceOpen,
34927
- priceTakeProfit: data.priceTakeProfit,
34928
- priceStopLoss: data.priceStopLoss,
34929
- originalPriceTakeProfit: data.originalPriceTakeProfit,
34930
- originalPriceStopLoss: data.originalPriceStopLoss,
34931
- scheduledAt: data.scheduledAt,
34932
- pendingAt: data.pendingAt,
34933
- createdAt: data.timestamp,
34934
- });
34935
- }
34936
- else if (data.action === "trailing-stop") {
34937
- this._addNotification({
34938
- type: "trailing_stop.commit",
34939
- id: CREATE_KEY_FN(),
34940
- timestamp: data.timestamp,
34941
- backtest: data.backtest,
34942
- symbol: data.symbol,
34943
- strategyName: data.strategyName,
34944
- exchangeName: data.exchangeName,
34945
- signalId: data.signalId,
34946
- percentShift: data.percentShift,
34947
- currentPrice: data.currentPrice,
34948
- position: data.position,
34949
- priceOpen: data.priceOpen,
34950
- priceTakeProfit: data.priceTakeProfit,
34951
- priceStopLoss: data.priceStopLoss,
34952
- originalPriceTakeProfit: data.originalPriceTakeProfit,
34953
- originalPriceStopLoss: data.originalPriceStopLoss,
34954
- scheduledAt: data.scheduledAt,
34955
- pendingAt: data.pendingAt,
34956
- createdAt: data.timestamp,
34957
- });
34958
- }
34959
- else if (data.action === "trailing-take") {
34960
- this._addNotification({
34961
- type: "trailing_take.commit",
34962
- id: CREATE_KEY_FN(),
34963
- timestamp: data.timestamp,
34964
- backtest: data.backtest,
34965
- symbol: data.symbol,
34966
- strategyName: data.strategyName,
34967
- exchangeName: data.exchangeName,
34968
- signalId: data.signalId,
34969
- percentShift: data.percentShift,
34970
- currentPrice: data.currentPrice,
34971
- position: data.position,
34972
- priceOpen: data.priceOpen,
34973
- priceTakeProfit: data.priceTakeProfit,
34974
- priceStopLoss: data.priceStopLoss,
34975
- originalPriceTakeProfit: data.originalPriceTakeProfit,
34976
- originalPriceStopLoss: data.originalPriceStopLoss,
34977
- scheduledAt: data.scheduledAt,
34978
- pendingAt: data.pendingAt,
34979
- createdAt: data.timestamp,
34980
- });
34981
- }
34982
- else if (data.action === "activate-scheduled") {
34983
- this._addNotification({
34984
- type: "activate_scheduled.commit",
34985
- id: CREATE_KEY_FN(),
34986
- timestamp: data.timestamp,
34987
- backtest: data.backtest,
34988
- symbol: data.symbol,
34989
- strategyName: data.strategyName,
34990
- exchangeName: data.exchangeName,
34991
- signalId: data.signalId,
34992
- activateId: data.activateId,
34993
- currentPrice: data.currentPrice,
34994
- position: data.position,
34995
- priceOpen: data.priceOpen,
34996
- priceTakeProfit: data.priceTakeProfit,
34997
- priceStopLoss: data.priceStopLoss,
34998
- originalPriceTakeProfit: data.originalPriceTakeProfit,
34999
- originalPriceStopLoss: data.originalPriceStopLoss,
35000
- scheduledAt: data.scheduledAt,
35001
- pendingAt: data.pendingAt,
35002
- createdAt: data.timestamp,
35003
- });
35004
- }
35005
- };
35006
- /**
35007
- * Processes risk rejection events.
35008
- */
35009
- this._handleRisk = async (data) => {
35010
- this._addNotification({
35011
- type: "risk.rejection",
35012
- id: CREATE_KEY_FN(),
35013
- timestamp: data.timestamp,
35014
- backtest: data.backtest,
35015
- symbol: data.symbol,
35016
- strategyName: data.strategyName,
35017
- exchangeName: data.exchangeName,
35018
- rejectionNote: data.rejectionNote,
35019
- rejectionId: data.rejectionId,
35020
- activePositionCount: data.activePositionCount,
35021
- currentPrice: data.currentPrice,
35022
- signalId: data.currentSignal.id,
35023
- position: data.currentSignal.position,
35024
- priceOpen: data.currentSignal.priceOpen,
35025
- priceTakeProfit: data.currentSignal.priceTakeProfit,
35026
- priceStopLoss: data.currentSignal.priceStopLoss,
35027
- minuteEstimatedTime: data.currentSignal.minuteEstimatedTime,
35028
- signalNote: data.currentSignal.note,
35029
- createdAt: data.timestamp,
35030
- });
35031
- };
35032
- /**
35033
- * Processes error events.
35034
- */
35035
- this._handleError = async (error) => {
35036
- this._addNotification({
35037
- type: "error.info",
35038
- id: CREATE_KEY_FN(),
35039
- error: errorData(error),
35040
- message: getErrorMessage(error),
35041
- backtest: false,
35042
- });
35043
- };
35044
- /**
35045
- * Processes critical error events.
35046
- */
35047
- this._handleCriticalError = async (error) => {
35048
- this._addNotification({
35049
- type: "error.critical",
35050
- id: CREATE_KEY_FN(),
35051
- error: errorData(error),
35052
- message: getErrorMessage(error),
35053
- backtest: false,
35054
- });
35055
- };
35056
- /**
35057
- * Processes validation error events.
35058
- */
35059
- this._handleValidationError = async (error) => {
35060
- this._addNotification({
35061
- type: "error.validation",
35062
- id: CREATE_KEY_FN(),
35063
- error: errorData(error),
35064
- message: getErrorMessage(error),
35065
- backtest: false,
35066
- });
35067
- };
35068
- /**
35069
- * Subscribes to all notification emitters and returns an unsubscribe function.
35070
- * Protected against multiple subscriptions using singleshot.
35071
- *
35072
- * @returns Unsubscribe function to stop receiving all notification events
35073
- *
35074
- * @example
35075
- * ```typescript
35076
- * const instance = new NotificationInstance();
35077
- * const unsubscribe = instance.subscribe();
35078
- * // ... later
35079
- * unsubscribe();
35080
- * ```
35081
- */
35082
- this.enable = singleshot(() => {
35083
- const unSignal = signalEmitter.subscribe(this._handleSignal);
35084
- const unProfit = partialProfitSubject.subscribe(this._handlePartialProfit);
35085
- const unLoss = partialLossSubject.subscribe(this._handlePartialLoss);
35086
- const unBreakeven = breakevenSubject.subscribe(this._handleBreakeven);
35087
- const unStrategyCommit = strategyCommitSubject.subscribe(this._handleStrategyCommit);
35088
- const unRisk = riskSubject.subscribe(this._handleRisk);
35089
- const unError = errorEmitter.subscribe(this._handleError);
35090
- const unExit = exitEmitter.subscribe(this._handleCriticalError);
35091
- const unValidation = validationSubject.subscribe(this._handleValidationError);
35092
- const disposeFn = compose(() => unSignal(), () => unProfit(), () => unLoss(), () => unBreakeven(), () => unStrategyCommit(), () => unRisk(), () => unError(), () => unExit(), () => unValidation());
35093
- return () => {
35094
- disposeFn();
35095
- this.enable.clear();
35096
- };
35097
- });
35098
- }
35099
- /**
35100
- * Adds notification to history with automatic limit management.
35101
- */
35102
- _addNotification(notification) {
35103
- this._notifications.unshift(notification);
35104
- // Trim history if exceeded MAX_NOTIFICATIONS
35105
- if (this._notifications.length > MAX_NOTIFICATIONS) {
35106
- this._notifications.pop();
35107
- }
35108
- }
35109
- /**
35110
- * Returns all notifications in chronological order (newest first).
35111
- *
35112
- * @returns Array of strongly-typed notification objects
35113
- *
35114
- * @example
35115
- * ```typescript
35116
- * const notifications = instance.getData();
35117
- *
35118
- * notifications.forEach(notification => {
35119
- * switch (notification.type) {
35120
- * case "signal.closed":
35121
- * console.log(`${notification.symbol}: ${notification.pnlPercentage}%`);
35122
- * break;
35123
- * case "partial.loss":
35124
- * if (notification.level >= 30) {
35125
- * console.warn(`High loss: ${notification.symbol}`);
35126
- * }
35127
- * break;
35128
- * }
35129
- * });
35130
- * ```
35131
- */
35132
- getData() {
35133
- return [...this._notifications];
35134
- }
35135
- /**
35136
- * Clears all notification history.
35137
- *
35138
- * @example
35139
- * ```typescript
35140
- * instance.clear();
35141
- * ```
35142
- */
35143
- clear() {
35144
- this._notifications = [];
35145
- }
35146
- /**
35147
- * Unsubscribes from all notification emitters to stop receiving events.
35148
- * Calls the unsubscribe function returned by subscribe().
35149
- * If not subscribed, does nothing.
35150
- *
35151
- * @example
35152
- * ```typescript
35153
- * const instance = new NotificationInstance();
35154
- * instance.subscribe();
35155
- * // ... later
35156
- * instance.unsubscribe();
35157
- * ```
35158
- */
35159
- disable() {
35160
- if (this.enable.hasValue()) {
35161
- const unsubscribeFn = this.enable();
35162
- unsubscribeFn();
35163
- }
35164
- }
35165
- }
35166
- /**
35167
- * Public facade for notification operations.
35168
- *
35169
- * Automatically subscribes on first use and provides simplified access to notification instance methods.
35170
- *
35171
- * @example
35172
- * ```typescript
35173
- * import { Notification } from "./classes/Notification";
35174
- *
35175
- * // Get all notifications (auto-subscribes if not subscribed)
35176
- * const all = await Notification.getData();
35177
- *
35178
- * // Process notifications with type discrimination
35179
- * all.forEach(notification => {
35180
- * switch (notification.type) {
35181
- * case "signal.closed":
35182
- * console.log(`Closed: ${notification.pnlPercentage}%`);
35183
- * break;
35184
- * case "partial.loss":
35185
- * if (notification.level >= 30) {
35186
- * alert("High loss!");
35187
- * }
35188
- * break;
35189
- * case "risk.rejection":
35190
- * console.warn(notification.rejectionNote);
35191
- * break;
35192
- * }
35193
- * });
35194
- *
35195
- * // Clear history
35196
- * await Notification.clear();
35197
- *
35198
- * // Unsubscribe when done
35199
- * await Notification.unsubscribe();
35200
- * ```
35201
- */
35202
- class NotificationUtils {
35203
- constructor() {
35204
- /** Internal instance containing business logic */
35205
- this._instance = new NotificationInstance();
35206
- }
35207
- /**
35208
- * Returns all notifications in chronological order (newest first).
35209
- * Automatically subscribes to emitters if not already subscribed.
35210
- *
35211
- * @returns Array of strongly-typed notification objects
35212
- *
35213
- * @example
35214
- * ```typescript
35215
- * const notifications = await Notification.getData();
35216
- *
35217
- * notifications.forEach(notification => {
35218
- * switch (notification.type) {
35219
- * case "signal.closed":
35220
- * console.log(`${notification.symbol}: ${notification.pnlPercentage}%`);
35221
- * break;
35222
- * case "partial.loss":
35223
- * if (notification.level >= 30) {
35224
- * console.warn(`High loss: ${notification.symbol}`);
35225
- * }
35226
- * break;
35227
- * }
35228
- * });
35229
- * ```
35230
- */
35231
- async getData() {
35232
- if (!this._instance.enable.hasValue()) {
35233
- throw new Error("Notification not initialized. Call enable() before getting data.");
35234
- }
35235
- return this._instance.getData();
35236
- }
35237
- /**
35238
- * Clears all notification history.
35239
- * Automatically subscribes to emitters if not already subscribed.
35240
- *
35241
- * @example
35242
- * ```typescript
35243
- * await Notification.clear();
35244
- * ```
35245
- */
35246
- async clear() {
35247
- if (!this._instance.enable.hasValue()) {
35248
- throw new Error("Notification not initialized. Call enable() before clearing data.");
35249
- }
35250
- this._instance.clear();
35251
- }
35252
- /**
35253
- * Unsubscribes from all notification emitters.
35254
- * Call this when you no longer need to collect notifications.
35255
- *
35256
- * @example
35257
- * ```typescript
35258
- * await Notification.unsubscribe();
35259
- * ```
35260
- */
35261
- async enable() {
35262
- this._instance.enable();
35263
- }
35264
- /**
35265
- * Unsubscribes from all notification emitters.
35266
- * Call this when you no longer need to collect notifications.
35267
- * @example
35268
- * ```typescript
35269
- * await Notification.unsubscribe();
35270
- * ```
35271
- */
35272
- async disable() {
35273
- this._instance.disable();
35274
- }
35275
- }
35276
- /**
35277
- * Singleton instance of NotificationUtils for convenient notification access.
35278
- *
35279
- * @example
35280
- * ```typescript
35281
- * import { Notification } from "./classes/Notification";
35282
- *
35283
- * // Get all notifications
35284
- * const all = await Notification.getData();
35285
- *
35286
- * // Filter by type using type discrimination
35287
- * const closedSignals = all.filter(n => n.type === "signal.closed");
35288
- * const highLosses = all.filter(n =>
35289
- * n.type === "partial.loss" && n.level >= 30
35290
- * );
35291
- *
35292
- * // Clear history
35293
- * await Notification.clear();
35294
- * ```
35295
- */
35296
- const Notification = new NotificationUtils();
35297
-
35298
36464
  const BREAKEVEN_METHOD_NAME_GET_DATA = "BreakevenUtils.getData";
35299
36465
  const BREAKEVEN_METHOD_NAME_GET_REPORT = "BreakevenUtils.getReport";
35300
36466
  const BREAKEVEN_METHOD_NAME_DUMP = "BreakevenUtils.dump";
@@ -35806,4 +36972,4 @@ const set = (object, path, value) => {
35806
36972
  }
35807
36973
  };
35808
36974
 
35809
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
36975
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };