@livefolio/sdk 0.3.2 → 0.3.3

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.js CHANGED
@@ -238,35 +238,19 @@ function getComputation(type) {
238
238
  return COMPUTATIONS[type] ?? null;
239
239
  }
240
240
 
241
- // src/providers/quote-overlay.ts
242
- function createQuoteOverlay(base, overridesByDate, options = {}) {
243
- return {
244
- async fetchBars(symbol, from) {
245
- const bars = await base.fetchBars(symbol, from);
246
- const dates = Object.keys(overridesByDate).sort();
247
- if (dates.length === 0) return bars;
248
- const result = [...bars];
249
- for (const date of dates) {
250
- const overrideValue = overridesByDate[date][symbol];
251
- let value = overrideValue;
252
- if (value === void 0) {
253
- if (!options.fallbackMissingQuotes) continue;
254
- if (result.length === 0) continue;
255
- value = result[result.length - 1].value;
256
- }
257
- const existingIdx = result.findIndex((b) => b.date === date);
258
- if (existingIdx >= 0) {
259
- result[existingIdx] = { date, value };
260
- } else {
261
- result.push({ date, value });
262
- }
263
- }
264
- return result;
265
- }
266
- };
267
- }
268
-
269
241
  // src/handles/indicator.ts
242
+ var FRED_SYMBOL_TO_TYPE = {
243
+ DGS3MO: "T3M",
244
+ DGS6MO: "T6M",
245
+ DGS1: "T1Y",
246
+ DGS2: "T2Y",
247
+ DGS3: "T3Y",
248
+ DGS5: "T5Y",
249
+ DGS7: "T7Y",
250
+ DGS10: "T10Y",
251
+ DGS20: "T20Y",
252
+ DGS30: "T30Y"
253
+ };
270
254
  function _subtractCalendarDays(date, days) {
271
255
  const d = new Date(date);
272
256
  d.setUTCDate(d.getUTCDate() - days);
@@ -337,14 +321,24 @@ var IndicatorHandle = class _IndicatorHandle {
337
321
  const { id } = await this.resolve();
338
322
  const latestClosed = await this._getLatestClosedTradingDay();
339
323
  if (this._cachedAsOf === latestClosed) return;
324
+ let horizon = latestClosed;
325
+ if (this.delay > 0) {
326
+ const tradingDays = await this._storage.tradingDays.getRange();
327
+ const idx = tradingDays.indexOf(latestClosed);
328
+ if (idx >= this.delay) {
329
+ horizon = tradingDays[idx - this.delay];
330
+ }
331
+ }
340
332
  const latestSeries = await this._getLatestSeriesDate(id);
341
- if (latestSeries === latestClosed) {
333
+ if (latestSeries === horizon) {
342
334
  this._cachedSeries = null;
343
335
  this._cachedAsOf = latestClosed;
344
336
  return;
345
337
  }
346
338
  if (!this._syncing) {
347
- this._syncing = this._sync(latestSeries ?? void 0, latestClosed).finally(() => {
339
+ this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
340
+ console.warn("[sdk] indicator sync failed, using stored data:", err);
341
+ }).finally(() => {
348
342
  this._syncing = null;
349
343
  });
350
344
  }
@@ -401,7 +395,16 @@ var IndicatorHandle = class _IndicatorHandle {
401
395
  if (info.provider !== "computed") {
402
396
  bars = await this._applyLeverage(bars, fromDate);
403
397
  }
404
- bars = bars.filter((b) => b.date <= latestClosed);
398
+ let horizon = latestClosed;
399
+ if (this.delay > 0) {
400
+ const tradingDays = await this._storage.tradingDays.getRange();
401
+ const idx = tradingDays.indexOf(latestClosed);
402
+ if (idx < this.delay) {
403
+ return;
404
+ }
405
+ horizon = tradingDays[idx - this.delay];
406
+ }
407
+ bars = bars.filter((b) => b.date <= horizon);
405
408
  if (bars.length > 0) {
406
409
  await this._upsertSeries(bars);
407
410
  }
@@ -414,17 +417,6 @@ var IndicatorHandle = class _IndicatorHandle {
414
417
  const { id } = await this.resolve();
415
418
  return this._storage.indicators.getSeries(id, range);
416
419
  }
417
- withMarket(market) {
418
- if (market === this._market) return this;
419
- return _IndicatorHandle.fromResolved(this._storage, market, this.id, {
420
- type: this.type,
421
- ticker: this.ticker,
422
- lookback: this.lookback,
423
- delay: this.delay,
424
- unit: this.unit,
425
- threshold: this.threshold
426
- });
427
- }
428
420
  /**
429
421
  * Apply leverage compounding to a raw bar series, anchored to a stored
430
422
  * leveraged value. Used by both `_sync` and `computeAt` so they stay
@@ -458,21 +450,21 @@ var IndicatorHandle = class _IndicatorHandle {
458
450
  return leveraged;
459
451
  }
460
452
  /**
461
- * Compute the indicator's value at `date` using the given market (typically
462
- * an overlay market for pre-close preview). Pure no writes to storage.
453
+ * Compute the indicator's value at `date` without persisting anything, with
454
+ * optional live-quote `overrides` keyed by raw market symbol (the same symbol
455
+ * space `MarketProvider.fetchBars` uses — ticker symbols for Price/SMA/etc.,
456
+ * `^VIX` / `^VIX3M` for macro, FRED series IDs like `DGS3MO` for Treasury).
457
+ *
458
+ * Bars for the underlying symbol are resolved storage-first when the market
459
+ * hasn't yet produced bars for `date` (trading day still open), and storage
460
+ * is the fallback whenever the remote fetch fails — see `_resolveRawBars`.
463
461
  *
464
- * For fetched types (yahoo/fred): fetches a small window of bars from
465
- * `market`, applies leverage compounding anchored to the stored leveraged
466
- * value at the bar before `date`.
467
- * For computed types (SMA, RSI, etc.): fetches enough raw price bars to
468
- * cover the indicator's lookback from `market`, applies leverage anchored
469
- * to the stored value just before the fetch window, runs the computation,
470
- * and returns the value at `date`.
471
- * For Threshold: returns the threshold constant.
472
- * For calendar: computes calendar value from the trading days list.
473
- * Returns null if the value cannot be computed.
462
+ * For Threshold: returns the threshold constant. For calendar types: computed
463
+ * from `tradingDays.getRange()`. For all others: `_resolveRawBars` leverage
464
+ * compounding (if any) lookback-specific computation. Returns null if the
465
+ * value cannot be computed.
474
466
  */
475
- async computeAt(market, date) {
467
+ async computeAt(date, overrides) {
476
468
  if (this.type === "Threshold") return this.threshold;
477
469
  const tickerSymbol = this.ticker?.symbol ?? null;
478
470
  const info = getProviderInfo(this.type, tickerSymbol);
@@ -484,8 +476,16 @@ var IndicatorHandle = class _IndicatorHandle {
484
476
  return computed.find((b) => b.date === date)?.value ?? null;
485
477
  }
486
478
  if (info.provider === "computed") {
487
- const from2 = _subtractCalendarDays(date, this.lookback + 10);
488
- const rawBars2 = await market.fetchBars(info.symbol, from2);
479
+ let calendarDays;
480
+ if (this.type === "RSI") {
481
+ calendarDays = Math.max(this.lookback * 10, 90);
482
+ } else if (this.type === "EMA") {
483
+ calendarDays = Math.max(this.lookback * 5, 60);
484
+ } else {
485
+ calendarDays = Math.ceil(this.lookback * 1.5) + 15;
486
+ }
487
+ const from2 = _subtractCalendarDays(date, this.lookback + calendarDays);
488
+ const rawBars2 = await this._resolveRawBars(info.symbol, from2, date, overrides);
489
489
  const anchorDate = rawBars2.length > 0 ? rawBars2[0].date : void 0;
490
490
  const priceBars = await this._applyLeverage(rawBars2, anchorDate);
491
491
  const computeFn = getComputation(this.type);
@@ -494,8 +494,8 @@ var IndicatorHandle = class _IndicatorHandle {
494
494
  return computed.find((b) => b.date === date)?.value ?? null;
495
495
  }
496
496
  const symbol = info.provider === "yahoo" ? info.symbol : info.seriesId;
497
- const from = _subtractCalendarDays(date, 5);
498
- const rawBars = await market.fetchBars(symbol, from);
497
+ const from = _subtractCalendarDays(date, 15);
498
+ const rawBars = await this._resolveRawBars(symbol, from, date, overrides);
499
499
  const leverage = this.ticker?.leverage ?? 1;
500
500
  if (leverage === 1) {
501
501
  return rawBars.find((b) => b.date === date)?.value ?? null;
@@ -511,6 +511,81 @@ var IndicatorHandle = class _IndicatorHandle {
511
511
  const rawReturn = (rawBars[dateIdx].value - prevBar.value) / prevBar.value;
512
512
  return leveragedPrev * (1 + leverage * rawReturn);
513
513
  }
514
+ /**
515
+ * Raw (unleveraged) bars for `symbol` up through `date`, with the live quote
516
+ * from `overrides[symbol]` (if any) spliced in at `date`.
517
+ *
518
+ * Decision policy:
519
+ * - `date` > `tradingDays.getLatestClosed()`: market has nothing for that
520
+ * day yet — skip the remote fetch entirely and read from storage.
521
+ * - otherwise: try `this._market.fetchBars(symbol, from)`. On failure, fall
522
+ * back to storage — upstream HTTP providers (Yahoo / FRED) are flaky.
523
+ *
524
+ * After the base is resolved, `overrides[symbol]` is spliced at `date`
525
+ * (replaces the existing bar, or is appended in-order). When no override is
526
+ * present but `date` isn't in the base bars, the last known value is carried
527
+ * forward to `date` — this preserves the fallbackMissingQuotes behaviour the
528
+ * old overlay exposed so leverage compounding / computations always have a
529
+ * point at `date` to land on.
530
+ */
531
+ async _resolveRawBars(symbol, from, date, overrides) {
532
+ const latestClosed = await this._storage.tradingDays.getLatestClosed();
533
+ const closedForDate = latestClosed !== null && date <= latestClosed;
534
+ let bars;
535
+ if (closedForDate) {
536
+ try {
537
+ bars = await this._market.fetchBars(symbol, from);
538
+ } catch {
539
+ bars = await this._readStoredBars(symbol, from);
540
+ }
541
+ } else {
542
+ bars = await this._readStoredBars(symbol, from);
543
+ }
544
+ const override = overrides?.[symbol];
545
+ const existingIdx = bars.findIndex((b) => b.date === date);
546
+ if (override !== void 0) {
547
+ if (existingIdx >= 0) {
548
+ bars[existingIdx] = { date, value: override };
549
+ } else {
550
+ bars = [...bars, { date, value: override }].sort((a, b) => a.date.localeCompare(b.date));
551
+ }
552
+ } else if (existingIdx < 0 && bars.length > 0) {
553
+ bars = [...bars, { date, value: bars[bars.length - 1].value }];
554
+ }
555
+ return bars;
556
+ }
557
+ /**
558
+ * Resolve raw (unleveraged) bars for a market symbol from storage. Maps:
559
+ * - `^VIX` → the VIX indicator's stored series
560
+ * - `^VIX3M` → the VIX3M indicator's stored series
561
+ * - `DGS*` → the matching Treasury-tenor indicator's stored series
562
+ * - anything else → the `Price` indicator for that ticker symbol with
563
+ * `leverage = 1` (the raw contract that `MarketProvider.fetchBars` has).
564
+ *
565
+ * Returns `[]` when the resolved indicator has no stored bars yet.
566
+ */
567
+ async _readStoredBars(symbol, from) {
568
+ let identity;
569
+ if (symbol === "^VIX") {
570
+ identity = { type: "VIX", tickerId: null, lookback: 0, delay: 0, unit: null, threshold: null };
571
+ } else if (symbol === "^VIX3M") {
572
+ identity = { type: "VIX3M", tickerId: null, lookback: 0, delay: 0, unit: null, threshold: null };
573
+ } else if (FRED_SYMBOL_TO_TYPE[symbol]) {
574
+ identity = {
575
+ type: FRED_SYMBOL_TO_TYPE[symbol],
576
+ tickerId: null,
577
+ lookback: 0,
578
+ delay: 0,
579
+ unit: null,
580
+ threshold: null
581
+ };
582
+ } else {
583
+ const { id: tickerId } = await this._storage.tickers.findOrCreate(symbol, 1);
584
+ identity = { type: "Price", tickerId, lookback: 0, delay: 0, unit: null, threshold: null };
585
+ }
586
+ const { id } = await this._storage.indicators.findOrCreate(identity);
587
+ return this._storage.indicators.getSeries(id, { from });
588
+ }
514
589
  // ── Public data access ─────────────────────────────────────────────
515
590
  async series(range) {
516
591
  if (this.type === "Threshold") {
@@ -533,36 +608,37 @@ var IndicatorHandle = class _IndicatorHandle {
533
608
  return this._storage.indicators.getValue(id, date);
534
609
  }
535
610
  /**
536
- * Read-only preview of the indicator series that includes an in-memory bar
537
- * at `date` computed via `computeAt` against a quote-overlay market. Does
611
+ * Read-only preview of the indicator series with an in-memory bar at `date`
612
+ * computed via `computeAt` with the supplied live-quote `overrides`. Does
538
613
  * NOT write to `indicators_series`. Safe to call before market close.
539
614
  *
540
- * @param date - Target trading day whose value is computed in-memory from
541
- * the overridden quotes. Must be in `tradingDays.getRange()`.
542
- * @param quoteOverrides - Raw (unleveraged) quotes keyed by ticker symbol.
543
- * Symbols omitted here fall back to yesterday's close via the overlay.
615
+ * @param date - Target trading day whose value is computed in-memory.
616
+ * Must be in `tradingDays.getRange()`.
617
+ * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
618
+ * Symbols omitted fall back to the last known value (see `_resolveRawBars`).
544
619
  * @param range - Optional filter applied to the returned bars.
545
620
  * @returns Stored historical bars plus (or with) today's in-memory value.
546
621
  */
547
- async previewSeries(date, quoteOverrides, range) {
622
+ async previewSeries(date, overrides, range) {
548
623
  const tradingDays = await this._storage.tradingDays.getRange();
549
624
  if (!tradingDays.includes(date)) {
550
625
  throw new Error(`previewSeries: ${date} is not a trading day`);
551
626
  }
552
- const overlay = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
553
627
  let bars;
554
628
  if (this.type === "Threshold") {
555
629
  bars = await this._syntheticThresholdSeries();
556
630
  } else {
557
631
  bars = await this._querySeriesFromDb();
558
632
  }
559
- const todayValue = await this.computeAt(overlay, date);
560
- if (todayValue !== null) {
561
- const idx = bars.findIndex((b) => b.date === date);
562
- if (idx >= 0) {
563
- bars[idx] = { date, value: todayValue };
564
- } else {
565
- bars = [...bars, { date, value: todayValue }].sort((a, b) => a.date.localeCompare(b.date));
633
+ if (this.delay === 0) {
634
+ const todayValue = await this.computeAt(date, overrides);
635
+ if (todayValue !== null) {
636
+ const idx = bars.findIndex((b) => b.date === date);
637
+ if (idx >= 0) {
638
+ bars[idx] = { date, value: todayValue };
639
+ } else {
640
+ bars = [...bars, { date, value: todayValue }].sort((a, b) => a.date.localeCompare(b.date));
641
+ }
566
642
  }
567
643
  }
568
644
  if (range) {
@@ -652,15 +728,17 @@ var SignalHandle = class _SignalHandle {
652
728
  comparison;
653
729
  tolerance;
654
730
  _storage;
655
- _market;
656
731
  _resolvedId = null;
657
732
  _resolving = null;
658
733
  _cachedSeries = null;
659
734
  _cachedAsOf = null;
660
735
  _syncing = null;
661
- constructor(storage, market, identity) {
736
+ // The `market` parameter is kept in the signature for API compatibility with
737
+ // `new SignalHandle(storage, market, identity)` — signals no longer carry
738
+ // their own market reference since `computeAt` delegates to the indicator
739
+ // handles, which already hold one.
740
+ constructor(storage, _market, identity) {
662
741
  this._storage = storage;
663
- this._market = market;
664
742
  this.indicator1 = identity.indicator1;
665
743
  this.indicator2 = identity.indicator2;
666
744
  this.comparison = identity.comparison;
@@ -716,7 +794,9 @@ var SignalHandle = class _SignalHandle {
716
794
  return;
717
795
  }
718
796
  if (!this._syncing) {
719
- this._syncing = this._sync(latestSeries ?? void 0, latestClosed).finally(() => {
797
+ this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
798
+ console.warn("[sdk] signal sync failed, using stored data:", err);
799
+ }).finally(() => {
720
800
  this._syncing = null;
721
801
  });
722
802
  }
@@ -744,19 +824,11 @@ var SignalHandle = class _SignalHandle {
744
824
  const { id } = await this.resolve();
745
825
  return this._storage.signals.getSeries(id, range);
746
826
  }
747
- withMarket(market) {
748
- if (market === this._market) return this;
749
- return _SignalHandle.fromResolved(this._storage, market, this.id, {
750
- indicator1: this.indicator1.withMarket(market),
751
- indicator2: this.indicator2.withMarket(market),
752
- comparison: this.comparison,
753
- tolerance: this.tolerance
754
- });
755
- }
756
827
  /**
757
- * Compute the signal's boolean value at `date` using the given market
758
- * (typically an overlay market for pre-close preview). Pure — no writes.
759
- * Returns null if either indicator cannot produce a value at `date`.
828
+ * Compute the signal's boolean value at `date` without persisting anything,
829
+ * with optional live-quote `overrides` that are routed through each
830
+ * indicator's `computeAt`. Returns null if either indicator cannot produce
831
+ * a value at `date`.
760
832
  *
761
833
  * @param prevBool - The signal's boolean value at the bar immediately
762
834
  * preceding `date`, used for hysteresis when `tolerance > 0`. If not
@@ -764,10 +836,10 @@ var SignalHandle = class _SignalHandle {
764
836
  * standalone callers). On the preview path `_evaluate` passes this from
765
837
  * the in-memory `dateMap` so we never read stale storage.
766
838
  */
767
- async computeAt(market, date, prevBool) {
839
+ async computeAt(date, overrides, prevBool) {
768
840
  const [v1, v2] = await Promise.all([
769
- this.indicator1.computeAt(market, date),
770
- this.indicator2.computeAt(market, date)
841
+ this.indicator1.computeAt(date, overrides),
842
+ this.indicator2.computeAt(date, overrides)
771
843
  ]);
772
844
  if (v1 === null || v2 === null) return null;
773
845
  const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
@@ -818,26 +890,25 @@ var SignalHandle = class _SignalHandle {
818
890
  }
819
891
  /**
820
892
  * Read-only preview of the signal series with an in-memory bar at `date`
821
- * computed via `computeAt` against a quote-overlay market. Does NOT write
822
- * to `signals_series`.
893
+ * computed via `computeAt` with the supplied live-quote `overrides`. Does
894
+ * NOT write to `signals_series`.
823
895
  *
824
896
  * @param date - Target trading day whose boolean is computed in-memory.
825
- * @param quoteOverrides - Raw (unleveraged) quotes keyed by ticker symbol.
897
+ * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
826
898
  * @param range - Optional filter applied to the returned bars.
827
899
  */
828
- async previewSeries(date, quoteOverrides, range) {
900
+ async previewSeries(date, overrides, range) {
829
901
  const tradingDays = await this._storage.tradingDays.getRange();
830
902
  if (!tradingDays.includes(date)) {
831
903
  throw new Error(`previewSeries: ${date} is not a trading day`);
832
904
  }
833
- const overlay = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
834
905
  let bars = await this._querySeriesFromDb();
835
906
  const dateMap = /* @__PURE__ */ new Map();
836
907
  for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
837
908
  const limitIdx = tradingDays.indexOf(date);
838
909
  const prevDate = limitIdx > 0 ? tradingDays[limitIdx - 1] : void 0;
839
910
  const prevBool = prevDate !== void 0 ? dateMap.get(prevDate) ?? null : null;
840
- const todayBool = await this.computeAt(overlay, date, prevBool);
911
+ const todayBool = await this.computeAt(date, overrides, prevBool);
841
912
  if (todayBool !== null) {
842
913
  const numeric = todayBool ? 1 : 0;
843
914
  const idx = bars.findIndex((b) => b.date === date);
@@ -1219,7 +1290,9 @@ var SimulationHandle = class {
1219
1290
  _lastLeveragedPrices;
1220
1291
  _currentLeveragedPrices;
1221
1292
  _lastDate;
1222
- constructor(series, trades, startingPortfolio, finalState) {
1293
+ _pushedQuotes;
1294
+ _liveEvaluator;
1295
+ constructor(series, trades, startingPortfolio, finalState, liveEvaluator) {
1223
1296
  this.series = series;
1224
1297
  this.trades = trades;
1225
1298
  this.startingPortfolio = startingPortfolio;
@@ -1238,6 +1311,8 @@ var SimulationHandle = class {
1238
1311
  this._currentLeveragedPrices = /* @__PURE__ */ new Map();
1239
1312
  this._lastDate = "";
1240
1313
  }
1314
+ this._pushedQuotes = {};
1315
+ this._liveEvaluator = liveEvaluator ?? null;
1241
1316
  }
1242
1317
  push(...prices) {
1243
1318
  if (!this._portfolio || !this._currentAllocation) {
@@ -1272,6 +1347,48 @@ var SimulationHandle = class {
1272
1347
  pendingTrades: this._portfolio.trades(this._currentAllocation, priceArray, this._lastDate)
1273
1348
  };
1274
1349
  }
1350
+ /**
1351
+ * One-call live update. Feeds portfolio-relevant ticker prices into `push`
1352
+ * (derived from `quotes` via the running portfolio's holdings), accumulates
1353
+ * every symbol in `quotes` into an internal override map so macro symbols
1354
+ * (e.g. `^VIX`) persist across ticks, then delegates to the simulation's
1355
+ * strategy for rule / signal / indicator evaluation at `date`.
1356
+ *
1357
+ * Without a live evaluator attached, returns just the portfolio snapshot
1358
+ * with allocation/rules/signals empty.
1359
+ *
1360
+ * @param quotes Symbol → raw live price. Portfolio tickers flow through
1361
+ * `push` for leveraged-equity math; non-portfolio symbols are still
1362
+ * layered into the overlay so indicators can see them.
1363
+ * @param options.date Target trading day to evaluate against. Defaults to
1364
+ * the current UTC ISO date; callers with non-UTC semantics or after-hours
1365
+ * rollover should supply their own.
1366
+ */
1367
+ async pushAndPreview(quotes, options = {}) {
1368
+ const priceArgs = [];
1369
+ if (this._portfolio) {
1370
+ const seen = /* @__PURE__ */ new Set();
1371
+ for (const [ticker] of this._portfolio.holdings) {
1372
+ if (ticker.symbol === "CASHX") continue;
1373
+ if (seen.has(ticker.symbol)) continue;
1374
+ const price = quotes[ticker.symbol];
1375
+ if (price !== void 0) {
1376
+ priceArgs.push([ticker, price]);
1377
+ seen.add(ticker.symbol);
1378
+ }
1379
+ }
1380
+ }
1381
+ const snapshot = this.push(...priceArgs);
1382
+ for (const [symbol, price] of Object.entries(quotes)) {
1383
+ this._pushedQuotes[symbol] = price;
1384
+ }
1385
+ if (!this._liveEvaluator) {
1386
+ return { snapshot, allocation: null, activeRuleIndex: -1, rules: [] };
1387
+ }
1388
+ const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1389
+ const strategyState = await this._liveEvaluator.previewLiveState(date, { ...this._pushedQuotes });
1390
+ return { snapshot, ...strategyState };
1391
+ }
1275
1392
  };
1276
1393
 
1277
1394
  // src/handles/strategy.ts
@@ -1456,7 +1573,9 @@ var StrategyHandle = class {
1456
1573
  return;
1457
1574
  }
1458
1575
  if (!this._syncing) {
1459
- this._syncing = this._sync(latestClosed).finally(() => {
1576
+ this._syncing = this._sync(latestClosed).catch((err) => {
1577
+ console.warn("[sdk] strategy sync failed, using stored data:", err);
1578
+ }).finally(() => {
1460
1579
  this._syncing = null;
1461
1580
  });
1462
1581
  }
@@ -1466,7 +1585,7 @@ var StrategyHandle = class {
1466
1585
  }
1467
1586
  async _sync(latestClosed) {
1468
1587
  const { id } = await this.resolve();
1469
- const { entries } = await this._evaluate(this._market, latestClosed);
1588
+ const { entries } = await this._evaluate(latestClosed);
1470
1589
  if (entries.length > 0) {
1471
1590
  await this._storage.strategies.writeSeries(id, entries);
1472
1591
  }
@@ -1474,26 +1593,25 @@ var StrategyHandle = class {
1474
1593
  /**
1475
1594
  * Pure evaluate — runs the same pipeline as _sync but returns the computed
1476
1595
  * evaluation instead of persisting. Used by both _sync (post-close write
1477
- * path) and previewAllocation (pre-close read-only path).
1596
+ * path) and the public preview methods (pre-close read-only path).
1478
1597
  *
1479
- * The `market === this._market` identity check is what distinguishes the two
1480
- * paths: when they are the same object the method takes the write path (syncing
1481
- * signals through storage as normal); when they differ it takes the read-only
1482
- * preview path (historical bars from storage, today's value computed in-memory
1483
- * via `computeAt`). Callers that want the preview path must therefore supply a
1484
- * *different* market object — for example one produced by `createQuoteOverlay`.
1598
+ * When `overrides` is `undefined` we take the write path syncing signals
1599
+ * through storage as normal. When `overrides` is provided (even an empty
1600
+ * map) we take the read-only preview path: historical signal bars come
1601
+ * straight from storage, today's bar is computed in-memory via
1602
+ * `signal.computeAt(date, overrides, prevBool)`, and nothing is written.
1485
1603
  */
1486
- async _evaluate(market, limitDate) {
1604
+ async _evaluate(limitDate, overrides) {
1487
1605
  const allSignals = /* @__PURE__ */ new Set();
1488
1606
  for (const rule of this._rules) {
1489
1607
  if (rule.when) rule.when.forEach((s) => allSignals.add(s));
1490
1608
  }
1491
1609
  const signalSeries = /* @__PURE__ */ new Map();
1492
1610
  const tradingDays = await this._storage.tradingDays.getRange();
1493
- if (market === this._market) {
1611
+ if (overrides === void 0) {
1494
1612
  await Promise.all(
1495
1613
  Array.from(allSignals).map(async (signal) => {
1496
- const bars = await signal.withMarket(market).series();
1614
+ const bars = await signal.series();
1497
1615
  const dateMap = /* @__PURE__ */ new Map();
1498
1616
  for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
1499
1617
  signalSeries.set(signal.id, dateMap);
@@ -1508,7 +1626,7 @@ var StrategyHandle = class {
1508
1626
  const dateMap = /* @__PURE__ */ new Map();
1509
1627
  for (const bar of historicalBars) dateMap.set(bar.date, bar.value === 1);
1510
1628
  const prevBool = prevDate !== void 0 ? dateMap.get(prevDate) ?? null : null;
1511
- const todayValue = await signal.computeAt(market, limitDate, prevBool);
1629
+ const todayValue = await signal.computeAt(limitDate, overrides, prevBool);
1512
1630
  if (todayValue !== null) {
1513
1631
  dateMap.set(limitDate, todayValue);
1514
1632
  }
@@ -1584,7 +1702,10 @@ var StrategyHandle = class {
1584
1702
  closePrices,
1585
1703
  leveragedPrices
1586
1704
  };
1587
- return new SimulationHandle(result.series, result.trades, options.portfolio, finalState);
1705
+ const liveEvaluator = {
1706
+ previewLiveState: (date, overrides) => this.previewLiveState(date, overrides)
1707
+ };
1708
+ return new SimulationHandle(result.series, result.trades, options.portfolio, finalState, liveEvaluator);
1588
1709
  }
1589
1710
  /**
1590
1711
  * Preview the allocation this strategy would produce for `date` if today
@@ -1592,20 +1713,19 @@ var StrategyHandle = class {
1592
1713
  * signals_series, or indicators_series. Safe to call before market close.
1593
1714
  *
1594
1715
  * @param date - The trading day to preview (must be in tradingDays.getRange()).
1595
- * @param quoteOverrides - Raw (unleveraged) live prices keyed by ticker symbol.
1596
- * Symbols absent from this map will fall back to yesterday's close via the
1597
- * quote overlay.
1716
+ * @param overrides - Raw (unleveraged) live prices keyed by market symbol.
1717
+ * Symbols absent from this map fall back to the last stored value
1718
+ * (see `IndicatorHandle._resolveRawBars`).
1598
1719
  * @returns The AllocationHandle for `date`, or null if the strategy has no
1599
1720
  * evaluable entry for that date.
1600
1721
  */
1601
- async previewAllocation(date, quoteOverrides) {
1722
+ async previewAllocation(date, overrides) {
1602
1723
  await this.resolve();
1603
1724
  const tradingDays = await this._storage.tradingDays.getRange();
1604
1725
  if (!tradingDays.includes(date)) {
1605
1726
  throw new Error(`previewAllocation: ${date} is not a trading day`);
1606
1727
  }
1607
- const overlayMarket = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
1608
- const { allocations, entries } = await this._evaluate(overlayMarket, date);
1728
+ const { allocations, entries } = await this._evaluate(date, overrides);
1609
1729
  const target = entries.find((e) => e.date === date);
1610
1730
  if (!target) return null;
1611
1731
  const alloc = allocations.find((a) => a.id === target.allocationId);
@@ -1614,20 +1734,19 @@ var StrategyHandle = class {
1614
1734
  /**
1615
1735
  * Read-only preview of the strategy's allocation series including `date`.
1616
1736
  * Returns stored historical allocations plus an in-memory bar at `date`
1617
- * computed via the same overlay path as `previewAllocation`.
1737
+ * computed via the same overrides-based preview path as `previewAllocation`.
1618
1738
  *
1619
- * @param date - Target trading day to splice in-memory via overlay market.
1620
- * @param quoteOverrides - Raw (unleveraged) quotes keyed by ticker symbol.
1739
+ * @param date - Target trading day to splice in-memory.
1740
+ * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
1621
1741
  * @param range - Optional filter applied to the returned bars.
1622
1742
  */
1623
- async previewSeries(date, quoteOverrides, range) {
1743
+ async previewSeries(date, overrides, range) {
1624
1744
  await this.resolve();
1625
1745
  const tradingDays = await this._storage.tradingDays.getRange();
1626
1746
  if (!tradingDays.includes(date)) {
1627
1747
  throw new Error(`previewSeries: ${date} is not a trading day`);
1628
1748
  }
1629
- const overlayMarket = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
1630
- const { allocations, entries } = await this._evaluate(overlayMarket, date);
1749
+ const { allocations, entries } = await this._evaluate(date, overrides);
1631
1750
  const allocById = /* @__PURE__ */ new Map();
1632
1751
  for (const a of allocations) allocById.set(a.id, a);
1633
1752
  for (const [id, a] of this._allocationMap) if (!allocById.has(id)) allocById.set(id, a);
@@ -1642,6 +1761,70 @@ var StrategyHandle = class {
1642
1761
  }
1643
1762
  return bars;
1644
1763
  }
1764
+ /**
1765
+ * Full live strategy view at `date` under live-quote `overrides`: the active
1766
+ * allocation, the index of the rule that fired (or fallback), and per-rule
1767
+ * per-signal indicator values + truth. Computed entirely through the
1768
+ * overrides preview path — no writes to any `*_series` tables.
1769
+ *
1770
+ * Threshold indicators have their date suppressed (`null`) since their
1771
+ * synthetic series runs over every trading day in storage including future
1772
+ * dates and would report a far-future date for the last bar.
1773
+ */
1774
+ async previewLiveState(date, overrides) {
1775
+ await this.resolve();
1776
+ const tradingDays = await this._storage.tradingDays.getRange();
1777
+ if (!tradingDays.includes(date)) {
1778
+ throw new Error(`previewLiveState: ${date} is not a trading day`);
1779
+ }
1780
+ const [{ allocations, entries }, rules] = await Promise.all([
1781
+ this._evaluate(date, overrides),
1782
+ Promise.all(
1783
+ this._rules.map(async (rule) => {
1784
+ const signalHandles = rule.when ?? [];
1785
+ const signals = await Promise.all(
1786
+ signalHandles.map(async (sig) => {
1787
+ const [i1Series, i2Series, sigSeries] = await Promise.all([
1788
+ sig.indicator1.previewSeries(date, overrides),
1789
+ sig.indicator2.previewSeries(date, overrides),
1790
+ sig.previewSeries(date, overrides)
1791
+ ]);
1792
+ const last1 = i1Series.at(-1);
1793
+ const last2 = i2Series.at(-1);
1794
+ const lastSig = sigSeries.at(-1);
1795
+ const i1IsThreshold = sig.indicator1.type === "Threshold";
1796
+ const i2IsThreshold = sig.indicator2.type === "Threshold";
1797
+ return {
1798
+ indicator1: {
1799
+ value: last1?.value ?? null,
1800
+ date: i1IsThreshold ? null : last1?.date ?? null
1801
+ },
1802
+ indicator2: {
1803
+ value: last2?.value ?? null,
1804
+ date: i2IsThreshold ? null : last2?.date ?? null
1805
+ },
1806
+ isTrue: lastSig?.value === 1
1807
+ };
1808
+ })
1809
+ );
1810
+ return { signals };
1811
+ })
1812
+ )
1813
+ ]);
1814
+ const target = entries.find((e) => e.date === date);
1815
+ const allocation = target ? allocations.find((a) => a.id === target.allocationId) ?? this._allocationMap.get(target.allocationId) ?? null : null;
1816
+ const fallbackIndex = this._rules.length - 1;
1817
+ let activeRuleIndex = fallbackIndex;
1818
+ if (target) {
1819
+ for (let r = 0; r < this._rules.length; r++) {
1820
+ if (this._rules[r].hold.id === target.allocationId) {
1821
+ activeRuleIndex = r;
1822
+ break;
1823
+ }
1824
+ }
1825
+ }
1826
+ return { allocation, activeRuleIndex, rules };
1827
+ }
1645
1828
  async _fetchPricesForTickers(bars, from, to) {
1646
1829
  const tickerMap = /* @__PURE__ */ new Map();
1647
1830
  for (const bar of bars) {