@livefolio/sdk 0.2.13 → 0.3.0

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.
Files changed (110) hide show
  1. package/README.md +277 -84
  2. package/dist/index.d.ts +397 -23
  3. package/dist/index.js +1375 -51
  4. package/dist/index.js.map +1 -1
  5. package/package.json +36 -35
  6. package/dist/auth/client.d.ts +0 -4
  7. package/dist/auth/client.d.ts.map +0 -1
  8. package/dist/auth/client.js +0 -37
  9. package/dist/auth/client.js.map +0 -1
  10. package/dist/auth/index.d.ts +0 -3
  11. package/dist/auth/index.d.ts.map +0 -1
  12. package/dist/auth/index.js +0 -6
  13. package/dist/auth/index.js.map +0 -1
  14. package/dist/auth/types.d.ts +0 -9
  15. package/dist/auth/types.d.ts.map +0 -1
  16. package/dist/auth/types.js +0 -3
  17. package/dist/auth/types.js.map +0 -1
  18. package/dist/index.d.ts.map +0 -1
  19. package/dist/market/client.d.ts +0 -4
  20. package/dist/market/client.d.ts.map +0 -1
  21. package/dist/market/client.js +0 -137
  22. package/dist/market/client.js.map +0 -1
  23. package/dist/market/index.d.ts +0 -5
  24. package/dist/market/index.d.ts.map +0 -1
  25. package/dist/market/index.js +0 -9
  26. package/dist/market/index.js.map +0 -1
  27. package/dist/market/trackedTickers.d.ts +0 -4
  28. package/dist/market/trackedTickers.d.ts.map +0 -1
  29. package/dist/market/trackedTickers.js +0 -256
  30. package/dist/market/trackedTickers.js.map +0 -1
  31. package/dist/market/types.d.ts +0 -29
  32. package/dist/market/types.d.ts.map +0 -1
  33. package/dist/market/types.js +0 -3
  34. package/dist/market/types.js.map +0 -1
  35. package/dist/portfolio/client.d.ts +0 -4
  36. package/dist/portfolio/client.d.ts.map +0 -1
  37. package/dist/portfolio/client.js +0 -13
  38. package/dist/portfolio/client.js.map +0 -1
  39. package/dist/portfolio/index.d.ts +0 -5
  40. package/dist/portfolio/index.d.ts.map +0 -1
  41. package/dist/portfolio/index.js +0 -26
  42. package/dist/portfolio/index.js.map +0 -1
  43. package/dist/portfolio/rebalance.d.ts +0 -62
  44. package/dist/portfolio/rebalance.d.ts.map +0 -1
  45. package/dist/portfolio/rebalance.js +0 -205
  46. package/dist/portfolio/rebalance.js.map +0 -1
  47. package/dist/portfolio/symbols.d.ts +0 -13
  48. package/dist/portfolio/symbols.d.ts.map +0 -1
  49. package/dist/portfolio/symbols.js +0 -112
  50. package/dist/portfolio/symbols.js.map +0 -1
  51. package/dist/portfolio/types.d.ts +0 -13
  52. package/dist/portfolio/types.d.ts.map +0 -1
  53. package/dist/portfolio/types.js +0 -3
  54. package/dist/portfolio/types.js.map +0 -1
  55. package/dist/strategy/backtest.d.ts +0 -6
  56. package/dist/strategy/backtest.d.ts.map +0 -1
  57. package/dist/strategy/backtest.js +0 -698
  58. package/dist/strategy/backtest.js.map +0 -1
  59. package/dist/strategy/cache.d.ts +0 -8
  60. package/dist/strategy/cache.d.ts.map +0 -1
  61. package/dist/strategy/cache.js +0 -336
  62. package/dist/strategy/cache.js.map +0 -1
  63. package/dist/strategy/client.d.ts +0 -4
  64. package/dist/strategy/client.d.ts.map +0 -1
  65. package/dist/strategy/client.js +0 -29
  66. package/dist/strategy/client.js.map +0 -1
  67. package/dist/strategy/evaluate.d.ts +0 -12
  68. package/dist/strategy/evaluate.d.ts.map +0 -1
  69. package/dist/strategy/evaluate.js +0 -562
  70. package/dist/strategy/evaluate.js.map +0 -1
  71. package/dist/strategy/get.d.ts +0 -5
  72. package/dist/strategy/get.d.ts.map +0 -1
  73. package/dist/strategy/get.js +0 -25
  74. package/dist/strategy/get.js.map +0 -1
  75. package/dist/strategy/index.d.ts +0 -14
  76. package/dist/strategy/index.d.ts.map +0 -1
  77. package/dist/strategy/index.js +0 -44
  78. package/dist/strategy/index.js.map +0 -1
  79. package/dist/strategy/livefolio.d.ts +0 -25
  80. package/dist/strategy/livefolio.d.ts.map +0 -1
  81. package/dist/strategy/livefolio.js +0 -67
  82. package/dist/strategy/livefolio.js.map +0 -1
  83. package/dist/strategy/performance.d.ts +0 -17
  84. package/dist/strategy/performance.d.ts.map +0 -1
  85. package/dist/strategy/performance.js +0 -57
  86. package/dist/strategy/performance.js.map +0 -1
  87. package/dist/strategy/rules.d.ts +0 -3
  88. package/dist/strategy/rules.d.ts.map +0 -1
  89. package/dist/strategy/rules.js +0 -95
  90. package/dist/strategy/rules.js.map +0 -1
  91. package/dist/strategy/stream.d.ts +0 -5
  92. package/dist/strategy/stream.d.ts.map +0 -1
  93. package/dist/strategy/stream.js +0 -58
  94. package/dist/strategy/stream.js.map +0 -1
  95. package/dist/strategy/symbols.d.ts +0 -4
  96. package/dist/strategy/symbols.d.ts.map +0 -1
  97. package/dist/strategy/symbols.js +0 -41
  98. package/dist/strategy/symbols.js.map +0 -1
  99. package/dist/strategy/time.d.ts +0 -10
  100. package/dist/strategy/time.d.ts.map +0 -1
  101. package/dist/strategy/time.js +0 -33
  102. package/dist/strategy/time.js.map +0 -1
  103. package/dist/strategy/types.d.ts +0 -200
  104. package/dist/strategy/types.d.ts.map +0 -1
  105. package/dist/strategy/types.js +0 -3
  106. package/dist/strategy/types.js.map +0 -1
  107. package/dist/types.d.ts +0 -4
  108. package/dist/types.d.ts.map +0 -1
  109. package/dist/types.js +0 -3
  110. package/dist/types.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,55 +1,1379 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.portfolio = exports.strategy = exports.market = exports.auth = void 0;
