@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Livefolio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,333 +1,114 @@
|
|
|
1
1
|
# @livefolio/sdk
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for building
|
|
3
|
+
TypeScript SDK for building, backtesting, and live-evaluating tactical
|
|
4
|
+
allocation strategies. Declare a strategy as a `TacticalSpec`, run it
|
|
5
|
+
against historical data with `runBacktest`, then continue from the final
|
|
6
|
+
state into live evaluation with `runLive` — same spec, no hand-off seam.
|
|
7
|
+
|
|
8
|
+
> **Full documentation, guides, and API reference:** [livefolio.github.io/sdk](https://livefolio.github.io/sdk/)
|
|
4
9
|
|
|
5
10
|
## Install
|
|
6
11
|
|
|
7
12
|
```bash
|
|
8
|
-
npm install @livefolio/sdk @
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
const spy = sdk.ticker('SPY'); // no DB call
|
|
42
|
-
const sma200 = sdk.sma(spy, 200); // no DB call
|
|
43
|
-
const series = await sma200.series(); // NOW: resolve -> fetch -> compute -> return
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Automatic Sync
|
|
47
|
-
|
|
48
|
-
Indicator series data is fetched and computed transparently. When you call `.series()` or `.value()`:
|
|
49
|
-
|
|
50
|
-
1. The indicator and its dependencies are resolved (upserted) in the database
|
|
51
|
-
2. If the series is stale or missing, raw data is fetched from the appropriate source
|
|
52
|
-
3. Derived indicators are computed from their dependencies
|
|
53
|
-
4. Results are upserted to the database and cached in memory
|
|
54
|
-
|
|
55
|
-
Since market data is daily closing prices, data is immutable once the trading day closes. The SDK takes advantage of this for aggressive caching.
|
|
56
|
-
|
|
57
|
-
## API Reference
|
|
58
|
-
|
|
59
|
-
### `createClient(options)`
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
createClient({
|
|
63
|
-
supabase: SupabaseClient, // required
|
|
64
|
-
fredApiKey?: string, // required for treasury indicators
|
|
65
|
-
})
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Returns a `LivefolioClient` with the factory methods below.
|
|
69
|
-
|
|
70
|
-
### Tickers
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
sdk.ticker(symbol: string, leverage?: number)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Creates a `TickerHandle`. Leverage defaults to `1`.
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
const spy = sdk.ticker('SPY');
|
|
80
|
-
const spxl = sdk.ticker('SPXL', 3);
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Ticker-Bound Indicators
|
|
84
|
-
|
|
85
|
-
These require a `TickerHandle` and compute from that ticker's price history.
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
sdk.sma(ticker, lookback, opts?) // Simple Moving Average
|
|
89
|
-
sdk.ema(ticker, lookback, opts?) // Exponential Moving Average
|
|
90
|
-
sdk.rsi(ticker, lookback, opts?) // Relative Strength Index
|
|
91
|
-
sdk.price(ticker, opts?) // Raw closing price
|
|
92
|
-
sdk.returns(ticker, lookback, opts?) // Period returns
|
|
93
|
-
sdk.volatility(ticker, lookback, opts?) // Rolling standard deviation
|
|
94
|
-
sdk.drawdown(ticker, lookback, opts?) // Drawdown from rolling max
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
`opts` is `{ delay?: number }` -- defaults to `0`.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
const spy = sdk.ticker('SPY');
|
|
101
|
-
const sma200 = sdk.sma(spy, 200);
|
|
102
|
-
const rsi14 = sdk.rsi(spy, 14);
|
|
103
|
-
const delayed = sdk.sma(spy, 50, { delay: 1 });
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Standalone Indicators
|
|
107
|
-
|
|
108
|
-
No ticker required. Data comes directly from external APIs.
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
sdk.vix(opts?) // CBOE Volatility Index
|
|
112
|
-
sdk.vix3m(opts?) // CBOE 3-Month Volatility Index
|
|
113
|
-
sdk.treasury(tenor, opts?) // Treasury rates (requires fredApiKey)
|
|
114
|
-
sdk.calendar(period, opts?) // Date components from trading calendar
|
|
115
|
-
sdk.threshold(value, unit?) // Constant value
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Treasury tenors: `'T3M'`, `'T6M'`, `'T1Y'`, `'T2Y'`, `'T3Y'`, `'T5Y'`, `'T7Y'`, `'T10Y'`, `'T20Y'`, `'T30Y'`
|
|
119
|
-
|
|
120
|
-
Calendar periods: `'Month'`, `'Day of Week'`, `'Day of Month'`, `'Day of Year'`
|
|
121
|
-
|
|
122
|
-
Threshold units: `'%'`, `'$'`, or omit for unitless.
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
const vix = sdk.vix();
|
|
126
|
-
const t10y = sdk.treasury('T10Y');
|
|
127
|
-
const month = sdk.calendar('Month');
|
|
128
|
-
const half = sdk.threshold(0.5);
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Signals
|
|
132
|
-
|
|
133
|
-
Compare two indicators to create a boolean signal. Supports hysteresis via tolerance to reduce whipsawing.
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
sdk.gt(ind1, ind2, tolerance?) // ind1 > ind2
|
|
137
|
-
sdk.lt(ind1, ind2, tolerance?) // ind1 < ind2
|
|
138
|
-
sdk.eq(ind1, ind2, tolerance?) // ind1 within tolerance range of ind2
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Tolerance defaults to `0` (no hysteresis). When set, a buffer zone prevents the signal from flipping until the indicator moves fully through the buffer.
|
|
142
|
-
|
|
143
|
-
- **Relative tolerance** (Price, SMA, EMA, RSI, Threshold, Calendar): buffer = `ind2 * (1 +/- tolerance/100)`
|
|
144
|
-
- **Absolute tolerance** (Return, Volatility, Drawdown, VIX, VIX3M, Treasury): buffer = `ind2 +/- tolerance`
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
const spy = sdk.ticker('SPY');
|
|
148
|
-
const price = sdk.price(spy);
|
|
149
|
-
const sma200 = sdk.sma(spy, 200);
|
|
150
|
-
|
|
151
|
-
const bullish = sdk.gt(price, sma200, 5); // 5% tolerance
|
|
152
|
-
|
|
153
|
-
const series = await bullish.series(); // DailyBar[] with value 0 or 1
|
|
154
|
-
const current = await bullish.value(); // 0 or 1
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Signal handles support the same `.series(range?)`, `.value(date?)`, and `.resolve()` methods as indicator handles. Data is automatically synced -- both underlying indicators are refreshed before computing the signal.
|
|
158
|
-
|
|
159
|
-
### Allocations
|
|
160
|
-
|
|
161
|
-
Define portfolio holdings as weighted ticker pairs.
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
sdk.allocation(...holdings: [TickerHandle, number][])
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Weights must sum to 1. Allocations are deduplicated by holdings -- creating the same allocation twice returns the same database row.
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
const aggressive = sdk.allocation([spy, 0.75], [gld, 0.25]);
|
|
171
|
-
const defensive = sdk.allocation([shy, 1.0]);
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Strategies
|
|
175
|
-
|
|
176
|
-
Compose signals and allocations into a priority-ordered rule list evaluated on a rebalancing schedule.
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
// Create a new strategy
|
|
180
|
-
sdk.strategy(options: StrategyOptions)
|
|
181
|
-
|
|
182
|
-
// Reference an existing strategy by link ID
|
|
183
|
-
sdk.strategy(linkId: string)
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Each rule's `when` array is AND-ed together. OR is expressed by having multiple rules point to the same allocation. The last rule must be a fallback with no `when` clause. Rules are evaluated top-to-bottom; first match wins.
|
|
187
|
-
|
|
188
|
-
```ts
|
|
189
|
-
const spy = sdk.ticker('SPY');
|
|
190
|
-
const shy = sdk.ticker('SHY');
|
|
191
|
-
const price = sdk.price(spy);
|
|
192
|
-
const sma200 = sdk.sma(spy, 200);
|
|
193
|
-
|
|
194
|
-
const bullish = sdk.gt(price, sma200, 5);
|
|
195
|
-
|
|
196
|
-
const aggressive = sdk.allocation([spy, 1.0]);
|
|
197
|
-
const defensive = sdk.allocation([shy, 1.0]);
|
|
198
|
-
|
|
199
|
-
const strategy = sdk.strategy({
|
|
200
|
-
name: 'Tactical SPY/SHY',
|
|
201
|
-
freq: 'Monthly', // rebalance on last trading day of each month
|
|
202
|
-
offset: 0, // positive = earlier, negative = later
|
|
203
|
-
rules: [
|
|
204
|
-
{ when: [bullish], hold: aggressive },
|
|
205
|
-
{ hold: defensive }, // fallback
|
|
13
|
+
npm install @livefolio/sdk @livefolio/yfinance
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@livefolio/yfinance` is one option for the data layer. Implement your
|
|
17
|
+
own `DataFeed` for proprietary feeds.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
fromSpec,
|
|
24
|
+
runBacktest,
|
|
25
|
+
FeatureRuntime,
|
|
26
|
+
NYSEExchangeCalendar,
|
|
27
|
+
MemoryFeatureCache,
|
|
28
|
+
BacktestExecutor,
|
|
29
|
+
} from '@livefolio/sdk';
|
|
30
|
+
import type { TacticalSpec, Asset, DateRange } from '@livefolio/sdk';
|
|
31
|
+
import { YfinanceDataFeed } from '@livefolio/yfinance';
|
|
32
|
+
|
|
33
|
+
// 1. Declare the strategy as data.
|
|
34
|
+
const SPY = { id: 'us:SPY', symbol: 'SPY' };
|
|
35
|
+
const QQQ = { id: 'us:QQQ', symbol: 'QQQ' };
|
|
36
|
+
const IEF = { id: 'us:IEF', symbol: 'IEF' };
|
|
37
|
+
|
|
38
|
+
const spec: TacticalSpec = {
|
|
39
|
+
kind: 'tactical/v1',
|
|
40
|
+
universe: [SPY, QQQ, IEF],
|
|
41
|
+
rebalance: { frequency: 'Weekly' },
|
|
42
|
+
features: [
|
|
43
|
+
{ id: 'spy_price', kind: 'price', asset: SPY },
|
|
44
|
+
{ id: 'spy_sma200', kind: 'sma', asset: SPY, period: 200 },
|
|
206
45
|
],
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const startingPortfolio = sdk.portfolio([cashx, 100_000]);
|
|
231
|
-
|
|
232
|
-
const sim = await strategy.simulate({ from: '2020-01-01', to: '2025-12-31', portfolio: startingPortfolio });
|
|
233
|
-
|
|
234
|
-
sim.series // DailyBar[] — portfolio value per trading day
|
|
235
|
-
sim.trades // Trade[] — every buy/sell event
|
|
236
|
-
sim.startingPortfolio // PortfolioHandle — starting positions
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
You can also start from existing positions:
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
const existingPortfolio = sdk.portfolio([spy, 100], [cashx, 5000]);
|
|
243
|
-
const sim = await strategy.simulate({ from: '2024-01-01', to: '2025-12-31', portfolio: existingPortfolio });
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
The simulator rebalances at the strategy's `freq` cadence, fetches price data for all tickers in all allocations automatically, and tracks positions and cash through each trading day.
|
|
247
|
-
|
|
248
|
-
```ts
|
|
249
|
-
interface Trade {
|
|
250
|
-
date: string;
|
|
251
|
-
symbol: string;
|
|
252
|
-
quantity: number; // number of shares traded
|
|
253
|
-
price: number;
|
|
254
|
-
action: 'buy' | 'sell';
|
|
46
|
+
rules: {
|
|
47
|
+
op: 'if',
|
|
48
|
+
cond: { op: 'gt', left: { ref: 'spy_price' }, right: { ref: 'spy_sma200' } },
|
|
49
|
+
then: { op: 'allocate', weights: { 'us:SPY': 0.6, 'us:QQQ': 0.4 } },
|
|
50
|
+
else: { op: 'allocate', weights: { 'us:IEF': 1.0 } },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// 2. Wire the runtime layers.
|
|
55
|
+
const dataFeed = new YfinanceDataFeed();
|
|
56
|
+
const calendar = new NYSEExchangeCalendar();
|
|
57
|
+
const featureCache = new MemoryFeatureCache();
|
|
58
|
+
const range: DateRange = {
|
|
59
|
+
from: new Date('2020-01-01T00:00:00Z'),
|
|
60
|
+
to: new Date('2024-12-31T00:00:00Z'),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const runtime = new FeatureRuntime({ dataFeed, featureCache, range, freq: '1d' });
|
|
64
|
+
|
|
65
|
+
async function nextOpen(asset: Asset, t: Date) {
|
|
66
|
+
// Look up the next session's open price for this asset.
|
|
67
|
+
// Implementation depends on your data layer; see docs for details.
|
|
68
|
+
throw new Error('implement nextOpen against your data feed');
|
|
255
69
|
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
Agents compute whatever derived metrics they need (CAGR, Sharpe, drawdown, etc.) from the raw data:
|
|
259
|
-
|
|
260
|
-
```ts
|
|
261
|
-
const values = sim.series.map(b => b.value);
|
|
262
|
-
const dailyReturns = values.slice(1).map((v, i) => (v - values[i]) / values[i]);
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
### Handle Methods
|
|
266
|
-
|
|
267
|
-
Every `IndicatorHandle`, `SignalHandle`, and `StrategyHandle` exposes:
|
|
268
70
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
allocation: AllocationHandle;
|
|
282
|
-
}
|
|
71
|
+
const executor = new BacktestExecutor({ calendar, nextOpen });
|
|
72
|
+
const strategy = fromSpec(spec, { runtime, calendar });
|
|
73
|
+
|
|
74
|
+
// 3. Run.
|
|
75
|
+
const result = await runBacktest({
|
|
76
|
+
strategy,
|
|
77
|
+
range,
|
|
78
|
+
initialPortfolio: { cash: 100_000, positions: [], t: range.from },
|
|
79
|
+
dataFeed,
|
|
80
|
+
executor,
|
|
81
|
+
calendar,
|
|
82
|
+
});
|
|
283
83
|
|
|
284
|
-
|
|
285
|
-
const subset = await sma200.series({ from: '2024-01-01', to: '2024-12-31' });
|
|
84
|
+
console.log(result.snapshots.at(-1));
|
|
286
85
|
```
|
|
287
86
|
|
|
288
|
-
|
|
87
|
+
For live evaluation, recipes, and the full API reference, head to the
|
|
88
|
+
[documentation site](https://livefolio.github.io/sdk/).
|
|
289
89
|
|
|
290
|
-
|
|
90
|
+
## Working on this repo
|
|
291
91
|
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm install # install deps (sdk + parity workspace)
|
|
94
|
+
npm test # run all Vitest suites
|
|
95
|
+
npm run build # bundle to dist/ with tsup
|
|
295
96
|
```
|
|
296
97
|
|
|
297
|
-
|
|
98
|
+
### Running the docs site locally
|
|
298
99
|
|
|
299
|
-
|
|
100
|
+
The docs site is VitePress + TypeDoc, sourced from TSDoc comments and
|
|
101
|
+
the markdown under `docs-site/`.
|
|
300
102
|
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
|
|
103
|
+
```bash
|
|
104
|
+
npm run docs:dev # live-reload dev server at http://localhost:5173
|
|
105
|
+
npm run docs:build # typedoc + vitepress build → docs-site/.vitepress/dist
|
|
106
|
+
npm run docs:preview # serve the production build locally
|
|
107
|
+
npm run docs:check # type-check the runnable code samples in scripts/docs/
|
|
304
108
|
```
|
|
305
109
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
| Indicator Type | Source |
|
|
309
|
-
|---|---|
|
|
310
|
-
| Price, VIX, VIX3M | Yahoo Finance |
|
|
311
|
-
| Treasury rates (T3M--T30Y) | FRED API |
|
|
312
|
-
| SMA, EMA, RSI, Returns, Volatility, Drawdown | Computed from Price |
|
|
313
|
-
| Calendar (Month, Day of Week, etc.) | Computed from trading days |
|
|
314
|
-
| Threshold | Constant (no external data) |
|
|
315
|
-
|
|
316
|
-
## Database
|
|
317
|
-
|
|
318
|
-
The SDK uses Supabase as its backing store. Schema files are in `supabase/schemas/`. Key tables:
|
|
319
|
-
|
|
320
|
-
- `trading_days` -- market calendar with session timestamps
|
|
321
|
-
- `tickers` -- symbols with leverage multiplier
|
|
322
|
-
- `indicators` -- indicator definitions (type, params, ticker reference)
|
|
323
|
-
- `indicators_series` -- daily indicator values linked to trading days
|
|
324
|
-
- `signals` -- signal definitions (two indicators, comparison, tolerance)
|
|
325
|
-
- `signals_series` -- daily boolean signal values linked to trading days
|
|
326
|
-
- `allocations` -- portfolio holdings as JSONB (deduplicated)
|
|
327
|
-
- `strategies` -- strategy definitions with rebalance frequency and rule JSONB
|
|
328
|
-
- `strategies_series` -- active allocation per trading day per strategy (dense)
|
|
329
|
-
|
|
330
|
-
Run `supabase db reset` to set up the local database from the schema and seed files.
|
|
110
|
+
CI runs `docs:check` separately from `npm test` — run it after touching
|
|
111
|
+
public types or sample code.
|
|
331
112
|
|
|
332
113
|
## License
|
|
333
114
|
|