@adaptic/utils 0.0.991 → 0.0.993

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,27 @@
1
1
  # @adaptic/utils
2
2
 
3
- Last updated: 20 Feb 2025
4
-
5
3
  A comprehensive utility library for financial data processing, time manipulation, and formatting.
6
4
 
7
5
  NPM repo: https://www.npmjs.com/package/@adaptic/utils
8
6
 
7
+ ## Branch Model
8
+
9
+ This repo has two publish lineages:
10
+
11
+ - `master` -> `@adaptic/utils@0.1.x` on npm dist-tag `latest`. What external
12
+ unpinned `npm install @adaptic/utils` will pull.
13
+ - `stable-release` -> `@adaptic/utils@0.0.x` (0.0.992+) on npm dist-tag
14
+ `stable`. What `engine` and `backend-legacy` actually consume via pinned
15
+ versions.
16
+
17
+ All new work lands on `stable-release`. `master` is only updated when
18
+ intentionally cutting a 0.1.x patch for legacy external consumers.
19
+
9
20
  ## Installation
10
21
 
11
22
  ```bash
12
- npm install @adaptic/utils
23
+ npm install @adaptic/utils # 0.1.x from master
24
+ npm install @adaptic/utils@stable # 0.0.x from stable-release (engine pins this)
13
25
  ```
14
26
 
15
27
  ## Usage
package/dist/index.cjs CHANGED
@@ -2691,6 +2691,48 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
2691
2691
  };
2692
2692
  reconnectAttempts = {};
2693
2693
  reconnectTimers = {};
2694
+ /**
2695
+ * Wall-clock timestamp of the most recent Alpaca app-level error code
2696
+ * 406 ("connection limit exceeded") received on each stream. Used by
2697
+ * {@link scheduleReconnect} to apply a long backoff with jitter rather
2698
+ * than the normal sub-second exponential ramp — without this, hitting
2699
+ * Alpaca's account-wide concurrent-connection cap (typical for blue/green
2700
+ * deploy rollovers where the old pod's WS slots haven't been released
2701
+ * yet) produced a 10-attempt retry storm that compounded the slot
2702
+ * pressure and consumed the per-account connection quota across the
2703
+ * organisation.
2704
+ *
2705
+ * Cleared once the long-backoff retry is scheduled so that subsequent
2706
+ * normal failures fall back to the standard sub-second exponential.
2707
+ *
2708
+ * @see CONNECTION_LIMIT_BACKOFF_MS / CONNECTION_LIMIT_BACKOFF_JITTER_MS
2709
+ */
2710
+ lastConnectionLimitAt = {};
2711
+ /**
2712
+ * Five-minute base backoff after Alpaca's app-level 406. Long enough
2713
+ * for Alpaca's server-side cleanup to release stale slots in typical
2714
+ * rollover scenarios; short enough that an operator doesn't need to
2715
+ * intervene. Mirrors the equivalent MassiveClient
2716
+ * `MAX_CONNECTIONS_RETRY_DELAY_MS` (engine v1.0.59) so both providers
2717
+ * behave identically under the same failure mode.
2718
+ */
2719
+ CONNECTION_LIMIT_BACKOFF_MS = 5 * 60_000;
2720
+ /**
2721
+ * ±30 s of uniform jitter on the connection-limit backoff. Prevents
2722
+ * a thundering-herd retry when all three streams (stock / option /
2723
+ * crypto) hit 406 simultaneously during a deploy rollover — without
2724
+ * jitter they'd all retry at the same wall-clock instant and could
2725
+ * re-trip the account cap together.
2726
+ */
2727
+ CONNECTION_LIMIT_BACKOFF_JITTER_MS = 30_000;
2728
+ /**
2729
+ * Recency window within which a 406 is considered "still applicable"
2730
+ * to a subsequent reconnect-schedule call. The 406 message handler
2731
+ * stamps {@link lastConnectionLimitAt} and the `close` handler fires
2732
+ * shortly afterwards (sub-second typically) — the window is wide
2733
+ * enough to absorb scheduling delays without false-positives.
2734
+ */
2735
+ CONNECTION_LIMIT_RECENCY_MS = 30_000;
2694
2736
  setMode(mode = "production") {
2695
2737
  if (mode === "sandbox") {
2696
2738
  // sandbox mode
@@ -2822,6 +2864,18 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
2822
2864
  }
2823
2865
  else if (message.T === "error") {
2824
2866
  log$l(`${streamType} stream error: ${message.msg} (code: ${message.code}, raw: ${JSON.stringify(message)})`, { type: "error" });
2867
+ // Alpaca code 406: "connection limit exceeded" — account-wide
2868
+ // concurrent-WS cap reached. The Alpaca server will close the
2869
+ // socket immediately after this frame, which would normally
2870
+ // trigger our standard sub-second exponential reconnect chain
2871
+ // (1 s, 2 s, 4 s, 8 s, 16 s, 30 s × 5) — exactly the wrong
2872
+ // behaviour against a rate-limit response. Stamp the recency
2873
+ // marker so {@link scheduleReconnect} switches to the
2874
+ // 5-minute jittered backoff instead.
2875
+ if (typeof message.code === "number" &&
2876
+ message.code === 406) {
2877
+ this.lastConnectionLimitAt[streamType] = Date.now();
2878
+ }
2825
2879
  }
2826
2880
  else if (message.S) {
2827
2881
  super.emit(`${streamType}-${message.T}`, message);
@@ -2855,6 +2909,37 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
2855
2909
  });
