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