@livefolio/sdk 0.3.3 → 0.3.5

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,6 +317,28 @@ var COMPUTATIONS = {
237
317
  function getComputation(type) {
238
318
  return COMPUTATIONS[type] ?? null;
239
319
  }
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];
341
+ }
240
342
 
241
343
  // src/handles/indicator.ts
242
344
  var FRED_SYMBOL_TO_TYPE = {
@@ -349,6 +451,34 @@ var IndicatorHandle = class _IndicatorHandle {
349
451
  async _sync(fromDate, latestClosed) {
350
452
  const tickerSymbol = this.ticker?.symbol ?? null;
351
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
+ }
352
482
  let bars;
353
483
  switch (info.provider) {
354
484
  case "yahoo":
@@ -375,43 +505,73 @@ var IndicatorHandle = class _IndicatorHandle {
375
505
  if (!computeFn) throw new Error(`No computation found for type "${this.type}"`);
376
506
  bars = computeFn(priceBars, this.lookback);
377
507
  }
378
- if (fromDate) {
379
- bars = bars.filter((b) => b.date > fromDate);
380
- }
508
+ if (fromDate) bars = bars.filter((b) => b.date > fromDate);
381
509
  break;
382
510
  }
383
511
  case "calendar": {
384
512
  const allDays = await this._storage.tradingDays.getRange();
385
513
  const dayBars = allDays.map((date) => ({ date, value: 0 }));
386
514
  bars = computeCalendar(dayBars, this.type);
387
- if (fromDate) {
388
- bars = bars.filter((b) => b.date > fromDate);
389
- }
515
+ if (fromDate) bars = bars.filter((b) => b.date > fromDate);
390
516
  break;
391
517
  }
392
- case "none":
393
- return;
394
518
  }
395
519
  if (info.provider !== "computed") {
396
520
  bars = await this._applyLeverage(bars, fromDate);
397
521
  }
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;
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;
404
539
  }
405
- horizon = tradingDays[idx - this.delay];
406
540
  }
407
- bars = bars.filter((b) => b.date <= horizon);
408
- if (bars.length > 0) {
409
- await this._upsertSeries(bars);
541
+ await this._upsertSeries(bars, metadata);
542
+ }
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
+ );
410
569
  }
570
+ return [];
411
571
  }
412
- async _upsertSeries(bars) {
572
+ async _upsertSeries(bars, metadata) {
413
573
  const { id } = await this.resolve();
414
- await this._storage.indicators.writeSeries(id, bars);
574
+ await this._storage.indicators.writeSeries(id, bars, metadata !== void 0 ? { metadata } : void 0);
415
575
  }
416
576
  async _querySeriesFromDb(range) {
417
577
  const { id } = await this.resolve();
@@ -476,6 +636,32 @@ var IndicatorHandle = class _IndicatorHandle {
476
636
  return computed.find((b) => b.date === date)?.value ?? null;
477
637
  }
478
638
  if (info.provider === "computed") {
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 leverage2 = this.ticker?.leverage ?? 1;
651
+ let nextValue = rawBar;
652
+ if (leverage2 !== 1 && !isRateTickerSymbol(this.ticker?.symbol ?? null)) {
653
+ const prevRaw = await this._resolveRawBarAt(info.symbol, checkpoint.date, void 0);
654
+ const prevLev = checkpoint.metadata.prev;
655
+ if (prevRaw !== null && prevRaw !== 0 && typeof prevLev === "number") {
656
+ const dailyReturn = (rawBar - prevRaw) / prevRaw;
657
+ nextValue = prevLev * (1 + leverage2 * dailyReturn);
658
+ }
659
+ }
660
+ const step = this.type === "Return" && info.rateSeries ? returnNext(checkpoint.metadata, nextValue, this.lookback, "abs") : nextFn(checkpoint.metadata, nextValue, this.lookback);
661
+ return step.value;
662
+ }
663
+ }
664
+ }
479
665
  let calendarDays;
