@adaptic/utils 0.0.981 → 0.0.982

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
@@ -9257,6 +9257,172 @@ function getEquityValues(equityData, portfolioHistory, marketTimeUtil, period) {
9257
9257
  };
9258
9258
  }
9259
9259
 
9260
+ // risk-free-rate.ts
9261
+ /**
9262
+ * Conservative fallback annual risk-free rate used when no live rate has been
9263
+ * fetched yet AND the remote source is unreachable. Chosen to roughly match the
9264
+ * longer-run (post-2000) average of the 3-month US Treasury bill yield.
9265
+ *
9266
+ * This is the rate of LAST resort. Callers that need a live number should
9267
+ * prefer {@link getRiskFreeRate} (async) and only rely on
9268
+ * {@link getCachedRiskFreeRateSync} for hot paths that cannot be made async.
9269
+ */
9270
+ const DEFAULT_RISK_FREE_RATE = 0.02;
9271
+ /**
9272
+ * Cache TTL for the risk-free rate: 24 hours. Treasury yields update daily
9273
+ * (auction + close), so refreshing more aggressively provides no useful signal
9274
+ * and risks rate-limiting the public endpoint.
9275
+ */
9276
+ const RISK_FREE_RATE_TTL_MS = 24 * 60 * 60 * 1000;
9277
+ /**
9278
+ * US Treasury Fiscal Data API — Daily Treasury Bill Rates. Free, no API key,
9279
+ * updated each business day. Returns the most recent 4-, 8-, 13-, 17-, 26-,
9280
+ * and 52-week T-Bill rates. We use the 13-week ("3-month") field for Sharpe /
9281
+ * alpha, which is the industry-standard short risk-free proxy.
9282
+ *
9283
+ * See https://fiscaldata.treasury.gov/datasets/daily-treasury-bill-rates/
9284
+ */
9285
+ const TREASURY_BILL_RATES_URL = "https://api.fiscaldata.treasury.gov/services/api/fiscal_service/v2/accounting/od/daily_treasury_bill_rates" +
9286
+ "?sort=-record_date&page%5Bsize%5D=1" +
9287
+ "&fields=record_date,security_term_week_num,avg_inv_rate";
9288
+ let cache = null;
9289
+ let inflight = null;
9290
+ /**
9291
+ * Clears the cached risk-free rate. Exported for tests and for callers that
9292
+ * want to force a re-fetch (e.g., at the start of a backtest run with a
9293
+ * different asOf date).
9294
+ */
9295
+ function resetRiskFreeRateCache() {
9296
+ cache = null;
9297
+ inflight = null;
9298
+ }
9299
+ /**
9300
+ * Explicitly sets the cached risk-free rate. Useful for deterministic tests,
9301
+ * backtests (where rf should be pinned to the asOf date), and environments
9302
+ * where an upstream service already provides the rate.
9303
+ *
9304
+ * @param rate - Annualized decimal rate (e.g. 0.0452 for 4.52%).
9305
+ */
9306
+ function setRiskFreeRate(rate) {
9307
+ if (!Number.isFinite(rate) || rate < 0 || rate > 1) {
9308
+ throw new Error(`Invalid risk-free rate: ${rate}. Must be a finite decimal in [0, 1].`);
9309
+ }
9310
+ cache = { rate, fetchedAt: Date.now() };
9311
+ }
9312
+ /**
9313
+ * Returns true iff the cached entry is present and younger than the TTL.
9314
+ */
9315
+ function isFresh(entry) {
9316
+ return entry !== null && Date.now() - entry.fetchedAt < RISK_FREE_RATE_TTL_MS;
9317
+ }
9318
+ /**
9319
+ * Fetches the latest 3-month (13-week) T-Bill annualized rate from the US
9320
+ * Treasury Fiscal Data API. Returns the rate as a decimal (e.g. 0.0452 for
9321
+ * 4.52%). Throws on any failure (network, parse, or missing field) — callers
9322
+ * are expected to handle fallback via {@link getRiskFreeRate}.
9323
+ */
9324
+ async function fetchTreasuryBillRate() {
9325
+ const signal = createTimeoutSignal(DEFAULT_TIMEOUTS.GENERAL);
9326
+ const res = await fetch(TREASURY_BILL_RATES_URL, {
9327
+ headers: { Accept: "application/json" },
9328
+ signal,
9329
+ });
9330
+ if (!res.ok) {
9331
+ throw new Error(`Treasury Fiscal Data API returned HTTP ${res.status} ${res.statusText}`);
9332
+ }
9333
+ const body = (await res.json());
9334
+ const rows = Array.isArray(body.data) ? body.data : [];
9335
+ if (rows.length === 0) {
9336
+ throw new Error("Treasury Fiscal Data API returned no rows");
9337
+ }
9338
+ // Prefer the 13-week (3-month) bill; fall back to the shortest term
9339
+ // available on that record date if 13-week is missing.
9340
+ const preferred = rows.find((row) => row.security_term_week_num === "13") ?? rows[0];
9341
+ const percent = Number.parseFloat(preferred.avg_inv_rate);
9342
+ if (!Number.isFinite(percent)) {
9343
+ throw new Error(`Treasury Fiscal Data API returned non-numeric rate: ${preferred.avg_inv_rate}`);
9344
+ }
9345
+ // avg_inv_rate is quoted as a percentage (e.g. "4.52"); normalize to a
9346
+ // decimal for downstream math.
9347
+ return percent / 100;
9348
+ }
9349
+ /**
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;
9369
+ }
9370
+ if (inflight !== null) {
9371
+ return inflight;
9372
+ }
9373
+ inflight = (async () => {
9374
+ try {
9375
+ const rate = await fetchTreasuryBillRate();
9376
+ cache = { rate, fetchedAt: Date.now() };
9377
+ return rate;
9378
+ }
9379
+ catch (error) {
9380
+ const message = error instanceof Error ? error.message : String(error);
9381
+ 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;
9384
+ }
9385
+ 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;
9387
+ }
9388
+ finally {
9389
+ inflight = null;
9390
+ }
9391
+ })();
9392
+ return inflight;
9393
+ }
9394
+ /**
9395
+ * Synchronous accessor that returns the most recent cached risk-free rate
9396
+ * without performing I/O. If the cache is stale, a background refresh is
9397
+ * kicked off (fire-and-forget) so the next synchronous call sees a fresh
9398
+ * value. Intended for hot paths (e.g., Sharpe/alpha calculation inside tight
9399
+ * loops) where the existing function signature cannot be made async.
9400
+ *
9401
+ * Callers that can tolerate an async boundary should prefer
9402
+ * {@link getRiskFreeRate}.
9403
+ *
9404
+ * @returns The cached annualized risk-free rate as a decimal, or
9405
+ * {@link DEFAULT_RISK_FREE_RATE} if no value has been cached yet.
9406
+ */
9407
+ function getCachedRiskFreeRateSync() {
9408
+ if (cache === null) {
9409
+ // 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.
9413
+ });
9414
+ return DEFAULT_RISK_FREE_RATE;
9415
+ }
9416
+ if (!isFresh(cache)) {
9417
+ // Stale: trigger background refresh but still return the last-known-good
9418
+ // value so the call remains synchronous.
9419
+ void getRiskFreeRate().catch(() => {
9420
+ // Errors are already logged inside getRiskFreeRate.
9421
+ });
9422
+ }
9423
+ return cache.rate;
9424
+ }
9425
+
9260
9426
  // metric-calcs.ts