37
- exports.createLivefolioClient = createLivefolioClient;
38
- exports.auth = __importStar(require("./auth"));
39
- exports.market = __importStar(require("./market"));
40
- exports.strategy = __importStar(require("./strategy"));
41
- exports.portfolio = __importStar(require("./portfolio"));
42
- const auth_1 = require("./auth");
43
- const market_1 = require("./market");
44
- const strategy_1 = require("./strategy");
45
- const portfolio_1 = require("./portfolio");
46
- function createLivefolioClient(supabase) {
1
+ // src/handles/ticker.ts
2
+ var TickerHandle = class _TickerHandle {
3
+ symbol;
4
+ leverage;
5
+ _storage;
6
+ _resolvedId = null;
7
+ _resolving = null;
8
+ constructor(storage, symbol, leverage = 1) {
9
+ this._storage = storage;
10
+ this.symbol = symbol.toUpperCase();
11
+ this.leverage = leverage;
12
+ }
13
+ get id() {
14
+ if (this._resolvedId == null)
15
+ throw new Error("TickerHandle not yet resolved. Call resolve(), or access via an async method.");
16
+ return this._resolvedId;
17
+ }
18
+ async resolve() {
19
+ if (this._resolvedId != null) return { id: this._resolvedId };
20
+ if (!this._resolving) this._resolving = this._doResolve();
21
+ return this._resolving;
22
+ }
23
+ static fromResolved(storage, id, symbol, leverage) {
24
+ const handle = new _TickerHandle(storage, symbol, leverage);
25
+ handle._resolvedId = id;
26
+ return handle;
27
+ }
28
+ async _doResolve() {
29
+ const result = await this._storage.tickers.findOrCreate(this.symbol, this.leverage);
30
+ this._resolvedId = result.id;
31
+ return result;
32
+ }
33
+ };
34
+
35
+ // src/providers/mappings.ts
36
+ var FRED_SERIES = {
37
+ T3M: "DGS3MO",
38
+ T6M: "DGS6MO",
39
+ T1Y: "DGS1",
40
+ T2Y: "DGS2",
41
+ T3Y: "DGS3",
42
+ T5Y: "DGS5",
43
+ T7Y: "DGS7",
44
+ T10Y: "DGS10",
45
+ T20Y: "DGS20",
46
+ T30Y: "DGS30"
47
+ };
48
+ var COMPUTED_TYPES = /* @__PURE__ */ new Set(["SMA", "EMA", "RSI", "Return", "Volatility", "Drawdown"]);
49
+ var CALENDAR_TYPES = /* @__PURE__ */ new Set(["Month", "Day of Week", "Day of Month", "Day of Year"]);
50
+ function getProviderInfo(type, tickerSymbol) {
51
+ if (type === "Price") return { provider: "yahoo", symbol: tickerSymbol };
52
+ if (type === "VIX") return { provider: "yahoo", symbol: "^VIX" };
53
+ if (type === "VIX3M") return { provider: "yahoo", symbol: "^VIX3M" };
54
+ if (type in FRED_SERIES) return { provider: "fred", seriesId: FRED_SERIES[type] };
55
+ if (COMPUTED_TYPES.has(type)) return { provider: "computed", dependsOn: "Price", symbol: tickerSymbol };
56
+ if (CALENDAR_TYPES.has(type)) return { provider: "calendar" };
57
+ return { provider: "none" };
58
+ }
59
+
60
+ // src/computations/sma.ts
61
+ function computeSma(bars, lookback) {
62
+ if (bars.length < lookback) return [];
63
+ const result = [];
64
+ let sum = 0;
65
+ for (let i = 0; i < lookback; i++) sum += bars[i].value;
66
+ result.push({ date: bars[lookback - 1].date, value: sum / lookback });
67
+ for (let i = lookback; i < bars.length; i++) {
68
+ sum += bars[i].value - bars[i - lookback].value;
69
+ result.push({ date: bars[i].date, value: sum / lookback });
70
+ }
71
+ return result;
72
+ }
73
+
74
+ // src/computations/ema.ts
75
+ function computeEma(bars, lookback) {
76
+ if (bars.length < lookback) return [];
77
+ const multiplier = 2 / (lookback + 1);
78
+ const result = [];
79
+ let sum = 0;
80
+ for (let i = 0; i < lookback; i++) sum += bars[i].value;
81
+ let ema = sum / lookback;
82
+ result.push({ date: bars[lookback - 1].date, value: ema });
83
+ for (let i = lookback; i < bars.length; i++) {
84
+ ema = bars[i].value * multiplier + ema * (1 - multiplier);
85
+ result.push({ date: bars[i].date, value: ema });
86
+ }
87
+ return result;
88
+ }
89
+
90
+ // src/computations/rsi.ts
91
+ function computeRsi(bars, lookback) {
92
+ if (bars.length < lookback + 1) return [];
93
+ const changes = [];
94
+ for (let i = 1; i < bars.length; i++) {
95
+ changes.push(bars[i].value - bars[i - 1].value);
96
+ }
97
+ let avgGain = 0;
98
+ let avgLoss = 0;
99
+ for (let i = 0; i < lookback; i++) {
100
+ if (changes[i] > 0) avgGain += changes[i];
101
+ else avgLoss += Math.abs(changes[i]);
102
+ }
103
+ avgGain /= lookback;
104
+ avgLoss /= lookback;
105
+ const result = [];
106
+ const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
107
+ result.push({
108
+ date: bars[lookback].date,
109
+ value: avgLoss === 0 ? 100 : 100 - 100 / (1 + rs)
110
+ });
111
+ for (let i = lookback; i < changes.length; i++) {
112
+ const gain = changes[i] > 0 ? changes[i] : 0;
113
+ const loss = changes[i] < 0 ? Math.abs(changes[i]) : 0;
114
+ avgGain = (avgGain * (lookback - 1) + gain) / lookback;
115
+ avgLoss = (avgLoss * (lookback - 1) + loss) / lookback;
116
+ const smoothRs = avgLoss === 0 ? 100 : avgGain / avgLoss;
117
+ result.push({
118
+ date: bars[i + 1].date,
119
+ value: avgLoss === 0 ? 100 : 100 - 100 / (1 + smoothRs)
120
+ });
121
+ }
122
+ return result;
123
+ }
124
+
125
+ // src/computations/returns.ts
126
+ function computeReturns(bars, lookback) {
127
+ if (bars.length <= lookback) return [];
128
+ const result = [];
129
+ for (let i = lookback; i < bars.length; i++) {
130
+ result.push({
131
+ date: bars[i].date,
132
+ value: (bars[i].value - bars[i - lookback].value) / bars[i - lookback].value
133
+ });
134
+ }
135
+ return result;
136
+ }
137
+
138
+ // src/computations/volatility.ts
139
+ function computeVolatility(bars, lookback) {
140
+ if (bars.length < lookback + 1) return [];
141
+ const dailyReturns = [];
142
+ for (let i = 1; i < bars.length; i++) {
143
+ dailyReturns.push({
144
+ date: bars[i].date,
145
+ value: bars[i].value / bars[i - 1].value - 1
146
+ });
147
+ }
148
+ if (dailyReturns.length < lookback) return [];
149
+ const result = [];
150
+ for (let i = lookback - 1; i < dailyReturns.length; i++) {
151
+ const window = dailyReturns.slice(i - lookback + 1, i + 1);
152
+ const mean = window.reduce((s, r) => s + r.value, 0) / lookback;
153
+ const variance = window.reduce((s, r) => s + (r.value - mean) ** 2, 0) / lookback;
154
+ result.push({ date: dailyReturns[i].date, value: Math.sqrt(variance) });
155
+ }
156
+ return result;
157
+ }
158
+
159
+ // src/computations/drawdown.ts
160
+ function computeDrawdown(bars, lookback) {
161
+ if (bars.length < lookback) return [];
162
+ const result = [];
163
+ for (let i = lookback - 1; i < bars.length; i++) {
164
+ let max = -Infinity;
165
+ for (let j = i - lookback + 1; j <= i; j++) {
166
+ if (bars[j].value > max) max = bars[j].value;
167
+ }
168
+ result.push({ date: bars[i].date, value: (bars[i].value - max) / max });
169
+ }
170
+ return result;
171
+ }
172
+
173
+ // src/computations/calendar.ts
174
+ function dayOfYear(d) {
175
+ const start = new Date(d.getFullYear(), 0, 0);
176
+ const diff = d.getTime() - start.getTime();
177
+ return Math.floor(diff / (1e3 * 60 * 60 * 24));
178
+ }
179
+ function computeCalendar(bars, period) {
180
+ return bars.map((bar) => {
181
+ const [y, m, d] = bar.date.split("-").map(Number);
182
+ const date = new Date(y, m - 1, d);
183
+ let value;
184
+ switch (period) {
185
+ case "Month":
186
+ value = date.getMonth() + 1;
187
+ break;
188
+ case "Day of Week":
189
+ value = date.getDay();
190
+ break;
191
+ case "Day of Month":
192
+ value = date.getDate();
193
+ break;
194
+ case "Day of Year":
195
+ value = dayOfYear(date);
196
+ break;
197
+ }
198
+ return { date: bar.date, value };
199
+ });
200
+ }
201
+
202
+ // src/computations/index.ts
203
+ var COMPUTATIONS = {
204
+ SMA: computeSma,
205
+ EMA: computeEma,
206
+ RSI: computeRsi,
207
+ Return: computeReturns,
208
+ Volatility: computeVolatility,
209
+ Drawdown: computeDrawdown
210
+ };
211
+ function getComputation(type) {
212
+ return COMPUTATIONS[type] ?? null;
213
+ }
214
+
215
+ // src/handles/indicator.ts
216
+ var IndicatorHandle = class _IndicatorHandle {
217
+ type;
218
+ ticker;
219
+ lookback;
220
+ delay;
221
+ unit;
222
+ threshold;
223
+ _storage;
224
+ _market;
225
+ _resolvedId = null;
226
+ _resolving = null;
227
+ _cachedSeries = null;
228
+ _cachedAsOf = null;
229
+ _syncing = null;
230
+ constructor(storage, market, identity) {
231
+ this._storage = storage;
232
+ this._market = market;
233
+ this.type = identity.type;
234
+ this.ticker = identity.ticker;
235
+ this.lookback = identity.lookback;
236
+ this.delay = identity.delay;
237
+ this.unit = identity.unit;
238
+ this.threshold = identity.threshold;
239
+ }
240
+ get id() {
241
+ if (this._resolvedId == null)
242
+ throw new Error("IndicatorHandle not yet resolved. Call resolve(), or access via an async method.");
243
+ return this._resolvedId;
244
+ }
245
+ async resolve() {
246
+ if (this._resolvedId != null) return { id: this._resolvedId };
247
+ if (!this._resolving) this._resolving = this._doResolve();
248
+ return this._resolving;
249
+ }
250
+ static fromResolved(storage, market, id, identity) {
251
+ const handle = new _IndicatorHandle(storage, market, identity);
252
+ handle._resolvedId = id;
253
+ return handle;
254
+ }
255
+ async _doResolve() {
256
+ const tickerId = this.ticker ? (await this.ticker.resolve()).id : null;
257
+ const result = await this._storage.indicators.findOrCreate({
258
+ type: this.type,
259
+ tickerId,
260
+ lookback: this.lookback,
261
+ delay: this.delay,
262
+ unit: this.unit,
263
+ threshold: this.threshold
264
+ });
265
+ this._resolvedId = result.id;
266
+ return result;
267
+ }
268
+ // ── Freshness & Sync ───────────────────────────────────────────────
269
+ async _getLatestClosedTradingDay() {
270
+ const date = await this._storage.tradingDays.getLatestClosed();
271
+ if (!date) throw new Error("No closed trading days found");
272
+ return date;
273
+ }
274
+ async _getLatestSeriesDate(indicatorId) {
275
+ return this._storage.indicators.getLatestSeriesDate(indicatorId);
276
+ }
277
+ async _ensureFresh() {
278
+ const { id } = await this.resolve();
279
+ const latestClosed = await this._getLatestClosedTradingDay();
280
+ if (this._cachedAsOf === latestClosed) return;
281
+ const latestSeries = await this._getLatestSeriesDate(id);
282
+ if (latestSeries === latestClosed) {
283
+ this._cachedSeries = null;
284
+ this._cachedAsOf = latestClosed;
285
+ return;
286
+ }
287
+ if (!this._syncing) {
288
+ this._syncing = this._sync(latestSeries ?? void 0, latestClosed).finally(() => {
289
+ this._syncing = null;
290
+ });
291
+ }
292
+ await this._syncing;
293
+ this._cachedSeries = null;
294
+ this._cachedAsOf = latestClosed;
295
+ }
296
+ async _sync(fromDate, latestClosed) {
297
+ const tickerSymbol = this.ticker?.symbol ?? null;
298
+ const info = getProviderInfo(this.type, tickerSymbol);
299
+ let bars;
300
+ switch (info.provider) {
301
+ case "yahoo":
302
+ bars = await this._market.fetchBars(info.symbol, fromDate);
303
+ break;
304
+ case "fred":
305
+ bars = await this._market.fetchBars(info.seriesId, fromDate);
306
+ break;
307
+ case "computed": {
308
+ const priceHandle = new _IndicatorHandle(this._storage, this._market, {
309
+ type: "Price",
310
+ ticker: this.ticker,
311
+ lookback: 0,
312
+ delay: 0,
313
+ unit: null,
314
+ threshold: null
315
+ });
316
+ await priceHandle._ensureFresh();
317
+ const priceBars = await priceHandle._querySeriesFromDb();
318
+ const computeFn = getComputation(this.type);
319
+ if (!computeFn) throw new Error(`No computation found for type "${this.type}"`);
320
+ bars = computeFn(priceBars, this.lookback);
321
+ if (fromDate) {
322
+ bars = bars.filter((b) => b.date > fromDate);
323
+ }
324
+ break;
325
+ }
326
+ case "calendar": {
327
+ const allDays = await this._storage.tradingDays.getRange();
328
+ const dayBars = allDays.map((date) => ({ date, value: 0 }));
329
+ bars = computeCalendar(dayBars, this.type);
330
+ if (fromDate) {
331
+ bars = bars.filter((b) => b.date > fromDate);
332
+ }
333
+ break;
334
+ }
335
+ case "none":
336
+ return;
337
+ }
338
+ const leverage = this.ticker?.leverage ?? 1;
339
+ if (leverage !== 1 && info.provider !== "computed" && bars.length > 0) {
340
+ let anchor;
341
+ if (fromDate) {
342
+ const lastStored = await this._storage.indicators.getValue(this._resolvedId, fromDate);
343
+ anchor = lastStored ?? bars[0].value;
344
+ } else {
345
+ anchor = bars[0].value;
346
+ }
347
+ const leveraged = [{ date: bars[0].date, value: anchor }];
348
+ for (let i = 1; i < bars.length; i++) {
349
+ const dailyReturn = (bars[i].value - bars[i - 1].value) / bars[i - 1].value;
350
+ const prev = leveraged[i - 1].value;
351
+ leveraged.push({ date: bars[i].date, value: prev * (1 + leverage * dailyReturn) });
352
+ }
353
+ bars = leveraged;
354
+ }
355
+ bars = bars.filter((b) => b.date <= latestClosed);
356
+ if (bars.length > 0) {
357
+ await this._upsertSeries(bars);
358
+ }
359
+ }
360
+ async _upsertSeries(bars) {
361
+ const { id } = await this.resolve();
362
+ await this._storage.indicators.writeSeries(id, bars);
363
+ }
364
+ async _querySeriesFromDb(range) {
365
+ const { id } = await this.resolve();
366
+ return this._storage.indicators.getSeries(id, range);
367
+ }
368
+ // ── Public data access ─────────────────────────────────────────────
369
+ async series(range) {
370
+ if (this.type === "Threshold") {
371
+ return this._syntheticThresholdSeries(range);
372
+ }
373
+ await this._ensureFresh();
374
+ if (this._cachedSeries && !range) return this._cachedSeries;
375
+ const bars = await this._querySeriesFromDb(range);
376
+ if (!range) this._cachedSeries = bars;
377
+ return bars;
378
+ }
379
+ async _syntheticThresholdSeries(range) {
380
+ const v = this.threshold;
381
+ const dates = await this._storage.tradingDays.getRange(range);
382
+ return dates.map((date) => ({ date, value: v }));
383
+ }
384
+ async value(date) {
385
+ await this._ensureFresh();
386
+ const { id } = await this.resolve();
387
+ return this._storage.indicators.getValue(id, date);
388
+ }
389
+ };
390
+
391
+ // src/computations/signal.ts
392
+ function computeBuffers(v2, tolerance, absolute) {
393
+ if (tolerance === 0) return { upper: v2, lower: v2 };
394
+ if (absolute) return { upper: v2 + tolerance, lower: v2 - tolerance };
395
+ return { upper: v2 * (1 + tolerance / 100), lower: v2 * (1 - tolerance / 100) };
396
+ }
397
+ function rawCompare(v1, v2, comparison) {
398
+ switch (comparison) {
399
+ case ">":
400
+ return v1 > v2 ? 1 : 0;
401
+ case "<":
402
+ return v1 < v2 ? 1 : 0;
403
+ case "=":
404
+ return v1 === v2 ? 1 : 0;
405
+ }
406
+ }
407
+ function evaluateSignal(series1, series2, comparison, tolerance, absolute, previousValue) {
408
+ const s2Map = /* @__PURE__ */ new Map();
409
+ for (const bar of series2) {
410
+ s2Map.set(bar.date, bar.value);
411
+ }
412
+ const result = [];
413
+ let prev = previousValue;
414
+ for (const bar1 of series1) {
415
+ const v2 = s2Map.get(bar1.date);
416
+ if (v2 === void 0) continue;
417
+ const v1 = bar1.value;
418
+ const { upper, lower } = computeBuffers(v2, tolerance, absolute);
419
+ let value;
420
+ if (tolerance === 0) {
421
+ value = rawCompare(v1, v2, comparison);
422
+ } else if (comparison === "=") {
423
+ value = v1 >= lower && v1 <= upper ? 1 : 0;
424
+ } else if (prev === void 0) {
425
+ value = rawCompare(v1, v2, comparison);
426
+ } else if (comparison === ">") {
427
+ if (prev === 1) {
428
+ value = v1 < lower ? 0 : 1;
429
+ } else {
430
+ value = v1 > upper ? 1 : 0;
431
+ }
432
+ } else {
433
+ if (prev === 1) {
434
+ value = v1 > upper ? 0 : 1;
435
+ } else {
436
+ value = v1 < lower ? 1 : 0;
437
+ }
438
+ }
439
+ result.push({ date: bar1.date, value });
440
+ prev = value;
441
+ }
442
+ return result;
443
+ }
444
+
445
+ // src/handles/signal.ts
446
+ var ABSOLUTE_TOLERANCE_TYPES = /* @__PURE__ */ new Set([
447
+ "Return",
448
+ "Volatility",
449
+ "Drawdown",
450
+ "VIX",
451
+ "VIX3M",
452
+ "T3M",
453
+ "T6M",
454
+ "T1Y",
455
+ "T2Y",
456
+ "T3Y",
457
+ "T5Y",
458
+ "T7Y",
459
+ "T10Y",
460
+ "T20Y",
461
+ "T30Y"
462
+ ]);
463
+ var SignalHandle = class _SignalHandle {
464
+ indicator1;
465
+ indicator2;
466
+ comparison;
467
+ tolerance;
468
+ _storage;
469
+ _market;
470
+ _resolvedId = null;
471
+ _resolving = null;
472
+ _cachedSeries = null;
473
+ _cachedAsOf = null;
474
+ _syncing = null;
475
+ constructor(storage, market, identity) {
476
+ this._storage = storage;
477
+ this._market = market;
478
+ this.indicator1 = identity.indicator1;
479
+ this.indicator2 = identity.indicator2;
480
+ this.comparison = identity.comparison;
481
+ this.tolerance = identity.tolerance;
482
+ }
483
+ get id() {
484
+ if (this._resolvedId == null)
485
+ throw new Error("SignalHandle not yet resolved. Call resolve(), or access via an async method.");
486
+ return this._resolvedId;
487
+ }
488
+ async resolve() {
489
+ if (this._resolvedId != null) return { id: this._resolvedId };
490
+ if (!this._resolving) this._resolving = this._doResolve();
491
+ return this._resolving;
492
+ }
493
+ static fromResolved(storage, market, id, identity) {
494
+ const handle = new _SignalHandle(storage, market, identity);
495
+ handle._resolvedId = id;
496
+ return handle;
497
+ }
498
+ async _doResolve() {
499
+ const [ind1, ind2] = await Promise.all([this.indicator1.resolve(), this.indicator2.resolve()]);
500
+ const result = await this._storage.signals.findOrCreate({
501
+ indicatorId1: ind1.id,
502
+ indicatorId2: ind2.id,
503
+ comparison: this.comparison,
504
+ tolerance: this.tolerance
505
+ });
506
+ this._resolvedId = result.id;
507
+ return result;
508
+ }
509
+ // ── Freshness & Sync ───────────────────────────────────────────────
510
+ async _getLatestClosedTradingDay() {
511
+ const date = await this._storage.tradingDays.getLatestClosed();
512
+ if (!date) throw new Error("No closed trading days found");
513
+ return date;
514
+ }
515
+ async _getLatestSignalSeriesDate(signalId) {
516
+ return this._storage.signals.getLatestSeriesDate(signalId);
517
+ }
518
+ async _getLastSignalValue(signalId) {
519
+ return this._storage.signals.getLastValue(signalId);
520
+ }
521
+ async _ensureFresh() {
522
+ const { id } = await this.resolve();
523
+ const latestClosed = await this._getLatestClosedTradingDay();
524
+ if (this._cachedAsOf === latestClosed) return;
525
+ await Promise.all([this.indicator1.series(), this.indicator2.series()]);
526
+ const latestSeries = await this._getLatestSignalSeriesDate(id);
527
+ if (latestSeries === latestClosed) {
528
+ this._cachedSeries = null;
529
+ this._cachedAsOf = latestClosed;
530
+ return;
531
+ }
532
+ if (!this._syncing) {
533
+ this._syncing = this._sync(latestSeries ?? void 0, latestClosed).finally(() => {
534
+ this._syncing = null;
535
+ });
536
+ }
537
+ await this._syncing;
538
+ this._cachedSeries = null;
539
+ this._cachedAsOf = latestClosed;
540
+ }
541
+ async _sync(fromDate, latestClosed) {
542
+ const { id } = await this.resolve();
543
+ const range = fromDate ? { from: fromDate } : void 0;
544
+ const [series1, series2] = await Promise.all([this.indicator1.series(range), this.indicator2.series(range)]);
545
+ const previousValue = fromDate ? await this._getLastSignalValue(id) ?? void 0 : void 0;
546
+ const absolute = ABSOLUTE_TOLERANCE_TYPES.has(this.indicator1.type);
547
+ const signalBars = evaluateSignal(series1, series2, this.comparison, this.tolerance, absolute, previousValue);
548
+ const bars = signalBars.filter((b) => b.date <= latestClosed);
549
+ if (bars.length > 0) {
550
+ await this._upsertSeries(bars);
551
+ }
552
+ }
553
+ async _upsertSeries(bars) {
554
+ const { id } = await this.resolve();
555
+ await this._storage.signals.writeSeries(id, bars);
556
+ }
557
+ async _querySeriesFromDb(range) {
558
+ const { id } = await this.resolve();
559
+ return this._storage.signals.getSeries(id, range);
560
+ }
561
+ // ── Public data access ─────────────────────────────────────────────
562
+ async series(range) {
563
+ await this._ensureFresh();
564
+ if (this._cachedSeries && !range) return this._cachedSeries;
565
+ const bars = await this._querySeriesFromDb(range);
566
+ if (!range) this._cachedSeries = bars;
567
+ return bars;
568
+ }
569
+ async value(date) {
570
+ await this._ensureFresh();
571
+ if (date) {
572
+ const series = await this._querySeriesFromDb({ from: date, to: date });
573
+ return series.length > 0 ? series[0].value : null;
574
+ }
575
+ const { id } = await this.resolve();
576
+ return this._storage.signals.getLastValue(id);
577
+ }
578
+ };
579
+
580
+ // src/handles/allocation.ts
581
+ var AllocationHandle = class _AllocationHandle {
582
+ holdings;
583
+ _storage;
584
+ _resolvedId = null;
585
+ _resolving = null;
586
+ constructor(storage, holdings) {
587
+ const total = holdings.reduce((sum, [, weight]) => sum + weight, 0);
588
+ if (Math.abs(total - 1) > 1e-9) {
589
+ throw new Error(`Allocation weights must sum to 1, got ${total}`);
590
+ }
591
+ this._storage = storage;
592
+ this.holdings = holdings;
593
+ }
594
+ get id() {
595
+ if (this._resolvedId == null)
596
+ throw new Error("AllocationHandle not yet resolved. Call resolve(), or access via an async method.");
597
+ return this._resolvedId;
598
+ }
599
+ async resolve() {
600
+ if (this._resolvedId != null) return { id: this._resolvedId };
601
+ if (!this._resolving) this._resolving = this._doResolve();
602
+ return this._resolving;
603
+ }
604
+ static fromResolved(storage, id, holdings) {
605
+ const handle = new _AllocationHandle(storage, holdings);
606
+ handle._resolvedId = id;
607
+ return handle;
608
+ }
609
+ async _doResolve() {
610
+ await Promise.all(this.holdings.map(([ticker]) => ticker.resolve()));
611
+ const holdingsJson = {};
612
+ for (const [ticker, weight] of this.holdings) {
613
+ const key = ticker.leverage !== 1 ? `${ticker.symbol}?L=${ticker.leverage}` : ticker.symbol;
614
+ holdingsJson[key] = weight;
615
+ }
616
+ const result = await this._storage.allocations.findOrCreate(holdingsJson);
617
+ this._resolvedId = result.id;
618
+ return result;
619
+ }
620
+ };
621
+
622
+ // src/handles/strategy.ts
623
+ import { customAlphabet } from "nanoid";
624
+
625
+ // src/computations/strategy.ts
626
+ function getPeriodKey(dateStr, freq) {
627
+ const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00Z");
628
+ const y = d.getUTCFullYear();
629
+ const m = d.getUTCMonth();
630
+ switch (freq) {
631
+ case "Weekly": {
632
+ const thu = new Date(d);
633
+ thu.setUTCDate(thu.getUTCDate() + 3 - (thu.getUTCDay() + 6) % 7);
634
+ const yearStart = new Date(Date.UTC(thu.getUTCFullYear(), 0, 1));
635
+ const weekNo = Math.ceil(((thu.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
636
+ return `${thu.getUTCFullYear()}-W${weekNo}`;
637
+ }
638
+ case "Monthly":
639
+ return `${y}-${m}`;
640
+ case "Bi-monthly":
641
+ return `${y}-${Math.floor(m / 2)}`;
642
+ case "Quarterly":
643
+ return `${y}-Q${Math.floor(m / 3)}`;
644
+ case "Every 4 Months":
645
+ return `${y}-${Math.floor(m / 4)}`;
646
+ case "Semiannually":
647
+ return `${y}-H${Math.floor(m / 6)}`;
648
+ case "Yearly":
649
+ return `${y}`;
650
+ default:
651
+ return `${y}-${m}`;
652
+ }
653
+ }
654
+ function computeRebalanceDates(tradingDays, freq, offset) {
655
+ if (freq === "Daily") return new Set(tradingDays);
656
+ const groups = /* @__PURE__ */ new Map();
657
+ for (let i = 0; i < tradingDays.length; i++) {
658
+ const key = getPeriodKey(tradingDays[i], freq);
659
+ if (!groups.has(key)) groups.set(key, []);
660
+ groups.get(key).push(i);
661
+ }
662
+ const result = /* @__PURE__ */ new Set();
663
+ for (const indices of groups.values()) {
664
+ const lastIdx = indices[indices.length - 1];
665
+ const targetIdx = lastIdx - offset;
666
+ if (targetIdx >= 0 && targetIdx < tradingDays.length) {
667
+ result.add(tradingDays[targetIdx]);
668
+ }
669
+ }
670
+ return result;
671
+ }
672
+ function evaluateStrategy(signalSeries, rules, rebalanceDates, tradingDays) {
673
+ const result = /* @__PURE__ */ new Map();
674
+ let current;
675
+ for (const date of tradingDays) {
676
+ if (rebalanceDates.has(date)) {
677
+ for (const rule of rules) {
678
+ if (rule.signalIds.length === 0) {
679
+ current = rule.allocationIndex;
680
+ break;
681
+ }
682
+ const allTrue = rule.signalIds.every((id) => signalSeries.get(id)?.get(date) ?? false);
683
+ if (allTrue) {
684
+ current = rule.allocationIndex;
685
+ break;
686
+ }
687
+ }
688
+ }
689
+ if (current !== void 0) {
690
+ result.set(date, current);
691
+ }
692
+ }
693
+ return result;
694
+ }
695
+
696
+ // src/handles/portfolio.ts
697
+ var PortfolioHandle = class {
698
+ holdings;
699
+ constructor(holdings) {
700
+ const seen = /* @__PURE__ */ new Set();
701
+ for (const [ticker, quantity] of holdings) {
702
+ const key = `${ticker.symbol}:${ticker.leverage}`;
703
+ if (seen.has(key)) {
704
+ throw new Error(`Duplicate ticker: ${ticker.symbol}`);
705
+ }
706
+ seen.add(key);
707
+ if (quantity < 0) {
708
+ throw new Error(`Quantity for ${ticker.symbol} is negative: ${quantity}`);
709
+ }
710
+ }
711
+ this.holdings = holdings;
712
+ }
713
+ _priceMap(prices) {
714
+ const map = /* @__PURE__ */ new Map();
715
+ for (const [ticker, price] of prices) {
716
+ map.set(`${ticker.symbol}:${ticker.leverage}`, price);
717
+ }
718
+ return map;
719
+ }
720
+ _priceFor(ticker, priceMap) {
721
+ if (ticker.symbol === "CASHX") return 1;
722
+ const key = `${ticker.symbol}:${ticker.leverage}`;
723
+ const price = priceMap.get(key);
724
+ if (price == null) {
725
+ throw new Error(`Missing price for ${ticker.symbol}`);
726
+ }
727
+ return price;
728
+ }
729
+ value(prices) {
730
+ const priceMap = this._priceMap(prices);
731
+ let total = 0;
732
+ for (const [ticker, quantity] of this.holdings) {
733
+ total += quantity * this._priceFor(ticker, priceMap);
734
+ }
735
+ return total;
736
+ }
737
+ weights(prices) {
738
+ const total = this.value(prices);
739
+ if (total === 0) return [];
740
+ const priceMap = this._priceMap(prices);
741
+ const result = [];
742
+ for (const [ticker, quantity] of this.holdings) {
743
+ const dollarValue = quantity * this._priceFor(ticker, priceMap);
744
+ if (dollarValue === 0) continue;
745
+ result.push([ticker, dollarValue / total]);
746
+ }
747
+ return result;
748
+ }
749
+ trades(target, prices, date) {
750
+ const priceMap = this._priceMap(prices);
751
+ const totalValue = this.value(prices);
752
+ const currentDollars = /* @__PURE__ */ new Map();
753
+ for (const [ticker, quantity] of this.holdings) {
754
+ if (ticker.symbol === "CASHX") continue;
755
+ const price = this._priceFor(ticker, priceMap);
756
+ currentDollars.set(ticker.symbol, quantity * price);
757
+ }
758
+ const targetDollars = /* @__PURE__ */ new Map();
759
+ for (const [ticker, weight] of target.holdings) {
760
+ if (ticker.symbol === "CASHX") continue;
761
+ targetDollars.set(ticker.symbol, totalValue * weight);
762
+ }
763
+ const tickerBySymbol = /* @__PURE__ */ new Map();
764
+ for (const [ticker] of this.holdings) {
765
+ if (ticker.symbol !== "CASHX") tickerBySymbol.set(ticker.symbol, ticker);
766
+ }
767
+ for (const [ticker] of target.holdings) {
768
+ if (ticker.symbol === "CASHX") continue;
769
+ const existing = tickerBySymbol.get(ticker.symbol);
770
+ if (existing && existing.leverage !== ticker.leverage) {
771
+ throw new Error(`Conflicting leverage for ${ticker.symbol}`);
772
+ }
773
+ tickerBySymbol.set(ticker.symbol, ticker);
774
+ }
775
+ const allSymbols = /* @__PURE__ */ new Set([...currentDollars.keys(), ...targetDollars.keys()]);
776
+ const sells = [];
777
+ const buys = [];
778
+ for (const symbol of allSymbols) {
779
+ const current = currentDollars.get(symbol) ?? 0;
780
+ const target$ = targetDollars.get(symbol) ?? 0;
781
+ const delta = target$ - current;
782
+ const ticker = tickerBySymbol.get(symbol);
783
+ const price = this._priceFor(ticker, priceMap);
784
+ const quantity = Math.abs(delta) / price;
785
+ if (quantity < 1e-10) continue;
786
+ const trade = { date, symbol, quantity, price, action: delta > 0 ? "buy" : "sell" };
787
+ if (trade.action === "sell") {
788
+ sells.push(trade);
789
+ } else {
790
+ buys.push(trade);
791
+ }
792
+ }
793
+ return [...sells, ...buys];
794
+ }
795
+ };
796
+
797
+ // src/backtest/simulate.ts
798
+ var EPSILON = 1e-8;
799
+ function tkey(symbol, leverage) {
800
+ return `${symbol}:${leverage}`;
801
+ }
802
+ function runSimulation(bars, prices, rebalanceDates, portfolio) {
803
+ const positions = {};
804
+ const lastPrice = {};
805
+ let cash = 0;
806
+ for (const [ticker, quantity] of portfolio.holdings) {
807
+ if (ticker.symbol === "CASHX") {
808
+ cash = quantity;
809
+ } else {
810
+ positions[tkey(ticker.symbol, ticker.leverage)] = quantity;
811
+ }
812
+ }
813
+ const series = [];
814
+ const trades = [];
815
+ function valuationPrice(key, date) {
816
+ const live = prices[key]?.[date];
817
+ if (live != null) {
818
+ lastPrice[key] = live;
819
+ return live;
820
+ }
821
+ return lastPrice[key];
822
+ }
823
+ for (const bar of bars) {
824
+ const date = bar.date;
825
+ if (rebalanceDates.has(date)) {
826
+ let portfolioValue = cash;
827
+ for (const [key, shares] of Object.entries(positions)) {
828
+ const price = valuationPrice(key, date);
829
+ if (price != null) portfolioValue += shares * price;
830
+ }
831
+ const targetWeights = {};
832
+ for (const [ticker, weight] of bar.allocation.holdings) {
833
+ targetWeights[tkey(ticker.symbol, ticker.leverage)] = weight;
834
+ }
835
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(positions), ...Object.keys(targetWeights)]);
836
+ for (const key of allKeys) {
837
+ const price = prices[key]?.[date];
838
+ if (price == null || price <= 0) continue;
839
+ const currentShares = positions[key] ?? 0;
840
+ const targetValue = portfolioValue * (targetWeights[key] ?? 0);
841
+ const targetShares = targetValue / price;
842
+ const delta = targetShares - currentShares;
843
+ if (Math.abs(delta) <= EPSILON) continue;
844
+ if (Math.abs(targetShares) <= EPSILON) {
845
+ delete positions[key];
846
+ } else {
847
+ positions[key] = targetShares;
848
+ }
849
+ cash -= delta * price;
850
+ trades.push({
851
+ date,
852
+ symbol: key.split(":")[0],
853
+ quantity: Math.abs(delta),
854
+ price,
855
+ action: delta > 0 ? "buy" : "sell"
856
+ });
857
+ }
858
+ if (Math.abs(cash) <= EPSILON) cash = 0;
859
+ }
860
+ let value = cash;
861
+ for (const [key, shares] of Object.entries(positions)) {
862
+ const price = valuationPrice(key, date);
863
+ if (price != null) value += shares * price;
864
+ }
865
+ series.push({ date, value });
866
+ }
867
+ const finalHoldings = [];
868
+ const tickerByKey = /* @__PURE__ */ new Map();
869
+ for (const bar of bars) {
870
+ for (const [ticker] of bar.allocation.holdings) {
871
+ const key = tkey(ticker.symbol, ticker.leverage);
872
+ if (!tickerByKey.has(key)) {
873
+ tickerByKey.set(key, ticker);
874
+ }
875
+ }
876
+ }
877
+ for (const [ticker] of portfolio.holdings) {
878
+ const key = tkey(ticker.symbol, ticker.leverage);
879
+ if (!tickerByKey.has(key)) {
880
+ tickerByKey.set(key, ticker);
881
+ }
882
+ }
883
+ for (const [key, shares] of Object.entries(positions)) {
884
+ const ticker = tickerByKey.get(key);
885
+ if (ticker && Math.abs(shares) > EPSILON) {
886
+ finalHoldings.push([ticker, shares]);
887
+ }
888
+ }
889
+ const cashKey = tkey("CASHX", 1);
890
+ const cashTicker = tickerByKey.get(cashKey) ?? portfolio.holdings.find(([t]) => t.symbol === "CASHX")?.[0];
891
+ if (cashTicker && Math.abs(cash) > EPSILON) {
892
+ finalHoldings.push([cashTicker, cash]);
893
+ }
894
+ const finalPortfolio = new PortfolioHandle(finalHoldings);
895
+ return { series, trades, finalPortfolio };
896
+ }
897
+
898
+ // src/backtest/types.ts
899
+ var SimulationHandle = class {
900
+ series;
901
+ trades;
902
+ startingPortfolio;
903
+ _portfolio;
904
+ _currentAllocation;
905
+ _lastClosePrices;
906
+ _lastLeveragedPrices;
907
+ _currentLeveragedPrices;
908
+ _lastDate;
909
+ constructor(series, trades, startingPortfolio, finalState) {
910
+ this.series = series;
911
+ this.trades = trades;
912
+ this.startingPortfolio = startingPortfolio;
913
+ if (finalState) {
914
+ this._portfolio = finalState.portfolio;
915
+ this._currentAllocation = finalState.allocation;
916
+ this._lastClosePrices = finalState.closePrices;
917
+ this._lastLeveragedPrices = new Map(Object.entries(finalState.leveragedPrices));
918
+ this._currentLeveragedPrices = new Map(Object.entries(finalState.leveragedPrices));
919
+ this._lastDate = series.at(-1)?.date ?? "";
920
+ } else {
921
+ this._portfolio = null;
922
+ this._currentAllocation = null;
923
+ this._lastClosePrices = {};
924
+ this._lastLeveragedPrices = /* @__PURE__ */ new Map();
925
+ this._currentLeveragedPrices = /* @__PURE__ */ new Map();
926
+ this._lastDate = "";
927
+ }
928
+ }
929
+ push(...prices) {
930
+ if (!this._portfolio || !this._currentAllocation) {
931
+ return { value: 0, holdings: [], weights: [], pendingTrades: [] };
932
+ }
933
+ for (const [ticker, realPrice] of prices) {
934
+ if (ticker.symbol === "CASHX") continue;
935
+ const lastClose = this._lastClosePrices[ticker.symbol];
936
+ if (lastClose == null) continue;
937
+ const realReturn = (realPrice - lastClose) / lastClose;
938
+ for (const [held] of this._portfolio.holdings) {
939
+ if (held.symbol !== ticker.symbol) continue;
940
+ if (held.symbol === "CASHX") continue;
941
+ const key = `${held.symbol}:${held.leverage}`;
942
+ const baseLeveragedPrice = this._lastLeveragedPrices.get(key);
943
+ if (baseLeveragedPrice == null) continue;
944
+ const leveragedReturn = held.leverage * realReturn;
945
+ this._currentLeveragedPrices.set(key, baseLeveragedPrice * (1 + leveragedReturn));
946
+ }
947
+ }
948
+ const priceArray = [];
949
+ for (const [held] of this._portfolio.holdings) {
950
+ if (held.symbol === "CASHX") continue;
951
+ const key = `${held.symbol}:${held.leverage}`;
952
+ const price = this._currentLeveragedPrices.get(key);
953
+ if (price != null) priceArray.push([held, price]);
954
+ }
47
955
  return {
48
- supabase,
49
- auth: (0, auth_1.createAuth)(supabase),
50
- market: (0, market_1.createMarket)(supabase),
51
- strategy: (0, strategy_1.createStrategy)(supabase),
52
- portfolio: (0, portfolio_1.createPortfolio)(supabase),
956
+ value: this._portfolio.value(priceArray),
957
+ holdings: this._portfolio.holdings,
958
+ weights: this._portfolio.weights(priceArray),
959
+ pendingTrades: this._portfolio.trades(this._currentAllocation, priceArray, this._lastDate)
53
960
  };
961
+ }
962
+ };
963
+
964
+ // src/handles/strategy.ts
965
+ var nanoid = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 21);
966
+ var StrategyHandle = class {
967
+ _linkId;
968
+ _name;
969
+ _freq;
970
+ _offset;
971
+ _rules;
972
+ _storage;
973
+ _market;
974
+ _resolvedId = null;
975
+ _resolvedLinkId = null;
976
+ _resolving = null;
977
+ _allocationMap = /* @__PURE__ */ new Map();
978
+ _cache = null;
979
+ _cachedAsOf = null;
980
+ _syncing = null;
981
+ constructor(storage, market, optionsOrLinkId) {
982
+ this._storage = storage;
983
+ this._market = market;
984
+ if (typeof optionsOrLinkId === "string") {
985
+ this._linkId = optionsOrLinkId;
986
+ this._name = null;
987
+ this._freq = "Daily";
988
+ this._offset = 0;
989
+ this._rules = [];
990
+ } else {
991
+ const opts = optionsOrLinkId;
992
+ if (opts.rules.length === 0) {
993
+ throw new Error("Strategy must have at least one rule");
994
+ }
995
+ const lastRule = opts.rules[opts.rules.length - 1];
996
+ if (lastRule.when && lastRule.when.length > 0) {
997
+ throw new Error("Last rule must be a fallback (no when clause)");
998
+ }
999
+ for (let i = 0; i < opts.rules.length - 1; i++) {
1000
+ const rule = opts.rules[i];
1001
+ if (rule.when !== void 0 && rule.when.length === 0) {
1002
+ throw new Error(
1003
+ `Rule ${i} has an empty when clause and will match unconditionally, making subsequent rules unreachable`
1004
+ );
1005
+ }
1006
+ }
1007
+ this._linkId = null;
1008
+ this._name = opts.name;
1009
+ this._freq = opts.freq ?? "Daily";
1010
+ this._offset = opts.offset ?? 0;
1011
+ this._rules = opts.rules;
1012
+ }
1013
+ }
1014
+ get id() {
1015
+ if (this._resolvedId == null) throw new Error("StrategyHandle not yet resolved. Call resolve() first.");
1016
+ return this._resolvedId;
1017
+ }
1018
+ get link() {
1019
+ if (this._resolvedLinkId == null) throw new Error("StrategyHandle not yet resolved. Call resolve() first.");
1020
+ return this._resolvedLinkId;
1021
+ }
1022
+ get name() {
1023
+ return this._name;
1024
+ }
1025
+ get freq() {
1026
+ return this._freq;
1027
+ }
1028
+ get offset() {
1029
+ return this._offset;
1030
+ }
1031
+ get rules() {
1032
+ return this._rules;
1033
+ }
1034
+ async resolve() {
1035
+ if (this._resolvedId != null) return { id: this._resolvedId };
1036
+ if (!this._resolving) {
1037
+ this._resolving = this._linkId !== null && this._name === null ? this._doResolveReference() : this._doResolveCreate();
1038
+ }
1039
+ return this._resolving;
1040
+ }
1041
+ async _doResolveCreate() {
1042
+ const allSignals = /* @__PURE__ */ new Set();
1043
+ const allAllocations = /* @__PURE__ */ new Set();
1044
+ for (const rule of this._rules) {
1045
+ if (rule.when) rule.when.forEach((s) => allSignals.add(s));
1046
+ allAllocations.add(rule.hold);
1047
+ }
1048
+ await Promise.all([
1049
+ ...Array.from(allSignals).map((s) => s.resolve()),
1050
+ ...Array.from(allAllocations).map((a) => a.resolve())
1051
+ ]);
1052
+ const linkId = nanoid();
1053
+ const result = await this._storage.strategies.create({
1054
+ linkId,
1055
+ name: this._name,
1056
+ freq: this._freq,
1057
+ offset: this._offset,
1058
+ rules: this._rules.map((rule) => ({
1059
+ signalIds: (rule.when ?? []).map((s) => s.id),
1060
+ allocationId: rule.hold.id
1061
+ }))
1062
+ });
1063
+ this._resolvedId = result.id;
1064
+ this._resolvedLinkId = linkId;
1065
+ for (const rule of this._rules) {
1066
+ this._allocationMap.set(rule.hold.id, rule.hold);
1067
+ }
1068
+ return result;
1069
+ }
1070
+ async _doResolveReference() {
1071
+ const ref = await this._storage.strategies.resolveReference(this._linkId);
1072
+ this._resolvedId = ref.id;
1073
+ this._resolvedLinkId = this._linkId;
1074
+ this._name = ref.name;
1075
+ this._freq = ref.freq;
1076
+ this._offset = ref.offset;
1077
+ const tickerMap = /* @__PURE__ */ new Map();
1078
+ for (const t of ref.rules.tickers) {
1079
+ tickerMap.set(t.id, TickerHandle.fromResolved(this._storage, t.id, t.symbol, t.leverage));
1080
+ }
1081
+ const indicatorMap = /* @__PURE__ */ new Map();
1082
+ for (const ind of ref.rules.indicators) {
1083
+ const ticker = ind.tickerId ? tickerMap.get(ind.tickerId) ?? null : null;
1084
+ indicatorMap.set(
1085
+ ind.id,
1086
+ IndicatorHandle.fromResolved(this._storage, this._market, ind.id, {
1087
+ type: ind.type,
1088
+ ticker,
1089
+ lookback: ind.lookback,
1090
+ delay: ind.delay,
1091
+ unit: ind.unit,
1092
+ threshold: ind.threshold
1093
+ })
1094
+ );
1095
+ }
1096
+ const signalMap = /* @__PURE__ */ new Map();
1097
+ for (const sig of ref.rules.signals) {
1098
+ signalMap.set(
1099
+ sig.id,
1100
+ SignalHandle.fromResolved(this._storage, this._market, sig.id, {
1101
+ indicator1: indicatorMap.get(sig.indicatorId1),
1102
+ indicator2: indicatorMap.get(sig.indicatorId2),
1103
+ comparison: sig.comparison,
1104
+ tolerance: sig.tolerance
1105
+ })
1106
+ );
1107
+ }
1108
+ const allocationHandleMap = /* @__PURE__ */ new Map();
1109
+ for (const alloc of ref.rules.allocations) {
1110
+ const holdings = Object.entries(alloc.holdings).map(([key, weight]) => {
1111
+ const match = key.match(/^(.+)\?L=(.+)$/);
1112
+ const symbol = match ? match[1] : key;
1113
+ const leverage = match ? Number(match[2]) : 1;
1114
+ return [new TickerHandle(this._storage, symbol, leverage), weight];
1115
+ });
1116
+ const handle = AllocationHandle.fromResolved(this._storage, alloc.id, holdings);
1117
+ allocationHandleMap.set(alloc.id, handle);
1118
+ this._allocationMap.set(alloc.id, handle);
1119
+ }
1120
+ this._rules = ref.rules.definition.map((rule) => ({
1121
+ when: rule.signalIds && rule.signalIds.length > 0 ? rule.signalIds.map((id) => signalMap.get(id)) : void 0,
1122
+ hold: allocationHandleMap.get(rule.allocationId)
1123
+ }));
1124
+ return { id: ref.id };
1125
+ }
1126
+ async _getLatestClosedTradingDay() {
1127
+ const date = await this._storage.tradingDays.getLatestClosed();
1128
+ if (!date) throw new Error("No closed trading days found");
1129
+ return date;
1130
+ }
1131
+ async _getLatestStrategySeriesDate() {
1132
+ const { id } = await this.resolve();
1133
+ return this._storage.strategies.getLatestSeriesDate(id);
1134
+ }
1135
+ async _ensureFresh() {
1136
+ await this.resolve();
1137
+ const latestClosed = await this._getLatestClosedTradingDay();
1138
+ if (this._cachedAsOf === latestClosed) return;
1139
+ const latestSeries = await this._getLatestStrategySeriesDate();
1140
+ if (latestSeries === latestClosed) {
1141
+ this._cache = null;
1142
+ this._cachedAsOf = latestClosed;
1143
+ return;
1144
+ }
1145
+ if (!this._syncing) {
1146
+ this._syncing = this._sync(latestClosed).finally(() => {
1147
+ this._syncing = null;
1148
+ });
1149
+ }
1150
+ await this._syncing;
1151
+ this._cache = null;
1152
+ this._cachedAsOf = latestClosed;
1153
+ }
1154
+ async _sync(latestClosed) {
1155
+ const { id } = await this.resolve();
1156
+ const signalSeries = /* @__PURE__ */ new Map();
1157
+ const allSignals = /* @__PURE__ */ new Set();
1158
+ for (const rule of this._rules) {
1159
+ if (rule.when) rule.when.forEach((s) => allSignals.add(s));
1160
+ }
1161
+ await Promise.all(
1162
+ Array.from(allSignals).map(async (signal) => {
1163
+ const bars = await signal.series();
1164
+ const dateMap = /* @__PURE__ */ new Map();
1165
+ for (const bar of bars) dateMap.set(bar.date, bar.value === 1);
1166
+ signalSeries.set(signal.id, dateMap);
1167
+ })
1168
+ );
1169
+ const tradingDays = await this._storage.tradingDays.getRange();
1170
+ const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
1171
+ const allocations = [];
1172
+ const allocIndexMap = /* @__PURE__ */ new Map();
1173
+ const rulesInput = this._rules.map((rule) => {
1174
+ let allocIdx = allocIndexMap.get(rule.hold.id);
1175
+ if (allocIdx === void 0) {
1176
+ allocIdx = allocations.length;
1177
+ allocations.push(rule.hold);
1178
+ allocIndexMap.set(rule.hold.id, allocIdx);
1179
+ }
1180
+ return {
1181
+ signalIds: (rule.when ?? []).map((s) => s.id),
1182
+ allocationIndex: allocIdx
1183
+ };
1184
+ });
1185
+ const evalResult = evaluateStrategy(signalSeries, rulesInput, rebalanceDates, tradingDays);
1186
+ const entries = Array.from(evalResult.entries()).filter(([date]) => date <= latestClosed).map(([date, allocIdx]) => ({
1187
+ date,
1188
+ allocationId: allocations[allocIdx].id
1189
+ }));
1190
+ if (entries.length > 0) {
1191
+ await this._storage.strategies.writeSeries(id, entries);
1192
+ }
1193
+ }
1194
+ async _querySeriesFromDb(range) {
1195
+ const { id } = await this.resolve();
1196
+ const entries = await this._storage.strategies.getSeries(id, range);
1197
+ return entries.map((e) => ({
1198
+ date: e.date,
1199
+ allocation: this._allocationMap.get(e.allocationId)
1200
+ }));
1201
+ }
1202
+ async series(range) {
1203
+ await this._ensureFresh();
1204
+ if (this._cache && !range) return this._cache;
1205
+ const bars = await this._querySeriesFromDb(range);
1206
+ if (!range) this._cache = bars;
1207
+ return bars;
1208
+ }
1209
+ async value(date) {
1210
+ await this._ensureFresh();
1211
+ const bars = date ? await this._querySeriesFromDb({ from: date, to: date }) : await this._querySeriesFromDb();
1212
+ if (bars.length === 0) return null;
1213
+ return date ? bars[0].allocation : bars[bars.length - 1].allocation;
1214
+ }
1215
+ async simulate(options) {
1216
+ const bars = await this.series({ from: options.from, to: options.to });
1217
+ if (bars.length === 0) {
1218
+ return new SimulationHandle([], [], options.portfolio);
1219
+ }
1220
+ const prices = await this._fetchPricesForTickers(bars, options.from, options.to);
1221
+ const tradingDays = bars.map((b) => b.date);
1222
+ const rebalanceDates = computeRebalanceDates(tradingDays, this._freq, this._offset);
1223
+ rebalanceDates.add(bars[0].date);
1224
+ const result = runSimulation(bars, prices, rebalanceDates, options.portfolio);
1225
+ const lastBar = bars[bars.length - 1];
1226
+ const lastDate = lastBar.date;
1227
+ const lastAllocation = lastBar.allocation;
1228
+ const leveragedPrices = {};
1229
+ for (const [ticker, _weight] of lastAllocation.holdings) {
1230
+ if (ticker.symbol === "CASHX") continue;
1231
+ const key = `${ticker.symbol}:${ticker.leverage}`;
1232
+ const price = prices[key]?.[lastDate];
1233
+ if (price != null) leveragedPrices[key] = price;
1234
+ }
1235
+ const closePrices = {};
1236
+ await this._fetchRawClosePrices(bars, lastDate, closePrices);
1237
+ const finalState = {
1238
+ portfolio: result.finalPortfolio,
1239
+ allocation: lastAllocation,
1240
+ closePrices,
1241
+ leveragedPrices
1242
+ };
1243
+ return new SimulationHandle(result.series, result.trades, options.portfolio, finalState);
1244
+ }
1245
+ async _fetchPricesForTickers(bars, from, to) {
1246
+ const tickerMap = /* @__PURE__ */ new Map();
1247
+ for (const bar of bars) {
1248
+ for (const [ticker] of bar.allocation.holdings) {
1249
+ const key = `${ticker.symbol}:${ticker.leverage}`;
1250
+ if (!tickerMap.has(key)) {
1251
+ tickerMap.set(key, ticker);
1252
+ }
1253
+ }
1254
+ }
1255
+ const entries = await Promise.all(
1256
+ Array.from(tickerMap.entries()).map(async ([key, ticker]) => {
1257
+ const priceIndicator = new IndicatorHandle(this._storage, this._market, {
1258
+ type: "Price",
1259
+ ticker,
1260
+ lookback: 0,
1261
+ delay: 0,
1262
+ unit: null,
1263
+ threshold: null
1264
+ });
1265
+ const priceBars = await priceIndicator.series({ from, to });
1266
+ const dateMap = {};
1267
+ for (const bar of priceBars) {
1268
+ dateMap[bar.date] = bar.value;
1269
+ }
1270
+ return [key, dateMap];
1271
+ })
1272
+ );
1273
+ return Object.fromEntries(entries);
1274
+ }
1275
+ async _fetchRawClosePrices(bars, lastDate, closePrices) {
1276
+ const symbols = /* @__PURE__ */ new Set();
1277
+ for (const bar of bars) {
1278
+ for (const [ticker] of bar.allocation.holdings) {
1279
+ if (ticker.symbol !== "CASHX") symbols.add(ticker.symbol);
1280
+ }
1281
+ }
1282
+ await Promise.all(
1283
+ Array.from(symbols).map(async (symbol) => {
1284
+ const rawTicker = new TickerHandle(this._storage, symbol, 1);
1285
+ const priceIndicator = new IndicatorHandle(this._storage, this._market, {
1286
+ type: "Price",
1287
+ ticker: rawTicker,
1288
+ lookback: 0,
1289
+ delay: 0,
1290
+ unit: null,
1291
+ threshold: null
1292
+ });
1293
+ const priceBars = await priceIndicator.series({ from: lastDate, to: lastDate });
1294
+ if (priceBars.length > 0) {
1295
+ closePrices[symbol] = priceBars[0].value;
1296
+ }
1297
+ })
1298
+ );
1299
+ }
1300
+ };
1301
+
1302
+ // src/client.ts
1303
+ function tickerBound(storage, market, type, ticker, lookback, opts) {
1304
+ return new IndicatorHandle(storage, market, {
1305
+ type,
1306
+ ticker,
1307
+ lookback,
1308
+ delay: opts?.delay ?? 0,
1309
+ unit: null,
1310
+ threshold: null
1311
+ });
1312
+ }
1313
+ function standalone(storage, market, type, opts) {
1314
+ return new IndicatorHandle(storage, market, {
1315
+ type,
1316
+ ticker: null,
1317
+ lookback: 0,
1318
+ delay: opts?.delay ?? 0,
1319
+ unit: null,
1320
+ threshold: null
1321
+ });
1322
+ }
1323
+ function createClient(options) {
1324
+ const { storage, market } = options;
1325
+ return {
1326
+ ticker: (symbol, leverage) => new TickerHandle(storage, symbol, leverage),
1327
+ sma: (ticker, lookback, opts) => tickerBound(storage, market, "SMA", ticker, lookback, opts),
1328
+ ema: (ticker, lookback, opts) => tickerBound(storage, market, "EMA", ticker, lookback, opts),
1329
+ price: (ticker, opts) => tickerBound(storage, market, "Price", ticker, 0, opts),
1330
+ returns: (ticker, lookback, opts) => tickerBound(storage, market, "Return", ticker, lookback, opts),
1331
+ volatility: (ticker, lookback, opts) => tickerBound(storage, market, "Volatility", ticker, lookback, opts),
1332
+ drawdown: (ticker, lookback, opts) => tickerBound(storage, market, "Drawdown", ticker, lookback, opts),
1333
+ rsi: (ticker, lookback, opts) => tickerBound(storage, market, "RSI", ticker, lookback, opts),
1334
+ vix: (opts) => standalone(storage, market, "VIX", opts),
1335
+ vix3m: (opts) => standalone(storage, market, "VIX3M", opts),
1336
+ treasury: (tenor, opts) => standalone(storage, market, tenor, opts),
1337
+ calendar: (period, opts) => standalone(storage, market, period, opts),
1338
+ threshold: (value, unit) => new IndicatorHandle(storage, market, {
1339
+ type: "Threshold",
1340
+ ticker: null,
1341
+ lookback: 0,
1342
+ delay: 0,
1343
+ unit: unit ?? null,
1344
+ threshold: value
1345
+ }),
1346
+ gt: (ind1, ind2, tolerance) => new SignalHandle(storage, market, {
1347
+ indicator1: ind1,
1348
+ indicator2: ind2,
1349
+ comparison: ">",
1350
+ tolerance: tolerance ?? 0
1351
+ }),
1352
+ lt: (ind1, ind2, tolerance) => new SignalHandle(storage, market, {
1353
+ indicator1: ind1,
1354
+ indicator2: ind2,
1355
+ comparison: "<",
1356
+ tolerance: tolerance ?? 0
1357
+ }),
1358
+ eq: (ind1, ind2, tolerance) => new SignalHandle(storage, market, {
1359
+ indicator1: ind1,
1360
+ indicator2: ind2,
1361
+ comparison: "=",
1362
+ tolerance: tolerance ?? 0
1363
+ }),
1364
+ allocation: (...holdings) => new AllocationHandle(storage, holdings),
1365
+ portfolio: (...holdings) => new PortfolioHandle(holdings),
1366
+ strategy: (optionsOrLinkId) => new StrategyHandle(storage, market, optionsOrLinkId)
1367
+ };
54
1368
  }
1369
+ export {
1370
+ AllocationHandle,
1371
+ IndicatorHandle,
1372
+ PortfolioHandle,
1373
+ SignalHandle,
1374
+ SimulationHandle,
1375
+ StrategyHandle,
1376
+ TickerHandle,
1377
+ createClient
1378
+ };
55
1379
  //# sourceMappingURL=index.js.map