@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/LICENSE +21 -0
- package/README.md +91 -310
- package/dist/index.d.ts +3352 -688
- package/dist/index.js +2689 -2650
- package/dist/index.js.map +1 -1
- package/package.json +15 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,732 +1,3396 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
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
|
-
|
|
515
|
-
|
|
2010
|
+
declare class RoutingDataFeedError extends Error {
|
|
2011
|
+
constructor(message: string);
|
|
516
2012
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
interface
|
|
663
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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 };
|