@livefolio/sdk 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,732 +1,3396 @@
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;
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;
379
897
  }
380
- interface DrawdownEntry {
381
- peakDate: string;
382
- troughDate: string;
383
- recoveryDate: string | null;
384
- depth: number;
385
- durationDays: number;
386
- underwaterDays: number;
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>;
387
1014
  }
388
- interface MonthlyReturnsTable {
389
- rows: Array<{
390
- year: number;
391
- months: (number | null)[];
392
- ytd: number | null;
393
- }>;
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>;
394
1529
  }
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
- };
1530
+
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>;
452
1750
  }
453
1751
 
454
- interface SimulateOptions {
455
- from: string;
456
- to: string;
457
- portfolio: PortfolioHandle;
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
+ * In-process, Map-backed implementation of {@link FeatureCache}. Caches
1889
+ * computed indicator series in memory for the lifetime of the instance.
1890
+ * There is no eviction policy — the cache grows until the instance is
1891
+ * garbage-collected.
1892
+ *
1893
+ * **When to use**: the right choice for single-run backtests or unit tests
1894
+ * where the full dataset fits in process memory and cross-run persistence is
1895
+ * not required. For long-running hosted services or multi-process setups,
1896
+ * substitute a persistent implementation (e.g. Redis-backed) that satisfies
1897
+ * the {@link FeatureCache} interface.
1898
+ *
1899
+ * Cache keys are content-addressed strings composed of `(feature kind, params
1900
+ * hash, asset scope, date range, frequency)` — see the internal
1901
+ * `canonicalKey` function. The `invalidate` method performs prefix-based
1902
+ * deletion using the same key segments.
1903
+ *
1904
+ * @example
1905
+ * ```ts
1906
+ * import { MemoryFeatureCache } from '@livefolio/sdk';
1907
+ * import { FeatureRuntime } from '@livefolio/sdk/features';
1908
+ *
1909
+ * const cache = new MemoryFeatureCache();
1910
+ * const runtime = new FeatureRuntime({ feed: myDataFeed, cache });
1911
+ * ```
1912
+ */
1913
+ declare class MemoryFeatureCache implements FeatureCache {
1914
+ private store;
1915
+ get(key: FeatureKey): Promise<Series | undefined>;
1916
+ set(key: FeatureKey, series: Series): Promise<void>;
1917
+ invalidate(prefix: Partial<FeatureKey>): Promise<void>;
458
1918
  }
459
- interface Trade {
460
- date: string;
461
- symbol: string;
462
- quantity: number;
1919
+
1920
+ /**
1921
+ * Callback that resolves the next-open price for `asset` as seen from date `t`.
1922
+ * {@link BacktestExecutor} calls this once per order to determine the fill price.
1923
+ *
1924
+ * The function should return the opening price of the first trading session
1925
+ * strictly after `t`, along with that session's timestamp. In a typical
1926
+ * backtest setup this reads from the same data feed used to compute features.
1927
+ *
1928
+ * @param asset - The instrument being filled.
1929
+ * @param t - The date on which the rebalance order was submitted (the
1930
+ * "signal date"). The fill should occur on the next open after
1931
+ * this date to avoid look-ahead.
1932
+ * @returns An object with the fill timestamp `t` and the opening `price`.
1933
+ */
1934
+ type NextOpenFn = (asset: Asset, t: Date) => Promise<{
1935
+ t: Date;
463
1936
  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>;
477
- }
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;
489
- }
490
- /** Per-rule collection of live signal states, in the same order as the rule's `when` list. */
491
- interface LiveRuleState {
492
- signals: LiveSignalState[];
493
- }
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[];
499
- }
1937
+ }>;
1938
+ /**
1939
+ * Constructor options for {@link BacktestExecutor}.
1940
+ */
1941
+ type BacktestExecutorOptions = {
1942
+ /** Exchange calendar used to route fills to the next open session. */
1943
+ calendar: Calendar;
1944
+ /**
1945
+ * Callback that resolves the next-open price for a given asset and date.
1946
+ * See {@link NextOpenFn} for the exact contract.
1947
+ */
1948
+ nextOpen: NextOpenFn;
1949
+ /**
1950
+ * One-way slippage in basis points applied to every fill. The fill price is
1951
+ * adjusted by `price × (1 + sign × slippageBps / 10 000)` where `sign` is
1952
+ * `+1` for buys and `−1` for sells. Defaults to `0`.
1953
+ */
1954
+ slippageBps?: number;
1955
+ /**
1956
+ * Flat per-share commission in the portfolio's base currency. Multiplied by
1957
+ * the fill quantity and recorded in `Fill.fees`. Defaults to `0`.
1958
+ */
1959
+ perShareFee?: number;
1960
+ };
500
1961
  /**
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.
1962
+ * Reference {@link Executor} implementation for backtesting. Fills each order
1963
+ * at the next-open price returned by the {@link NextOpenFn} callback, with
1964
+ * optional slippage and per-share commissions applied.
1965
+ *
1966
+ * **When to use**: suitable for historical simulations and unit tests where
1967
+ * real broker connectivity is not needed. For live or paper trading, substitute
1968
+ * a broker-backed `Executor` that satisfies the same interface.
1969
+ *
1970
+ * **Fill mechanics**: for each order in `orders`, the executor calls
1971
+ * `opts.nextOpen(asset, t)` to obtain the fill price and timestamp. The
1972
+ * raw price is then adjusted for slippage:
1973
+ * ```
1974
+ * adjustedPrice = nextOpen.price × (1 + sign × slippageBps / 10 000)
1975
+ * ```
1976
+ * where `sign` is `+1` for net-buy direction and `−1` for net-sell direction.
1977
+ * A flat per-share fee is added to `Fill.fees`. Orders with zero quantity are
1978
+ * silently skipped.
1979
+ *
1980
+ * @example
1981
+ * ```ts
1982
+ * import { BacktestExecutor } from '@livefolio/sdk';
1983
+ * import { getCalendar } from '@livefolio/sdk';
1984
+ *
1985
+ * const executor = new BacktestExecutor({
1986
+ * calendar: getCalendar('NYSE'),
1987
+ * nextOpen: async (asset, t) => {
1988
+ * // Return the first open bar strictly after t from your data feed.
1989
+ * const bar = await feed.nextBar(asset, t);
1990
+ * return { t: bar.t, price: bar.open };
1991
+ * },
1992
+ * slippageBps: 5, // 0.05% one-way
1993
+ * perShareFee: 0.005,
1994
+ * });
1995
+ * ```
504
1996
  */
