@adaptic/utils 0.0.983 → 0.0.985

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/dist/index.cjs CHANGED
@@ -2654,8 +2654,7 @@ const DEFAULT_ADJUSTMENT = "all";
2654
2654
  // deployments with SIP entitlements set ALPACA_MARKET_DATA_FEED=sip in their
2655
2655
  // environment to restore full data access. Per-call overrides (the optional
2656
2656
  // `feed` argument on getOptions*/getLatestBars/etc.) still win.
2657
- const DEFAULT_FEED$1 = (process.env.ALPACA_MARKET_DATA_FEED ||
2658
- "iex");
2657
+ const DEFAULT_FEED$1 = (process.env.ALPACA_MARKET_DATA_FEED || "iex");
2659
2658
  const DEFAULT_CURRENCY$1 = "USD";
2660
2659
  /**
2661
2660
  * Singleton class for interacting with Alpaca Market Data API
@@ -8523,6 +8522,11 @@ const fetchPrices = async (params, options) => {
8523
8522
  try {
8524
8523
  let allResults = [];
8525
8524
  let nextUrl = `${baseUrl}?${urlParams.toString()}`;
8525
+ // DE-006: track upstream freshness across pagination. If any page
8526
+ // reports DELAYED, the whole batch is treated as DELAYED — this is the
8527
+ // safer default for downstream latency-sensitive logic (e.g., trade
8528
+ // execution should refuse stale prices, not silently mix them).
8529
+ let aggregatedStatus = "OK";
8526
8530
  while (nextUrl) {
8527
8531
  //getLogger().info(`Debug: Fetching ${nextUrl}`);
8528
8532
  await rateLimiters.massive.acquire();
@@ -8532,6 +8536,7 @@ const fetchPrices = async (params, options) => {
8532
8536
  throw new Error(`Massive.com API responded with status: ${data.status}`);
8533
8537
  }
8534
8538
  if (data.status === "DELAYED") {
8539
+ aggregatedStatus = "DELAYED";
8535
8540
  const now = Date.now();
8536
8541
  const lastWarn = delayedWarnTimestamps.get(params.ticker) ?? 0;
8537
8542
  if (now - lastWarn > DELAYED_WARN_COOLDOWN_MS) {
@@ -8545,6 +8550,14 @@ const fetchPrices = async (params, options) => {
8545
8550
  // Check if there's a next page and append API key
8546
8551
  nextUrl = data.next_url ? `${data.next_url}&apiKey=${apiKey}` : "";
8547
8552
  }
8553
+ // DE-006: stamp each bar with the upstream freshness so downstream
8554
+ // consumers (engine pricing pipeline, performance metrics, risk gates)
8555
+ // can branch on `bar._freshness?.status === "DELAYED"`.
8556
+ const freshness = {
8557
+ status: aggregatedStatus,
8558
+ receivedAt: new Date(),
8559
+ ...(aggregatedStatus === "DELAYED" ? { delayedSince: null } : {}),
8560
+ };
8548
8561
  return allResults.map((entry) => ({
8549
8562
  date: new Date(entry.t).toLocaleString("en-US", {
8550
8563
  year: "numeric",
@@ -8565,6 +8578,7 @@ const fetchPrices = async (params, options) => {
8565
8578
  vol: entry.v,
8566
8579
  vwap: entry.vw,
8567
8580
  trades: entry.n,
8581
+ _freshness: freshness,
8568
8582
  }));
8569
8583
  }
8570
8584
  catch (error) {
@@ -8605,6 +8619,43 @@ const fetchPrices = async (params, options) => {
8605
8619
  }
8606
8620
  });
8607
8621
  };
8622
+ /**
8623
+ * Variant of {@link fetchPrices} that returns a discriminated
8624
+ * {@link MassiveResult} wrapper, surfacing the upstream feed status (`OK` vs
8625
+ * `DELAYED`) at the result level. This is the preferred entry point for new
8626
+ * code that needs to gate latency-sensitive decisions on freshness.
8627
+ *
8628
+ * The underlying bars are still stamped with `_freshness` so consumers that
8629
+ * already destructure the array can branch per-bar; the wrapper simply
8630
+ * promotes that information to the top of the result for clarity.
8631
+ *
8632
+ * DE-006: closes the loop for callers that need to know when the Massive feed
8633
+ * is on a delayed plan (e.g. free tier, market-data outage downgrade).
8634
+ *
8635
+ * @param params - Same parameters accepted by {@link fetchPrices}.
8636
+ * @param options - Same options accepted by {@link fetchPrices}.
8637
+ * @returns A {@link MassiveResult} carrying the bar array plus freshness
8638
+ * metadata.
8639
+ */
8640
+ const fetchPricesWithFreshness = async (params, options) => {
8641
+ const data = await fetchPrices(params, options);
8642
+ // Bars are stamped uniformly inside `fetchPrices`; reading the first one is
8643
+ // sufficient. If the result is empty (no bars in the requested window) we
8644
+ // default to OK with the current wall clock — there is no upstream signal
8645
+ // to contradict it.
8646
+ const sampleFreshness = data[0]?._freshness;
8647
+ const status = sampleFreshness?.status ?? "OK";
8648
+ const receivedAt = sampleFreshness?.receivedAt ?? new Date();
8649
+ if (status === "DELAYED") {
8650
+ return {
8651
+ status: "DELAYED",
8652
+ data,
8653
+ receivedAt,
8654
+ delayedSince: sampleFreshness?.delayedSince ?? null,
8655
+ };
8656
+ }
8657
+ return { status: "OK", data, receivedAt };
8658
+ };
8608
8659
  /**
8609
8660
  * Analyzes the price data for a given stock.
8610
8661
  * @param {MassivePriceData[]} priceData - The price data to analyze.
@@ -9248,7 +9299,11 @@ function getEquityValues(equityData, portfolioHistory, marketTimeUtil, period) {
9248
9299
  initialEquity = Number(validData[0].value);
9249
9300
  }
9250
9301
  return {
9251
- latestEquity: Number(latestPoint.valueOf),
9302
+ // DE-005: previously `Number(latestPoint.valueOf)`, which read the
9303
+ // un-invoked function reference and silently returned NaN. `latestPoint`
9304
+ // is already a number (see line above; sourced from `point.value` which
9305
+ // is typed `number` in EquityPoint), so use it directly.
9306
+ latestEquity: latestPoint,
9252
9307
  initialEquity,
9253
9308
  latestTimestamp: validData[validData.length - 1].time,
9254
9309
  initialTimestamp: validData[0].time,
@@ -9347,25 +9402,42 @@ async function fetchTreasuryBillRate() {
9347
9402
  return percent / 100;
9348
9403
  }
9349
9404
  /**
9350
- * Returns the current annualized risk-free rate (decimal, e.g. 0.0452 for
9351
- * 4.52%), fetching from the US Treasury Fiscal Data API and caching for 24h.
9352
- *
9353
- * Behavior:
9354
- * - If a fresh cached value exists (<24h old), returns it without a network
9355
- * round-trip.
9356
- * - If the cache is stale or empty, fetches the latest 13-week T-Bill rate,
9357
- * updates the cache, and returns it.
9358
- * - If the fetch fails, returns the last-known-good cached value (even if
9359
- * expired) or {@link DEFAULT_RISK_FREE_RATE} as a last resort, logging a
9360
- * warning in both cases.
9361
- * - Concurrent calls during a cold cache are deduplicated so only one network
9362
- * request is in flight at a time.
9363
- *
9364
- * @returns Annualized risk-free rate as a decimal.
9365
- */
9366
- async function getRiskFreeRate() {
9367
- if (isFresh(cache)) {
9368
- return cache.rate;
9405
+ * Provenance-aware variant of {@link getRiskFreeRate} that returns the rate
9406
+ * AND tells the caller where it came from. Use this in any code path that
9407
+ * publishes performance metrics (Sharpe, alpha, Sortino) so downstream
9408
+ * reports can flag computations made against a fictional fallback rate.
9409
+ *
9410
+ * Behavior is identical to {@link getRiskFreeRate} for the cache and
9411
+ * deduplication semantics; only the return shape differs:
9412
+ *
9413
+ * - Fresh cache hit: `{ source: "cached", fetchedAt: <original fetch time> }`
9414
+ * - Cold or stale cache + successful fetch: `{ source: "live", fetchedAt: <now> }`
9415
+ * - Cold or stale cache + failed fetch but cached value present:
9416
+ * `{ source: "cached", fetchedAt: <original fetch time> }` the stale
9417
+ * value is reused so existing alpha calculations keep working through a
9418
+ * transient outage. The provenance still says `cached`, not `live`.
9419
+ * - Cold cache + failed fetch + no cached value:
9420
+ * `{ source: "default", fetchedAt: <now> }` — the 2% fallback is used. This
9421
+ * is the case downstream reports MUST flag, because Sharpe / alpha computed
9422
+ * against a 2% floor that was never observed in market data is a fiction.
9423
+ *
9424
+ * DE-029: closes the silent-failure loop where a first-fetch network failure
9425
+ * propagated `DEFAULT_RISK_FREE_RATE` indistinguishable from a live
9426
+ * observation.
9427
+ *
9428
+ * @returns The annualized risk-free rate plus its provenance.
9429
+ */
9430
+ async function getRiskFreeRateWithProvenance() {
9431
+ // Snapshot the cache reference before the freshness check so the type
9432
+ // narrows correctly without a non-null assertion (the check itself uses a
9433
+ // mutable module-level variable, which TypeScript will not narrow across).
9434
+ const snapshot = cache;
9435
+ if (snapshot !== null && isFresh(snapshot)) {
9436
+ return {
9437
+ rate: snapshot.rate,
9438
+ source: "cached",
9439
+ fetchedAt: new Date(snapshot.fetchedAt),
9440
+ };
9369
9441
  }
9370
9442
  if (inflight !== null) {
9371
9443
  return inflight;
@@ -9373,17 +9445,34 @@ async function getRiskFreeRate() {
9373
9445
  inflight = (async () => {
9374
9446
  try {
9375
9447
  const rate = await fetchTreasuryBillRate();
9376
- cache = { rate, fetchedAt: Date.now() };
9377
- return rate;
9448
+ const fetchedAtMs = Date.now();
9449
+ cache = { rate, fetchedAt: fetchedAtMs };
9450
+ return {
9451
+ rate,
9452
+ source: "live",
9453
+ fetchedAt: new Date(fetchedAtMs),
9454
+ };
9378
9455
  }
9379
9456
  catch (error) {
9380
9457
  const message = error instanceof Error ? error.message : String(error);
9381
9458
  if (cache !== null) {
9382
- getLogger().warn("Failed to refresh risk-free rate; using last-known-good cached value", { error: message, cachedRate: cache.rate, cacheAgeMs: Date.now() - cache.fetchedAt });
9383
- return cache.rate;
9459
+ getLogger().warn("Failed to refresh risk-free rate; using last-known-good cached value", {
9460
+ error: message,
9461
+ cachedRate: cache.rate,
9462
+ cacheAgeMs: Date.now() - cache.fetchedAt,
9463
+ });
9464
+ return {
9465
+ rate: cache.rate,
9466
+ source: "cached",
9467
+ fetchedAt: new Date(cache.fetchedAt),
9468
+ };
9384
9469
  }
9385
9470
  getLogger().warn("Failed to fetch risk-free rate and no cached value available; falling back to DEFAULT_RISK_FREE_RATE", { error: message, fallback: DEFAULT_RISK_FREE_RATE });
9386
- return DEFAULT_RISK_FREE_RATE;
9471
+ return {
9472
+ rate: DEFAULT_RISK_FREE_RATE,
9473
+ source: "default",
9474
+ fetchedAt: new Date(),
9475
+ };
9387
9476
  }
9388
9477
  finally {
9389
9478
  inflight = null;
@@ -9391,6 +9480,31 @@ async function getRiskFreeRate() {
9391
9480
  })();
9392
9481
  return inflight;
9393
9482
  }
9483
+ /**
9484
+ * Returns the current annualized risk-free rate (decimal, e.g. 0.0452 for
9485
+ * 4.52%), fetching from the US Treasury Fiscal Data API and caching for 24h.
9486
+ *
9487
+ * Behavior:
9488
+ * - If a fresh cached value exists (<24h old), returns it without a network
9489
+ * round-trip.
9490
+ * - If the cache is stale or empty, fetches the latest 13-week T-Bill rate,
9491
+ * updates the cache, and returns it.
9492
+ * - If the fetch fails, returns the last-known-good cached value (even if
9493
+ * expired) or {@link DEFAULT_RISK_FREE_RATE} as a last resort, logging a
9494
+ * warning in both cases.
9495
+ * - Concurrent calls during a cold cache are deduplicated so only one network
9496
+ * request is in flight at a time.
9497
+ *
9498
+ * For provenance-aware callers (performance reports, audit logging) prefer
9499
+ * {@link getRiskFreeRateWithProvenance}, which returns both the rate AND
9500
+ * whether it came from a live fetch, the cache, or the fallback default.
9501
+ *
9502
+ * @returns Annualized risk-free rate as a decimal.
9503
+ */
9504
+ async function getRiskFreeRate() {
9505
+ const result = await getRiskFreeRateWithProvenance();
9506
+ return result.rate;
9507
+ }
9394
9508
  /**
9395
9509
  * Synchronous accessor that returns the most recent cached risk-free rate
9396
9510
  * without performing I/O. If the cache is stale, a background refresh is
@@ -9405,22 +9519,53 @@ async function getRiskFreeRate() {
9405
9519
  * {@link DEFAULT_RISK_FREE_RATE} if no value has been cached yet.
9406
9520
  */
9407
9521
  function getCachedRiskFreeRateSync() {
9522
+ return getCachedRiskFreeRateSyncWithProvenance().rate;
9523
+ }
9524
+ /**
9525
+ * Provenance-aware sibling of {@link getCachedRiskFreeRateSync}. Returns the
9526
+ * cached rate plus a flag indicating whether a real value has been cached
9527
+ * (`"cached"`) or whether the caller is being given the {@link DEFAULT_RISK_FREE_RATE}
9528
+ * fallback (`"default"`).
9529
+ *
9530
+ * Use this in synchronous hot paths that nonetheless need to flag computations
9531
+ * made against the fallback (e.g., real-time alpha streaming where async
9532
+ * round-trips are not viable but downstream reports must still distinguish
9533
+ * live from fictional rates).
9534
+ *
9535
+ * As with the original sync accessor, a stale cache triggers a fire-and-forget
9536
+ * background refresh so the next synchronous call sees fresh data; the call
9537
+ * itself remains synchronous and returns the last-known-good value.
9538
+ *
9539
+ * DE-029: closes the silent-failure loop where the synchronous fallback was
9540
+ * indistinguishable from a real cache hit.
9541
+ *
9542
+ * @returns A {@link RiskFreeRateResult} carrying the rate and its provenance.
9543
+ */
9544
+ function getCachedRiskFreeRateSyncWithProvenance() {
9408
9545
  if (cache === null) {
9409
9546
  // Kick off a background fetch so the next sync caller has a real number.
9410
- void getRiskFreeRate().catch(() => {
9411
- // Errors are already logged inside getRiskFreeRate; swallow here to
9412
- // keep this truly fire-and-forget.
9547
+ void getRiskFreeRateWithProvenance().catch(() => {
9548
+ // Errors are already logged inside getRiskFreeRateWithProvenance;
9549
+ // swallow here to keep this truly fire-and-forget.
9413
9550
  });
9414
- return DEFAULT_RISK_FREE_RATE;
9551
+ return {
9552
+ rate: DEFAULT_RISK_FREE_RATE,
9553
+ source: "default",
9554
+ fetchedAt: new Date(),
9555
+ };
9415
9556
  }
9416
9557
  if (!isFresh(cache)) {
9417
9558
  // Stale: trigger background refresh but still return the last-known-good
9418
9559
  // value so the call remains synchronous.
9419
- void getRiskFreeRate().catch(() => {
9420
- // Errors are already logged inside getRiskFreeRate.
9560
+ void getRiskFreeRateWithProvenance().catch(() => {
9561
+ // Errors are already logged inside getRiskFreeRateWithProvenance.
9421
9562
  });
9422
9563
  }
9423
- return cache.rate;
9564
+ return {
9565
+ rate: cache.rate,
9566
+ source: "cached",
9567
+ fetchedAt: new Date(cache.fetchedAt),
9568
+ };
9424
9569
  }
9425
9570
 
9426
9571
  // metric-calcs.ts
@@ -68578,6 +68723,7 @@ const adaptic = {
68578
68723
  fetchLastQuote: fetchLastQuote,
68579
68724
  fetchTrades: fetchTrades,
68580
68725
  fetchPrices: fetchPrices,
68726
+ fetchPricesWithFreshness: fetchPricesWithFreshness,
68581
68727
  analyseMassivePriceData: analyseMassivePriceData,
68582
68728
  formatPriceData: formatPriceData,
68583
68729
  fetchDailyOpenClose: fetchDailyOpenClose,
@@ -68801,6 +68947,7 @@ exports.getAverageDailyVolume = getAverageDailyVolume;
68801
68947
  exports.getBars = getBars;
68802
68948
  exports.getBuyingPower = getBuyingPower;
68803
68949
  exports.getCachedRiskFreeRateSync = getCachedRiskFreeRateSync;
68950
+ exports.getCachedRiskFreeRateSyncWithProvenance = getCachedRiskFreeRateSyncWithProvenance;
68804
68951
  exports.getCrypto24HourChange = getCrypto24HourChange;
68805
68952
  exports.getCryptoBars = getCryptoBars;
68806
68953
  exports.getCryptoDailyPrices = getCryptoDailyPrices;
@@ -68858,6 +69005,7 @@ exports.getPortfolioHistory = getPortfolioHistory;
68858
69005
  exports.getPreviousClose = getPreviousClose;
68859
69006
  exports.getPriceRange = getPriceRange;
68860
69007
  exports.getRiskFreeRate = getRiskFreeRate;
69008
+ exports.getRiskFreeRateWithProvenance = getRiskFreeRateWithProvenance;
68861
69009
  exports.getSpread = getSpread;
68862
69010
  exports.getSpreads = getSpreads;
68863
69011
  exports.getStockStreamUrl = getStockStreamUrl;