backtest-kit 3.0.0 → 3.0.1

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