@livefolio/sdk 0.3.3 → 0.3.5
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 -3
- package/dist/index.js +402 -53
- 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);
|
|
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
|
+
);
|
|
410
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,32 @@ 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 leverage2 = this.ticker?.leverage ?? 1;
|
|
651
|
+
let nextValue = rawBar;
|
|
652
|
+
if (leverage2 !== 1 && !isRateTickerSymbol(this.ticker?.symbol ?? null)) {
|
|
653
|
+
const prevRaw = await this._resolveRawBarAt(info.symbol, checkpoint.date, void 0);
|
|
654
|
+
const prevLev = checkpoint.metadata.prev;
|
|
655
|
+
if (prevRaw !== null && prevRaw !== 0 && typeof prevLev === "number") {
|
|
656
|
+
const dailyReturn = (rawBar - prevRaw) / prevRaw;
|
|
657
|
+
nextValue = prevLev * (1 + leverage2 * dailyReturn);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const step = this.type === "Return" && info.rateSeries ? returnNext(checkpoint.metadata, nextValue, this.lookback, "abs") : nextFn(checkpoint.metadata, nextValue, this.lookback);
|
|
661
|
+
return step.value;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
479
665
|
let calendarDays;
|
|
480
666
|
if (this.type === "RSI") {
|
|
481
667
|
calendarDays = Math.max(this.lookback * 10, 90);
|
|
@@ -554,6 +740,18 @@ var IndicatorHandle = class _IndicatorHandle {
|
|
|
554
740
|
}
|
|
555
741
|
return bars;
|
|
556
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Resolve the single raw (unleveraged) value for `symbol` at `date`.
|
|
745
|
+
* Returns the override directly when present; otherwise delegates to
|
|
746
|
+
* `_resolveRawBars` with a one-day window and picks the matching bar.
|
|
747
|
+
*/
|
|
748
|
+
async _resolveRawBarAt(symbol, date, overrides) {
|
|
749
|
+
const override = overrides?.[symbol];
|
|
750
|
+
if (override !== void 0) return override;
|
|
751
|
+
const bars = await this._resolveRawBars(symbol, date, date, overrides);
|
|
752
|
+
const hit = bars.find((b) => b.date === date);
|
|
753
|
+
return hit?.value ?? null;
|
|
754
|
+
}
|
|
557
755
|
/**
|
|
558
756
|
* Resolve raw (unleveraged) bars for a market symbol from storage. Maps:
|
|
559
757
|
* - `^VIX` → the VIX indicator's stored series
|
|
@@ -786,13 +984,16 @@ var SignalHandle = class _SignalHandle {
|
|
|
786
984
|
const { id } = await this.resolve();
|
|
787
985
|
const latestClosed = await this._getLatestClosedTradingDay();
|
|
788
986
|
if (this._cachedAsOf === latestClosed) return;
|
|
789
|
-
await Promise.all([this.indicator1.series(), this.indicator2.series()]);
|
|
790
987
|
const latestSeries = await this._getLatestSignalSeriesDate(id);
|
|
791
988
|
if (latestSeries === latestClosed) {
|
|
792
989
|
this._cachedSeries = null;
|
|
793
990
|
this._cachedAsOf = latestClosed;
|
|
794
991
|
return;
|
|
795
992
|
}
|
|
993
|
+
const isFastPath = await this._isSingleBarFastPath(latestSeries ?? void 0, latestClosed);
|
|
994
|
+
if (!isFastPath) {
|
|
995
|
+
await Promise.all([this.indicator1.series(), this.indicator2.series()]);
|
|
996
|
+
}
|
|
796
997
|
if (!this._syncing) {
|
|
797
998
|
this._syncing = this._sync(latestSeries ?? void 0, latestClosed).catch((err) => {
|
|
798
999
|
console.warn("[sdk] signal sync failed, using stored data:", err);
|
|
@@ -804,17 +1005,61 @@ var SignalHandle = class _SignalHandle {
|
|
|
804
1005
|
this._cachedSeries = null;
|
|
805
1006
|
this._cachedAsOf = latestClosed;
|
|
806
1007
|
}
|
|
1008
|
+
async _isSingleBarFastPath(fromDate, latestClosed) {
|
|
1009
|
+
if (!fromDate) return false;
|
|
1010
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1011
|
+
const fromIdx = tradingDays.indexOf(fromDate);
|
|
1012
|
+
const closedIdx = tradingDays.indexOf(latestClosed);
|
|
1013
|
+
return fromIdx >= 0 && closedIdx === fromIdx + 1;
|
|
1014
|
+
}
|
|
807
1015
|
async _sync(fromDate, latestClosed) {
|
|
808
1016
|
const { id } = await this.resolve();
|
|
1017
|
+
const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
|
|
1018
|
+
if (fromDate) {
|
|
1019
|
+
const tradingDays = await this._storage.tradingDays.getRange();
|
|
1020
|
+
const fromIdx = tradingDays.indexOf(fromDate);
|
|
1021
|
+
const closedIdx = tradingDays.indexOf(latestClosed);
|
|
1022
|
+
if (fromIdx >= 0 && closedIdx === fromIdx + 1) {
|
|
1023
|
+
const newDate = tradingDays[closedIdx];
|
|
1024
|
+
const [v1, v2] = await Promise.all([
|
|
1025
|
+
this.indicator1.computeAt(newDate, void 0),
|
|
1026
|
+
this.indicator2.computeAt(newDate, void 0)
|
|
1027
|
+
]);
|
|
1028
|
+
if (v1 === null || v2 === null) return;
|
|
1029
|
+
const prev = await this._getLastSignalValue(id) ?? void 0;
|
|
1030
|
+
const value = this._evaluateOneBar(v1, v2, absolute, prev);
|
|
1031
|
+
await this._upsertSeries([{ date: newDate, value }]);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
809
1035
|
const range = fromDate ? { from: fromDate } : void 0;
|
|
810
1036
|
const [series1, series2] = await Promise.all([this.indicator1.series(range), this.indicator2.series(range)]);
|
|
811
1037
|
const previousValue = fromDate ? await this._getLastSignalValue(id) ?? void 0 : void 0;
|
|
812
|
-
const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
|
|
813
1038
|
const signalBars = evaluateSignal(series1, series2, this.comparison, this.tolerance, absolute, previousValue);
|
|
814
1039
|
const bars = signalBars.filter((b) => b.date <= latestClosed);
|
|
815
|
-
if (bars.length > 0)
|
|
816
|
-
|
|
1040
|
+
if (bars.length > 0) await this._upsertSeries(bars);
|
|
1041
|
+
}
|
|
1042
|
+
_evaluateOneBar(v1, v2, absolute, prev) {
|
|
1043
|
+
if (this.tolerance === 0) {
|
|
1044
|
+
switch (this.comparison) {
|
|
1045
|
+
case ">":
|
|
1046
|
+
return v1 > v2 ? 1 : 0;
|
|
1047
|
+
case "<":
|
|
1048
|
+
return v1 < v2 ? 1 : 0;
|
|
1049
|
+
case "=":
|
|
1050
|
+
return v1 === v2 ? 1 : 0;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const upper = absolute ? v2 + this.tolerance : v2 * (1 + this.tolerance / 100);
|
|
1054
|
+
const lower = absolute ? v2 - this.tolerance : v2 * (1 - this.tolerance / 100);
|
|
1055
|
+
if (this.comparison === "=") return v1 >= lower && v1 <= upper ? 1 : 0;
|
|
1056
|
+
if (prev === void 0) {
|
|
1057
|
+
return this.comparison === ">" ? v1 > v2 ? 1 : 0 : v1 < v2 ? 1 : 0;
|
|
817
1058
|
}
|
|
1059
|
+
if (this.comparison === ">") {
|
|
1060
|
+
return prev === 1 ? v1 < lower ? 0 : 1 : v1 > upper ? 1 : 0;
|
|
1061
|
+
}
|
|
1062
|
+
return prev === 1 ? v1 > upper ? 0 : 1 : v1 < lower ? 1 : 0;
|
|
818
1063
|
}
|
|
819
1064
|
async _upsertSeries(bars) {
|
|
820
1065
|
const { id } = await this.resolve();
|
|
@@ -956,6 +1201,9 @@ var AllocationHandle = class _AllocationHandle {
|
|
|
956
1201
|
handle._resolvedId = id;
|
|
957
1202
|
return handle;
|
|
958
1203
|
}
|
|
1204
|
+
toJSON() {
|
|
1205
|
+
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);
|
|
1206
|
+
}
|
|
959
1207
|
async _doResolve() {
|
|
960
1208
|
await Promise.all(this.holdings.map(([ticker]) => ticker.resolve()));
|
|
961
1209
|
const holdingsJson = {};
|
|
@@ -1461,6 +1709,22 @@ var StrategyHandle = class {
|
|
|
1461
1709
|
get rules() {
|
|
1462
1710
|
return this._rules;
|
|
1463
1711
|
}
|
|
1712
|
+
marketSymbols() {
|
|
1713
|
+
const set = /* @__PURE__ */ new Set();
|
|
1714
|
+
for (const rule of this._rules) {
|
|
1715
|
+
for (const [ticker] of rule.hold.holdings) {
|
|
1716
|
+
if (ticker.symbol !== "CASHX") set.add(ticker.symbol);
|
|
1717
|
+
}
|
|
1718
|
+
for (const signal of rule.when ?? []) {
|
|
1719
|
+
for (const ind of [signal.indicator1, signal.indicator2]) {
|
|
1720
|
+
if (ind.ticker !== null && ind.ticker.symbol !== "CASHX") set.add(ind.ticker.symbol);
|
|
1721
|
+
if (ind.type === "VIX") set.add("^VIX");
|
|
1722
|
+
if (ind.type === "VIX3M") set.add("^VIX3M");
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return Array.from(set).sort();
|
|
1727
|
+
}
|
|
1464
1728
|
async resolve() {
|
|
1465
1729
|
if (this._resolvedId != null) return { id: this._resolvedId };
|
|
1466
1730
|
if (!this._resolving) {
|
|
@@ -1558,20 +1822,10 @@ var StrategyHandle = class {
|
|
|
1558
1822
|
if (!date) throw new Error("No closed trading days found");
|
|
1559
1823
|
return date;
|
|
1560
1824
|
}
|
|
1561
|
-
async _getLatestStrategySeriesDate() {
|
|
1562
|
-
const { id } = await this.resolve();
|
|
1563
|
-
return this._storage.strategies.getLatestSeriesDate(id);
|
|
1564
|
-
}
|
|
1565
1825
|
async _ensureFresh() {
|
|
1566
1826
|
await this.resolve();
|
|
1567
1827
|
const latestClosed = await this._getLatestClosedTradingDay();
|
|
1568
1828
|
if (this._cachedAsOf === latestClosed) return;
|
|
1569
|
-
const latestSeries = await this._getLatestStrategySeriesDate();
|
|
1570
|
-
if (latestSeries === latestClosed) {
|
|
1571
|
-
this._cache = null;
|
|
1572
|
-
this._cachedAsOf = latestClosed;
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
1829
|
if (!this._syncing) {
|
|
1576
1830
|
this._syncing = this._sync(latestClosed).catch((err) => {
|
|
1577
1831
|
console.warn("[sdk] strategy sync failed, using stored data:", err);
|
|
@@ -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 isLatestRefresh = incrementalDays.length === 0 && limitDate === lastDate;
|
|
1890
|
+
const newDays = isLatestRefresh ? [limitDate] : incrementalDays;
|
|
1891
|
+
const startIdx = isLatestRefresh ? 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
|