@livefolio/sdk 0.5.0-rc.2 → 0.5.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +160 -9
- package/dist/index.js +160 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -127,6 +127,22 @@ type Series = ReadonlyArray<{
|
|
|
127
127
|
t: Date;
|
|
128
128
|
v: number;
|
|
129
129
|
}>;
|
|
130
|
+
/**
|
|
131
|
+
* A cash distribution (dividend) or interest payment for an asset, carrying
|
|
132
|
+
* what the SDK needs to credit cash and classify the income per-lot.
|
|
133
|
+
*
|
|
134
|
+
* `incomeKind: 'qualified-eligible'` means the distribution CAN be qualified if
|
|
135
|
+
* a holding lot satisfies the 60-of-121-day rule; the runtime resolves the
|
|
136
|
+
* actual qualified-vs-ordinary split per lot. `'ordinary'`/`'interest'` are
|
|
137
|
+
* never qualified.
|
|
138
|
+
*/
|
|
139
|
+
type DividendEvent = {
|
|
140
|
+
asset: Asset;
|
|
141
|
+
exDate: Date;
|
|
142
|
+
payDate: Date;
|
|
143
|
+
amountPerShare: number;
|
|
144
|
+
incomeKind: 'qualified-eligible' | 'ordinary' | 'interest';
|
|
145
|
+
};
|
|
130
146
|
|
|
131
147
|
/**
|
|
132
148
|
* Stable opaque string identifier for a {@link Position}. Generated by the
|
|
@@ -187,7 +203,7 @@ type IncomeKind = 'capital-gain' | 'qualified-dividend' | 'ordinary-dividend' |
|
|
|
187
203
|
* sales reduce `quantity` and pro-rate `basis`.
|
|
188
204
|
*/
|
|
189
205
|
type Lot = {
|
|
190
|
-
/** Opaque id assigned by
|
|
206
|
+
/** Opaque id assigned by `nextLotId` (internal) on creation. */
|
|
191
207
|
id: string;
|
|
192
208
|
asset: Asset;
|
|
193
209
|
/** Shares remaining in this lot after any partial sales. */
|
|
@@ -767,9 +783,13 @@ interface DataFeed {
|
|
|
767
783
|
* @param asset - The instrument to fetch bars for.
|
|
768
784
|
* @param range - Half-open date range; `range.from` inclusive, `range.to` exclusive.
|
|
769
785
|
* @param freq - Bar width. `'1d'` returns one bar per trading day.
|
|
786
|
+
* @param kind - `'adjusted'` (default) applies split/dividend adjustments;
|
|
787
|
+
* `'unadjusted'` returns raw prices. Indicators consume adjusted bars;
|
|
788
|
+
* execution fills and dividend cash-flow use unadjusted bars. Vendors that
|
|
789
|
+
* do not distinguish may ignore this and always return their single series.
|
|
770
790
|
* @returns An async iterable of {@link Bar} objects.
|
|
771
791
|
*/
|
|
772
|
-
bars(asset: Asset, range: DateRange, freq: Frequency): AsyncIterable<Bar>;
|
|
792
|
+
bars(asset: Asset, range: DateRange, freq: Frequency, kind?: 'adjusted' | 'unadjusted'): AsyncIterable<Bar>;
|
|
773
793
|
/**
|
|
774
794
|
* Returns a snapshot of fundamental data for `asset` as of `t`.
|
|
775
795
|
* Optional — not all data providers carry fundamentals.
|
|
@@ -790,6 +810,17 @@ interface DataFeed {
|
|
|
790
810
|
* @returns An async iterable of {@link DataEvent} objects.
|
|
791
811
|
*/
|
|
792
812
|
events?(range: DateRange, kinds: ReadonlyArray<EventKind>): AsyncIterable<DataEvent>;
|
|
813
|
+
/**
|
|
814
|
+
* Returns cash distributions (dividends / interest) for `asset` over `range`.
|
|
815
|
+
* Optional — providers without dividend data omit it. Consumed by
|
|
816
|
+
* `runBacktest`'s per-session dividend hook. Amounts are per-share, on the
|
|
817
|
+
* unadjusted price basis.
|
|
818
|
+
*
|
|
819
|
+
* @param asset - The instrument to query.
|
|
820
|
+
* @param range - Half-open date range.
|
|
821
|
+
* @returns A promise resolving to an array of {@link DividendEvent} objects.
|
|
822
|
+
*/
|
|
823
|
+
dividends?(asset: Asset, range: DateRange): Promise<DividendEvent[]>;
|
|
793
824
|
}
|
|
794
825
|
|
|
795
826
|
/**
|
|
@@ -1599,6 +1630,27 @@ declare class FeatureRuntime {
|
|
|
1599
1630
|
compute(spec: FeatureSpec, asset: Asset): Promise<Series>;
|
|
1600
1631
|
}
|
|
1601
1632
|
|
|
1633
|
+
/**
|
|
1634
|
+
* A scheduled cash injection or withdrawal. Applied at the start of the
|
|
1635
|
+
* matching session — BEFORE `universe`/`features`/`build` — by `runBacktest`
|
|
1636
|
+
* (see Task 10 wiring). Events with `t <= sessionT` that have not yet been
|
|
1637
|
+
* consumed are applied (and summed) on that session.
|
|
1638
|
+
*/
|
|
1639
|
+
type CashEvent = {
|
|
1640
|
+
t: Date;
|
|
1641
|
+
/** Positive = deposit, negative = withdrawal. */
|
|
1642
|
+
delta: number;
|
|
1643
|
+
/**
|
|
1644
|
+
* Optional attribution tag for downstream metrics. `'deposit'`/`'withdrawal'`
|
|
1645
|
+
* are the natural tags for user-scheduled flows (this surface's main use case).
|
|
1646
|
+
* `'interest'`/`'dividend'` are accepted for callers who want to tag a
|
|
1647
|
+
* manually-scheduled flow as income, but the SDK's automatic per-session
|
|
1648
|
+
* interest/dividend hooks do NOT emit `CashEvent`s — they credit cash directly
|
|
1649
|
+
* and report via `BacktestSnapshot.interestIncome`/`dividendIncome`. User code
|
|
1650
|
+
* typically only sets `'deposit'`/`'withdrawal'`.
|
|
1651
|
+
*/
|
|
1652
|
+
reason?: 'deposit' | 'withdrawal' | 'interest' | 'dividend';
|
|
1653
|
+
};
|
|
1602
1654
|
/**
|
|
1603
1655
|
* All inputs required to run a historical backtest.
|
|
1604
1656
|
*
|
|
@@ -1653,6 +1705,16 @@ type RunBacktestOptions<F extends Features = Features, S = unknown> = {
|
|
|
1653
1705
|
* runtime seed its buffer from the historical bars without refetching).
|
|
1654
1706
|
*/
|
|
1655
1707
|
featureRuntime?: FeatureRuntime;
|
|
1708
|
+
/**
|
|
1709
|
+
* Optional scheduled deposits/withdrawals applied per-session before the
|
|
1710
|
+
* strategy runs. Matched by `t <= sessionT`; multiple due events are summed.
|
|
1711
|
+
* Defaults to none (today's behavior). See `BacktestSnapshot.cashFlow`.
|
|
1712
|
+
*
|
|
1713
|
+
* @remarks A withdrawal that exceeds available cash is allowed to drive cash
|
|
1714
|
+
* negative (logged via `console.warn`); automatic force-selling of holdings to
|
|
1715
|
+
* fund withdrawals is deferred to a later release, so this behavior may change.
|
|
1716
|
+
*/
|
|
1717
|
+
cashEvents?: ReadonlyArray<CashEvent>;
|
|
1656
1718
|
};
|
|
1657
1719
|
/**
|
|
1658
1720
|
* A point-in-time snapshot of the simulation at the end of a single trading session.
|
|
@@ -1669,6 +1731,10 @@ type BacktestSnapshot = {
|
|
|
1669
1731
|
orders: ReadonlyArray<Order>;
|
|
1670
1732
|
/** Fills returned by the executor for the orders above. */
|
|
1671
1733
|
fills: ReadonlyArray<Fill>;
|
|
1734
|
+
/**
|
|
1735
|
+
* Net cash delta applied this session via `cashEvents`. Omitted when zero.
|
|
1736
|
+
*/
|
|
1737
|
+
cashFlow?: number;
|
|
1672
1738
|
};
|
|
1673
1739
|
/**
|
|
1674
1740
|
* The return value of `runBacktest`, containing the full simulation history
|
|
@@ -1820,6 +1886,18 @@ interface StreamingDataFeed {
|
|
|
1820
1886
|
subscribe(assets: ReadonlyArray<Asset>): AsyncIterable<StreamingBar>;
|
|
1821
1887
|
}
|
|
1822
1888
|
|
|
1889
|
+
/**
|
|
1890
|
+
* Mutable queue for scheduling {@link CashEvent}s into a running `runLive`
|
|
1891
|
+
* stream. Construct one, pass it via {@link RunLiveOptions.cashEventQueue}, and
|
|
1892
|
+
* `push` events from outside the generator; they are drained at each session
|
|
1893
|
+
* close. A non-breaking alternative to a returned scheduling handle.
|
|
1894
|
+
*/
|
|
1895
|
+
declare class CashEventQueue {
|
|
1896
|
+
private pending;
|
|
1897
|
+
push(e: CashEvent): void;
|
|
1898
|
+
/** Remove and return events with `t <= now`, leaving later events queued. */
|
|
1899
|
+
drainDue(now: Date): CashEvent[];
|
|
1900
|
+
}
|
|
1823
1901
|
/**
|
|
1824
1902
|
* Unified event stream from {@link runLive}. Discriminated union of two variants:
|
|
1825
1903
|
*
|
|
@@ -1861,6 +1939,25 @@ type LiveEvent<F extends Features = Features, _S = unknown> = {
|
|
|
1861
1939
|
* `portfolio` + `prices`.
|
|
1862
1940
|
*/
|
|
1863
1941
|
previewPortfolio: Portfolio;
|
|
1942
|
+
} | {
|
|
1943
|
+
/**
|
|
1944
|
+
* A net cash injection/withdrawal applied to the committed portfolio at a
|
|
1945
|
+
* session close. Emitted BEFORE the `snapshot` for that session, so the
|
|
1946
|
+
* subsequent snapshot's `portfolio.cash` already reflects this delta.
|
|
1947
|
+
* Only emitted when the summed delta of due events is non-zero.
|
|
1948
|
+
*/
|
|
1949
|
+
type: 'cash';
|
|
1950
|
+
/** Session-close timestamp at which the delta was applied. */
|
|
1951
|
+
t: Date;
|
|
1952
|
+
/** Net cash delta applied (sum of all due events). Positive = deposit. */
|
|
1953
|
+
delta: number;
|
|
1954
|
+
/**
|
|
1955
|
+
* Attribution tag of the FIRST contributing event. When multiple cash
|
|
1956
|
+
* events are due at the same session close they are summed into one delta
|
|
1957
|
+
* and the other reasons are dropped — use `cashEventQueue` and drain
|
|
1958
|
+
* manually if you need per-event attribution.
|
|
1959
|
+
*/
|
|
1960
|
+
reason?: CashEvent['reason'];
|
|
1864
1961
|
} | (BacktestSnapshot & {
|
|
1865
1962
|
type: 'snapshot';
|
|
1866
1963
|
});
|
|
@@ -1891,6 +1988,20 @@ type RunLiveOptions<F extends Features = Features, S = unknown> = {
|
|
|
1891
1988
|
* seeded from `history.bars`.
|
|
1892
1989
|
*/
|
|
1893
1990
|
streamingRuntime?: FeatureRuntime;
|
|
1991
|
+
/**
|
|
1992
|
+
* Cash injections/withdrawals known up front. Each is applied to the
|
|
1993
|
+
* committed portfolio at the first session close whose date is `>= e.t`,
|
|
1994
|
+
* summed with any other due events for that session. For dynamic scheduling
|
|
1995
|
+
* after the stream has started, use {@link RunLiveOptions.cashEventQueue}.
|
|
1996
|
+
*/
|
|
1997
|
+
cashEvents?: ReadonlyArray<CashEvent>;
|
|
1998
|
+
/**
|
|
1999
|
+
* Optional mutable queue for scheduling cash events into the running stream
|
|
2000
|
+
* from outside the generator. Drained (alongside `cashEvents`) at each
|
|
2001
|
+
* session close. Construct a {@link CashEventQueue}, pass it here, and `push`
|
|
2002
|
+
* events as they arrive.
|
|
2003
|
+
*/
|
|
2004
|
+
cashEventQueue?: CashEventQueue;
|
|
1894
2005
|
};
|
|
1895
2006
|
/**
|
|
1896
2007
|
* Drives a {@link Strategy} against a streaming market-data source and yields
|
|
@@ -2122,6 +2233,28 @@ type BacktestExecutorOptions = {
|
|
|
2122
2233
|
* the fill quantity and recorded in `Fill.fees`. Defaults to `0`.
|
|
2123
2234
|
*/
|
|
2124
2235
|
perShareFee?: number;
|
|
2236
|
+
/**
|
|
2237
|
+
* Tax-lot selection method applied to long sells (a `rebalance` reduce or a
|
|
2238
|
+
* `close` of a long position). When set to a non-default method
|
|
2239
|
+
* (`'LIFO'` / `'HIFO'` / `'min-tax'`), such a sell is split into one
|
|
2240
|
+
* {@link Fill} per selected lot — each carrying `Fill.lotId` — so
|
|
2241
|
+
* {@link applyFills} consumes those exact lots.
|
|
2242
|
+
*
|
|
2243
|
+
* Defaults to `'FIFO'` (equivalently, leaving this unset): the executor emits
|
|
2244
|
+
* a single fill per order with no `lotId`, and `applyFills` performs its own
|
|
2245
|
+
* internal FIFO. Buys, short-side closes, and `adjust` orders are never split.
|
|
2246
|
+
*/
|
|
2247
|
+
lotMethod?: 'FIFO' | 'LIFO' | 'HIFO' | 'min-tax';
|
|
2248
|
+
/**
|
|
2249
|
+
* Short- and long-term capital-gains tax rates (as decimals, e.g. `0.37`)
|
|
2250
|
+
* forwarded to the `'min-tax'` selector. **Required** when
|
|
2251
|
+
* `lotMethod === 'min-tax'`; the constructor throws otherwise. Ignored for
|
|
2252
|
+
* all other lot methods.
|
|
2253
|
+
*/
|
|
2254
|
+
taxRates?: {
|
|
2255
|
+
shortTerm: number;
|
|
2256
|
+
longTerm: number;
|
|
2257
|
+
};
|
|
2125
2258
|
};
|
|
2126
2259
|
/**
|
|
2127
2260
|
* Reference {@link Executor} implementation for backtesting. Fills each order
|
|
@@ -2142,6 +2275,12 @@ type BacktestExecutorOptions = {
|
|
|
2142
2275
|
* A flat per-share fee is added to `Fill.fees`. Orders with zero quantity are
|
|
2143
2276
|
* silently skipped.
|
|
2144
2277
|
*
|
|
2278
|
+
* **Lot selection**: by default each order yields exactly one fill. When a
|
|
2279
|
+
* non-default `lotMethod` (`'LIFO'` / `'HIFO'` / `'min-tax'`) is configured,
|
|
2280
|
+
* long sells (a `rebalance` reduce or a `close` of a long position) are split
|
|
2281
|
+
* into one fill per selected lot — each tagged with `Fill.lotId` — so
|
|
2282
|
+
* {@link applyFills} consumes those exact lots. See {@link BacktestExecutorOptions.lotMethod}.
|
|
2283
|
+
*
|
|
2145
2284
|
* @example
|
|
2146
2285
|
* ```ts
|
|
2147
2286
|
* import { BacktestExecutor } from '@livefolio/sdk';
|
|
@@ -2170,7 +2309,8 @@ declare class BacktestExecutor implements Executor {
|
|
|
2170
2309
|
* when the routed feed does not support the requested optional method.
|
|
2171
2310
|
*
|
|
2172
2311
|
* Distinguish the two cases via the message text: "no feed registered" vs
|
|
2173
|
-
* "does not implement fundamentals"
|
|
2312
|
+
* "does not implement `<method>`" (e.g. "does not implement fundamentals()" or
|
|
2313
|
+
* "does not implement dividends()").
|
|
2174
2314
|
*/
|
|
2175
2315
|
declare class RoutingDataFeedError extends Error {
|
|
2176
2316
|
constructor(message: string);
|
|
@@ -2191,9 +2331,11 @@ type RoutingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], DataFeed>>
|
|
|
2191
2331
|
* - **Function form:** `new RoutingDataFeed((a) => a.kind === 'macro' ? fred : yahoo)`.
|
|
2192
2332
|
* Use when routing depends on more than `kind` (e.g. allowlists).
|
|
2193
2333
|
*
|
|
2194
|
-
*
|
|
2195
|
-
*
|
|
2196
|
-
*
|
|
2334
|
+
* `dividends()` and `fundamentals()` ARE implemented — each targets a single
|
|
2335
|
+
* asset, so it resolves to that asset's routed feed (or throws if the feed
|
|
2336
|
+
* lacks the method). The router does **not** implement `events()` — the
|
|
2337
|
+
* optional method is genuinely absent (`'events' in router === false`) because
|
|
2338
|
+
* cross-feed event fan-out is a separate, deferred problem.
|
|
2197
2339
|
*
|
|
2198
2340
|
* @example
|
|
2199
2341
|
* ```ts
|
|
@@ -2212,8 +2354,17 @@ type RoutingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], DataFeed>>
|
|
|
2212
2354
|
declare class RoutingDataFeed implements DataFeed {
|
|
2213
2355
|
private readonly route;
|
|
2214
2356
|
constructor(routes: RoutingDataFeedRouteMap | RoutingDataFeedRouteFn);
|
|
2215
|
-
bars(asset: Asset, range: DateRange, freq: Frequency): AsyncGenerator<Bar>;
|
|
2357
|
+
bars(asset: Asset, range: DateRange, freq: Frequency, kind?: 'adjusted' | 'unadjusted'): AsyncGenerator<Bar>;
|
|
2216
2358
|
fundamentals(asset: Asset, t: Date): Promise<Fundamentals>;
|
|
2359
|
+
/**
|
|
2360
|
+
* Resolves `asset` to its routed feed and delegates `dividends`. This method
|
|
2361
|
+
* is ALWAYS present on the router (unlike a leaf feed's optional `dividends?`),
|
|
2362
|
+
* so `typeof routingFeed.dividends === 'function'` is always `true` — capability
|
|
2363
|
+
* detection must account for the routed feed possibly lacking it at call time.
|
|
2364
|
+
*
|
|
2365
|
+
* @throws {RoutingDataFeedError} when the routed feed does not implement `dividends`.
|
|
2366
|
+
*/
|
|
2367
|
+
dividends(asset: Asset, range: DateRange): Promise<DividendEvent[]>;
|
|
2217
2368
|
private resolve;
|
|
2218
2369
|
}
|
|
2219
2370
|
|
|
@@ -3114,7 +3265,7 @@ declare function evaluateFeatureSpecs(specs: ReadonlyArray<TacticalFeatureSpec>,
|
|
|
3114
3265
|
* independently scaled); `volume` is passed through from the underlying bar.
|
|
3115
3266
|
*
|
|
3116
3267
|
* Non-synthetic assets are proxied transparently to the original `dataFeed`.
|
|
3117
|
-
* `fundamentals` and `
|
|
3268
|
+
* `fundamentals`, `events`, and `dividends` methods, if present, are forwarded unchanged.
|
|
3118
3269
|
*
|
|
3119
3270
|
* Throws at construction time if `synthetics` contains duplicate `id` values.
|
|
3120
3271
|
*
|
|
@@ -3977,4 +4128,4 @@ declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>)
|
|
|
3977
4128
|
*/
|
|
3978
4129
|
declare function positionsByAsset(portfolio: Portfolio): Position[];
|
|
3979
4130
|
|
|
3980
|
-
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, type IncomeKind, LSEExchangeCalendar, type LiveEvent, type Lot, type LotSlice, type MacroAsset, MemoryFeatureCache, NYSEExchangeCalendar, type NextOpenFn, ORDINARY_OFFSET_CAP, type OpenOrder, type Order, type PollingSchedule, type PollingStreamOptions, type Portfolio, type Position, type PositionId, type PriceMap, type Quote, type QuoteFeed, type RealizeResult, type RealizedEvent, type RebalanceConfig, type RebalanceFrequency, type RebalanceOrder, type ReturnMode, RoutingDataFeed, RoutingDataFeedError, type RoutingDataFeedRouteFn, type RoutingDataFeedRouteMap, RoutingQuoteFeed, RoutingQuoteFeedError, type RoutingQuoteFeedRouteFn, type RoutingQuoteFeedRouteMap, RoutingStreamingDataFeed, RoutingStreamingDataFeedError, type RoutingStreamingDataFeedRouteFn, type RoutingStreamingDataFeedRouteMap, type RuleNode, type RuleTreeState, type RunBacktestOptions, type RunLiveOptions, type Series, type Session, type SpecialClose, type SpecialOpen, type Strategy, type StreamingBar, type StreamingDataFeed, type SyntheticAsset, type TacticalFeatureKind, type TacticalFeatureSpec, type TacticalFeatures, type TacticalSpec, type TargetWeights, type TaxRates, type TaxableIncome, type TimeOfDay, type Tolerance, type WithStreamingSyntheticsOptions, aggregateByYear, applyFills, applyOrders, barsToSeries, bucketByTerm, collectBars, computeTaxBill, crossOffset, defineFeature, drawdown, ema, evaluateFeatureSpecs, evaluateRuleTree, index$1 as features, fromSpec, getCalendar, getFeatureCompute, holdingPeriodDays, isLongTerm, isRebalanceDay, netWithinBucket, paramsHash, periodKey, pollingStreamFromHistorical, positionsByAsset, realize, reconcile, returnSeries, rsi, runBacktest, runLive, selectFIFO, selectHIFO, selectLIFO, selectMinTax, seriesAt, sma, index$2 as tactical, index as tax, volatility, withStreamingSynthetics, withSynthetics };
|
|
4131
|
+
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 CashEvent, CashEventQueue, type CloseOrder, type Comparison, type ComparisonOp, type ComputeFn, Crypto24x7Calendar, type DataEvent, type DataFeed, type DateRange, type DividendEvent, 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, type IncomeKind, LSEExchangeCalendar, type LiveEvent, type Lot, type LotSlice, type MacroAsset, MemoryFeatureCache, NYSEExchangeCalendar, type NextOpenFn, ORDINARY_OFFSET_CAP, type OpenOrder, type Order, type PollingSchedule, type PollingStreamOptions, type Portfolio, type Position, type PositionId, type PriceMap, type Quote, type QuoteFeed, type RealizeResult, type RealizedEvent, type RebalanceConfig, type RebalanceFrequency, type RebalanceOrder, type ReturnMode, RoutingDataFeed, RoutingDataFeedError, type RoutingDataFeedRouteFn, type RoutingDataFeedRouteMap, RoutingQuoteFeed, RoutingQuoteFeedError, type RoutingQuoteFeedRouteFn, type RoutingQuoteFeedRouteMap, RoutingStreamingDataFeed, RoutingStreamingDataFeedError, type RoutingStreamingDataFeedRouteFn, type RoutingStreamingDataFeedRouteMap, type RuleNode, type RuleTreeState, type RunBacktestOptions, type RunLiveOptions, type Series, type Session, type SpecialClose, type SpecialOpen, type Strategy, type StreamingBar, type StreamingDataFeed, type SyntheticAsset, type TacticalFeatureKind, type TacticalFeatureSpec, type TacticalFeatures, type TacticalSpec, type TargetWeights, type TaxRates, type TaxableIncome, type TimeOfDay, type Tolerance, type WithStreamingSyntheticsOptions, aggregateByYear, applyFills, applyOrders, barsToSeries, bucketByTerm, collectBars, computeTaxBill, crossOffset, defineFeature, drawdown, ema, evaluateFeatureSpecs, evaluateRuleTree, index$1 as features, fromSpec, getCalendar, getFeatureCompute, holdingPeriodDays, isLongTerm, isRebalanceDay, netWithinBucket, paramsHash, periodKey, pollingStreamFromHistorical, positionsByAsset, realize, reconcile, returnSeries, rsi, runBacktest, runLive, selectFIFO, selectHIFO, selectLIFO, selectMinTax, seriesAt, sma, index$2 as tactical, index as tax, volatility, withStreamingSynthetics, withSynthetics };
|
package/dist/index.js
CHANGED
|
@@ -331,7 +331,24 @@ async function runBacktest(opts) {
|
|
|
331
331
|
let portfolio = opts.initialPortfolio;
|
|
332
332
|
let state = initialStateValue;
|
|
333
333
|
const snapshots = [];
|
|
334
|
+
const cashEvents = [...opts.cashEvents ?? []].sort((a, b) => a.t.getTime() - b.t.getTime());
|
|
335
|
+
let eventCursor = 0;
|
|
336
|
+
let warnedNegativeCash = false;
|
|
334
337
|
for (const t of sessions) {
|
|
338
|
+
let cashFlow = 0;
|
|
339
|
+
while (eventCursor < cashEvents.length && cashEvents[eventCursor].t.getTime() <= t.getTime()) {
|
|
340
|
+
cashFlow += cashEvents[eventCursor].delta;
|
|
341
|
+
eventCursor++;
|
|
342
|
+
}
|
|
343
|
+
if (cashFlow !== 0) {
|
|
344
|
+
portfolio = { ...portfolio, cash: portfolio.cash + cashFlow };
|
|
345
|
+
if (portfolio.cash < 0 && !warnedNegativeCash) {
|
|
346
|
+
warnedNegativeCash = true;
|
|
347
|
+
console.warn(
|
|
348
|
+
`[runBacktest] cash went negative at ${t.toISOString()}: ${portfolio.cash}. Withdrawals exceed available cash (force-sell is deferred); further occurrences this run are suppressed.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
335
352
|
const universe = opts.strategy.universe(t, portfolio);
|
|
336
353
|
const features = await opts.strategy.features(universe, portfolio, t);
|
|
337
354
|
const buildResult = opts.strategy.build(features, portfolio, state, t);
|
|
@@ -344,7 +361,7 @@ async function runBacktest(opts) {
|
|
|
344
361
|
}
|
|
345
362
|
const fills = await opts.executor.submit(orders, t, portfolio);
|
|
346
363
|
portfolio = applyFills(portfolio, fills, orders);
|
|
347
|
-
snapshots.push({ t, portfolio, orders, fills });
|
|
364
|
+
snapshots.push({ t, portfolio, orders, fills, ...cashFlow !== 0 ? { cashFlow } : {} });
|
|
348
365
|
}
|
|
349
366
|
const bars = opts.featureRuntime?.getAllBars() ?? /* @__PURE__ */ new Map();
|
|
350
367
|
return { snapshots, finalPortfolio: portfolio, finalState: state, bars };
|
|
@@ -757,6 +774,18 @@ var MemoryFeatureCache = class {
|
|
|
757
774
|
};
|
|
758
775
|
|
|
759
776
|
// src/strategy/run-live.ts
|
|
777
|
+
var CashEventQueue = class {
|
|
778
|
+
pending = [];
|
|
779
|
+
push(e) {
|
|
780
|
+
this.pending.push(e);
|
|
781
|
+
}
|
|
782
|
+
/** Remove and return events with `t <= now`, leaving later events queued. */
|
|
783
|
+
drainDue(now) {
|
|
784
|
+
const due = this.pending.filter((e) => e.t.getTime() <= now.getTime());
|
|
785
|
+
if (due.length > 0) this.pending = this.pending.filter((e) => e.t.getTime() > now.getTime());
|
|
786
|
+
return due;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
760
789
|
function snapshotState(state) {
|
|
761
790
|
if (state === void 0) return void 0;
|
|
762
791
|
return structuredClone(state);
|
|
@@ -775,6 +804,8 @@ async function* runLive(opts) {
|
|
|
775
804
|
});
|
|
776
805
|
let portfolio = history.finalPortfolio;
|
|
777
806
|
let state = history.finalState;
|
|
807
|
+
const seedQueue = new CashEventQueue();
|
|
808
|
+
for (const e of opts.cashEvents ?? []) seedQueue.push(e);
|
|
778
809
|
const anchorTime = history.snapshots.length > 0 ? history.snapshots[history.snapshots.length - 1].t : /* @__PURE__ */ new Date(0);
|
|
779
810
|
const universe = strategy.universe(anchorTime, portfolio);
|
|
780
811
|
let currentSession = history.snapshots.length > 0 ? calendar.next(history.snapshots[history.snapshots.length - 1].t) : null;
|
|
@@ -826,6 +857,17 @@ async function* runLive(opts) {
|
|
|
826
857
|
}
|
|
827
858
|
const fills = await executor.submit(orders, currentSession, portfolio);
|
|
828
859
|
portfolio = applyFills(portfolio, fills, orders);
|
|
860
|
+
const dueCash = [...seedQueue.drainDue(currentSession), ...opts.cashEventQueue?.drainDue(currentSession) ?? []];
|
|
861
|
+
const cashDelta = dueCash.reduce((s, e) => s + e.delta, 0);
|
|
862
|
+
if (cashDelta !== 0) {
|
|
863
|
+
portfolio = { ...portfolio, cash: portfolio.cash + cashDelta };
|
|
864
|
+
yield {
|
|
865
|
+
type: "cash",
|
|
866
|
+
t: currentSession,
|
|
867
|
+
delta: cashDelta,
|
|
868
|
+
reason: dueCash[0]?.reason
|
|
869
|
+
};
|
|
870
|
+
}
|
|
829
871
|
yield {
|
|
830
872
|
type: "snapshot",
|
|
831
873
|
t: currentSession,
|
|
@@ -867,40 +909,124 @@ async function* runLive(opts) {
|
|
|
867
909
|
}
|
|
868
910
|
}
|
|
869
911
|
|
|
912
|
+
// src/tax/lot-selection.ts
|
|
913
|
+
function take(sorted, qty) {
|
|
914
|
+
let need = qty;
|
|
915
|
+
const out = [];
|
|
916
|
+
for (const lot of sorted) {
|
|
917
|
+
if (lot.quantity <= 0) continue;
|
|
918
|
+
if (need <= 0) break;
|
|
919
|
+
const q = Math.min(lot.quantity, need);
|
|
920
|
+
out.push({ lotId: lot.id, quantity: q });
|
|
921
|
+
need -= q;
|
|
922
|
+
}
|
|
923
|
+
if (need > 1e-9) {
|
|
924
|
+
const held = sorted.reduce((s, l) => s + Math.max(0, l.quantity), 0);
|
|
925
|
+
throw new RangeError(`lot-selection: need ${qty} but only ${held} held`);
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
function selectFIFO(lots, qty) {
|
|
930
|
+
return take(
|
|
931
|
+
[...lots].sort((a, b) => a.openDate.getTime() - b.openDate.getTime()),
|
|
932
|
+
qty
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
function selectLIFO(lots, qty) {
|
|
936
|
+
return take(
|
|
937
|
+
[...lots].sort((a, b) => b.openDate.getTime() - a.openDate.getTime()),
|
|
938
|
+
qty
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
function selectHIFO(lots, qty) {
|
|
942
|
+
return take(
|
|
943
|
+
[...lots].filter((l) => l.quantity > 0).sort((a, b) => b.basis / b.quantity - a.basis / a.quantity),
|
|
944
|
+
qty
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
function selectMinTax(lots, qty, ctx) {
|
|
948
|
+
const tier = (l) => {
|
|
949
|
+
const gainPerShare = ctx.price - l.basis / l.quantity;
|
|
950
|
+
const lt = isLongTerm(holdingPeriodDays(l, ctx.asOf));
|
|
951
|
+
if (gainPerShare < 0) return lt ? 0 : 1;
|
|
952
|
+
return lt ? 2 : 3;
|
|
953
|
+
};
|
|
954
|
+
const sorted = [...lots].filter((l) => l.quantity > 0).sort((a, b) => {
|
|
955
|
+
const ta = tier(a);
|
|
956
|
+
const tb = tier(b);
|
|
957
|
+
if (ta !== tb) return ta - tb;
|
|
958
|
+
const gainA = ctx.price - a.basis / a.quantity;
|
|
959
|
+
const gainB = ctx.price - b.basis / b.quantity;
|
|
960
|
+
return gainA - gainB;
|
|
961
|
+
});
|
|
962
|
+
return take(sorted, qty);
|
|
963
|
+
}
|
|
964
|
+
|
|
870
965
|
// src/reference/backtest-executor.ts
|
|
871
966
|
function resolveAsset(order, portfolio) {
|
|
872
967
|
switch (order.kind) {
|
|
873
968
|
case "open":
|
|
874
|
-
return { asset: order.asset, sign: order.side === "long" ? 1 : -1, qty: order.quantity };
|
|
969
|
+
return { asset: order.asset, sign: order.side === "long" ? 1 : -1, qty: order.quantity, lotConsumingSell: false };
|
|
875
970
|
case "rebalance":
|
|
876
|
-
return {
|
|
971
|
+
return {
|
|
972
|
+
asset: order.asset,
|
|
973
|
+
sign: order.delta >= 0 ? 1 : -1,
|
|
974
|
+
qty: Math.abs(order.delta),
|
|
975
|
+
lotConsumingSell: order.delta < 0
|
|
976
|
+
};
|
|
877
977
|
case "close": {
|
|
878
978
|
const p = portfolio.positions.find((x) => x.id === order.positionId);
|
|
879
979
|
if (!p) throw new Error(`BacktestExecutor: close target position ${order.positionId} not found`);
|
|
880
|
-
return {
|
|
980
|
+
return {
|
|
981
|
+
asset: p.asset,
|
|
982
|
+
sign: p.side === "long" ? -1 : 1,
|
|
983
|
+
qty: order.quantity ?? p.quantity,
|
|
984
|
+
lotConsumingSell: p.side === "long"
|
|
985
|
+
};
|
|
881
986
|
}
|
|
882
987
|
case "adjust": {
|
|
883
988
|
const p = portfolio.positions.find((x) => x.id === order.positionId);
|
|
884
989
|
if (!p) throw new Error(`BacktestExecutor: adjust target position ${order.positionId} not found`);
|
|
885
990
|
const target = order.changes.quantity ?? p.quantity;
|
|
886
991
|
const delta = target - p.quantity;
|
|
887
|
-
return { asset: p.asset, sign: delta >= 0 ? 1 : -1, qty: Math.abs(delta) };
|
|
992
|
+
return { asset: p.asset, sign: delta >= 0 ? 1 : -1, qty: Math.abs(delta), lotConsumingSell: false };
|
|
888
993
|
}
|
|
889
994
|
}
|
|
890
995
|
}
|
|
891
996
|
var BacktestExecutor = class {
|
|
892
997
|
constructor(opts) {
|
|
893
998
|
this.opts = opts;
|
|
999
|
+
if (opts.lotMethod === "min-tax" && !opts.taxRates) {
|
|
1000
|
+
throw new Error("BacktestExecutor: lotMethod 'min-tax' requires taxRates");
|
|
1001
|
+
}
|
|
894
1002
|
}
|
|
895
1003
|
async submit(orders, t, portfolio) {
|
|
896
1004
|
const fills = [];
|
|
897
1005
|
const slip = (this.opts.slippageBps ?? 0) / 1e4;
|
|
898
1006
|
const feePer = this.opts.perShareFee ?? 0;
|
|
1007
|
+
const method = this.opts.lotMethod;
|
|
899
1008
|
for (const order of orders) {
|
|
900
|
-
const { asset, sign, qty } = resolveAsset(order, portfolio);
|
|
1009
|
+
const { asset, sign, qty, lotConsumingSell } = resolveAsset(order, portfolio);
|
|
901
1010
|
if (qty === 0) continue;
|
|
902
1011
|
const open = await this.opts.nextOpen(asset, t);
|
|
903
1012
|
const adjustedPrice = open.price * (1 + sign * slip);
|
|
1013
|
+
if (method && method !== "FIFO" && lotConsumingSell) {
|
|
1014
|
+
const lots = (portfolio.lots ?? []).filter((l) => l.asset.id === asset.id && l.quantity > 0);
|
|
1015
|
+
if (lots.length > 0) {
|
|
1016
|
+
const slices = method === "LIFO" ? selectLIFO(lots, qty) : method === "HIFO" ? selectHIFO(lots, qty) : selectMinTax(lots, qty, { price: adjustedPrice, asOf: open.t, rates: this.opts.taxRates });
|
|
1017
|
+
for (const slice of slices) {
|
|
1018
|
+
fills.push({
|
|
1019
|
+
orderRef: order.id,
|
|
1020
|
+
t: open.t,
|
|
1021
|
+
quantity: slice.quantity,
|
|
1022
|
+
price: adjustedPrice,
|
|
1023
|
+
fees: feePer * slice.quantity,
|
|
1024
|
+
lotId: slice.lotId
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
904
1030
|
fills.push({
|
|
905
1031
|
orderRef: order.id,
|
|
906
1032
|
t: open.t,
|
|
@@ -932,9 +1058,9 @@ var RoutingDataFeed = class {
|
|
|
932
1058
|
// Async generator (rather than plain delegation) so resolve() runs lazily on
|
|
933
1059
|
// the first next() call, surfacing errors via the iterable's normal rejection
|
|
934
1060
|
// path instead of throwing synchronously at call time.
|
|
935
|
-
async *bars(asset, range, freq) {
|
|
1061
|
+
async *bars(asset, range, freq, kind) {
|
|
936
1062
|
const feed = this.resolve(asset);
|
|
937
|
-
yield* feed.bars(asset, range, freq);
|
|
1063
|
+
yield* feed.bars(asset, range, freq, kind);
|
|
938
1064
|
}
|
|
939
1065
|
async fundamentals(asset, t) {
|
|
940
1066
|
const feed = this.resolve(asset);
|
|
@@ -945,6 +1071,23 @@ var RoutingDataFeed = class {
|
|
|
945
1071
|
}
|
|
946
1072
|
return feed.fundamentals(asset, t);
|
|
947
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Resolves `asset` to its routed feed and delegates `dividends`. This method
|
|
1076
|
+
* is ALWAYS present on the router (unlike a leaf feed's optional `dividends?`),
|
|
1077
|
+
* so `typeof routingFeed.dividends === 'function'` is always `true` — capability
|
|
1078
|
+
* detection must account for the routed feed possibly lacking it at call time.
|
|
1079
|
+
*
|
|
1080
|
+
* @throws {RoutingDataFeedError} when the routed feed does not implement `dividends`.
|
|
1081
|
+
*/
|
|
1082
|
+
async dividends(asset, range) {
|
|
1083
|
+
const feed = this.resolve(asset);
|
|
1084
|
+
if (typeof feed.dividends !== "function") {
|
|
1085
|
+
throw new RoutingDataFeedError(
|
|
1086
|
+
`RoutingDataFeed: routed feed for asset.kind="${asset.kind}" (id="${asset.id}") does not implement dividends()`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
return feed.dividends(asset, range);
|
|
1090
|
+
}
|
|
948
1091
|
resolve(asset) {
|
|
949
1092
|
const feed = this.route(asset);
|
|
950
1093
|
if (feed === void 0) {
|
|
@@ -2838,11 +2981,13 @@ function withSynthetics(dataFeed, synthetics) {
|
|
|
2838
2981
|
byId.set(s.id, s);
|
|
2839
2982
|
}
|
|
2840
2983
|
const wrapped = {
|
|
2841
|
-
|
|
2984
|
+
// `kind` ('adjusted' | 'unadjusted') is forwarded to the underlying feed
|
|
2985
|
+
// before synthesis so synthetic bars derive from the requested price series.
|
|
2986
|
+
bars(asset, range, freq, kind) {
|
|
2842
2987
|
const synth = byId.get(asset.id);
|
|
2843
|
-
if (!synth) return dataFeed.bars(asset, range, freq);
|
|
2988
|
+
if (!synth) return dataFeed.bars(asset, range, freq, kind);
|
|
2844
2989
|
const underlying = resolveAssetRef(synth.underlying);
|
|
2845
|
-
return synthesize(dataFeed.bars(underlying, range, freq), synth.leverage, synth.expense);
|
|
2990
|
+
return synthesize(dataFeed.bars(underlying, range, freq, kind), synth.leverage, synth.expense);
|
|
2846
2991
|
}
|
|
2847
2992
|
};
|
|
2848
2993
|
if (dataFeed.fundamentals) {
|
|
@@ -2851,6 +2996,9 @@ function withSynthetics(dataFeed, synthetics) {
|
|
|
2851
2996
|
if (dataFeed.events) {
|
|
2852
2997
|
wrapped.events = dataFeed.events.bind(dataFeed);
|
|
2853
2998
|
}
|
|
2999
|
+
if (dataFeed.dividends) {
|
|
3000
|
+
wrapped.dividends = dataFeed.dividends.bind(dataFeed);
|
|
3001
|
+
}
|
|
2854
3002
|
return wrapped;
|
|
2855
3003
|
}
|
|
2856
3004
|
function withStreamingSynthetics(inner, synthetics, opts) {
|
|
@@ -3085,59 +3233,6 @@ __export(tax_exports, {
|
|
|
3085
3233
|
selectMinTax: () => selectMinTax
|
|
3086
3234
|
});
|
|
3087
3235
|
|
|
3088
|
-
// src/tax/lot-selection.ts
|
|
3089
|
-
function take(sorted, qty) {
|
|
3090
|
-
let need = qty;
|
|
3091
|
-
const out = [];
|
|
3092
|
-
for (const lot of sorted) {
|
|
3093
|
-
if (lot.quantity <= 0) continue;
|
|
3094
|
-
if (need <= 0) break;
|
|
3095
|
-
const q = Math.min(lot.quantity, need);
|
|
3096
|
-
out.push({ lotId: lot.id, quantity: q });
|
|
3097
|
-
need -= q;
|
|
3098
|
-
}
|
|
3099
|
-
if (need > 1e-9) {
|
|
3100
|
-
const held = sorted.reduce((s, l) => s + Math.max(0, l.quantity), 0);
|
|
3101
|
-
throw new RangeError(`lot-selection: need ${qty} but only ${held} held`);
|
|
3102
|
-
}
|
|
3103
|
-
return out;
|
|
3104
|
-
}
|
|
3105
|
-
function selectFIFO(lots, qty) {
|
|
3106
|
-
return take(
|
|
3107
|
-
[...lots].sort((a, b) => a.openDate.getTime() - b.openDate.getTime()),
|
|
3108
|
-
qty
|
|
3109
|
-
);
|
|
3110
|
-
}
|
|
3111
|
-
function selectLIFO(lots, qty) {
|
|
3112
|
-
return take(
|
|
3113
|
-
[...lots].sort((a, b) => b.openDate.getTime() - a.openDate.getTime()),
|
|
3114
|
-
qty
|
|
3115
|
-
);
|
|
3116
|
-
}
|
|
3117
|
-
function selectHIFO(lots, qty) {
|
|
3118
|
-
return take(
|
|
3119
|
-
[...lots].filter((l) => l.quantity > 0).sort((a, b) => b.basis / b.quantity - a.basis / a.quantity),
|
|
3120
|
-
qty
|
|
3121
|
-
);
|
|
3122
|
-
}
|
|
3123
|
-
function selectMinTax(lots, qty, ctx) {
|
|
3124
|
-
const tier = (l) => {
|
|
3125
|
-
const gainPerShare = ctx.price - l.basis / l.quantity;
|
|
3126
|
-
const lt = isLongTerm(holdingPeriodDays(l, ctx.asOf));
|
|
3127
|
-
if (gainPerShare < 0) return lt ? 0 : 1;
|
|
3128
|
-
return lt ? 2 : 3;
|
|
3129
|
-
};
|
|
3130
|
-
const sorted = [...lots].filter((l) => l.quantity > 0).sort((a, b) => {
|
|
3131
|
-
const ta = tier(a);
|
|
3132
|
-
const tb = tier(b);
|
|
3133
|
-
if (ta !== tb) return ta - tb;
|
|
3134
|
-
const gainA = ctx.price - a.basis / a.quantity;
|
|
3135
|
-
const gainB = ctx.price - b.basis / b.quantity;
|
|
3136
|
-
return gainA - gainB;
|
|
3137
|
-
});
|
|
3138
|
-
return take(sorted, qty);
|
|
3139
|
-
}
|
|
3140
|
-
|
|
3141
3236
|
// src/tax/aggregation.ts
|
|
3142
3237
|
var ORDINARY_OFFSET_CAP = 3e3;
|
|
3143
3238
|
function bucketByTerm(events) {
|
|
@@ -3258,6 +3353,7 @@ function positionsByAsset(portfolio) {
|
|
|
3258
3353
|
}
|
|
3259
3354
|
export {
|
|
3260
3355
|
BacktestExecutor,
|
|
3356
|
+
CashEventQueue,
|
|
3261
3357
|
Crypto24x7Calendar,
|
|
3262
3358
|
ExchangeCalendar,
|
|
3263
3359
|
FeatureRuntime,
|