@livefolio/sdk 0.3.1 → 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 +206 -4
- package/dist/index.js +560 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -239,6 +239,23 @@ function getComputation(type) {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
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
|
+
};
|
|
254
|
+
function _subtractCalendarDays(date, days) {
|
|
255
|
+
const d = new Date(date);
|
|
256
|
+
d.setUTCDate(d.getUTCDate() - days);
|
|
257
|
+
return d.toISOString().slice(0, 10);
|
|
258
|
+
}
|
|
242
259
|
var IndicatorHandle = class _IndicatorHandle {
|
|
243
260
|
type;
|
|
244
261
|
ticker;
|
|
@@ -304,14 +321,24 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
304
321
|
const { id } = await this.resolve();
|
|
305
322
|
const latestClosed = await this._getLatestClosedTradingDay();
|
|
306
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
|
+
}
|
|
307
332
|
const latestSeries = await this._getLatestSeriesDate(id);
|
|
308
|
-
if (latestSeries ===
|
|
333
|
+
if (latestSeries === horizon) {
|
|
309
334
|
this._cachedSeries = null;
|
|
310
335
|
this._cachedAsOf = latestClosed;
|
|
311
336
|
return;
|
|
312
337
|
}
|
|
313
338
|
if (!this._syncing) {
|
|
314
|
-
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(() => {
|
|
315
342
|
this._syncing = null;
|
|
316
343
|
});
|
|
317
344
|
}
|
|
@@ -365,25 +392,19 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
365
392
|
case "none":
|
|
366
393
|
return;
|
|
367
394
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
const leveraged = [{ date: bars[0].date, value: anchor }];
|
|
379
|
-
for (let i = 1; i < bars.length; i++) {
|
|
380
|
-
const dailyReturn = (bars[i].value - bars[i - 1].value) / bars[i - 1].value;
|
|
381
|
-
const prev = leveraged[i - 1].value;
|
|
382
|
-
leveraged.push({ date: bars[i].date, value: prev * (1 + leverage * dailyReturn) });
|
|
395
|
+
if (info.provider !== "computed") {
|
|
396
|
+
bars = await this._applyLeverage(bars, fromDate);
|
|
397
|
+
}
|
|
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;
|
|
383
404
|
}
|
|
384
|
-
|
|
405
|
+
horizon = tradingDays[idx - this.delay];
|
|
385
406
|
}
|
|
386
|
-
bars = bars.filter((b) => b.date <=
|
|
407
|
+
bars = bars.filter((b) => b.date <= horizon);
|
|
387
408
|
if (bars.length > 0) {
|
|
388
409
|
await this._upsertSeries(bars);
|
|
389
410
|
}
|
|
@@ -396,6 +417,175 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
396
417
|
const { id } = await this.resolve();
|
|
397
418
|
return this._storage.indicators.getSeries(id, range);
|
|
398
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Apply leverage compounding to a raw bar series, anchored to a stored
|
|
422
|
+
* leveraged value. Used by both `_sync` and `computeAt` so they stay
|
|
423
|
+
* consistent.
|
|
424
|
+
*
|
|
425
|
+
* `anchorDate` is the date of the last *already-stored* leveraged bar
|
|
426
|
+
* (i.e., the bar just before `rawBars[0]`). The stored leveraged value
|
|
427
|
+
* at that date becomes `leveraged[0]`; raw returns are then compounded
|
|
428
|
+
* forward for each subsequent bar.
|
|
429
|
+
*
|
|
430
|
+
* If no stored anchor exists (first-ever sync), falls back to rawBars[0]
|
|
431
|
+
* as the starting raw value — identical to `_sync`'s behaviour.
|
|
432
|
+
*/
|
|
433
|
+
async _applyLeverage(rawBars, anchorDate) {
|
|
434
|
+
const leverage = this.ticker?.leverage ?? 1;
|
|
435
|
+
if (leverage === 1 || rawBars.length === 0) return rawBars;
|
|
436
|
+
if (isRateTickerSymbol(this.ticker?.symbol ?? null)) return rawBars;
|
|
437
|
+
let anchor;
|
|
438
|
+
if (anchorDate) {
|
|
439
|
+
const lastStored = await this._storage.indicators.getValue(this._resolvedId, anchorDate);
|
|
440
|
+
anchor = lastStored ?? rawBars[0].value;
|
|
441
|
+
} else {
|
|
442
|
+
anchor = rawBars[0].value;
|
|
443
|
+
}
|
|
444
|
+
const leveraged = [{ date: rawBars[0].date, value: anchor }];
|
|
445
|
+
for (let i = 1; i < rawBars.length; i++) {
|
|
446
|
+
const dailyReturn = (rawBars[i].value - rawBars[i - 1].value) / rawBars[i - 1].value;
|
|
447
|
+
const prev = leveraged[i - 1].value;
|
|
448
|
+
leveraged.push({ date: rawBars[i].date, value: prev * (1 + leverage * dailyReturn) });
|
|
449
|
+
}
|
|
450
|
+
return leveraged;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
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`.
|
|
461
|
+
*
|
|
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.
|
|
466
|
+
*/
|
|
467
|
+
async computeAt(date, overrides) {
|
|
468
|
+
if (this.type === "Threshold") return this.threshold;
|
|
469
|
+
const tickerSymbol = this.ticker?.symbol ?? null;
|
|
470
|
+
const info = getProviderInfo(this.type, tickerSymbol);
|
|
471
|
+
if (info.provider === "none") return null;
|
|
472
|
+
if (info.provider === "calendar") {
|
|
473
|
+
const allDays = await this._storage.tradingDays.getRange();
|
|
474
|
+
const dayBars = allDays.map((d) => ({ date: d, value: 0 }));
|
|
475
|
+
const computed = computeCalendar(dayBars, this.type);
|
|
476
|
+
return computed.find((b) => b.date === date)?.value ?? null;
|
|
477
|
+
}
|
|
478
|
+
if (info.provider === "computed") {
|
|
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
|
+
const anchorDate = rawBars2.length > 0 ? rawBars2[0].date : void 0;
|
|
490
|
+
const priceBars = await this._applyLeverage(rawBars2, anchorDate);
|
|
491
|
+
const computeFn = getComputation(this.type);
|
|
492
|
+
if (!computeFn) throw new Error(`No computation found for type "${this.type}"`);
|
|
493
|
+
const computed = computeFn(priceBars, this.lookback);
|
|
494
|
+
return computed.find((b) => b.date === date)?.value ?? null;
|
|
495
|
+
}
|
|
496
|
+
const symbol = info.provider === "yahoo" ? info.symbol : info.seriesId;
|
|
497
|
+
const from = _subtractCalendarDays(date, 15);
|
|
498
|
+
const rawBars = await this._resolveRawBars(symbol, from, date, overrides);
|
|
499
|
+
const leverage = this.ticker?.leverage ?? 1;
|
|
500
|
+
if (leverage === 1) {
|
|
501
|
+
return rawBars.find((b) => b.date === date)?.value ?? null;
|
|
502
|
+
}
|
|
503
|
+
const dateIdx = rawBars.findIndex((b) => b.date === date);
|
|
504
|
+
if (dateIdx < 0) return null;
|
|
505
|
+
const prevBar = rawBars[dateIdx - 1];
|
|
506
|
+
if (!prevBar) {
|
|
507
|
+
return rawBars[dateIdx].value;
|
|
508
|
+
}
|
|
509
|
+
const storedPrev = await this._storage.indicators.getValue(this._resolvedId, prevBar.date);
|
|
510
|
+
const leveragedPrev = storedPrev ?? prevBar.value;
|
|
511
|
+
const rawReturn = (rawBars[dateIdx].value - prevBar.value) / prevBar.value;
|
|
512
|
+
return leveragedPrev * (1 + leverage * rawReturn);
|
|
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
|
+
}
|
|
399
589
|
// ── Public data access ─────────────────────────────────────────────
|
|
400
590
|
async series(range) {
|
|
401
591
|
if (this.type === "Threshold") {
|
|
@@ -417,6 +607,47 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
417
607
|
const { id } = await this.resolve();
|
|
418
608
|
return this._storage.indicators.getValue(id, date);
|
|
419
609
|
}
|
|
610
|
+
/**
|
|
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
|
|
613
|
+
* NOT write to `indicators_series`. Safe to call before market close.
|
|
614
|
+
*
|
|
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`).
|
|
619
|
+
* @param range - Optional filter applied to the returned bars.
|
|
620
|
+
* @returns Stored historical bars plus (or with) today's in-memory value.
|
|
621
|
+
*/
|
|
622
|
+
async previewSeries(date, overrides, range) {
|
|
623
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
624
|
+
if (!tradingDays.includes(date)) {
|
|
625
|
+
throw new Error(`previewSeries: ${date} is not a trading day`);
|
|
626
|
+
}
|
|
627
|
+
let bars;
|
|
628
|
+
if (this.type === "Threshold") {
|
|
629
|
+
bars = await this._syntheticThresholdSeries();
|
|
630
|
+
} else {
|
|
631
|
+
bars = await this._querySeriesFromDb();
|
|
632
|
+
}
|
|
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
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (range) {
|
|
645
|
+
bars = bars.filter(
|
|
646
|
+
(b) => (range.from === void 0 || b.date >= range.from) && (range.to === void 0 || b.date <= range.to)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return bars;
|
|
650
|
+
}
|
|
420
651
|
};
|
|
421
652
|
|
|
422
653
|
// src/computations/signal.ts
|
|
@@ -497,15 +728,17 @@ var SignalHandle = class _SignalHandle {
|
|
|
497
728
|
comparison;
|
|
498
729
|
tolerance;
|
|
499
730
|
_storage;
|
|
500
|
-
_market;
|
|
501
731
|
_resolvedId = null;
|
|
502
732
|
_resolving = null;
|
|
503
733
|
_cachedSeries = null;
|
|
504
734
|
_cachedAsOf = null;
|
|
505
735
|
_syncing = null;
|
|
506
|
-
|
|
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) {
|
|
507
741
|
this._storage = storage;
|
|
508
|
-
this._market = market;
|
|
509
742
|
this.indicator1 = identity.indicator1;
|
|
510
743
|
this.indicator2 = identity.indicator2;
|
|
511
744
|
this.comparison = identity.comparison;
|
|
@@ -561,7 +794,9 @@ var SignalHandle = class _SignalHandle {
|
|
|
561
794
|
return;
|
|
562
795
|
}
|
|
563
796
|
if (!this._syncing) {
|
|
564
|
-
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(() => {
|
|
565
800
|
this._syncing = null;
|
|
566
801
|
});
|
|
567
802
|
}
|
|
@@ -589,6 +824,53 @@ var SignalHandle = class _SignalHandle {
|
|
|
589
824
|
const { id } = await this.resolve();
|
|
590
825
|
return this._storage.signals.getSeries(id, range);
|
|
591
826
|
}
|
|
827
|
+
/**
|
|
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`.
|
|
832
|
+
*
|
|
833
|
+
* @param prevBool - The signal's boolean value at the bar immediately
|
|
834
|
+
* preceding `date`, used for hysteresis when `tolerance > 0`. If not
|
|
835
|
+
* provided, falls back to `storage.signals.getLastValue` (suitable for
|
|
836
|
+
* standalone callers). On the preview path `_evaluate` passes this from
|
|
837
|
+
* the in-memory `dateMap` so we never read stale storage.
|
|
838
|
+
*/
|
|
839
|
+
async computeAt(date, overrides, prevBool) {
|
|
840
|
+
const [v1, v2] = await Promise.all([
|
|
841
|
+
this.indicator1.computeAt(date, overrides),
|
|
842
|
+
this.indicator2.computeAt(date, overrides)
|
|
843
|
+
]);
|
|
844
|
+
if (v1 === null || v2 === null) return null;
|
|
845
|
+
const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
|
|
846
|
+
if (this.tolerance === 0) {
|
|
847
|
+
switch (this.comparison) {
|
|
848
|
+
case ">":
|
|
849
|
+
return v1 > v2;
|
|
850
|
+
case "<":
|
|
851
|
+
return v1 < v2;
|
|
852
|
+
case "=":
|
|
853
|
+
return v1 === v2;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const tolerance = this.tolerance;
|
|
857
|
+
const upper = absolute ? v2 + tolerance : v2 * (1 + tolerance / 100);
|
|
858
|
+
const lower = absolute ? v2 - tolerance : v2 * (1 - tolerance / 100);
|
|
859
|
+
if (this.comparison === "=") {
|
|
860
|
+
return v1 >= lower && v1 <= upper;
|
|
861
|
+
}
|
|
862
|
+
let resolvedPrevBool;
|
|
863
|
+
if (prevBool !== void 0 && prevBool !== null) {
|
|
864
|
+
resolvedPrevBool = prevBool;
|
|
865
|
+
} else {
|
|
866
|
+
const prev = await this._storage.signals.getLastValue(this.id);
|
|
867
|
+
resolvedPrevBool = prev === 1;
|
|
868
|
+
}
|
|
869
|
+
if (this.comparison === ">") {
|
|
870
|
+
return resolvedPrevBool ? v1 >= lower : v1 > upper;
|
|
871
|
+
}
|
|
872
|
+
return resolvedPrevBool ? v1 <= upper : v1 < lower;
|
|
873
|
+
}
|
|
592
874
|
// ── Public data access ─────────────────────────────────────────────
|
|
593
875
|
async series(range) {
|
|
594
876
|
await this._ensureFresh();
|
|
@@ -606,6 +888,43 @@ var SignalHandle = class _SignalHandle {
|
|
|
606
888
|
const { id } = await this.resolve();
|
|
607
889
|
return this._storage.signals.getLastValue(id);
|
|
608
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* Read-only preview of the signal series with an in-memory bar at `date`
|
|
893
|
+
* computed via `computeAt` with the supplied live-quote `overrides`. Does
|
|
894
|
+
* NOT write to `signals_series`.
|
|
895
|
+
*
|
|
896
|
+
* @param date - Target trading day whose boolean is computed in-memory.
|
|
897
|
+
* @param overrides - Raw (unleveraged) quotes keyed by market symbol.
|
|
898
|
+
* @param range - Optional filter applied to the returned bars.
|
|
899
|
+
*/
|
|
900
|
+
async previewSeries(date, overrides, range) {
|
|
901
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
902
|
+
if (!tradingDays.includes(date)) {
|
|
903
|
+
throw new Error(`previewSeries: ${date} is not a trading day`);
|
|
904
|
+
}
|
|
905
|
+
let bars = await this._querySeriesFromDb();
|
|
906
|
+
const dateMap = /* @__PURE__ */ new Map();
|
|
907
|
+
for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
|
|
908
|
+
const limitIdx = tradingDays.indexOf(date);
|
|
909
|
+
const prevDate = limitIdx > 0 ? tradingDays[limitIdx - 1] : void 0;
|
|
910
|
+
const prevBool = prevDate !== void 0 ? dateMap.get(prevDate) ?? null : null;
|
|
911
|
+
const todayBool = await this.computeAt(date, overrides, prevBool);
|
|
912
|
+
if (todayBool !== null) {
|
|
913
|
+
const numeric = todayBool ? 1 : 0;
|
|
914
|
+
const idx = bars.findIndex((b) => b.date === date);
|
|
915
|
+
if (idx >= 0) {
|
|
916
|
+
bars[idx] = { date, value: numeric };
|
|
917
|
+
} else {
|
|
918
|
+
bars = [...bars, { date, value: numeric }].sort((a, b) => a.date.localeCompare(b.date));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (range) {
|
|
922
|
+
bars = bars.filter(
|
|
923
|
+
(b) => (range.from === void 0 || b.date >= range.from) && (range.to === void 0 || b.date <= range.to)
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
return bars;
|
|
927
|
+
}
|
|
609
928
|
};
|
|
610
929
|
|
|
611
930
|
// src/handles/allocation.ts
|
|
@@ -971,7 +1290,9 @@ var SimulationHandle = class {
|
|
|
971
1290
|
_lastLeveragedPrices;
|
|
972
1291
|
_currentLeveragedPrices;
|
|
973
1292
|
_lastDate;
|
|
974
|
-
|
|
1293
|
+
_pushedQuotes;
|
|
1294
|
+
_liveEvaluator;
|
|
1295
|
+
constructor(series, trades, startingPortfolio, finalState, liveEvaluator) {
|
|
975
1296
|
this.series = series;
|
|
976
1297
|
this.trades = trades;
|
|
977
1298
|
this.startingPortfolio = startingPortfolio;
|
|
@@ -990,6 +1311,8 @@ var SimulationHandle = class {
|
|
|
990
1311
|
this._currentLeveragedPrices = /* @__PURE__ */ new Map();
|
|
991
1312
|
this._lastDate = "";
|
|
992
1313
|
}
|
|
1314
|
+
this._pushedQuotes = {};
|
|
1315
|
+
this._liveEvaluator = liveEvaluator ?? null;
|
|
993
1316
|
}
|
|
994
1317
|
push(...prices) {
|
|
995
1318
|
if (!this._portfolio || !this._currentAllocation) {
|
|
@@ -1024,6 +1347,48 @@ var SimulationHandle = class {
|
|
|
1024
1347
|
pendingTrades: this._portfolio.trades(this._currentAllocation, priceArray, this._lastDate)
|
|
1025
1348
|
};
|
|
1026
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
|
+
}
|
|
1027
1392
|
};
|
|
1028
1393
|
|
|
1029
1394
|
// src/handles/strategy.ts
|
|
@@ -1208,7 +1573,9 @@ var StrategyHandle = class {
|
|
|
1208
1573
|
return;
|
|
1209
1574
|
}
|
|
1210
1575
|
if (!this._syncing) {
|
|
1211
|
-
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(() => {
|
|
1212
1579
|
this._syncing = null;
|
|
1213
1580
|
});
|
|
1214
1581
|
}
|
|
@@ -1218,20 +1585,55 @@ var StrategyHandle = class {
|
|
|
1218
1585
|
}
|
|
1219
1586
|
async _sync(latestClosed) {
|
|
1220
1587
|
const { id } = await this.resolve();
|
|
1221
|
-
const
|
|
1588
|
+
const { entries } = await this._evaluate(latestClosed);
|
|
1589
|
+
if (entries.length > 0) {
|
|
1590
|
+
await this._storage.strategies.writeSeries(id, entries);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Pure evaluate — runs the same pipeline as _sync but returns the computed
|
|
1595
|
+
* evaluation instead of persisting. Used by both _sync (post-close write
|
|
1596
|
+
* path) and the public preview methods (pre-close read-only path).
|
|
1597
|
+
*
|
|
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.
|
|
1603
|
+
*/
|
|
1604
|
+
async _evaluate(limitDate, overrides) {
|
|
1222
1605
|
const allSignals = /* @__PURE__ */ new Set();
|
|
1223
1606
|
for (const rule of this._rules) {
|
|
1224
1607
|
if (rule.when) rule.when.forEach((s) => allSignals.add(s));
|
|
1225
1608
|
}
|
|
1226
|
-
|
|
1227
|
-
Array.from(allSignals).map(async (signal) => {
|
|
1228
|
-
const bars = await signal.series();
|
|
1229
|
-
const dateMap = /* @__PURE__ */ new Map();
|
|
1230
|
-
for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
|
|
1231
|
-
signalSeries.set(signal.id, dateMap);
|
|
1232
|
-
})
|
|
1233
|
-
);
|
|
1609
|
+
const signalSeries = /* @__PURE__ */ new Map();
|
|
1234
1610
|
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1611
|
+
if (overrides === void 0) {
|
|
1612
|
+
await Promise.all(
|
|
1613
|
+
Array.from(allSignals).map(async (signal) => {
|
|
1614
|
+
const bars = await signal.series();
|
|
1615
|
+
const dateMap = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
|
|
1617
|
+
signalSeries.set(signal.id, dateMap);
|
|
1618
|
+
})
|
|
1619
|
+
);
|
|
1620
|
+
} else {
|
|
1621
|
+
const limitIdx = tradingDays.indexOf(limitDate);
|
|
1622
|
+
const prevDate = limitIdx > 0 ? tradingDays[limitIdx - 1] : void 0;
|
|
1623
|
+
await Promise.all(
|
|
1624
|
+
Array.from(allSignals).map(async (signal) => {
|
|
1625
|
+
const historicalBars = await this._storage.signals.getSeries(signal.id);
|
|
1626
|
+
const dateMap = /* @__PURE__ */ new Map();
|
|
1627
|
+
for (const bar of historicalBars) dateMap.set(bar.date, bar.value === 1);
|
|
1628
|
+
const prevBool = prevDate !== void 0 ? dateMap.get(prevDate) ?? null : null;
|
|
1629
|
+
const todayValue = await signal.computeAt(limitDate, overrides, prevBool);
|
|
1630
|
+
if (todayValue !== null) {
|
|
1631
|
+
dateMap.set(limitDate, todayValue);
|
|
1632
|
+
}
|
|
1633
|
+
signalSeries.set(signal.id, dateMap);
|
|
1634
|
+
})
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1235
1637
|
const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
|
|
1236
1638
|
const allocations = [];
|
|
1237
1639
|
const allocIndexMap = /* @__PURE__ */ new Map();
|
|
@@ -1248,13 +1650,8 @@ var StrategyHandle = class {
|
|
|
1248
1650
|
};
|
|
1249
1651
|
});
|
|
1250
1652
|
const evalResult = evaluateStrategy(signalSeries, rulesInput, rebalanceDates, tradingDays);
|
|
1251
|
-
const entries = Array.from(evalResult.entries()).filter(([date]) => date <=
|
|
1252
|
-
|
|
1253
|
-
allocationId: allocations[allocIdx].id
|
|
1254
|
-
}));
|
|
1255
|
-
if (entries.length > 0) {
|
|
1256
|
-
await this._storage.strategies.writeSeries(id, entries);
|
|
1257
|
-
}
|
|
1653
|
+
const entries = Array.from(evalResult.entries()).filter(([date]) => date <= limitDate).map(([date, allocIdx]) => ({ date, allocationId: allocations[allocIdx].id }));
|
|
1654
|
+
return { allocations, entries };
|
|
1258
1655
|
}
|
|
1259
1656
|
async _querySeriesFromDb(range) {
|
|
1260
1657
|
const { id } = await this.resolve();
|
|
@@ -1305,7 +1702,128 @@ var StrategyHandle = class {
|
|
|
1305
1702
|
closePrices,
|
|
1306
1703
|
leveragedPrices
|
|
1307
1704
|
};
|
|
1308
|
-
|
|
1705
|
+
const liveEvaluator = {
|
|
1706
|
+
previewLiveState: (date, overrides) => this.previewLiveState(date, overrides)
|
|
1707
|
+
};
|
|
1708
|
+
return new SimulationHandle(result.series, result.trades, options.portfolio, finalState, liveEvaluator);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Preview the allocation this strategy would produce for `date` if today
|
|
1712
|
+
* closed at the provided raw quote prices. Does NOT write to strategies_series,
|
|
1713
|
+
* signals_series, or indicators_series. Safe to call before market close.
|
|
1714
|
+
*
|
|
1715
|
+
* @param date - The trading day to preview (must be in tradingDays.getRange()).
|
|
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`).
|
|
1719
|
+
* @returns The AllocationHandle for `date`, or null if the strategy has no
|
|
1720
|
+
* evaluable entry for that date.
|
|
1721
|
+
*/
|
|
1722
|
+
async previewAllocation(date, overrides) {
|
|
1723
|
+
await this.resolve();
|
|
1724
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1725
|
+
if (!tradingDays.includes(date)) {
|
|
1726
|
+
throw new Error(`previewAllocation: ${date} is not a trading day`);
|
|
1727
|
+
}
|
|
1728
|
+
const { allocations, entries } = await this._evaluate(date, overrides);
|
|
1729
|
+
const target = entries.find((e) => e.date === date);
|
|
1730
|
+
if (!target) return null;
|
|
1731
|
+
const alloc = allocations.find((a) => a.id === target.allocationId);
|
|
1732
|
+
return alloc ?? this._allocationMap.get(target.allocationId) ?? null;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Read-only preview of the strategy's allocation series including `date`.
|
|
1736
|
+
* Returns stored historical allocations plus an in-memory bar at `date`
|
|
1737
|
+
* computed via the same overrides-based preview path as `previewAllocation`.
|
|
1738
|
+
*
|
|
1739
|
+
* @param date - Target trading day to splice in-memory.
|
|
1740
|
+
* @param overrides - Raw (unleveraged) quotes keyed by market symbol.
|
|
1741
|
+
* @param range - Optional filter applied to the returned bars.
|
|
1742
|
+
*/
|
|
1743
|
+
async previewSeries(date, overrides, range) {
|
|
1744
|
+
await this.resolve();
|
|
1745
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1746
|
+
if (!tradingDays.includes(date)) {
|
|
1747
|
+
throw new Error(`previewSeries: ${date} is not a trading day`);
|
|
1748
|
+
}
|
|
1749
|
+
const { allocations, entries } = await this._evaluate(date, overrides);
|
|
1750
|
+
const allocById = /* @__PURE__ */ new Map();
|
|
1751
|
+
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) => ({
|
|
1754
|
+
date: e.date,
|
|
1755
|
+
allocation: allocById.get(e.allocationId)
|
|
1756
|
+
}));
|
|
1757
|
+
if (range) {
|
|
1758
|
+
bars = bars.filter(
|
|
1759
|
+
(b) => (range.from === void 0 || b.date >= range.from) && (range.to === void 0 || b.date <= range.to)
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
return bars;
|
|
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 };
|
|
1309
1827
|
}
|
|
1310
1828
|
async _fetchPricesForTickers(bars, from, to) {
|
|
1311
1829
|
const tickerMap = /* @__PURE__ */ new Map();
|