@livefolio/sdk 0.3.7 → 0.4.1

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 CHANGED
@@ -1,732 +1,3596 @@
1
- interface MarketProvider {
2
- fetchBars(symbol: string, from?: string): Promise<DailyBar[]>;
3
- }
1
+ /**
2
+ * Stable string identifier for an asset. Matches the `id` field of {@link Asset}.
3
+ * Use this type when you only need to key or compare assets without carrying
4
+ * the full {@link Asset} object.
5
+ */
6
+ type AssetId = string;
7
+ /**
8
+ * An equity instrument — common stock or ETF.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const aapl: EquityAsset = {
13
+ * kind: 'equity',
14
+ * id: 'AAPL',
15
+ * symbol: 'AAPL',
16
+ * exchange: 'NASDAQ',
17
+ * };
18
+ * ```
19
+ */
20
+ type EquityAsset = {
21
+ kind: 'equity';
22
+ /** Stable opaque ID — typically the ticker, but treat as opaque. */
23
+ id: AssetId;
24
+ /** Display symbol, e.g. `'AAPL'`. */
25
+ symbol: string;
26
+ /** MIC or common exchange name, e.g. `'NYSE'`, `'NASDAQ'`. Optional. */
27
+ exchange?: string;
28
+ };
29
+ /**
30
+ * A macroeconomic time series — e.g. a FRED series like `DGS10` (10-year
31
+ * Treasury yield) or `CPIAUCSL` (CPI, all items). Models single-value series
32
+ * as bars whose OHLC are equal to the published value.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const dgs10: MacroAsset = {
37
+ * kind: 'macro',
38
+ * id: 'DGS10',
39
+ * symbol: '10Y Treasury',
40
+ * source: 'FRED',
41
+ * };
42
+ * ```
43
+ */
44
+ type MacroAsset = {
45
+ kind: 'macro';
46
+ /** Provider-scoped series ID, e.g. `'DGS10'`, `'CPIAUCSL'`. */
47
+ id: AssetId;
48
+ /** Human-readable label, e.g. `'10Y Treasury'`, `'CPI'`. */
49
+ symbol: string;
50
+ /** Data provider tag, e.g. `'FRED'`. Optional. */
51
+ source?: string;
52
+ };
53
+ /**
54
+ * A tradeable or queryable instrument. Discriminated by `kind`. Add a new
55
+ * variant to this union when introducing a new asset class (futures, option,
56
+ * crypto, etc.); each variant is the natural narrowing point for vendor-
57
+ * specific fields.
58
+ */
59
+ type Asset = EquityAsset | MacroAsset;
60
+ /**
61
+ * Bar granularity. Determines the width of each {@link Bar} returned by
62
+ * {@link DataFeed.bars}.
63
+ *
64
+ * - `'1m'` — one-minute bars
65
+ * - `'5m'` — five-minute bars
66
+ * - `'15m'` — fifteen-minute bars
67
+ * - `'1h'` — hourly bars
68
+ * - `'1d'` — daily bars (most common for end-of-day strategies)
69
+ */
70
+ type Frequency = '1m' | '5m' | '15m' | '1h' | '1d';
71
+ /**
72
+ * Half-open calendar interval `[from, to)`. Used throughout the SDK wherever
73
+ * a date range is required. `from` is inclusive; `to` is exclusive.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import type { DateRange } from '@livefolio/sdk';
78
+ *
79
+ * const range: DateRange = {
80
+ * from: new Date('2024-01-01'),
81
+ * to: new Date('2025-01-01'),
82
+ * };
83
+ * ```
84
+ */
85
+ type DateRange = {
86
+ /** Inclusive start of the range. */
87
+ from: Date;
88
+ /** Exclusive end of the range. */
89
+ to: Date;
90
+ };
91
+ /**
92
+ * A single OHLCV bar for one asset at one point in time.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import type { Bar } from '@livefolio/sdk';
97
+ *
98
+ * // Typical daily bar for a $150 stock
99
+ * const bar: Bar = {
100
+ * t: new Date('2024-06-01'),
101
+ * open: 150.0,
102
+ * high: 153.4,
103
+ * low: 149.1,
104
+ * close: 152.7,
105
+ * volume: 8_500_000,
106
+ * };
107
+ * ```
108
+ */
109
+ type Bar = {
110
+ /** Bar timestamp (opening instant for intraday bars; midnight UTC for daily). */
111
+ t: Date;
112
+ open: number;
113
+ high: number;
114
+ low: number;
115
+ close: number;
116
+ /** Total shares traded during the bar period. */
117
+ volume: number;
118
+ };
119
+ /**
120
+ * An ordered time series of scalar values. Used as the output type of feature
121
+ * computations and as the storage unit in {@link FeatureCache}.
122
+ *
123
+ * Each element pairs a timestamp `t` with a numeric value `v`. The array is
124
+ * `ReadonlyArray`, so implementations must not mutate it after construction.
125
+ */
126
+ type Series = ReadonlyArray<{
127
+ t: Date;
128
+ v: number;
129
+ }>;
4
130
 
