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