480
666
  if (this.type === "RSI") {
481
667
  calendarDays = Math.max(this.lookback * 10, 90);
@@ -554,6 +740,18 @@ var IndicatorHandle = class _IndicatorHandle {
554
740
  }
555
741
  return bars;
556
742
  }
743
+ /**
744
+ * Resolve the single raw (unleveraged) value for `symbol` at `date`.
745
+ * Returns the override directly when present; otherwise delegates to
746
+ * `_resolveRawBars` with a one-day window and picks the matching bar.
747
+ */
748
+ async _resolveRawBarAt(symbol, date, overrides) {
749
+ const override = overrides?.[symbol];
750
+ if (override !== void 0) return override;
751
+ const bars = await this._resolveRawBars(symbol, date, date, overrides);
752
+ const hit = bars.find((b) => b.date === date);
753
+ return hit?.value ?? null;
754
+ }
557
755
  /**
558
756
  * Resolve raw (unleveraged) bars for a market symbol from storage. Maps:
559
757
  * - `^VIX` → the VIX indicator's stored series
@@ -786,13 +984,16 @@ var SignalHandle = class _SignalHandle {
786
984
  const { id } = await this.resolve();
787
985
  const latestClosed = await this._getLatestClosedTradingDay();
788
986
  if (this._cachedAsOf === latestClosed) return;
789
- await Promise.all([this.indicator1.series(), this.indicator2.series()]);
790
987
  const latestSeries = await this._getLatestSignalSeriesDate(id);
791
988
  if (latestSeries === latestClosed) {
792
989
  this._cachedSeries = null;
793
990
  this._cachedAsOf = latestClosed;
794
991
  return;
795
992
  }
993
+ const isFastPath = await this._isSingleBarFastPath(latestSeries ?? void 0, latestClosed);
994
+ if (!isFastPath) {
995
+ await Promise.all([this.indicator1.series(), this.indicator2.series()]);
996
+ }
796
997
  if (!this._syncing) {
797
998
  this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
798
999
  console.warn("[sdk] signal sync failed, using stored data:", err);
@@ -804,17 +1005,61 @@ var SignalHandle = class _SignalHandle {
804
1005
  this._cachedSeries = null;
805
1006
  this._cachedAsOf = latestClosed;
806
1007
  }
1008
+ async _isSingleBarFastPath(fromDate, latestClosed) {
1009
+ if (!fromDate) return false;
1010
+ const tradingDays = await this._storage.tradingDays.getRange();
1011
+ const fromIdx = tradingDays.indexOf(fromDate);
1012
+ const closedIdx = tradingDays.indexOf(latestClosed);
1013
+ return fromIdx >= 0 && closedIdx === fromIdx + 1;
1014
+ }
807
1015
  async _sync(fromDate, latestClosed) {
808
1016
  const { id } = await this.resolve();
1017
+ const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
1018
+ if (fromDate) {
1019
+ const tradingDays = await this._storage.tradingDays.getRange();
1020
+ const fromIdx = tradingDays.indexOf(fromDate);
1021
+ const closedIdx = tradingDays.indexOf(latestClosed);
1022
+ if (fromIdx >= 0 && closedIdx === fromIdx + 1) {
1023
+ const newDate = tradingDays[closedIdx];
1024
+ const [v1, v2] = await Promise.all([
1025
+ this.indicator1.computeAt(newDate, void 0),
1026
+ this.indicator2.computeAt(newDate, void 0)
1027
+ ]);
1028
+ if (v1 === null || v2 === null) return;
1029
+ const prev = await this._getLastSignalValue(id) ?? void 0;
1030
+ const value = this._evaluateOneBar(v1, v2, absolute, prev);
1031
+ await this._upsertSeries([{ date: newDate, value }]);
1032
+ return;
1033
+ }
1034
+ }
809
1035
  const range = fromDate ? { from: fromDate } : void 0;
810
1036
  const [series1, series2] = await Promise.all([this.indicator1.series(range), this.indicator2.series(range)]);
811
1037
  const previousValue = fromDate ? await this._getLastSignalValue(id) ?? void 0 : void 0;
812
- const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
813
1038
  const signalBars = evaluateSignal(series1, series2, this.comparison, this.tolerance, absolute, previousValue);
814
1039
  const bars = signalBars.filter((b) => b.date <= latestClosed);
815
- if (bars.length > 0) {
816
- await this._upsertSeries(bars);
1040
+ if (bars.length > 0) await this._upsertSeries(bars);
1041
+ }
1042
+ _evaluateOneBar(v1, v2, absolute, prev) {
1043
+ if (this.tolerance === 0) {
1044
+ switch (this.comparison) {
1045
+ case ">":
1046
+ return v1 > v2 ? 1 : 0;
1047
+ case "<":
1048
+ return v1 < v2 ? 1 : 0;
1049
+ case "=":
1050
+ return v1 === v2 ? 1 : 0;
1051
+ }
1052
+ }
1053
+ const upper = absolute ? v2 + this.tolerance : v2 * (1 + this.tolerance / 100);
1054
+ const lower = absolute ? v2 - this.tolerance : v2 * (1 - this.tolerance / 100);
1055
+ if (this.comparison === "=") return v1 >= lower && v1 <= upper ? 1 : 0;
1056
+ if (prev === void 0) {
1057
+ return this.comparison === ">" ? v1 > v2 ? 1 : 0 : v1 < v2 ? 1 : 0;
817
1058
  }
1059
+ if (this.comparison === ">") {
1060
+ return prev === 1 ? v1 < lower ? 0 : 1 : v1 > upper ? 1 : 0;
1061
+ }
1062
+ return prev === 1 ? v1 > upper ? 0 : 1 : v1 < lower ? 1 : 0;
818
1063
  }
819
1064
  async _upsertSeries(bars) {
820
1065
  const { id } = await this.resolve();
@@ -956,6 +1201,9 @@ var AllocationHandle = class _AllocationHandle {
956
1201
  handle._resolvedId = id;
957
1202
  return handle;
958
1203
  }
1204
+ toJSON() {
1205
+ 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);
1206
+ }
959
1207
  async _doResolve() {
960
1208
  await Promise.all(this.holdings.map(([ticker]) => ticker.resolve()));
961
1209
  const holdingsJson = {};
@@ -1461,6 +1709,22 @@ var StrategyHandle = class {
1461
1709
  get rules() {
1462
1710
  return this._rules;
1463
1711
  }
1712
+ marketSymbols() {
1713
+ const set = /* @__PURE__ */ new Set();
1714
+ for (const rule of this._rules) {
1715
+ for (const [ticker] of rule.hold.holdings) {
1716
+ if (ticker.symbol !== "CASHX") set.add(ticker.symbol);
1717
+ }
1718
+ for (const signal of rule.when ?? []) {
1719
+ for (const ind of [signal.indicator1, signal.indicator2]) {
1720
+ if (ind.ticker !== null && ind.ticker.symbol !== "CASHX") set.add(ind.ticker.symbol);
1721
+ if (ind.type === "VIX") set.add("^VIX");
1722
+ if (ind.type === "VIX3M") set.add("^VIX3M");
1723
+ }
1724
+ }
1725
+ }
1726
+ return Array.from(set).sort();
1727
+ }
1464
1728
  async resolve() {
1465
1729
  if (this._resolvedId != null) return { id: this._resolvedId };
1466
1730
  if (!this._resolving) {
@@ -1558,20 +1822,10 @@ var StrategyHandle = class {
1558
1822
  if (!date) throw new Error("No closed trading days found");
1559
1823
  return date;
1560
1824
  }
1561
- async _getLatestStrategySeriesDate() {
1562
- const { id } = await this.resolve();
1563
- return this._storage.strategies.getLatestSeriesDate(id);
1564
- }
1565
1825
  async _ensureFresh() {
1566
1826
  await this.resolve();
1567
1827
  const latestClosed = await this._getLatestClosedTradingDay();
1568
1828
  if (this._cachedAsOf === latestClosed) return;
1569
- const latestSeries = await this._getLatestStrategySeriesDate();
1570
- if (latestSeries === latestClosed) {
1571
- this._cache = null;
1572
- this._cachedAsOf = latestClosed;
1573
- return;
1574
- }
1575
1829
  if (!this._syncing) {
1576
1830
  this._syncing = this._sync(latestClosed).catch((err) => {
1577
1831
  console.warn("[sdk] strategy sync failed, using stored data:", err);
@@ -1600,14 +1854,87 @@ var StrategyHandle = class {
1600
1854
  * map) we take the read-only preview path: historical signal bars come
1601
1855
  * straight from storage, today's bar is computed in-memory via
1602
1856
  * `signal.computeAt(date, overrides, prevBool)`, and nothing is written.
1857
+ *
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.
1603
1863
  */
1604
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 isLatestRefresh = incrementalDays.length === 0 && limitDate === lastDate;
1890
+ const newDays = isLatestRefresh ? [limitDate] : incrementalDays;
1891
+ const startIdx = isLatestRefresh ? limitIdx : incrementalStartIdx;
1892
+ if (newDays.length === 0) return { allocations, entries: [] };
1605
1893
  const allSignals = /* @__PURE__ */ new Set();
1606
- for (const rule of this._rules) {
1607
- 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
+ }
1608
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));
1609
1937
  const signalSeries = /* @__PURE__ */ new Map();
1610
- const tradingDays = await this._storage.tradingDays.getRange();
1611
1938
  if (overrides === void 0) {
1612
1939
  await Promise.all(
1613
1940
  Array.from(allSignals).map(async (signal) => {
@@ -1635,20 +1962,6 @@ var StrategyHandle = class {
1635
1962
  );
1636
1963
  }
1637
1964
  const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
1638
- const allocations = [];
1639
- const allocIndexMap = /* @__PURE__ */ new Map();
1640
- const rulesInput = this._rules.map((rule) => {
1641
- let allocIdx = allocIndexMap.get(rule.hold.id);
1642
- if (allocIdx === void 0) {
1643
- allocIdx = allocations.length;
1644
- allocations.push(rule.hold);
1645
- allocIndexMap.set(rule.hold.id, allocIdx);
1646
- }
1647
- return {
1648
- signalIds: (rule.when ?? []).map((s) => s.id),
1649
- allocationIndex: allocIdx
1650
- };
1651
- });
1652
1965
  const evalResult = evaluateStrategy(signalSeries, rulesInput, rebalanceDates, tradingDays);
1653
1966
  const entries = Array.from(evalResult.entries()).filter(([date]) => date <= limitDate).map(([date, allocIdx]) => ({ date, allocationId: allocations[allocIdx].id }));
1654
1967
  return { allocations, entries };
@@ -1749,11 +2062,30 @@ var StrategyHandle = class {
1749
2062
  const { allocations, entries } = await this._evaluate(date, overrides);
1750
2063
  const allocById = /* @__PURE__ */ new Map();
1751
2064
  for (const a of allocations) allocById.set(a.id, a);
1752
- for (const [id, a] of this._allocationMap) if (!allocById.has(id)) allocById.set(id, a);
1753
- 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) => ({
1754
2083
  date: e.date,
1755
2084
  allocation: allocById.get(e.allocationId)
1756
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));
1757
2089
  if (range) {
1758
2090
  bars = bars.filter(
1759
2091
  (b) => (range.from === void 0 || b.date >= range.from) && (range.to === void 0 || b.date <= range.to)
@@ -1949,6 +2281,21 @@ function createClient(options) {
1949
2281
  strategy: (optionsOrLinkId) => new StrategyHandle(storage, market, optionsOrLinkId)
1950
2282
  };
1951
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
+ }
1952
2299
  export {
1953
2300
  AllocationHandle,
1954
2301
  IndicatorHandle,
@@ -1957,6 +2304,8 @@ export {
1957
2304
  SimulationHandle,
1958
2305
  StrategyHandle,
1959
2306
  TickerHandle,
2307
+ allocationsEqual,
2308
+ computeRebalanceDates,
1960
2309
  createClient
1961
2310
  };
1962
2311
  //# sourceMappingURL=index.js.map