5
- type IndicatorType = 'Price' | 'SMA' | 'EMA' | 'RSI' | 'Return' | 'Volatility' | 'Drawdown' | 'VIX' | 'VIX3M' | 'T3M' | 'T6M' | 'T1Y' | 'T2Y' | 'T3Y' | 'T5Y' | 'T7Y' | 'T10Y' | 'T20Y' | 'T30Y' | 'Month' | 'Day of Week' | 'Day of Month' | 'Day of Year' | 'Threshold';
6
- type TradingFreq = 'Daily' | 'Weekly' | 'Monthly' | 'Bi-monthly' | 'Quarterly' | 'Every 4 Months' | 'Semiannually' | 'Yearly';
7
- type Comparison = '>' | '<' | '=';
8
- type Unit = '%' | 'bps' | 'std';
9
- interface StrategySeriesEntry {
10
- date: string;
11
- allocationId: number;
12
- }
13
- interface StrategyRuleDefinition {
14
- signalIds?: number[];
15
- allocationId: number;
16
- }
17
- interface StrategyDefinition {
18
- linkId: string;
19
- name: string;
20
- freq: TradingFreq;
21
- offset: number;
22
- rules: StrategyRuleDefinition[];
23
- }
24
- interface StrategyReferenceData {
25
- id: number;
26
- name: string;
27
- freq: TradingFreq;
28
- offset: number;
29
- rules: {
30
- signals: {
31
- id: number;
32
- indicatorId1: number;
33
- indicatorId2: number;
34
- comparison: Comparison;
35
- tolerance: number;
36
- }[];
37
- allocations: {
38
- id: number;
39
- holdings: Record<string, number>;
40
- }[];
41
- indicators: {
42
- id: number;
43
- type: IndicatorType;
44
- tickerId: number | null;
45
- lookback: number;
46
- delay: number;
47
- unit: Unit | null;
48
- threshold: number | null;
49
- }[];
50
- tickers: {
51
- id: number;
52
- symbol: string;
53
- leverage: number;
54
- }[];
55
- definition: StrategyRuleDefinition[];
131
+ /**
132
+ * Stable opaque string identifier for a {@link Position}. Generated by the
133
+ * SDK when a position is opened; callers should treat this as an opaque key
134
+ * and not parse its contents.
135
+ */
136
+ type PositionId = string;
137
+ /**
138
+ * A single open position in an asset.
139
+ *
140
+ * `basis` tracks the total cost of the position (including fees) and is used
141
+ * for realized P&L calculations. It is updated by `applyFills` as the
142
+ * position is added to or reduced.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * import type { Position } from '@livefolio/sdk';
147
+ *
148
+ * const pos: Position = {
149
+ * id: 'pos_1',
150
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
151
+ * side: 'long',
152
+ * quantity: 100,
153
+ * entry: { date: new Date('2024-01-02'), price: 185.5 },
154
+ * basis: 18_550.50, // quantity * price + fees
155
+ * tags: { strategy: 'momentum' },
156
+ * };
157
+ * ```
158
+ */
159
+ type Position = {
160
+ /** Stable opaque ID assigned when the position was opened. */
161
+ id: PositionId;
162
+ asset: Asset;
163
+ side: 'long' | 'short';
164
+ /** Current number of shares (or units) held. */
165
+ quantity: number;
166
+ /** Price and date at which this position lot was initially opened. */
167
+ entry: {
168
+ date: Date;
169
+ price: number;
56
170
  };
57
- }
58
-
59
- declare class TickerHandle {
60
- readonly symbol: string;
61
- readonly leverage: number;
62
- private _storage;
63
- private _resolvedId;
64
- private _resolving;
65
- constructor(storage: StorageProvider, symbol: string, leverage?: number);
66
- get id(): number;
67
- resolve(): Promise<{
68
- id: number;
69
- }>;
70
- static fromResolved(storage: StorageProvider, id: number, symbol: string, leverage: number): TickerHandle;
71
- private _doResolve;
72
- }
73
-
74
- interface DailyBar {
75
- date: string;
76
- value: number;
77
- }
78
- interface IndicatorIdentity {
79
- type: IndicatorType;
80
- ticker: TickerHandle | null;
81
- lookback: number;
82
- delay: number;
83
- unit: Unit | null;
84
- threshold: number | null;
85
- }
86
- interface DateRange {
87
- from?: string;
88
- to?: string;
89
- }
90
- declare class IndicatorHandle {
91
- readonly type: IndicatorType;
92
- readonly ticker: TickerHandle | null;
93
- readonly lookback: number;
94
- readonly delay: number;
95
- readonly unit: Unit | null;
96
- readonly threshold: number | null;
97
- private _storage;
98
- private _market;
99
- private _resolvedId;
100
- private _resolving;
101
- private _cachedSeries;
102
- private _cachedAsOf;
103
- private _syncing;
104
- constructor(storage: StorageProvider, market: MarketProvider, identity: IndicatorIdentity);
105
- get id(): number;
106
- resolve(): Promise<{
107
- id: number;
108
- }>;
109
- static fromResolved(storage: StorageProvider, market: MarketProvider, id: number, identity: IndicatorIdentity): IndicatorHandle;
110
- private _doResolve;
111
- private _getLatestClosedTradingDay;
112
- private _getLatestSeriesDate;
113
- private _ensureFresh;
114
- private _sync;
115
- private _fetchRawBarsForIncremental;
116
- private _upsertSeries;
117
- private _querySeriesFromDb;
118
- /**
119
- * Apply leverage compounding to a raw bar series, anchored to a stored
120
- * leveraged value. Used by both `_sync` and `computeAt` so they stay
121
- * consistent.
122
- *
123
- * `anchorDate` is the date of the last *already-stored* leveraged bar
124
- * (i.e., the bar just before `rawBars[0]`). The stored leveraged value
125
- * at that date becomes `leveraged[0]`; raw returns are then compounded
126
- * forward for each subsequent bar.
127
- *
128
- * If no stored anchor exists (first-ever sync), falls back to rawBars[0]
129
- * as the starting raw value — identical to `_sync`'s behaviour.
130
- */
131
- private _applyLeverage;
132
- /**
133
- * Compute the indicator's value at `date` without persisting anything, with
134
- * optional live-quote `overrides` keyed by raw market symbol (the same symbol
135
- * space `MarketProvider.fetchBars` uses — ticker symbols for Price/SMA/etc.,
136
- * `^VIX` / `^VIX3M` for macro, FRED series IDs like `DGS3MO` for Treasury).
137
- *
138
- * Bars for the underlying symbol are resolved storage-first when the market
139
- * hasn't yet produced bars for `date` (trading day still open), and storage
140
- * is the fallback whenever the remote fetch fails — see `_resolveRawBars`.
141
- *
142
- * For Threshold: returns the threshold constant. For calendar types: computed
143
- * from `tradingDays.getRange()`. For all others: `_resolveRawBars` → leverage
144
- * compounding (if any) → lookback-specific computation. Returns null if the
145
- * value cannot be computed.
146
- */
147
- computeAt(date: string, overrides?: Record<string, number>): Promise<number | null>;
148
- /**
149
- * Raw (unleveraged) bars for `symbol` up through `date`, with the live quote
150
- * from `overrides[symbol]` (if any) spliced in at `date`.
151
- *
152
- * Decision policy:
153
- * - `date` > `tradingDays.getLatestClosed()`: market has nothing for that
154
- * day yet — skip the remote fetch entirely and read from storage.
155
- * - otherwise: try `this._market.fetchBars(symbol, from)`. On failure, fall
156
- * back to storage — upstream HTTP providers (Yahoo / FRED) are flaky.
157
- *
158
- * After the base is resolved, `overrides[symbol]` is spliced at `date`
159
- * (replaces the existing bar, or is appended in-order). When no override is
160
- * present but `date` isn't in the base bars, the last known value is carried
161
- * forward to `date` — this preserves the fallbackMissingQuotes behaviour the
162
- * old overlay exposed so leverage compounding / computations always have a
163
- * point at `date` to land on.
164
- */
165
- private _resolveRawBars;
166
- /**
167
- * Resolve the single raw (unleveraged) value for `symbol` at `date`.
168
- * Returns the override directly when present; otherwise delegates to
169
- * `_resolveRawBars` with a one-day window and picks the matching bar.
170
- */
171
- private _resolveRawBarAt;
172
- /**
173
- * Resolve raw (unleveraged) bars for a market symbol from storage. Maps:
174
- * - `^VIX` → the VIX indicator's stored series
175
- * - `^VIX3M` → the VIX3M indicator's stored series
176
- * - `DGS*` → the matching Treasury-tenor indicator's stored series
177
- * - anything else → the `Price` indicator for that ticker symbol with
178
- * `leverage = 1` (the raw contract that `MarketProvider.fetchBars` has).
179
- *
180
- * Returns `[]` when the resolved indicator has no stored bars yet.
181
- */
182
- private _readStoredBars;
183
- series(range?: DateRange): Promise<DailyBar[]>;
184
- private _syntheticThresholdSeries;
185
- value(date?: string): Promise<number | null>;
186
- /**
187
- * Read-only preview of the indicator series with an in-memory bar at `date`
188
- * computed via `computeAt` with the supplied live-quote `overrides`. Does
189
- * NOT write to `indicators_series`. Safe to call before market close.
190
- *
191
- * @param date - Target trading day whose value is computed in-memory.
192
- * Must be in `tradingDays.getRange()`.
193
- * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
194
- * Symbols omitted fall back to the last known value (see `_resolveRawBars`).
195
- * @param range - Optional filter applied to the returned bars.
196
- * @returns Stored historical bars plus (or with) today's in-memory value.
197
- */
198
- previewSeries(date: string, overrides: Record<string, number>, range?: DateRange): Promise<DailyBar[]>;
199
- }
171
+ /**
172
+ * Total cost basis of the current quantity in base currency
173
+ * (fill price × shares + fees at entry, adjusted on partial closes).
174
+ */
175
+ basis: number;
176
+ /** Optional key-value labels for strategy attribution or downstream reporting. */
177
+ tags?: Record<string, unknown>;
178
+ };
179
+ /**
180
+ * A point-in-time snapshot of the full portfolio state.
181
+ *
182
+ * `t` advances to the latest fill timestamp each time {@link applyFills} is
183
+ * called. It is used as the portfolio's logical "now" for downstream
184
+ * computations.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * import type { Portfolio } from '@livefolio/sdk';
189
+ *
190
+ * const portfolio: Portfolio = {
191
+ * cash: 50_000,
192
+ * positions: [],
193
+ * t: new Date('2024-01-01'),
194
+ * };
195
+ * ```
196
+ */
197
+ type Portfolio = {
198
+ /** Uninvested cash in the portfolio's base currency. */
199
+ cash: number;
200
+ /** All currently open positions. Immutable array — replaced (not mutated) by apply functions. */
201
+ positions: ReadonlyArray<Position>;
202
+ /** Logical timestamp of the most recent fill (or the portfolio's start date). */
203
+ t: Date;
204
+ };
200
205
 
201
- interface StorageProvider {
202
- tickers: {
203
- upsert(symbol: string, leverage: number): Promise<{
204
- id: number;
205
- }>;
206
- findOrCreate(symbol: string, leverage: number): Promise<{
207
- id: number;
208
- }>;
209
- };
210
- indicators: {
211
- upsert(identity: {
212
- type: string;
213
- tickerId: number | null;
214
- lookback: number;
215
- delay: number;
216
- unit: string | null;
217
- threshold: number | null;
218
- }): Promise<{
219
- id: number;
220
- }>;
221
- findOrCreate(identity: {
222
- type: string;
223
- tickerId: number | null;
224
- lookback: number;
225
- delay: number;
226
- unit: string | null;
227
- threshold: number | null;
228
- }): Promise<{
229
- id: number;
230
- }>;
231
- getSeries(indicatorId: number, range?: DateRange): Promise<DailyBar[]>;
232
- writeSeries(indicatorId: number, bars: DailyBar[], opts?: {
233
- metadata?: unknown;
234
- }): Promise<void>;
235
- getLatestSeriesDate(indicatorId: number): Promise<string | null>;
236
- getValue(indicatorId: number, date?: string): Promise<number | null>;
237
- getLatestBar(indicatorId: number): Promise<{
238
- date: string;
239
- value: number;
240
- metadata: unknown;
241
- } | null>;
242
- };
243
- signals: {
244
- upsert(identity: {
245
- indicatorId1: number;
246
- indicatorId2: number;
247
- comparison: string;
248
- tolerance: number;
249
- }): Promise<{
250
- id: number;
251
- }>;
252
- findOrCreate(identity: {
253
- indicatorId1: number;
254
- indicatorId2: number;
255
- comparison: string;
256
- tolerance: number;
257
- }): Promise<{
258
- id: number;
259
- }>;
260
- getSeries(signalId: number, range?: DateRange): Promise<DailyBar[]>;
261
- writeSeries(signalId: number, bars: DailyBar[]): Promise<void>;
262
- getLatestSeriesDate(signalId: number): Promise<string | null>;
263
- getLastValue(signalId: number): Promise<number | null>;
264
- };
265
- allocations: {
266
- findOrCreate(holdings: Record<string, number>): Promise<{
267
- id: number;
268
- }>;
269
- };
270
- strategies: {
271
- create(definition: StrategyDefinition): Promise<{
272
- id: number;
273
- }>;
274
- getSeries(strategyId: number, range?: DateRange): Promise<StrategySeriesEntry[]>;
275
- writeSeries(strategyId: number, entries: StrategySeriesEntry[]): Promise<void>;
276
- getLatestSeriesDate(strategyId: number): Promise<string | null>;
277
- getLatestAllocationId(strategyId: number): Promise<number | null>;
278
- resolveReference(linkId: string): Promise<StrategyReferenceData>;
206
+ /**
207
+ * Opens a new position in `asset`. The executor creates a fresh {@link Position}
208
+ * entry and debits cash by `quantity * fillPrice + fees`.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * import type { OpenOrder } from '@livefolio/sdk';
213
+ *
214
+ * const order: OpenOrder = {
215
+ * id: 'ord_001',
216
+ * kind: 'open',
217
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
218
+ * side: 'long',
219
+ * quantity: 100,
220
+ * tag: 'momentum-entry',
221
+ * };
222
+ * ```
223
+ */
224
+ type OpenOrder = {
225
+ /** Caller-supplied identifier carried back on the resulting {@link Fill} via `orderRef`. */
226
+ id: string;
227
+ kind: 'open';
228
+ /** The instrument to buy (long) or sell short. */
229
+ asset: Asset;
230
+ /** `'long'` to buy; `'short'` to sell short. */
231
+ side: 'long' | 'short';
232
+ /** Number of shares (or units) to transact. Must be positive. */
233
+ quantity: number;
234
+ /** Optional label propagated to the resulting {@link Position} for analysis. */
235
+ tag?: string;
236
+ };
237
+ /**
238
+ * Closes an existing position identified by `positionId`. If `quantity` is
239
+ * supplied, only that many shares are closed (partial close); omitting
240
+ * `quantity` closes the entire position.
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * import type { CloseOrder } from '@livefolio/sdk';
245
+ *
246
+ * const order: CloseOrder = {
247
+ * id: 'ord_002',
248
+ * kind: 'close',
249
+ * positionId: 'pos_1',
250
+ * };
251
+ * ```
252
+ */
253
+ type CloseOrder = {
254
+ /** Caller-supplied identifier carried back on the resulting {@link Fill} via `orderRef`. */
255
+ id: string;
256
+ kind: 'close';
257
+ /** ID of the {@link Position} to close. */
258
+ positionId: PositionId;
259
+ /**
260
+ * Shares to close. Omit to close the full position. Must not exceed
261
+ * the position's current quantity.
262
+ */
263
+ quantity?: number;
264
+ };
265
+ /**
266
+ * Adjusts fields of an existing position without fully closing it. Currently
267
+ * supports changing `quantity` (e.g. after a corporate action). Cash is not
268
+ * affected other than deducting fees.
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * import type { AdjustOrder } from '@livefolio/sdk';
273
+ *
274
+ * const order: AdjustOrder = {
275
+ * id: 'ord_003',
276
+ * kind: 'adjust',
277
+ * positionId: 'pos_1',
278
+ * changes: { quantity: 150 },
279
+ * };
280
+ * ```
281
+ */
282
+ type AdjustOrder = {
283
+ /** Caller-supplied identifier carried back on the resulting {@link Fill} via `orderRef`. */
284
+ id: string;
285
+ kind: 'adjust';
286
+ /** ID of the {@link Position} to modify. */
287
+ positionId: PositionId;
288
+ /**
289
+ * Fields to update. Currently only `quantity` is supported. Omitting a
290
+ * field leaves it unchanged.
291
+ */
292
+ changes: {
293
+ quantity?: number;
279
294
  };
280
- tradingDays: {
281
- getRange(range?: DateRange): Promise<string[]>;
282
- getLatestClosed(): Promise<string | null>;
295
+ };
296
+ /**
297
+ * Adjusts a long position in `asset` by `delta` shares. Positive `delta`
298
+ * increases the position (buy); negative `delta` decreases it (sell). If no
299
+ * position exists and `delta > 0`, a new position is opened.
300
+ *
301
+ * Used by the rebalance engine when transitioning a portfolio to target
302
+ * weights — strategy code typically does not construct these directly.
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * import type { RebalanceOrder } from '@livefolio/sdk';
307
+ *
308
+ * const order: RebalanceOrder = {
309
+ * id: 'ord_004',
310
+ * kind: 'rebalance',
311
+ * asset: { kind: 'equity', id: 'MSFT', symbol: 'MSFT' },
312
+ * delta: -50, // reduce long by 50 shares
313
+ * };
314
+ * ```
315
+ */
316
+ type RebalanceOrder = {
317
+ /** Caller-supplied identifier carried back on the resulting {@link Fill} via `orderRef`. */
318
+ id: string;
319
+ kind: 'rebalance';
320
+ /** The instrument whose long position is being adjusted. */
321
+ asset: Asset;
322
+ /**
323
+ * Share delta. Positive → buy more; negative → sell (reduce or close).
324
+ * Zero-delta orders MUST be omitted by callers.
325
+ */
326
+ delta: number;
327
+ };
328
+ /**
329
+ * Discriminated union of all order types. Narrow on `order.kind` to access
330
+ * kind-specific fields.
331
+ *
332
+ * Variants:
333
+ * - `'open'` — {@link OpenOrder}: opens a new long or short position.
334
+ * - `'close'` — {@link CloseOrder}: closes an existing position fully or partially.
335
+ * - `'adjust'` — {@link AdjustOrder}: mutates fields of an existing position.
336
+ * - `'rebalance'` — {@link RebalanceOrder}: delta-adjusts a long position; used by the
337
+ * rebalance engine.
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * import type { Order } from '@livefolio/sdk';
342
+ *
343
+ * function describe(order: Order): string {
344
+ * switch (order.kind) {
345
+ * case 'open': return `open ${order.side} ${order.quantity} ${order.asset.symbol}`;
346
+ * case 'close': return `close position ${order.positionId}`;
347
+ * case 'adjust': return `adjust position ${order.positionId}`;
348
+ * case 'rebalance': return `rebalance ${order.asset.symbol} by ${order.delta}`;
349
+ * }
350
+ * }
351
+ * ```
352
+ */
353
+ type Order = OpenOrder | CloseOrder | AdjustOrder | RebalanceOrder;
354
+ /**
355
+ * Execution confirmation returned by {@link Executor.submit}. Each fill
356
+ * corresponds to one order and records the exact price, quantity, and fees
357
+ * that were transacted.
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * import type { Fill } from '@livefolio/sdk';
362
+ *
363
+ * const fill: Fill = {
364
+ * orderRef: 'ord_001',
365
+ * t: new Date('2024-06-04T13:30:00Z'),
366
+ * quantity: 100,
367
+ * price: 152.75,
368
+ * fees: 0.50,
369
+ * };
370
+ * ```
371
+ */
372
+ type Fill = {
373
+ /** References the `id` of the originating {@link Order}. */
374
+ orderRef: string;
375
+ /** Timestamp at which the fill was executed (>= the order submission time). */
376
+ t: Date;
377
+ /** Shares (or units) actually transacted. May be less than the ordered quantity for partial fills. */
378
+ quantity: number;
379
+ /** Per-share execution price after slippage. */
380
+ price: number;
381
+ /** Total transaction fees in the portfolio's base currency. */
382
+ fees: number;
383
+ };
384
+
385
+ /**
386
+ * Constraint on the feature map type parameter used throughout the strategy API.
387
+ * A `Features` object is a plain, readonly record that maps string keys to arbitrary
388
+ * computed values. Keeping it generic (rather than forcing `Record<string, number>`)
389
+ * lets callers attach structured objects, series snapshots, or price maps alongside
390
+ * numeric scalars.
391
+ */
392
+ type Features = Readonly<Record<string, unknown>>;
393
+ /**
394
+ * The contract that every allocation strategy must implement.
395
+ *
396
+ * The runtime loop calls these methods in order on every rebalance session:
397
+ *
398
+ * 1. `universe` — decide which assets are tradeable today.
399
+ * 2. `features` — compute any indicators or derived values needed for the decision.
400
+ * 3. `build` — emit a list of orders (and the next state value) given the feature snapshot.
401
+ *
402
+ * Invariants an implementation MUST uphold:
403
+ * - `universe` must be synchronous and cheap; it is called before any I/O.
404
+ * - `features` may be async and is responsible for all data fetching and
405
+ * indicator computation. The returned `F` value is passed verbatim to `build`.
406
+ * - `build` must be synchronous. It receives the current portfolio, the
407
+ * full feature snapshot, and the carried-over state, and returns zero or more
408
+ * orders plus the next state value. Returning an empty array is a valid no-op
409
+ * (no rebalance).
410
+ * - None of the three methods should have side effects on shared mutable state
411
+ * between calls; the portfolio is the single source of truth for position state.
412
+ *
413
+ * The `F` type parameter lets TypeScript verify that features produced by
414
+ * `features()` exactly match what `build()` consumes, eliminating a whole class
415
+ * of runtime key-mismatch bugs.
416
+ *
417
+ * The `S` type parameter (defaults to `void`) enables explicit state threading.
418
+ * When `S = void`, the strategy is state-less: `initialState` is omitted, `build`
419
+ * receives `undefined` for `state`, and may return either a legacy `ReadonlyArray<Order>`
420
+ * or the new `{ orders, state: undefined }` form. When `S` is a concrete type,
421
+ * `initialState()` is required and `build` must return `{ orders, state }` so the
422
+ * runtime can carry the state forward without closures — enabling snapshot/restore
423
+ * for preview-builds in live mode.
424
+ *
425
+ * @example State-less strategy (legacy shape — S = void):
426
+ * ```ts
427
+ * import type { Strategy, Features } from '@livefolio/sdk';
428
+ *
429
+ * type MyFeatures = { spy_sma20: number; spy_price: number } & Features;
430
+ *
431
+ * const myStrategy: Strategy<MyFeatures> = {
432
+ * universe: (_t, _portfolio) => [{ kind: 'equity', id: 'US:SPY', symbol: 'SPY' }],
433
+ *
434
+ * features: async (_universe, _portfolio, _t) => ({
435
+ * spy_sma20: 432.5,
436
+ * spy_price: 440.0,
437
+ * }),
438
+ *
439
+ * build: (features, _portfolio, _state, _t) => {
440
+ * if (features.spy_price > features.spy_sma20) {
441
+ * // buy signal — delegate actual order creation to reconcile()
442
+ * }
443
+ * return [];
444
+ * },
445
+ * };
446
+ * ```
447
+ *
448
+ * @example
449
+ * State-bearing strategy (`S = { lastBar: number }`):
450
+ * ```ts
451
+ * import type { Strategy, Features } from '@livefolio/sdk';
452
+ *
453
+ * type MyFeatures = { price: number } & Features;
454
+ * type MyState = { lastBar: number };
455
+ *
456
+ * const myStrategy: Strategy<MyFeatures, MyState> = {
457
+ * universe: () => [{ kind: 'equity', id: 'US:SPY', symbol: 'SPY' }],
458
+ * features: async () => ({ price: 440 }),
459
+ * initialState: () => ({ lastBar: 0 }),
460
+ * build: (features, _portfolio, state, _t) => ({
461
+ * orders: [],
462
+ * state: { lastBar: features.price },
463
+ * }),
464
+ * };
465
+ * ```
466
+ */
467
+ interface Strategy<F extends Features = Features, S = void> {
468
+ /**
469
+ * Returns the set of assets that are eligible for trading on date `t`.
470
+ *
471
+ * The universe may change dynamically based on the current portfolio or
472
+ * calendar date (e.g. exclusion lists, liquidity filters). Assets returned
473
+ * here are the ones for which `features` will fetch data.
474
+ *
475
+ * @param t - The session date (midnight UTC on the trading day).
476
+ * @param portfolio - The portfolio state carried into this session.
477
+ * @returns A readonly array of `Asset` descriptors. May be empty if no assets
478
+ * are eligible; `build` will then receive no prices and should return `[]`.
479
+ */
480
+ universe(t: Date, portfolio: Portfolio): ReadonlyArray<Asset>;
481
+ /**
482
+ * Computes the feature snapshot used by `build` to make allocation decisions.
483
+ *
484
+ * This is the only async step in the strategy loop. Implementations typically
485
+ * call `FeatureRuntime.compute` for each indicator, which handles caching and
486
+ * data fetching transparently.
487
+ *
488
+ * @param universe - The assets returned by `universe()` for this session.
489
+ * @param portfolio - The portfolio state at the start of this session.
490
+ * @param t - The session date.
491
+ * @returns A feature object of type `F`, or a promise that resolves to one.
492
+ * The object is passed unchanged to `build`.
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * features: async (universe, _portfolio, t) => {
497
+ * const prices = await Promise.all(
498
+ * universe.map(async (asset) => {
499
+ * const s = await runtime.compute({ kind: 'price' }, asset);
500
+ * return [asset.id, seriesAt(s, t)] as const;
501
+ * }),
502
+ * );
503
+ * return { prices: new Map(prices) };
504
+ * }
505
+ * ```
506
+ */
507
+ features(universe: ReadonlyArray<Asset>, portfolio: Portfolio, t: Date): F | Promise<F>;
508
+ /**
509
+ * Returns the initial auxiliary state for this strategy. Optional — when
510
+ * omitted, the runtime treats the strategy as state-less (`S = void`) and
511
+ * passes `undefined` for `state` in every `build` call.
512
+ *
513
+ * Required for any strategy that holds hysteresis or other carry-over state
514
+ * across rebalance steps. The returned value MUST be JSON-serializable so the
515
+ * runtime can snapshot/restore it (preview-build during live mode) and so
516
+ * `BacktestResult.finalState` round-trips through any persistence layer.
517
+ */
518
+ initialState?(): S;
519
+ /**
520
+ * Translates the feature snapshot into orders and the next state value.
521
+ *
522
+ * Must be synchronous. Returning an empty `orders` array is valid and means
523
+ * "hold current positions unchanged". Typically delegates weight calculation
524
+ * to `evaluateRuleTree` and order construction to `reconcile`.
525
+ *
526
+ * For state-less strategies (`S = void`), the legacy return shape
527
+ * `ReadonlyArray<Order>` is still accepted — the runtime normalizes both forms.
528
+ * New code should always return `{ orders, state }`.
529
+ *
530
+ * @param features - The value returned by `features()` for this session.
531
+ * @param portfolio - The portfolio state at the start of this session.
532
+ * @param state - The state carried over from the previous session. `undefined`
533
+ * when `S = void`.
534
+ * @param t - The session date.
535
+ * @returns Either a readonly array of `Order` objects (legacy state-less form)
536
+ * or `{ orders: ReadonlyArray<Order>; state: S }` (new state-bearing form).
537
+ * The executor receives the orders and converts them into `Fill` records that
538
+ * update the portfolio.
539
+ */
540
+ build(features: F, portfolio: Portfolio, state: S, t: Date): ReadonlyArray<Order> | {
541
+ orders: ReadonlyArray<Order>;
542
+ state: S;
283
543
  };
284
544
  }
285
545
 
286
- interface SignalIdentity {
287
- indicator1: IndicatorHandle;
288
- indicator2: IndicatorHandle;
289
- comparison: Comparison;
290
- tolerance: number;
291
- }
292
- declare class SignalHandle {
293
- readonly indicator1: IndicatorHandle;
294
- readonly indicator2: IndicatorHandle;
295
- readonly comparison: Comparison;
296
- readonly tolerance: number;
297
- private _storage;
298
- private _resolvedId;
299
- private _resolving;
300
- private _cachedSeries;
301
- private _cachedAsOf;
302
- private _syncing;
303
- constructor(storage: StorageProvider, _market: MarketProvider, identity: SignalIdentity);
304
- get id(): number;
305
- resolve(): Promise<{
306
- id: number;
307
- }>;
308
- static fromResolved(storage: StorageProvider, market: MarketProvider, id: number, identity: SignalIdentity): SignalHandle;
309
- private _doResolve;
310
- private _getLatestClosedTradingDay;
311
- private _getLatestSignalSeriesDate;
312
- private _getLastSignalValue;
313
- private _ensureFresh;
314
- private _isSingleBarFastPath;
315
- private _sync;
316
- private _evaluateOneBar;
317
- private _upsertSeries;
318
- private _querySeriesFromDb;
319
- /**
320
- * Compute the signal's boolean value at `date` without persisting anything,
321
- * with optional live-quote `overrides` that are routed through each
322
- * indicator's `computeAt`. Returns null if either indicator cannot produce
323
- * a value at `date`.
324
- *
325
- * @param prevBool - The signal's boolean value at the bar immediately
326
- * preceding `date`, used for hysteresis when `tolerance > 0`. If not
327
- * provided, falls back to `storage.signals.getLastValue` (suitable for
328
- * standalone callers). On the preview path `_evaluate` passes this from
329
- * the in-memory `dateMap` so we never read stale storage.
330
- */
331
- computeAt(date: string, overrides?: Record<string, number>, prevBool?: boolean | null): Promise<boolean | null>;
332
- series(range?: DateRange): Promise<DailyBar[]>;
333
- value(date?: string): Promise<number | null>;
334
- /**
335
- * Read-only preview of the signal series with an in-memory bar at `date`
336
- * computed via `computeAt` with the supplied live-quote `overrides`. Does
337
- * NOT write to `signals_series`.
338
- *
339
- * @param date - Target trading day whose boolean is computed in-memory.
340
- * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
341
- * @param range - Optional filter applied to the returned bars.
342
- */
343
- previewSeries(date: string, overrides: Record<string, number>, range?: DateRange): Promise<DailyBar[]>;
344
- }
546
+ /**
547
+ * Maps each asset ID to its desired portfolio weight as a fraction of total
548
+ * portfolio value (e.g. `0.6` means 60 %). Weights need not sum to 1; any
549
+ * residual becomes cash. Passing a weight of `0` or omitting an asset entirely
550
+ * will generate a full exit order for any existing long position in that asset.
551
+ */
552
+ type TargetWeights = ReadonlyMap<AssetId, number>;
553
+ /**
554
+ * Maps each asset ID to its current market price. Required for every asset that
555
+ * appears in `TargetWeights` and for every asset currently held in the portfolio.
556
+ * `reconcile` throws if a target asset has no corresponding price entry.
557
+ */
558
+ type PriceMap = ReadonlyMap<AssetId, number>;
559
+ /**
560
+ * Converts target portfolio weights into a minimal set of `RebalanceOrder`
561
+ * instructions that move the current portfolio toward the desired allocation.
562
+ *
563
+ * The algorithm:
564
+ * 1. Compute total portfolio value as `cash + Σ(held_shares × price)`.
565
+ * 2. For each target asset, derive `targetShares = floor(totalValue × weight / price)`.
566
+ * 3. Emit a `RebalanceOrder` with `delta = targetShares - heldShares` for any
567
+ * asset where the delta is non-zero (positive = buy, negative = sell).
568
+ * 4. Emit exit orders (`delta = -heldShares`) for any long position in an asset
569
+ * that does not appear in `targets`.
570
+ *
571
+ * Only long positions are considered — short positions in the portfolio are
572
+ * ignored. Share counts are always floored to integer lots.
573
+ *
574
+ * @param targets - Desired weight per asset. Keys are `AssetId` strings. A weight
575
+ * of `0` is valid and will result in a full exit for any existing position.
576
+ * @param portfolio - Current portfolio. Cash and long positions determine total
577
+ * value and existing share counts.
578
+ * @param prices - Current prices for all assets that appear in `targets` or are
579
+ * currently held. Throws `Error` if a target asset is missing from this map.
580
+ * @returns A readonly array of `RebalanceOrder` objects. The array may be empty
581
+ * if the portfolio is already at the target allocation. Order IDs are
582
+ * deterministic within a single call (`rebal_<assetId>_<counter>`).
583
+ *
584
+ * @example
585
+ * ```ts
586
+ * import { reconcile } from '@livefolio/sdk';
587
+ *
588
+ * const targets = new Map([
589
+ * ['US:SPY', 0.6],
590
+ * ['US:BND', 0.4],
591
+ * ]);
592
+ * const prices = new Map([
593
+ * ['US:SPY', 440.0],
594
+ * ['US:BND', 75.0],
595
+ * ]);
596
+ *
597
+ * // Empty portfolio with $100 000 cash
598
+ * const portfolio = { cash: 100_000, positions: [] };
599
+ * const orders = reconcile(targets, portfolio, prices);
600
+ * // orders => [
601
+ * // { id: 'rebal_US:SPY_0', kind: 'rebalance', asset: { ... }, delta: 136 },
602
+ * // { id: 'rebal_US:BND_1', kind: 'rebalance', asset: { ... }, delta: 533 },
603
+ * // ]
604
+ * ```
605
+ */
606
+ declare function reconcile(targets: TargetWeights, portfolio: Portfolio, prices: PriceMap): ReadonlyArray<RebalanceOrder>;
345
607
 
346
- declare class AllocationHandle {
347
- readonly holdings: [TickerHandle, number][];
348
- private _storage;
349
- private _resolvedId;
350
- private _resolving;
351
- constructor(storage: StorageProvider, holdings: [TickerHandle, number][]);
352
- get id(): number;
353
- resolve(): Promise<{
354
- id: number;
355
- }>;
356
- static fromResolved(storage: StorageProvider, id: number, holdings: [TickerHandle, number][]): AllocationHandle;
357
- toJSON(): Array<{
358
- symbol: string;
359
- leverage: number;
360
- weight: number;
361
- }>;
362
- private _doResolve;
608
+ /**
609
+ * A flat record of fundamental data points for an asset at a point in time.
610
+ * Values may be numeric (e.g. P/E ratio), string (e.g. sector name), or
611
+ * `null` when a data provider does not carry that field.
612
+ *
613
+ * @example
614
+ * ```ts
615
+ * import type { Fundamentals } from '@livefolio/sdk';
616
+ *
617
+ * const f: Fundamentals = {
618
+ * peRatio: 28.5,
619
+ * sector: 'Technology',
620
+ * debtEquity: null, // not available for this provider
621
+ * };
622
+ * ```
623
+ */
624
+ type Fundamentals = Readonly<Record<string, number | string | null>>;
625
+ /**
626
+ * Categories of corporate events emitted by {@link DataFeed.events}.
627
+ *
628
+ * - `'earnings'` — quarterly/annual earnings announcement
629
+ * - `'dividend'` — cash or stock dividend declaration
630
+ * - `'split'` — forward or reverse stock split
631
+ * - `'corporate-action'`— catch-all for other actions (mergers, spin-offs, etc.)
632
+ */
633
+ type EventKind = 'earnings' | 'dividend' | 'split' | 'corporate-action';
634
+ /**
635
+ * A single corporate event affecting an asset.
636
+ *
637
+ * The `payload` shape is event-kind-specific and defined by the data provider.
638
+ * Callers should narrow on `kind` before reading `payload` fields.
639
+ *
640
+ * @example
641
+ * ```ts
642
+ * import type { DataEvent } from '@livefolio/sdk';
643
+ *
644
+ * const event: DataEvent = {
645
+ * kind: 'dividend',
646
+ * t: new Date('2024-02-09'),
647
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
648
+ * payload: { amount: 0.24, currency: 'USD', exDate: '2024-02-09' },
649
+ * };
650
+ * ```
651
+ */
652
+ type DataEvent = {
653
+ kind: EventKind;
654
+ /** Effective date of the event (ex-date for dividends, announcement date for earnings). */
655
+ t: Date;
656
+ asset: Asset;
657
+ /** Event-kind-specific fields. Shape is defined by the data provider. */
658
+ payload: Readonly<Record<string, unknown>>;
659
+ };
660
+ /**
661
+ * Market-data source. Provides price bars, fundamentals, and corporate events.
662
+ *
663
+ * Implementations MUST guarantee:
664
+ * - `bars` yields {@link Bar} objects in **ascending `t` order**. Gaps (e.g.
665
+ * non-trading days) MUST be omitted rather than filled with synthetic bars.
666
+ * - `bars` respects the half-open interval: the first bar's `t` is `>= range.from`
667
+ * and the last bar's `t` is `< range.to`.
668
+ * - `fundamentals` (optional) returns a snapshot as of `t`; returning `undefined`
669
+ * is valid when no data is available.
670
+ * - `events` (optional) yields events in ascending `t` order, filtered to the
671
+ * requested `kinds`.
672
+ *
673
+ * Reference implementations: use `vi.fn()` in tests or provide a typed mock
674
+ * that returns pre-seeded bar arrays.
675
+ *
676
+ * @example
677
+ * ```ts
678
+ * import type { DataFeed, Asset, DateRange } from '@livefolio/sdk';
679
+ * import { vi } from 'vitest';
680
+ *
681
+ * const feed: DataFeed = {
682
+ * bars: vi.fn().mockImplementation(async function* () {
683
+ * yield { t: new Date('2024-01-02'), open: 100, high: 102, low: 99, close: 101, volume: 1_000_000 };
684
+ * }),
685
+ * };
686
+ * ```
687
+ */
688
+ interface DataFeed {
689
+ /**
690
+ * Streams price bars for `asset` over the half-open interval
691
+ * `[range.from, range.to)` at the requested `freq` granularity.
692
+ *
693
+ * Bars MUST be yielded in ascending `t` order. Non-trading periods MUST be
694
+ * omitted (sparse output is expected and normal).
695
+ *
696
+ * @param asset - The instrument to fetch bars for.
697
+ * @param range - Half-open date range; `range.from` inclusive, `range.to` exclusive.
698
+ * @param freq - Bar width. `'1d'` returns one bar per trading day.
699
+ * @returns An async iterable of {@link Bar} objects.
700
+ */
701
+ bars(asset: Asset, range: DateRange, freq: Frequency): AsyncIterable<Bar>;
702
+ /**
703
+ * Returns a snapshot of fundamental data for `asset` as of `t`.
704
+ * Optional — not all data providers carry fundamentals.
705
+ *
706
+ * @param asset - The instrument to query.
707
+ * @param t - The point-in-time date for the snapshot.
708
+ * @returns A flat record of fundamental values, or `undefined` if unavailable.
709
+ */
710
+ fundamentals?(asset: Asset, t: Date): Promise<Fundamentals>;
711
+ /**
712
+ * Streams corporate events within `range` filtered to the requested
713
+ * `kinds`. Optional — providers that do not carry event data may omit this.
714
+ *
715
+ * Events MUST be yielded in ascending `t` order.
716
+ *
717
+ * @param range - Half-open date range.
718
+ * @param kinds - Event categories to include.
719
+ * @returns An async iterable of {@link DataEvent} objects.
720
+ */
721
+ events?(range: DateRange, kinds: ReadonlyArray<EventKind>): AsyncIterable<DataEvent>;
363
722
  }
364
723
 
365
- declare class PortfolioHandle {
366
- readonly holdings: [TickerHandle, number][];
367
- constructor(holdings: [TickerHandle, number][]);
368
- private _priceMap;
369
- private _priceFor;
370
- value(prices: [TickerHandle, number][]): number;
371
- weights(prices: [TickerHandle, number][]): [TickerHandle, number][];
372
- trades(target: AllocationHandle, prices: [TickerHandle, number][], date: string): Trade[];
724
+ /**
725
+ * Order-routing layer. Translates a set of {@link Order} objects into
726
+ * confirmed {@link Fill} records.
727
+ *
728
+ * Implementations MUST guarantee:
729
+ * - `submit` returns one {@link Fill} per order that was (at least partially)
730
+ * executed. Orders that produce zero fills (e.g. quantity of 0) MUST be
731
+ * omitted from the result rather than emitting a zero-quantity fill.
732
+ * - `submit` MUST be **idempotent per unique order id**: submitting the same
733
+ * `Order` array twice with the same ids MUST NOT double-fill positions.
734
+ * - Fill timestamps (`fill.t`) MUST be `>= t`.
735
+ * - The function is `async`; implementations that need to call a broker API
736
+ * or await next-bar price data should resolve only after execution is
737
+ * confirmed or simulated.
738
+ *
739
+ * Reference implementation: {@link BacktestExecutor} — fills at next-open with
740
+ * configurable slippage and per-share fees.
741
+ *
742
+ * @example
743
+ * ```ts
744
+ * import type { Executor, Order, Fill, Portfolio } from '@livefolio/sdk';
745
+ * import { vi } from 'vitest';
746
+ *
747
+ * const executor: Executor = {
748
+ * submit: vi.fn().mockResolvedValue([] as ReadonlyArray<Fill>),
749
+ * };
750
+ * ```
751
+ */
752
+ interface Executor {
753
+ /**
754
+ * Submits a batch of orders for execution and returns the resulting fills.
755
+ *
756
+ * @param orders - The orders to execute. Each order has a unique `id`;
757
+ * implementations use `id` to correlate fills via `fill.orderRef`.
758
+ * @param t - The logical "now" timestamp at which orders are being
759
+ * submitted (e.g. end-of-day for a daily-rebalance strategy).
760
+ * @param portfolio - The current portfolio state at time `t`. Implementations
761
+ * may use this to resolve position details for close/adjust orders.
762
+ * @returns A readonly array of {@link Fill} records. One fill per executed
763
+ * order; omit orders that result in zero quantity.
764
+ */
765
+ submit(orders: ReadonlyArray<Order>, t: Date, portfolio: Portfolio): Promise<ReadonlyArray<Fill>>;
373
766
  }
374
767
 
375
- interface MetricsOptions {
376
- riskFreeRate?: number;
377
- topDrawdowns?: number;
378
- varConfidence?: number;
379
- }
380
- interface DrawdownEntry {
381
- peakDate: string;
382
- troughDate: string;
383
- recoveryDate: string | null;
384
- depth: number;
385
- durationDays: number;
386
- underwaterDays: number;
768
+ /**
769
+ * Wall-clock time of day, expressed in local exchange time.
770
+ *
771
+ * @example
772
+ * ```ts
773
+ * import type { TimeOfDay } from '@livefolio/sdk';
774
+ *
775
+ * const marketOpen: TimeOfDay = { h: 9, m: 30 }; // 09:30 NYSE
776
+ * const earlyClose: TimeOfDay = { h: 13, m: 0 }; // 13:00 on early-close days
777
+ * ```
778
+ */
779
+ type TimeOfDay = {
780
+ h: number;
781
+ m: number;
782
+ };
783
+ /**
784
+ * The open and close instants for a single trading session.
785
+ *
786
+ * `date` is midnight of the session day (used as a stable key).
787
+ * `open` and `close` are the exact UTC instants the exchange accepts orders.
788
+ *
789
+ * @example
790
+ * ```ts
791
+ * import type { Session } from '@livefolio/sdk';
792
+ *
793
+ * // A normal NYSE session
794
+ * const session: Session = {
795
+ * date: new Date('2024-06-03'),
796
+ * open: new Date('2024-06-03T13:30:00Z'), // 09:30 ET in UTC
797
+ * close: new Date('2024-06-03T20:00:00Z'), // 16:00 ET in UTC
798
+ * };
799
+ * ```
800
+ */
801
+ type Session = {
802
+ date: Date;
803
+ open: Date;
804
+ close: Date;
805
+ };
806
+ /**
807
+ * Exchange calendar — the single source of truth for trading-day arithmetic.
808
+ *
809
+ * Implementations MUST guarantee:
810
+ * - `isOpen(t)` returns `true` only for instants strictly within a session
811
+ * `[session.open, session.close)`.
812
+ * - `next(t)` returns the midnight-UTC Date of the **next** trading day after
813
+ * `t`; it MUST never return a weekend or holiday.
814
+ * - `previous(t)` is the symmetric inverse of `next`.
815
+ * - `sessions(range)` returns one Date per trading day in the half-open
816
+ * interval `[range.from, range.to)`, in ascending order.
817
+ * - `schedule(range)` returns the same dates as `sessions` but enriched with
818
+ * `open`/`close` instants for each session.
819
+ * - `isEarlyClose(t)` returns `true` if the session containing (or nearest to)
820
+ * `t` ends before the exchange's normal close time.
821
+ *
822
+ * Reference implementation: {@link NYSEExchangeCalendar}, {@link LSEExchangeCalendar}.
823
+ *
824
+ * @example
825
+ * ```ts
826
+ * import { NYSEExchangeCalendar } from '@livefolio/sdk';
827
+ *
828
+ * const cal = new NYSEExchangeCalendar();
829
+ * const today = new Date('2024-11-29'); // Black Friday (early close)
830
+ * console.log(cal.isEarlyClose(today)); // true
831
+ * const next = cal.next(today);
832
+ * console.log(next.toISOString()); // 2024-12-02T00:00:00.000Z
833
+ * ```
834
+ */
835
+ interface Calendar {
836
+ /**
837
+ * Returns `true` if `t` falls inside an open trading session for this
838
+ * exchange (i.e. between session open and session close, inclusive of open,
839
+ * exclusive of close).
840
+ *
841
+ * @param t - The instant to test.
842
+ */
843
+ isOpen(t: Date): boolean;
844
+ /**
845
+ * Returns the midnight-UTC Date of the next trading day strictly after `t`.
846
+ * Skips weekends, exchange holidays, and any days with no scheduled session.
847
+ *
848
+ * @param t - Reference date. If `t` itself is a trading day the result is
849
+ * the *following* trading day, not `t`.
850
+ */
851
+ next(t: Date): Date;
852
+ /**
853
+ * Returns the midnight-UTC Date of the most recent trading day strictly
854
+ * before `t`. Symmetric inverse of {@link Calendar.next}.
855
+ *
856
+ * @param t - Reference date.
857
+ */
858
+ previous(t: Date): Date;
859
+ /**
860
+ * Returns the trading days (as midnight-UTC Dates) within the half-open
861
+ * interval `[range.from, range.to)`, in ascending order.
862
+ *
863
+ * @param range - Half-open date range; `range.from` is inclusive, `range.to`
864
+ * is exclusive.
865
+ * @returns Ascending array of trading-day Dates. Empty if no trading days
866
+ * fall in the range.
867
+ *
868
+ * @example
869
+ * ```ts
870
+ * import { NYSEExchangeCalendar } from '@livefolio/sdk';
871
+ *
872
+ * const cal = new NYSEExchangeCalendar();
873
+ * const days = cal.sessions({
874
+ * from: new Date('2024-12-23'),
875
+ * to: new Date('2025-01-02'),
876
+ * });
877
+ * // days.length === 5 (skips Christmas, Boxing Day is US-only partial, New Year's)
878
+ * ```
879
+ */
880
+ sessions(range: DateRange): ReadonlyArray<Date>;
881
+ /**
882
+ * Returns full {@link Session} records (date, open instant, close instant)
883
+ * for every trading day in the half-open interval `[range.from, range.to)`.
884
+ *
885
+ * @param range - Half-open date range.
886
+ * @returns Ascending array of {@link Session} objects. Early-close days have
887
+ * a `close` earlier than the normal session close.
888
+ */
889
+ schedule(range: DateRange): ReadonlyArray<Session>;
890
+ /**
891
+ * Returns `true` if the exchange closes early on the day containing `t`.
892
+ * Common examples: Black Friday (US), Christmas Eve, New Year's Eve.
893
+ *
894
+ * @param t - The instant or day to test.
895
+ */
896
+ isEarlyClose(t: Date): boolean;
387
897
  }
388
- interface MonthlyReturnsTable {
389
- rows: Array<{
390
- year: number;
391
- months: (number | null)[];
392
- ytd: number | null;
393
- }>;
898
+
899
+ /**
900
+ * Identifies what an indicator was computed over — either a single asset or
901
+ * a whole universe.
902
+ *
903
+ * - `{ kind: 'asset'; asset: AssetId }` — the indicator was computed for a
904
+ * specific instrument (e.g. 20-day SMA for AAPL).
905
+ * - `{ kind: 'universe'; universeHash: string }` — the indicator covers the
906
+ * full universe (e.g. cross-sectional momentum rank). `universeHash` is a
907
+ * content-hash of the sorted asset-id list so that cache keys survive
908
+ * universe reordering.
909
+ */
910
+ type FeatureScope = {
911
+ kind: 'asset';
912
+ asset: AssetId;
913
+ } | {
914
+ kind: 'universe';
915
+ universeHash: string;
916
+ };
917
+ /**
918
+ * Content-addressed cache key for a feature computation result.
919
+ *
920
+ * Every field participates in key equality — changing any one of them
921
+ * addresses a different cache entry. Use this type when building custom
922
+ * {@link FeatureCache} implementations.
923
+ *
924
+ * @example
925
+ * ```ts
926
+ * import type { FeatureKey } from '@livefolio/sdk';
927
+ *
928
+ * const key: FeatureKey = {
929
+ * feature: 'sma',
930
+ * paramsHash: 'abc123', // hash of { window: 20 }
931
+ * scope: { kind: 'asset', asset: 'AAPL' },
932
+ * range: { from: new Date('2024-01-01'), to: new Date('2025-01-01') },
933
+ * freq: '1d',
934
+ * };
935
+ * ```
936
+ */
937
+ type FeatureKey = {
938
+ /** Feature name, e.g. `'sma'`, `'rsi'`. */
939
+ feature: string;
940
+ /** Deterministic hash of the feature's parameter object. */
941
+ paramsHash: string;
942
+ /** Asset or universe this result covers. */
943
+ scope: FeatureScope;
944
+ /** The date range the cached series spans. */
945
+ range: DateRange;
946
+ /** Bar granularity used when computing the feature. */
947
+ freq: Frequency;
948
+ };
949
+ /**
950
+ * Content-addressed cache for feature computation results.
951
+ *
952
+ * Implementations MUST guarantee:
953
+ * - `get` returns the exact `Series` previously stored under `key`, or
954
+ * `undefined` if no entry exists. It MUST NOT return a stale or partially
955
+ * overlapping series.
956
+ * - `set` stores `series` under `key` and makes it immediately available to
957
+ * subsequent `get` calls.
958
+ * - `invalidate` (optional) removes all entries whose key fields match the
959
+ * supplied `prefix`. Partial matches (specifying only `feature`, for example)
960
+ * MUST invalidate every key that shares those fields, regardless of the
961
+ * remaining fields.
962
+ * - All methods are `async`; implementations backed by in-process Maps may
963
+ * resolve synchronously via `Promise.resolve`.
964
+ *
965
+ * Reference implementation: {@link MemoryFeatureCache} — in-process Map,
966
+ * no eviction, suitable for single-run backtests.
967
+ *
968
+ * @example
969
+ * ```ts
970
+ * import { MemoryFeatureCache } from '@livefolio/sdk';
971
+ * import type { FeatureKey, Series } from '@livefolio/sdk';
972
+ *
973
+ * const cache = new MemoryFeatureCache();
974
+ * const key: FeatureKey = {
975
+ * feature: 'sma',
976
+ * paramsHash: 'abc123',
977
+ * scope: { kind: 'asset', asset: 'AAPL' },
978
+ * range: { from: new Date('2024-01-01'), to: new Date('2025-01-01') },
979
+ * freq: '1d',
980
+ * };
981
+ * const series: Series = [{ t: new Date('2024-01-02'), v: 150.5 }];
982
+ *
983
+ * await cache.set(key, series);
984
+ * const hit = await cache.get(key); // same reference
985
+ * ```
986
+ */
987
+ interface FeatureCache {
988
+ /**
989
+ * Retrieves the cached {@link Series} for `key`, or `undefined` on a cache
990
+ * miss.
991
+ *
992
+ * @param key - Fully-qualified cache key.
993
+ * @returns The stored series, or `undefined` if not present.
994
+ */
995
+ get(key: FeatureKey): Promise<Series | undefined>;
996
+ /**
997
+ * Stores `series` under `key`. Overwrites any existing entry at that key.
998
+ *
999
+ * @param key - Fully-qualified cache key.
1000
+ * @param series - The computed series to store. Must not be mutated after
1001
+ * passing to `set`.
1002
+ */
1003
+ set(key: FeatureKey, series: Series): Promise<void>;
1004
+ /**
1005
+ * Removes all cache entries whose keys match the supplied `prefix`.
1006
+ * Optional — implementations that do not support invalidation may omit this.
1007
+ *
1008
+ * Matching is field-by-field: only the fields present in `prefix` are
1009
+ * compared; all others are treated as wildcards.
1010
+ *
1011
+ * @param prefix - Partial {@link FeatureKey}. Omitted fields match any value.
1012
+ */
1013
+ invalidate?(prefix: Partial<FeatureKey>): Promise<void>;
394
1014
  }
395
- interface MetricsResult {
396
- range: {
397
- from: string;
398
- to: string;
399
- years: number;
400
- };
401
- returns: {
402
- totalReturn: number;
403
- cagr: number;
404
- bestYear: {
405
- year: number;
406
- return: number;
407
- } | null;
408
- worstYear: {
409
- year: number;
410
- return: number;
411
- } | null;
412
- bestMonth: {
413
- date: string;
414
- return: number;
415
- } | null;
416
- worstMonth: {
417
- date: string;
418
- return: number;
419
- } | null;
420
- pctPositiveMonths: number;
421
- };
422
- risk: {
423
- volatility: number;
424
- downsideDeviation: number;
425
- maxDrawdown: DrawdownEntry;
426
- currentDrawdown: number;
427
- ulcerIndex: number;
428
- skew: number;
429
- kurtosis: number;
430
- var95: number;
431
- cvar95: number;
432
- };
433
- riskAdjusted: {
434
- sharpe: number;
435
- sortino: number;
436
- calmar: number;
437
- };
438
- activity: {
439
- rebalances: number;
440
- trades: number;
441
- turnover: number;
442
- winRate: number;
443
- };
444
- tables: {
445
- drawdowns: DrawdownEntry[];
446
- monthly: MonthlyReturnsTable;
447
- yearly: Array<{
448
- year: number;
449
- return: number;
450
- }>;
451
- };
1015
+
1016
+ /**
1017
+ * The OHLCV field of a `Bar` that should be used when converting a bar array
1018
+ * into a scalar time series. Defaults to `'close'` throughout the feature
1019
+ * pipeline unless overridden via `FeatureRuntimeOptions.field`.
1020
+ *
1021
+ * Variants: `'open'` | `'high'` | `'low'` | `'close'` | `'volume'`.
1022
+ */
1023
+ type BarField = 'open' | 'high' | 'low' | 'close' | 'volume';
1024
+ /**
1025
+ * Drains an `AsyncIterable<Bar>` into a plain `Bar[]` array.
1026
+ *
1027
+ * Used internally by `FeatureRuntime` to materialise the stream returned by
1028
+ * `DataFeed.bars` before passing it to indicator functions that expect a
1029
+ * fully-in-memory array. The resulting array is in the same order as the
1030
+ * iterable yields (typically chronological).
1031
+ *
1032
+ * @param it - Any `AsyncIterable<Bar>`, such as the return value of `DataFeed.bars`.
1033
+ * @returns A promise that resolves to a `Bar[]` containing every bar yielded
1034
+ * by the iterable, in iteration order.
1035
+ *
1036
+ * @example
1037
+ * ```ts
1038
+ * import { collectBars } from '@livefolio/sdk';
1039
+ *
1040
+ * const bars = await collectBars(dataFeed.bars(asset, range, '1d'));
1041
+ * // bars is now a Bar[] — safe to index, slice, and pass to barsToSeries
1042
+ * ```
1043
+ */
1044
+ declare function collectBars(it: AsyncIterable<Bar>): Promise<Bar[]>;
1045
+ /**
1046
+ * Converts an array of OHLCV bars into a `Series` by extracting a single
1047
+ * numeric field from each bar.
1048
+ *
1049
+ * The resulting `Series` is a readonly array of `{ t: Date; v: number }` points
1050
+ * in the same order as `bars`. The timestamp `t` is taken directly from `bar.t`,
1051
+ * and `v` is the value of `field` for that bar.
1052
+ *
1053
+ * @param bars - Source OHLCV bars in chronological order.
1054
+ * @param field - Which OHLCV field to extract. Defaults to `'close'`.
1055
+ * @returns A `Series` of the same length as `bars`. Returns an empty array when
1056
+ * `bars` is empty.
1057
+ *
1058
+ * @example
1059
+ * ```ts
1060
+ * import { collectBars, barsToSeries } from '@livefolio/sdk';
1061
+ *
1062
+ * const bars = await collectBars(dataFeed.bars(asset, range, '1d'));
1063
+ * const closeSeries = barsToSeries(bars); // default: 'close'
1064
+ * const volumeSeries = barsToSeries(bars, 'volume');
1065
+ * ```
1066
+ */
1067
+ declare function barsToSeries(bars: ReadonlyArray<Bar>, field?: BarField): Series;
1068
+ /**
1069
+ * Looks up the value at or immediately before timestamp `t` in a sorted `Series`.
1070
+ *
1071
+ * Uses binary search to find the largest index `i` where `series[i].t <= t`.
1072
+ * This is the standard "as-of" lookup used throughout the strategy loop to read
1073
+ * the most recent indicator value available on a given session date without
1074
+ * peeking into the future.
1075
+ *
1076
+ * @param series - A `Series` sorted in ascending timestamp order. Behaviour is
1077
+ * undefined for unsorted input.
1078
+ * @param t - The target date. May fall between two data points or exactly on one.
1079
+ * @returns The `v` value of the last data point at or before `t`, or `undefined`
1080
+ * if the series is empty or every point comes after `t`.
1081
+ *
1082
+ * @example
1083
+ * ```ts
1084
+ * import { seriesAt } from '@livefolio/sdk';
1085
+ *
1086
+ * const series = [
1087
+ * { t: new Date('2023-01-02'), v: 380.0 },
1088
+ * { t: new Date('2023-01-03'), v: 385.0 },
1089
+ * { t: new Date('2023-01-04'), v: 390.0 },
1090
+ * ];
1091
+ *
1092
+ * seriesAt(series, new Date('2023-01-03')); // => 385.0 (exact match)
1093
+ * seriesAt(series, new Date('2023-01-03T12:00:00Z')); // => 385.0 (between points)
1094
+ * seriesAt(series, new Date('2023-01-01')); // => undefined (before all points)
1095
+ * ```
1096
+ */
1097
+ declare function seriesAt(series: Series, t: Date): number | undefined;
1098
+
1099
+ /**
1100
+ * Controls whether `returnSeries` computes percentage or absolute returns.
1101
+ *
1102
+ * - `'pct'` (default) — percentage return: `(curr - prev) / prev`. The result is
1103
+ * a dimensionless ratio (e.g. `0.05` means +5 %).
1104
+ * - `'abs'` — absolute price difference: `curr - prev`. The result is in the same
1105
+ * units as the input price series.
1106
+ */
1107
+ type ReturnMode = 'pct' | 'abs';
1108
+ /**
1109
+ * Computes a rolling period return over a price series.
1110
+ *
1111
+ * Math definition:
1112
+ * ```
1113
+ * // Percentage (default):
1114
+ * return[i] = (series[i] - series[i - period]) / series[i - period]
1115
+ *
1116
+ * // Absolute:
1117
+ * return[i] = series[i] - series[i - period]
1118
+ * ```
1119
+ *
1120
+ * Warmup: the first `period` bars are consumed as the lookback window for the
1121
+ * first output. The first output point corresponds to input index `period`.
1122
+ * The output array is shorter than the input (no `undefined` placeholders).
1123
+ *
1124
+ * Edge cases:
1125
+ * - `period <= 0` — throws `Error`.
1126
+ * - `series.length <= period` — returns `[]` (needs strictly more than `period` bars).
1127
+ * - `mode === 'pct'` with `prev === 0` — produces `Infinity` or `NaN`; callers
1128
+ * should filter or guard against zero-price series.
1129
+ *
1130
+ * @param series - Input price series sorted in ascending timestamp order.
1131
+ * @param period - Lookback distance in bars. A value of `1` gives a single-bar
1132
+ * (daily) return; larger values give multi-bar returns.
1133
+ * @param mode - Whether to return a percentage ratio or an absolute difference.
1134
+ * Defaults to `'pct'`.
1135
+ * @returns A `Series` of length `max(0, series.length - period)`. Each point's
1136
+ * timestamp `t` is taken from `series[i]` (the later of the two bars).
1137
+ *
1138
+ * @example
1139
+ * ```ts
1140
+ * import { returnSeries } from '@livefolio/sdk';
1141
+ *
1142
+ * const prices = [
1143
+ * { t: new Date('2023-01-02'), v: 100 },
1144
+ * { t: new Date('2023-01-03'), v: 105 },
1145
+ * { t: new Date('2023-01-04'), v: 110 },
1146
+ * ];
1147
+ *
1148
+ * const pct = returnSeries(prices, 1);
1149
+ * // pct[0] => { t: new Date('2023-01-03'), v: 0.05 } // (105-100)/100
1150
+ * // pct[1] => { t: new Date('2023-01-04'), v: ~0.048 } // (110-105)/105
1151
+ *
1152
+ * const abs = returnSeries(prices, 1, 'abs');
1153
+ * // abs[0] => { t: new Date('2023-01-03'), v: 5 } // 105-100
1154
+ * // abs[1] => { t: new Date('2023-01-04'), v: 5 } // 110-105
1155
+ *
1156
+ * // Two-bar return
1157
+ * const twoBar = returnSeries(prices, 2);
1158
+ * // twoBar[0] => { t: new Date('2023-01-04'), v: 0.1 } // (110-100)/100
1159
+ * ```
1160
+ */
1161
+ declare function returnSeries(series: Series, period: number, mode?: ReturnMode): Series;
1162
+
1163
+ /**
1164
+ * A discriminated union describing every built-in feature kind and its parameters.
1165
+ *
1166
+ * Each variant has a `kind` field that identifies the indicator together with the
1167
+ * parameters that fully determine its output. `FeatureSpec` objects are used as
1168
+ * cache keys (via `paramsHash`) and as dispatch tokens (via `getFeatureCompute`).
1169
+ *
1170
+ * Variants:
1171
+ * - `{ kind: 'price' }` — raw price series; no parameters.
1172
+ * - `{ kind: 'sma'; period: number }` — simple moving average over `period` bars.
1173
+ * - `{ kind: 'ema'; period: number }` — exponential moving average seeded from an SMA.
1174
+ * - `{ kind: 'rsi'; period: number }` — Wilder's Relative Strength Index.
1175
+ * - `{ kind: 'return'; period: number; mode?: ReturnMode }` — period return; percent or absolute.
1176
+ * - `{ kind: 'volatility'; period: number }` — rolling population standard deviation of daily returns.
1177
+ * - `{ kind: 'drawdown'; period: number }` — drawdown relative to the rolling maximum.
1178
+ */
1179
+ type FeatureSpec = {
1180
+ kind: 'price';
1181
+ } | {
1182
+ kind: 'sma';
1183
+ period: number;
1184
+ } | {
1185
+ kind: 'ema';
1186
+ period: number;
1187
+ } | {
1188
+ kind: 'rsi';
1189
+ period: number;
1190
+ } | {
1191
+ kind: 'return';
1192
+ period: number;
1193
+ mode?: ReturnMode;
1194
+ } | {
1195
+ kind: 'volatility';
1196
+ period: number;
1197
+ } | {
1198
+ kind: 'drawdown';
1199
+ period: number;
1200
+ };
1201
+ /**
1202
+ * String literal union of all valid feature `kind` values derived from `FeatureSpec`.
1203
+ *
1204
+ * Useful for typing registry keys and dispatch tables without manually listing all
1205
+ * variants: `'price' | 'sma' | 'ema' | 'rsi' | 'return' | 'volatility' | 'drawdown'`.
1206
+ */
1207
+ type FeatureKind = FeatureSpec['kind'];
1208
+ /**
1209
+ * A pure function that computes a feature `Series` from an input price `Series` and the typed
1210
+ * `FeatureSpec` describing the indicator's parameters. Implementations must be deterministic and
1211
+ * side-effect free — the SDK uses the function as a content-addressed dispatch token via
1212
+ * {@link getFeatureCompute} and as the registration target for {@link defineFeature}.
1213
+ *
1214
+ * @param series - Input price series (typically the asset's close prices).
1215
+ * @param spec - Typed indicator spec carrying the parameters relevant to `kind`.
1216
+ * @returns The computed feature series, aligned to `series` on `t`.
1217
+ */
1218
+ type ComputeFn = (series: Series, spec: FeatureSpec) => Series;
1219
+ /**
1220
+ * Registers a compute function for a new or existing feature kind.
1221
+ *
1222
+ * Throws if `kind` is already registered. Call this once at module initialisation
1223
+ * time (top-level) to extend the built-in feature registry with custom indicators.
1224
+ * The compute function receives the raw price `Series` and the full typed spec
1225
+ * object for that kind; it must return a `Series` of the same or shorter length.
1226
+ *
1227
+ * @param kind - The `FeatureKind` string that identifies this indicator.
1228
+ * @param compute - Pure function that transforms a price series according to `spec`.
1229
+ * The `spec` argument is narrowed to `Extract<FeatureSpec, { kind: K }>` so
1230
+ * TypeScript enforces that only the correct parameter shape is accessed.
1231
+ * @returns `void`. Registration is a side-effectful, one-time operation.
1232
+ *
1233
+ * @example
1234
+ * ```ts
1235
+ * import { defineFeature } from '@livefolio/sdk';
1236
+ *
1237
+ * // Register a custom 'zscore' feature kind
1238
+ * defineFeature('sma', (series, spec) => {
1239
+ * // Already built-in; this would throw due to duplicate registration.
1240
+ * // Shown here for illustration only.
1241
+ * return series;
1242
+ * });
1243
+ * ```
1244
+ */
1245
+ declare function defineFeature<K extends FeatureKind>(kind: K, compute: (series: Series, spec: Extract<FeatureSpec, {
1246
+ kind: K;
1247
+ }>) => Series): void;
1248
+ /**
1249
+ * Retrieves the registered compute function for a given feature kind.
1250
+ *
1251
+ * Throws `Error` if the kind has not been registered. In normal usage the
1252
+ * built-in `defineFeature` calls at the bottom of this module pre-populate the
1253
+ * registry, so this function only throws for custom kinds that were never registered.
1254
+ *
1255
+ * @param kind - The `FeatureKind` string to look up.
1256
+ * @returns The `ComputeFn` registered for `kind`.
1257
+ *
1258
+ * @example
1259
+ * ```ts
1260
+ * import { getFeatureCompute } from '@livefolio/sdk';
1261
+ *
1262
+ * const computeSma = getFeatureCompute('sma');
1263
+ * const result = computeSma(priceSeries, { kind: 'sma', period: 20 });
1264
+ * ```
1265
+ */
1266
+ declare function getFeatureCompute(kind: FeatureKind): ComputeFn;
1267
+ /**
1268
+ * Returns a deterministic string that depends only on the spec's logical
1269
+ * content — key order and undefined optional fields are normalized away.
1270
+ *
1271
+ * The same logical spec always produces the same hash regardless of how the
1272
+ * object was constructed (different key insertion order, explicit `undefined`
1273
+ * vs. omitted optional field). This string is used as the `paramsHash` field
1274
+ * in `FeatureKey` to ensure cache-hit equivalence for semantically identical
1275
+ * specs.
1276
+ *
1277
+ * Callers depend on this function's contract (same logical content → same
1278
+ * result), not on the encoding. Future replacement with SHA-256 is
1279
+ * non-breaking.
1280
+ *
1281
+ * @param spec - The `FeatureSpec` object to hash.
1282
+ * @returns A JSON string with sorted keys and no undefined values.
1283
+ *
1284
+ * @example
1285
+ * ```ts
1286
+ * import { paramsHash } from '@livefolio/sdk';
1287
+ *
1288
+ * paramsHash({ kind: 'sma', period: 20 });
1289
+ * // => '{"kind":"sma","period":20}'
1290
+ *
1291
+ * // Optional field omitted vs explicitly undefined — same result:
1292
+ * paramsHash({ kind: 'return', period: 10 });
1293
+ * paramsHash({ kind: 'return', period: 10, mode: undefined });
1294
+ * // both => '{"kind":"return","period":10}'
1295
+ * ```
1296
+ */
1297
+ declare function paramsHash(spec: FeatureSpec): string;
1298
+
1299
+ /**
1300
+ * Configuration for a `FeatureRuntime` instance.
1301
+ *
1302
+ * Accepts two shapes — select via the `mode` discriminant:
1303
+ *
1304
+ * - **`'historical'`** (default, `mode` may be omitted): range-bounded backtest
1305
+ * mode. Bars are fetched from `DataFeed` once per asset and cached in memory.
1306
+ * - **`'streaming'`**: open-ended live mode. No fixed `range`; bars are pushed
1307
+ * in via `appendBar`. Indicator computation reads from the growing in-process
1308
+ * buffer instead of calling `DataFeed.bars`.
1309
+ *
1310
+ * The historical variant is backward-compatible — existing callers that omit
1311
+ * `mode` compile and behave identically to before.
1312
+ */
1313
+ type FeatureRuntimeOptions = {
1314
+ /** Selects historical (range-bounded) mode. May be omitted; defaults to
1315
+ * `'historical'`. */
1316
+ mode?: 'historical';
1317
+ /** The market-data source used to fetch OHLCV bars. */
1318
+ dataFeed: DataFeed;
1319
+ /**
1320
+ * Persistent indicator cache. Use `MemoryFeatureCache` for a single backtest
1321
+ * run, or supply a cross-process cache implementation to share results across
1322
+ * multiple runs or processes.
1323
+ */
1324
+ featureCache: FeatureCache;
1325
+ /**
1326
+ * The date range over which bars are fetched. This should span at least the
1327
+ * backtest range plus any indicator warmup period (e.g. `period - 1` extra
1328
+ * bars for SMA/EMA). `FeatureRuntime` does not automatically extend the range
1329
+ * for warmup — the caller is responsible for providing enough history.
1330
+ */
1331
+ range: DateRange;
1332
+ /**
1333
+ * Bar frequency forwarded to `DataFeed.bars` and embedded in cache keys.
1334
+ * Must match the granularity expected by the indicators (e.g. `'1d'` for
1335
+ * daily SMA/EMA).
1336
+ */
1337
+ freq: Frequency;
1338
+ /**
1339
+ * Which OHLCV field to use as the scalar price series. Defaults to `'close'`.
1340
+ * All indicators within a single `FeatureRuntime` instance share the same field.
1341
+ */
1342
+ field?: BarField;
1343
+ } | {
1344
+ /** Selects streaming (open-ended live) mode. Required. */
1345
+ mode: 'streaming';
1346
+ /** Optional — not called in streaming mode. Omit in streaming-only usages.
1347
+ * If provided it is stored but never invoked; supply only when sharing an
1348
+ * options object that also carries a `DataFeed` for other purposes. */
1349
+ dataFeed?: DataFeed;
1350
+ /**
1351
+ * Persistent indicator cache. In streaming mode the cache is bypassed
1352
+ * entirely — every `compute` call recomputes from the in-memory bar buffer.
1353
+ * The instance is still required so that a shared cache object can be
1354
+ * passed without special-casing at the call site.
1355
+ */
1356
+ featureCache: FeatureCache;
1357
+ /**
1358
+ * Bar frequency embedded in cache keys. Must match the granularity of the
1359
+ * bars pushed via `appendBar`.
1360
+ */
1361
+ freq: Frequency;
1362
+ /**
1363
+ * Which OHLCV field to use as the scalar price series. Defaults to `'close'`.
1364
+ */
1365
+ field?: BarField;
1366
+ /**
1367
+ * Optional seed bars per asset (keyed by `AssetId`), used to bootstrap the
1368
+ * streaming buffer from a prior historical run's `BacktestResult`.
1369
+ * Bars must already be in ascending `t` order per asset.
1370
+ */
1371
+ initialBars?: ReadonlyMap<AssetId, ReadonlyArray<Bar>>;
1372
+ };
1373
+ /**
1374
+ * Orchestrates indicator computation for a single backtest run or a live
1375
+ * streaming session.
1376
+ *
1377
+ * `FeatureRuntime` is the bridge between raw OHLCV data (via `DataFeed`) and the
1378
+ * typed indicator functions registered in the feature registry. It handles:
1379
+ *
1380
+ * - **Bar fetching** (historical mode) — Calls `DataFeed.bars` once per
1381
+ * `(asset, range, freq)` tuple and caches the resulting `Series` in memory for
1382
+ * the lifetime of the instance. Concurrent calls for the same asset share a
1383
+ * single in-flight promise; there is no redundant fetching even if `compute` is
1384
+ * called from multiple `Promise.all` branches simultaneously.
1385
+ * - **Bar buffering** (streaming mode) — Bars are pushed in via `appendBar`.
1386
+ * `compute` reads directly from the in-memory buffer; `DataFeed.bars` is never
1387
+ * called.
1388
+ * - **Indicator dispatch** — Delegates computation to the function registered via
1389
+ * `defineFeature` for the given `FeatureSpec.kind`.
1390
+ * - **Persistent caching** (historical mode only) — Checks `FeatureCache` before
1391
+ * computing. On a miss, the result is stored in the cache. Subsequent calls with
1392
+ * the same `(spec, asset)` combination return instantly from cache without
1393
+ * re-fetching bars.
1394
+ *
1395
+ * Caching semantics:
1396
+ * - Historical mode: cache keys incorporate `spec`, `asset.id`, `range`, and `freq`.
1397
+ * Results are read from and written to `featureCache`.
1398
+ * - Streaming mode: `featureCache` is bypassed entirely — every `compute` call
1399
+ * recomputes from the growing in-memory bar buffer. The `seriesCache` (per-asset
1400
+ * in-memory base-series cache) is the only cache layer; it is invalidated on each
1401
+ * `appendBar` so the next `compute` sees the updated buffer.
1402
+ * - Calling `appendBar` invalidates the in-memory series cache for that asset so
1403
+ * the next `compute` call rebuilds the series from the updated buffer.
1404
+ *
1405
+ * @example Historical mode (default)
1406
+ * ```ts
1407
+ * import {
1408
+ * FeatureRuntime,
1409
+ * MemoryFeatureCache,
1410
+ * seriesAt,
1411
+ * } from '@livefolio/sdk';
1412
+ *
1413
+ * const runtime = new FeatureRuntime({
1414
+ * dataFeed,
1415
+ * featureCache: new MemoryFeatureCache(),
1416
+ * range: { from: new Date('2022-01-01'), to: new Date('2023-12-31') },
1417
+ * freq: '1d',
1418
+ * });
1419
+ *
1420
+ * const spy = { kind: 'equity' as const, id: 'US:SPY', symbol: 'SPY' };
1421
+ * const smaSeries = await runtime.compute({ kind: 'sma', period: 20 }, spy);
1422
+ * const latestSma = seriesAt(smaSeries, new Date('2023-06-15'));
1423
+ * // => number | undefined
1424
+ * ```
1425
+ *
1426
+ * @example Streaming mode
1427
+ * ```ts
1428
+ * const runtime = new FeatureRuntime({
1429
+ * featureCache: new MemoryFeatureCache(),
1430
+ * mode: 'streaming',
1431
+ * freq: '1d',
1432
+ * initialBars, // optional seed from BacktestResult
1433
+ * });
1434
+ *
1435
+ * runtime.appendBar(spy, latestBar);
1436
+ * const smaSeries = await runtime.compute({ kind: 'sma', period: 20 }, spy);
1437
+ * ```
1438
+ */
1439
+ declare class FeatureRuntime {
1440
+ private readonly mode;
1441
+ private readonly dataFeed;
1442
+ private readonly featureCache;
1443
+ private readonly range;
1444
+ private readonly freq;
1445
+ private readonly field;
1446
+ /** Per-asset bar buffer used in streaming mode. */
1447
+ private readonly streamingBars;
1448
+ /** Per-asset bar buffer accumulated in historical mode after bars are fetched from DataFeed. */
1449
+ private readonly historicalBars;
1450
+ /** Per-asset in-flight Series promise — shared across concurrent `compute` calls
1451
+ * for the same asset to avoid redundant bar fetching (historical) or rebuilding
1452
+ * (streaming). Invalidated on `appendBar`. */
1453
+ private readonly seriesCache;
1454
+ constructor(opts: FeatureRuntimeOptions);
1455
+ /**
1456
+ * Appends a bar to the streaming buffer for the given asset.
1457
+ *
1458
+ * Bars must be provided in non-decreasing `t` order per asset. A bar with
1459
+ * the same `t` as the most recent buffered bar replaces it in place — this
1460
+ * is the mark-to-market wiggle path used by `runLive`, where each incoming
1461
+ * tick updates the running open/high/low/close of the in-flight session bar.
1462
+ * A bar with `t` strictly greater than the buffered tail starts a fresh
1463
+ * in-flight bar (e.g. at session boundaries). A bar with `t` strictly less
1464
+ * than the buffered tail throws.
1465
+ *
1466
+ * Also invalidates the in-memory series cache for the asset so the next
1467
+ * `compute` call rebuilds the series from the updated buffer.
1468
+ *
1469
+ * @throws If called on a historical-mode runtime.
1470
+ * @throws If `bar.t` is strictly less than the last buffered bar's `t`.
1471
+ */
1472
+ appendBar(asset: Asset, bar: Bar): void;
1473
+ private baseSeries;
1474
+ /**
1475
+ * Returns the bar buffer for `asset`, or an empty array if none has been
1476
+ * fetched/buffered yet.
1477
+ *
1478
+ * In historical mode this is populated after the first `compute` call that
1479
+ * triggers a bar fetch for the asset. In streaming mode it reflects all bars
1480
+ * pushed via `appendBar`.
1481
+ */
1482
+ getBars(asset: Asset): ReadonlyArray<Bar>;
1483
+ /**
1484
+ * Returns the full per-asset bar map (keyed by `AssetId`). Merges historical
1485
+ * and streaming buffers (streaming wins on collision, but in practice an
1486
+ * instance is in one mode at a time so collisions don't occur).
1487
+ *
1488
+ * Returns a fresh `Map` — callers cannot mutate the internal state.
1489
+ */
1490
+ getAllBars(): ReadonlyMap<AssetId, ReadonlyArray<Bar>>;
1491
+ private cacheKey;
1492
+ /**
1493
+ * Computes (or retrieves from cache) the output `Series` for a given feature
1494
+ * spec applied to a specific asset.
1495
+ *
1496
+ * **Historical mode:** on the first call for a `(spec, asset)` pair:
1497
+ * 1. Fetches or reuses the in-memory base `Series` for the asset.
1498
+ * 2. Dispatches to the registered compute function for `spec.kind`.
1499
+ * 3. Stores the result in `featureCache`.
1500
+ * On subsequent calls, returns the cached `Series` directly from `featureCache`.
1501
+ *
1502
+ * **Streaming mode:** reads from the in-memory bar buffer populated via
1503
+ * `appendBar`. `DataFeed.bars` is never called. The persistent `featureCache`
1504
+ * is bypassed entirely — results are never read from or written to it.
1505
+ * The in-memory `seriesCache` (base series per asset) is the only cache layer
1506
+ * and is invalidated on each `appendBar`, so every `compute` after a new bar
1507
+ * reflects the updated buffer.
1508
+ *
1509
+ * @param spec - The feature specification describing which indicator to compute
1510
+ * and its parameters (e.g. `{ kind: 'sma', period: 20 }`).
1511
+ * @param asset - The asset for which to compute the feature. The asset's `id`
1512
+ * is used both for data fetching and cache key construction.
1513
+ * @returns A promise that resolves to the computed `Series`. The series length
1514
+ * is determined by the indicator's warmup: for example, SMA(20) returns
1515
+ * `series.length - 19` data points. Returns an empty array when the
1516
+ * base series is shorter than the indicator's warmup period.
1517
+ *
1518
+ * @example
1519
+ * ```ts
1520
+ * const spy = { kind: 'equity' as const, id: 'US:SPY', symbol: 'SPY' };
1521
+ *
1522
+ * const [priceSeries, smaSeries] = await Promise.all([
1523
+ * runtime.compute({ kind: 'price' }, spy),
1524
+ * runtime.compute({ kind: 'sma', period: 20 }, spy),
1525
+ * ]);
1526
+ * ```
1527
+ */
1528
+ compute(spec: FeatureSpec, asset: Asset): Promise<Series>;
452
1529
  }
453
1530
 
454
- interface SimulateOptions {
455
- from: string;
456
- to: string;
457
- portfolio: PortfolioHandle;
1531
+ /**
1532
+ * All inputs required to run a historical backtest.
1533
+ *
1534
+ * Callers must provide a concrete `Strategy`, a `DateRange`, and the four
1535
+ * pluggable runtime layers (`dataFeed`, `executor`, `calendar`, `featureCache`).
1536
+ * The reference implementations (`MemoryFeatureCache`, `BacktestExecutor`,
1537
+ * `NYSEExchangeCalendar`) satisfy all four without network dependencies.
1538
+ */
1539
+ type RunBacktestOptions<F extends Features = Features, S = unknown> = {
1540
+ /** The strategy under test. Must implement `universe`, `features`, and `build`. */
1541
+ strategy: Strategy<F, S>;
1542
+ /**
1543
+ * Inclusive date range over which to iterate. The calendar resolves this
1544
+ * range into the actual sequence of trading sessions.
1545
+ */
1546
+ range: DateRange;
1547
+ /**
1548
+ * Starting portfolio state. Cash and positions are carried forward through
1549
+ * the simulation as orders are filled. This value is never mutated.
1550
+ */
1551
+ initialPortfolio: Portfolio;
1552
+ /**
1553
+ * Source of OHLCV bar data and optionally fundamentals / corporate events.
1554
+ * `FeatureRuntime` uses this to hydrate price series before computing indicators.
1555
+ */
1556
+ dataFeed: DataFeed;
1557
+ /**
1558
+ * Order router responsible for converting `Order` objects into `Fill` records.
1559
+ * Use `BacktestExecutor` for historical simulations or swap in a live
1560
+ * broker implementation for paper/live trading.
1561
+ */
1562
+ executor: Executor;
1563
+ /**
1564
+ * Trading-day calendar. Used to enumerate `sessions` within `range` and to
1565
+ * determine rebalance day boundaries via `next`.
1566
+ */
1567
+ calendar: Calendar;
1568
+ /**
1569
+ * Optional persistent indicator cache. When omitted, each `runBacktest` call
1570
+ * recomputes all indicators from scratch. Provide `MemoryFeatureCache` (or a
1571
+ * cross-process cache) to memoize results across multiple runs.
1572
+ */
1573
+ featureCache?: FeatureCache;
1574
+ /**
1575
+ * Bar frequency forwarded to `DataFeed.bars`. Defaults to `'1d'` when omitted.
1576
+ * Must match the granularity expected by the strategy's indicator specs.
1577
+ */
1578
+ freq?: Frequency;
1579
+ /**
1580
+ * Optional `FeatureRuntime` instance. When provided, its accumulated bar buffer
1581
+ * is exported on `BacktestResult.bars` for use by `runLive` (lets the streaming
1582
+ * runtime seed its buffer from the historical bars without refetching).
1583
+ */
1584
+ featureRuntime?: FeatureRuntime;
1585
+ };
1586
+ /**
1587
+ * A point-in-time snapshot of the simulation at the end of a single trading session.
1588
+ *
1589
+ * Each entry in `BacktestResult.snapshots` corresponds to one call of the strategy
1590
+ * loop: `universe → features → build → executor.submit → applyFills`.
1591
+ */
1592
+ type BacktestSnapshot = {
1593
+ /** The session date for this snapshot (midnight UTC on the trading day). */
1594
+ t: Date;
1595
+ /** Portfolio state *after* fills have been applied for this session. */
1596
+ portfolio: Portfolio;
1597
+ /** Orders emitted by `strategy.build` during this session. */
1598
+ orders: ReadonlyArray<Order>;
1599
+ /** Fills returned by the executor for the orders above. */
1600
+ fills: ReadonlyArray<Fill>;
1601
+ };
1602
+ /**
1603
+ * The return value of `runBacktest`, containing the full simulation history
1604
+ * and the terminal portfolio state.
1605
+ */
1606
+ type BacktestResult<S = unknown> = {
1607
+ /**
1608
+ * Ordered list of snapshots, one per trading session in `range`. Empty when
1609
+ * the calendar has no sessions in the requested range.
1610
+ */
1611
+ snapshots: ReadonlyArray<BacktestSnapshot>;
1612
+ /**
1613
+ * Portfolio after the last session's fills have been applied. Equivalent to
1614
+ * `snapshots[snapshots.length - 1].portfolio` when there is at least one session,
1615
+ * or `initialPortfolio` when the range is empty.
1616
+ */
1617
+ finalPortfolio: Portfolio;
1618
+ /**
1619
+ * Final value of the strategy's auxiliary state after the last `build()` call.
1620
+ * `undefined` when the strategy is state-less (no `initialState()` defined).
1621
+ * Used by `runLive` to seed the live runtime so the first live tick continues
1622
+ * from the exact state the historical run ended on.
1623
+ */
1624
+ finalState: S | undefined;
1625
+ /**
1626
+ * Per-asset bar buffer accumulated by the `FeatureRuntime` during this run.
1627
+ * Empty `Map` when no `featureRuntime` was provided in `RunBacktestOptions`.
1628
+ * Used by `runLive` to seed its streaming `FeatureRuntime` so indicators with
1629
+ * warmup periods (SMA(200), etc.) work on the first live tick.
1630
+ */
1631
+ bars: ReadonlyMap<AssetId, ReadonlyArray<Bar>>;
1632
+ };
1633
+ /**
1634
+ * Drives a `Strategy` over a historical date range and returns a full audit trail
1635
+ * of orders, fills, and portfolio states.
1636
+ *
1637
+ * The simulation loop:
1638
+ * 1. Enumerate trading sessions via `opts.calendar.sessions(opts.range)`.
1639
+ * 2. Call `strategy.initialState?.()` once to seed the carry-over state.
1640
+ * 3. For each session `t`, call `strategy.universe(t, portfolio)`.
1641
+ * 4. Await `strategy.features(universe, portfolio, t)`.
1642
+ * 5. Call `strategy.build(features, portfolio, state, t)` to obtain orders and
1643
+ * the next state value. Both legacy `Order[]` returns and new `{ orders, state }`
1644
+ * returns are normalised — the legacy form leaves state unchanged.
1645
+ * 6. Await `opts.executor.submit(orders, t, portfolio)` to obtain fills.
1646
+ * 7. Apply fills to the portfolio with `applyFills`.
1647
+ * 8. Append a `BacktestSnapshot` and advance to the next session.
1648
+ *
1649
+ * The portfolio is never mutated in place; each session receives the immutable
1650
+ * result of the previous session's `applyFills`.
1651
+ *
1652
+ * @param opts - Backtest configuration. See {@link RunBacktestOptions}.
1653
+ * @returns A promise that resolves to a {@link BacktestResult} containing one
1654
+ * snapshot per trading session, the final portfolio state, and the final
1655
+ * strategy state (`finalState`). Returns
1656
+ * `{ snapshots: [], finalPortfolio: opts.initialPortfolio, finalState: undefined }`
1657
+ * when the calendar has no sessions in the requested range.
1658
+ *
1659
+ * @example
1660
+ * ```ts
1661
+ * import {
1662
+ * runBacktest,
1663
+ * fromSpec,
1664
+ * MemoryFeatureCache,
1665
+ * BacktestExecutor,
1666
+ * NYSEExchangeCalendar,
1667
+ * FeatureRuntime,
1668
+ * } from '@livefolio/sdk';
1669
+ *
1670
+ * const calendar = new NYSEExchangeCalendar();
1671
+ * const range = { from: new Date('2023-01-01'), to: new Date('2023-12-31') };
1672
+ * const featureCache = new MemoryFeatureCache();
1673
+ * const runtime = new FeatureRuntime({ dataFeed, featureCache, range, freq: '1d' });
1674
+ *
1675
+ * const strategy = fromSpec(myTacticalSpec, { runtime, calendar });
1676
+ *
1677
+ * const result = await runBacktest({
1678
+ * strategy,
1679
+ * range,
1680
+ * initialPortfolio: { cash: 100_000, positions: [] },
1681
+ * dataFeed,
1682
+ * executor: new BacktestExecutor({ dataFeed }),
1683
+ * calendar,
1684
+ * featureCache,
1685
+ * freq: '1d',
1686
+ * });
1687
+ *
1688
+ * console.log(result.finalPortfolio.cash);
1689
+ * console.log(result.snapshots.length); // one entry per NYSE trading day in 2023
1690
+ * ```
1691
+ */
1692
+ declare function runBacktest<F extends Features = Features, S = unknown>(opts: RunBacktestOptions<F, S>): Promise<BacktestResult<S>>;
1693
+
1694
+ /**
1695
+ * A single tick or bar update from a streaming market-data source. The bar's
1696
+ * `t` is the **arrival timestamp** of this tick — for a 24/7 source like Yahoo
1697
+ * WS, ticks arrive continuously and the runtime is responsible for deciding
1698
+ * which session/bar the tick belongs to via the `Calendar`.
1699
+ */
1700
+ type StreamingBar = {
1701
+ /** The asset this tick is for. */
1702
+ asset: Asset;
1703
+ /** The bar payload — typically a 1-tick OHLCV with `open = high = low = close = <tick price>` and `volume = 0`. */
1704
+ bar: Bar;
1705
+ };
1706
+ /**
1707
+ * Streaming market-data source. Sibling interface to {@link DataFeed} — they
1708
+ * are NOT a union. Historical adapters implement `DataFeed.bars()`; streaming
1709
+ * adapters implement `StreamingDataFeed.subscribe()`. A single vendor that
1710
+ * offers both (e.g. Polygon, Alpaca) implements both interfaces on one class.
1711
+ *
1712
+ * Implementations MUST guarantee:
1713
+ * - `subscribe` yields {@link StreamingBar} objects in **ascending `bar.t` order**
1714
+ * per asset. Ordering across assets is not required.
1715
+ * - The iterable is **open-ended** — it does not terminate on its own. Consumers
1716
+ * stop iteration by breaking the `for await` loop or by signalling cancel
1717
+ * through whatever mechanism the runtime provides.
1718
+ * - Ticks may arrive **outside session hours** (24/7 sources like Yahoo WS).
1719
+ * Session boundary logic is the runtime's responsibility, not the adapter's.
1720
+ *
1721
+ * @example
1722
+ * ```ts
1723
+ * import type { StreamingDataFeed, Asset } from '@livefolio/sdk';
1724
+ *
1725
+ * const feed: StreamingDataFeed = {
1726
+ * async *subscribe(assets) {
1727
+ * while (true) {
1728
+ * const tick = await waitForNextTick();
1729
+ * yield { asset: tick.asset, bar: tick.bar };
1730
+ * }
1731
+ * },
1732
+ * };
1733
+ * ```
1734
+ */
1735
+ interface StreamingDataFeed {
1736
+ /**
1737
+ * Subscribes to live tick updates for the given assets.
1738
+ *
1739
+ * **Frequency note:** This interface intentionally omits a `freq` parameter. Bar/tick aggregation
1740
+ * is the runtime's responsibility — see the `runLive` design in
1741
+ * `docs/specs/2026-05-02-v0.4-phase-9-streaming-design.md` Decision #1 (resolved during design).
1742
+ * Adapters emit raw ticks at whatever cadence the vendor provides; the runtime decides which
1743
+ * session/bar each tick belongs to via the `Calendar`. Multi-frequency streaming (sub-daily
1744
+ * strategies) is a separate phase, currently out of scope.
1745
+ *
1746
+ * @param assets - The instruments to subscribe to.
1747
+ * @returns An open-ended async iterable of {@link StreamingBar} updates.
1748
+ */
1749
+ subscribe(assets: ReadonlyArray<Asset>): AsyncIterable<StreamingBar>;
458
1750
  }
459
- interface Trade {
460
- date: string;
461
- symbol: string;
462
- quantity: number;
1751
+
1752
+ /**
1753
+ * Unified event stream from {@link runLive}. Discriminated union of two variants:
1754
+ *
1755
+ * - **`mark`** — emitted per tick. The strategy is run in PREVIEW mode (state
1756
+ * is snapshot/restored, no executor call, no portfolio commit). Use this to
1757
+ * render the wiggling rightmost chart point and the "if the session ended now,
1758
+ * the strategy would do X" preview UX.
1759
+ * - **`snapshot`** — emitted when a tick crosses a session boundary. The
1760
+ * just-closed bar is finalized, `strategy.build` runs for real, orders are
1761
+ * submitted to the executor, fills are applied, and state advances. Same
1762
+ * shape as {@link BacktestSnapshot} from {@link runBacktest} (plus the
1763
+ * `type: 'snapshot'` discriminant), so consumers can append snapshot events
1764
+ * to the same chart array used by historical results.
1765
+ */
1766
+ type LiveEvent<F extends Features = Features, _S = unknown> = {
1767
+ type: 'mark';
1768
+ /** Wall-clock arrival time of this tick. */
1769
+ t: Date;
1770
+ /** Portfolio at the start of the current session — unchanged by the preview. */
1771
+ portfolio: Portfolio;
1772
+ /**
1773
+ * Per-asset accumulating close so far in the current session. Only assets
1774
+ * that have received at least one tick this session appear in the map.
1775
+ */
1776
+ prices: ReadonlyMap<AssetId, number>;
1777
+ /** Features recomputed for the in-progress session. */
1778
+ features: F;
1779
+ /**
1780
+ * Orders the strategy would emit if the session closed at the current
1781
+ * tick price. Computed from a state SNAPSHOT — the returned `state` value
1782
+ * is discarded, so no committed state is mutated.
1783
+ */
1784
+ previewOrders: ReadonlyArray<Order>;
1785
+ /**
1786
+ * Best-effort placeholder — returns the unchanged portfolio. A future
1787
+ * `simulateFills(orders, prices)` helper will compute the hypothetical
1788
+ * post-rebalance NAV that would result from applying `previewOrders` at
1789
+ * `prices`. Until then, consumers compute NAV themselves from
1790
+ * `portfolio` + `prices`.
1791
+ */
1792
+ previewPortfolio: Portfolio;
1793
+ } | (BacktestSnapshot & {
1794
+ type: 'snapshot';
1795
+ });
1796
+ /** Required inputs to {@link runLive}. */
1797
+ type RunLiveOptions<F extends Features = Features, S = unknown> = {
1798
+ /** The strategy to drive. If its `features` method depends on a captured
1799
+ * `FeatureRuntime` (e.g. tactical strategies built via `fromSpec`), pass the
1800
+ * same runtime instance via {@link RunLiveOptions.streamingRuntime} so the
1801
+ * live bar buffer stays in sync with what the strategy reads. */
1802
+ strategy: Strategy<F, S>;
1803
+ /**
1804
+ * Result of a prior {@link runBacktest} call. Provides the seed `portfolio`,
1805
+ * `state`, and `bars` map for the streaming runtime.
1806
+ */
1807
+ history: BacktestResult<S>;
1808
+ /** Source of streaming ticks. */
1809
+ dataFeed: StreamingDataFeed;
1810
+ /** Order router used at session boundaries to settle the just-closed bar. */
1811
+ executor: Executor;
1812
+ /** Calendar that resolves a tick's wall-clock time into its session date. */
1813
+ calendar: Calendar;
1814
+ /**
1815
+ * Optional streaming {@link FeatureRuntime}. Provide this to share the
1816
+ * runtime with the strategy — tactical strategies built via `fromSpec`
1817
+ * capture a runtime in their `features` closure, so passing the same
1818
+ * instance here keeps the live bar buffer in sync with what the strategy
1819
+ * reads. When omitted, `runLive` constructs its own streaming runtime
1820
+ * seeded from `history.bars`.
1821
+ */
1822
+ streamingRuntime?: FeatureRuntime;
1823
+ };
1824
+ /**
1825
+ * Drives a {@link Strategy} against a streaming market-data source and yields
1826
+ * a unified event stream that consumer charts can append to historical
1827
+ * snapshots without code branching.
1828
+ *
1829
+ * **Lifecycle on each tick:**
1830
+ * 1. Resolve the tick's session date via the supplied {@link Calendar} —
1831
+ * `calendar.previous(calendar.next(tick.t))`. This correctly handles
1832
+ * after-hours ticks (NYSE 17:00 ET stays in the same session) and DST
1833
+ * transitions.
1834
+ * 2. If the tick crosses a session boundary, finalize the just-closed bar:
1835
+ * append it to the streaming `FeatureRuntime`, run `strategy.build` for
1836
+ * REAL (committing state), submit orders to the executor, apply fills,
1837
+ * and yield a `snapshot` event identical in shape to {@link BacktestSnapshot}.
1838
+ * 3. Record the tick into the current session's accumulating bar.
1839
+ * 4. Re-run `strategy.features` and `strategy.build` in PREVIEW mode (state
1840
+ * is snapshot/restored — committed state is untouched). Yield a `mark`
1841
+ * event with the recomputed features and preview orders.
1842
+ *
1843
+ * **State semantics:** preview-build always operates on a deep clone of the
1844
+ * committed `state`. Only the boundary-crossing commit branch advances
1845
+ * committed state. This guarantees that 1000 ticks within a single session
1846
+ * produce 1000 marks but leave `state` exactly where the prior session-close
1847
+ * commit left it.
1848
+ *
1849
+ * **FeatureRuntime:** if the strategy was built via `fromSpec` it captures its
1850
+ * own runtime in the `features` closure. Pass that same instance via
1851
+ * {@link RunLiveOptions.streamingRuntime} so `appendBar` calls land on the
1852
+ * runtime the strategy actually reads. When omitted, `runLive` constructs its
1853
+ * own streaming runtime seeded from `history.bars` — this works for hand-rolled
1854
+ * strategies whose `features` method consults the runtime directly, but it
1855
+ * leaves a `fromSpec` strategy reading a stale captured runtime.
1856
+ *
1857
+ * **Bar lineage:** the streaming `FeatureRuntime` (provided or constructed) is
1858
+ * seeded from `history.bars`, so indicators with warmup periods (SMA(200),
1859
+ * etc.) work on the first live tick.
1860
+ *
1861
+ * **Universe:** captured once at startup from
1862
+ * `strategy.universe(anchorTime, portfolio)`, where `anchorTime` is the last
1863
+ * historical snapshot's timestamp (or epoch zero for empty history). Dynamic
1864
+ * universes are not yet supported in live mode.
1865
+ *
1866
+ * **Termination:** the iterable terminates when the underlying
1867
+ * `StreamingDataFeed.subscribe` iterable terminates. Real adapters yield
1868
+ * forever; tests use bounded iterables to assert specific event sequences.
1869
+ *
1870
+ * @param opts - Live-runtime configuration. See {@link RunLiveOptions}.
1871
+ * @returns An open-ended `AsyncIterable<LiveEvent>`. Consumers `for await` the
1872
+ * stream and dispatch on `ev.type`.
1873
+ *
1874
+ * @example
1875
+ * ```ts
1876
+ * for await (const ev of runLive({ strategy, history, dataFeed, executor, calendar })) {
1877
+ * if (ev.type === 'mark') {
1878
+ * chart.updateLastBar({ t: ev.t, prices: ev.prices, previewOrders: ev.previewOrders });
1879
+ * } else {
1880
+ * chart.appendBar(ev); // BacktestSnapshot-shaped
1881
+ * }
1882
+ * }
1883
+ * ```
1884
+ */
1885
+ declare function runLive<F extends Features = Features, S = unknown>(opts: RunLiveOptions<F, S>): AsyncIterable<LiveEvent<F, S>>;
1886
+
1887
+ /**
1888
+ * A point-in-time quote for an asset. The `t` field is the vendor-stamped
1889
+ * quote time — callers should treat it as the staleness upper bound, not
1890
+ * "now". `price` is the last trade price, or the mid when the vendor only
1891
+ * exposes bid/ask. `bid` and `ask` surface Level 1 data when available.
1892
+ *
1893
+ * @example
1894
+ * ```ts
1895
+ * import type { Quote } from '@livefolio/sdk';
1896
+ *
1897
+ * const q: Quote = {
1898
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
1899
+ * t: new Date('2024-06-03T13:30:00Z'),
1900
+ * price: 195.12,
1901
+ * bid: 195.11,
1902
+ * ask: 195.13,
1903
+ * currency: 'USD',
1904
+ * };
1905
+ * ```
1906
+ */
1907
+ type Quote = {
1908
+ asset: Asset;
1909
+ /** Vendor-stamped quote time. */
1910
+ t: Date;
1911
+ /** Last trade price, or mid if the vendor only exposes bid/ask. */
463
1912
  price: number;
464
- action: 'buy' | 'sell';
465
- }
466
- interface PortfolioSnapshot {
467
- value: number;
468
- holdings: [TickerHandle, number][];
469
- weights: [TickerHandle, number][];
470
- pendingTrades: Trade[];
471
- }
472
- interface FinalState {
473
- portfolio: PortfolioHandle;
474
- allocation: AllocationHandle;
475
- closePrices: Record<string, number>;
476
- leveragedPrices: Record<string, number>;
1913
+ /** Best bid, when the vendor exposes Level 1 data. */
1914
+ bid?: number;
1915
+ /** Best ask, when the vendor exposes Level 1 data. */
1916
+ ask?: number;
1917
+ /** Quote currency, when the vendor reports it. */
1918
+ currency?: string;
1919
+ };
1920
+ /**
1921
+ * One-shot current-price source. Sibling interface to {@link DataFeed} and
1922
+ * {@link StreamingDataFeed} — they are NOT a union and there is no
1923
+ * composition helper. Historical adapters implement `DataFeed.bars()`;
1924
+ * streaming adapters implement `StreamingDataFeed.subscribe()`; quote
1925
+ * adapters implement `QuoteFeed.quote()`. A vendor that offers all three
1926
+ * implements all three interfaces on one class.
1927
+ *
1928
+ * Implementations MUST guarantee:
1929
+ * - `quote` returns a freshly fetched {@link Quote} each call. Implementations
1930
+ * MAY cache for a short TTL to coalesce bursts; cache behavior MUST be
1931
+ * documented on the adapter.
1932
+ * - The returned `Quote.t` is the vendor's stamp, not the local clock.
1933
+ * - `quote` rejects with a typed error if the asset is unsupported or the
1934
+ * vendor is unreachable. It MUST NOT silently return a stale or fabricated
1935
+ * price.
1936
+ *
1937
+ * `quoteBatch` is optional. Vendors whose endpoints accept a symbol list
1938
+ * SHOULD implement it to avoid N-round-trip storms. Callers feature-detect:
1939
+ *
1940
+ * ```ts
1941
+ * const quotes = feed.quoteBatch
1942
+ * ? await feed.quoteBatch(assets)
1943
+ * : await Promise.all(assets.map((a) => feed.quote(a)));
1944
+ * ```
1945
+ *
1946
+ * When `quoteBatch` is implemented, the returned array MUST preserve request
1947
+ * order — `quotes[i]` corresponds to `assets[i]`.
1948
+ *
1949
+ * @example
1950
+ * ```ts
1951
+ * import type { QuoteFeed } from '@livefolio/sdk';
1952
+ *
1953
+ * const feed: QuoteFeed = {
1954
+ * async quote(asset) {
1955
+ * return { asset, t: new Date(), price: 195.12 };
1956
+ * },
1957
+ * };
1958
+ * ```
1959
+ */
1960
+ interface QuoteFeed {
1961
+ /**
1962
+ * Returns a freshly fetched quote for `asset`.
1963
+ *
1964
+ * @param asset - The instrument to quote.
1965
+ * @returns A {@link Quote} carrying the vendor-stamped time and price.
1966
+ */
1967
+ quote(asset: Asset): Promise<Quote>;
1968
+ /**
1969
+ * Returns quotes for `assets` in a single vendor round-trip. Optional —
1970
+ * adapters whose vendor does not expose a batch endpoint may omit this.
1971
+ *
1972
+ * Returned array MUST preserve request order: `result[i]` corresponds to
1973
+ * `assets[i]`.
1974
+ *
1975
+ * @param assets - The instruments to quote.
1976
+ * @returns An array of {@link Quote} objects in request order.
1977
+ */
1978
+ quoteBatch?(assets: ReadonlyArray<Asset>): Promise<ReadonlyArray<Quote>>;
477
1979
  }
478
- /** Per-signal slice of a live strategy snapshot. */
479
- interface LiveSignalState {
480
- indicator1: {
481
- value: number | null;
482
- date: string | null;
483
- };
484
- indicator2: {
485
- value: number | null;
486
- date: string | null;
487
- };
488
- isTrue: boolean;
1980
+
1981
+ /**
1982
+ * In-process, Map-backed implementation of {@link FeatureCache}. Caches
1983
+ * computed indicator series in memory for the lifetime of the instance.
1984
+ * There is no eviction policy — the cache grows until the instance is
1985
+ * garbage-collected.
1986
+ *
1987
+ * **When to use**: the right choice for single-run backtests or unit tests
1988
+ * where the full dataset fits in process memory and cross-run persistence is
1989
+ * not required. For long-running hosted services or multi-process setups,
1990
+ * substitute a persistent implementation (e.g. Redis-backed) that satisfies
1991
+ * the {@link FeatureCache} interface.
1992
+ *
1993
+ * Cache keys are content-addressed strings composed of `(feature kind, params
1994
+ * hash, asset scope, date range, frequency)` — see the internal
1995
+ * `canonicalKey` function. The `invalidate` method performs prefix-based
1996
+ * deletion using the same key segments.
1997
+ *
1998
+ * @example
1999
+ * ```ts
2000
+ * import { MemoryFeatureCache } from '@livefolio/sdk';
2001
+ * import { FeatureRuntime } from '@livefolio/sdk/features';
2002
+ *
2003
+ * const cache = new MemoryFeatureCache();
2004
+ * const runtime = new FeatureRuntime({ feed: myDataFeed, cache });
2005
+ * ```
2006
+ */
2007
+ declare class MemoryFeatureCache implements FeatureCache {
2008
+ private store;
2009
+ get(key: FeatureKey): Promise<Series | undefined>;
2010
+ set(key: FeatureKey, series: Series): Promise<void>;
2011
+ invalidate(prefix: Partial<FeatureKey>): Promise<void>;
489
2012
  }
490
- /** Per-rule collection of live signal states, in the same order as the rule's `when` list. */
491
- interface LiveRuleState {
492
- signals: LiveSignalState[];
2013
+
2014
+ /**
2015
+ * Callback that resolves the next-open price for `asset` as seen from date `t`.
2016
+ * {@link BacktestExecutor} calls this once per order to determine the fill price.
2017
+ *
2018
+ * The function should return the opening price of the first trading session
2019
+ * strictly after `t`, along with that session's timestamp. In a typical
2020
+ * backtest setup this reads from the same data feed used to compute features.
2021
+ *
2022
+ * @param asset - The instrument being filled.
2023
+ * @param t - The date on which the rebalance order was submitted (the
2024
+ * "signal date"). The fill should occur on the next open after
2025
+ * this date to avoid look-ahead.
2026
+ * @returns An object with the fill timestamp `t` and the opening `price`.
2027
+ */
2028
+ type NextOpenFn = (asset: Asset, t: Date) => Promise<{
2029
+ t: Date;
2030
+ price: number;
2031
+ }>;
2032
+ /**
2033
+ * Constructor options for {@link BacktestExecutor}.
2034
+ */
2035
+ type BacktestExecutorOptions = {
2036
+ /** Exchange calendar used to route fills to the next open session. */
2037
+ calendar: Calendar;
2038
+ /**
2039
+ * Callback that resolves the next-open price for a given asset and date.
2040
+ * See {@link NextOpenFn} for the exact contract.
2041
+ */
2042
+ nextOpen: NextOpenFn;
2043
+ /**
2044
+ * One-way slippage in basis points applied to every fill. The fill price is
2045
+ * adjusted by `price × (1 + sign × slippageBps / 10 000)` where `sign` is
2046
+ * `+1` for buys and `−1` for sells. Defaults to `0`.
2047
+ */
2048
+ slippageBps?: number;
2049
+ /**
2050
+ * Flat per-share commission in the portfolio's base currency. Multiplied by
2051
+ * the fill quantity and recorded in `Fill.fees`. Defaults to `0`.
2052
+ */
2053
+ perShareFee?: number;
2054
+ };
2055
+ /**
2056
+ * Reference {@link Executor} implementation for backtesting. Fills each order
2057
+ * at the next-open price returned by the {@link NextOpenFn} callback, with
2058
+ * optional slippage and per-share commissions applied.
2059
+ *
2060
+ * **When to use**: suitable for historical simulations and unit tests where
2061
+ * real broker connectivity is not needed. For live or paper trading, substitute
2062
+ * a broker-backed `Executor` that satisfies the same interface.
2063
+ *
2064
+ * **Fill mechanics**: for each order in `orders`, the executor calls
2065
+ * `opts.nextOpen(asset, t)` to obtain the fill price and timestamp. The
2066
+ * raw price is then adjusted for slippage:
2067
+ * ```
2068
+ * adjustedPrice = nextOpen.price × (1 + sign × slippageBps / 10 000)
2069
+ * ```
2070
+ * where `sign` is `+1` for net-buy direction and `−1` for net-sell direction.
2071
+ * A flat per-share fee is added to `Fill.fees`. Orders with zero quantity are
2072
+ * silently skipped.
2073
+ *
2074
+ * @example
2075
+ * ```ts
2076
+ * import { BacktestExecutor } from '@livefolio/sdk';
2077
+ * import { getCalendar } from '@livefolio/sdk';
2078
+ *
2079
+ * const executor = new BacktestExecutor({
2080
+ * calendar: getCalendar('NYSE'),
2081
+ * nextOpen: async (asset, t) => {
2082
+ * // Return the first open bar strictly after t from your data feed.
2083
+ * const bar = await feed.nextBar(asset, t);
2084
+ * return { t: bar.t, price: bar.open };
2085
+ * },
2086
+ * slippageBps: 5, // 0.05% one-way
2087
+ * perShareFee: 0.005,
2088
+ * });
2089
+ * ```
2090
+ */
2091
+ declare class BacktestExecutor implements Executor {
2092
+ private readonly opts;
2093
+ constructor(opts: BacktestExecutorOptions);
2094
+ submit(orders: ReadonlyArray<Order>, t: Date, portfolio: Portfolio): Promise<ReadonlyArray<Fill>>;
493
2095
  }
494
- /** Full live strategy view for a single evaluation date — no portfolio info. */
495
- interface StrategyLiveState {
496
- allocation: AllocationHandle | null;
497
- activeRuleIndex: number;
498
- rules: LiveRuleState[];
2096
+
2097
+ /**
2098
+ * Error thrown by {@link RoutingDataFeed} when an asset cannot be routed or
2099
+ * when the routed feed does not support the requested optional method.
2100
+ *
2101
+ * Distinguish the two cases via the message text: "no feed registered" vs
2102
+ * "does not implement fundamentals".
2103
+ */
2104
+ declare class RoutingDataFeedError extends Error {
2105
+ constructor(message: string);
499
2106
  }
2107
+ /** Function form of the routing rule. Returns the feed for `asset`, or `undefined` when no feed handles it. */
2108
+ type RoutingDataFeedRouteFn = (asset: Asset) => DataFeed | undefined;
2109
+ /** Map form of the routing rule. Keys are `Asset['kind']` discriminants. */
2110
+ type RoutingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], DataFeed>>>;
500
2111
  /**
501
- * Combined live state returned by `SimulationHandle.pushAndPreview`: both the
502
- * portfolio snapshot from a `push` and the strategy evaluation at the target
503
- * date under the accumulated live-quote overrides.
2112
+ * A {@link DataFeed} that delegates each call to one of several underlying
2113
+ * feeds based on the asset. Use this to compose vendors e.g. Yahoo for
2114
+ * equities and FRED for macro series — behind a single `DataFeed` instance
2115
+ * accepted by `runBacktest`, `FeatureRuntime`, and `BacktestExecutor`.
2116
+ *
2117
+ * Routing rules:
2118
+ * - **Map form:** `new RoutingDataFeed({ equity: yahoo, macro: fred })`.
2119
+ * Keys are `asset.kind` discriminants. The 90% case.
2120
+ * - **Function form:** `new RoutingDataFeed((a) => a.kind === 'macro' ? fred : yahoo)`.
2121
+ * Use when routing depends on more than `kind` (e.g. allowlists).
2122
+ *
2123
+ * The router does **not** implement `events()` — the optional method is
2124
+ * genuinely absent (`'events' in router === false`). Cross-feed event
2125
+ * fan-out is deferred until a real consumer materializes.
2126
+ *
2127
+ * @example
2128
+ * ```ts
2129
+ * import { RoutingDataFeed } from '@livefolio/sdk';
2130
+ *
2131
+ * const feed = new RoutingDataFeed({ equity: yahooFeed, macro: fredFeed });
2132
+ *
2133
+ * const result = await runBacktest({
2134
+ * strategy, range, initialPortfolio,
2135
+ * dataFeed: feed,
2136
+ * executor,
2137
+ * calendar,
2138
+ * });
2139
+ * ```
504
2140
  */
505
- interface LivePreviewState extends StrategyLiveState {
506
- snapshot: PortfolioSnapshot;
2141
+ declare class RoutingDataFeed implements DataFeed {
2142
+ private readonly route;
2143
+ constructor(routes: RoutingDataFeedRouteMap | RoutingDataFeedRouteFn);
2144
+ bars(asset: Asset, range: DateRange, freq: Frequency): AsyncGenerator<Bar>;
2145
+ fundamentals(asset: Asset, t: Date): Promise<Fundamentals>;
2146
+ private resolve;
507
2147
  }
2148
+
508
2149
  /**
509
- * Callback shape that `SimulationHandle.pushAndPreview` delegates to. Exists
510
- * purely to break the circular import between `SimulationHandle` (in this
511
- * file) and `StrategyHandle` (which creates simulations) — a strategy passes
512
- * a bound `(date, overrides) => previewLiveState(...)` into the handle.
2150
+ * Error thrown by {@link RoutingStreamingDataFeed} when an asset cannot be routed.
513
2151
  */
514
- interface LiveEvaluator {
515
- previewLiveState(date: string, overrides: Record<string, number>): Promise<StrategyLiveState>;
2152
+ declare class RoutingStreamingDataFeedError extends Error {
2153
+ constructor(message: string);
516
2154
  }
517
- declare class SimulationHandle {
518
- readonly series: DailyBar[];
519
- readonly trades: Trade[];
520
- readonly startingPortfolio: PortfolioHandle;
521
- private _portfolio;
522
- private _currentAllocation;
523
- private _lastClosePrices;
524
- private _lastLeveragedPrices;
525
- private _currentLeveragedPrices;
526
- private _lastDate;
527
- private _pushedQuotes;
528
- private _liveEvaluator;
529
- constructor(series: DailyBar[], trades: Trade[], startingPortfolio: PortfolioHandle, finalState?: FinalState, liveEvaluator?: LiveEvaluator);
530
- push(...prices: [TickerHandle, number][]): PortfolioSnapshot;
531
- /**
532
- * One-call live update. Feeds portfolio-relevant ticker prices into `push`
533
- * (derived from `quotes` via the running portfolio's holdings), accumulates
534
- * every symbol in `quotes` into an internal override map so macro symbols
535
- * (e.g. `^VIX`) persist across ticks, then delegates to the simulation's
536
- * strategy for rule / signal / indicator evaluation at `date`.
537
- *
538
- * Without a live evaluator attached, returns just the portfolio snapshot
539
- * with allocation/rules/signals empty.
540
- *
541
- * @param quotes Symbol raw live price. Portfolio tickers flow through
542
- * `push` for leveraged-equity math; non-portfolio symbols are still
543
- * layered into the overlay so indicators can see them.
544
- * @param options.date Target trading day to evaluate against. Defaults to
545
- * the current UTC ISO date; callers with non-UTC semantics or after-hours
546
- * rollover should supply their own.
547
- */
548
- metrics(options?: MetricsOptions): MetricsResult;
549
- pushAndPreview(quotes: Record<string, number>, options?: {
550
- date?: string;
551
- }): Promise<LivePreviewState>;
2155
+ /** Function form of the routing rule. Returns the feed for `asset`, or `undefined` when no feed handles it. */
2156
+ type RoutingStreamingDataFeedRouteFn = (asset: Asset) => StreamingDataFeed | undefined;
2157
+ /** Map form of the routing rule. Keys are `Asset['kind']` discriminants. */
2158
+ type RoutingStreamingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], StreamingDataFeed>>>;
2159
+ /**
2160
+ * A {@link StreamingDataFeed} that delegates `subscribe()` to one of several
2161
+ * underlying feeds based on the asset. Use this to compose vendors — e.g.
2162
+ * Polygon for equities and a polling adapter for macro series — behind a
2163
+ * single `StreamingDataFeed` instance accepted by `runLive`.
2164
+ *
2165
+ * Routing rules:
2166
+ * - **Map form:** `new RoutingStreamingDataFeed({ equity: polygon, macro: polling })`.
2167
+ * Keys are `asset.kind` discriminants. The 90% case.
2168
+ * - **Function form:** `new RoutingStreamingDataFeed((a) => a.kind === 'macro' ? polling : polygon)`.
2169
+ * Use when routing depends on more than `kind` (e.g. allowlists).
2170
+ *
2171
+ * Assets are grouped by routed feed (by reference identity) before calling
2172
+ * upstream `subscribe()` so a vendor adapter that opens one socket for
2173
+ * `[AAPL, MSFT]` keeps doing that rather than receiving one-asset-at-a-time calls.
2174
+ *
2175
+ * @example
2176
+ * ```ts
2177
+ * import { RoutingStreamingDataFeed, pollingStreamFromHistorical } from '@livefolio/sdk';
2178
+ *
2179
+ * const feed = new RoutingStreamingDataFeed({
2180
+ * equity: polygonStreaming,
2181
+ * macro: pollingStreamFromHistorical({ feed: fredHistorical, freq: '1d', schedule: { kind: 'session-close', calendar: nyse } }),
2182
+ * });
2183
+ * ```
2184
+ */
2185
+ declare class RoutingStreamingDataFeed implements StreamingDataFeed {
2186
+ private readonly route;
2187
+ constructor(routes: RoutingStreamingDataFeedRouteMap | RoutingStreamingDataFeedRouteFn);
2188
+ subscribe(assets: ReadonlyArray<Asset>): AsyncIterable<StreamingBar>;
2189
+ private merged;
552
2190
  }
553
2191
 
554
- interface StrategyRule {
555
- when?: SignalHandle[];
556
- hold: AllocationHandle;
2192
+ /**
2193
+ * Error thrown by {@link RoutingQuoteFeed} when an asset cannot be routed.
2194
+ */
2195
+ declare class RoutingQuoteFeedError extends Error {
2196
+ constructor(message: string);
557
2197
  }
558
- interface StrategyBar {
559
- date: string;
560
- allocation: AllocationHandle;
2198
+ /** Function form of the routing rule. Returns the feed for `asset`, or `undefined` when no feed handles it. */
2199
+ type RoutingQuoteFeedRouteFn = (asset: Asset) => QuoteFeed | undefined;
2200
+ /** Map form of the routing rule. Keys are `Asset['kind']` discriminants. */
2201
+ type RoutingQuoteFeedRouteMap = Readonly<Partial<Record<Asset['kind'], QuoteFeed>>>;
2202
+ /**
2203
+ * A {@link QuoteFeed} that delegates each call to one of several underlying
2204
+ * feeds based on the asset. Use this to compose vendors — e.g. Alpaca for
2205
+ * equity quotes and a polling adapter for macro series — behind a single
2206
+ * `QuoteFeed` instance.
2207
+ *
2208
+ * Routing rules:
2209
+ * - **Map form:** `new RoutingQuoteFeed({ equity: alpaca, macro: fredPolling })`.
2210
+ * Keys are `asset.kind` discriminants. The 90% case.
2211
+ * - **Function form:** `new RoutingQuoteFeed((a) => a.kind === 'macro' ? fred : alpaca)`.
2212
+ * Use when routing depends on more than `kind` (e.g. allowlists).
2213
+ *
2214
+ * The router always implements `quoteBatch` — even if some inner feeds lack
2215
+ * it, the router falls back to per-asset `quote()` calls within that group,
2216
+ * preserving request order across the full result.
2217
+ *
2218
+ * @example
2219
+ * ```ts
2220
+ * import { RoutingQuoteFeed } from '@livefolio/sdk';
2221
+ *
2222
+ * const feed = new RoutingQuoteFeed({ equity: alpacaQuotes, macro: fredQuotes });
2223
+ * const quotes = await feed.quoteBatch([aaplAsset, dgs10Asset, msftAsset]);
2224
+ * // quotes[0] is for AAPL, quotes[1] for DGS10, quotes[2] for MSFT — request order preserved.
2225
+ * ```
2226
+ */
2227
+ declare class RoutingQuoteFeed implements QuoteFeed {
2228
+ private readonly route;
2229
+ constructor(routes: RoutingQuoteFeedRouteMap | RoutingQuoteFeedRouteFn);
2230
+ quote(asset: Asset): Promise<Quote>;
2231
+ quoteBatch(assets: ReadonlyArray<Asset>): Promise<ReadonlyArray<Quote>>;
2232
+ private resolve;
561
2233
  }
562
- interface StrategyOptions {
2234
+
2235
+ type PollingSchedule = {
2236
+ kind: 'interval';
2237
+ intervalMs: number;
2238
+ } | {
2239
+ kind: 'session-close';
2240
+ calendar: Calendar;
2241
+ };
2242
+ type PollingStreamOptions = {
2243
+ /** Historical feed to poll. Each tick of the schedule calls `feed.bars(asset, …)` for each subscribed asset. */
2244
+ feed: DataFeed;
2245
+ /** Bar frequency to request. Single value — multi-frequency requires composing two polling streams via `RoutingStreamingDataFeed`. */
2246
+ freq: Frequency;
2247
+ /** When to poll. */
2248
+ schedule: PollingSchedule;
2249
+ /**
2250
+ * Window-start for the first poll per asset. Subsequent polls fetch
2251
+ * `(lastSeenT, now]` per asset. Defaults to `new Date(0)` — every bar the
2252
+ * feed has on the first poll is yielded. For replay-then-stream, set this
2253
+ * to your backtest range's `to` so polling picks up exactly where the
2254
+ * backtest left off.
2255
+ */
2256
+ initialFrom?: Date;
2257
+ /** Inject for tests or for accelerated-time simulations. Defaults to `() => new Date()`. */
2258
+ now?: () => Date;
2259
+ /** Inject for tests or for accelerated-time simulations. Defaults to `setTimeout`-based promise. */
2260
+ sleep?: (ms: number) => Promise<void>;
2261
+ };
2262
+ declare function pollingStreamFromHistorical(opts: PollingStreamOptions): StreamingDataFeed;
2263
+
2264
+ /**
2265
+ * A year-derived holiday rule consumed by {@link ExchangeCalendar}. The rule
2266
+ * is active only for years in the range `[validFrom, validUntil]` (both
2267
+ * inclusive; omit either bound to leave it open).
2268
+ *
2269
+ * `resolve(year)` returns the UTC midnight `Date` for the holiday in that year,
2270
+ * or `null` if the holiday does not occur that year (e.g. a conditional rule
2271
+ * for Good Friday in certain years). When `observe` is `true`, a Saturday
2272
+ * result is moved to Friday and a Sunday result is moved to Monday (standard
2273
+ * US-style holiday observation).
2274
+ */
2275
+ type HolidayRule = {
2276
+ /** Human-readable name, used for debugging and logging. */
563
2277
  name: string;
564
- freq?: TradingFreq;
565
- offset?: number;
566
- rules: StrategyRule[];
2278
+ /** Returns the UTC midnight `Date` for this holiday in `year`, or `null` to skip. */
2279
+ resolve: (year: number) => Date | null;
2280
+ /** First year (inclusive) this rule applies. Defaults to −∞. */
2281
+ validFrom?: number;
2282
+ /** Last year (inclusive) this rule applies. Defaults to +∞. */
2283
+ validUntil?: number;
2284
+ /** When `true`, Saturday dates are moved to Friday, Sunday dates to Monday. */
2285
+ observe?: boolean;
2286
+ };
2287
+ /**
2288
+ * A year-derived early-close rule consumed by {@link ExchangeCalendar}. Follows
2289
+ * the same validity bounds and `resolve` contract as {@link HolidayRule}, but
2290
+ * instead of marking a day closed entirely it overrides the session close time
2291
+ * to `closeAt` for the matched date.
2292
+ */
2293
+ type SpecialClose = {
2294
+ /** Human-readable name, used for debugging and logging. */
2295
+ name: string;
2296
+ /** Returns the UTC midnight `Date` for this early-close day in `year`, or `null` to skip. */
2297
+ resolve: (year: number) => Date | null;
2298
+ /** The overridden close time in local exchange time. */
2299
+ closeAt: TimeOfDay;
2300
+ /** First year (inclusive) this rule applies. Defaults to −∞. */
2301
+ validFrom?: number;
2302
+ /** Last year (inclusive) this rule applies. Defaults to +∞. */
2303
+ validUntil?: number;
2304
+ };
2305
+ /**
2306
+ * A year-derived late-open rule consumed by {@link ExchangeCalendar}. Follows
2307
+ * the same validity bounds and `resolve` contract as {@link HolidayRule}, but
2308
+ * overrides the session open time to `openAt` for the matched date.
2309
+ */
2310
+ type SpecialOpen = {
2311
+ /** Human-readable name, used for debugging and logging. */
2312
+ name: string;
2313
+ /** Returns the UTC midnight `Date` for this late-open day in `year`, or `null` to skip. */
2314
+ resolve: (year: number) => Date | null;
2315
+ /** The overridden open time in local exchange time. */
2316
+ openAt: TimeOfDay;
2317
+ /** First year (inclusive) this rule applies. Defaults to −∞. */
2318
+ validFrom?: number;
2319
+ /** Last year (inclusive) this rule applies. Defaults to +∞. */
2320
+ validUntil?: number;
2321
+ };
2322
+ /**
2323
+ * Map of `YYYY-MM-DD` date strings to override times. Used for one-off
2324
+ * historical specials (e.g. a single early close due to a snowstorm) that do
2325
+ * not fit a repeating year-derived rule. Keys must be in `YYYY-MM-DD` format
2326
+ * in UTC.
2327
+ */
2328
+ type AdhocTimeOverrides = ReadonlyMap<string, TimeOfDay>;
2329
+
2330
+ /**
2331
+ * Abstract base class for exchange trading calendars. Implements the full
2332
+ * {@link Calendar} interface by composing up to nine overridable hooks that
2333
+ * subclasses provide. Concrete implementations ship for {@link NYSEExchangeCalendar}
2334
+ * and {@link LSEExchangeCalendar}; additional exchanges can be added by
2335
+ * extending this class.
2336
+ *
2337
+ * **Per-year caching**: holiday sets and special-session maps are computed once
2338
+ * per calendar year and stored in private Maps, so repeated calls to `isOpen`,
2339
+ * `next`, or `sessions` within the same year are cheap.
2340
+ *
2341
+ * **Hook resolution order** (adhoc beats rule, rule beats regular):
2342
+ * 1. `adhocHolidays()` / `specialClosesAdhoc()` / `specialOpensAdhoc()` —
2343
+ * `YYYY-MM-DD` string sets/maps populated once at first access.
2344
+ * 2. `regularHolidays()` / `specialCloses()` / `specialOpens()` —
2345
+ * year-derived rule arrays applied per year via the resolver helpers.
2346
+ * 3. `regularOpen(date)` / `regularClose(date)` / `weekmask(date)` —
2347
+ * per-date fallbacks that subclasses override to encode era-varying session
2348
+ * times and trading-day sets.
2349
+ *
2350
+ * **Extending**: override only the hooks you need. All hooks have no-op / sensible
2351
+ * defaults (Mon–Fri weekmask, 09:30–16:00 session) so a minimal subclass need
2352
+ * only set `name`, `tz`, and `regularHolidays()`.
2353
+ */
2354
+ declare abstract class ExchangeCalendar implements Calendar {
2355
+ /** Short exchange name used as the registry key in {@link getCalendar}. */
2356
+ abstract readonly name: string;
2357
+ /** IANA timezone identifier, e.g. `'America/New_York'` or `'Europe/London'`. */
2358
+ abstract readonly tz: string;
2359
+ private readonly holidayCache;
2360
+ private readonly specialCloseCache;
2361
+ private readonly specialOpenCache;
2362
+ private adhocHolidaysCache;
2363
+ private adhocSpecialClosesCache;
2364
+ private adhocSpecialOpensCache;
2365
+ /**
2366
+ * Returns the ordered list of year-derived holiday rules for this exchange.
2367
+ * The base implementation returns an empty array (no regular holidays). Override
2368
+ * to supply the full rule set; each {@link HolidayRule} in the array is applied
2369
+ * via {@link resolveHolidays} once per calendar year and cached. Rules may be
2370
+ * era-bounded via `validFrom` / `validUntil`.
2371
+ */
2372
+ protected regularHolidays(): ReadonlyArray<HolidayRule>;
2373
+ /**
2374
+ * Returns the set of `YYYY-MM-DD` strings for one-off full-day closures that
2375
+ * do not fit a repeating rule (e.g. presidential funerals, natural disasters).
2376
+ * The base implementation returns an empty set. Override with the complete
2377
+ * historical adhoc list for the exchange. This method is called at most once
2378
+ * per `ExchangeCalendar` instance; the result is cached.
2379
+ */
2380
+ protected adhocHolidays(): ReadonlySet<string>;
2381
+ /**
2382
+ * Returns the ordered list of year-derived early-close rules for this exchange.
2383
+ * The base implementation returns an empty array. Override to supply rules such
2384
+ * as "day after Thanksgiving closes at 13:00". Results are computed once per
2385
+ * year and cached; each rule is applied via {@link resolveSpecialCloses}.
2386
+ */
2387
+ protected specialCloses(): ReadonlyArray<SpecialClose>;
2388
+ /**
2389
+ * Returns the map of `YYYY-MM-DD` strings to override close times for
2390
+ * one-off early-close days that do not fit a repeating rule. The base
2391
+ * implementation returns an empty map. Override with the historical adhoc
2392
+ * set for the exchange. Called at most once per instance; result is cached.
2393
+ */
2394
+ protected specialClosesAdhoc(): AdhocTimeOverrides;
2395
+ /**
2396
+ * Returns the ordered list of year-derived late-open rules for this exchange.
2397
+ * The base implementation returns an empty array. Override to supply rules such
2398
+ * as "delayed open due to a moment of silence". Results are computed once per
2399
+ * year and cached; each rule is applied via {@link resolveSpecialOpens}.
2400
+ */
2401
+ protected specialOpens(): ReadonlyArray<SpecialOpen>;
2402
+ /**
2403
+ * Returns the map of `YYYY-MM-DD` strings to override open times for
2404
+ * one-off late-open days that do not fit a repeating rule. The base
2405
+ * implementation returns an empty map. Override with the historical adhoc
2406
+ * set for the exchange. Called at most once per instance; result is cached.
2407
+ */
2408
+ protected specialOpensAdhoc(): AdhocTimeOverrides;
2409
+ /**
2410
+ * Returns the default open time in local exchange time for `date` when no
2411
+ * special-open rule matches. The base implementation returns 09:30. Override
2412
+ * to encode era-varying session times (e.g. NYSE opened at 10:00 before
2413
+ * 1985-09-30).
2414
+ *
2415
+ * @param date - UTC midnight `Date` for the trading day being queried.
2416
+ */
2417
+ protected regularOpen(_date: Date): TimeOfDay;
2418
+ /**
2419
+ * Returns the default close time in local exchange time for `date` when no
2420
+ * special-close rule matches. The base implementation returns 16:00. Override
2421
+ * to encode era-varying session times (e.g. NYSE closed at 15:00 before
2422
+ * 1952-09-29, and at 15:30 until 1974-01-02).
2423
+ *
2424
+ * @param date - UTC midnight `Date` for the trading day being queried.
2425
+ */
2426
+ protected regularClose(_date: Date): TimeOfDay;
2427
+ /**
2428
+ * Returns the set of weekday indices (using `Date.getUTCDay()` convention:
2429
+ * 0 = Sunday, 1 = Monday, …, 6 = Saturday) that are regular trading days.
2430
+ * The base implementation returns `{1, 2, 3, 4, 5}` (Mon–Fri). Override to
2431
+ * encode historical six-day trading weeks (e.g. NYSE traded Mon–Sat before
2432
+ * 1952-09-29, keyed by `date` so the shift is era-aware).
2433
+ *
2434
+ * @param date - UTC midnight `Date` for the day being tested.
2435
+ */
2436
+ protected weekmask(_date: Date): ReadonlySet<number>;
2437
+ private getAdhocHolidays;
2438
+ private getAdhocSpecialCloses;
2439
+ private getAdhocSpecialOpens;
2440
+ /**
2441
+ * Cached lookup of regular-holiday timestamps for the given year.
2442
+ * Assumes `regularHolidays()` returns the same rule list on every call.
2443
+ */
2444
+ private holidaysForYear;
2445
+ private specialClosesForYear;
2446
+ private specialOpensForYear;
2447
+ private normalize;
2448
+ /** Returns `true` when `t` falls on a regular trading day (weekmask check, then holiday check). */
2449
+ isOpen(t: Date): boolean;
2450
+ /** Returns the first trading day strictly after `t`. */
2451
+ next(t: Date): Date;
2452
+ /** Returns the first trading day strictly before `t`. */
2453
+ previous(t: Date): Date;
2454
+ /**
2455
+ * Returns UTC midnight `Date` objects for every trading day in
2456
+ * `[range.from, range.to)`. The `from` bound is inclusive; `to` is exclusive.
2457
+ */
2458
+ sessions(range: DateRange): ReadonlyArray<Date>;
2459
+ schedule(range: DateRange): ReadonlyArray<Session>;
2460
+ isEarlyClose(t: Date): boolean;
2461
+ /** Adhoc overrides win over rule-driven; both win over `regularOpen(date)`. */
2462
+ private openTimeFor;
2463
+ /** Adhoc overrides win over rule-driven; both win over `regularClose(date)`. */
2464
+ private closeTimeFor;
2465
+ private localizedTimestamp;
567
2466
  }
568
- declare class StrategyHandle {
569
- private _linkId;
570
- private _name;
571
- private _freq;
572
- private _offset;
573
- private _rules;
574
- private _storage;
575
- private _market;
576
- private _resolvedId;
577
- private _resolvedLinkId;
578
- private _resolving;
579
- private _allocationMap;
580
- private _cache;
581
- private _cachedAsOf;
582
- private _syncing;
583
- constructor(storage: StorageProvider, market: MarketProvider, optionsOrLinkId: StrategyOptions | string);
584
- get id(): number;
585
- get link(): string;
586
- get name(): string | null;
587
- get freq(): TradingFreq;
588
- get offset(): number;
589
- get rules(): StrategyRule[];
590
- marketSymbols(): string[];
591
- resolve(): Promise<{
592
- id: number;
593
- }>;
594
- private _doResolveCreate;
595
- private _doResolveReference;
596
- private _getLatestClosedTradingDay;
597
- private _ensureFresh;
598
- private _sync;
599
- /**
600
- * Pure evaluate — runs the same pipeline as _sync but returns the computed
601
- * evaluation instead of persisting. Used by both _sync (post-close write
602
- * path) and the public preview methods (pre-close read-only path).
603
- *
604
- * When `overrides` is `undefined` we take the write path — syncing signals
605
- * through storage as normal. When `overrides` is provided (even an empty
606
- * map) we take the read-only preview path: historical signal bars come
607
- * straight from storage, today's bar is computed in-memory via
608
- * `signal.computeAt(date, overrides, prevBool)`, and nothing is written.
609
- *
610
- * Incremental path: when a strategy checkpoint exists (`getLatestSeriesDate`
611
- * returns non-null), only the window (lastDate, limitDate] is processed.
612
- * The current allocation is carried forward from `getLatestAllocationId`.
613
- * Bootstrap: when no checkpoint exists, falls back to `_evaluateCold` which
614
- * runs the full-history evaluation.
615
- */
616
- private _evaluate;
617
- private _evaluateCold;
618
- private _querySeriesFromDb;
619
- series(range?: DateRange): Promise<StrategyBar[]>;
620
- value(date?: string): Promise<AllocationHandle | null>;
621
- simulate(options: SimulateOptions): Promise<SimulationHandle>;
622
- /**
623
- * Preview the allocation this strategy would produce for `date` if today
624
- * closed at the provided raw quote prices. Does NOT write to strategies_series,
625
- * signals_series, or indicators_series. Safe to call before market close.
626
- *
627
- * @param date - The trading day to preview (must be in tradingDays.getRange()).
628
- * @param overrides - Raw (unleveraged) live prices keyed by market symbol.
629
- * Symbols absent from this map fall back to the last stored value
630
- * (see `IndicatorHandle._resolveRawBars`).
631
- * @returns The AllocationHandle for `date`, or null if the strategy has no
632
- * evaluable entry for that date.
633
- */
634
- previewAllocation(date: string, overrides: Record<string, number>): Promise<AllocationHandle | null>;
635
- /**
636
- * Read-only preview of the strategy's allocation series including `date`.
637
- * Returns stored historical allocations plus an in-memory bar at `date`
638
- * computed via the same overrides-based preview path as `previewAllocation`.
639
- *
640
- * @param date - Target trading day to splice in-memory.
641
- * @param overrides - Raw (unleveraged) quotes keyed by market symbol.
642
- * @param range - Optional filter applied to the returned bars.
643
- */
644
- previewSeries(date: string, overrides: Record<string, number>, range?: DateRange): Promise<StrategyBar[]>;
645
- /**
646
- * Full live strategy view at `date` under live-quote `overrides`: the active
647
- * allocation, the index of the rule that fired (or fallback), and per-rule
648
- * per-signal indicator values + truth. Computed entirely through the
649
- * overrides preview path — no writes to any `*_series` tables.
650
- *
651
- * Threshold indicators have their date suppressed (`null`) since their
652
- * synthetic series runs over every trading day in storage including future
653
- * dates and would report a far-future date for the last bar.
654
- */
655
- previewLiveState(date: string, overrides: Record<string, number>): Promise<StrategyLiveState>;
656
- private _fetchPricesForTickers;
657
- private _fetchRawClosePrices;
2467
+
2468
+ /**
2469
+ * New York Stock Exchange (NYSE) trading-day calendar covering 1885-01-01 to
2470
+ * the present. Also applicable to NYSE-equivalent venues (NASDAQ, BATS, DJIA,
2471
+ * DOW). Faithful port of `pandas_market_calendars`' `nyse.py`.
2472
+ *
2473
+ * **Era boundaries:**
2474
+ * - Weekmask: Mon–Sat through 1952-09-28; Mon–Fri from 1952-09-29 onward
2475
+ * (Saturday trading retired on that date).
2476
+ * - Regular open: 10:00 ET before 1985-09-30; 09:30 ET from 1985-09-30 onward.
2477
+ * - Regular close: 15:00 ET before 1952-09-29; 15:30 ET through 1973-12-31;
2478
+ * 16:00 ET from 1974-01-02 onward. Saturday closes (pre-1952) are
2479
+ * approximated as 12:00.
2480
+ *
2481
+ * Holiday coverage includes the full set of regular (rule-derived) and adhoc
2482
+ * (literal date set) closures sourced from `pandas_market_calendars`, spanning
2483
+ * historical events from the Ulysses Grant funeral (1885) through the Jimmy
2484
+ * Carter national day of mourning (2025).
2485
+ */
2486
+ declare class NYSEExchangeCalendar extends ExchangeCalendar {
2487
+ readonly name = "NYSE";
2488
+ readonly tz = "America/New_York";
2489
+ protected regularHolidays(): ReadonlyArray<HolidayRule>;
2490
+ protected adhocHolidays(): ReadonlySet<string>;
2491
+ protected specialCloses(): ReadonlyArray<SpecialClose>;
2492
+ protected specialClosesAdhoc(): AdhocTimeOverrides;
2493
+ protected specialOpens(): ReadonlyArray<SpecialOpen>;
2494
+ protected specialOpensAdhoc(): AdhocTimeOverrides;
2495
+ protected regularOpen(date: Date): TimeOfDay;
2496
+ protected regularClose(date: Date): TimeOfDay;
2497
+ protected weekmask(date: Date): ReadonlySet<number>;
658
2498
  }
659
2499
 
660
- type TreasuryTenor = Extract<IndicatorType, 'T3M' | 'T6M' | 'T1Y' | 'T2Y' | 'T3Y' | 'T5Y' | 'T7Y' | 'T10Y' | 'T20Y' | 'T30Y'>;
661
- type CalendarPeriod = Extract<IndicatorType, 'Month' | 'Day of Week' | 'Day of Month' | 'Day of Year'>;
662
- interface IndicatorOpts {
663
- delay?: number;
2500
+ /**
2501
+ * London Stock Exchange (LSE) trading-day calendar. Faithful port of
2502
+ * `pandas_market_calendars`' `lse.py` and `holidays/uk.py`. Historical
2503
+ * coverage begins 1801-01-01, aligned with the start of the modern exchange
2504
+ * after the Banking and Financial Dealings Act 1971 codified the current
2505
+ * bank-holiday framework.
2506
+ *
2507
+ * **Session**: 08:00–16:30 Europe/London. The exchange observes BST (UTC+1)
2508
+ * in summer and GMT (UTC+0) in winter — DST handling is delegated to luxon via
2509
+ * the `Europe/London` IANA timezone, so wall-clock session times are stable
2510
+ * across the DST transition while their UTC equivalents shift by one hour.
2511
+ *
2512
+ * **Early closes**: Christmas Eve (Dec 24) and New Year's Eve (Dec 31) close
2513
+ * at 12:30. Both use `previous_friday` observance — when the calendar date
2514
+ * falls on a weekend, the early close moves to the prior Friday.
2515
+ *
2516
+ * **Era boundaries**: bank-holiday exceptions for Royal Jubilees and VE-Day
2517
+ * anniversaries are implemented by splitting affected `Spring Bank Holiday`
2518
+ * and `Early May Bank Holiday` rules into era-bounded shards (matching
2519
+ * upstream `start_date` / `end_date` markers) and adding the displaced dates
2520
+ * as adhoc closures.
2521
+ */
2522
+ declare class LSEExchangeCalendar extends ExchangeCalendar {
2523
+ readonly name = "LSE";
2524
+ readonly tz = "Europe/London";
2525
+ protected regularHolidays(): ReadonlyArray<HolidayRule>;
2526
+ protected adhocHolidays(): ReadonlySet<string>;
2527
+ protected specialCloses(): ReadonlyArray<SpecialClose>;
2528
+ protected specialClosesAdhoc(): AdhocTimeOverrides;
2529
+ protected specialOpens(): ReadonlyArray<SpecialOpen>;
2530
+ protected specialOpensAdhoc(): AdhocTimeOverrides;
2531
+ protected regularOpen(_date: Date): TimeOfDay;
2532
+ protected regularClose(_date: Date): TimeOfDay;
2533
+ protected weekmask(_date: Date): ReadonlySet<number>;
664
2534
  }
665
- interface LivefolioClient {
666
- ticker(symbol: string, leverage?: number): TickerHandle;
667
- sma(ticker: TickerHandle, lookback: number, opts?: IndicatorOpts): IndicatorHandle;
668
- ema(ticker: TickerHandle, lookback: number, opts?: IndicatorOpts): IndicatorHandle;
669
- price(ticker: TickerHandle, opts?: IndicatorOpts): IndicatorHandle;
670
- returns(ticker: TickerHandle, lookback: number, opts?: IndicatorOpts): IndicatorHandle;
671
- volatility(ticker: TickerHandle, lookback: number, opts?: IndicatorOpts): IndicatorHandle;
672
- drawdown(ticker: TickerHandle, lookback: number, opts?: IndicatorOpts): IndicatorHandle;
673
- rsi(ticker: TickerHandle, lookback: number, opts?: IndicatorOpts): IndicatorHandle;
674
- vix(opts?: IndicatorOpts): IndicatorHandle;
675
- vix3m(opts?: IndicatorOpts): IndicatorHandle;
676
- treasury(tenor: TreasuryTenor, opts?: IndicatorOpts): IndicatorHandle;
677
- calendar(period: CalendarPeriod, opts?: IndicatorOpts): IndicatorHandle;
678
- threshold(value: number, unit?: Unit): IndicatorHandle;
679
- gt(ind1: IndicatorHandle, ind2: IndicatorHandle, tolerance?: number): SignalHandle;
680
- lt(ind1: IndicatorHandle, ind2: IndicatorHandle, tolerance?: number): SignalHandle;
681
- eq(ind1: IndicatorHandle, ind2: IndicatorHandle, tolerance?: number): SignalHandle;
682
- allocation(...holdings: [TickerHandle, number][]): AllocationHandle;
683
- portfolio(...holdings: [TickerHandle, number][]): PortfolioHandle;
684
- strategy(linkId: string): StrategyHandle;
685
- strategy(options: StrategyOptions): StrategyHandle;
686
- strategy(optionsOrLinkId: string | StrategyOptions): StrategyHandle;
2535
+
2536
+ /**
2537
+ * Union of supported exchange names accepted by {@link getCalendar}.
2538
+ *
2539
+ * - `'NYSE'` — New York Stock Exchange (and NYSE-equivalent venues).
2540
+ * - `'LSE'` — London Stock Exchange.
2541
+ */
2542
+ type ExchangeName = 'NYSE' | 'LSE';
2543
+ /**
2544
+ * Returns a new instance of the {@link ExchangeCalendar} registered under
2545
+ * `name`. Acts as a simple factory / registry for the two built-in calendar
2546
+ * implementations.
2547
+ *
2548
+ * Supported exchange names: `'NYSE'` ({@link NYSEExchangeCalendar}) and
2549
+ * `'LSE'` ({@link LSEExchangeCalendar}). TypeScript's exhaustive switch
2550
+ * prevents unknown names from compiling.
2551
+ *
2552
+ * @param name - One of the supported {@link ExchangeName} values.
2553
+ * @returns A fresh `ExchangeCalendar` instance for the named exchange.
2554
+ *
2555
+ * @example
2556
+ * ```ts
2557
+ * import { getCalendar } from '@livefolio/sdk';
2558
+ *
2559
+ * const nyse = getCalendar('NYSE');
2560
+ * console.log(nyse.isOpen(new Date('2024-07-04'))); // false — US Independence Day
2561
+ *
2562
+ * const lse = getCalendar('LSE');
2563
+ * console.log(lse.isOpen(new Date('2024-12-25'))); // false — Christmas Day
2564
+ * ```
2565
+ */
2566
+ declare function getCalendar(name: ExchangeName): ExchangeCalendar;
2567
+
2568
+ /**
2569
+ * 24/7 calendar where every day is a single session running midnight UTC to
2570
+ * the next midnight UTC. Suitable for crypto strategies (BTC, ETH) and any
2571
+ * always-on market.
2572
+ *
2573
+ * - `isOpen(t)` always returns `true`.
2574
+ * - `next(t)` returns midnight UTC of the day after `t`.
2575
+ * - `previous(t)` returns midnight UTC of the day before `t`.
2576
+ * - `sessions(range)` returns one Date per day in `[range.from, range.to)`.
2577
+ * - `schedule(range)` returns full Sessions with `open`/`close` at midnight UTC.
2578
+ * - `isEarlyClose(t)` always returns `false`.
2579
+ */
2580
+ declare class Crypto24x7Calendar implements Calendar {
2581
+ isOpen(_t: Date): boolean;
2582
+ next(t: Date): Date;
2583
+ previous(t: Date): Date;
2584
+ sessions(range: DateRange): ReadonlyArray<Date>;
2585
+ schedule(range: DateRange): ReadonlyArray<Session>;
2586
+ isEarlyClose(_t: Date): boolean;
687
2587
  }
688
- interface LivefolioClientOptions {
689
- storage: StorageProvider;
690
- market: MarketProvider;
2588
+
2589
+ /**
2590
+ * A reference to an asset within a {@link TacticalSpec}. Unlike the runtime
2591
+ * {@link Asset} type, `AssetRef` is the spec-form representation: it lives
2592
+ * inside serialized JSON specs and carries only the fields a spec author
2593
+ * needs to declare.
2594
+ *
2595
+ * `id` is the stable opaque identifier (see {@link AssetId}); `symbol` is the
2596
+ * human-readable ticker; `exchange` is optional. `kind` selects the asset
2597
+ * variant; absent `kind` defaults to `'equity'` for backward compatibility
2598
+ * with v0.4 specs authored before macro support landed.
2599
+ */
2600
+ type AssetRef = {
2601
+ /** Stable opaque asset identifier matching {@link AssetId}. */
2602
+ id: AssetId;
2603
+ /** Human-readable ticker symbol, e.g. `'AAPL'`. */
2604
+ symbol: string;
2605
+ /** Optional MIC or common exchange name, e.g. `'NYSE'`. Equity-only. */
2606
+ exchange?: string;
2607
+ /**
2608
+ * Asset class. Defaults to `'equity'` when omitted. Set to `'macro'` to
2609
+ * author FRED-style time-series assets that route to a non-equity
2610
+ * `DataFeed` (typically via `RoutingDataFeed`).
2611
+ */
2612
+ kind?: 'equity' | 'macro';
2613
+ };
2614
+ /**
2615
+ * A simulated leveraged or expense-adjusted asset that the runtime synthesizes
2616
+ * on-the-fly from its `underlying` data feed. The bar stream is computed by
2617
+ * {@link withSynthetics}, which wraps a real {@link DataFeed} and intercepts
2618
+ * requests for the synthetic's `id`.
2619
+ *
2620
+ * The synthesized daily close is:
2621
+ * ```
2622
+ * close_t = close_{t-1} × (1 + leverage × underlyingReturn_t) × (1 − expense/252)
2623
+ * ```
2624
+ *
2625
+ * @example
2626
+ * ```ts
2627
+ * import type { SyntheticAsset } from '@livefolio/sdk';
2628
+ *
2629
+ * const qqq3x: SyntheticAsset = {
2630
+ * id: 'QQQ_3X',
2631
+ * symbol: 'QQQ3X',
2632
+ * underlying: { id: 'QQQ', symbol: 'QQQ' },
2633
+ * leverage: 3,
2634
+ * expense: 0.0095, // 0.95% annual
2635
+ * };
2636
+ * ```
2637
+ */
2638
+ type SyntheticAsset = {
2639
+ /** Stable ID for this synthetic; must be unique in the spec universe. */
2640
+ id: AssetId;
2641
+ /** Display symbol for this synthetic. */
2642
+ symbol: string;
2643
+ /** Reference to the real asset whose returns are scaled. */
2644
+ underlying: AssetRef;
2645
+ /** Daily return multiplier (e.g. `3` for 3× leverage, `-1` for inverse). */
2646
+ leverage: number;
2647
+ /** Annual expense ratio as a decimal (e.g. `0.0095` for 0.95%). Defaults to 0. */
2648
+ expense?: number;
2649
+ /** When set, orders are routed to this proxy asset instead of the synthetic id. */
2650
+ tradeAs?: AssetRef;
2651
+ };
2652
+ /**
2653
+ * Cadence at which the strategy is allowed to rebalance.
2654
+ *
2655
+ * - `'Daily'` — every trading day
2656
+ * - `'Weekly'` — last trading day of each ISO week
2657
+ * - `'Monthly'` — last trading day of each calendar month
2658
+ * - `'Quarterly'` — last trading day of each calendar quarter
2659
+ * - `'Yearly'` — last trading day of each calendar year
2660
+ */
2661
+ type RebalanceFrequency = 'Daily' | 'Weekly' | 'Monthly' | 'Quarterly' | 'Yearly';
2662
+ /**
2663
+ * Controls when the strategy is permitted to issue rebalance orders.
2664
+ * If omitted from a {@link TacticalSpec}, the default is `{ frequency: 'Daily' }`.
2665
+ *
2666
+ * @see {@link isRebalanceDay} for the trading-calendar-aware gate implementation.
2667
+ */
2668
+ type RebalanceConfig = {
2669
+ /** How often the strategy may rebalance. */
2670
+ frequency: RebalanceFrequency;
2671
+ };
2672
+ /**
2673
+ * A single feature entry in a {@link TacticalSpec}. Each variant declares the
2674
+ * indicator kind, the asset to compute it on, and the time-series lookup
2675
+ * parameters. The `id` is the name used to reference the computed value in
2676
+ * {@link FeatureRef} inside the rule tree.
2677
+ *
2678
+ * Supported variants:
2679
+ * - `price` — most-recent closing price
2680
+ * - `sma` — simple moving average over `period` trading days
2681
+ * - `ema` — exponential moving average over `period` trading days
2682
+ * - `rsi` — relative strength index over `period` trading days
2683
+ * - `return` — cumulative or log return over `period` trading days (see {@link ReturnMode})
2684
+ * - `volatility` — annualised rolling standard deviation over `period` trading days
2685
+ * - `drawdown` — peak-to-trough drawdown over `period` trading days
2686
+ *
2687
+ * The optional `delay` shifts the lookup back by that many bars (0 = current
2688
+ * bar, 1 = previous bar, etc.). Useful for avoiding look-ahead bias when using
2689
+ * end-of-day prices.
2690
+ */
2691
+ type TacticalFeatureSpec = {
2692
+ id: string;
2693
+ kind: 'price';
2694
+ asset: AssetRef;
2695
+ delay?: number;
2696
+ } | {
2697
+ id: string;
2698
+ kind: 'sma';
2699
+ asset: AssetRef;
2700
+ period: number;
2701
+ delay?: number;
2702
+ } | {
2703
+ id: string;
2704
+ kind: 'ema';
2705
+ asset: AssetRef;
2706
+ period: number;
2707
+ delay?: number;
2708
+ } | {
2709
+ id: string;
2710
+ kind: 'rsi';
2711
+ asset: AssetRef;
2712
+ period: number;
2713
+ delay?: number;
2714
+ } | {
2715
+ id: string;
2716
+ kind: 'return';
2717
+ asset: AssetRef;
2718
+ period: number;
2719
+ mode?: ReturnMode;
2720
+ delay?: number;
2721
+ } | {
2722
+ id: string;
2723
+ kind: 'volatility';
2724
+ asset: AssetRef;
2725
+ period: number;
2726
+ delay?: number;
2727
+ } | {
2728
+ id: string;
2729
+ kind: 'drawdown';
2730
+ asset: AssetRef;
2731
+ period: number;
2732
+ delay?: number;
2733
+ };
2734
+ /**
2735
+ * Union of all indicator kind strings that can appear in a
2736
+ * {@link TacticalFeatureSpec}. Derived automatically from the spec union so it
2737
+ * stays in sync.
2738
+ */
2739
+ type TacticalFeatureKind = TacticalFeatureSpec['kind'];
2740
+ /**
2741
+ * A reference to a computed feature value within a rule node. The `ref` string
2742
+ * must match an `id` declared in the `features` array of the enclosing
2743
+ * {@link TacticalSpec}. At evaluation time the runtime replaces the ref with
2744
+ * the resolved numeric value.
2745
+ *
2746
+ * @see {@link Comparison} where `FeatureRef` is accepted as an operand.
2747
+ */
2748
+ type FeatureRef = {
2749
+ ref: string;
2750
+ };
2751
+ /**
2752
+ * Binary comparison operator used in a {@link Comparison} node.
2753
+ *
2754
+ * - `'gt'` — strictly greater than (`l > r`)
2755
+ * - `'lt'` — strictly less than (`l < r`)
2756
+ * - `'gte'` — greater than or equal to (`l >= r`)
2757
+ * - `'lte'` — less than or equal to (`l <= r`)
2758
+ */
2759
+ type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte';
2760
+ /**
2761
+ * Hysteresis band applied to a {@link Comparison} with `op: 'gt'` or `op: 'lt'`.
2762
+ * Once the comparison has flipped, it will not flip back until the left operand
2763
+ * exits the tolerance band around the right operand.
2764
+ *
2765
+ * `mode: 'absolute'` defines a ±`value` band around `right`.
2766
+ * `mode: 'relative'` defines a ±`value`% band (i.e. `value` is a percentage).
2767
+ *
2768
+ * A `Tolerance` requires the parent {@link Comparison} to carry a stable `id`
2769
+ * so the runtime can persist the last-known state across rebalance periods.
2770
+ */
2771
+ type Tolerance = {
2772
+ /** Half-width of the hysteresis band. */
2773
+ value: number;
2774
+ /** `'absolute'` uses raw units; `'relative'` uses a percentage of `right`. */
2775
+ mode: 'absolute' | 'relative';
2776
+ };
2777
+ /**
2778
+ * A binary comparison between two operands. Each operand is either a literal
2779
+ * number or a {@link FeatureRef} resolved at evaluation time. The result is
2780
+ * `true` when `left op right` holds.
2781
+ *
2782
+ * When `tolerance` is provided the comparison implements hysteresis — the
2783
+ * result is sticky and only changes when the left operand exits the band. A
2784
+ * stable `id` is required in that case so {@link RuleTreeState} can track the
2785
+ * last outcome.
2786
+ *
2787
+ * @example
2788
+ * ```ts
2789
+ * import type { Comparison } from '@livefolio/sdk';
2790
+ *
2791
+ * // Feature "sma200" > feature "sma50"
2792
+ * const cond: Comparison = {
2793
+ * op: 'gt',
2794
+ * left: { ref: 'sma200' },
2795
+ * right: { ref: 'sma50' },
2796
+ * };
2797
+ * ```
2798
+ */
2799
+ type Comparison = {
2800
+ /** Which binary operator to apply. */
2801
+ op: ComparisonOp;
2802
+ /** Left-hand operand — a feature reference or a literal number. */
2803
+ left: FeatureRef | number;
2804
+ /** Right-hand operand — a feature reference or a literal number. */
2805
+ right: FeatureRef | number;
2806
+ /**
2807
+ * Optional hysteresis band. Requires `op` to be `'gt'` or `'lt'` and
2808
+ * requires `id` to be set.
2809
+ */
2810
+ tolerance?: Tolerance;
2811
+ /**
2812
+ * Stable identifier used to persist comparison state across steps when
2813
+ * `tolerance` is set. Must be unique within the rule tree.
2814
+ */
2815
+ id?: string;
2816
+ };
2817
+ /**
2818
+ * A leaf node in a {@link RuleNode} tree that terminates evaluation and
2819
+ * returns a target weight allocation. `weights` is a map from asset IDs to
2820
+ * fractional weights; weights should sum to 1 for a fully-invested portfolio,
2821
+ * but the runtime does not enforce this constraint.
2822
+ *
2823
+ * @example
2824
+ * ```ts
2825
+ * import type { AllocateNode } from '@livefolio/sdk';
2826
+ *
2827
+ * // 60% equities, 40% bonds
2828
+ * const node: AllocateNode = {
2829
+ * op: 'allocate',
2830
+ * weights: { SPY: 0.6, TLT: 0.4 },
2831
+ * };
2832
+ * ```
2833
+ */
2834
+ type AllocateNode = {
2835
+ op: 'allocate';
2836
+ /** Map from asset ID to target portfolio weight (0–1). */
2837
+ weights: Record<AssetId, number>;
2838
+ };
2839
+ /**
2840
+ * A branching node in a {@link RuleNode} tree. Evaluates `cond` and recurses
2841
+ * into `then` when the condition is true, or into `else` otherwise. Nesting
2842
+ * `IfNode` trees builds arbitrary decision logic over computed features.
2843
+ *
2844
+ * @example
2845
+ * ```ts
2846
+ * import type { IfNode } from '@livefolio/sdk';
2847
+ *
2848
+ * const node: IfNode = {
2849
+ * op: 'if',
2850
+ * cond: { op: 'gt', left: { ref: 'sma50' }, right: { ref: 'sma200' } },
2851
+ * then: { op: 'allocate', weights: { SPY: 1 } },
2852
+ * else: { op: 'allocate', weights: { SHY: 1 } },
2853
+ * };
2854
+ * ```
2855
+ */
2856
+ type IfNode = {
2857
+ op: 'if';
2858
+ /** Condition to evaluate. */
2859
+ cond: Comparison;
2860
+ /** Sub-tree evaluated when `cond` is true. */
2861
+ then: RuleNode;
2862
+ /** Sub-tree evaluated when `cond` is false. */
2863
+ else: RuleNode;
2864
+ };
2865
+ /**
2866
+ * A node in the tactical rule tree. Either a branching {@link IfNode} or a
2867
+ * terminal {@link AllocateNode}.
2868
+ *
2869
+ * - `op: 'if'` — see {@link IfNode}
2870
+ * - `op: 'allocate'` — see {@link AllocateNode}
2871
+ */
2872
+ type RuleNode = AllocateNode | IfNode;
2873
+ /**
2874
+ * A fully self-contained declaration of a tactical allocation strategy. Plain
2875
+ * data — no methods, no closures. Pass to {@link fromSpec} to obtain a
2876
+ * runnable {@link Strategy}.
2877
+ *
2878
+ * The dialect version distinguishes `'tactical/v0'` (deprecated, byte-for-byte
2879
+ * equivalent to v1, emits a one-time console warning) from `'tactical/v1'` (current).
2880
+ *
2881
+ * @example
2882
+ * ```ts
2883
+ * import type { TacticalSpec } from '@livefolio/sdk';
2884
+ *
2885
+ * const spec: TacticalSpec = {
2886
+ * kind: 'tactical/v1',
2887
+ * universe: [
2888
+ * { id: 'SPY', symbol: 'SPY' },
2889
+ * { id: 'SHY', symbol: 'SHY' },
2890
+ * ],
2891
+ * rebalance: { frequency: 'Monthly' },
2892
+ * features: [
2893
+ * { id: 'spy_sma200', kind: 'sma', asset: { id: 'SPY', symbol: 'SPY' }, period: 200 },
2894
+ * { id: 'spy_price', kind: 'price', asset: { id: 'SPY', symbol: 'SPY' } },
2895
+ * ],
2896
+ * rules: {
2897
+ * op: 'if',
2898
+ * cond: { op: 'gt', left: { ref: 'spy_price' }, right: { ref: 'spy_sma200' } },
2899
+ * then: { op: 'allocate', weights: { SPY: 1 } },
2900
+ * else: { op: 'allocate', weights: { SHY: 1 } },
2901
+ * },
2902
+ * };
2903
+ * ```
2904
+ */
2905
+ type TacticalSpec = {
2906
+ /**
2907
+ * Dialect version. Use `'tactical/v1'`. `'tactical/v0'` is accepted but
2908
+ * deprecated and will emit a one-time warning.
2909
+ */
2910
+ kind: 'tactical/v0' | 'tactical/v1';
2911
+ /** Ordered list of assets eligible for allocation. */
2912
+ universe: AssetRef[];
2913
+ /** Optional synthetic assets whose bar data is derived from an underlying. */
2914
+ synthetics?: SyntheticAsset[];
2915
+ /** Rebalance cadence. Defaults to `{ frequency: 'Daily' }` when omitted. */
2916
+ rebalance?: RebalanceConfig;
2917
+ /** Named feature computations referenced by the rule tree. */
2918
+ features: TacticalFeatureSpec[];
2919
+ /** Root of the rule tree that maps resolved feature values to target weights. */
2920
+ rules: RuleNode;
2921
+ };
2922
+ /**
2923
+ * Persistent state carried across rebalance steps for all named {@link Comparison}
2924
+ * nodes that use hysteresis (`tolerance` set). Maps `comparison.id` to the last
2925
+ * evaluated outcome: `1` = condition was true, `0` = condition was false.
2926
+ *
2927
+ * Managed internally by {@link fromSpec}; exposed as a type for testing and
2928
+ * for callers that drive {@link evaluateRuleTree} directly.
2929
+ */
2930
+ type RuleTreeState = ReadonlyMap<string, 0 | 1>;
2931
+
2932
+ /**
2933
+ * Evaluates a {@link RuleNode} tree against a resolved set of feature values
2934
+ * and returns the target allocation weights together with the updated
2935
+ * hysteresis state.
2936
+ *
2937
+ * The tree is walked depth-first. At each {@link IfNode} the comparison is
2938
+ * evaluated (with hysteresis applied when `tolerance` and `id` are present)
2939
+ * and the walk follows either `then` or `else`. Evaluation terminates at an
2940
+ * {@link AllocateNode} whose `weights` map is returned verbatim.
2941
+ *
2942
+ * Hysteresis: when a {@link Comparison} carries a `tolerance`, the previous
2943
+ * outcome in `state` is used to decide whether to flip the result. The updated
2944
+ * outcomes for all visited comparisons are collected in the returned `state` map.
2945
+ *
2946
+ * Throws if a {@link FeatureRef} resolves to `undefined` (the caller should
2947
+ * suppress this with a guard or catch-block, as {@link fromSpec} does).
2948
+ *
2949
+ * @param rules - Root of the rule tree to evaluate.
2950
+ * @param values - Resolved feature values, keyed by feature id. Must contain
2951
+ * every `ref` string used in the tree; missing keys throw.
2952
+ * @param state - Prior hysteresis state from the previous evaluation step.
2953
+ * Pass an empty `Map` on the first call.
2954
+ * @returns An object with:
2955
+ * - `weights` — target portfolio weights as a `Map<AssetId, number>`.
2956
+ * - `state` — updated {@link RuleTreeState} to pass to the next step.
2957
+ *
2958
+ * @example
2959
+ * ```ts
2960
+ * import { evaluateRuleTree } from '@livefolio/sdk';
2961
+ * import type { RuleNode, RuleTreeState } from '@livefolio/sdk';
2962
+ *
2963
+ * const rules: RuleNode = {
2964
+ * op: 'if',
2965
+ * cond: { op: 'gt', left: { ref: 'price' }, right: { ref: 'sma200' } },
2966
+ * then: { op: 'allocate', weights: { SPY: 1 } },
2967
+ * else: { op: 'allocate', weights: { SHY: 1 } },
2968
+ * };
2969
+ *
2970
+ * let state: RuleTreeState = new Map();
2971
+ * const values = new Map([['price', 450], ['sma200', 420]]);
2972
+ * const result = evaluateRuleTree(rules, values, state);
2973
+ * // result.weights → Map { 'SPY' => 1 }
2974
+ * state = result.state;
2975
+ * ```
2976
+ */
2977
+ declare function evaluateRuleTree(rules: RuleNode, values: ReadonlyMap<string, number>, state?: RuleTreeState): {
2978
+ weights: TargetWeights;
2979
+ state: RuleTreeState;
2980
+ };
2981
+
2982
+ /**
2983
+ * Resolves each {@link TacticalFeatureSpec} in `specs` to a scalar value as of
2984
+ * date `t` by calling into `runtime` and reading the series at the appropriate
2985
+ * bar index. All specs are computed in parallel via `Promise.all`.
2986
+ *
2987
+ * The returned map uses each spec's `id` as the key. The value is `undefined`
2988
+ * when the indicator series has no data on or before `t`, or when the `delay`
2989
+ * offset steps past the beginning of the series.
2990
+ *
2991
+ * Validation performed before dispatching to the runtime:
2992
+ * - Duplicate `id` values in `specs` throw immediately.
2993
+ * - A non-integer or negative `delay` throws immediately.
2994
+ *
2995
+ * @param specs - Ordered list of feature declarations from {@link TacticalSpec.features}.
2996
+ * @param runtime - Feature computation backend that owns the data feed and cache.
2997
+ * @param t - Evaluation date; the series is read at the latest bar on or before `t`.
2998
+ * @returns A map from feature id to resolved numeric value (`undefined` when unavailable).
2999
+ *
3000
+ * @example
3001
+ * ```ts
3002
+ * import { evaluateFeatureSpecs } from '@livefolio/sdk';
3003
+ * import type { TacticalFeatureSpec } from '@livefolio/sdk';
3004
+ *
3005
+ * const specs: TacticalFeatureSpec[] = [
3006
+ * { id: 'spy_sma200', kind: 'sma', asset: { id: 'SPY', symbol: 'SPY' }, period: 200 },
3007
+ * { id: 'spy_price', kind: 'price', asset: { id: 'SPY', symbol: 'SPY' } },
3008
+ * ];
3009
+ *
3010
+ * const values = await evaluateFeatureSpecs(specs, runtime, new Date('2024-06-01'));
3011
+ * // values.get('spy_price') → 528.3
3012
+ * ```
3013
+ */
3014
+ declare function evaluateFeatureSpecs(specs: ReadonlyArray<TacticalFeatureSpec>, runtime: FeatureRuntime, t: Date): Promise<Map<string, number | undefined>>;
3015
+
3016
+ /**
3017
+ * Wraps a {@link DataFeed} so that requests for any asset whose `id` appears in
3018
+ * `synthetics` are intercepted and their bar stream is derived on-the-fly from
3019
+ * the corresponding `underlying` asset.
3020
+ *
3021
+ * The synthesized close price on each bar is computed as:
3022
+ * ```
3023
+ * close_t = close_{t-1} × (1 + leverage × underlyingReturn_t) × (1 − expense/252)
3024
+ * ```
3025
+ * The first bar in the stream uses the underlying close directly. OHLC fields
3026
+ * other than `close` are all set to the synthesized close (they are not
3027
+ * independently scaled); `volume` is passed through from the underlying bar.
3028
+ *
3029
+ * Non-synthetic assets are proxied transparently to the original `dataFeed`.
3030
+ * `fundamentals` and `events` methods, if present, are forwarded unchanged.
3031
+ *
3032
+ * Throws at construction time if `synthetics` contains duplicate `id` values.
3033
+ *
3034
+ * @param dataFeed - The real data feed to wrap.
3035
+ * @param synthetics - Synthetic asset definitions; typically `spec.synthetics ?? []`.
3036
+ * @returns A new {@link DataFeed} that intercepts synthetic asset ids.
3037
+ *
3038
+ * @example
3039
+ * ```ts
3040
+ * import { withSynthetics } from '@livefolio/sdk';
3041
+ * import type { SyntheticAsset } from '@livefolio/sdk';
3042
+ *
3043
+ * const leveraged: SyntheticAsset = {
3044
+ * id: 'SPY_3X', symbol: 'SPY3X',
3045
+ * underlying: { id: 'SPY', symbol: 'SPY' },
3046
+ * leverage: 3,
3047
+ * expense: 0.01,
3048
+ * };
3049
+ *
3050
+ * const feed = withSynthetics(realFeed, [leveraged]);
3051
+ * // Requesting bars for asset { id: 'SPY_3X', ... } now returns 3× leveraged returns.
3052
+ * ```
3053
+ */
3054
+ declare function withSynthetics(dataFeed: DataFeed, synthetics: ReadonlyArray<SyntheticAsset>): DataFeed;
3055
+ /**
3056
+ * Options for {@link withStreamingSynthetics}.
3057
+ */
3058
+ interface WithStreamingSyntheticsOptions {
3059
+ /**
3060
+ * Last known close per asset id, used to seed `prevUnderlyingClose` and
3061
+ * `prevSynthClose` so the first live tick of a synthetic continues smoothly
3062
+ * from the end of its historical series.
3063
+ *
3064
+ * Build it from a {@link BacktestResult}:
3065
+ * ```ts
3066
+ * const seedLastCloses = new Map<AssetId, number>();
3067
+ * for (const [id, bars] of history.bars) {
3068
+ * const last = bars.at(-1)?.close;
3069
+ * if (last !== undefined) seedLastCloses.set(id, last);
3070
+ * }
3071
+ * ```
3072
+ *
3073
+ * Without seeding, the first synthesized tick lands on the underlying's
3074
+ * price and produces a visible jump in live preview.
3075
+ */
3076
+ seedLastCloses: ReadonlyMap<AssetId, number>;
691
3077
  }
692
- declare function createClient(options: LivefolioClientOptions): LivefolioClient;
693
-
694
- type StreamStatus = 'connected' | 'disconnected' | 'reconnecting';
695
- interface PriceStream {
696
- subscribe(...symbols: string[]): void;
697
- unsubscribe(...symbols: string[]): void;
698
- on(event: 'tick', cb: (symbol: string, price: number, time: string) => void): void;
699
- on(event: 'status', cb: (status: StreamStatus) => void): void;
700
- on(event: 'error', cb: (error: Error) => void): void;
701
- off(event: 'tick', cb: (symbol: string, price: number, time: string) => void): void;
702
- off(event: 'status', cb: (status: StreamStatus) => void): void;
703
- off(event: 'error', cb: (error: Error) => void): void;
704
- close(): void;
3078
+ /**
3079
+ * Streaming-feed counterpart to {@link withSynthetics}. Wraps a
3080
+ * {@link StreamingDataFeed} so that subscriptions for synthetic asset ids
3081
+ * resolve to upstream subscriptions on the underlying, with each underlying
3082
+ * tick re-emitted as a synthesized tick on the synthetic's id using the same
3083
+ * `(1 + leverage × r) × (1 − expense/252)` formula as the historical wrapper.
3084
+ *
3085
+ * Behavior:
3086
+ * - Non-synthetic ids in the `assets` argument pass through to the inner feed
3087
+ * unchanged.
3088
+ * - Underlyings that aren't directly in `assets` but are needed by a synthetic
3089
+ * are subscribed silently only the synthesized ticks are yielded back to
3090
+ * the caller for those.
3091
+ * - Underlyings that the caller _did_ ask for in `assets` are yielded both as
3092
+ * the raw underlying tick and as the synthesized tick(s).
3093
+ *
3094
+ * Throws at construction time if `synthetics` contains duplicate `id` values.
3095
+ *
3096
+ * @example
3097
+ * ```ts
3098
+ * import { withStreamingSynthetics, runLive } from '@livefolio/sdk';
3099
+ *
3100
+ * const seedLastCloses = new Map<AssetId, number>();
3101
+ * for (const [id, bars] of history.bars) {
3102
+ * const last = bars.at(-1)?.close;
3103
+ * if (last !== undefined) seedLastCloses.set(id, last);
3104
+ * }
3105
+ *
3106
+ * const liveFeed = withStreamingSynthetics(rawStreamingFeed, spec.synthetics ?? [], {
3107
+ * seedLastCloses,
3108
+ * });
3109
+ *
3110
+ * for await (const event of runLive({ strategy, history, dataFeed: liveFeed, executor, calendar })) {
3111
+ * // …
3112
+ * }
3113
+ * ```
3114
+ */
3115
+ declare function withStreamingSynthetics(inner: StreamingDataFeed, synthetics: ReadonlyArray<SyntheticAsset>, opts: WithStreamingSyntheticsOptions): StreamingDataFeed;
3116
+
3117
+ /** Test-only: reset the once-per-process deprecation gate. */
3118
+ declare function _resetTacticalDeprecationWarningForTesting(): void;
3119
+ /**
3120
+ * The feature bundle computed on each rebalance step and passed to the rule
3121
+ * tree. Produced by the `features` method of the {@link Strategy} returned by
3122
+ * {@link fromSpec}.
3123
+ *
3124
+ * - `values` — named indicator results keyed by the `id` field of each
3125
+ * {@link TacticalFeatureSpec}. A value is `undefined` when the indicator
3126
+ * cannot be computed for that bar (e.g. insufficient history).
3127
+ * - `prices` — most-recent closing prices for each asset in the universe,
3128
+ * keyed by asset ID.
3129
+ */
3130
+ type TacticalFeatures = {
3131
+ values: ReadonlyMap<string, number | undefined>;
3132
+ prices: ReadonlyMap<AssetId, number>;
3133
+ };
3134
+ /**
3135
+ * Runtime dependencies required by {@link fromSpec} to hydrate a
3136
+ * {@link TacticalSpec} into a runnable {@link Strategy}.
3137
+ */
3138
+ type FromSpecOptions = {
3139
+ /** Feature computation backend — wraps the data feed and caching layer. */
3140
+ runtime: FeatureRuntime;
3141
+ /** Exchange calendar used to gate rebalance days via {@link isRebalanceDay}. */
3142
+ calendar: Calendar;
3143
+ };
3144
+ /**
3145
+ * Returns a stable string key that identifies the rebalance period containing
3146
+ * date `t` for the given `freq`. Two dates that map to the same key belong to
3147
+ * the same period and therefore produce the same rebalance decision. Used
3148
+ * internally by {@link isRebalanceDay} to detect period boundaries.
3149
+ *
3150
+ * @param t - The date to classify.
3151
+ * @param freq - Rebalance cadence (see {@link RebalanceFrequency}).
3152
+ * @returns A compact string such as `'2024-3'` (monthly), `'2024-W14'`
3153
+ * (weekly), or `'2024-1'` (quarterly Q2).
3154
+ */
3155
+ declare function periodKey(t: Date, freq: RebalanceFrequency): string;
3156
+ /**
3157
+ * Returns `true` when `t` is the last trading day of its rebalance period
3158
+ * according to `freq` and `calendar`. The check is: `periodKey(t) !== periodKey(next(t))`.
3159
+ * For `'Daily'` cadence this always returns `true`.
3160
+ *
3161
+ * @param t - Current trading day (must itself be a trading day).
3162
+ * @param freq - Rebalance cadence (see {@link RebalanceFrequency}).
3163
+ * @param calendar - Exchange calendar used to find the next trading day.
3164
+ * @returns `true` if today is the last day of its period and orders should be issued.
3165
+ */
3166
+ declare function isRebalanceDay(t: Date, freq: RebalanceFrequency, calendar: Calendar): boolean;
3167
+ /**
3168
+ * Hydrates a plain {@link TacticalSpec} data object into a runnable
3169
+ * {@link Strategy} that `runBacktest` can drive step-by-step.
3170
+ *
3171
+ * State is threaded explicitly through `build` via the
3172
+ * {@link Strategy | `Strategy<F, S>.build`} signature. `initialState()` returns
3173
+ * an empty {@link RuleTreeState} Map; the runtime is responsible for storing and
3174
+ * forwarding the state between calls. This design makes `build` a pure function
3175
+ * of its inputs — calling it twice with identical arguments produces identical
3176
+ * outputs, enabling snapshot/restore for preview-builds in live mode.
3177
+ *
3178
+ * Validation performed at construction time:
3179
+ * - A `'tactical/v0'` `kind` emits a one-time deprecation warning to `console.warn`.
3180
+ * - Synthetic assets are checked for self-reference, symbol collisions, and
3181
+ * missing universe entries (see internal `validateSynthetics`).
3182
+ *
3183
+ * @param spec - The declarative strategy spec.
3184
+ * @param opts - Runtime dependencies (feature backend and calendar).
3185
+ * @returns A {@link Strategy} whose `features` method fetches indicator values
3186
+ * and whose `build` method converts them to rebalance orders.
3187
+ *
3188
+ * @example
3189
+ * ```ts
3190
+ * import { fromSpec, MemoryFeatureCache, NYSEExchangeCalendar } from '@livefolio/sdk';
3191
+ * import { FeatureRuntime } from '@livefolio/sdk/features';
3192
+ *
3193
+ * const calendar = new NYSEExchangeCalendar();
3194
+ * const cache = new MemoryFeatureCache();
3195
+ * const runtime = new FeatureRuntime({ feed: myDataFeed, cache });
3196
+ *
3197
+ * const strategy = fromSpec(mySpec, { runtime, calendar });
3198
+ * ```
3199
+ */
3200
+ declare function fromSpec(spec: TacticalSpec, opts: FromSpecOptions): Strategy<TacticalFeatures, RuleTreeState>;
3201
+
3202
+ type index$1_AllocateNode = AllocateNode;
3203
+ type index$1_AssetRef = AssetRef;
3204
+ type index$1_Comparison = Comparison;
3205
+ type index$1_ComparisonOp = ComparisonOp;
3206
+ type index$1_FeatureRef = FeatureRef;
3207
+ type index$1_FromSpecOptions = FromSpecOptions;
3208
+ type index$1_IfNode = IfNode;
3209
+ type index$1_RebalanceConfig = RebalanceConfig;
3210
+ type index$1_RebalanceFrequency = RebalanceFrequency;
3211
+ type index$1_RuleNode = RuleNode;
3212
+ type index$1_RuleTreeState = RuleTreeState;
3213
+ type index$1_SyntheticAsset = SyntheticAsset;
3214
+ type index$1_TacticalFeatureKind = TacticalFeatureKind;
3215
+ type index$1_TacticalFeatureSpec = TacticalFeatureSpec;
3216
+ type index$1_TacticalFeatures = TacticalFeatures;
3217
+ type index$1_TacticalSpec = TacticalSpec;
3218
+ type index$1_Tolerance = Tolerance;
3219
+ type index$1_WithStreamingSyntheticsOptions = WithStreamingSyntheticsOptions;
3220
+ declare const index$1__resetTacticalDeprecationWarningForTesting: typeof _resetTacticalDeprecationWarningForTesting;
3221
+ declare const index$1_evaluateFeatureSpecs: typeof evaluateFeatureSpecs;
3222
+ declare const index$1_evaluateRuleTree: typeof evaluateRuleTree;
3223
+ declare const index$1_fromSpec: typeof fromSpec;
3224
+ declare const index$1_isRebalanceDay: typeof isRebalanceDay;
3225
+ declare const index$1_periodKey: typeof periodKey;
3226
+ declare const index$1_withStreamingSynthetics: typeof withStreamingSynthetics;
3227
+ declare const index$1_withSynthetics: typeof withSynthetics;
3228
+ declare namespace index$1 {
3229
+ export { type index$1_AllocateNode as AllocateNode, type index$1_AssetRef as AssetRef, type index$1_Comparison as Comparison, type index$1_ComparisonOp as ComparisonOp, type index$1_FeatureRef as FeatureRef, type index$1_FromSpecOptions as FromSpecOptions, type index$1_IfNode as IfNode, type index$1_RebalanceConfig as RebalanceConfig, type index$1_RebalanceFrequency as RebalanceFrequency, type index$1_RuleNode as RuleNode, type index$1_RuleTreeState as RuleTreeState, type index$1_SyntheticAsset as SyntheticAsset, type index$1_TacticalFeatureKind as TacticalFeatureKind, type index$1_TacticalFeatureSpec as TacticalFeatureSpec, type index$1_TacticalFeatures as TacticalFeatures, type index$1_TacticalSpec as TacticalSpec, type index$1_Tolerance as Tolerance, type index$1_WithStreamingSyntheticsOptions as WithStreamingSyntheticsOptions, index$1__resetTacticalDeprecationWarningForTesting as _resetTacticalDeprecationWarningForTesting, index$1_evaluateFeatureSpecs as evaluateFeatureSpecs, index$1_evaluateRuleTree as evaluateRuleTree, index$1_fromSpec as fromSpec, index$1_isRebalanceDay as isRebalanceDay, index$1_periodKey as periodKey, index$1_withStreamingSynthetics as withStreamingSynthetics, index$1_withSynthetics as withSynthetics };
705
3230
  }
706
3231
 
707
- declare function allocationsEqual(a: AllocationHandle | null, b: AllocationHandle | null): boolean;
3232
+ /**
3233
+ * Computes a Simple Moving Average (SMA) over a price series.
3234
+ *
3235
+ * Math definition:
3236
+ * ```
3237
+ * SMA[i] = (series[i] + series[i-1] + ... + series[i-period+1]) / period
3238
+ * ```
3239
+ *
3240
+ * Warmup: the first output point corresponds to index `period - 1` in the input.
3241
+ * Inputs `series[0]` through `series[period - 2]` have no SMA value and are
3242
+ * excluded from the output entirely (no `undefined` placeholders; the output
3243
+ * array is shorter than the input).
3244
+ *
3245
+ * Edge cases:
3246
+ * - `period <= 0` — throws `Error`.
3247
+ * - `series.length < period` — returns `[]` (not enough data for even one window).
3248
+ * - `series.length === period` — returns a single-point `Series`.
3249
+ *
3250
+ * @param series - Input price series sorted in ascending timestamp order.
3251
+ * @param period - Window size in bars. Must be a positive integer.
3252
+ * @returns A `Series` of length `max(0, series.length - period + 1)`. Each point's
3253
+ * timestamp `t` is taken from the last bar in its window (`series[i + period - 1]`).
3254
+ *
3255
+ * @example
3256
+ * ```ts
3257
+ * import { sma } from '@livefolio/sdk';
3258
+ *
3259
+ * const prices = [
3260
+ * { t: new Date('2023-01-02'), v: 100 },
3261
+ * { t: new Date('2023-01-03'), v: 110 },
3262
+ * { t: new Date('2023-01-04'), v: 120 },
3263
+ * { t: new Date('2023-01-05'), v: 130 },
3264
+ * ];
3265
+ *
3266
+ * const result = sma(prices, 3);
3267
+ * // result.length === 2
3268
+ * // result[0] => { t: new Date('2023-01-04'), v: 110 } // (100+110+120)/3
3269
+ * // result[1] => { t: new Date('2023-01-05'), v: 120 } // (110+120+130)/3
3270
+ * ```
3271
+ */
3272
+ declare function sma(series: Series, period: number): Series;
708
3273
 
709
- declare function computeRebalanceDates(tradingDays: string[], freq: TradingFreq, offset: number): Set<string>;
3274
+ /**
3275
+ * Computes an Exponential Moving Average (EMA) over a price series.
3276
+ *
3277
+ * Math definition:
3278
+ * ```
3279
+ * k = 2 / (period + 1) // smoothing factor
3280
+ * EMA[0] = SMA(series[0..period-1]) // seeded from simple average
3281
+ * EMA[i] = series[i] * k + EMA[i-1] * (1 - k)
3282
+ * ```
3283
+ *
3284
+ * Warmup: the first `period - 1` input bars are consumed to seed the SMA
3285
+ * initial value. The first EMA output point corresponds to input index
3286
+ * `period - 1`; subsequent points are computed from the recursive formula.
3287
+ * The output array is shorter than the input (no `undefined` placeholders).
3288
+ *
3289
+ * Edge cases:
3290
+ * - `period <= 0` — throws `Error`.
3291
+ * - `series.length < period` — returns `[]` (not enough data to seed the EMA).
3292
+ * - `series.length === period` — returns a single-point `Series` equal to the
3293
+ * simple average of all input values.
3294
+ *
3295
+ * @param series - Input price series sorted in ascending timestamp order.
3296
+ * @param period - Lookback window in bars. Must be a positive integer. Controls
3297
+ * the smoothing factor `k = 2 / (period + 1)`: smaller periods react faster
3298
+ * to recent prices.
3299
+ * @returns A `Series` of length `max(0, series.length - period + 1)`. Each
3300
+ * point's timestamp `t` is taken from the corresponding input bar.
3301
+ *
3302
+ * @example
3303
+ * ```ts
3304
+ * import { ema } from '@livefolio/sdk';
3305
+ *
3306
+ * const prices = [
3307
+ * { t: new Date('2023-01-02'), v: 100 },
3308
+ * { t: new Date('2023-01-03'), v: 110 },
3309
+ * { t: new Date('2023-01-04'), v: 120 },
3310
+ * { t: new Date('2023-01-05'), v: 130 },
3311
+ * ];
3312
+ *
3313
+ * const result = ema(prices, 3);
3314
+ * // result.length === 2
3315
+ * // result[0] => { t: new Date('2023-01-04'), v: 110 } // SMA seed: (100+110+120)/3
3316
+ * // result[1] => { t: new Date('2023-01-05'), v: ~116.7 } // EMA: 130*0.5 + 110*0.5
3317
+ * ```
3318
+ */
3319
+ declare function ema(series: Series, period: number): Series;
710
3320
 
711
- declare function computeMetrics(series: DailyBar[], trades: Trade[], options?: MetricsOptions): MetricsResult;
3321
+ /**
3322
+ * Computes the Relative Strength Index (RSI) using Wilder's smoothing method.
3323
+ *
3324
+ * Math definition:
3325
+ * ```
3326
+ * changes[i] = series[i] - series[i-1]
3327
+ *
3328
+ * // Seed from simple averages of the first `period` changes:
3329
+ * avgGain[0] = mean(max(changes[0..period-1], 0))
3330
+ * avgLoss[0] = mean(max(-changes[0..period-1], 0))
3331
+ *
3332
+ * // Wilder's smoothing for subsequent periods:
3333
+ * avgGain[i] = (avgGain[i-1] * (period-1) + gain[i]) / period
3334
+ * avgLoss[i] = (avgLoss[i-1] * (period-1) + loss[i]) / period
3335
+ *
3336
+ * RS[i] = avgGain[i] / avgLoss[i]
3337
+ * RSI[i] = 100 - 100 / (1 + RS[i])
3338
+ * ```
3339
+ *
3340
+ * Special case: when `avgLoss === 0`, RSI is clamped to 100 (infinite RS means
3341
+ * no losing periods in the window).
3342
+ *
3343
+ * Warmup: requires `period + 1` input bars to produce the first RSI value
3344
+ * (one extra bar for the initial change calculation). The first output point
3345
+ * corresponds to input index `period`. The output array is shorter than the
3346
+ * input (no `undefined` placeholders).
3347
+ *
3348
+ * Edge cases:
3349
+ * - `period <= 0` — throws `Error`.
3350
+ * - `series.length < period + 1` — returns `[]`.
3351
+ * - Flat price series (all changes = 0) — returns RSI values of 100 because
3352
+ * `avgLoss` stays 0.
3353
+ *
3354
+ * @param series - Input price series sorted in ascending timestamp order.
3355
+ * @param period - Lookback window in bars for Wilder's smoothing. Must be a
3356
+ * positive integer; 14 is the conventional default.
3357
+ * @returns A `Series` of length `max(0, series.length - period)`. Each point's
3358
+ * timestamp `t` is taken from the corresponding input bar. Values are in
3359
+ * the range `[0, 100]`.
3360
+ *
3361
+ * @example
3362
+ * ```ts
3363
+ * import { rsi } from '@livefolio/sdk';
3364
+ *
3365
+ * // Minimal example: 5 bars, period 3 → 2 RSI values
3366
+ * const prices = [
3367
+ * { t: new Date('2023-01-02'), v: 100 },
3368
+ * { t: new Date('2023-01-03'), v: 102 },
3369
+ * { t: new Date('2023-01-04'), v: 101 },
3370
+ * { t: new Date('2023-01-05'), v: 105 },
3371
+ * { t: new Date('2023-01-06'), v: 104 },
3372
+ * ];
3373
+ *
3374
+ * const result = rsi(prices, 3);
3375
+ * // result.length === 2
3376
+ * // result[0].t => new Date('2023-01-05')
3377
+ * // result[1].t => new Date('2023-01-06')
3378
+ * // values are in [0, 100]
3379
+ * ```
3380
+ */
3381
+ declare function rsi(series: Series, period: number): Series;
712
3382
 
713
- declare function sharpe(returns: number[], rfAnnual: number): number;
714
- declare function sortino(returns: number[], rfAnnual: number): number;
3383
+ /**
3384
+ * Computes a rolling historical volatility as the population standard deviation
3385
+ * of daily log-like returns over a sliding window.
3386
+ *
3387
+ * Math definition:
3388
+ * ```
3389
+ * dailyReturn[i] = series[i] / series[i-1] - 1 // simple period return
3390
+ *
3391
+ * // For each window of `period` daily returns ending at index i:
3392
+ * mean = Σ dailyReturn[i..i-period+1] / period
3393
+ * variance = Σ (dailyReturn[j] - mean)² / period // population variance
3394
+ * vol[i] = sqrt(variance)
3395
+ * ```
3396
+ *
3397
+ * The result is the per-bar standard deviation expressed as a fraction (e.g.
3398
+ * `0.012` ≈ 1.2 % daily volatility). To annualise, multiply by `sqrt(252)` for
3399
+ * daily bars.
3400
+ *
3401
+ * Warmup: requires `period + 1` price bars to produce the first volatility value:
3402
+ * one extra bar to compute the first daily return, then `period` returns for the
3403
+ * first window. The first output point corresponds to the `period`-th daily-return
3404
+ * index. The output array is shorter than the input (no `undefined` placeholders).
3405
+ *
3406
+ * Edge cases:
3407
+ * - `period <= 0` — throws `Error`.
3408
+ * - `series.length < period + 1` — returns `[]`.
3409
+ * - Flat price series (all returns = 0) — returns volatility values of `0`.
3410
+ *
3411
+ * @param series - Input price series sorted in ascending timestamp order. Values
3412
+ * must be positive (non-zero); zero prices produce `NaN` daily returns.
3413
+ * @param period - Window size in daily-return bars. Must be a positive integer;
3414
+ * common values are 20 (≈ 1 month) or 252 (1 year) for daily data.
3415
+ * @returns A `Series` of length `max(0, series.length - period)`. Each point's
3416
+ * timestamp `t` is taken from the last daily-return bar in its window.
3417
+ *
3418
+ * @example
3419
+ * ```ts
3420
+ * import { volatility } from '@livefolio/sdk';
3421
+ *
3422
+ * // 5 price bars → 4 daily returns → 1 vol point (period=4)
3423
+ * const prices = [
3424
+ * { t: new Date('2023-01-02'), v: 100 },
3425
+ * { t: new Date('2023-01-03'), v: 101 },
3426
+ * { t: new Date('2023-01-04'), v: 99 },
3427
+ * { t: new Date('2023-01-05'), v: 102 },
3428
+ * { t: new Date('2023-01-06'), v: 100 },
3429
+ * ];
3430
+ *
3431
+ * const vol = volatility(prices, 4);
3432
+ * // vol.length === 1
3433
+ * // vol[0].t => new Date('2023-01-06')
3434
+ * // vol[0].v => population std-dev of the 4 daily returns (≈ 0.012)
3435
+ * ```
3436
+ */
3437
+ declare function volatility(series: Series, period: number): Series;
715
3438
 
716
- declare function computeDrawdownTable(series: DailyBar[], topN: number): DrawdownEntry[];
3439
+ /**
3440
+ * Computes the rolling drawdown relative to the period high for each bar.
3441
+ *
3442
+ * Math definition:
3443
+ * ```
3444
+ * rollingMax[i] = max(series[i-period+1], ..., series[i])
3445
+ * drawdown[i] = (series[i] - rollingMax[i]) / rollingMax[i]
3446
+ * ```
3447
+ *
3448
+ * The result is a non-positive fraction (e.g. `-0.15` means the current price
3449
+ * is 15 % below the period high). A value of `0` means the current price equals
3450
+ * the rolling maximum — i.e. the asset is at a new high within the window.
3451
+ *
3452
+ * Warmup: the first output point corresponds to input index `period - 1` (the
3453
+ * first complete window). The output array is shorter than the input by
3454
+ * `period - 1` points (no `undefined` placeholders).
3455
+ *
3456
+ * Edge cases:
3457
+ * - `period <= 0` — throws `Error`.
3458
+ * - `series.length < period` — returns `[]`.
3459
+ * - All prices in a window are equal — returns `0` (current equals max).
3460
+ * - Zero prices in the window — produces `NaN` (division by zero); callers
3461
+ * should guard against zero-price series.
3462
+ *
3463
+ * @param series - Input price series sorted in ascending timestamp order. Values
3464
+ * should be positive (non-zero) to avoid `NaN` results.
3465
+ * @param period - Rolling window size in bars. Must be a positive integer.
3466
+ * A value of `1` always returns `0` (current equals one-bar max).
3467
+ * @returns A `Series` of length `max(0, series.length - period + 1)`. Each
3468
+ * point's timestamp `t` is taken from `series[i]` (the last bar in its window).
3469
+ * Values are in the range `(-∞, 0]` but in practice within `[-1, 0]` for
3470
+ * positive price series.
3471
+ *
3472
+ * @example
3473
+ * ```ts
3474
+ * import { drawdown } from '@livefolio/sdk';
3475
+ *
3476
+ * const prices = [
3477
+ * { t: new Date('2023-01-02'), v: 100 },
3478
+ * { t: new Date('2023-01-03'), v: 105 },
3479
+ * { t: new Date('2023-01-04'), v: 95 },
3480
+ * { t: new Date('2023-01-05'), v: 98 },
3481
+ * ];
3482
+ *
3483
+ * const dd = drawdown(prices, 3);
3484
+ * // dd.length === 2
3485
+ * // dd[0].t => new Date('2023-01-04'), dd[0].v => (95-105)/105 ≈ -0.095
3486
+ * // dd[1].t => new Date('2023-01-05'), dd[1].v => (98-105)/105 ≈ -0.067
3487
+ * ```
3488
+ */
3489
+ declare function drawdown(series: Series, period: number): Series;
717
3490
 
718
- interface MonthlyReturn {
719
- year: number;
720
- month: number;
721
- return: number;
722
- partial: boolean;
723
- }
724
- interface YearlyReturn {
725
- year: number;
726
- return: number;
727
- partial: boolean;
3491
+ type index_BarField = BarField;
3492
+ type index_ComputeFn = ComputeFn;
3493
+ type index_FeatureKind = FeatureKind;
3494
+ type index_FeatureRuntime = FeatureRuntime;
3495
+ declare const index_FeatureRuntime: typeof FeatureRuntime;
3496
+ type index_FeatureRuntimeOptions = FeatureRuntimeOptions;
3497
+ type index_FeatureSpec = FeatureSpec;
3498
+ type index_ReturnMode = ReturnMode;
3499
+ declare const index_barsToSeries: typeof barsToSeries;
3500
+ declare const index_collectBars: typeof collectBars;
3501
+ declare const index_defineFeature: typeof defineFeature;
3502
+ declare const index_drawdown: typeof drawdown;
3503
+ declare const index_ema: typeof ema;
3504
+ declare const index_getFeatureCompute: typeof getFeatureCompute;
3505
+ declare const index_paramsHash: typeof paramsHash;
3506
+ declare const index_returnSeries: typeof returnSeries;
3507
+ declare const index_rsi: typeof rsi;
3508
+ declare const index_seriesAt: typeof seriesAt;
3509
+ declare const index_sma: typeof sma;
3510
+ declare const index_volatility: typeof volatility;
3511
+ declare namespace index {
3512
+ export { type index_BarField as BarField, type index_ComputeFn as ComputeFn, type index_FeatureKind as FeatureKind, index_FeatureRuntime as FeatureRuntime, type index_FeatureRuntimeOptions as FeatureRuntimeOptions, type index_FeatureSpec as FeatureSpec, type index_ReturnMode as ReturnMode, index_barsToSeries as barsToSeries, index_collectBars as collectBars, index_defineFeature as defineFeature, index_drawdown as drawdown, index_ema as ema, index_getFeatureCompute as getFeatureCompute, index_paramsHash as paramsHash, index_returnSeries as returnSeries, index_rsi as rsi, index_seriesAt as seriesAt, index_sma as sma, index_volatility as volatility };
728
3513
  }
729
- declare function monthlyReturns(series: DailyBar[]): MonthlyReturn[];
730
- declare function yearlyReturns(series: DailyBar[]): YearlyReturn[];
731
3514
 
732
- export { AllocationHandle, type Comparison, type DailyBar, type DateRange, type DrawdownEntry, IndicatorHandle, type IndicatorIdentity, type IndicatorType, type LiveEvaluator, type LivePreviewState, type LiveRuleState, type LiveSignalState, type LivefolioClient, type LivefolioClientOptions, type MarketProvider, type MetricsOptions, type MetricsResult, type MonthlyReturn, type MonthlyReturnsTable, PortfolioHandle, type PortfolioSnapshot, type PriceStream, SignalHandle, type SignalIdentity, type SimulateOptions, SimulationHandle, type StorageProvider, type StrategyBar, type StrategyDefinition, StrategyHandle, type StrategyLiveState, type StrategyOptions, type StrategyReferenceData, type StrategyRule, type StrategyRuleDefinition, type StrategySeriesEntry, type StreamStatus, TickerHandle, type Trade, type TradingFreq, type Unit, type YearlyReturn, allocationsEqual, computeDrawdownTable, computeMetrics, monthlyReturns as computeMonthlyReturns, computeRebalanceDates, sharpe as computeSharpe, sortino as computeSortino, yearlyReturns as computeYearlyReturns, createClient };
3515
+ /**
3516
+ * Applies a batch of confirmed fills to a portfolio, returning a new
3517
+ * {@link Portfolio} snapshot. This is the single function that advances
3518
+ * portfolio state after order execution.
3519
+ *
3520
+ * For each fill the corresponding order is looked up in `orders` by
3521
+ * `fill.orderRef`. The order's `kind` determines the accounting treatment:
3522
+ * - `'open'` — adds a new {@link Position} and debits cash.
3523
+ * - `'close'` — removes shares from an existing position and credits cash.
3524
+ * - `'adjust'` — updates the position's `quantity`; only fees are debited.
3525
+ * - `'rebalance'` — buys or sells shares in the long position for `asset`;
3526
+ * creates or removes the position as needed.
3527
+ *
3528
+ * The returned `portfolio.t` is updated to the maximum fill timestamp.
3529
+ *
3530
+ * @param portfolio - The current portfolio state before this batch.
3531
+ * @param fills - Execution confirmations returned by {@link Executor.submit}.
3532
+ * Each fill's `orderRef` MUST match an `id` in `orders`.
3533
+ * @param orders - The full order batch that was submitted. Used to look up
3534
+ * order details for each fill.
3535
+ * @returns A new {@link Portfolio} with updated positions, cash, and timestamp.
3536
+ * The input `portfolio` is not mutated.
3537
+ *
3538
+ * @example
3539
+ * ```ts
3540
+ * import { applyFills } from '@livefolio/sdk';
3541
+ * import type { Portfolio, Order, Fill } from '@livefolio/sdk';
3542
+ *
3543
+ * const portfolio: Portfolio = { cash: 10_000, positions: [], t: new Date('2024-01-01') };
3544
+ *
3545
+ * const order: Order = {
3546
+ * id: 'ord_1', kind: 'open',
3547
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
3548
+ * side: 'long', quantity: 10,
3549
+ * };
3550
+ * const fill: Fill = { orderRef: 'ord_1', t: new Date('2024-01-02'), quantity: 10, price: 185, fees: 0 };
3551
+ *
3552
+ * const next = applyFills(portfolio, [fill], [order]);
3553
+ * // next.cash === 8_250, next.positions.length === 1
3554
+ * ```
3555
+ */
3556
+ declare function applyFills(portfolio: Portfolio, fills: ReadonlyArray<Fill>, orders: ReadonlyArray<Order>): Portfolio;
3557
+ /**
3558
+ * Projects a portfolio forward through a set of pending (unfilled) orders,
3559
+ * returning a structurally updated snapshot. Used by strategy build helpers
3560
+ * to read the expected post-step state before fills arrive.
3561
+ *
3562
+ * **v0.4 contract — structural projection only.** Quantities are updated
3563
+ * exactly as the orders specify, but:
3564
+ * - `cash` is left unchanged (no price is available at projection time).
3565
+ * - Newly opened positions have `basis: 0` and `entry.price: 0` as
3566
+ * provisional values. A price-aware projection is planned for a later phase.
3567
+ *
3568
+ * Use {@link applyFills} (not this function) to settle the portfolio after
3569
+ * confirmed execution.
3570
+ *
3571
+ * @param portfolio - The current portfolio state to project from.
3572
+ * @param orders - The pending orders to apply structurally. Order must have
3573
+ * a valid `id` and `kind`; price fields are ignored.
3574
+ * @returns A new {@link Portfolio} with positions reflecting the orders.
3575
+ * `cash` and `t` are copied unchanged from `portfolio`.
3576
+ *
3577
+ * @example
3578
+ * ```ts
3579
+ * import { applyOrders } from '@livefolio/sdk';
3580
+ * import type { Portfolio, Order } from '@livefolio/sdk';
3581
+ *
3582
+ * const portfolio: Portfolio = { cash: 10_000, positions: [], t: new Date('2024-01-01') };
3583
+ *
3584
+ * const order: Order = {
3585
+ * id: 'ord_1', kind: 'open',
3586
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
3587
+ * side: 'long', quantity: 10,
3588
+ * };
3589
+ *
3590
+ * const projected = applyOrders(portfolio, [order]);
3591
+ * // projected.positions.length === 1, projected.cash === 10_000 (unchanged)
3592
+ * ```
3593
+ */
3594
+ declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>): Portfolio;
3595
+
3596
+ export { type AdhocTimeOverrides, type AdjustOrder, type AllocateNode, type Asset, type AssetId, type AssetRef, BacktestExecutor, type BacktestExecutorOptions, type BacktestResult, type BacktestSnapshot, type Bar, type BarField, type Calendar, type CloseOrder, type Comparison, type ComparisonOp, type ComputeFn, Crypto24x7Calendar, type DataEvent, type DataFeed, type DateRange, type EquityAsset, type EventKind, ExchangeCalendar, type ExchangeName, type Executor, type FeatureCache, type FeatureKey, type FeatureKind, type FeatureRef, FeatureRuntime, type FeatureRuntimeOptions, type FeatureScope, type FeatureSpec, type Features, type Fill, type Frequency, type FromSpecOptions, type Fundamentals, type HolidayRule, type IfNode, LSEExchangeCalendar, type LiveEvent, type MacroAsset, MemoryFeatureCache, NYSEExchangeCalendar, type NextOpenFn, type OpenOrder, type Order, type PollingSchedule, type PollingStreamOptions, type Portfolio, type Position, type PositionId, type PriceMap, type Quote, type QuoteFeed, type RebalanceConfig, type RebalanceFrequency, type RebalanceOrder, type ReturnMode, RoutingDataFeed, RoutingDataFeedError, type RoutingDataFeedRouteFn, type RoutingDataFeedRouteMap, RoutingQuoteFeed, RoutingQuoteFeedError, type RoutingQuoteFeedRouteFn, type RoutingQuoteFeedRouteMap, RoutingStreamingDataFeed, RoutingStreamingDataFeedError, type RoutingStreamingDataFeedRouteFn, type RoutingStreamingDataFeedRouteMap, type RuleNode, type RuleTreeState, type RunBacktestOptions, type RunLiveOptions, type Series, type Session, type SpecialClose, type SpecialOpen, type Strategy, type StreamingBar, type StreamingDataFeed, type SyntheticAsset, type TacticalFeatureKind, type TacticalFeatureSpec, type TacticalFeatures, type TacticalSpec, type TargetWeights, type TimeOfDay, type Tolerance, type WithStreamingSyntheticsOptions, applyFills, applyOrders, barsToSeries, collectBars, defineFeature, drawdown, ema, evaluateFeatureSpecs, evaluateRuleTree, index as features, fromSpec, getCalendar, getFeatureCompute, isRebalanceDay, paramsHash, periodKey, pollingStreamFromHistorical, reconcile, returnSeries, rsi, runBacktest, runLive, seriesAt, sma, index$1 as tactical, volatility, withStreamingSynthetics, withSynthetics };