@livefolio/sdk 0.3.2 → 0.3.4

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
@@ -96,6 +96,15 @@ function computeSma(bars, lookback) {
96
96
  }
97
97
  return result;
98
98
  }
99
+ function smaInitialState(bars, lookback) {
100
+ if (bars.length < lookback) return null;
101
+ return { tail: bars.slice(-lookback).map((b) => b.value) };
102
+ }
103
+ function smaNext(prev, newRaw, lookback) {
104
+ const tail = [...prev.tail.slice(1), newRaw];
105
+ const sum = tail.reduce((a, b) => a + b, 0);
106
+ return { value: sum / lookback, state: { tail } };
107
+ }
99
108
 
100
109
  // src/computations/ema.ts
101
110
  function computeEma(bars, lookback) {
@@ -112,6 +121,17 @@ function computeEma(bars, lookback) {
112
121
  }
113
122
  return result;
114
123
  }
124
+ function emaInitialState(bars, lookback) {
125
+ if (bars.length < lookback) return null;
126
+ const series = computeEma(bars, lookback);
127
+ if (series.length === 0) return null;
128
+ return { ema: series[series.length - 1].value };
129
+ }
130
+ function emaNext(prev, newRaw, lookback) {
131
+ const multiplier = 2 / (lookback + 1);
132
+ const ema = newRaw * multiplier + prev.ema * (1 - multiplier);
133
+ return { value: ema, state: { ema } };
134
+ }
115
135
 
116
136
  // src/computations/rsi.ts
117
137
  function computeRsi(bars, lookback) {
@@ -147,6 +167,34 @@ function computeRsi(bars, lookback) {
147
167
  }
148
168
  return result;
149
169
  }
170
+ function rsiInitialState(bars, lookback) {
171
+ if (bars.length < lookback + 1) return null;
172
+ let avgGain = 0;
173
+ let avgLoss = 0;
174
+ for (let i = 1; i <= lookback; i++) {
175
+ const change = bars[i].value - bars[i - 1].value;
176
+ if (change > 0) avgGain += change;
177
+ else avgLoss += -change;
178
+ }
179
+ avgGain /= lookback;
180
+ avgLoss /= lookback;
181
+ let state = { avgGain, avgLoss, prev: bars[lookback].value };
182
+ for (let i = lookback + 1; i < bars.length; i++) {
183
+ const { state: next } = rsiNext(state, bars[i].value, lookback);
184
+ state = next;
185
+ }
186
+ return state;
187
+ }
188
+ function rsiNext(prev, newRaw, lookback) {
189
+ const change = newRaw - prev.prev;
190
+ const gain = change > 0 ? change : 0;
191
+ const loss = change < 0 ? -change : 0;
192
+ const avgGain = (prev.avgGain * (lookback - 1) + gain) / lookback;
193
+ const avgLoss = (prev.avgLoss * (lookback - 1) + loss) / lookback;
194
+ const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
195
+ const value = avgLoss === 0 ? 100 : 100 - 100 / (1 + rs);
196
+ return { value, state: { avgGain, avgLoss, prev: newRaw } };
197
+ }
150
198
 
151
199
  // src/computations/returns.ts
152
200
  function computeReturns(bars, lookback, mode = "pct") {
@@ -160,6 +208,16 @@ function computeReturns(bars, lookback, mode = "pct") {
160
208
  }
161
209
  return result;
162
210
  }
211
+ function returnInitialState(bars, lookback) {
212
+ if (bars.length < lookback + 1) return null;
213
+ return { tail: bars.slice(-(lookback + 1)).map((b) => b.value) };
214
+ }
215
+ function returnNext(prev, newRaw, lookback, mode = "pct") {
216
+ const tail = [...prev.tail.slice(1), newRaw];
217
+ const old = tail[0];
218
+ const value = mode === "abs" ? newRaw - old : (newRaw - old) / old;
219
+ return { value, state: { tail } };
220
+ }
163
221
 
164
222
  // src/computations/volatility.ts
165
223
  function computeVolatility(bars, lookback) {
@@ -181,6 +239,18 @@ function computeVolatility(bars, lookback) {
181
239
  }
182
240
  return result;
183
241
  }
242
+ function volatilityInitialState(bars, lookback) {
243
+ if (bars.length < lookback + 1) return null;
244
+ return { tail: bars.slice(-(lookback + 1)).map((b) => b.value) };
245
+ }
246
+ function volatilityNext(prev, newRaw, lookback) {
247
+ const tail = [...prev.tail.slice(1), newRaw];
248
+ const returns = [];
249
+ for (let i = 1; i < tail.length; i++) returns.push(tail[i] / tail[i - 1] - 1);
250
+ const mean = returns.reduce((s, r) => s + r, 0) / lookback;
251
+ const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / lookback;
252
+ return { value: Math.sqrt(variance), state: { tail } };
253
+ }
184
254
 
185
255
  // src/computations/drawdown.ts
186
256
  function computeDrawdown(bars, lookback) {
@@ -195,6 +265,16 @@ function computeDrawdown(bars, lookback) {
195
265
  }
196
266
  return result;
197
267
  }
268
+ function drawdownInitialState(bars, lookback) {
269
+ if (bars.length < lookback) return null;
270
+ return { tail: bars.slice(-lookback).map((b) => b.value) };
271
+ }
272
+ function drawdownNext(prev, newRaw, _lookback) {
273
+ const tail = [...prev.tail.slice(1), newRaw];
274
+ let max = -Infinity;
275
+ for (const v of tail) if (v > max) max = v;
276
+ return { value: (newRaw - max) / max, state: { tail } };
277
+ }
198
278
 
199
279
  // src/computations/calendar.ts
