@livefolio/sdk 0.5.0-rc.3 → 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 +80 -9
- package/dist/index.js +116 -63
- 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
|
/**
|
|
@@ -2202,6 +2233,28 @@ type BacktestExecutorOptions = {
|
|
|
2202
2233
|
* the fill quantity and recorded in `Fill.fees`. Defaults to `0`.
|
|
2203
2234
|
*/
|
|
2204
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
|
+
};
|
|
2205
2258
|
};
|
|
2206
2259
|
/**
|
|
2207
2260
|
* Reference {@link Executor} implementation for backtesting. Fills each order
|
|
@@ -2222,6 +2275,12 @@ type BacktestExecutorOptions = {
|
|
|
2222
2275
|
* A flat per-share fee is added to `Fill.fees`. Orders with zero quantity are
|
|
2223
2276
|
* silently skipped.
|
|
2224
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
|
+
*
|
|
2225
2284
|
* @example
|
|
2226
2285
|
* ```ts
|
|
2227
2286
|
* import { BacktestExecutor } from '@livefolio/sdk';
|
|
@@ -2250,7 +2309,8 @@ declare class BacktestExecutor implements Executor {
|
|
|
2250
2309
|
* when the routed feed does not support the requested optional method.
|
|
2251
2310
|
*
|
|
2252
2311
|
* Distinguish the two cases via the message text: "no feed registered" vs
|
|
2253
|
-
* "does not implement fundamentals"
|
|
2312
|
+
* "does not implement `<method>`" (e.g. "does not implement fundamentals()" or
|
|
2313
|
+
* "does not implement dividends()").
|
|
2254
2314
|
*/
|
|
2255
2315
|
declare class RoutingDataFeedError extends Error {
|
|
2256
2316
|
constructor(message: string);
|
|
@@ -2271,9 +2331,11 @@ type RoutingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], DataFeed>>
|
|
|
2271
2331
|
* - **Function form:** `new RoutingDataFeed((a) => a.kind === 'macro' ? fred : yahoo)`.
|
|
2272
2332
|
* Use when routing depends on more than `kind` (e.g. allowlists).
|
|
2273
2333
|
*
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
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.
|
|
2277
2339
|
*
|
|
2278
2340
|
* @example
|
|
2279
2341
|
* ```ts
|
|
@@ -2292,8 +2354,17 @@ type RoutingDataFeedRouteMap = Readonly<Partial<Record<Asset['kind'], DataFeed>>
|
|
|
2292
2354
|
declare class RoutingDataFeed implements DataFeed {
|
|
2293
2355
|
private readonly route;
|
|
2294
2356
|
constructor(routes: RoutingDataFeedRouteMap | RoutingDataFeedRouteFn);
|
|
2295
|
-
bars(asset: Asset, range: DateRange, freq: Frequency): AsyncGenerator<Bar>;
|
|
2357
|
+
bars(asset: Asset, range: DateRange, freq: Frequency, kind?: 'adjusted' | 'unadjusted'): AsyncGenerator<Bar>;
|
|
2296
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[]>;
|
|
2297
2368
|
private resolve;
|
|
2298
2369
|
}
|
|
2299
2370
|
|
|
@@ -3194,7 +3265,7 @@ declare function evaluateFeatureSpecs(specs: ReadonlyArray<TacticalFeatureSpec>,
|
|
|
3194
3265
|
* independently scaled); `volume` is passed through from the underlying bar.
|
|
3195
3266
|
*
|
|
3196
3267
|
* Non-synthetic assets are proxied transparently to the original `dataFeed`.
|
|
3197
|
-
* `fundamentals` and `
|
|
3268
|
+
* `fundamentals`, `events`, and `dividends` methods, if present, are forwarded unchanged.
|
|
3198
3269
|
*
|
|
3199
3270
|
* Throws at construction time if `synthetics` contains duplicate `id` values.
|
|
3200
3271
|
*
|
|
@@ -4057,4 +4128,4 @@ declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>)
|
|
|
4057
4128
|
*/
|
|
4058
4129
|
declare function positionsByAsset(portfolio: Portfolio): Position[];
|
|
4059
4130
|
|
|
4060
|
-
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 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
|
@@ -909,40 +909,124 @@ async function* runLive(opts) {
|
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
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
|
+
|
|
912
965
|
// src/reference/backtest-executor.ts
|
|
913
966
|
function resolveAsset(order, portfolio) {
|
|
914
967
|
switch (order.kind) {
|
|
915
968
|
case "open":
|
|
916
|
-
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 };
|
|
917
970
|
case "rebalance":
|
|
918
|
-
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
|
+
};
|
|
919
977
|
case "close": {
|
|
920
978
|
const p = portfolio.positions.find((x) => x.id === order.positionId);
|
|
921
979
|
if (!p) throw new Error(`BacktestExecutor: close target position ${order.positionId} not found`);
|
|
922
|
-
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
|
+
};
|
|
923
986
|
}
|
|
924
987
|
case "adjust": {
|
|
925
988
|
const p = portfolio.positions.find((x) => x.id === order.positionId);
|
|
926
989
|
if (!p) throw new Error(`BacktestExecutor: adjust target position ${order.positionId} not found`);
|
|
927
990
|
const target = order.changes.quantity ?? p.quantity;
|
|
928
991
|
const delta = target - p.quantity;
|
|
929
|
-
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 };
|
|
930
993
|
}
|
|
931
994
|
}
|
|
932
995
|
}
|
|
933
996
|
var BacktestExecutor = class {
|
|
934
997
|
constructor(opts) {
|
|
935
998
|
this.opts = opts;
|
|
999
|
+
if (opts.lotMethod === "min-tax" && !opts.taxRates) {
|
|
1000
|
+
throw new Error("BacktestExecutor: lotMethod 'min-tax' requires taxRates");
|
|
1001
|
+
}
|
|
936
1002
|
}
|
|
937
1003
|
async submit(orders, t, portfolio) {
|
|
938
1004
|
const fills = [];
|
|
939
1005
|
const slip = (this.opts.slippageBps ?? 0) / 1e4;
|
|
940
1006
|
const feePer = this.opts.perShareFee ?? 0;
|
|
1007
|
+
const method = this.opts.lotMethod;
|
|
941
1008
|
for (const order of orders) {
|
|
942
|
-
const { asset, sign, qty } = resolveAsset(order, portfolio);
|
|
1009
|
+
const { asset, sign, qty, lotConsumingSell } = resolveAsset(order, portfolio);
|
|
943
1010
|
if (qty === 0) continue;
|
|
944
1011
|
const open = await this.opts.nextOpen(asset, t);
|
|
945
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
|
+
}
|
|
946
1030
|
fills.push({
|
|
947
1031
|
orderRef: order.id,
|
|
948
1032
|
t: open.t,
|
|
@@ -974,9 +1058,9 @@ var RoutingDataFeed = class {
|
|
|
974
1058
|
// Async generator (rather than plain delegation) so resolve() runs lazily on
|
|
975
1059
|
// the first next() call, surfacing errors via the iterable's normal rejection
|
|
976
1060
|
// path instead of throwing synchronously at call time.
|
|
977
|
-
async *bars(asset, range, freq) {
|
|
1061
|
+
async *bars(asset, range, freq, kind) {
|
|
978
1062
|
const feed = this.resolve(asset);
|
|
979
|
-
yield* feed.bars(asset, range, freq);
|
|
1063
|
+
yield* feed.bars(asset, range, freq, kind);
|
|
980
1064
|
}
|
|
981
1065
|
async fundamentals(asset, t) {
|
|
982
1066
|
const feed = this.resolve(asset);
|
|
@@ -987,6 +1071,23 @@ var RoutingDataFeed = class {
|
|
|
987
1071
|
}
|
|
988
1072
|
return feed.fundamentals(asset, t);
|
|
989
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
|
+
}
|
|
990
1091
|
resolve(asset) {
|
|
991
1092
|
const feed = this.route(asset);
|
|
992
1093
|
if (feed === void 0) {
|
|
@@ -2880,11 +2981,13 @@ function withSynthetics(dataFeed, synthetics) {
|
|
|
2880
2981
|
byId.set(s.id, s);
|
|
2881
2982
|
}
|
|
2882
2983
|
const wrapped = {
|
|
2883
|
-
|
|
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) {
|
|
2884
2987
|
const synth = byId.get(asset.id);
|
|
2885
|
-
if (!synth) return dataFeed.bars(asset, range, freq);
|
|
2988
|
+
if (!synth) return dataFeed.bars(asset, range, freq, kind);
|
|
2886
2989
|
const underlying = resolveAssetRef(synth.underlying);
|
|
2887
|
-
return synthesize(dataFeed.bars(underlying, range, freq), synth.leverage, synth.expense);
|
|
2990
|
+
return synthesize(dataFeed.bars(underlying, range, freq, kind), synth.leverage, synth.expense);
|
|
2888
2991
|
}
|
|
2889
2992
|
};
|
|
2890
2993
|
if (dataFeed.fundamentals) {
|
|
@@ -2893,6 +2996,9 @@ function withSynthetics(dataFeed, synthetics) {
|
|
|
2893
2996
|
if (dataFeed.events) {
|
|
2894
2997
|
wrapped.events = dataFeed.events.bind(dataFeed);
|
|
2895
2998
|
}
|
|
2999
|
+
if (dataFeed.dividends) {
|
|
3000
|
+
wrapped.dividends = dataFeed.dividends.bind(dataFeed);
|
|
3001
|
+
}
|
|
2896
3002
|
return wrapped;
|
|
2897
3003
|
}
|
|
2898
3004
|
function withStreamingSynthetics(inner, synthetics, opts) {
|
|
@@ -3127,59 +3233,6 @@ __export(tax_exports, {
|
|
|
3127
3233
|
selectMinTax: () => selectMinTax
|
|
3128
3234
|
});
|
|
3129
3235
|
|
|
3130
|
-
// src/tax/lot-selection.ts
|
|
3131
|
-
function take(sorted, qty) {
|
|
3132
|
-
let need = qty;
|
|
3133
|
-
const out = [];
|
|
3134
|
-
for (const lot of sorted) {
|
|
3135
|
-
if (lot.quantity <= 0) continue;
|
|
3136
|
-
if (need <= 0) break;
|
|
3137
|
-
const q = Math.min(lot.quantity, need);
|
|
3138
|
-
out.push({ lotId: lot.id, quantity: q });
|
|
3139
|
-
need -= q;
|
|
3140
|
-
}
|
|
3141
|
-
if (need > 1e-9) {
|
|
3142
|
-
const held = sorted.reduce((s, l) => s + Math.max(0, l.quantity), 0);
|
|
3143
|
-
throw new RangeError(`lot-selection: need ${qty} but only ${held} held`);
|
|
3144
|
-
}
|
|
3145
|
-
return out;
|
|
3146
|
-
}
|
|
3147
|
-
function selectFIFO(lots, qty) {
|
|
3148
|
-
return take(
|
|
3149
|
-
[...lots].sort((a, b) => a.openDate.getTime() - b.openDate.getTime()),
|
|
3150
|
-
qty
|
|
3151
|
-
);
|
|
3152
|
-
}
|
|
3153
|
-
function selectLIFO(lots, qty) {
|
|
3154
|
-
return take(
|
|
3155
|
-
[...lots].sort((a, b) => b.openDate.getTime() - a.openDate.getTime()),
|
|
3156
|
-
qty
|
|
3157
|
-
);
|
|
3158
|
-
}
|
|
3159
|
-
function selectHIFO(lots, qty) {
|
|
3160
|
-
return take(
|
|
3161
|
-
[...lots].filter((l) => l.quantity > 0).sort((a, b) => b.basis / b.quantity - a.basis / a.quantity),
|
|
3162
|
-
qty
|
|
3163
|
-
);
|
|
3164
|
-
}
|
|
3165
|
-
function selectMinTax(lots, qty, ctx) {
|
|
3166
|
-
const tier = (l) => {
|
|
3167
|
-
const gainPerShare = ctx.price - l.basis / l.quantity;
|
|
3168
|
-
const lt = isLongTerm(holdingPeriodDays(l, ctx.asOf));
|
|
3169
|
-
if (gainPerShare < 0) return lt ? 0 : 1;
|
|
3170
|
-
return lt ? 2 : 3;
|
|
3171
|
-
};
|
|
3172
|
-
const sorted = [...lots].filter((l) => l.quantity > 0).sort((a, b) => {
|
|
3173
|
-
const ta = tier(a);
|
|
3174
|
-
const tb = tier(b);
|
|
3175
|
-
if (ta !== tb) return ta - tb;
|
|
3176
|
-
const gainA = ctx.price - a.basis / a.quantity;
|
|
3177
|
-
const gainB = ctx.price - b.basis / b.quantity;
|
|
3178
|
-
return gainA - gainB;
|
|
3179
|
-
});
|
|
3180
|
-
return take(sorted, qty);
|
|
3181
|
-
}
|
|
3182
|
-
|
|
3183
3236
|
// src/tax/aggregation.ts
|
|
3184
3237
|
var ORDINARY_OFFSET_CAP = 3e3;
|
|
3185
3238
|
function bucketByTerm(events) {
|