505
- interface LivePreviewState extends StrategyLiveState {
506
- snapshot: PortfolioSnapshot;
1997
+ declare class BacktestExecutor implements Executor {
1998
+ private readonly opts;
1999
+ constructor(opts: BacktestExecutorOptions);
2000
+ submit(orders: ReadonlyArray<Order>, t: Date, portfolio: Portfolio): Promise<ReadonlyArray<Fill>>;
507
2001
  }
2002
+
508
2003
  /**
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.
2004
+ * Error thrown by {@link RoutingDataFeed} when an asset cannot be routed or
2005
+ * when the routed feed does not support the requested optional method.
2006
+ *
2007
+ * Distinguish the two cases via the message text: "no feed registered" vs
2008
+ * "does not implement fundamentals".
513
2009
  */
514
- interface LiveEvaluator {
515
- previewLiveState(date: string, overrides: Record<string, number>): Promise<StrategyLiveState>;
2010
+ declare class RoutingDataFeedError extends Error {
2011
+ constructor(message: string);
516
2012
  }
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>;
2013
+ /** Function form of the routing rule. Returns the feed for `asset`, or `undefined` when no feed handles it. */
2014
+ type RoutingDataFeedRouteFn = (asset: Asset) => DataFeed | undefined;
2015
+ /** Map form of the routing rule. Keys are `Asset['kind']` discriminants. */
2016
+ type RoutingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], DataFeed>>>;
2017
+ /**
2018
+ * A {@link DataFeed} that delegates each call to one of several underlying
2019
+ * feeds based on the asset. Use this to compose vendors — e.g. Yahoo for
2020
+ * equities and FRED for macro series — behind a single `DataFeed` instance
2021
+ * accepted by `runBacktest`, `FeatureRuntime`, and `BacktestExecutor`.
2022
+ *
2023
+ * Routing rules:
2024
+ * - **Map form:** `new RoutingDataFeed({ equity: yahoo, macro: fred })`.
2025
+ * Keys are `asset.kind` discriminants. The 90% case.
2026
+ * - **Function form:** `new RoutingDataFeed((a) => a.kind === 'macro' ? fred : yahoo)`.
2027
+ * Use when routing depends on more than `kind` (e.g. allowlists).
2028
+ *
2029
+ * The router does **not** implement `events()` the optional method is
2030
+ * genuinely absent (`'events' in router === false`). Cross-feed event
2031
+ * fan-out is deferred until a real consumer materializes.
2032
+ *
2033
+ * @example
2034
+ * ```ts
2035
+ * import { RoutingDataFeed } from '@livefolio/sdk';
2036
+ *
2037
+ * const feed = new RoutingDataFeed({ equity: yahooFeed, macro: fredFeed });
2038
+ *
2039
+ * const result = await runBacktest({
2040
+ * strategy, range, initialPortfolio,
2041
+ * dataFeed: feed,
2042
+ * executor,
2043
+ * calendar,
2044
+ * });
2045
+ * ```
2046
+ */
2047
+ declare class RoutingDataFeed implements DataFeed {
2048
+ private readonly route;
2049
+ constructor(routes: RoutingDataFeedRouteMap | RoutingDataFeedRouteFn);
2050
+ bars(asset: Asset, range: DateRange, freq: Frequency): AsyncGenerator<Bar>;
2051
+ fundamentals(asset: Asset, t: Date): Promise<Fundamentals>;
2052
+ private resolve;
552
2053
  }
553
2054
 
554
- interface StrategyRule {
555
- when?: SignalHandle[];
556
- hold: AllocationHandle;
2055
+ /**
2056
+ * Error thrown by {@link RoutingStreamingDataFeed} when an asset cannot be routed.
2057
+ */
2058
+ declare class RoutingStreamingDataFeedError extends Error {
2059
+ constructor(message: string);
557
2060
  }
558
- interface StrategyBar {
559
- date: string;
560
- allocation: AllocationHandle;
2061
+ /** Function form of the routing rule. Returns the feed for `asset`, or `undefined` when no feed handles it. */
2062
+ type RoutingStreamingDataFeedRouteFn = (asset: Asset) => StreamingDataFeed | undefined;
2063
+ /** Map form of the routing rule. Keys are `Asset['kind']` discriminants. */
2064
+ type RoutingStreamingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], StreamingDataFeed>>>;
2065
+ /**
2066
+ * A {@link StreamingDataFeed} that delegates `subscribe()` to one of several
2067
+ * underlying feeds based on the asset. Use this to compose vendors — e.g.
2068
+ * Polygon for equities and a polling adapter for macro series — behind a
2069
+ * single `StreamingDataFeed` instance accepted by `runLive`.
2070
+ *
2071
+ * Routing rules:
2072
+ * - **Map form:** `new RoutingStreamingDataFeed({ equity: polygon, macro: polling })`.
2073
+ * Keys are `asset.kind` discriminants. The 90% case.
2074
+ * - **Function form:** `new RoutingStreamingDataFeed((a) => a.kind === 'macro' ? polling : polygon)`.
2075
+ * Use when routing depends on more than `kind` (e.g. allowlists).
2076
+ *
2077
+ * Assets are grouped by routed feed (by reference identity) before calling
2078
+ * upstream `subscribe()` — so a vendor adapter that opens one socket for
2079
+ * `[AAPL, MSFT]` keeps doing that rather than receiving one-asset-at-a-time calls.
2080
+ *
2081
+ * @example
2082
+ * ```ts
2083
+ * import { RoutingStreamingDataFeed, pollingStreamFromHistorical } from '@livefolio/sdk';
2084
+ *
2085
+ * const feed = new RoutingStreamingDataFeed({
2086
+ * equity: polygonStreaming,
2087
+ * macro: pollingStreamFromHistorical({ feed: fredHistorical, freq: '1d', schedule: { kind: 'session-close', calendar: nyse } }),
2088
+ * });
2089
+ * ```
2090
+ */
2091
+ declare class RoutingStreamingDataFeed implements StreamingDataFeed {
2092
+ private readonly route;
2093
+ constructor(routes: RoutingStreamingDataFeedRouteMap | RoutingStreamingDataFeedRouteFn);
2094
+ subscribe(assets: ReadonlyArray<Asset>): AsyncIterable<StreamingBar>;
2095
+ private merged;
561
2096
  }
562
- interface StrategyOptions {
2097
+
2098
+ type PollingSchedule = {
2099
+ kind: 'interval';
2100
+ intervalMs: number;
2101
+ } | {
2102
+ kind: 'session-close';
2103
+ calendar: Calendar;
2104
+ };
2105
+ type PollingStreamOptions = {
2106
+ /** Historical feed to poll. Each tick of the schedule calls `feed.bars(asset, …)` for each subscribed asset. */
2107
+ feed: DataFeed;
2108
+ /** Bar frequency to request. Single value — multi-frequency requires composing two polling streams via `RoutingStreamingDataFeed`. */
2109
+ freq: Frequency;
2110
+ /** When to poll. */
2111
+ schedule: PollingSchedule;
2112
+ /**
2113
+ * Window-start for the first poll per asset. Subsequent polls fetch
2114
+ * `(lastSeenT, now]` per asset. Defaults to `new Date(0)` — every bar the
2115
+ * feed has on the first poll is yielded. For replay-then-stream, set this
2116
+ * to your backtest range's `to` so polling picks up exactly where the
2117
+ * backtest left off.
2118
+ */
2119
+ initialFrom?: Date;
2120
+ /** Inject for tests or for accelerated-time simulations. Defaults to `() => new Date()`. */
2121
+ now?: () => Date;
2122
+ /** Inject for tests or for accelerated-time simulations. Defaults to `setTimeout`-based promise. */
2123
+ sleep?: (ms: number) => Promise<void>;
2124
+ };
2125
+ declare function pollingStreamFromHistorical(opts: PollingStreamOptions): StreamingDataFeed;
2126
+
2127
+ /**
2128
+ * A year-derived holiday rule consumed by {@link ExchangeCalendar}. The rule
2129
+ * is active only for years in the range `[validFrom, validUntil]` (both
2130
+ * inclusive; omit either bound to leave it open).
2131
+ *
2132
+ * `resolve(year)` returns the UTC midnight `Date` for the holiday in that year,
2133
+ * or `null` if the holiday does not occur that year (e.g. a conditional rule
2134
+ * for Good Friday in certain years). When `observe` is `true`, a Saturday
2135
+ * result is moved to Friday and a Sunday result is moved to Monday (standard
2136
+ * US-style holiday observation).
2137
+ */
2138
+ type HolidayRule = {
2139
+ /** Human-readable name, used for debugging and logging. */
563
2140
  name: string;
564
- freq?: TradingFreq;
565
- offset?: number;
566
- rules: StrategyRule[];
567
- }
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;
658
- }
2141
+ /** Returns the UTC midnight `Date` for this holiday in `year`, or `null` to skip. */
2142
+ resolve: (year: number) => Date | null;
2143
+ /** First year (inclusive) this rule applies. Defaults to −∞. */
2144
+ validFrom?: number;
2145
+ /** Last year (inclusive) this rule applies. Defaults to +∞. */
2146
+ validUntil?: number;
2147
+ /** When `true`, Saturday dates are moved to Friday, Sunday dates to Monday. */
2148
+ observe?: boolean;
2149
+ };
2150
+ /**
2151
+ * A year-derived early-close rule consumed by {@link ExchangeCalendar}. Follows
2152
+ * the same validity bounds and `resolve` contract as {@link HolidayRule}, but
2153
+ * instead of marking a day closed entirely it overrides the session close time
2154
+ * to `closeAt` for the matched date.
2155
+ */
2156
+ type SpecialClose = {
2157
+ /** Human-readable name, used for debugging and logging. */
2158
+ name: string;
2159
+ /** Returns the UTC midnight `Date` for this early-close day in `year`, or `null` to skip. */
2160
+ resolve: (year: number) => Date | null;
2161
+ /** The overridden close time in local exchange time. */
2162
+ closeAt: TimeOfDay;
2163
+ /** First year (inclusive) this rule applies. Defaults to −∞. */
2164
+ validFrom?: number;
2165
+ /** Last year (inclusive) this rule applies. Defaults to +∞. */
2166
+ validUntil?: number;
2167
+ };
2168
+ /**
2169
+ * A year-derived late-open rule consumed by {@link ExchangeCalendar}. Follows
2170
+ * the same validity bounds and `resolve` contract as {@link HolidayRule}, but
2171
+ * overrides the session open time to `openAt` for the matched date.
2172
+ */
2173
+ type SpecialOpen = {
2174
+ /** Human-readable name, used for debugging and logging. */
2175
+ name: string;
2176
+ /** Returns the UTC midnight `Date` for this late-open day in `year`, or `null` to skip. */
2177
+ resolve: (year: number) => Date | null;
2178
+ /** The overridden open time in local exchange time. */
2179
+ openAt: TimeOfDay;
2180
+ /** First year (inclusive) this rule applies. Defaults to −∞. */
2181
+ validFrom?: number;
2182
+ /** Last year (inclusive) this rule applies. Defaults to +∞. */
2183
+ validUntil?: number;
2184
+ };
2185
+ /**
2186
+ * Map of `YYYY-MM-DD` date strings to override times. Used for one-off
2187
+ * historical specials (e.g. a single early close due to a snowstorm) that do
2188
+ * not fit a repeating year-derived rule. Keys must be in `YYYY-MM-DD` format
2189
+ * in UTC.
2190
+ */
2191
+ type AdhocTimeOverrides = ReadonlyMap<string, TimeOfDay>;
659
2192
 
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;
2193
+ /**
2194
+ * Abstract base class for exchange trading calendars. Implements the full
2195
+ * {@link Calendar} interface by composing up to nine overridable hooks that
2196
+ * subclasses provide. Concrete implementations ship for {@link NYSEExchangeCalendar}
2197
+ * and {@link LSEExchangeCalendar}; additional exchanges can be added by
2198
+ * extending this class.
2199
+ *
2200
+ * **Per-year caching**: holiday sets and special-session maps are computed once
2201
+ * per calendar year and stored in private Maps, so repeated calls to `isOpen`,
2202
+ * `next`, or `sessions` within the same year are cheap.
2203
+ *
2204
+ * **Hook resolution order** (adhoc beats rule, rule beats regular):
2205
+ * 1. `adhocHolidays()` / `specialClosesAdhoc()` / `specialOpensAdhoc()` —
2206
+ * `YYYY-MM-DD` string sets/maps populated once at first access.
2207
+ * 2. `regularHolidays()` / `specialCloses()` / `specialOpens()` —
2208
+ * year-derived rule arrays applied per year via the resolver helpers.
2209
+ * 3. `regularOpen(date)` / `regularClose(date)` / `weekmask(date)` —
2210
+ * per-date fallbacks that subclasses override to encode era-varying session
2211
+ * times and trading-day sets.
2212
+ *
2213
+ * **Extending**: override only the hooks you need. All hooks have no-op / sensible
2214
+ * defaults (Mon–Fri weekmask, 09:30–16:00 session) so a minimal subclass need
2215
+ * only set `name`, `tz`, and `regularHolidays()`.
2216
+ */
2217
+ declare abstract class ExchangeCalendar implements Calendar {
2218
+ /** Short exchange name used as the registry key in {@link getCalendar}. */
2219
+ abstract readonly name: string;
2220
+ /** IANA timezone identifier, e.g. `'America/New_York'` or `'Europe/London'`. */
2221
+ abstract readonly tz: string;
2222
+ private readonly holidayCache;
2223
+ private readonly specialCloseCache;
2224
+ private readonly specialOpenCache;
2225
+ private adhocHolidaysCache;
2226
+ private adhocSpecialClosesCache;
2227
+ private adhocSpecialOpensCache;
2228
+ /**
2229
+ * Returns the ordered list of year-derived holiday rules for this exchange.
2230
+ * The base implementation returns an empty array (no regular holidays). Override
2231
+ * to supply the full rule set; each {@link HolidayRule} in the array is applied
2232
+ * via {@link resolveHolidays} once per calendar year and cached. Rules may be
2233
+ * era-bounded via `validFrom` / `validUntil`.
2234
+ */
2235
+ protected regularHolidays(): ReadonlyArray<HolidayRule>;
2236
+ /**
2237
+ * Returns the set of `YYYY-MM-DD` strings for one-off full-day closures that
2238
+ * do not fit a repeating rule (e.g. presidential funerals, natural disasters).
2239
+ * The base implementation returns an empty set. Override with the complete
2240
+ * historical adhoc list for the exchange. This method is called at most once
2241
+ * per `ExchangeCalendar` instance; the result is cached.
2242
+ */
2243
+ protected adhocHolidays(): ReadonlySet<string>;
2244
+ /**
2245
+ * Returns the ordered list of year-derived early-close rules for this exchange.
2246
+ * The base implementation returns an empty array. Override to supply rules such
2247
+ * as "day after Thanksgiving closes at 13:00". Results are computed once per
2248
+ * year and cached; each rule is applied via {@link resolveSpecialCloses}.
2249
+ */
2250
+ protected specialCloses(): ReadonlyArray<SpecialClose>;
2251
+ /**
2252
+ * Returns the map of `YYYY-MM-DD` strings to override close times for
2253
+ * one-off early-close days that do not fit a repeating rule. The base
2254
+ * implementation returns an empty map. Override with the historical adhoc
2255
+ * set for the exchange. Called at most once per instance; result is cached.
2256
+ */
2257
+ protected specialClosesAdhoc(): AdhocTimeOverrides;
2258
+ /**
2259
+ * Returns the ordered list of year-derived late-open rules for this exchange.
2260
+ * The base implementation returns an empty array. Override to supply rules such
2261
+ * as "delayed open due to a moment of silence". Results are computed once per
2262
+ * year and cached; each rule is applied via {@link resolveSpecialOpens}.
2263
+ */
2264
+ protected specialOpens(): ReadonlyArray<SpecialOpen>;
2265
+ /**
2266
+ * Returns the map of `YYYY-MM-DD` strings to override open times for
2267
+ * one-off late-open days that do not fit a repeating rule. The base
2268
+ * implementation returns an empty map. Override with the historical adhoc
2269
+ * set for the exchange. Called at most once per instance; result is cached.
2270
+ */
2271
+ protected specialOpensAdhoc(): AdhocTimeOverrides;
2272
+ /**
2273
+ * Returns the default open time in local exchange time for `date` when no
2274
+ * special-open rule matches. The base implementation returns 09:30. Override
2275
+ * to encode era-varying session times (e.g. NYSE opened at 10:00 before
2276
+ * 1985-09-30).
2277
+ *
2278
+ * @param date - UTC midnight `Date` for the trading day being queried.
2279
+ */
2280
+ protected regularOpen(_date: Date): TimeOfDay;
2281
+ /**
2282
+ * Returns the default close time in local exchange time for `date` when no
2283
+ * special-close rule matches. The base implementation returns 16:00. Override
2284
+ * to encode era-varying session times (e.g. NYSE closed at 15:00 before
2285
+ * 1952-09-29, and at 15:30 until 1974-01-02).
2286
+ *
2287
+ * @param date - UTC midnight `Date` for the trading day being queried.
2288
+ */
2289
+ protected regularClose(_date: Date): TimeOfDay;
2290
+ /**
2291
+ * Returns the set of weekday indices (using `Date.getUTCDay()` convention:
2292
+ * 0 = Sunday, 1 = Monday, …, 6 = Saturday) that are regular trading days.
2293
+ * The base implementation returns `{1, 2, 3, 4, 5}` (Mon–Fri). Override to
2294
+ * encode historical six-day trading weeks (e.g. NYSE traded Mon–Sat before
2295
+ * 1952-09-29, keyed by `date` so the shift is era-aware).
2296
+ *
2297
+ * @param date - UTC midnight `Date` for the day being tested.
2298
+ */
2299
+ protected weekmask(_date: Date): ReadonlySet<number>;
2300
+ private getAdhocHolidays;
2301
+ private getAdhocSpecialCloses;
2302
+ private getAdhocSpecialOpens;
2303
+ /**
2304
+ * Cached lookup of regular-holiday timestamps for the given year.
2305
+ * Assumes `regularHolidays()` returns the same rule list on every call.
2306
+ */
2307
+ private holidaysForYear;
2308
+ private specialClosesForYear;
2309
+ private specialOpensForYear;
2310
+ private normalize;
2311
+ /** Returns `true` when `t` falls on a regular trading day (weekmask check, then holiday check). */
2312
+ isOpen(t: Date): boolean;
2313
+ /** Returns the first trading day strictly after `t`. */
2314
+ next(t: Date): Date;
2315
+ /** Returns the first trading day strictly before `t`. */
2316
+ previous(t: Date): Date;
2317
+ /**
2318
+ * Returns UTC midnight `Date` objects for every trading day in
2319
+ * `[range.from, range.to)`. The `from` bound is inclusive; `to` is exclusive.
2320
+ */
2321
+ sessions(range: DateRange): ReadonlyArray<Date>;
2322
+ schedule(range: DateRange): ReadonlyArray<Session>;
2323
+ isEarlyClose(t: Date): boolean;
2324
+ /** Adhoc overrides win over rule-driven; both win over `regularOpen(date)`. */
2325
+ private openTimeFor;
2326
+ /** Adhoc overrides win over rule-driven; both win over `regularClose(date)`. */
2327
+ private closeTimeFor;
2328
+ private localizedTimestamp;
664
2329
  }
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;
2330
+
2331
+ /**
2332
+ * New York Stock Exchange (NYSE) trading-day calendar covering 1885-01-01 to
2333
+ * the present. Also applicable to NYSE-equivalent venues (NASDAQ, BATS, DJIA,
2334
+ * DOW). Faithful port of `pandas_market_calendars`' `nyse.py`.
2335
+ *
2336
+ * **Era boundaries:**
2337
+ * - Weekmask: Mon–Sat through 1952-09-28; Mon–Fri from 1952-09-29 onward
2338
+ * (Saturday trading retired on that date).
2339
+ * - Regular open: 10:00 ET before 1985-09-30; 09:30 ET from 1985-09-30 onward.
2340
+ * - Regular close: 15:00 ET before 1952-09-29; 15:30 ET through 1973-12-31;
2341
+ * 16:00 ET from 1974-01-02 onward. Saturday closes (pre-1952) are
2342
+ * approximated as 12:00.
2343
+ *
2344
+ * Holiday coverage includes the full set of regular (rule-derived) and adhoc
2345
+ * (literal date set) closures sourced from `pandas_market_calendars`, spanning
2346
+ * historical events from the Ulysses Grant funeral (1885) through the Jimmy
2347
+ * Carter national day of mourning (2025).
2348
+ */
2349
+ declare class NYSEExchangeCalendar extends ExchangeCalendar {
2350
+ readonly name = "NYSE";
2351
+ readonly tz = "America/New_York";
2352
+ protected regularHolidays(): ReadonlyArray<HolidayRule>;
2353
+ protected adhocHolidays(): ReadonlySet<string>;
2354
+ protected specialCloses(): ReadonlyArray<SpecialClose>;
2355
+ protected specialClosesAdhoc(): AdhocTimeOverrides;
2356
+ protected specialOpens(): ReadonlyArray<SpecialOpen>;
2357
+ protected specialOpensAdhoc(): AdhocTimeOverrides;
2358
+ protected regularOpen(date: Date): TimeOfDay;
2359
+ protected regularClose(date: Date): TimeOfDay;
2360
+ protected weekmask(date: Date): ReadonlySet<number>;
687
2361
  }
688
- interface LivefolioClientOptions {
689
- storage: StorageProvider;
690
- market: MarketProvider;
2362
+
2363
+ /**
2364
+ * London Stock Exchange (LSE) trading-day calendar. Faithful port of
2365
+ * `pandas_market_calendars`' `lse.py` and `holidays/uk.py`. Historical
2366
+ * coverage begins 1801-01-01, aligned with the start of the modern exchange
2367
+ * after the Banking and Financial Dealings Act 1971 codified the current
2368
+ * bank-holiday framework.
2369
+ *
2370
+ * **Session**: 08:00–16:30 Europe/London. The exchange observes BST (UTC+1)
2371
+ * in summer and GMT (UTC+0) in winter — DST handling is delegated to luxon via
2372
+ * the `Europe/London` IANA timezone, so wall-clock session times are stable
2373
+ * across the DST transition while their UTC equivalents shift by one hour.
2374
+ *
2375
+ * **Early closes**: Christmas Eve (Dec 24) and New Year's Eve (Dec 31) close
2376
+ * at 12:30. Both use `previous_friday` observance — when the calendar date
2377
+ * falls on a weekend, the early close moves to the prior Friday.
2378
+ *
2379
+ * **Era boundaries**: bank-holiday exceptions for Royal Jubilees and VE-Day
2380
+ * anniversaries are implemented by splitting affected `Spring Bank Holiday`
2381
+ * and `Early May Bank Holiday` rules into era-bounded shards (matching
2382
+ * upstream `start_date` / `end_date` markers) and adding the displaced dates
2383
+ * as adhoc closures.
2384
+ */
2385
+ declare class LSEExchangeCalendar extends ExchangeCalendar {
2386
+ readonly name = "LSE";
2387
+ readonly tz = "Europe/London";
2388
+ protected regularHolidays(): ReadonlyArray<HolidayRule>;
2389
+ protected adhocHolidays(): ReadonlySet<string>;
2390
+ protected specialCloses(): ReadonlyArray<SpecialClose>;
2391
+ protected specialClosesAdhoc(): AdhocTimeOverrides;
2392
+ protected specialOpens(): ReadonlyArray<SpecialOpen>;
2393
+ protected specialOpensAdhoc(): AdhocTimeOverrides;
2394
+ protected regularOpen(_date: Date): TimeOfDay;
2395
+ protected regularClose(_date: Date): TimeOfDay;
2396
+ protected weekmask(_date: Date): ReadonlySet<number>;
691
2397
  }
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;
2398
+
2399
+ /**
2400
+ * Union of supported exchange names accepted by {@link getCalendar}.
2401
+ *
2402
+ * - `'NYSE'` — New York Stock Exchange (and NYSE-equivalent venues).
2403
+ * - `'LSE'` — London Stock Exchange.
2404
+ */
2405
+ type ExchangeName = 'NYSE' | 'LSE';
2406
+ /**
2407
+ * Returns a new instance of the {@link ExchangeCalendar} registered under
2408
+ * `name`. Acts as a simple factory / registry for the two built-in calendar
2409
+ * implementations.
2410
+ *
2411
+ * Supported exchange names: `'NYSE'` ({@link NYSEExchangeCalendar}) and
2412
+ * `'LSE'` ({@link LSEExchangeCalendar}). TypeScript's exhaustive switch
2413
+ * prevents unknown names from compiling.
2414
+ *
2415
+ * @param name - One of the supported {@link ExchangeName} values.
2416
+ * @returns A fresh `ExchangeCalendar` instance for the named exchange.
2417
+ *
2418
+ * @example
2419
+ * ```ts
2420
+ * import { getCalendar } from '@livefolio/sdk';
2421
+ *
2422
+ * const nyse = getCalendar('NYSE');
2423
+ * console.log(nyse.isOpen(new Date('2024-07-04'))); // false — US Independence Day
2424
+ *
2425
+ * const lse = getCalendar('LSE');
2426
+ * console.log(lse.isOpen(new Date('2024-12-25'))); // false — Christmas Day
2427
+ * ```
2428
+ */
2429
+ declare function getCalendar(name: ExchangeName): ExchangeCalendar;
2430
+
2431
+ /**
2432
+ * 24/7 calendar where every day is a single session running midnight UTC to
2433
+ * the next midnight UTC. Suitable for crypto strategies (BTC, ETH) and any
2434
+ * always-on market.
2435
+ *
2436
+ * - `isOpen(t)` always returns `true`.
2437
+ * - `next(t)` returns midnight UTC of the day after `t`.
2438
+ * - `previous(t)` returns midnight UTC of the day before `t`.
2439
+ * - `sessions(range)` returns one Date per day in `[range.from, range.to)`.
2440
+ * - `schedule(range)` returns full Sessions with `open`/`close` at midnight UTC.
2441
+ * - `isEarlyClose(t)` always returns `false`.
2442
+ */
2443
+ declare class Crypto24x7Calendar implements Calendar {
2444
+ isOpen(_t: Date): boolean;
2445
+ next(t: Date): Date;
2446
+ previous(t: Date): Date;
2447
+ sessions(range: DateRange): ReadonlyArray<Date>;
2448
+ schedule(range: DateRange): ReadonlyArray<Session>;
2449
+ isEarlyClose(_t: Date): boolean;
705
2450
  }
706
2451
 
707
- declare function allocationsEqual(a: AllocationHandle | null, b: AllocationHandle | null): boolean;
2452
+ /**
2453
+ * A reference to an asset within a {@link TacticalSpec}. Unlike the runtime
2454
+ * {@link Asset} type, `AssetRef` is the spec-form representation: it lives
2455
+ * inside serialized JSON specs and carries only the fields a spec author
2456
+ * needs to declare.
2457
+ *
2458
+ * `id` is the stable opaque identifier (see {@link AssetId}); `symbol` is the
2459
+ * human-readable ticker; `exchange` is optional. `kind` selects the asset
2460
+ * variant; absent `kind` defaults to `'equity'` for backward compatibility
2461
+ * with v0.4 specs authored before macro support landed.
2462
+ */
2463
+ type AssetRef = {
2464
+ /** Stable opaque asset identifier matching {@link AssetId}. */
2465
+ id: AssetId;
2466
+ /** Human-readable ticker symbol, e.g. `'AAPL'`. */
2467
+ symbol: string;
2468
+ /** Optional MIC or common exchange name, e.g. `'NYSE'`. Equity-only. */
2469
+ exchange?: string;
2470
+ /**
2471
+ * Asset class. Defaults to `'equity'` when omitted. Set to `'macro'` to
2472
+ * author FRED-style time-series assets that route to a non-equity
2473
+ * `DataFeed` (typically via `RoutingDataFeed`).
2474
+ */
2475
+ kind?: 'equity' | 'macro';
2476
+ };
2477
+ /**
2478
+ * A simulated leveraged or expense-adjusted asset that the runtime synthesizes
2479
+ * on-the-fly from its `underlying` data feed. The bar stream is computed by
2480
+ * {@link withSynthetics}, which wraps a real {@link DataFeed} and intercepts
2481
+ * requests for the synthetic's `id`.
2482
+ *
2483
+ * The synthesized daily close is:
2484
+ * ```
2485
+ * close_t = close_{t-1} × (1 + leverage × underlyingReturn_t) × (1 − expense/252)
2486
+ * ```
2487
+ *
2488
+ * @example
2489
+ * ```ts
2490
+ * import type { SyntheticAsset } from '@livefolio/sdk';
2491
+ *
2492
+ * const qqq3x: SyntheticAsset = {
2493
+ * id: 'QQQ_3X',
2494
+ * symbol: 'QQQ3X',
2495
+ * underlying: { id: 'QQQ', symbol: 'QQQ' },
2496
+ * leverage: 3,
2497
+ * expense: 0.0095, // 0.95% annual
2498
+ * };
2499
+ * ```
2500
+ */
2501
+ type SyntheticAsset = {
2502
+ /** Stable ID for this synthetic; must be unique in the spec universe. */
2503
+ id: AssetId;
2504
+ /** Display symbol for this synthetic. */
2505
+ symbol: string;
2506
+ /** Reference to the real asset whose returns are scaled. */
2507
+ underlying: AssetRef;
2508
+ /** Daily return multiplier (e.g. `3` for 3× leverage, `-1` for inverse). */
2509
+ leverage: number;
2510
+ /** Annual expense ratio as a decimal (e.g. `0.0095` for 0.95%). Defaults to 0. */
2511
+ expense?: number;
2512
+ /** When set, orders are routed to this proxy asset instead of the synthetic id. */
2513
+ tradeAs?: AssetRef;
2514
+ };
2515
+ /**
2516
+ * Cadence at which the strategy is allowed to rebalance.
2517
+ *
2518
+ * - `'Daily'` — every trading day
2519
+ * - `'Weekly'` — last trading day of each ISO week
2520
+ * - `'Monthly'` — last trading day of each calendar month
2521
+ * - `'Quarterly'` — last trading day of each calendar quarter
2522
+ * - `'Yearly'` — last trading day of each calendar year
2523
+ */
2524
+ type RebalanceFrequency = 'Daily' | 'Weekly' | 'Monthly' | 'Quarterly' | 'Yearly';
2525
+ /**
2526
+ * Controls when the strategy is permitted to issue rebalance orders.
2527
+ * If omitted from a {@link TacticalSpec}, the default is `{ frequency: 'Daily' }`.
2528
+ *
2529
+ * @see {@link isRebalanceDay} for the trading-calendar-aware gate implementation.
2530
+ */
2531
+ type RebalanceConfig = {
2532
+ /** How often the strategy may rebalance. */
2533
+ frequency: RebalanceFrequency;
2534
+ };
2535
+ /**
2536
+ * A single feature entry in a {@link TacticalSpec}. Each variant declares the
2537
+ * indicator kind, the asset to compute it on, and the time-series lookup
2538
+ * parameters. The `id` is the name used to reference the computed value in
2539
+ * {@link FeatureRef} inside the rule tree.
2540
+ *
2541
+ * Supported variants:
2542
+ * - `price` — most-recent closing price
2543
+ * - `sma` — simple moving average over `period` trading days
2544
+ * - `ema` — exponential moving average over `period` trading days
2545
+ * - `rsi` — relative strength index over `period` trading days
2546
+ * - `return` — cumulative or log return over `period` trading days (see {@link ReturnMode})
2547
+ * - `volatility` — annualised rolling standard deviation over `period` trading days
2548
+ * - `drawdown` — peak-to-trough drawdown over `period` trading days
2549
+ *
2550
+ * The optional `delay` shifts the lookup back by that many bars (0 = current
2551
+ * bar, 1 = previous bar, etc.). Useful for avoiding look-ahead bias when using
2552
+ * end-of-day prices.
2553
+ */
2554
+ type TacticalFeatureSpec = {
2555
+ id: string;
2556
+ kind: 'price';
2557
+ asset: AssetRef;
2558
+ delay?: number;
2559
+ } | {
2560
+ id: string;
2561
+ kind: 'sma';
2562
+ asset: AssetRef;
2563
+ period: number;
2564
+ delay?: number;
2565
+ } | {
2566
+ id: string;
2567
+ kind: 'ema';
2568
+ asset: AssetRef;
2569
+ period: number;
2570
+ delay?: number;
2571
+ } | {
2572
+ id: string;
2573
+ kind: 'rsi';
2574
+ asset: AssetRef;
2575
+ period: number;
2576
+ delay?: number;
2577
+ } | {
2578
+ id: string;
2579
+ kind: 'return';
2580
+ asset: AssetRef;
2581
+ period: number;
2582
+ mode?: ReturnMode;
2583
+ delay?: number;
2584
+ } | {
2585
+ id: string;
2586
+ kind: 'volatility';
2587
+ asset: AssetRef;
2588
+ period: number;
2589
+ delay?: number;
2590
+ } | {
2591
+ id: string;
2592
+ kind: 'drawdown';
2593
+ asset: AssetRef;
2594
+ period: number;
2595
+ delay?: number;
2596
+ };
2597
+ /**
2598
+ * Union of all indicator kind strings that can appear in a
2599
+ * {@link TacticalFeatureSpec}. Derived automatically from the spec union so it
2600
+ * stays in sync.
2601
+ */
2602
+ type TacticalFeatureKind = TacticalFeatureSpec['kind'];
2603
+ /**
2604
+ * A reference to a computed feature value within a rule node. The `ref` string
2605
+ * must match an `id` declared in the `features` array of the enclosing
2606
+ * {@link TacticalSpec}. At evaluation time the runtime replaces the ref with
2607
+ * the resolved numeric value.
2608
+ *
2609
+ * @see {@link Comparison} where `FeatureRef` is accepted as an operand.
2610
+ */
2611
+ type FeatureRef = {
2612
+ ref: string;
2613
+ };
2614
+ /**
2615
+ * Binary comparison operator used in a {@link Comparison} node.
2616
+ *
2617
+ * - `'gt'` — strictly greater than (`l > r`)
2618
+ * - `'lt'` — strictly less than (`l < r`)
2619
+ * - `'gte'` — greater than or equal to (`l >= r`)
2620
+ * - `'lte'` — less than or equal to (`l <= r`)
2621
+ */
2622
+ type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte';
2623
+ /**
2624
+ * Hysteresis band applied to a {@link Comparison} with `op: 'gt'` or `op: 'lt'`.
2625
+ * Once the comparison has flipped, it will not flip back until the left operand
2626
+ * exits the tolerance band around the right operand.
2627
+ *
2628
+ * `mode: 'absolute'` defines a ±`value` band around `right`.
2629
+ * `mode: 'relative'` defines a ±`value`% band (i.e. `value` is a percentage).
2630
+ *
2631
+ * A `Tolerance` requires the parent {@link Comparison} to carry a stable `id`
2632
+ * so the runtime can persist the last-known state across rebalance periods.
2633
+ */
2634
+ type Tolerance = {
2635
+ /** Half-width of the hysteresis band. */
2636
+ value: number;
2637
+ /** `'absolute'` uses raw units; `'relative'` uses a percentage of `right`. */
2638
+ mode: 'absolute' | 'relative';
2639
+ };
2640
+ /**
2641
+ * A binary comparison between two operands. Each operand is either a literal
2642
+ * number or a {@link FeatureRef} resolved at evaluation time. The result is
2643
+ * `true` when `left op right` holds.
2644
+ *
2645
+ * When `tolerance` is provided the comparison implements hysteresis — the
2646
+ * result is sticky and only changes when the left operand exits the band. A
2647
+ * stable `id` is required in that case so {@link RuleTreeState} can track the
2648
+ * last outcome.
2649
+ *
2650
+ * @example
2651
+ * ```ts
2652
+ * import type { Comparison } from '@livefolio/sdk';
2653
+ *
2654
+ * // Feature "sma200" > feature "sma50"
2655
+ * const cond: Comparison = {
2656
+ * op: 'gt',
2657
+ * left: { ref: 'sma200' },
2658
+ * right: { ref: 'sma50' },
2659
+ * };
2660
+ * ```
2661
+ */
2662
+ type Comparison = {
2663
+ /** Which binary operator to apply. */
2664
+ op: ComparisonOp;
2665
+ /** Left-hand operand — a feature reference or a literal number. */
2666
+ left: FeatureRef | number;
2667
+ /** Right-hand operand — a feature reference or a literal number. */
2668
+ right: FeatureRef | number;
2669
+ /**
2670
+ * Optional hysteresis band. Requires `op` to be `'gt'` or `'lt'` and
2671
+ * requires `id` to be set.
2672
+ */
2673
+ tolerance?: Tolerance;
2674
+ /**
2675
+ * Stable identifier used to persist comparison state across steps when
2676
+ * `tolerance` is set. Must be unique within the rule tree.
2677
+ */
2678
+ id?: string;
2679
+ };
2680
+ /**
2681
+ * A leaf node in a {@link RuleNode} tree that terminates evaluation and
2682
+ * returns a target weight allocation. `weights` is a map from asset IDs to
2683
+ * fractional weights; weights should sum to 1 for a fully-invested portfolio,
2684
+ * but the runtime does not enforce this constraint.
2685
+ *
2686
+ * @example
2687
+ * ```ts
2688
+ * import type { AllocateNode } from '@livefolio/sdk';
2689
+ *
2690
+ * // 60% equities, 40% bonds
2691
+ * const node: AllocateNode = {
2692
+ * op: 'allocate',
2693
+ * weights: { SPY: 0.6, TLT: 0.4 },
2694
+ * };
2695
+ * ```
2696
+ */
2697
+ type AllocateNode = {
2698
+ op: 'allocate';
2699
+ /** Map from asset ID to target portfolio weight (0–1). */
2700
+ weights: Record<AssetId, number>;
2701
+ };
2702
+ /**
2703
+ * A branching node in a {@link RuleNode} tree. Evaluates `cond` and recurses
2704
+ * into `then` when the condition is true, or into `else` otherwise. Nesting
2705
+ * `IfNode` trees builds arbitrary decision logic over computed features.
2706
+ *
2707
+ * @example
2708
+ * ```ts
2709
+ * import type { IfNode } from '@livefolio/sdk';
2710
+ *
2711
+ * const node: IfNode = {
2712
+ * op: 'if',
2713
+ * cond: { op: 'gt', left: { ref: 'sma50' }, right: { ref: 'sma200' } },
2714
+ * then: { op: 'allocate', weights: { SPY: 1 } },
2715
+ * else: { op: 'allocate', weights: { SHY: 1 } },
2716
+ * };
2717
+ * ```
2718
+ */
2719
+ type IfNode = {
2720
+ op: 'if';
2721
+ /** Condition to evaluate. */
2722
+ cond: Comparison;
2723
+ /** Sub-tree evaluated when `cond` is true. */
2724
+ then: RuleNode;
2725
+ /** Sub-tree evaluated when `cond` is false. */
2726
+ else: RuleNode;
2727
+ };
2728
+ /**
2729
+ * A node in the tactical rule tree. Either a branching {@link IfNode} or a
2730
+ * terminal {@link AllocateNode}.
2731
+ *
2732
+ * - `op: 'if'` — see {@link IfNode}
2733
+ * - `op: 'allocate'` — see {@link AllocateNode}
2734
+ */
2735
+ type RuleNode = AllocateNode | IfNode;
2736
+ /**
2737
+ * A fully self-contained declaration of a tactical allocation strategy. Plain
2738
+ * data — no methods, no closures. Pass to {@link fromSpec} to obtain a
2739
+ * runnable {@link Strategy}.
2740
+ *
2741
+ * The dialect version distinguishes `'tactical/v0'` (deprecated, byte-for-byte
2742
+ * equivalent to v1, emits a one-time console warning) from `'tactical/v1'` (current).
2743
+ *
2744
+ * @example
2745
+ * ```ts
2746
+ * import type { TacticalSpec } from '@livefolio/sdk';
2747
+ *
2748
+ * const spec: TacticalSpec = {
2749
+ * kind: 'tactical/v1',
2750
+ * universe: [
2751
+ * { id: 'SPY', symbol: 'SPY' },
2752
+ * { id: 'SHY', symbol: 'SHY' },
2753
+ * ],
2754
+ * rebalance: { frequency: 'Monthly' },
2755
+ * features: [
2756
+ * { id: 'spy_sma200', kind: 'sma', asset: { id: 'SPY', symbol: 'SPY' }, period: 200 },
2757
+ * { id: 'spy_price', kind: 'price', asset: { id: 'SPY', symbol: 'SPY' } },
2758
+ * ],
2759
+ * rules: {
2760
+ * op: 'if',
2761
+ * cond: { op: 'gt', left: { ref: 'spy_price' }, right: { ref: 'spy_sma200' } },
2762
+ * then: { op: 'allocate', weights: { SPY: 1 } },
2763
+ * else: { op: 'allocate', weights: { SHY: 1 } },
2764
+ * },
2765
+ * };
2766
+ * ```
2767
+ */
2768
+ type TacticalSpec = {
2769
+ /**
2770
+ * Dialect version. Use `'tactical/v1'`. `'tactical/v0'` is accepted but
2771
+ * deprecated and will emit a one-time warning.
2772
+ */
2773
+ kind: 'tactical/v0' | 'tactical/v1';
2774
+ /** Ordered list of assets eligible for allocation. */
2775
+ universe: AssetRef[];
2776
+ /** Optional synthetic assets whose bar data is derived from an underlying. */
2777
+ synthetics?: SyntheticAsset[];
2778
+ /** Rebalance cadence. Defaults to `{ frequency: 'Daily' }` when omitted. */
2779
+ rebalance?: RebalanceConfig;
2780
+ /** Named feature computations referenced by the rule tree. */
2781
+ features: TacticalFeatureSpec[];
2782
+ /** Root of the rule tree that maps resolved feature values to target weights. */
2783
+ rules: RuleNode;
2784
+ };
2785
+ /**
2786
+ * Persistent state carried across rebalance steps for all named {@link Comparison}
2787
+ * nodes that use hysteresis (`tolerance` set). Maps `comparison.id` to the last
2788
+ * evaluated outcome: `1` = condition was true, `0` = condition was false.
2789
+ *
2790
+ * Managed internally by {@link fromSpec}; exposed as a type for testing and
2791
+ * for callers that drive {@link evaluateRuleTree} directly.
2792
+ */
2793
+ type RuleTreeState = ReadonlyMap<string, 0 | 1>;
708
2794
 
709
- declare function computeRebalanceDates(tradingDays: string[], freq: TradingFreq, offset: number): Set<string>;
2795
+ /**
2796
+ * Evaluates a {@link RuleNode} tree against a resolved set of feature values
2797
+ * and returns the target allocation weights together with the updated
2798
+ * hysteresis state.
2799
+ *
2800
+ * The tree is walked depth-first. At each {@link IfNode} the comparison is
2801
+ * evaluated (with hysteresis applied when `tolerance` and `id` are present)
2802
+ * and the walk follows either `then` or `else`. Evaluation terminates at an
2803
+ * {@link AllocateNode} whose `weights` map is returned verbatim.
2804
+ *
2805
+ * Hysteresis: when a {@link Comparison} carries a `tolerance`, the previous
2806
+ * outcome in `state` is used to decide whether to flip the result. The updated
2807
+ * outcomes for all visited comparisons are collected in the returned `state` map.
2808
+ *
2809
+ * Throws if a {@link FeatureRef} resolves to `undefined` (the caller should
2810
+ * suppress this with a guard or catch-block, as {@link fromSpec} does).
2811
+ *
2812
+ * @param rules - Root of the rule tree to evaluate.
2813
+ * @param values - Resolved feature values, keyed by feature id. Must contain
2814
+ * every `ref` string used in the tree; missing keys throw.
2815
+ * @param state - Prior hysteresis state from the previous evaluation step.
2816
+ * Pass an empty `Map` on the first call.
2817
+ * @returns An object with:
2818
+ * - `weights` — target portfolio weights as a `Map<AssetId, number>`.
2819
+ * - `state` — updated {@link RuleTreeState} to pass to the next step.
2820
+ *
2821
+ * @example
2822
+ * ```ts
2823
+ * import { evaluateRuleTree } from '@livefolio/sdk';
2824
+ * import type { RuleNode, RuleTreeState } from '@livefolio/sdk';
2825
+ *
2826
+ * const rules: RuleNode = {
2827
+ * op: 'if',
2828
+ * cond: { op: 'gt', left: { ref: 'price' }, right: { ref: 'sma200' } },
2829
+ * then: { op: 'allocate', weights: { SPY: 1 } },
2830
+ * else: { op: 'allocate', weights: { SHY: 1 } },
2831
+ * };
2832
+ *
2833
+ * let state: RuleTreeState = new Map();
2834
+ * const values = new Map([['price', 450], ['sma200', 420]]);
2835
+ * const result = evaluateRuleTree(rules, values, state);
2836
+ * // result.weights → Map { 'SPY' => 1 }
2837
+ * state = result.state;
2838
+ * ```
2839
+ */
2840
+ declare function evaluateRuleTree(rules: RuleNode, values: ReadonlyMap<string, number>, state?: RuleTreeState): {
2841
+ weights: TargetWeights;
2842
+ state: RuleTreeState;
2843
+ };
710
2844
 
711
- declare function computeMetrics(series: DailyBar[], trades: Trade[], options?: MetricsOptions): MetricsResult;
2845
+ /**
2846
+ * Resolves each {@link TacticalFeatureSpec} in `specs` to a scalar value as of
2847
+ * date `t` by calling into `runtime` and reading the series at the appropriate
2848
+ * bar index. All specs are computed in parallel via `Promise.all`.
2849
+ *
2850
+ * The returned map uses each spec's `id` as the key. The value is `undefined`
2851
+ * when the indicator series has no data on or before `t`, or when the `delay`
2852
+ * offset steps past the beginning of the series.
2853
+ *
2854
+ * Validation performed before dispatching to the runtime:
2855
+ * - Duplicate `id` values in `specs` throw immediately.
2856
+ * - A non-integer or negative `delay` throws immediately.
2857
+ *
2858
+ * @param specs - Ordered list of feature declarations from {@link TacticalSpec.features}.
2859
+ * @param runtime - Feature computation backend that owns the data feed and cache.
2860
+ * @param t - Evaluation date; the series is read at the latest bar on or before `t`.
2861
+ * @returns A map from feature id to resolved numeric value (`undefined` when unavailable).
2862
+ *
2863
+ * @example
2864
+ * ```ts
2865
+ * import { evaluateFeatureSpecs } from '@livefolio/sdk';
2866
+ * import type { TacticalFeatureSpec } from '@livefolio/sdk';
2867
+ *
2868
+ * const specs: TacticalFeatureSpec[] = [
2869
+ * { id: 'spy_sma200', kind: 'sma', asset: { id: 'SPY', symbol: 'SPY' }, period: 200 },
2870
+ * { id: 'spy_price', kind: 'price', asset: { id: 'SPY', symbol: 'SPY' } },
2871
+ * ];
2872
+ *
2873
+ * const values = await evaluateFeatureSpecs(specs, runtime, new Date('2024-06-01'));
2874
+ * // values.get('spy_price') → 528.3
2875
+ * ```
2876
+ */
2877
+ declare function evaluateFeatureSpecs(specs: ReadonlyArray<TacticalFeatureSpec>, runtime: FeatureRuntime, t: Date): Promise<Map<string, number | undefined>>;
712
2878
 
713
- declare function sharpe(returns: number[], rfAnnual: number): number;
714
- declare function sortino(returns: number[], rfAnnual: number): number;
2879
+ /**
2880
+ * Wraps a {@link DataFeed} so that requests for any asset whose `id` appears in
2881
+ * `synthetics` are intercepted and their bar stream is derived on-the-fly from
2882
+ * the corresponding `underlying` asset.
2883
+ *
2884
+ * The synthesized close price on each bar is computed as:
2885
+ * ```
2886
+ * close_t = close_{t-1} × (1 + leverage × underlyingReturn_t) × (1 − expense/252)
2887
+ * ```
2888
+ * The first bar in the stream uses the underlying close directly. OHLC fields
2889
+ * other than `close` are all set to the synthesized close (they are not
2890
+ * independently scaled); `volume` is passed through from the underlying bar.
2891
+ *
2892
+ * Non-synthetic assets are proxied transparently to the original `dataFeed`.
2893
+ * `fundamentals` and `events` methods, if present, are forwarded unchanged.
2894
+ *
2895
+ * Throws at construction time if `synthetics` contains duplicate `id` values.
2896
+ *
2897
+ * @param dataFeed - The real data feed to wrap.
2898
+ * @param synthetics - Synthetic asset definitions; typically `spec.synthetics ?? []`.
2899
+ * @returns A new {@link DataFeed} that intercepts synthetic asset ids.
2900
+ *
2901
+ * @example
2902
+ * ```ts
2903
+ * import { withSynthetics } from '@livefolio/sdk';
2904
+ * import type { SyntheticAsset } from '@livefolio/sdk';
2905
+ *
2906
+ * const leveraged: SyntheticAsset = {
2907
+ * id: 'SPY_3X', symbol: 'SPY3X',
2908
+ * underlying: { id: 'SPY', symbol: 'SPY' },
2909
+ * leverage: 3,
2910
+ * expense: 0.01,
2911
+ * };
2912
+ *
2913
+ * const feed = withSynthetics(realFeed, [leveraged]);
2914
+ * // Requesting bars for asset { id: 'SPY_3X', ... } now returns 3× leveraged returns.
2915
+ * ```
2916
+ */
2917
+ declare function withSynthetics(dataFeed: DataFeed, synthetics: ReadonlyArray<SyntheticAsset>): DataFeed;
715
2918
 
716
- declare function computeDrawdownTable(series: DailyBar[], topN: number): DrawdownEntry[];
2919
+ /** Test-only: reset the once-per-process deprecation gate. */
2920
+ declare function _resetTacticalDeprecationWarningForTesting(): void;
2921
+ /**
2922
+ * The feature bundle computed on each rebalance step and passed to the rule
2923
+ * tree. Produced by the `features` method of the {@link Strategy} returned by
2924
+ * {@link fromSpec}.
2925
+ *
2926
+ * - `values` — named indicator results keyed by the `id` field of each
2927
+ * {@link TacticalFeatureSpec}. A value is `undefined` when the indicator
2928
+ * cannot be computed for that bar (e.g. insufficient history).
2929
+ * - `prices` — most-recent closing prices for each asset in the universe,
2930
+ * keyed by asset ID.
2931
+ */
2932
+ type TacticalFeatures = {
2933
+ values: ReadonlyMap<string, number | undefined>;
2934
+ prices: ReadonlyMap<AssetId, number>;
2935
+ };
2936
+ /**
2937
+ * Runtime dependencies required by {@link fromSpec} to hydrate a
2938
+ * {@link TacticalSpec} into a runnable {@link Strategy}.
2939
+ */
2940
+ type FromSpecOptions = {
2941
+ /** Feature computation backend — wraps the data feed and caching layer. */
2942
+ runtime: FeatureRuntime;
2943
+ /** Exchange calendar used to gate rebalance days via {@link isRebalanceDay}. */
2944
+ calendar: Calendar;
2945
+ };
2946
+ /**
2947
+ * Returns a stable string key that identifies the rebalance period containing
2948
+ * date `t` for the given `freq`. Two dates that map to the same key belong to
2949
+ * the same period and therefore produce the same rebalance decision. Used
2950
+ * internally by {@link isRebalanceDay} to detect period boundaries.
2951
+ *
2952
+ * @param t - The date to classify.
2953
+ * @param freq - Rebalance cadence (see {@link RebalanceFrequency}).
2954
+ * @returns A compact string such as `'2024-3'` (monthly), `'2024-W14'`
2955
+ * (weekly), or `'2024-1'` (quarterly Q2).
2956
+ */
2957
+ declare function periodKey(t: Date, freq: RebalanceFrequency): string;
2958
+ /**
2959
+ * Returns `true` when `t` is the last trading day of its rebalance period
2960
+ * according to `freq` and `calendar`. The check is: `periodKey(t) !== periodKey(next(t))`.
2961
+ * For `'Daily'` cadence this always returns `true`.
2962
+ *
2963
+ * @param t - Current trading day (must itself be a trading day).
2964
+ * @param freq - Rebalance cadence (see {@link RebalanceFrequency}).
2965
+ * @param calendar - Exchange calendar used to find the next trading day.
2966
+ * @returns `true` if today is the last day of its period and orders should be issued.
2967
+ */
2968
+ declare function isRebalanceDay(t: Date, freq: RebalanceFrequency, calendar: Calendar): boolean;
2969
+ /**
2970
+ * Hydrates a plain {@link TacticalSpec} data object into a runnable
2971
+ * {@link Strategy} that `runBacktest` can drive step-by-step.
2972
+ *
2973
+ * State is threaded explicitly through `build` via the
2974
+ * {@link Strategy | `Strategy<F, S>.build`} signature. `initialState()` returns
2975
+ * an empty {@link RuleTreeState} Map; the runtime is responsible for storing and
2976
+ * forwarding the state between calls. This design makes `build` a pure function
2977
+ * of its inputs — calling it twice with identical arguments produces identical
2978
+ * outputs, enabling snapshot/restore for preview-builds in live mode.
2979
+ *
2980
+ * Validation performed at construction time:
2981
+ * - A `'tactical/v0'` `kind` emits a one-time deprecation warning to `console.warn`.
2982
+ * - Synthetic assets are checked for self-reference, symbol collisions, and
2983
+ * missing universe entries (see internal `validateSynthetics`).
2984
+ *
2985
+ * @param spec - The declarative strategy spec.
2986
+ * @param opts - Runtime dependencies (feature backend and calendar).
2987
+ * @returns A {@link Strategy} whose `features` method fetches indicator values
2988
+ * and whose `build` method converts them to rebalance orders.
2989
+ *
2990
+ * @example
2991
+ * ```ts
2992
+ * import { fromSpec, MemoryFeatureCache, NYSEExchangeCalendar } from '@livefolio/sdk';
2993
+ * import { FeatureRuntime } from '@livefolio/sdk/features';
2994
+ *
2995
+ * const calendar = new NYSEExchangeCalendar();
2996
+ * const cache = new MemoryFeatureCache();
2997
+ * const runtime = new FeatureRuntime({ feed: myDataFeed, cache });
2998
+ *
2999
+ * const strategy = fromSpec(mySpec, { runtime, calendar });
3000
+ * ```
3001
+ */
3002
+ declare function fromSpec(spec: TacticalSpec, opts: FromSpecOptions): Strategy<TacticalFeatures, RuleTreeState>;
717
3003
 
718
- interface MonthlyReturn {
719
- year: number;
720
- month: number;
721
- return: number;
722
- partial: boolean;
3004
+ type index$1_AllocateNode = AllocateNode;
3005
+ type index$1_AssetRef = AssetRef;
3006
+ type index$1_Comparison = Comparison;
3007
+ type index$1_ComparisonOp = ComparisonOp;
3008
+ type index$1_FeatureRef = FeatureRef;
3009
+ type index$1_FromSpecOptions = FromSpecOptions;
3010
+ type index$1_IfNode = IfNode;
3011
+ type index$1_RebalanceConfig = RebalanceConfig;
3012
+ type index$1_RebalanceFrequency = RebalanceFrequency;
3013
+ type index$1_RuleNode = RuleNode;
3014
+ type index$1_RuleTreeState = RuleTreeState;
3015
+ type index$1_SyntheticAsset = SyntheticAsset;
3016
+ type index$1_TacticalFeatureKind = TacticalFeatureKind;
3017
+ type index$1_TacticalFeatureSpec = TacticalFeatureSpec;
3018
+ type index$1_TacticalFeatures = TacticalFeatures;
3019
+ type index$1_TacticalSpec = TacticalSpec;
3020
+ type index$1_Tolerance = Tolerance;
3021
+ declare const index$1__resetTacticalDeprecationWarningForTesting: typeof _resetTacticalDeprecationWarningForTesting;
3022
+ declare const index$1_evaluateFeatureSpecs: typeof evaluateFeatureSpecs;
3023
+ declare const index$1_evaluateRuleTree: typeof evaluateRuleTree;
3024
+ declare const index$1_fromSpec: typeof fromSpec;
3025
+ declare const index$1_isRebalanceDay: typeof isRebalanceDay;
3026
+ declare const index$1_periodKey: typeof periodKey;
3027
+ declare const index$1_withSynthetics: typeof withSynthetics;
3028
+ declare namespace index$1 {
3029
+ 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, 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_withSynthetics as withSynthetics };
723
3030
  }
724
- interface YearlyReturn {
725
- year: number;
726
- return: number;
727
- partial: boolean;
3031
+
3032
+ /**
3033
+ * Computes a Simple Moving Average (SMA) over a price series.
3034
+ *
3035
+ * Math definition:
3036
+ * ```
3037
+ * SMA[i] = (series[i] + series[i-1] + ... + series[i-period+1]) / period
3038
+ * ```
3039
+ *
3040
+ * Warmup: the first output point corresponds to index `period - 1` in the input.
3041
+ * Inputs `series[0]` through `series[period - 2]` have no SMA value and are
3042
+ * excluded from the output entirely (no `undefined` placeholders; the output
3043
+ * array is shorter than the input).
3044
+ *
3045
+ * Edge cases:
3046
+ * - `period <= 0` — throws `Error`.
3047
+ * - `series.length < period` — returns `[]` (not enough data for even one window).
3048
+ * - `series.length === period` — returns a single-point `Series`.
3049
+ *
3050
+ * @param series - Input price series sorted in ascending timestamp order.
3051
+ * @param period - Window size in bars. Must be a positive integer.
3052
+ * @returns A `Series` of length `max(0, series.length - period + 1)`. Each point's
3053
+ * timestamp `t` is taken from the last bar in its window (`series[i + period - 1]`).
3054
+ *
3055
+ * @example
3056
+ * ```ts
3057
+ * import { sma } from '@livefolio/sdk';
3058
+ *
3059
+ * const prices = [
3060
+ * { t: new Date('2023-01-02'), v: 100 },
3061
+ * { t: new Date('2023-01-03'), v: 110 },
3062
+ * { t: new Date('2023-01-04'), v: 120 },
3063
+ * { t: new Date('2023-01-05'), v: 130 },
3064
+ * ];
3065
+ *
3066
+ * const result = sma(prices, 3);
3067
+ * // result.length === 2
3068
+ * // result[0] => { t: new Date('2023-01-04'), v: 110 } // (100+110+120)/3
3069
+ * // result[1] => { t: new Date('2023-01-05'), v: 120 } // (110+120+130)/3
3070
+ * ```
3071
+ */
3072
+ declare function sma(series: Series, period: number): Series;
3073
+
3074
+ /**
3075
+ * Computes an Exponential Moving Average (EMA) over a price series.
3076
+ *
3077
+ * Math definition:
3078
+ * ```
3079
+ * k = 2 / (period + 1) // smoothing factor
3080
+ * EMA[0] = SMA(series[0..period-1]) // seeded from simple average
3081
+ * EMA[i] = series[i] * k + EMA[i-1] * (1 - k)
3082
+ * ```
3083
+ *
3084
+ * Warmup: the first `period - 1` input bars are consumed to seed the SMA
3085
+ * initial value. The first EMA output point corresponds to input index
3086
+ * `period - 1`; subsequent points are computed from the recursive formula.
3087
+ * The output array is shorter than the input (no `undefined` placeholders).
3088
+ *
3089
+ * Edge cases:
3090
+ * - `period <= 0` — throws `Error`.
3091
+ * - `series.length < period` — returns `[]` (not enough data to seed the EMA).
3092
+ * - `series.length === period` — returns a single-point `Series` equal to the
3093
+ * simple average of all input values.
3094
+ *
3095
+ * @param series - Input price series sorted in ascending timestamp order.
3096
+ * @param period - Lookback window in bars. Must be a positive integer. Controls
3097
+ * the smoothing factor `k = 2 / (period + 1)`: smaller periods react faster
3098
+ * to recent prices.
3099
+ * @returns A `Series` of length `max(0, series.length - period + 1)`. Each
3100
+ * point's timestamp `t` is taken from the corresponding input bar.
3101
+ *
3102
+ * @example
3103
+ * ```ts
3104
+ * import { ema } from '@livefolio/sdk';
3105
+ *
3106
+ * const prices = [
3107
+ * { t: new Date('2023-01-02'), v: 100 },
3108
+ * { t: new Date('2023-01-03'), v: 110 },
3109
+ * { t: new Date('2023-01-04'), v: 120 },
3110
+ * { t: new Date('2023-01-05'), v: 130 },
3111
+ * ];
3112
+ *
3113
+ * const result = ema(prices, 3);
3114
+ * // result.length === 2
3115
+ * // result[0] => { t: new Date('2023-01-04'), v: 110 } // SMA seed: (100+110+120)/3
3116
+ * // result[1] => { t: new Date('2023-01-05'), v: ~116.7 } // EMA: 130*0.5 + 110*0.5
3117
+ * ```
3118
+ */
3119
+ declare function ema(series: Series, period: number): Series;
3120
+
3121
+ /**
3122
+ * Computes the Relative Strength Index (RSI) using Wilder's smoothing method.
3123
+ *
3124
+ * Math definition:
3125
+ * ```
3126
+ * changes[i] = series[i] - series[i-1]
3127
+ *
3128
+ * // Seed from simple averages of the first `period` changes:
3129
+ * avgGain[0] = mean(max(changes[0..period-1], 0))
3130
+ * avgLoss[0] = mean(max(-changes[0..period-1], 0))
3131
+ *
3132
+ * // Wilder's smoothing for subsequent periods:
3133
+ * avgGain[i] = (avgGain[i-1] * (period-1) + gain[i]) / period
3134
+ * avgLoss[i] = (avgLoss[i-1] * (period-1) + loss[i]) / period
3135
+ *
3136
+ * RS[i] = avgGain[i] / avgLoss[i]
3137
+ * RSI[i] = 100 - 100 / (1 + RS[i])
3138
+ * ```
3139
+ *
3140
+ * Special case: when `avgLoss === 0`, RSI is clamped to 100 (infinite RS means
3141
+ * no losing periods in the window).
3142
+ *
3143
+ * Warmup: requires `period + 1` input bars to produce the first RSI value
3144
+ * (one extra bar for the initial change calculation). The first output point
3145
+ * corresponds to input index `period`. The output array is shorter than the
3146
+ * input (no `undefined` placeholders).
3147
+ *
3148
+ * Edge cases:
3149
+ * - `period <= 0` — throws `Error`.
3150
+ * - `series.length < period + 1` — returns `[]`.
3151
+ * - Flat price series (all changes = 0) — returns RSI values of 100 because
3152
+ * `avgLoss` stays 0.
3153
+ *
3154
+ * @param series - Input price series sorted in ascending timestamp order.
3155
+ * @param period - Lookback window in bars for Wilder's smoothing. Must be a
3156
+ * positive integer; 14 is the conventional default.
3157
+ * @returns A `Series` of length `max(0, series.length - period)`. Each point's
3158
+ * timestamp `t` is taken from the corresponding input bar. Values are in
3159
+ * the range `[0, 100]`.
3160
+ *
3161
+ * @example
3162
+ * ```ts
3163
+ * import { rsi } from '@livefolio/sdk';
3164
+ *
3165
+ * // Minimal example: 5 bars, period 3 → 2 RSI values
3166
+ * const prices = [
3167
+ * { t: new Date('2023-01-02'), v: 100 },
3168
+ * { t: new Date('2023-01-03'), v: 102 },
3169
+ * { t: new Date('2023-01-04'), v: 101 },
3170
+ * { t: new Date('2023-01-05'), v: 105 },
3171
+ * { t: new Date('2023-01-06'), v: 104 },
3172
+ * ];
3173
+ *
3174
+ * const result = rsi(prices, 3);
3175
+ * // result.length === 2
3176
+ * // result[0].t => new Date('2023-01-05')
3177
+ * // result[1].t => new Date('2023-01-06')
3178
+ * // values are in [0, 100]
3179
+ * ```
3180
+ */
3181
+ declare function rsi(series: Series, period: number): Series;
3182
+
3183
+ /**
3184
+ * Computes a rolling historical volatility as the population standard deviation
3185
+ * of daily log-like returns over a sliding window.
3186
+ *
3187
+ * Math definition:
3188
+ * ```
3189
+ * dailyReturn[i] = series[i] / series[i-1] - 1 // simple period return
3190
+ *
3191
+ * // For each window of `period` daily returns ending at index i:
3192
+ * mean = Σ dailyReturn[i..i-period+1] / period
3193
+ * variance = Σ (dailyReturn[j] - mean)² / period // population variance
3194
+ * vol[i] = sqrt(variance)
3195
+ * ```
3196
+ *
3197
+ * The result is the per-bar standard deviation expressed as a fraction (e.g.
3198
+ * `0.012` ≈ 1.2 % daily volatility). To annualise, multiply by `sqrt(252)` for
3199
+ * daily bars.
3200
+ *
3201
+ * Warmup: requires `period + 1` price bars to produce the first volatility value:
3202
+ * one extra bar to compute the first daily return, then `period` returns for the
3203
+ * first window. The first output point corresponds to the `period`-th daily-return
3204
+ * index. The output array is shorter than the input (no `undefined` placeholders).
3205
+ *
3206
+ * Edge cases:
3207
+ * - `period <= 0` — throws `Error`.
3208
+ * - `series.length < period + 1` — returns `[]`.
3209
+ * - Flat price series (all returns = 0) — returns volatility values of `0`.
3210
+ *
3211
+ * @param series - Input price series sorted in ascending timestamp order. Values
3212
+ * must be positive (non-zero); zero prices produce `NaN` daily returns.
3213
+ * @param period - Window size in daily-return bars. Must be a positive integer;
3214
+ * common values are 20 (≈ 1 month) or 252 (1 year) for daily data.
3215
+ * @returns A `Series` of length `max(0, series.length - period)`. Each point's
3216
+ * timestamp `t` is taken from the last daily-return bar in its window.
3217
+ *
3218
+ * @example
3219
+ * ```ts
3220
+ * import { volatility } from '@livefolio/sdk';
3221
+ *
3222
+ * // 5 price bars → 4 daily returns → 1 vol point (period=4)
3223
+ * const prices = [
3224
+ * { t: new Date('2023-01-02'), v: 100 },
3225
+ * { t: new Date('2023-01-03'), v: 101 },
3226
+ * { t: new Date('2023-01-04'), v: 99 },
3227
+ * { t: new Date('2023-01-05'), v: 102 },
3228
+ * { t: new Date('2023-01-06'), v: 100 },
3229
+ * ];
3230
+ *
3231
+ * const vol = volatility(prices, 4);
3232
+ * // vol.length === 1
3233
+ * // vol[0].t => new Date('2023-01-06')
3234
+ * // vol[0].v => population std-dev of the 4 daily returns (≈ 0.012)
3235
+ * ```
3236
+ */
3237
+ declare function volatility(series: Series, period: number): Series;
3238
+
3239
+ /**
3240
+ * Computes the rolling drawdown relative to the period high for each bar.
3241
+ *
3242
+ * Math definition:
3243
+ * ```
3244
+ * rollingMax[i] = max(series[i-period+1], ..., series[i])
3245
+ * drawdown[i] = (series[i] - rollingMax[i]) / rollingMax[i]
3246
+ * ```
3247
+ *
3248
+ * The result is a non-positive fraction (e.g. `-0.15` means the current price
3249
+ * is 15 % below the period high). A value of `0` means the current price equals
3250
+ * the rolling maximum — i.e. the asset is at a new high within the window.
3251
+ *
3252
+ * Warmup: the first output point corresponds to input index `period - 1` (the
3253
+ * first complete window). The output array is shorter than the input by
3254
+ * `period - 1` points (no `undefined` placeholders).
3255
+ *
3256
+ * Edge cases:
3257
+ * - `period <= 0` — throws `Error`.
3258
+ * - `series.length < period` — returns `[]`.
3259
+ * - All prices in a window are equal — returns `0` (current equals max).
3260
+ * - Zero prices in the window — produces `NaN` (division by zero); callers
3261
+ * should guard against zero-price series.
3262
+ *
3263
+ * @param series - Input price series sorted in ascending timestamp order. Values
3264
+ * should be positive (non-zero) to avoid `NaN` results.
3265
+ * @param period - Rolling window size in bars. Must be a positive integer.
3266
+ * A value of `1` always returns `0` (current equals one-bar max).
3267
+ * @returns A `Series` of length `max(0, series.length - period + 1)`. Each
3268
+ * point's timestamp `t` is taken from `series[i]` (the last bar in its window).
3269
+ * Values are in the range `(-∞, 0]` but in practice within `[-1, 0]` for
3270
+ * positive price series.
3271
+ *
3272
+ * @example
3273
+ * ```ts
3274
+ * import { drawdown } from '@livefolio/sdk';
3275
+ *
3276
+ * const prices = [
3277
+ * { t: new Date('2023-01-02'), v: 100 },
3278
+ * { t: new Date('2023-01-03'), v: 105 },
3279
+ * { t: new Date('2023-01-04'), v: 95 },
3280
+ * { t: new Date('2023-01-05'), v: 98 },
3281
+ * ];
3282
+ *
3283
+ * const dd = drawdown(prices, 3);
3284
+ * // dd.length === 2
3285
+ * // dd[0].t => new Date('2023-01-04'), dd[0].v => (95-105)/105 ≈ -0.095
3286
+ * // dd[1].t => new Date('2023-01-05'), dd[1].v => (98-105)/105 ≈ -0.067
3287
+ * ```
3288
+ */
3289
+ declare function drawdown(series: Series, period: number): Series;
3290
+
3291
+ type index_BarField = BarField;
3292
+ type index_ComputeFn = ComputeFn;
3293
+ type index_FeatureKind = FeatureKind;
3294
+ type index_FeatureRuntime = FeatureRuntime;
3295
+ declare const index_FeatureRuntime: typeof FeatureRuntime;
3296
+ type index_FeatureRuntimeOptions = FeatureRuntimeOptions;
3297
+ type index_FeatureSpec = FeatureSpec;
3298
+ type index_ReturnMode = ReturnMode;
3299
+ declare const index_barsToSeries: typeof barsToSeries;
3300
+ declare const index_collectBars: typeof collectBars;
3301
+ declare const index_defineFeature: typeof defineFeature;
3302
+ declare const index_drawdown: typeof drawdown;
3303
+ declare const index_ema: typeof ema;
3304
+ declare const index_getFeatureCompute: typeof getFeatureCompute;
3305
+ declare const index_paramsHash: typeof paramsHash;
3306
+ declare const index_returnSeries: typeof returnSeries;
3307
+ declare const index_rsi: typeof rsi;
3308
+ declare const index_seriesAt: typeof seriesAt;
3309
+ declare const index_sma: typeof sma;
3310
+ declare const index_volatility: typeof volatility;
3311
+ declare namespace index {
3312
+ 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
3313
  }
729
- declare function monthlyReturns(series: DailyBar[]): MonthlyReturn[];
730
- declare function yearlyReturns(series: DailyBar[]): YearlyReturn[];
731
3314
 
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 };
3315
+ /**
3316
+ * Applies a batch of confirmed fills to a portfolio, returning a new
3317
+ * {@link Portfolio} snapshot. This is the single function that advances
3318
+ * portfolio state after order execution.
3319
+ *
3320
+ * For each fill the corresponding order is looked up in `orders` by
3321
+ * `fill.orderRef`. The order's `kind` determines the accounting treatment:
3322
+ * - `'open'` — adds a new {@link Position} and debits cash.
3323
+ * - `'close'` — removes shares from an existing position and credits cash.
3324
+ * - `'adjust'` — updates the position's `quantity`; only fees are debited.
3325
+ * - `'rebalance'` — buys or sells shares in the long position for `asset`;
3326
+ * creates or removes the position as needed.
3327
+ *
3328
+ * The returned `portfolio.t` is updated to the maximum fill timestamp.
3329
+ *
3330
+ * @param portfolio - The current portfolio state before this batch.
3331
+ * @param fills - Execution confirmations returned by {@link Executor.submit}.
3332
+ * Each fill's `orderRef` MUST match an `id` in `orders`.
3333
+ * @param orders - The full order batch that was submitted. Used to look up
3334
+ * order details for each fill.
3335
+ * @returns A new {@link Portfolio} with updated positions, cash, and timestamp.
3336
+ * The input `portfolio` is not mutated.
3337
+ *
3338
+ * @example
3339
+ * ```ts
3340
+ * import { applyFills } from '@livefolio/sdk';
3341
+ * import type { Portfolio, Order, Fill } from '@livefolio/sdk';
3342
+ *
3343
+ * const portfolio: Portfolio = { cash: 10_000, positions: [], t: new Date('2024-01-01') };
3344
+ *
3345
+ * const order: Order = {
3346
+ * id: 'ord_1', kind: 'open',
3347
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
3348
+ * side: 'long', quantity: 10,
3349
+ * };
3350
+ * const fill: Fill = { orderRef: 'ord_1', t: new Date('2024-01-02'), quantity: 10, price: 185, fees: 0 };
3351
+ *
3352
+ * const next = applyFills(portfolio, [fill], [order]);
3353
+ * // next.cash === 8_250, next.positions.length === 1
3354
+ * ```
3355
+ */
3356
+ declare function applyFills(portfolio: Portfolio, fills: ReadonlyArray<Fill>, orders: ReadonlyArray<Order>): Portfolio;
3357
+ /**
3358
+ * Projects a portfolio forward through a set of pending (unfilled) orders,
3359
+ * returning a structurally updated snapshot. Used by strategy build helpers
3360
+ * to read the expected post-step state before fills arrive.
3361
+ *
3362
+ * **v0.4 contract — structural projection only.** Quantities are updated
3363
+ * exactly as the orders specify, but:
3364
+ * - `cash` is left unchanged (no price is available at projection time).
3365
+ * - Newly opened positions have `basis: 0` and `entry.price: 0` as
3366
+ * provisional values. A price-aware projection is planned for a later phase.
3367
+ *
3368
+ * Use {@link applyFills} (not this function) to settle the portfolio after
3369
+ * confirmed execution.
3370
+ *
3371
+ * @param portfolio - The current portfolio state to project from.
3372
+ * @param orders - The pending orders to apply structurally. Order must have
3373
+ * a valid `id` and `kind`; price fields are ignored.
3374
+ * @returns A new {@link Portfolio} with positions reflecting the orders.
3375
+ * `cash` and `t` are copied unchanged from `portfolio`.
3376
+ *
3377
+ * @example
3378
+ * ```ts
3379
+ * import { applyOrders } from '@livefolio/sdk';
3380
+ * import type { Portfolio, Order } from '@livefolio/sdk';
3381
+ *
3382
+ * const portfolio: Portfolio = { cash: 10_000, positions: [], t: new Date('2024-01-01') };
3383
+ *
3384
+ * const order: Order = {
3385
+ * id: 'ord_1', kind: 'open',
3386
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
3387
+ * side: 'long', quantity: 10,
3388
+ * };
3389
+ *
3390
+ * const projected = applyOrders(portfolio, [order]);
3391
+ * // projected.positions.length === 1, projected.cash === 10_000 (unchanged)
3392
+ * ```
3393
+ */
3394
+ declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>): Portfolio;
3395
+
3396
+ 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 RebalanceConfig, type RebalanceFrequency, type RebalanceOrder, type ReturnMode, RoutingDataFeed, RoutingDataFeedError, type RoutingDataFeedRouteFn, type RoutingDataFeedRouteMap, 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, 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, withSynthetics };