200
280
  function dayOfYear(d) {
@@ -237,36 +317,42 @@ var COMPUTATIONS = {
237
317
  function getComputation(type) {
238
318
  return COMPUTATIONS[type] ?? null;
239
319
  }
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
- };
320
+ var NEXT = {
321
+ SMA: smaNext,
322
+ EMA: emaNext,
323
+ RSI: rsiNext,
324
+ Return: ((prev, newRaw, lookback) => returnNext(prev, newRaw, lookback, "pct")),
325
+ Volatility: volatilityNext,
326
+ Drawdown: drawdownNext
327
+ };
328
+ var SEED = {
329
+ SMA: smaInitialState,
330
+ EMA: emaInitialState,
331
+ RSI: rsiInitialState,
332
+ Return: returnInitialState,
333
+ Volatility: volatilityInitialState,
334
+ Drawdown: drawdownInitialState
335
+ };
336
+ function getNextComputation(type) {
337
+ return NEXT[type];
338
+ }
339
+ function getInitialStateFn(type) {
340
+ return SEED[type];
267
341
  }
268
342
 
269
343
  // src/handles/indicator.ts
344
+ var FRED_SYMBOL_TO_TYPE = {
345
+ DGS3MO: "T3M",
346
+ DGS6MO: "T6M",
347
+ DGS1: "T1Y",
348
+ DGS2: "T2Y",
349
+ DGS3: "T3Y",
350
+ DGS5: "T5Y",
351
+ DGS7: "T7Y",
352
+ DGS10: "T10Y",
353
+ DGS20: "T20Y",
354
+ DGS30: "T30Y"
355
+ };
270
356
  function _subtractCalendarDays(date, days) {
271
357
  const d = new Date(date);
272
358
  d.setUTCDate(d.getUTCDate() - days);
@@ -337,14 +423,24 @@ var IndicatorHandle = class _IndicatorHandle {
337
423
  const { id } = await this.resolve();
338
424
  const latestClosed = await this._getLatestClosedTradingDay();
339
425
  if (this._cachedAsOf === latestClosed) return;
426
+ let horizon = latestClosed;
427
+ if (this.delay > 0) {
428
+ const tradingDays = await this._storage.tradingDays.getRange();
429
+ const idx = tradingDays.indexOf(latestClosed);
430
+ if (idx >= this.delay) {
431
+ horizon = tradingDays[idx - this.delay];
432
+ }
433
+ }
340
434
  const latestSeries = await this._getLatestSeriesDate(id);
341
- if (latestSeries === latestClosed) {
435
+ if (latestSeries === horizon) {
342
436
  this._cachedSeries = null;
343
437
  this._cachedAsOf = latestClosed;
344
438
  return;
345
439
  }
346
440
  if (!this._syncing) {
347
- this._syncing = this._sync(latestSeries ?? void 0, latestClosed).finally(() => {
441
+ this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
442
+ console.warn("[sdk] indicator sync failed, using stored data:", err);
443
+ }).finally(() => {
348
444
  this._syncing = null;
349
445
  });
350
446
  }
@@ -355,6 +451,34 @@ var IndicatorHandle = class _IndicatorHandle {
355
451
  async _sync(fromDate, latestClosed) {
356
452
  const tickerSymbol = this.ticker?.symbol ?? null;
357
453
  const info = getProviderInfo(this.type, tickerSymbol);
454
+ if (info.provider === "none") return;
455
+ let horizon = latestClosed;
456
+ if (this.delay > 0) {
457
+ const tradingDays = await this._storage.tradingDays.getRange();
458
+ const idx = tradingDays.indexOf(latestClosed);
459
+ if (idx < this.delay) return;
460
+ horizon = tradingDays[idx - this.delay];
461
+ }
462
+ const nextFn = getNextComputation(this.type);
463
+ const seedFn = getInitialStateFn(this.type);
464
+ const { id } = await this.resolve();
465
+ const checkpoint = nextFn ? await this._storage.indicators.getLatestBar(id) : null;
466
+ if (fromDate && nextFn && seedFn && checkpoint && checkpoint.metadata != null && checkpoint.date < horizon) {
467
+ const rawBars = await this._fetchRawBarsForIncremental(info, checkpoint.date, horizon);
468
+ if (rawBars.length === 0) return;
469
+ const newBars = [];
470
+ let state = checkpoint.metadata;
471
+ for (const raw of rawBars) {
472
+ if (raw.date <= checkpoint.date) continue;
473
+ if (raw.date > horizon) break;
474
+ const step = this.type === "Return" && info.provider === "computed" && info.rateSeries ? returnNext(state, raw.value, this.lookback, "abs") : nextFn(state, raw.value, this.lookback);
475
+ newBars.push({ date: raw.date, value: step.value });
476
+ state = step.state;
477
+ }
478
+ if (newBars.length === 0) return;
479
+ await this._storage.indicators.writeSeries(id, newBars, { metadata: state });
480
+ return;
481
+ }
358
482
  let bars;
359
483
  switch (info.provider) {
360
484
  case "yahoo":
@@ -381,50 +505,78 @@ var IndicatorHandle = class _IndicatorHandle {
381
505
  if (!computeFn) throw new Error(`No computation found for type "${this.type}"`);
382
506
  bars = computeFn(priceBars, this.lookback);
383
507
  }
384
- if (fromDate) {
385
- bars = bars.filter((b) => b.date > fromDate);
386
- }
508
+ if (fromDate) bars = bars.filter((b) => b.date > fromDate);
387
509
  break;
388
510
  }
389
511
  case "calendar": {
390
512
  const allDays = await this._storage.tradingDays.getRange();
391
513
  const dayBars = allDays.map((date) => ({ date, value: 0 }));
392
514
  bars = computeCalendar(dayBars, this.type);
393
- if (fromDate) {
394
- bars = bars.filter((b) => b.date > fromDate);
395
- }
515
+ if (fromDate) bars = bars.filter((b) => b.date > fromDate);
396
516
  break;
397
517
  }
398
- case "none":
399
- return;
400
518
  }
401
519
  if (info.provider !== "computed") {
402
520
  bars = await this._applyLeverage(bars, fromDate);
403
521
  }
404
- bars = bars.filter((b) => b.date <= latestClosed);
405
- if (bars.length > 0) {
406
- await this._upsertSeries(bars);
522
+ bars = bars.filter((b) => b.date <= horizon);
523
+ if (bars.length === 0) return;
524
+ let metadata = void 0;
525
+ if (seedFn) {
526
+ if (info.provider === "computed") {
527
+ const priceHandle = new _IndicatorHandle(this._storage, this._market, {
528
+ type: "Price",
529
+ ticker: this.ticker,
530
+ lookback: 0,
531
+ delay: 0,
532
+ unit: null,
533
+ threshold: null
534
+ });
535
+ const priceBars = (await priceHandle._querySeriesFromDb()).filter((b) => b.date <= horizon);
536
+ metadata = seedFn(priceBars, this.lookback) ?? void 0;
537
+ } else {
538
+ metadata = seedFn(bars, this.lookback) ?? void 0;
539
+ }
407
540
  }
541
+ await this._upsertSeries(bars, metadata);
408
542
  }
409
- async _upsertSeries(bars) {
543
+ async _fetchRawBarsForIncremental(info, sinceDate, horizon) {
544
+ if (info.provider === "computed") {
545
+ const priceHandle = new _IndicatorHandle(this._storage, this._market, {
546
+ type: "Price",
547
+ ticker: this.ticker,
548
+ lookback: 0,
549
+ delay: 0,
550
+ unit: null,
551
+ threshold: null
552
+ });
553
+ await priceHandle._ensureFresh();
554
+ return (await priceHandle._querySeriesFromDb({ from: sinceDate })).filter(
555
+ (b) => b.date > sinceDate && b.date <= horizon
556
+ );
557
+ }
558
+ if (info.provider === "yahoo" || info.provider === "fred") {
559
+ const symbol = info.provider === "yahoo" ? info.symbol : info.seriesId;
560
+ const bars = await this._market.fetchBars(symbol, sinceDate);
561
+ return bars.filter((b) => b.date > sinceDate && b.date <= horizon);
562
+ }
563
+ if (info.provider === "calendar") {
564
+ const allDays = await this._storage.tradingDays.getRange();
565
+ const dayBars = allDays.map((date) => ({ date, value: 0 }));
566
+ return computeCalendar(dayBars, this.type).filter(
567
+ (b) => b.date > sinceDate && b.date <= horizon
568
+ );
569
+ }
570
+ return [];
571
+ }
572
+ async _upsertSeries(bars, metadata) {
410
573
  const { id } = await this.resolve();
411
- await this._storage.indicators.writeSeries(id, bars);
574
+ await this._storage.indicators.writeSeries(id, bars, metadata !== void 0 ? { metadata } : void 0);
412
575
  }
413
576
  async _querySeriesFromDb(range) {
414
577
  const { id } = await this.resolve();
415
578
  return this._storage.indicators.getSeries(id, range);
416
579
  }
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
580
  /**
429
581
  * Apply leverage compounding to a raw bar series, anchored to a stored
430
582
  * leveraged value. Used by both `_sync` and `computeAt` so they stay
@@ -458,21 +610,21 @@ var IndicatorHandle = class _IndicatorHandle {
458
610
  return leveraged;
459
611
  }
460
612
  /**
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.
613
+ * Compute the indicator's value at `date` without persisting anything, with
614
+ * optional live-quote `overrides` keyed by raw market symbol (the same symbol
615
+ * space `MarketProvider.fetchBars` uses — ticker symbols for Price/SMA/etc.,
616
+ * `^VIX` / `^VIX3M` for macro, FRED series IDs like `DGS3MO` for Treasury).
463
617
  *
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.
618
+ * Bars for the underlying symbol are resolved storage-first when the market
619
+ * hasn't yet produced bars for `date` (trading day still open), and storage
620
+ * is the fallback whenever the remote fetch fails — see `_resolveRawBars`.
621
+ *
622
+ * For Threshold: returns the threshold constant. For calendar types: computed
623
+ * from `tradingDays.getRange()`. For all others: `_resolveRawBars` leverage
624
+ * compounding (if any) lookback-specific computation. Returns null if the
625
+ * value cannot be computed.
474
626
  */
475
- async computeAt(market, date) {
627
+ async computeAt(date, overrides) {
476
628
  if (this.type === "Threshold") return this.threshold;
477
629
  const tickerSymbol = this.ticker?.symbol ?? null;
478
630
  const info = getProviderInfo(this.type, tickerSymbol);
@@ -484,8 +636,32 @@ var IndicatorHandle = class _IndicatorHandle {
484
636
  return computed.find((b) => b.date === date)?.value ?? null;
485
637
  }
486
638
  if (info.provider === "computed") {
487
- const from2 = _subtractCalendarDays(date, this.lookback + 10);
488
- const rawBars2 = await market.fetchBars(info.symbol, from2);
639
+ const nextFn = getNextComputation(this.type);
640
+ if (nextFn) {
641
+ const { id } = await this.resolve();
642
+ const checkpoint = await this._storage.indicators.getLatestBar(id);
643
+ if (checkpoint && checkpoint.metadata != null) {
644
+ const tradingDays = await this._storage.tradingDays.getRange();
645
+ const ckIdx = tradingDays.indexOf(checkpoint.date);
646
+ const tgtIdx = tradingDays.indexOf(date);
647
+ if (ckIdx >= 0 && tgtIdx === ckIdx + 1) {
648
+ const rawBar = await this._resolveRawBarAt(info.symbol, date, overrides);
649
+ if (rawBar === null) return null;
650
+ const step = this.type === "Return" && info.rateSeries ? returnNext(checkpoint.metadata, rawBar, this.lookback, "abs") : nextFn(checkpoint.metadata, rawBar, this.lookback);
651
+ return step.value;
652
+ }
653
+ }
654
+ }
655
+ let calendarDays;
656
+ if (this.type === "RSI") {
657
+ calendarDays = Math.max(this.lookback * 10, 90);
658
+ } else if (this.type === "EMA") {
659
+ calendarDays = Math.max(this.lookback * 5, 60);
660
+ } else {
661
+ calendarDays = Math.ceil(this.lookback * 1.5) + 15;
662
+ }
663
+ const from2 = _subtractCalendarDays(date, this.lookback + calendarDays);
664
+ const rawBars2 = await this._resolveRawBars(info.symbol, from2, date, overrides);
489
665
  const anchorDate = rawBars2.length > 0 ? rawBars2[0].date : void 0;
490
666
  const priceBars = await this._applyLeverage(rawBars2, anchorDate);
491
667
  const computeFn = getComputation(this.type);
@@ -494,8 +670,8 @@ var IndicatorHandle = class _IndicatorHandle {
494
670
  return computed.find((b) => b.date === date)?.value ?? null;
495
671
  }
496
672
  const symbol = info.provider === "yahoo" ? info.symbol : info.seriesId;
497
- const from = _subtractCalendarDays(date, 5);
498
- const rawBars = await market.fetchBars(symbol, from);
673
+ const from = _subtractCalendarDays(date, 15);
674
+ const rawBars = await this._resolveRawBars(symbol, from, date, overrides);
499
675
  const leverage = this.ticker?.leverage ?? 1;
500
676
  if (leverage === 1) {
501
677
  return rawBars.find((b) => b.date === date)?.value ?? null;
@@ -511,6 +687,93 @@ var IndicatorHandle = class _IndicatorHandle {
511
687
  const rawReturn = (rawBars[dateIdx].value - prevBar.value) / prevBar.value;
512
688
  return leveragedPrev * (1 + leverage * rawReturn);
513
689
  }
690
+ /**
691
+ * Raw (unleveraged) bars for `symbol` up through `date`, with the live quote
692
+ * from `overrides[symbol]` (if any) spliced in at `date`.
693
+ *
694
+ * Decision policy:
695
+ * - `date` > `tradingDays.getLatestClosed()`: market has nothing for that
696
+ * day yet — skip the remote fetch entirely and read from storage.
697
+ * - otherwise: try `this._market.fetchBars(symbol, from)`. On failure, fall
698
+ * back to storage — upstream HTTP providers (Yahoo / FRED) are flaky.
699
+ *
700
+ * After the base is resolved, `overrides[symbol]` is spliced at `date`
701
+ * (replaces the existing bar, or is appended in-order). When no override is
702
+ * present but `date` isn't in the base bars, the last known value is carried
703
+ * forward to `date` — this preserves the fallbackMissingQuotes behaviour the
704
+ * old overlay exposed so leverage compounding / computations always have a
705
+ * point at `date` to land on.
706
+ */
707
+ async _resolveRawBars(symbol, from, date, overrides) {
708
+ const latestClosed = await this._storage.tradingDays.getLatestClosed();
709
+ const closedForDate = latestClosed !== null && date <= latestClosed;
710
+ let bars;
711
+ if (closedForDate) {
712
+ try {
713
+ bars = await this._market.fetchBars(symbol, from);
714
+ } catch {
715
+ bars = await this._readStoredBars(symbol, from);
716
+ }
717
+ } else {
718
+ bars = await this._readStoredBars(symbol, from);
719
+ }
720
+ const override = overrides?.[symbol];
721
+ const existingIdx = bars.findIndex((b) => b.date === date);
722
+ if (override !== void 0) {
723
+ if (existingIdx >= 0) {
724
+ bars[existingIdx] = { date, value: override };
725
+ } else {
726
+ bars = [...bars, { date, value: override }].sort((a, b) => a.date.localeCompare(b.date));
727
+ }
728
+ } else if (existingIdx < 0 && bars.length > 0) {
729
+ bars = [...bars, { date, value: bars[bars.length - 1].value }];
730
+ }
731
+ return bars;
732
+ }
733
+ /**
734
+ * Resolve the single raw (unleveraged) value for `symbol` at `date`.
735
+ * Returns the override directly when present; otherwise delegates to
736
+ * `_resolveRawBars` with a one-day window and picks the matching bar.
737
+ */
738
+ async _resolveRawBarAt(symbol, date, overrides) {
739
+ const override = overrides?.[symbol];
740
+ if (override !== void 0) return override;
741
+ const bars = await this._resolveRawBars(symbol, date, date, overrides);
742
+ const hit = bars.find((b) => b.date === date);
743
+ return hit?.value ?? null;
744
+ }
745
+ /**
746
+ * Resolve raw (unleveraged) bars for a market symbol from storage. Maps:
747
+ * - `^VIX` → the VIX indicator's stored series
748
+ * - `^VIX3M` → the VIX3M indicator's stored series
749
+ * - `DGS*` → the matching Treasury-tenor indicator's stored series
750
+ * - anything else → the `Price` indicator for that ticker symbol with
751
+ * `leverage = 1` (the raw contract that `MarketProvider.fetchBars` has).
752
+ *
753
+ * Returns `[]` when the resolved indicator has no stored bars yet.
754
+ */
755
+ async _readStoredBars(symbol, from) {
756
+ let identity;
757
+ if (symbol === "^VIX") {
758
+ identity = { type: "VIX", tickerId: null, lookback: 0, delay: 0, unit: null, threshold: null };
759
+ } else if (symbol === "^VIX3M") {
760
+ identity = { type: "VIX3M", tickerId: null, lookback: 0, delay: 0, unit: null, threshold: null };
761
+ } else if (FRED_SYMBOL_TO_TYPE[symbol]) {
762
+ identity = {
763
+ type: FRED_SYMBOL_TO_TYPE[symbol],
764
+ tickerId: null,
765
+ lookback: 0,
766
+ delay: 0,
767
+ unit: null,
768
+ threshold: null
769
+ };
770
+ } else {
771
+ const { id: tickerId } = await this._storage.tickers.findOrCreate(symbol, 1);
772
+ identity = { type: "Price", tickerId, lookback: 0, delay: 0, unit: null, threshold: null };
773
+ }
774
+ const { id } = await this._storage.indicators.findOrCreate(identity);
775
+ return this._storage.indicators.getSeries(id, { from });
776
+ }
514
777
  // ── Public data access ─────────────────────────────────────────────
515
778
  async series(range) {
516
779
  if (this.type === "Threshold") {
@@ -533,36 +796,37 @@ var IndicatorHandle = class _IndicatorHandle {
533
796
  return this._storage.indicators.getValue(id, date);
534
797
  }
535
798
  /**
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
799
+ * Read-only preview of the indicator series with an in-memory bar at `date`
800
+ * computed via `computeAt` with the supplied live-quote `overrides`. Does
538
801
  * NOT write to `indicators_series`. Safe to call before market close.
539
802
  *
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.
803
+ * @param date - Target trading day whose value is computed in-memory.
804
+ * Must be in `tradingDays.getRange()`.
805
+ * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
806
+ * Symbols omitted fall back to the last known value (see `_resolveRawBars`).
544
807
  * @param range - Optional filter applied to the returned bars.
545
808
  * @returns Stored historical bars plus (or with) today's in-memory value.
546
809
  */
547
- async previewSeries(date, quoteOverrides, range) {
810
+ async previewSeries(date, overrides, range) {
548
811
  const tradingDays = await this._storage.tradingDays.getRange();
549
812
  if (!tradingDays.includes(date)) {
550
813
  throw new Error(`previewSeries: ${date} is not a trading day`);
551
814
  }
552
- const overlay = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
553
815
  let bars;
554
816
  if (this.type === "Threshold") {
555
817
  bars = await this._syntheticThresholdSeries();
556
818
  } else {
557
819
  bars = await this._querySeriesFromDb();
558
820
  }
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));
821
+ if (this.delay === 0) {
822
+ const todayValue = await this.computeAt(date, overrides);
823
+ if (todayValue !== null) {
824
+ const idx = bars.findIndex((b) => b.date === date);
825
+ if (idx >= 0) {
826
+ bars[idx] = { date, value: todayValue };
827
+ } else {
828
+ bars = [...bars, { date, value: todayValue }].sort((a, b) => a.date.localeCompare(b.date));
829
+ }
566
830
  }
567
831
  }
568
832
  if (range) {
@@ -652,15 +916,17 @@ var SignalHandle = class _SignalHandle {
652
916
  comparison;
653
917
  tolerance;
654
918
  _storage;
655
- _market;
656
919
  _resolvedId = null;
657
920
  _resolving = null;
658
921
  _cachedSeries = null;
659
922
  _cachedAsOf = null;
660
923
  _syncing = null;
661
- constructor(storage, market, identity) {
924
+ // The `market` parameter is kept in the signature for API compatibility with
925
+ // `new SignalHandle(storage, market, identity)` — signals no longer carry
926
+ // their own market reference since `computeAt` delegates to the indicator
927
+ // handles, which already hold one.
928
+ constructor(storage, _market, identity) {
662
929
  this._storage = storage;
663
- this._market = market;
664
930
  this.indicator1 = identity.indicator1;
665
931
  this.indicator2 = identity.indicator2;
666
932
  this.comparison = identity.comparison;
@@ -708,15 +974,20 @@ var SignalHandle = class _SignalHandle {
708
974
  const { id } = await this.resolve();
709
975
  const latestClosed = await this._getLatestClosedTradingDay();
710
976
  if (this._cachedAsOf === latestClosed) return;
711
- await Promise.all([this.indicator1.series(), this.indicator2.series()]);
712
977
  const latestSeries = await this._getLatestSignalSeriesDate(id);
713
978
  if (latestSeries === latestClosed) {
714
979
  this._cachedSeries = null;
715
980
  this._cachedAsOf = latestClosed;
716
981
  return;
717
982
  }
983
+ const isFastPath = await this._isSingleBarFastPath(latestSeries ?? void 0, latestClosed);
984
+ if (!isFastPath) {
985
+ await Promise.all([this.indicator1.series(), this.indicator2.series()]);
986
+ }
718
987
  if (!this._syncing) {
719
- this._syncing = this._sync(latestSeries ?? void 0, latestClosed).finally(() => {
988
+ this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
989
+ console.warn("[sdk] signal sync failed, using stored data:", err);
990
+ }).finally(() => {
720
991
  this._syncing = null;
721
992
  });
722
993
  }
@@ -724,17 +995,61 @@ var SignalHandle = class _SignalHandle {
724
995
  this._cachedSeries = null;
725
996
  this._cachedAsOf = latestClosed;
726
997
  }
998
+ async _isSingleBarFastPath(fromDate, latestClosed) {
999
+ if (!fromDate) return false;
1000
+ const tradingDays = await this._storage.tradingDays.getRange();
1001
+ const fromIdx = tradingDays.indexOf(fromDate);
1002
+ const closedIdx = tradingDays.indexOf(latestClosed);
1003
+ return fromIdx >= 0 && closedIdx === fromIdx + 1;
1004
+ }
727
1005
  async _sync(fromDate, latestClosed) {
728
1006
  const { id } = await this.resolve();
1007
+ const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
1008
+ if (fromDate) {
1009
+ const tradingDays = await this._storage.tradingDays.getRange();
1010
+ const fromIdx = tradingDays.indexOf(fromDate);
1011
+ const closedIdx = tradingDays.indexOf(latestClosed);
1012
+ if (fromIdx >= 0 && closedIdx === fromIdx + 1) {
1013
+ const newDate = tradingDays[closedIdx];
1014
+ const [v1, v2] = await Promise.all([
1015
+ this.indicator1.computeAt(newDate, void 0),
1016
+ this.indicator2.computeAt(newDate, void 0)
1017
+ ]);
1018
+ if (v1 === null || v2 === null) return;
1019
+ const prev = await this._getLastSignalValue(id) ?? void 0;
1020
+ const value = this._evaluateOneBar(v1, v2, absolute, prev);
1021
+ await this._upsertSeries([{ date: newDate, value }]);
1022
+ return;
1023
+ }
1024
+ }
729
1025
  const range = fromDate ? { from: fromDate } : void 0;
730
1026
  const [series1, series2] = await Promise.all([this.indicator1.series(range), this.indicator2.series(range)]);
731
1027
  const previousValue = fromDate ? await this._getLastSignalValue(id) ?? void 0 : void 0;
732
- const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
733
1028
  const signalBars = evaluateSignal(series1, series2, this.comparison, this.tolerance, absolute, previousValue);
734
1029
  const bars = signalBars.filter((b) => b.date <= latestClosed);
735
- if (bars.length > 0) {
736
- await this._upsertSeries(bars);
1030
+ if (bars.length > 0) await this._upsertSeries(bars);
1031
+ }
1032
+ _evaluateOneBar(v1, v2, absolute, prev) {
1033
+ if (this.tolerance === 0) {
1034
+ switch (this.comparison) {
1035
+ case ">":
1036
+ return v1 > v2 ? 1 : 0;
1037
+ case "<":
1038
+ return v1 < v2 ? 1 : 0;
1039
+ case "=":
1040
+ return v1 === v2 ? 1 : 0;
1041
+ }
1042
+ }
1043
+ const upper = absolute ? v2 + this.tolerance : v2 * (1 + this.tolerance / 100);
1044
+ const lower = absolute ? v2 - this.tolerance : v2 * (1 - this.tolerance / 100);
1045
+ if (this.comparison === "=") return v1 >= lower && v1 <= upper ? 1 : 0;
1046
+ if (prev === void 0) {
1047
+ return this.comparison === ">" ? v1 > v2 ? 1 : 0 : v1 < v2 ? 1 : 0;
1048
+ }
1049
+ if (this.comparison === ">") {
1050
+ return prev === 1 ? v1 < lower ? 0 : 1 : v1 > upper ? 1 : 0;
737
1051
  }
1052
+ return prev === 1 ? v1 > upper ? 0 : 1 : v1 < lower ? 1 : 0;
738
1053
  }
739
1054
  async _upsertSeries(bars) {
740
1055
  const { id } = await this.resolve();
@@ -744,19 +1059,11 @@ var SignalHandle = class _SignalHandle {
744
1059
  const { id } = await this.resolve();
745
1060
  return this._storage.signals.getSeries(id, range);
746
1061
  }
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
1062
  /**
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`.
1063
+ * Compute the signal's boolean value at `date` without persisting anything,
1064
+ * with optional live-quote `overrides` that are routed through each
1065
+ * indicator's `computeAt`. Returns null if either indicator cannot produce
1066
+ * a value at `date`.
760
1067
  *
761
1068
  * @param prevBool - The signal's boolean value at the bar immediately
762
1069
  * preceding `date`, used for hysteresis when `tolerance > 0`. If not
@@ -764,10 +1071,10 @@ var SignalHandle = class _SignalHandle {
764
1071
  * standalone callers). On the preview path `_evaluate` passes this from
765
1072
  * the in-memory `dateMap` so we never read stale storage.
766
1073
  */
767
- async computeAt(market, date, prevBool) {
1074
+ async computeAt(date, overrides, prevBool) {
768
1075
  const [v1, v2] = await Promise.all([
769
- this.indicator1.computeAt(market, date),
770
- this.indicator2.computeAt(market, date)
1076
+ this.indicator1.computeAt(date, overrides),
1077
+ this.indicator2.computeAt(date, overrides)
771
1078
  ]);
772
1079
  if (v1 === null || v2 === null) return null;
773
1080
  const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
@@ -818,26 +1125,25 @@ var SignalHandle = class _SignalHandle {
818
1125
  }
819
1126
  /**
820
1127
  * 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`.
1128
+ * computed via `computeAt` with the supplied live-quote `overrides`. Does
1129
+ * NOT write to `signals_series`.
823
1130
  *
824
1131
  * @param date - Target trading day whose boolean is computed in-memory.
825
- * @param quoteOverrides - Raw (unleveraged) quotes keyed by ticker symbol.
1132
+ * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
826
1133
  * @param range - Optional filter applied to the returned bars.
827
1134
  */
828
- async previewSeries(date, quoteOverrides, range) {
1135
+ async previewSeries(date, overrides, range) {
829
1136
  const tradingDays = await this._storage.tradingDays.getRange();
830
1137
  if (!tradingDays.includes(date)) {
831
1138
  throw new Error(`previewSeries: ${date} is not a trading day`);
832
1139
  }
833
- const overlay = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
834
1140
  let bars = await this._querySeriesFromDb();
835
1141
  const dateMap = /* @__PURE__ */ new Map();
836
1142
  for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
837
1143
  const limitIdx = tradingDays.indexOf(date);
838
1144
  const prevDate = limitIdx > 0 ? tradingDays[limitIdx - 1] : void 0;
839
1145
  const prevBool = prevDate !== void 0 ? dateMap.get(prevDate) ?? null : null;
840
- const todayBool = await this.computeAt(overlay, date, prevBool);
1146
+ const todayBool = await this.computeAt(date, overrides, prevBool);
841
1147
  if (todayBool !== null) {
842
1148
  const numeric = todayBool ? 1 : 0;
843
1149
  const idx = bars.findIndex((b) => b.date === date);
@@ -885,6 +1191,9 @@ var AllocationHandle = class _AllocationHandle {
885
1191
  handle._resolvedId = id;
886
1192
  return handle;
887
1193
  }
1194
+ toJSON() {
1195
+ return this.holdings.map(([ticker, weight]) => ({ symbol: ticker.symbol, leverage: ticker.leverage, weight })).sort((a, b) => a.symbol.localeCompare(b.symbol) || a.leverage - b.leverage);
1196
+ }
888
1197
  async _doResolve() {
889
1198
  await Promise.all(this.holdings.map(([ticker]) => ticker.resolve()));
890
1199
  const holdingsJson = {};
@@ -1219,7 +1528,9 @@ var SimulationHandle = class {
1219
1528
  _lastLeveragedPrices;
1220
1529
  _currentLeveragedPrices;
1221
1530
  _lastDate;
1222
- constructor(series, trades, startingPortfolio, finalState) {
1531
+ _pushedQuotes;
1532
+ _liveEvaluator;
1533
+ constructor(series, trades, startingPortfolio, finalState, liveEvaluator) {
1223
1534
  this.series = series;
1224
1535
  this.trades = trades;
1225
1536
  this.startingPortfolio = startingPortfolio;
@@ -1238,6 +1549,8 @@ var SimulationHandle = class {
1238
1549
  this._currentLeveragedPrices = /* @__PURE__ */ new Map();
1239
1550
  this._lastDate = "";
1240
1551
  }
1552
+ this._pushedQuotes = {};
1553
+ this._liveEvaluator = liveEvaluator ?? null;
1241
1554
  }
1242
1555
  push(...prices) {
1243
1556
  if (!this._portfolio || !this._currentAllocation) {
@@ -1272,6 +1585,48 @@ var SimulationHandle = class {
1272
1585
  pendingTrades: this._portfolio.trades(this._currentAllocation, priceArray, this._lastDate)
1273
1586
  };
1274
1587
  }
1588
+ /**
1589
+ * One-call live update. Feeds portfolio-relevant ticker prices into `push`
1590
+ * (derived from `quotes` via the running portfolio's holdings), accumulates
1591
+ * every symbol in `quotes` into an internal override map so macro symbols
1592
+ * (e.g. `^VIX`) persist across ticks, then delegates to the simulation's
1593
+ * strategy for rule / signal / indicator evaluation at `date`.
1594
+ *
1595
+ * Without a live evaluator attached, returns just the portfolio snapshot
1596
+ * with allocation/rules/signals empty.
1597
+ *
1598
+ * @param quotes Symbol → raw live price. Portfolio tickers flow through
1599
+ * `push` for leveraged-equity math; non-portfolio symbols are still
1600
+ * layered into the overlay so indicators can see them.
1601
+ * @param options.date Target trading day to evaluate against. Defaults to
1602
+ * the current UTC ISO date; callers with non-UTC semantics or after-hours
1603
+ * rollover should supply their own.
1604
+ */
1605
+ async pushAndPreview(quotes, options = {}) {
1606
+ const priceArgs = [];
1607
+ if (this._portfolio) {
1608
+ const seen = /* @__PURE__ */ new Set();
1609
+ for (const [ticker] of this._portfolio.holdings) {
1610
+ if (ticker.symbol === "CASHX") continue;
1611
+ if (seen.has(ticker.symbol)) continue;
1612
+ const price = quotes[ticker.symbol];
1613
+ if (price !== void 0) {
1614
+ priceArgs.push([ticker, price]);
1615
+ seen.add(ticker.symbol);
1616
+ }
1617
+ }
1618
+ }
1619
+ const snapshot = this.push(...priceArgs);
1620
+ for (const [symbol, price] of Object.entries(quotes)) {
1621
+ this._pushedQuotes[symbol] = price;
1622
+ }
1623
+ if (!this._liveEvaluator) {
1624
+ return { snapshot, allocation: null, activeRuleIndex: -1, rules: [] };
1625
+ }
1626
+ const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1627
+ const strategyState = await this._liveEvaluator.previewLiveState(date, { ...this._pushedQuotes });
1628
+ return { snapshot, ...strategyState };
1629
+ }
1275
1630
  };
1276
1631
 
1277
1632
  // src/handles/strategy.ts
@@ -1344,6 +1699,22 @@ var StrategyHandle = class {
1344
1699
  get rules() {
1345
1700
  return this._rules;
1346
1701
  }
1702
+ marketSymbols() {
1703
+ const set = /* @__PURE__ */ new Set();
1704
+ for (const rule of this._rules) {
1705
+ for (const [ticker] of rule.hold.holdings) {
1706
+ if (ticker.symbol !== "CASHX") set.add(ticker.symbol);
1707
+ }
1708
+ for (const signal of rule.when ?? []) {
1709
+ for (const ind of [signal.indicator1, signal.indicator2]) {
1710
+ if (ind.ticker !== null && ind.ticker.symbol !== "CASHX") set.add(ind.ticker.symbol);
1711
+ if (ind.type === "VIX") set.add("^VIX");
1712
+ if (ind.type === "VIX3M") set.add("^VIX3M");
1713
+ }
1714
+ }
1715
+ }
1716
+ return Array.from(set).sort();
1717
+ }
1347
1718
  async resolve() {
1348
1719
  if (this._resolvedId != null) return { id: this._resolvedId };
1349
1720
  if (!this._resolving) {
@@ -1456,7 +1827,9 @@ var StrategyHandle = class {
1456
1827
  return;
1457
1828
  }
1458
1829
  if (!this._syncing) {
1459
- this._syncing = this._sync(latestClosed).finally(() => {
1830
+ this._syncing = this._sync(latestClosed).catch((err) => {
1831
+ console.warn("[sdk] strategy sync failed, using stored data:", err);
1832
+ }).finally(() => {
1460
1833
  this._syncing = null;
1461
1834
  });
1462
1835
  }
@@ -1466,7 +1839,7 @@ var StrategyHandle = class {
1466
1839
  }
1467
1840
  async _sync(latestClosed) {
1468
1841
  const { id } = await this.resolve();
1469
- const { entries } = await this._evaluate(this._market, latestClosed);
1842
+ const { entries } = await this._evaluate(latestClosed);
1470
1843
  if (entries.length > 0) {
1471
1844
  await this._storage.strategies.writeSeries(id, entries);
1472
1845
  }
@@ -1474,26 +1847,98 @@ var StrategyHandle = class {
1474
1847
  /**
1475
1848
  * Pure evaluate — runs the same pipeline as _sync but returns the computed
1476
1849
  * evaluation instead of persisting. Used by both _sync (post-close write
1477
- * path) and previewAllocation (pre-close read-only path).
1850
+ * path) and the public preview methods (pre-close read-only path).
1851
+ *
1852
+ * When `overrides` is `undefined` we take the write path — syncing signals
1853
+ * through storage as normal. When `overrides` is provided (even an empty
1854
+ * map) we take the read-only preview path: historical signal bars come
1855
+ * straight from storage, today's bar is computed in-memory via
1856
+ * `signal.computeAt(date, overrides, prevBool)`, and nothing is written.
1478
1857
  *
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`.
1858
+ * Incremental path: when a strategy checkpoint exists (`getLatestSeriesDate`
1859
+ * returns non-null), only the window (lastDate, limitDate] is processed.
1860
+ * The current allocation is carried forward from `getLatestAllocationId`.
1861
+ * Bootstrap: when no checkpoint exists, falls back to `_evaluateCold` which
1862
+ * runs the full-history evaluation.
1485
1863
  */
1486
- async _evaluate(market, limitDate) {
1864
+ async _evaluate(limitDate, overrides) {
1865
+ const { id } = await this.resolve();
1866
+ const lastDate = await this._storage.strategies.getLatestSeriesDate(id);
1867
+ const tradingDays = await this._storage.tradingDays.getRange();
1868
+ const limitIdx = tradingDays.indexOf(limitDate);
1869
+ const allocations = [];
1870
+ const allocIndexMap = /* @__PURE__ */ new Map();
1871
+ const rulesInput = this._rules.map((rule) => {
1872
+ let allocIdx = allocIndexMap.get(rule.hold.id);
1873
+ if (allocIdx === void 0) {
1874
+ allocIdx = allocations.length;
1875
+ allocations.push(rule.hold);
1876
+ allocIndexMap.set(rule.hold.id, allocIdx);
1877
+ }
1878
+ return {
1879
+ signalIds: (rule.when ?? []).map((s) => s.id),
1880
+ allocationIndex: allocIdx
1881
+ };
1882
+ });
1883
+ if (lastDate === null) {
1884
+ return this._evaluateCold(limitDate, overrides, rulesInput, allocations, tradingDays);
1885
+ }
1886
+ const lastAllocId = await this._storage.strategies.getLatestAllocationId(id);
1887
+ const incrementalStartIdx = tradingDays.indexOf(lastDate) + 1;
1888
+ const incrementalDays = tradingDays.slice(incrementalStartIdx, limitIdx + 1);
1889
+ const isOverrideRefresh = incrementalDays.length === 0 && overrides !== void 0 && limitDate === lastDate;
1890
+ const newDays = isOverrideRefresh ? [limitDate] : incrementalDays;
1891
+ const startIdx = isOverrideRefresh ? limitIdx : incrementalStartIdx;
1892
+ if (newDays.length === 0) return { allocations, entries: [] };
1487
1893
  const allSignals = /* @__PURE__ */ new Set();
1488
- for (const rule of this._rules) {
1489
- if (rule.when) rule.when.forEach((s) => allSignals.add(s));
1894
+ for (const rule of this._rules) if (rule.when) rule.when.forEach((s) => allSignals.add(s));
1895
+ const signalSeries = /* @__PURE__ */ new Map();
1896
+ await Promise.all(
1897
+ Array.from(allSignals).map(async (signal) => {
1898
+ const bars = overrides === void 0 ? await signal.series({ from: newDays[0], to: limitDate }) : await this._storage.signals.getSeries(signal.id, { from: newDays[0], to: limitDate });
1899
+ const dateMap = /* @__PURE__ */ new Map();
1900
+ for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
1901
+ if (overrides !== void 0) {
1902
+ const prevDateIdx = startIdx - 1 >= 0 ? tradingDays[startIdx - 1] : void 0;
1903
+ const prevBool = prevDateIdx !== void 0 ? await signal.value(prevDateIdx) === 1 : null;
1904
+ const todayValue = await signal.computeAt(limitDate, overrides, prevBool);
1905
+ if (todayValue !== null) dateMap.set(limitDate, todayValue);
1906
+ }
1907
+ signalSeries.set(signal.id, dateMap);
1908
+ })
1909
+ );
1910
+ const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
1911
+ const entries = [];
1912
+ let current = lastAllocId !== null ? allocIndexMap.get(lastAllocId) ?? void 0 : void 0;
1913
+ for (const date of newDays) {
1914
+ if (rebalanceDates.has(date)) {
1915
+ for (const rule of rulesInput) {
1916
+ if (rule.signalIds.length === 0) {
1917
+ current = rule.allocationIndex;
1918
+ break;
1919
+ }
1920
+ const allTrue = rule.signalIds.every((sid) => signalSeries.get(sid)?.get(date) ?? false);
1921
+ if (allTrue) {
1922
+ current = rule.allocationIndex;
1923
+ break;
1924
+ }
1925
+ }
1926
+ }
1927
+ if (current !== void 0) {
1928
+ entries.push({ date, allocationId: allocations[current].id });
1929
+ }
1490
1930
  }
1931
+ return { allocations, entries };
1932
+ }
1933
+ // Renamed body of the old _evaluate — used only for first-ever evaluate (bootstrap).
1934
+ async _evaluateCold(limitDate, overrides, rulesInput, allocations, tradingDays) {
1935
+ const allSignals = /* @__PURE__ */ new Set();
1936
+ for (const rule of this._rules) if (rule.when) rule.when.forEach((s) => allSignals.add(s));
1491
1937
  const signalSeries = /* @__PURE__ */ new Map();
1492
- const tradingDays = await this._storage.tradingDays.getRange();
1493
- if (market === this._market) {
1938
+ if (overrides === void 0) {
1494
1939
  await Promise.all(
1495
1940
  Array.from(allSignals).map(async (signal) => {
1496
- const bars = await signal.withMarket(market).series();
1941
+ const bars = await signal.series();
1497
1942
  const dateMap = /* @__PURE__ */ new Map();
1498
1943
  for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
1499
1944
  signalSeries.set(signal.id, dateMap);
@@ -1508,7 +1953,7 @@ var StrategyHandle = class {
1508
1953
  const dateMap = /* @__PURE__ */ new Map();
1509
1954
  for (const bar of historicalBars) dateMap.set(bar.date, bar.value === 1);
1510
1955
  const prevBool = prevDate !== void 0 ? dateMap.get(prevDate) ?? null : null;
1511
- const todayValue = await signal.computeAt(market, limitDate, prevBool);
1956
+ const todayValue = await signal.computeAt(limitDate, overrides, prevBool);
1512
1957
  if (todayValue !== null) {
1513
1958
  dateMap.set(limitDate, todayValue);
1514
1959
  }
@@ -1517,20 +1962,6 @@ var StrategyHandle = class {
1517
1962
  );
1518
1963
  }
1519
1964
  const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
1520
- const allocations = [];
1521
- const allocIndexMap = /* @__PURE__ */ new Map();
1522
- const rulesInput = this._rules.map((rule) => {
1523
- let allocIdx = allocIndexMap.get(rule.hold.id);
1524
- if (allocIdx === void 0) {
1525
- allocIdx = allocations.length;
1526
- allocations.push(rule.hold);
1527
- allocIndexMap.set(rule.hold.id, allocIdx);
1528
- }
1529
- return {
1530
- signalIds: (rule.when ?? []).map((s) => s.id),
1531
- allocationIndex: allocIdx
1532
- };
1533
- });
1534
1965
  const evalResult = evaluateStrategy(signalSeries, rulesInput, rebalanceDates, tradingDays);
1535
1966
  const entries = Array.from(evalResult.entries()).filter(([date]) => date <= limitDate).map(([date, allocIdx]) => ({ date, allocationId: allocations[allocIdx].id }));
1536
1967
  return { allocations, entries };
@@ -1584,7 +2015,10 @@ var StrategyHandle = class {
1584
2015
  closePrices,
1585
2016
  leveragedPrices
1586
2017
  };
1587
- return new SimulationHandle(result.series, result.trades, options.portfolio, finalState);
2018
+ const liveEvaluator = {
2019
+ previewLiveState: (date, overrides) => this.previewLiveState(date, overrides)
2020
+ };
2021
+ return new SimulationHandle(result.series, result.trades, options.portfolio, finalState, liveEvaluator);
1588
2022
  }
1589
2023
  /**
1590
2024
  * Preview the allocation this strategy would produce for `date` if today
@@ -1592,20 +2026,19 @@ var StrategyHandle = class {
1592
2026
  * signals_series, or indicators_series. Safe to call before market close.
1593
2027
  *
1594
2028
  * @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.
2029
+ * @param overrides - Raw (unleveraged) live prices keyed by market symbol.
2030
+ * Symbols absent from this map fall back to the last stored value
2031
+ * (see `IndicatorHandle._resolveRawBars`).
1598
2032
  * @returns The AllocationHandle for `date`, or null if the strategy has no
1599
2033
  * evaluable entry for that date.
1600
2034
  */
1601
- async previewAllocation(date, quoteOverrides) {
2035
+ async previewAllocation(date, overrides) {
1602
2036
  await this.resolve();
1603
2037
  const tradingDays = await this._storage.tradingDays.getRange();
1604
2038
  if (!tradingDays.includes(date)) {
1605
2039
  throw new Error(`previewAllocation: ${date} is not a trading day`);
1606
2040
  }
1607
- const overlayMarket = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
1608
- const { allocations, entries } = await this._evaluate(overlayMarket, date);
2041
+ const { allocations, entries } = await this._evaluate(date, overrides);
1609
2042
  const target = entries.find((e) => e.date === date);
1610
2043
  if (!target) return null;
1611
2044
  const alloc = allocations.find((a) => a.id === target.allocationId);
@@ -1614,27 +2047,45 @@ var StrategyHandle = class {
1614
2047
  /**
1615
2048
  * Read-only preview of the strategy's allocation series including `date`.
1616
2049
  * Returns stored historical allocations plus an in-memory bar at `date`
1617
- * computed via the same overlay path as `previewAllocation`.
2050
+ * computed via the same overrides-based preview path as `previewAllocation`.
1618
2051
  *
1619
- * @param date - Target trading day to splice in-memory via overlay market.
1620
- * @param quoteOverrides - Raw (unleveraged) quotes keyed by ticker symbol.
2052
+ * @param date - Target trading day to splice in-memory.
2053
+ * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
1621
2054
  * @param range - Optional filter applied to the returned bars.
1622
2055
  */
1623
- async previewSeries(date, quoteOverrides, range) {
2056
+ async previewSeries(date, overrides, range) {
1624
2057
  await this.resolve();
1625
2058
  const tradingDays = await this._storage.tradingDays.getRange();
1626
2059
  if (!tradingDays.includes(date)) {
1627
2060
  throw new Error(`previewSeries: ${date} is not a trading day`);
1628
2061
  }
1629
- const overlayMarket = createQuoteOverlay(this._market, { [date]: quoteOverrides }, { fallbackMissingQuotes: true });
1630
- const { allocations, entries } = await this._evaluate(overlayMarket, date);
2062
+ const { allocations, entries } = await this._evaluate(date, overrides);
1631
2063
  const allocById = /* @__PURE__ */ new Map();
1632
2064
  for (const a of allocations) allocById.set(a.id, a);
1633
- for (const [id, a] of this._allocationMap) if (!allocById.has(id)) allocById.set(id, a);
1634
- let bars = entries.map((e) => ({
2065
+ for (const [id2, a] of this._allocationMap) if (!allocById.has(id2)) allocById.set(id2, a);
2066
+ const { id } = await this.resolve();
2067
+ const lastDate = await this._storage.strategies.getLatestSeriesDate(id);
2068
+ let storedBars = [];
2069
+ if (lastDate !== null && entries.length > 0 && entries[0].date > (tradingDays[0] ?? "")) {
2070
+ const storedEntries = await this._storage.strategies.getSeries(id, { to: lastDate });
2071
+ storedBars = storedEntries.map((e) => ({
2072
+ date: e.date,
2073
+ allocation: allocById.get(e.allocationId) ?? this._allocationMap.get(e.allocationId)
2074
+ }));
2075
+ } else if (lastDate !== null && entries.length === 0) {
2076
+ const storedEntries = await this._storage.strategies.getSeries(id);
2077
+ storedBars = storedEntries.map((e) => ({
2078
+ date: e.date,
2079
+ allocation: allocById.get(e.allocationId) ?? this._allocationMap.get(e.allocationId)
2080
+ }));
2081
+ }
2082
+ const newBars = entries.map((e) => ({
1635
2083
  date: e.date,
1636
2084
  allocation: allocById.get(e.allocationId)
1637
2085
  }));
2086
+ const newDates = new Set(newBars.map((b) => b.date));
2087
+ let bars = [...storedBars.filter((b) => !newDates.has(b.date)), ...newBars];
2088
+ bars.sort((a, b) => a.date.localeCompare(b.date));
1638
2089
  if (range) {
1639
2090
  bars = bars.filter(
1640
2091
  (b) => (range.from === void 0 || b.date >= range.from) && (range.to === void 0 || b.date <= range.to)
@@ -1642,6 +2093,70 @@ var StrategyHandle = class {
1642
2093
  }
1643
2094
  return bars;
1644
2095
  }
2096
+ /**
2097
+ * Full live strategy view at `date` under live-quote `overrides`: the active
2098
+ * allocation, the index of the rule that fired (or fallback), and per-rule
2099
+ * per-signal indicator values + truth. Computed entirely through the
2100
+ * overrides preview path — no writes to any `*_series` tables.
2101
+ *
2102
+ * Threshold indicators have their date suppressed (`null`) since their
2103
+ * synthetic series runs over every trading day in storage including future
2104
+ * dates and would report a far-future date for the last bar.
2105
+ */
2106
+ async previewLiveState(date, overrides) {
2107
+ await this.resolve();
2108
+ const tradingDays = await this._storage.tradingDays.getRange();
2109
+ if (!tradingDays.includes(date)) {
2110
+ throw new Error(`previewLiveState: ${date} is not a trading day`);
2111
+ }
2112
+ const [{ allocations, entries }, rules] = await Promise.all([
2113
+ this._evaluate(date, overrides),
2114
+ Promise.all(
2115
+ this._rules.map(async (rule) => {
2116
+ const signalHandles = rule.when ?? [];
2117
+ const signals = await Promise.all(
2118
+ signalHandles.map(async (sig) => {
2119
+ const [i1Series, i2Series, sigSeries] = await Promise.all([
2120
+ sig.indicator1.previewSeries(date, overrides),
2121
+ sig.indicator2.previewSeries(date, overrides),
2122
+ sig.previewSeries(date, overrides)
2123
+ ]);
2124
+ const last1 = i1Series.at(-1);
2125
+ const last2 = i2Series.at(-1);
2126
+ const lastSig = sigSeries.at(-1);
2127
+ const i1IsThreshold = sig.indicator1.type === "Threshold";
2128
+ const i2IsThreshold = sig.indicator2.type === "Threshold";
2129
+ return {
2130
+ indicator1: {
2131
+ value: last1?.value ?? null,
2132
+ date: i1IsThreshold ? null : last1?.date ?? null
2133
+ },
2134
+ indicator2: {
2135
+ value: last2?.value ?? null,
2136
+ date: i2IsThreshold ? null : last2?.date ?? null
2137
+ },
2138
+ isTrue: lastSig?.value === 1
2139
+ };
2140
+ })
2141
+ );
2142
+ return { signals };
2143
+ })
2144
+ )
2145
+ ]);
2146
+ const target = entries.find((e) => e.date === date);
2147
+ const allocation = target ? allocations.find((a) => a.id === target.allocationId) ?? this._allocationMap.get(target.allocationId) ?? null : null;
2148
+ const fallbackIndex = this._rules.length - 1;
2149
+ let activeRuleIndex = fallbackIndex;
2150
+ if (target) {
2151
+ for (let r = 0; r < this._rules.length; r++) {
2152
+ if (this._rules[r].hold.id === target.allocationId) {
2153
+ activeRuleIndex = r;
2154
+ break;
2155
+ }
2156
+ }
2157
+ }
2158
+ return { allocation, activeRuleIndex, rules };
2159
+ }
1645
2160
  async _fetchPricesForTickers(bars, from, to) {
1646
2161
  const tickerMap = /* @__PURE__ */ new Map();
1647
2162
  for (const bar of bars) {
@@ -1766,6 +2281,21 @@ function createClient(options) {
1766
2281
  strategy: (optionsOrLinkId) => new StrategyHandle(storage, market, optionsOrLinkId)
1767
2282
  };
1768
2283
  }
2284
+
2285
+ // src/handles/allocation-equality.ts
2286
+ function allocationsEqual(a, b) {
2287
+ if (a === null && b === null) return true;
2288
+ if (a === null || b === null) return false;
2289
+ const aj = a.toJSON();
2290
+ const bj = b.toJSON();
2291
+ if (aj.length !== bj.length) return false;
2292
+ for (let i = 0; i < aj.length; i++) {
2293
+ if (aj[i].symbol !== bj[i].symbol) return false;
2294
+ if (aj[i].leverage !== bj[i].leverage) return false;
2295
+ if (Math.abs(aj[i].weight - bj[i].weight) > 1e-9) return false;
2296
+ }
2297
+ return true;
2298
+ }
1769
2299
  export {
1770
2300
  AllocationHandle,
1771
2301
  IndicatorHandle,
@@ -1774,6 +2304,8 @@ export {
1774
2304
  SimulationHandle,
1775
2305
  StrategyHandle,
1776
2306
  TickerHandle,
2307
+ allocationsEqual,
2308
+ computeRebalanceDates,
1777
2309
  createClient
1778
2310
  };
1779
2311
  //# sourceMappingURL=index.js.map