2856
2910
  }
2857
2911
  scheduleReconnect(streamType) {
2912
+ // 406-recovery fast path. When the most recent close was preceded
2913
+ // by an Alpaca app-level 406 ("connection limit exceeded"), the
2914
+ // standard sub-second exponential ramp is exactly wrong — it
2915
+ // hammers the rate-limit endpoint and prolongs the slot pressure.
2916
+ // Use a 5-minute jittered backoff instead and reset the normal
2917
+ // attempt counter so we don't fall off the end of maxAttempts
2918
+ // and permanently give up on a transient rollover blip.
2919
+ const connectionLimitAt = this.lastConnectionLimitAt[streamType];
2920
+ const isRecentConnectionLimit = typeof connectionLimitAt === "number" &&
2921
+ Date.now() - connectionLimitAt <= this.CONNECTION_LIMIT_RECENCY_MS;
2922
+ if (isRecentConnectionLimit) {
2923
+ const jitter = Math.floor((Math.random() - 0.5) *
2924
+ 2 *
2925
+ this.CONNECTION_LIMIT_BACKOFF_JITTER_MS);
2926
+ const delayMs = this.CONNECTION_LIMIT_BACKOFF_MS + jitter;
2927
+ // Reset normal attempt counter so the next 406 retry doesn't
2928
+ // inherit a stale exponential cap.
2929
+ this.reconnectAttempts[streamType] = 0;
2930
+ // Consume the recency marker — subsequent reconnects fall back
2931
+ // to the standard exponential path unless a new 406 arrives.
2932
+ delete this.lastConnectionLimitAt[streamType];
2933
+ log$l(`${streamType} stream: Alpaca 406 connection-limit recovery — backing off ${Math.round(delayMs / 1000)}s before retry to allow account-wide slot release`, { type: "warn" });
2934
+ if (this.reconnectTimers[streamType]) {
2935
+ clearTimeout(this.reconnectTimers[streamType]);
2936
+ }
2937
+ this.reconnectTimers[streamType] = setTimeout(() => {
2938
+ log$l(`${streamType} stream: attempting reconnect after 406-recovery backoff`, { type: "info" });
2939
+ this.connect(streamType);
2940
+ }, delayMs);
2941
+ return;
2942
+ }
2858
2943
  const attempts = this.reconnectAttempts[streamType] ?? 0;
2859
2944
  const maxAttempts = 10;
2860
2945
  if (attempts >= maxAttempts) {
@@ -11532,7 +11617,6 @@ function calculateBollingerBands(priceData, { period = 20, standardDeviations =
11532
11617
  close: priceData[i].close,
11533
11618
  });
11534
11619
  }
11535
- // logIfDebug(`Calculated Bollinger Bands for ${result.length} periods`);
11536
11620
  return result;
11537
11621
  }
11538
11622
  /**
@@ -11595,7 +11679,6 @@ function calculateEMA(priceData, { period = 20, period2 = 9 } = {}) {
11595
11679
  }
11596
11680
  result.push(entry);
11597
11681
  }
11598
- // logIfDebug(`Calculated EMA for ${result.length} periods`);
11599
11682
  return result;
11600
11683
  }
11601
11684
  /**
@@ -11657,7 +11740,6 @@ function calculateFibonacciLevels(priceData, { lookbackPeriod = 20, retracementL
11657
11740
  close: priceData[i].close,
11658
11741
  });
11659
11742
  }
11660
- // logIfDebug(`Calculated Fibonacci levels for ${result.length} periods`);
11661
11743
  return result;
11662
11744
  }
11663
11745
  /**
@@ -11705,7 +11787,6 @@ function calculateMACD(priceData, { shortPeriod = 12, longPeriod = 26, signalPer
11705
11787
  close: emaLong[i].close,
11706
11788
  });
11707
11789
  }
11708
- // logIfDebug(`Calculated MACD for ${result.length} periods`);
11709
11790
  return result;
11710
11791
  }
11711
11792
  /**
@@ -11761,7 +11842,6 @@ function calculateRSI(priceData, { period = 14 } = {}) {
11761
11842
  close: priceData[i].close,
11762
11843
  });
11763
11844
  }
11764
- // logIfDebug(`Calculated RSI for ${result.length} periods`);
11765
11845
  return result;
11766
11846
  }
11767
11847
  /**
@@ -11814,7 +11894,6 @@ function calculateStochasticOscillator(priceData, { lookbackPeriod = 5, signalPe
11814
11894
  });
11815
11895
  }
11816
11896
  }
11817
- // logIfDebug(`Calculated Stochastic Oscillator for ${result.length} periods`);
11818
11897
  return result;
11819
11898
  }
11820
11899
  /**