@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 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 and backtesting trading strategies. Provides lazy handles for tickers and indicators that automatically fetch market data from Yahoo Finance and FRED, compute derived indicators, and cache results in a Supabase database.
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 @supabase/supabase-js
9
- ```
10
-
11
- ## Quick Start
12
-
13
- ```ts
14
- import { createClient } from '@livefolio/sdk';
15
- import { createClient as createSupabaseClient } from '@supabase/supabase-js';
16
-
17
- const supabase = createSupabaseClient(SUPABASE_URL, SUPABASE_KEY);
18
-
19
- const sdk = createClient({
20
- supabase,
21
- fredApiKey: 'your-fred-api-key', // optional, required for treasury indicators
22
- });
23
-
24
- const spy = sdk.ticker('SPY');
25
- const sma200 = sdk.sma(spy, 200);
26
-
27
- // First call fetches SPY prices from Yahoo Finance, computes the 200-day SMA,
28
- // stores everything in the database, and returns the result.
29
- // Subsequent calls return cached data instantly.
30
- const series = await sma200.series();
31
- const latest = await sma200.value();
32
- ```
33
-
34
- ## Concepts
35
-
36
- ### Lazy Handles
37
-
38
- Everything in the SDK is a **lazy handle** -- a lightweight object that describes *what* you want without hitting the database or any external API. Data is only fetched when you call `.series()` or `.value()`.
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
- const history = await strategy.series();
210
- // StrategyBar[] { date: string, allocation: AllocationHandle }
211
-
212
- const current = await strategy.value();
213
- // AllocationHandle for the latest trading day
214
- ```
215
-
216
- Trading frequencies: `'Daily'`, `'Weekly'`, `'Monthly'`, `'Bi-monthly'`, `'Quarterly'`, `'Every 4 Months'`, `'Semiannually'`, `'Yearly'`.
217
-
218
- Strategy series are **dense** -- one row per trading day. On rebalance dates the rules are evaluated; on other days the previous allocation carries forward.
219
-
220
- Each strategy gets a unique `link_id` (nanoid) on creation. Reference an existing strategy by its link ID to reload it without recreating.
221
-
222
- ### Simulation
223
-
224
- Run a portfolio simulation over a date range. Returns a `SimulationHandle` with the equity curve and trade history.
225
-
226
- ```ts
227
- const spy = sdk.ticker('SPY');
228
- const cashx = sdk.ticker('CASHX');
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
- #### `.series(range?)`
270
-
271
- Returns the full time series. For indicators and signals this is `DailyBar[]`; for strategies it is `StrategyBar[]`.
272
-
273
- ```ts
274
- interface DailyBar {
275
- date: string; // 'YYYY-MM-DD'
276
- value: number;
277
- }
278
-
279
- interface StrategyBar {
280
- date: string;
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
- const all = await sma200.series();
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
- #### `.value(date?)`
87
+ For live evaluation, recipes, and the full API reference, head to the
88
+ [documentation site](https://livefolio.github.io/sdk/).
289
89
 
290
- Returns the latest value, or the value for a specific date. Returns `null` if no data exists. For strategies, returns `AllocationHandle | null`.
90
+ ## Working on this repo
291
91
 
292
- ```ts
293
- const latest = await sma200.value();
294
- const specific = await sma200.value('2024-06-15');
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
- #### `.resolve()`
98
+ ### Running the docs site locally
298
99
 
299
- Explicitly upserts the indicator to the database and returns the row. Normally you don't need to call this -- `.series()` and `.value()` call it automatically.
100
+ The docs site is VitePress + TypeDoc, sourced from TSDoc comments and
101
+ the markdown under `docs-site/`.
300
102
 
301
- ```ts
302
- const row = await sma200.resolve();
303
- console.log(row.id); // database ID
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
- ## Data Sources
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