@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 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 {@link nextLotId} on creation. */
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
- * The router does **not** implement `events()` — the optional method is
2275
- * genuinely absent (`'events' in router === false`). Cross-feed event
2276
- * fan-out is deferred until a real consumer materializes.
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 `events` methods, if present, are forwarded unchanged.
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 { asset: order.asset, sign: order.delta >= 0 ? 1 : -1, qty: Math.abs(order.delta) };
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 { asset: p.asset, sign: p.side === "long" ? -1 : 1, qty: order.quantity ?? p.quantity };
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
- bars(asset, range, freq) {
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) {