9261
9427
  /**
9262
9428
  * Calculates daily returns from an array of closing prices
@@ -9418,9 +9584,11 @@ function calculateBetaFromReturns$1(portfolioReturns, benchmarkReturns) {
9418
9584
  covariance += portfolioDiff * benchmarkDiff;
9419
9585
  variance += benchmarkDiff * benchmarkDiff;
9420
9586
  }
9421
- // Finalize calculations
9422
- covariance /= n;
9423
- variance /= n;
9587
+ // Finalize calculations using sample (Bessel-corrected) estimators —
9588
+ // divide by (n - 1), not n. The guard above (validIndices.length < 2)
9589
+ // already ensures n >= 2, so (n - 1) is always safe.
9590
+ covariance /= n - 1;
9591
+ variance /= n - 1;
9424
9592
  // Handle zero variance case
9425
9593
  if (Math.abs(variance) < 1e-10) {
9426
9594
  getLogger().warn("Benchmark variance is effectively zero. Setting beta to 0.");
@@ -9489,8 +9657,9 @@ async function calculateRiskAdjustedReturn$1(tradeBars) {
9489
9657
  getLogger().warn("Standard deviation is zero or non-finite, cannot calculate Sharpe ratio.");
9490
9658
  return "N/A";
9491
9659
  }
9492
- // Assume a risk-free rate, e.g., 2%
9493
- const riskFreeRate = 0.02; // Annual risk-free rate (2%)
9660
+ // Fetch live annualized risk-free rate (3-month T-Bill), cached daily.
9661
+ // See src/risk-free-rate.ts for source + fallback behavior.
9662
+ const riskFreeRate = await getRiskFreeRate();
9494
9663
  // Calculate Sharpe Ratio
9495
9664
  const sharpeRatio = (avgAnnualReturn - riskFreeRate) / stdDevAnnual;
9496
9665
  if (!isFinite(sharpeRatio)) {
@@ -9538,7 +9707,10 @@ async function calculateAlphaAndBeta$1(tradeBars, benchmarkBars, isShort) {
9538
9707
  alignedTradeReturns.length;
9539
9708
  const avgBenchmarkReturn = alignedBenchmarkReturns.reduce((sum, ret) => sum + ret, 0) /
9540
9709
  alignedBenchmarkReturns.length;
9541
- const riskFreeRateDaily = 0.02 / 252; // Assuming 2% annual risk-free rate
9710
+ // Fetch live annualized risk-free rate (3-month T-Bill), cached daily.
9711
+ // See src/risk-free-rate.ts for source + fallback behavior.
9712
+ const riskFreeRateAnnual = await getRiskFreeRate();
9713
+ const riskFreeRateDaily = riskFreeRateAnnual / 252;
9542
9714
  // Alpha calculation adjusts based on position direction
9543
9715
  const alpha = avgTradeReturn -
9544
9716
  (riskFreeRateDaily +
@@ -9842,8 +10014,9 @@ async function calculateRiskAdjustedReturn(portfolioHistory) {
9842
10014
  getLogger().warn("Standard deviation is zero or non-finite, cannot calculate Sharpe ratio.");
9843
10015
  return "N/A";
9844
10016
  }
9845
- // Assume a risk-free rate, e.g., 2%
9846
- const riskFreeRate = 0.02; // Annual risk-free rate (2%)
10017
+ // Fetch live annualized risk-free rate (3-month T-Bill), cached daily.
10018
+ // See src/risk-free-rate.ts for source + fallback behavior.
10019
+ const riskFreeRate = await getRiskFreeRate();
9847
10020
  // Calculate Sharpe Ratio
9848
10021
  const sharpeRatio = (avgAnnualReturn - riskFreeRate) / stdDevAnnual;
9849
10022
  if (!isFinite(sharpeRatio)) {
@@ -10027,7 +10200,9 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
10027
10200
  };
10028
10201
  }
10029
10202
  // **Calculate alpha**
10030
- const riskFreeRateAnnual = 0.02; // 2%
10203
+ // Fetch live annualized risk-free rate (3-month T-Bill), cached daily.
10204
+ // See src/risk-free-rate.ts for source + fallback behavior.
10205
+ const riskFreeRateAnnual = await getRiskFreeRate();
10031
10206
  const tradingDaysPerYear = 252;
10032
10207
  const riskFreeRateDaily = riskFreeRateAnnual / tradingDaysPerYear;
10033
10208
  const alpha = portfolioAvgReturn -
@@ -10291,8 +10466,12 @@ function calculateBetaFromReturns(portfolioReturns, benchmarkReturns) {
10291
10466
  covariance += portfolioDiff * benchmarkDiff;
10292
10467
  variance += benchmarkDiff ** 2;
10293
10468
  }
10294
- covariance /= n;
10295
- variance /= n;
10469
+ // Use sample (Bessel-corrected) estimators — divide by (n - 1), not n.
10470
+ // For n === 1 there is no degrees-of-freedom left; treat as zero variance
10471
+ // so beta falls through to the zero-variance guard below.
10472
+ const denom = n > 1 ? n - 1 : 1;
10473
+ covariance /= denom;
10474
+ variance /= denom;
10296
10475
  // Handle zero variance
10297
10476
  if (variance === 0) {
10298
10477
  getLogger().warn("Benchmark variance is zero. Setting beta to 0.");
@@ -68498,6 +68677,7 @@ exports.BarError = BarError;
68498
68677
  exports.CryptoDataError = CryptoDataError;
68499
68678
  exports.CryptoOrderError = CryptoOrderError;
68500
68679
  exports.DEFAULT_CACHE_OPTIONS = DEFAULT_CACHE_OPTIONS;
68680
+ exports.DEFAULT_RISK_FREE_RATE = DEFAULT_RISK_FREE_RATE;
68501
68681
  exports.DEFAULT_TIMEOUTS = DEFAULT_TIMEOUTS;
68502
68682
  exports.DEFAULT_TRADING_POLICY = DEFAULT_TRADING_POLICY;
68503
68683
  exports.DataFormatError = DataFormatError;
@@ -68520,6 +68700,7 @@ exports.NewsError = NewsError;
68520
68700
  exports.OptionStrategyError = OptionStrategyError;
68521
68701
  exports.OptionsDataError = OptionsDataError;
68522
68702
  exports.QuoteError = QuoteError;
68703
+ exports.RISK_FREE_RATE_TTL_MS = RISK_FREE_RATE_TTL_MS;
68523
68704
  exports.RateLimitError = RateLimitError;
68524
68705
  exports.RawMassivePriceDataSchema = RawMassivePriceDataSchema;
68525
68706
  exports.StampedeProtectedCache = StampedeProtectedCache;
@@ -68619,6 +68800,7 @@ exports.getAlpacaClock = getAlpacaClock;
68619
68800
  exports.getAverageDailyVolume = getAverageDailyVolume;
68620
68801
  exports.getBars = getBars;
68621
68802
  exports.getBuyingPower = getBuyingPower;
68803
+ exports.getCachedRiskFreeRateSync = getCachedRiskFreeRateSync;
68622
68804
  exports.getCrypto24HourChange = getCrypto24HourChange;
68623
68805
  exports.getCryptoBars = getCryptoBars;
68624
68806
  exports.getCryptoDailyPrices = getCryptoDailyPrices;
@@ -68675,6 +68857,7 @@ exports.getPopularCryptoPairs = getPopularCryptoPairs;
68675
68857
  exports.getPortfolioHistory = getPortfolioHistory;
68676
68858
  exports.getPreviousClose = getPreviousClose;
68677
68859
  exports.getPriceRange = getPriceRange;
68860
+ exports.getRiskFreeRate = getRiskFreeRate;
68678
68861
  exports.getSpread = getSpread;
68679
68862
  exports.getSpreads = getSpreads;
68680
68863
  exports.getStockStreamUrl = getStockStreamUrl;
@@ -68718,6 +68901,7 @@ exports.protectLongPosition = protectLongPosition;
68718
68901
  exports.protectShortPosition = protectShortPosition;
68719
68902
  exports.rateLimiters = rateLimiters;
68720
68903
  exports.resetLogger = resetLogger;
68904
+ exports.resetRiskFreeRateCache = resetRiskFreeRateCache;
68721
68905
  exports.rollOptionPosition = rollOptionPosition;
68722
68906
  exports.roundPriceForAlpaca = roundPriceForAlpaca$3;
68723
68907
  exports.roundPriceForAlpacaNumber = roundPriceForAlpacaNumber;
@@ -68728,6 +68912,7 @@ exports.sellCryptoNotional = sellCryptoNotional;
68728
68912
  exports.sellToClose = sellToClose;
68729
68913
  exports.sellToOpen = sellToOpen;
68730
68914
  exports.setLogger = setLogger;
68915
+ exports.setRiskFreeRate = setRiskFreeRate;
68731
68916
  exports.shortWithStopLoss = shortWithStopLoss;
68732
68917
  exports.sortOrdersByDate = sortOrdersByDate;
68733
68918
  exports.tradingPolicy = index;