@livefolio/sdk 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +35 -2
- package/dist/index.js +392 -43
- 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,6 +317,28 @@ var COMPUTATIONS = {
|
|
|
237
317
|
function getComputation(type) {
|
|
238
318
|
return COMPUTATIONS[type] ?? null;
|
|
239
319
|
}
|
|
320
|
+
var NEXT = {
|
|
321
|
+
SMA: smaNext,
|
|
322
|
+
EMA: emaNext,
|
|
323
|
+
RSI: rsiNext,
|
|
324
|
+
Return: ((prev, newRaw, lookback) => returnNext(prev, newRaw, lookback, "pct")),
|
|
325
|
+
Volatility: volatilityNext,
|
|
326
|
+
Drawdown: drawdownNext
|
|
327
|
+
};
|
|
328
|
+
var SEED = {
|
|
329
|
+
SMA: smaInitialState,
|
|
330
|
+
EMA: emaInitialState,
|
|
331
|
+
RSI: rsiInitialState,
|
|
332
|
+
Return: returnInitialState,
|
|
333
|
+
Volatility: volatilityInitialState,
|
|
334
|
+
Drawdown: drawdownInitialState
|
|
335
|
+
};
|
|
336
|
+
function getNextComputation(type) {
|
|
337
|
+
return NEXT[type];
|
|
338
|
+
}
|
|
339
|
+
function getInitialStateFn(type) {
|
|
340
|
+
return SEED[type];
|
|
341
|
+
}
|
|
240
342
|
|
|
241
343
|
// src/handles/indicator.ts
|
|
242
344
|
var FRED_SYMBOL_TO_TYPE = {
|
|
@@ -349,6 +451,34 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
349
451
|
async _sync(fromDate, latestClosed) {
|
|
350
452
|
const tickerSymbol = this.ticker?.symbol ?? null;
|
|
351
453
|
const info = getProviderInfo(this.type, tickerSymbol);
|
|
454
|
+
if (info.provider === "none") return;
|
|
455
|
+
let horizon = latestClosed;
|
|
456
|
+
if (this.delay > 0) {
|
|
457
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
458
|
+
const idx = tradingDays.indexOf(latestClosed);
|
|
459
|
+
if (idx < this.delay) return;
|
|
460
|
+
horizon = tradingDays[idx - this.delay];
|
|
461
|
+
}
|
|
462
|
+
const nextFn = getNextComputation(this.type);
|
|
463
|
+
const seedFn = getInitialStateFn(this.type);
|
|
464
|
+
const { id } = await this.resolve();
|
|
465
|
+
const checkpoint = nextFn ? await this._storage.indicators.getLatestBar(id) : null;
|
|
466
|
+
if (fromDate && nextFn && seedFn && checkpoint && checkpoint.metadata != null && checkpoint.date < horizon) {
|
|
467
|
+
const rawBars = await this._fetchRawBarsForIncremental(info, checkpoint.date, horizon);
|
|
468
|
+
if (rawBars.length === 0) return;
|
|
469
|
+
const newBars = [];
|
|
470
|
+
let state = checkpoint.metadata;
|
|
471
|
+
for (const raw of rawBars) {
|
|
472
|
+
if (raw.date <= checkpoint.date) continue;
|
|
473
|
+
if (raw.date > horizon) break;
|
|
474
|
+
const step = this.type === "Return" && info.provider === "computed" && info.rateSeries ? returnNext(state, raw.value, this.lookback, "abs") : nextFn(state, raw.value, this.lookback);
|
|
475
|
+
newBars.push({ date: raw.date, value: step.value });
|
|
476
|
+
state = step.state;
|
|
477
|
+
}
|
|
478
|
+
if (newBars.length === 0) return;
|
|
479
|
+
await this._storage.indicators.writeSeries(id, newBars, { metadata: state });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
352
482
|
let bars;
|
|
353
483
|
switch (info.provider) {
|
|
354
484
|
case "yahoo":
|
|
@@ -375,43 +505,73 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
375
505
|
if (!computeFn) throw new Error(`No computation found for type "${this.type}"`);
|
|
376
506
|
bars = computeFn(priceBars, this.lookback);
|
|
377
507
|
}
|
|
378
|
-
if (fromDate)
|
|
379
|
-
bars = bars.filter((b) => b.date > fromDate);
|
|
380
|
-
}
|
|
508
|
+
if (fromDate) bars = bars.filter((b) => b.date > fromDate);
|
|
381
509
|
break;
|
|
382
510
|
}
|
|
383
511
|
case "calendar": {
|
|
384
512
|
const allDays = await this._storage.tradingDays.getRange();
|
|
385
513
|
const dayBars = allDays.map((date) => ({ date, value: 0 }));
|
|
386
514
|
bars = computeCalendar(dayBars, this.type);
|
|
387
|
-
if (fromDate)
|
|
388
|
-
bars = bars.filter((b) => b.date > fromDate);
|
|
389
|
-
}
|
|
515
|
+
if (fromDate) bars = bars.filter((b) => b.date > fromDate);
|
|
390
516
|
break;
|
|
391
517
|
}
|
|
392
|
-
case "none":
|
|
393
|
-
return;
|
|
394
518
|
}
|
|
395
519
|
if (info.provider !== "computed") {
|
|
396
520
|
bars = await this._applyLeverage(bars, fromDate);
|
|
397
521
|
}
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (
|
|
403
|
-
|
|
522
|
+
bars = bars.filter((b) => b.date <= horizon);
|
|
523
|
+
if (bars.length === 0) return;
|
|
524
|
+
let metadata = void 0;
|
|
525
|
+
if (seedFn) {
|
|
526
|
+
if (info.provider === "computed") {
|
|
527
|
+
const priceHandle = new _IndicatorHandle(this._storage, this._market, {
|
|
528
|
+
type: "Price",
|
|
529
|
+
ticker: this.ticker,
|
|
530
|
+
lookback: 0,
|
|
531
|
+
delay: 0,
|
|
532
|
+
unit: null,
|
|
533
|
+
threshold: null
|
|
534
|
+
});
|
|
535
|
+
const priceBars = (await priceHandle._querySeriesFromDb()).filter((b) => b.date <= horizon);
|
|
536
|
+
metadata = seedFn(priceBars, this.lookback) ?? void 0;
|
|
537
|
+
} else {
|
|
538
|
+
metadata = seedFn(bars, this.lookback) ?? void 0;
|
|
404
539
|
}
|
|
405
|
-
horizon = tradingDays[idx - this.delay];
|
|
406
540
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
541
|
+
await this._upsertSeries(bars, metadata);
|
|
542
|
+
}
|
|
543
|
+
async _fetchRawBarsForIncremental(info, sinceDate, horizon) {
|
|
544
|
+
if (info.provider === "computed") {
|
|
545
|
+
const priceHandle = new _IndicatorHandle(this._storage, this._market, {
|
|
546
|
+
type: "Price",
|
|
547
|
+
ticker: this.ticker,
|
|
548
|
+
lookback: 0,
|
|
549
|
+
delay: 0,
|
|
550
|
+
unit: null,
|
|
551
|
+
threshold: null
|
|
552
|
+
});
|
|
553
|
+
await priceHandle._ensureFresh();
|
|
554
|
+
return (await priceHandle._querySeriesFromDb({ from: sinceDate })).filter(
|
|
555
|
+
(b) => b.date > sinceDate && b.date <= horizon
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (info.provider === "yahoo" || info.provider === "fred") {
|
|
559
|
+
const symbol = info.provider === "yahoo" ? info.symbol : info.seriesId;
|
|
560
|
+
const bars = await this._market.fetchBars(symbol, sinceDate);
|
|
561
|
+
return bars.filter((b) => b.date > sinceDate && b.date <= horizon);
|
|
410
562
|
}
|
|
563
|
+
if (info.provider === "calendar") {
|
|
564
|
+
const allDays = await this._storage.tradingDays.getRange();
|
|
565
|
+
const dayBars = allDays.map((date) => ({ date, value: 0 }));
|
|
566
|
+
return computeCalendar(dayBars, this.type).filter(
|
|
567
|
+
(b) => b.date > sinceDate && b.date <= horizon
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return [];
|
|
411
571
|
}
|
|
412
|
-
async _upsertSeries(bars) {
|
|
572
|
+
async _upsertSeries(bars, metadata) {
|
|
413
573
|
const { id } = await this.resolve();
|
|
414
|
-
await this._storage.indicators.writeSeries(id, bars);
|
|
574
|
+
await this._storage.indicators.writeSeries(id, bars, metadata !== void 0 ? { metadata } : void 0);
|
|
415
575
|
}
|
|
416
576
|
async _querySeriesFromDb(range) {
|
|
417
577
|
const { id } = await this.resolve();
|
|
@@ -476,6 +636,22 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
476
636
|
return computed.find((b) => b.date === date)?.value ?? null;
|
|
477
637
|
}
|
|
478
638
|
if (info.provider === "computed") {
|
|
639
|
+
const nextFn = getNextComputation(this.type);
|
|
640
|
+
if (nextFn) {
|
|
641
|
+
const { id } = await this.resolve();
|
|
642
|
+
const checkpoint = await this._storage.indicators.getLatestBar(id);
|
|
643
|
+
if (checkpoint && checkpoint.metadata != null) {
|
|
644
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
645
|
+
const ckIdx = tradingDays.indexOf(checkpoint.date);
|
|
646
|
+
const tgtIdx = tradingDays.indexOf(date);
|
|
647
|
+
if (ckIdx >= 0 && tgtIdx === ckIdx + 1) {
|
|
648
|
+
const rawBar = await this._resolveRawBarAt(info.symbol, date, overrides);
|
|
649
|
+
if (rawBar === null) return null;
|
|
650
|
+
const step = this.type === "Return" && info.rateSeries ? returnNext(checkpoint.metadata, rawBar, this.lookback, "abs") : nextFn(checkpoint.metadata, rawBar, this.lookback);
|
|
651
|
+
return step.value;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
479
655
|
let calendarDays;
|
|
480
656
|
if (this.type === "RSI") {
|
|
481
657
|
calendarDays = Math.max(this.lookback * 10, 90);
|
|
@@ -554,6 +730,18 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
554
730
|
}
|
|
555
731
|
return bars;
|
|
556
732
|
}
|
|
733
|
+
/**
|
|
734
|
+
* Resolve the single raw (unleveraged) value for `symbol` at `date`.
|
|
735
|
+
* Returns the override directly when present; otherwise delegates to
|
|
736
|
+
* `_resolveRawBars` with a one-day window and picks the matching bar.
|
|
737
|
+
*/
|
|
738
|
+
async _resolveRawBarAt(symbol, date, overrides) {
|
|
739
|
+
const override = overrides?.[symbol];
|
|
740
|
+
if (override !== void 0) return override;
|
|
741
|
+
const bars = await this._resolveRawBars(symbol, date, date, overrides);
|
|
742
|
+
const hit = bars.find((b) => b.date === date);
|
|
743
|
+
return hit?.value ?? null;
|
|
744
|
+
}
|
|
557
745
|
/**
|
|
558
746
|
* Resolve raw (unleveraged) bars for a market symbol from storage. Maps:
|
|
559
747
|
* - `^VIX` → the VIX indicator's stored series
|
|
@@ -786,13 +974,16 @@ var SignalHandle = class _SignalHandle {
|
|
|
786
974
|
const { id } = await this.resolve();
|
|
787
975
|
const latestClosed = await this._getLatestClosedTradingDay();
|
|
788
976
|
if (this._cachedAsOf === latestClosed) return;
|
|
789
|
-
await Promise.all([this.indicator1.series(), this.indicator2.series()]);
|
|
790
977
|
const latestSeries = await this._getLatestSignalSeriesDate(id);
|
|
791
978
|
if (latestSeries === latestClosed) {
|
|
792
979
|
this._cachedSeries = null;
|
|
793
980
|
this._cachedAsOf = latestClosed;
|
|
794
981
|
return;
|
|
795
982
|
}
|
|
983
|
+
const isFastPath = await this._isSingleBarFastPath(latestSeries ?? void 0, latestClosed);
|
|
984
|
+
if (!isFastPath) {
|
|
985
|
+
await Promise.all([this.indicator1.series(), this.indicator2.series()]);
|
|
986
|
+
}
|
|
796
987
|
if (!this._syncing) {
|
|
797
988
|
this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
|
|
798
989
|
console.warn("[sdk] signal sync failed, using stored data:", err);
|
|
@@ -804,17 +995,61 @@ var SignalHandle = class _SignalHandle {
|
|
|
804
995
|
this._cachedSeries = null;
|
|
805
996
|
this._cachedAsOf = latestClosed;
|
|
806
997
|
}
|
|
998
|
+
async _isSingleBarFastPath(fromDate, latestClosed) {
|
|
999
|
+
if (!fromDate) return false;
|
|
1000
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1001
|
+
const fromIdx = tradingDays.indexOf(fromDate);
|
|
1002
|
+
const closedIdx = tradingDays.indexOf(latestClosed);
|
|
1003
|
+
return fromIdx >= 0 && closedIdx === fromIdx + 1;
|
|
1004
|
+
}
|
|
807
1005
|
async _sync(fromDate, latestClosed) {
|
|
808
1006
|
const { id } = await this.resolve();
|
|
1007
|
+
const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
|
|
1008
|
+
if (fromDate) {
|
|
1009
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1010
|
+
const fromIdx = tradingDays.indexOf(fromDate);
|
|
1011
|
+
const closedIdx = tradingDays.indexOf(latestClosed);
|
|
1012
|
+
if (fromIdx >= 0 && closedIdx === fromIdx + 1) {
|
|
1013
|
+
const newDate = tradingDays[closedIdx];
|
|
1014
|
+
const [v1, v2] = await Promise.all([
|
|
1015
|
+
this.indicator1.computeAt(newDate, void 0),
|
|
1016
|
+
this.indicator2.computeAt(newDate, void 0)
|
|
1017
|
+
]);
|
|
1018
|
+
if (v1 === null || v2 === null) return;
|
|
1019
|
+
const prev = await this._getLastSignalValue(id) ?? void 0;
|
|
1020
|
+
const value = this._evaluateOneBar(v1, v2, absolute, prev);
|
|
1021
|
+
await this._upsertSeries([{ date: newDate, value }]);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
809
1025
|
const range = fromDate ? { from: fromDate } : void 0;
|
|
810
1026
|
const [series1, series2] = await Promise.all([this.indicator1.series(range), this.indicator2.series(range)]);
|
|
811
1027
|
const previousValue = fromDate ? await this._getLastSignalValue(id) ?? void 0 : void 0;
|
|
812
|
-
const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
|
|
813
1028
|
const signalBars = evaluateSignal(series1, series2, this.comparison, this.tolerance, absolute, previousValue);
|
|
814
1029
|
const bars = signalBars.filter((b) => b.date <= latestClosed);
|
|
815
|
-
if (bars.length > 0)
|
|
816
|
-
|
|
1030
|
+
if (bars.length > 0) await this._upsertSeries(bars);
|
|
1031
|
+
}
|
|
1032
|
+
_evaluateOneBar(v1, v2, absolute, prev) {
|
|
1033
|
+
if (this.tolerance === 0) {
|
|
1034
|
+
switch (this.comparison) {
|
|
1035
|
+
case ">":
|
|
1036
|
+
return v1 > v2 ? 1 : 0;
|
|
1037
|
+
case "<":
|
|
1038
|
+
return v1 < v2 ? 1 : 0;
|
|
1039
|
+
case "=":
|
|
1040
|
+
return v1 === v2 ? 1 : 0;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const upper = absolute ? v2 + this.tolerance : v2 * (1 + this.tolerance / 100);
|
|
1044
|
+
const lower = absolute ? v2 - this.tolerance : v2 * (1 - this.tolerance / 100);
|
|
1045
|
+
if (this.comparison === "=") return v1 >= lower && v1 <= upper ? 1 : 0;
|
|
1046
|
+
if (prev === void 0) {
|
|
1047
|
+
return this.comparison === ">" ? v1 > v2 ? 1 : 0 : v1 < v2 ? 1 : 0;
|
|
817
1048
|
}
|
|
1049
|
+
if (this.comparison === ">") {
|
|
1050
|
+
return prev === 1 ? v1 < lower ? 0 : 1 : v1 > upper ? 1 : 0;
|
|
1051
|
+
}
|
|
1052
|
+
return prev === 1 ? v1 > upper ? 0 : 1 : v1 < lower ? 1 : 0;
|
|
818
1053
|
}
|
|
819
1054
|
async _upsertSeries(bars) {
|
|
820
1055
|
const { id } = await this.resolve();
|
|
@@ -956,6 +1191,9 @@ var AllocationHandle = class _AllocationHandle {
|
|
|
956
1191
|
handle._resolvedId = id;
|
|
957
1192
|
return handle;
|
|
958
1193
|
}
|
|
1194
|
+
toJSON() {
|
|
1195
|
+
return this.holdings.map(([ticker, weight]) => ({ symbol: ticker.symbol, leverage: ticker.leverage, weight })).sort((a, b) => a.symbol.localeCompare(b.symbol) || a.leverage - b.leverage);
|
|
1196
|
+
}
|
|
959
1197
|
async _doResolve() {
|
|
960
1198
|
await Promise.all(this.holdings.map(([ticker]) => ticker.resolve()));
|
|
961
1199
|
const holdingsJson = {};
|
|
@@ -1461,6 +1699,22 @@ var StrategyHandle = class {
|
|
|
1461
1699
|
get rules() {
|
|
1462
1700
|
return this._rules;
|
|
1463
1701
|
}
|
|
1702
|
+
marketSymbols() {
|
|
1703
|
+
const set = /* @__PURE__ */ new Set();
|
|
1704
|
+
for (const rule of this._rules) {
|
|
1705
|
+
for (const [ticker] of rule.hold.holdings) {
|
|
1706
|
+
if (ticker.symbol !== "CASHX") set.add(ticker.symbol);
|
|
1707
|
+
}
|
|
1708
|
+
for (const signal of rule.when ?? []) {
|
|
1709
|
+
for (const ind of [signal.indicator1, signal.indicator2]) {
|
|
1710
|
+
if (ind.ticker !== null && ind.ticker.symbol !== "CASHX") set.add(ind.ticker.symbol);
|
|
1711
|
+
if (ind.type === "VIX") set.add("^VIX");
|
|
1712
|
+
if (ind.type === "VIX3M") set.add("^VIX3M");
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return Array.from(set).sort();
|
|
1717
|
+
}
|
|
1464
1718
|
async resolve() {
|
|
1465
1719
|
if (this._resolvedId != null) return { id: this._resolvedId };
|
|
1466
1720
|
if (!this._resolving) {
|
|
@@ -1600,14 +1854,87 @@ var StrategyHandle = class {
|
|
|
1600
1854
|
* map) we take the read-only preview path: historical signal bars come
|
|
1601
1855
|
* straight from storage, today's bar is computed in-memory via
|
|
1602
1856
|
* `signal.computeAt(date, overrides, prevBool)`, and nothing is written.
|
|
1857
|
+
*
|
|
1858
|
+
* Incremental path: when a strategy checkpoint exists (`getLatestSeriesDate`
|
|
1859
|
+
* returns non-null), only the window (lastDate, limitDate] is processed.
|
|
1860
|
+
* The current allocation is carried forward from `getLatestAllocationId`.
|
|
1861
|
+
* Bootstrap: when no checkpoint exists, falls back to `_evaluateCold` which
|
|
1862
|
+
* runs the full-history evaluation.
|
|
1603
1863
|
*/
|
|
1604
1864
|
async _evaluate(limitDate, overrides) {
|
|
1865
|
+
const { id } = await this.resolve();
|
|
1866
|
+
const lastDate = await this._storage.strategies.getLatestSeriesDate(id);
|
|
1867
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1868
|
+
const limitIdx = tradingDays.indexOf(limitDate);
|
|
1869
|
+
const allocations = [];
|
|
1870
|
+
const allocIndexMap = /* @__PURE__ */ new Map();
|
|
1871
|
+
const rulesInput = this._rules.map((rule) => {
|
|
1872
|
+
let allocIdx = allocIndexMap.get(rule.hold.id);
|
|
1873
|
+
if (allocIdx === void 0) {
|
|
1874
|
+
allocIdx = allocations.length;
|
|
1875
|
+
allocations.push(rule.hold);
|
|
1876
|
+
allocIndexMap.set(rule.hold.id, allocIdx);
|
|
1877
|
+
}
|
|
1878
|
+
return {
|
|
1879
|
+
signalIds: (rule.when ?? []).map((s) => s.id),
|
|
1880
|
+
allocationIndex: allocIdx
|
|
1881
|
+
};
|
|
1882
|
+
});
|
|
1883
|
+
if (lastDate === null) {
|
|
1884
|
+
return this._evaluateCold(limitDate, overrides, rulesInput, allocations, tradingDays);
|
|
1885
|
+
}
|
|
1886
|
+
const lastAllocId = await this._storage.strategies.getLatestAllocationId(id);
|
|
1887
|
+
const incrementalStartIdx = tradingDays.indexOf(lastDate) + 1;
|
|
1888
|
+
const incrementalDays = tradingDays.slice(incrementalStartIdx, limitIdx + 1);
|
|
1889
|
+
const isOverrideRefresh = incrementalDays.length === 0 && overrides !== void 0 && limitDate === lastDate;
|
|
1890
|
+
const newDays = isOverrideRefresh ? [limitDate] : incrementalDays;
|
|
1891
|
+
const startIdx = isOverrideRefresh ? limitIdx : incrementalStartIdx;
|
|
1892
|
+
if (newDays.length === 0) return { allocations, entries: [] };
|
|
1605
1893
|
const allSignals = /* @__PURE__ */ new Set();
|
|
1606
|
-
for (const rule of this._rules)
|
|
1607
|
-
|
|
1894
|
+
for (const rule of this._rules) if (rule.when) rule.when.forEach((s) => allSignals.add(s));
|
|
1895
|
+
const signalSeries = /* @__PURE__ */ new Map();
|
|
1896
|
+
await Promise.all(
|
|
1897
|
+
Array.from(allSignals).map(async (signal) => {
|
|
1898
|
+
const bars = overrides === void 0 ? await signal.series({ from: newDays[0], to: limitDate }) : await this._storage.signals.getSeries(signal.id, { from: newDays[0], to: limitDate });
|
|
1899
|
+
const dateMap = /* @__PURE__ */ new Map();
|
|
1900
|
+
for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
|
|
1901
|
+
if (overrides !== void 0) {
|
|
1902
|
+
const prevDateIdx = startIdx - 1 >= 0 ? tradingDays[startIdx - 1] : void 0;
|
|
1903
|
+
const prevBool = prevDateIdx !== void 0 ? await signal.value(prevDateIdx) === 1 : null;
|
|
1904
|
+
const todayValue = await signal.computeAt(limitDate, overrides, prevBool);
|
|
1905
|
+
if (todayValue !== null) dateMap.set(limitDate, todayValue);
|
|
1906
|
+
}
|
|
1907
|
+
signalSeries.set(signal.id, dateMap);
|
|
1908
|
+
})
|
|
1909
|
+
);
|
|
1910
|
+
const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
|
|
1911
|
+
const entries = [];
|
|
1912
|
+
let current = lastAllocId !== null ? allocIndexMap.get(lastAllocId) ?? void 0 : void 0;
|
|
1913
|
+
for (const date of newDays) {
|
|
1914
|
+
if (rebalanceDates.has(date)) {
|
|
1915
|
+
for (const rule of rulesInput) {
|
|
1916
|
+
if (rule.signalIds.length === 0) {
|
|
1917
|
+
current = rule.allocationIndex;
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
const allTrue = rule.signalIds.every((sid) => signalSeries.get(sid)?.get(date) ?? false);
|
|
1921
|
+
if (allTrue) {
|
|
1922
|
+
current = rule.allocationIndex;
|
|
1923
|
+
break;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (current !== void 0) {
|
|
1928
|
+
entries.push({ date, allocationId: allocations[current].id });
|
|
1929
|
+
}
|
|
1608
1930
|
}
|
|
1931
|
+
return { allocations, entries };
|
|
1932
|
+
}
|
|
1933
|
+
// Renamed body of the old _evaluate — used only for first-ever evaluate (bootstrap).
|
|
1934
|
+
async _evaluateCold(limitDate, overrides, rulesInput, allocations, tradingDays) {
|
|
1935
|
+
const allSignals = /* @__PURE__ */ new Set();
|
|
1936
|
+
for (const rule of this._rules) if (rule.when) rule.when.forEach((s) => allSignals.add(s));
|
|
1609
1937
|
const signalSeries = /* @__PURE__ */ new Map();
|
|
1610
|
-
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1611
1938
|
if (overrides === void 0) {
|
|
1612
1939
|
await Promise.all(
|
|
1613
1940
|
Array.from(allSignals).map(async (signal) => {
|
|
@@ -1635,20 +1962,6 @@ var StrategyHandle = class {
|
|
|
1635
1962
|
);
|
|
1636
1963
|
}
|
|
1637
1964
|
const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
|
|
1638
|
-
const allocations = [];
|
|
1639
|
-
const allocIndexMap = /* @__PURE__ */ new Map();
|
|
1640
|
-
const rulesInput = this._rules.map((rule) => {
|
|
1641
|
-
let allocIdx = allocIndexMap.get(rule.hold.id);
|
|
1642
|
-
if (allocIdx === void 0) {
|
|
1643
|
-
allocIdx = allocations.length;
|
|
1644
|
-
allocations.push(rule.hold);
|
|
1645
|
-
allocIndexMap.set(rule.hold.id, allocIdx);
|
|
1646
|
-
}
|
|
1647
|
-
return {
|
|
1648
|
-
signalIds: (rule.when ?? []).map((s) => s.id),
|
|
1649
|
-
allocationIndex: allocIdx
|
|
1650
|
-
};
|
|
1651
|
-
});
|
|
1652
1965
|
const evalResult = evaluateStrategy(signalSeries, rulesInput, rebalanceDates, tradingDays);
|
|
1653
1966
|
const entries = Array.from(evalResult.entries()).filter(([date]) => date <= limitDate).map(([date, allocIdx]) => ({ date, allocationId: allocations[allocIdx].id }));
|
|
1654
1967
|
return { allocations, entries };
|
|
@@ -1749,11 +2062,30 @@ var StrategyHandle = class {
|
|
|
1749
2062
|
const { allocations, entries } = await this._evaluate(date, overrides);
|
|
1750
2063
|
const allocById = /* @__PURE__ */ new Map();
|
|
1751
2064
|
for (const a of allocations) allocById.set(a.id, a);
|
|
1752
|
-
for (const [
|
|
1753
|
-
|
|
2065
|
+
for (const [id2, a] of this._allocationMap) if (!allocById.has(id2)) allocById.set(id2, a);
|
|
2066
|
+
const { id } = await this.resolve();
|
|
2067
|
+
const lastDate = await this._storage.strategies.getLatestSeriesDate(id);
|
|
2068
|
+
let storedBars = [];
|
|
2069
|
+
if (lastDate !== null && entries.length > 0 && entries[0].date > (tradingDays[0] ?? "")) {
|
|
2070
|
+
const storedEntries = await this._storage.strategies.getSeries(id, { to: lastDate });
|
|
2071
|
+
storedBars = storedEntries.map((e) => ({
|
|
2072
|
+
date: e.date,
|
|
2073
|
+
allocation: allocById.get(e.allocationId) ?? this._allocationMap.get(e.allocationId)
|
|
2074
|
+
}));
|
|
2075
|
+
} else if (lastDate !== null && entries.length === 0) {
|
|
2076
|
+
const storedEntries = await this._storage.strategies.getSeries(id);
|
|
2077
|
+
storedBars = storedEntries.map((e) => ({
|
|
2078
|
+
date: e.date,
|
|
2079
|
+
allocation: allocById.get(e.allocationId) ?? this._allocationMap.get(e.allocationId)
|
|
2080
|
+
}));
|
|
2081
|
+
}
|
|
2082
|
+
const newBars = entries.map((e) => ({
|
|
1754
2083
|
date: e.date,
|
|
1755
2084
|
allocation: allocById.get(e.allocationId)
|
|
1756
2085
|
}));
|
|
2086
|
+
const newDates = new Set(newBars.map((b) => b.date));
|
|
2087
|
+
let bars = [...storedBars.filter((b) => !newDates.has(b.date)), ...newBars];
|
|
2088
|
+
bars.sort((a, b) => a.date.localeCompare(b.date));
|
|
1757
2089
|
if (range) {
|
|
1758
2090
|
bars = bars.filter(
|
|
1759
2091
|
(b) => (range.from === void 0 || b.date >= range.from) && (range.to === void 0 || b.date <= range.to)
|
|
@@ -1949,6 +2281,21 @@ function createClient(options) {
|
|
|
1949
2281
|
strategy: (optionsOrLinkId) => new StrategyHandle(storage, market, optionsOrLinkId)
|
|
1950
2282
|
};
|
|
1951
2283
|
}
|
|
2284
|
+
|
|
2285
|
+
// src/handles/allocation-equality.ts
|
|
2286
|
+
function allocationsEqual(a, b) {
|
|
2287
|
+
if (a === null && b === null) return true;
|
|
2288
|
+
if (a === null || b === null) return false;
|
|
2289
|
+
const aj = a.toJSON();
|
|
2290
|
+
const bj = b.toJSON();
|
|
2291
|
+
if (aj.length !== bj.length) return false;
|
|
2292
|
+
for (let i = 0; i < aj.length; i++) {
|
|
2293
|
+
if (aj[i].symbol !== bj[i].symbol) return false;
|
|
2294
|
+
if (aj[i].leverage !== bj[i].leverage) return false;
|
|
2295
|
+
if (Math.abs(aj[i].weight - bj[i].weight) > 1e-9) return false;
|
|
2296
|
+
}
|
|
2297
|
+
return true;
|
|
2298
|
+
}
|
|
1952
2299
|
export {
|
|
1953
2300
|
AllocationHandle,
|
|
1954
2301
|
IndicatorHandle,
|
|
@@ -1957,6 +2304,8 @@ export {
|
|
|
1957
2304
|
SimulationHandle,
|
|
1958
2305
|
StrategyHandle,
|
|
1959
2306
|
TickerHandle,
|
|
2307
|
+
allocationsEqual,
|
|
2308
|
+
computeRebalanceDates,
|
|
1960
2309
|
createClient
|
|
1961
2310
|
};
|
|
1962
2311
|
//# sourceMappingURL=index.js.map
|