@livefolio/sdk 0.3.3 → 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,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);
410
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 [];
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,22 @@ 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 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
+ }
479
655
  let calendarDays;
480
656
  if (this.type === "RSI") {
481
657
  calendarDays = Math.max(this.lookback * 10, 90);
@@ -554,6 +730,18 @@ var IndicatorHandle = class _IndicatorHandle {
554
730
  }
555
731
  return bars;
556
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
+ }
557
745
  /**
558
746
  * Resolve raw (unleveraged) bars for a market symbol from storage. Maps:
559
747
  * - `^VIX` → the VIX indicator's stored series
@@ -786,13 +974,16 @@ var SignalHandle = class _SignalHandle {
786
974
  const { id } = await this.resolve();
787
975
  const latestClosed = await this._getLatestClosedTradingDay();
788
976
  if (this._cachedAsOf === latestClosed) return;
789
- await Promise.all([this.indicator1.series(), this.indicator2.series()]);
790
977
  const latestSeries = await this._getLatestSignalSeriesDate(id);
791
978
  if (latestSeries === latestClosed) {
792
979
  this._cachedSeries = null;
793
980
  this._cachedAsOf = latestClosed;
794
981
  return;
795
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
+ }
796
987
  if (!this._syncing) {
797
988
  this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
798
989
  console.warn("[sdk] signal sync failed, using stored data:", err);
@@ -804,17 +995,61 @@ var SignalHandle = class _SignalHandle {
804
995
  this._cachedSeries = null;
805
996
  this._cachedAsOf = latestClosed;
806
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
+ }
807
1005
  async _sync(fromDate, latestClosed) {
808
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
+ }
809
1025
  const range = fromDate ? { from: fromDate } : void 0;
810
1026
  const [series1, series2] = await Promise.all([this.indicator1.series(range), this.indicator2.series(range)]);
811
1027
  const previousValue = fromDate ? await this._getLastSignalValue(id) ?? void 0 : void 0;
812
- const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
813
1028
  const signalBars = evaluateSignal(series1, series2, this.comparison, this.tolerance, absolute, previousValue);
814
1029
  const bars = signalBars.filter((b) => b.date <= latestClosed);
815
- if (bars.length > 0) {
816
- 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;
817
1048
  }
1049
+ if (this.comparison === ">") {
1050
+ return prev === 1 ? v1 < lower ? 0 : 1 : v1 > upper ? 1 : 0;
1051
+ }
1052
+ return prev === 1 ? v1 > upper ? 0 : 1 : v1 < lower ? 1 : 0;
818
1053
  }
819
1054
  async _upsertSeries(bars) {
820
1055
  const { id } = await this.resolve();
@@ -956,6 +1191,9 @@ var AllocationHandle = class _AllocationHandle {
956
1191
  handle._resolvedId = id;
957
1192
  return handle;
958
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
+ }
959
1197
  async _doResolve() {
960
1198
  await Promise.all(this.holdings.map(([ticker]) => ticker.resolve()));
961
1199
  const holdingsJson = {};
@@ -1461,6 +1699,22 @@ var StrategyHandle = class {
1461
1699
  get rules() {
1462
1700
  return this._rules;
1463
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
+ }
1464
1718
  async resolve() {
1465
1719
  if (this._resolvedId != null) return { id: this._resolvedId };
1466
1720
  if (!this._resolving) {
@@ -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 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: [] };
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