@livefolio/sdk 0.5.0-rc.3 → 0.5.0-rc.5

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.js CHANGED
@@ -313,10 +313,80 @@ function applyOrders(portfolio, orders) {
313
313
  return { ...portfolio, positions };
314
314
  }
315
315
 
316
+ // src/tax/dividends.ts
317
+ var MS_PER_DAY2 = 864e5;
318
+ function isQualifiedForLot(lot, exDate, opts = {}) {
319
+ const required = opts.holdingDaysRequired ?? 60;
320
+ const window = opts.windowDays ?? 121;
321
+ const half = Math.floor(window / 2);
322
+ const windowStart = new Date(exDate.getTime() - half * MS_PER_DAY2);
323
+ const windowEnd = new Date(exDate.getTime() + half * MS_PER_DAY2);
324
+ const heldFrom = lot.openDate > windowStart ? lot.openDate : windowStart;
325
+ const heldTo = exDate < windowEnd ? exDate : windowEnd;
326
+ const days = Math.max(0, (heldTo.getTime() - heldFrom.getTime()) / MS_PER_DAY2);
327
+ return days >= required;
328
+ }
329
+ function distributeDividend(event, lotsHeldAtExDate) {
330
+ const eligible = event.incomeKind === "qualified-eligible";
331
+ const perLot = [];
332
+ let qualified = 0, ordinary = 0;
333
+ for (const lot of lotsHeldAtExDate) {
334
+ if (lot.quantity <= 0 || lot.openDate > event.exDate || lot.asset.id !== event.asset.id) continue;
335
+ const cash = lot.quantity * event.amountPerShare;
336
+ const isQ = eligible && isQualifiedForLot(lot, event.exDate);
337
+ perLot.push({ lotId: lot.id, cash, qualified: isQ });
338
+ if (isQ) qualified += cash;
339
+ else ordinary += cash;
340
+ }
341
+ return { totals: { qualified, ordinary }, perLot };
342
+ }
343
+ function reinvestDividend(cashAvailable, asset, pricePayDate, payDate, dripParent) {
344
+ const shares = Math.floor(cashAvailable / pricePayDate);
345
+ const cost = shares * pricePayDate;
346
+ return {
347
+ newLot: {
348
+ id: nextLotId(),
349
+ asset,
350
+ quantity: shares,
351
+ openDate: payDate,
352
+ openPrice: pricePayDate,
353
+ basis: cost,
354
+ dripParent
355
+ },
356
+ residual: cashAvailable - cost
357
+ };
358
+ }
359
+
360
+ // src/tax/cash-interest.ts
361
+ function accrueCashInterest(cash, dailyRate) {
362
+ const interest = cash * dailyRate;
363
+ return { newCash: cash + interest, interest };
364
+ }
365
+
316
366
  // src/strategy/run-backtest.ts
367
+ var CASH_ASSET = { kind: "equity", id: "_cash", symbol: "CASH" };
317
368
  function isStateResult(r) {
318
369
  return !Array.isArray(r);
319
370
  }
371
+ var DRIP_PRICE_WINDOW_MS = 7 * 864e5;
372
+ async function firstUnadjustedClose(feed, asset, payDate, freq) {
373
+ const to = new Date(payDate.getTime() + DRIP_PRICE_WINDOW_MS);
374
+ for await (const bar of feed.bars(asset, { from: payDate, to }, freq, "unadjusted")) return bar.close;
375
+ return void 0;
376
+ }
377
+ async function resolveDailyRate(cfg, t, feed, freq) {
378
+ if (!cfg || cfg.kind === "none") return 0;
379
+ if (cfg.kind === "flat") return cfg.apy / 365;
380
+ const id = cfg.assetId ?? "DGS3MO";
381
+ const asset = { kind: "macro", id, symbol: id, source: "FRED" };
382
+ const to = new Date(t.getTime() + 864e5);
383
+ let last;
384
+ for await (const bar of feed.bars(asset, { from: new Date(t.getTime() - 7 * 864e5), to }, freq, "unadjusted")) {
385
+ last = bar.close;
386
+ }
387
+ if (last === void 0) return 0;
388
+ return Math.max(0, (last / 100 - cfg.spread) / 365);
389
+ }
320
390
  async function runBacktest(opts) {
321
391
  const initialStateValue = opts.strategy.initialState?.();
322
392
  const sessions = opts.calendar.sessions(opts.range);
@@ -334,6 +404,16 @@ async function runBacktest(opts) {
334
404
  const cashEvents = [...opts.cashEvents ?? []].sort((a, b) => a.t.getTime() - b.t.getTime());
335
405
  let eventCursor = 0;
336
406
  let warnedNegativeCash = false;
407
+ let warnedDripFallback = false;
408
+ const divByAsset = /* @__PURE__ */ new Map();
409
+ if (opts.dataFeed.dividends && sessions.length > 0) {
410
+ const u0 = opts.strategy.universe(sessions[0], opts.initialPortfolio);
411
+ for (const asset of u0) {
412
+ divByAsset.set(asset.id, await opts.dataFeed.dividends(asset, opts.range));
413
+ }
414
+ }
415
+ const allDivs = [...divByAsset.values()].flat().sort((a, b) => a.exDate.getTime() - b.exDate.getTime());
416
+ let divCursor = 0;
337
417
  for (const t of sessions) {
338
418
  let cashFlow = 0;
339
419
  while (eventCursor < cashEvents.length && cashEvents[eventCursor].t.getTime() <= t.getTime()) {
@@ -349,6 +429,72 @@ async function runBacktest(opts) {
349
429
  );
350
430
  }
351
431
  }
432
+ let qualifiedTotal = 0;
433
+ let ordinaryTotal = 0;
434
+ while (divCursor < allDivs.length && allDivs[divCursor].exDate.getTime() <= t.getTime()) {
435
+ const div = allDivs[divCursor];
436
+ divCursor++;
437
+ const dist = distributeDividend(div, portfolio.lots ?? []);
438
+ if (dist.perLot.length === 0) continue;
439
+ qualifiedTotal += dist.totals.qualified;
440
+ ordinaryTotal += dist.totals.ordinary;
441
+ const reinvest = opts.dividends?.reinvest === true;
442
+ const lots = [...portfolio.lots ?? []];
443
+ const realized = [...portfolio.realized ?? []];
444
+ let cashCredit = 0;
445
+ const reinvestPrice = reinvest ? await firstUnadjustedClose(opts.dataFeed, div.asset, div.payDate, opts.freq ?? "1d") : void 0;
446
+ for (const slice of dist.perLot) {
447
+ realized.push({
448
+ asset: div.asset,
449
+ lotId: slice.lotId,
450
+ quantity: 0,
451
+ openDate: t,
452
+ closeDate: t,
453
+ proceeds: slice.cash,
454
+ basis: 0,
455
+ termType: "long",
456
+ gain: slice.cash,
457
+ incomeKind: slice.qualified ? "qualified-dividend" : "ordinary-dividend"
458
+ });
459
+ if (reinvest && reinvestPrice && reinvestPrice > 0) {
460
+ const { newLot, residual } = reinvestDividend(slice.cash, div.asset, reinvestPrice, div.payDate, slice.lotId);
461
+ if (newLot.quantity > 0) lots.push(newLot);
462
+ cashCredit += residual;
463
+ } else {
464
+ if (reinvest && !warnedDripFallback) {
465
+ warnedDripFallback = true;
466
+ console.warn(
467
+ `[runBacktest] DRIP fell back to a cash credit for ${div.asset.id} (pay date ${div.payDate.toISOString()}): no unadjusted bar within 7 days of the pay date. Further occurrences this run are suppressed.`
468
+ );
469
+ }
470
+ cashCredit += slice.cash;
471
+ }
472
+ }
473
+ portfolio = { ...portfolio, cash: portfolio.cash + cashCredit, lots, realized };
474
+ }
475
+ const dividendIncome = qualifiedTotal + ordinaryTotal > 0 ? { qualified: qualifiedTotal, ordinary: ordinaryTotal } : void 0;
476
+ let interestThisSession = 0;
477
+ const dailyRate = await resolveDailyRate(opts.cashYield, t, opts.dataFeed, opts.freq ?? "1d");
478
+ if (dailyRate > 0 && portfolio.cash > 0) {
479
+ const { newCash, interest } = accrueCashInterest(portfolio.cash, dailyRate);
480
+ const realized = [
481
+ ...portfolio.realized ?? [],
482
+ {
483
+ asset: CASH_ASSET,
484
+ lotId: "cash",
485
+ quantity: 0,
486
+ openDate: t,
487
+ closeDate: t,
488
+ proceeds: interest,
489
+ basis: 0,
490
+ termType: "short",
491
+ gain: interest,
492
+ incomeKind: "interest"
493
+ }
494
+ ];
495
+ portfolio = { ...portfolio, cash: newCash, realized };
496
+ interestThisSession = interest;
497
+ }
352
498
  const universe = opts.strategy.universe(t, portfolio);
353
499
  const features = await opts.strategy.features(universe, portfolio, t);
354
500
  const buildResult = opts.strategy.build(features, portfolio, state, t);
@@ -361,7 +507,15 @@ async function runBacktest(opts) {
361
507
  }
362
508
  const fills = await opts.executor.submit(orders, t, portfolio);
363
509
  portfolio = applyFills(portfolio, fills, orders);
364
- snapshots.push({ t, portfolio, orders, fills, ...cashFlow !== 0 ? { cashFlow } : {} });
510
+ snapshots.push({
511
+ t,
512
+ portfolio,
513
+ orders,
514
+ fills,
515
+ ...cashFlow !== 0 ? { cashFlow } : {},
516
+ ...dividendIncome ? { dividendIncome } : {},
517
+ ...interestThisSession !== 0 ? { interestIncome: interestThisSession } : {}
518
+ });
365
519
  }
366
520
  const bars = opts.featureRuntime?.getAllBars() ?? /* @__PURE__ */ new Map();
367
521
  return { snapshots, finalPortfolio: portfolio, finalState: state, bars };
@@ -909,40 +1063,124 @@ async function* runLive(opts) {
909
1063
  }
910
1064
  }
911
1065
 
1066
+ // src/tax/lot-selection.ts
1067
+ function take(sorted, qty) {
1068
+ let need = qty;
1069
+ const out = [];
1070
+ for (const lot of sorted) {
1071
+ if (lot.quantity <= 0) continue;
1072
+ if (need <= 0) break;
1073
+ const q = Math.min(lot.quantity, need);
1074
+ out.push({ lotId: lot.id, quantity: q });
1075
+ need -= q;
1076
+ }
1077
+ if (need > 1e-9) {
1078
+ const held = sorted.reduce((s, l) => s + Math.max(0, l.quantity), 0);
1079
+ throw new RangeError(`lot-selection: need ${qty} but only ${held} held`);
1080
+ }
1081
+ return out;
1082
+ }
1083
+ function selectFIFO(lots, qty) {
1084
+ return take(
1085
+ [...lots].sort((a, b) => a.openDate.getTime() - b.openDate.getTime()),
1086
+ qty
1087
+ );
1088
+ }
1089
+ function selectLIFO(lots, qty) {
1090
+ return take(
1091
+ [...lots].sort((a, b) => b.openDate.getTime() - a.openDate.getTime()),
1092
+ qty
1093
+ );
1094
+ }
1095
+ function selectHIFO(lots, qty) {
1096
+ return take(
1097
+ [...lots].filter((l) => l.quantity > 0).sort((a, b) => b.basis / b.quantity - a.basis / a.quantity),
1098
+ qty
1099
+ );
1100
+ }
1101
+ function selectMinTax(lots, qty, ctx) {
1102
+ const tier = (l) => {
1103
+ const gainPerShare = ctx.price - l.basis / l.quantity;
1104
+ const lt = isLongTerm(holdingPeriodDays(l, ctx.asOf));
1105
+ if (gainPerShare < 0) return lt ? 0 : 1;
1106
+ return lt ? 2 : 3;
1107
+ };
1108
+ const sorted = [...lots].filter((l) => l.quantity > 0).sort((a, b) => {
1109
+ const ta = tier(a);
1110
+ const tb = tier(b);
1111
+ if (ta !== tb) return ta - tb;
1112
+ const gainA = ctx.price - a.basis / a.quantity;
1113
+ const gainB = ctx.price - b.basis / b.quantity;
1114
+ return gainA - gainB;
1115
+ });
1116
+ return take(sorted, qty);
1117
+ }
1118
+
912
1119
  // src/reference/backtest-executor.ts
913
1120
  function resolveAsset(order, portfolio) {
914
1121
  switch (order.kind) {
915
1122
  case "open":
916
- return { asset: order.asset, sign: order.side === "long" ? 1 : -1, qty: order.quantity };
1123
+ return { asset: order.asset, sign: order.side === "long" ? 1 : -1, qty: order.quantity, lotConsumingSell: false };
917
1124
  case "rebalance":
918
- return { asset: order.asset, sign: order.delta >= 0 ? 1 : -1, qty: Math.abs(order.delta) };
1125
+ return {
1126
+ asset: order.asset,
1127
+ sign: order.delta >= 0 ? 1 : -1,
1128
+ qty: Math.abs(order.delta),
1129
+ lotConsumingSell: order.delta < 0
1130
+ };
919
1131
  case "close": {
920
1132
  const p = portfolio.positions.find((x) => x.id === order.positionId);
921
1133
  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 };
1134
+ return {
1135
+ asset: p.asset,
1136
+ sign: p.side === "long" ? -1 : 1,
1137
+ qty: order.quantity ?? p.quantity,
1138
+ lotConsumingSell: p.side === "long"
1139
+ };
923
1140
  }
924
1141
  case "adjust": {
925
1142
  const p = portfolio.positions.find((x) => x.id === order.positionId);
926
1143
  if (!p) throw new Error(`BacktestExecutor: adjust target position ${order.positionId} not found`);
927
1144
  const target = order.changes.quantity ?? p.quantity;
928
1145
  const delta = target - p.quantity;
929
- return { asset: p.asset, sign: delta >= 0 ? 1 : -1, qty: Math.abs(delta) };
1146
+ return { asset: p.asset, sign: delta >= 0 ? 1 : -1, qty: Math.abs(delta), lotConsumingSell: false };
930
1147
  }
931
1148
  }
932
1149
  }
933
1150
  var BacktestExecutor = class {
934
1151
  constructor(opts) {
935
1152
  this.opts = opts;
1153
+ if (opts.lotMethod === "min-tax" && !opts.taxRates) {
1154
+ throw new Error("BacktestExecutor: lotMethod 'min-tax' requires taxRates");
1155
+ }
936
1156
  }
937
1157
  async submit(orders, t, portfolio) {
938
1158
  const fills = [];
939
1159
  const slip = (this.opts.slippageBps ?? 0) / 1e4;
940
1160
  const feePer = this.opts.perShareFee ?? 0;
1161
+ const method = this.opts.lotMethod;
941
1162
  for (const order of orders) {
942
- const { asset, sign, qty } = resolveAsset(order, portfolio);
1163
+ const { asset, sign, qty, lotConsumingSell } = resolveAsset(order, portfolio);
943
1164
  if (qty === 0) continue;
944
1165
  const open = await this.opts.nextOpen(asset, t);
945
1166
  const adjustedPrice = open.price * (1 + sign * slip);
1167
+ if (method && method !== "FIFO" && lotConsumingSell) {
1168
+ const lots = (portfolio.lots ?? []).filter((l) => l.asset.id === asset.id && l.quantity > 0);
1169
+ if (lots.length > 0) {
1170
+ const slices = method === "LIFO" ? selectLIFO(lots, qty) : method === "HIFO" ? selectHIFO(lots, qty) : selectMinTax(lots, qty, { price: adjustedPrice, asOf: open.t, rates: this.opts.taxRates });
1171
+ for (const slice of slices) {
1172
+ fills.push({
1173
+ orderRef: order.id,
1174
+ t: open.t,
1175
+ quantity: slice.quantity,
1176
+ price: adjustedPrice,
1177
+ fees: feePer * slice.quantity,
1178
+ lotId: slice.lotId
1179
+ });
1180
+ }
1181
+ continue;
1182
+ }
1183
+ }
946
1184
  fills.push({
947
1185
  orderRef: order.id,
948
1186
  t: open.t,
@@ -974,9 +1212,9 @@ var RoutingDataFeed = class {
974
1212
  // Async generator (rather than plain delegation) so resolve() runs lazily on
975
1213
  // the first next() call, surfacing errors via the iterable's normal rejection
976
1214
  // path instead of throwing synchronously at call time.
977
- async *bars(asset, range, freq) {
1215
+ async *bars(asset, range, freq, kind) {
978
1216
  const feed = this.resolve(asset);
979
- yield* feed.bars(asset, range, freq);
1217
+ yield* feed.bars(asset, range, freq, kind);
980
1218
  }
981
1219
  async fundamentals(asset, t) {
982
1220
  const feed = this.resolve(asset);
@@ -987,6 +1225,23 @@ var RoutingDataFeed = class {
987
1225
  }
988
1226
  return feed.fundamentals(asset, t);
989
1227
  }
1228
+ /**
1229
+ * Resolves `asset` to its routed feed and delegates `dividends`. This method
1230
+ * is ALWAYS present on the router (unlike a leaf feed's optional `dividends?`),
1231
+ * so `typeof routingFeed.dividends === 'function'` is always `true` — capability
1232
+ * detection must account for the routed feed possibly lacking it at call time.
1233
+ *
1234
+ * @throws {RoutingDataFeedError} when the routed feed does not implement `dividends`.
1235
+ */
1236
+ async dividends(asset, range) {
1237
+ const feed = this.resolve(asset);
1238
+ if (typeof feed.dividends !== "function") {
1239
+ throw new RoutingDataFeedError(
1240
+ `RoutingDataFeed: routed feed for asset.kind="${asset.kind}" (id="${asset.id}") does not implement dividends()`
1241
+ );
1242
+ }
1243
+ return feed.dividends(asset, range);
1244
+ }
990
1245
  resolve(asset) {
991
1246
  const feed = this.route(asset);
992
1247
  if (feed === void 0) {
@@ -1179,7 +1434,7 @@ function pollingStreamFromHistorical(opts) {
1179
1434
  import { DateTime } from "luxon";
1180
1435
 
1181
1436
  // src/calendars/holiday-rules.ts
1182
- var MS_PER_DAY2 = 864e5;
1437
+ var MS_PER_DAY3 = 864e5;
1183
1438
  function nthWeekdayOfMonth(year, month, weekday, n) {
1184
1439
  const first = new Date(Date.UTC(year, month - 1, 1));
1185
1440
  const offset = (weekday - first.getUTCDay() + 7) % 7;
@@ -1188,7 +1443,7 @@ function nthWeekdayOfMonth(year, month, weekday, n) {
1188
1443
  function lastWeekdayOfMonth(year, month, weekday) {
1189
1444
  const last = new Date(Date.UTC(year, month, 0));
1190
1445
  const offset = (last.getUTCDay() - weekday + 7) % 7;
1191
- return new Date(last.getTime() - offset * MS_PER_DAY2);
1446
+ return new Date(last.getTime() - offset * MS_PER_DAY3);
1192
1447
  }
1193
1448
  function easter(year) {
1194
1449
  const a = year % 19;
@@ -1209,8 +1464,8 @@ function easter(year) {
1209
1464
  }
1210
1465
  function observed(d) {
1211
1466
  const dow = d.getUTCDay();
1212
- if (dow === 6) return new Date(d.getTime() - MS_PER_DAY2);
1213
- if (dow === 0) return new Date(d.getTime() + MS_PER_DAY2);
1467
+ if (dow === 6) return new Date(d.getTime() - MS_PER_DAY3);
1468
+ if (dow === 0) return new Date(d.getTime() + MS_PER_DAY3);
1214
1469
  return d;
1215
1470
  }
1216
1471
  function resolveHolidays(rules, year) {
@@ -1248,21 +1503,21 @@ function resolveSpecialOpens(rules, year) {
1248
1503
  return out;
1249
1504
  }
1250
1505
  function sundayToMonday(d) {
1251
- return d.getUTCDay() === 0 ? new Date(d.getTime() + MS_PER_DAY2) : d;
1506
+ return d.getUTCDay() === 0 ? new Date(d.getTime() + MS_PER_DAY3) : d;
1252
1507
  }
1253
1508
  function nearestWorkday(d) {
1254
1509
  const dow = d.getUTCDay();
1255
- if (dow === 6) return new Date(d.getTime() - MS_PER_DAY2);
1256
- if (dow === 0) return new Date(d.getTime() + MS_PER_DAY2);
1510
+ if (dow === 6) return new Date(d.getTime() - MS_PER_DAY3);
1511
+ if (dow === 0) return new Date(d.getTime() + MS_PER_DAY3);
1257
1512
  return d;
1258
1513
  }
1259
1514
  function firstMondayOnOrAfter(year, month, day, nth = 1) {
1260
1515
  const start = new Date(Date.UTC(year, month - 1, day));
1261
1516
  const offset = (1 - start.getUTCDay() + 7) % 7;
1262
- return new Date(start.getTime() + (offset + 7 * (nth - 1)) * MS_PER_DAY2);
1517
+ return new Date(start.getTime() + (offset + 7 * (nth - 1)) * MS_PER_DAY3);
1263
1518
  }
1264
1519
  function easterPlus(year, dayDelta) {
1265
- return new Date(easter(year).getTime() + dayDelta * MS_PER_DAY2);
1520
+ return new Date(easter(year).getTime() + dayDelta * MS_PER_DAY3);
1266
1521
  }
1267
1522
  function dropIfNotInDays(d, allowed) {
1268
1523
  if (d === null) return null;
@@ -1270,7 +1525,7 @@ function dropIfNotInDays(d, allowed) {
1270
1525
  }
1271
1526
 
1272
1527
  // src/calendars/exchange-calendar.ts
1273
- var MS_PER_DAY3 = 864e5;
1528
+ var MS_PER_DAY4 = 864e5;
1274
1529
  var DEFAULT_WEEKMASK = /* @__PURE__ */ new Set([1, 2, 3, 4, 5]);
1275
1530
  var EMPTY_ADHOC = /* @__PURE__ */ new Map();
1276
1531
  function ymdKey(d) {
@@ -1431,14 +1686,14 @@ var ExchangeCalendar = class {
1431
1686
  }
1432
1687
  /** Returns the first trading day strictly after `t`. */
1433
1688
  next(t) {
1434
- let d = new Date(this.normalize(t).getTime() + MS_PER_DAY3);
1435
- while (!this.isOpen(d)) d = new Date(d.getTime() + MS_PER_DAY3);
1689
+ let d = new Date(this.normalize(t).getTime() + MS_PER_DAY4);
1690
+ while (!this.isOpen(d)) d = new Date(d.getTime() + MS_PER_DAY4);
1436
1691
  return d;
1437
1692
  }
1438
1693
  /** Returns the first trading day strictly before `t`. */
1439
1694
  previous(t) {
1440
- let d = new Date(this.normalize(t).getTime() - MS_PER_DAY3);
1441
- while (!this.isOpen(d)) d = new Date(d.getTime() - MS_PER_DAY3);
1695
+ let d = new Date(this.normalize(t).getTime() - MS_PER_DAY4);
1696
+ while (!this.isOpen(d)) d = new Date(d.getTime() - MS_PER_DAY4);
1442
1697
  return d;
1443
1698
  }
1444
1699
  /**
@@ -1451,7 +1706,7 @@ var ExchangeCalendar = class {
1451
1706
  const end = this.normalize(range.to).getTime();
1452
1707
  while (d.getTime() < end) {
1453
1708
  if (this.isOpen(d)) out.push(d);
1454
- d = new Date(d.getTime() + MS_PER_DAY3);
1709
+ d = new Date(d.getTime() + MS_PER_DAY4);
1455
1710
  }
1456
1711
  return out;
1457
1712
  }
@@ -1502,7 +1757,7 @@ var ExchangeCalendar = class {
1502
1757
  };
1503
1758
 
1504
1759
  // src/calendars/nyse.ts
1505
- var MS_PER_DAY4 = 864e5;
1760
+ var MS_PER_DAY5 = 864e5;
1506
1761
  var SUN = 0;
1507
1762
  var MON = 1;
1508
1763
  var TUE = 2;
@@ -1666,7 +1921,7 @@ var REGULAR_HOLIDAYS = [
1666
1921
  resolve: (y) => {
1667
1922
  const start = utcDate(y, 11, 2);
1668
1923
  const offset = (TUE - start.getUTCDay() + 7) % 7;
1669
- return new Date(start.getTime() + offset * MS_PER_DAY4);
1924
+ return new Date(start.getTime() + offset * MS_PER_DAY5);
1670
1925
  }
1671
1926
  },
1672
1927
  // ── Veterans/Armistice Day (Nov 11, 1934-1953) ─────────────────────────────
@@ -1697,7 +1952,7 @@ var REGULAR_HOLIDAYS = [
1697
1952
  validUntil: 1941,
1698
1953
  resolve: (y) => {
1699
1954
  const last = lastWeekdayOfMonth(y, 11, THU);
1700
- return new Date(last.getTime() - 7 * MS_PER_DAY4);
1955
+ return new Date(last.getTime() - 7 * MS_PER_DAY5);
1701
1956
  }
1702
1957
  },
1703
1958
  // ── Christmas ──────────────────────────────────────────────────────────────
@@ -2054,7 +2309,7 @@ function* generateSummerSaturdays() {
2054
2309
  const end = /* @__PURE__ */ new Date(`${to}T00:00:00.000Z`);
2055
2310
  while (d.getTime() <= end.getTime()) {
2056
2311
  if (d.getUTCDay() === SAT) yield ymd(d);
2057
- d = new Date(d.getTime() + MS_PER_DAY4);
2312
+ d = new Date(d.getTime() + MS_PER_DAY5);
2058
2313
  }
2059
2314
  }
2060
2315
  }
@@ -2064,7 +2319,7 @@ function* generateWWIShutdown() {
2064
2319
  while (d.getTime() <= end.getTime()) {
2065
2320
  const dow = d.getUTCDay();
2066
2321
  if (dow !== SUN) yield ymd(d);
2067
- d = new Date(d.getTime() + MS_PER_DAY4);
2322
+ d = new Date(d.getTime() + MS_PER_DAY5);
2068
2323
  }
2069
2324
  }
2070
2325
  var ADHOC_HOLIDAYS = /* @__PURE__ */ new Set([
@@ -2082,7 +2337,7 @@ var SPECIAL_CLOSES = [
2082
2337
  closeAt: { h: 13, m: 0 },
2083
2338
  resolve: (y) => {
2084
2339
  const t = nthWeekdayOfMonth(y, 11, THU, 4);
2085
- return new Date(t.getTime() + MS_PER_DAY4);
2340
+ return new Date(t.getTime() + MS_PER_DAY5);
2086
2341
  }
2087
2342
  },
2088
2343
  {
@@ -2092,7 +2347,7 @@ var SPECIAL_CLOSES = [
2092
2347
  closeAt: { h: 14, m: 0 },
2093
2348
  resolve: (y) => {
2094
2349
  const t = nthWeekdayOfMonth(y, 11, THU, 4);
2095
- return new Date(t.getTime() + MS_PER_DAY4);
2350
+ return new Date(t.getTime() + MS_PER_DAY5);
2096
2351
  }
2097
2352
  },
2098
2353
  {
@@ -2253,7 +2508,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2253
2508
  if (dow >= MON && dow <= FRI) {
2254
2509
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2255
2510
  }
2256
- d = new Date(d.getTime() + MS_PER_DAY4);
2511
+ d = new Date(d.getTime() + MS_PER_DAY5);
2257
2512
  }
2258
2513
  })();
2259
2514
  (() => {
@@ -2264,7 +2519,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2264
2519
  if (dow >= MON && dow <= FRI) {
2265
2520
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2266
2521
  }
2267
- d = new Date(d.getTime() + MS_PER_DAY4);
2522
+ d = new Date(d.getTime() + MS_PER_DAY5);
2268
2523
  }
2269
2524
  })();
2270
2525
  (() => {
@@ -2275,7 +2530,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2275
2530
  if (dow >= MON && dow <= FRI) {
2276
2531
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2277
2532
  }
2278
- d = new Date(d.getTime() + MS_PER_DAY4);
2533
+ d = new Date(d.getTime() + MS_PER_DAY5);
2279
2534
  }
2280
2535
  })();
2281
2536
  (() => {
@@ -2286,7 +2541,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2286
2541
  if (dow >= MON && dow <= FRI) {
2287
2542
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2288
2543
  }
2289
- d = new Date(d.getTime() + MS_PER_DAY4);
2544
+ d = new Date(d.getTime() + MS_PER_DAY5);
2290
2545
  }
2291
2546
  })();
2292
2547
  (() => {
@@ -2297,7 +2552,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2297
2552
  if (dow >= MON && dow <= FRI) {
2298
2553
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 30 });
2299
2554
  }
2300
- d = new Date(d.getTime() + MS_PER_DAY4);
2555
+ d = new Date(d.getTime() + MS_PER_DAY5);
2301
2556
  }
2302
2557
  })();
2303
2558
  (() => {
@@ -2308,7 +2563,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2308
2563
  if (dow >= MON && dow <= FRI) {
2309
2564
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 15, m: 0 });
2310
2565
  }
2311
- d = new Date(d.getTime() + MS_PER_DAY4);
2566
+ d = new Date(d.getTime() + MS_PER_DAY5);
2312
2567
  }
2313
2568
  })();
2314
2569
  var SPECIAL_OPENS = [];
@@ -2425,7 +2680,7 @@ var NYSEExchangeCalendar = class extends ExchangeCalendar {
2425
2680
  };
2426
2681
 
2427
2682
  // src/calendars/lse.ts
2428
- var MS_PER_DAY5 = 864e5;
2683
+ var MS_PER_DAY6 = 864e5;
2429
2684
  var SUN2 = 0;
2430
2685
  var MON2 = 1;
2431
2686
  var TUE2 = 2;
@@ -2437,14 +2692,14 @@ var WEEKDAYS_MON_FRI2 = /* @__PURE__ */ new Set([1, 2, 3, 4, 5]);
2437
2692
  var MON_TUE = /* @__PURE__ */ new Set([MON2, TUE2]);
2438
2693
  function weekendToMonday(d) {
2439
2694
  const dow = d.getUTCDay();
2440
- if (dow === SAT2) return new Date(d.getTime() + 2 * MS_PER_DAY5);
2441
- if (dow === SUN2) return new Date(d.getTime() + MS_PER_DAY5);
2695
+ if (dow === SAT2) return new Date(d.getTime() + 2 * MS_PER_DAY6);
2696
+ if (dow === SUN2) return new Date(d.getTime() + MS_PER_DAY6);
2442
2697
  return d;
2443
2698
  }
2444
2699
  function previousFriday(d) {
2445
2700
  const dow = d.getUTCDay();
2446
- if (dow === SAT2) return new Date(d.getTime() - MS_PER_DAY5);
2447
- if (dow === SUN2) return new Date(d.getTime() - 2 * MS_PER_DAY5);
2701
+ if (dow === SAT2) return new Date(d.getTime() - MS_PER_DAY6);
2702
+ if (dow === SUN2) return new Date(d.getTime() - 2 * MS_PER_DAY6);
2448
2703
  return d;
2449
2704
  }
2450
2705
  var REGULAR_HOLIDAYS2 = [
@@ -2636,7 +2891,7 @@ function getCalendar(name) {
2636
2891
  }
2637
2892
 
2638
2893
  // src/calendars/crypto-24x7.ts
2639
- var MS_PER_DAY6 = 864e5;
2894
+ var MS_PER_DAY7 = 864e5;
2640
2895
  function midnightUtc(d) {
2641
2896
  return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
2642
2897
  }
@@ -2645,10 +2900,10 @@ var Crypto24x7Calendar = class {
2645
2900
  return true;
2646
2901
  }
2647
2902
  next(t) {
2648
- return new Date(midnightUtc(t).getTime() + MS_PER_DAY6);
2903
+ return new Date(midnightUtc(t).getTime() + MS_PER_DAY7);
2649
2904
  }
2650
2905
  previous(t) {
2651
- return new Date(midnightUtc(t).getTime() - MS_PER_DAY6);
2906
+ return new Date(midnightUtc(t).getTime() - MS_PER_DAY7);
2652
2907
  }
2653
2908
  sessions(range) {
2654
2909
  const out = [];
@@ -2656,7 +2911,7 @@ var Crypto24x7Calendar = class {
2656
2911
  const end = range.to.getTime();
2657
2912
  while (cursor.getTime() < end) {
2658
2913
  out.push(cursor);
2659
- cursor = new Date(cursor.getTime() + MS_PER_DAY6);
2914
+ cursor = new Date(cursor.getTime() + MS_PER_DAY7);
2660
2915
  }
2661
2916
  return out;
2662
2917
  }
@@ -2664,7 +2919,7 @@ var Crypto24x7Calendar = class {
2664
2919
  return this.sessions(range).map((date) => ({
2665
2920
  date,
2666
2921
  open: date,
2667
- close: new Date(date.getTime() + MS_PER_DAY6)
2922
+ close: new Date(date.getTime() + MS_PER_DAY7)
2668
2923
  }));
2669
2924
  }
2670
2925
  isEarlyClose(_t) {
@@ -2880,11 +3135,13 @@ function withSynthetics(dataFeed, synthetics) {
2880
3135
  byId.set(s.id, s);
2881
3136
  }
2882
3137
  const wrapped = {
2883
- bars(asset, range, freq) {
3138
+ // `kind` ('adjusted' | 'unadjusted') is forwarded to the underlying feed
3139
+ // before synthesis so synthetic bars derive from the requested price series.
3140
+ bars(asset, range, freq, kind) {
2884
3141
  const synth = byId.get(asset.id);
2885
- if (!synth) return dataFeed.bars(asset, range, freq);
3142
+ if (!synth) return dataFeed.bars(asset, range, freq, kind);
2886
3143
  const underlying = resolveAssetRef(synth.underlying);
2887
- return synthesize(dataFeed.bars(underlying, range, freq), synth.leverage, synth.expense);
3144
+ return synthesize(dataFeed.bars(underlying, range, freq, kind), synth.leverage, synth.expense);
2888
3145
  }
2889
3146
  };
2890
3147
  if (dataFeed.fundamentals) {
@@ -2893,6 +3150,9 @@ function withSynthetics(dataFeed, synthetics) {
2893
3150
  if (dataFeed.events) {
2894
3151
  wrapped.events = dataFeed.events.bind(dataFeed);
2895
3152
  }
3153
+ if (dataFeed.dividends) {
3154
+ wrapped.dividends = dataFeed.dividends.bind(dataFeed);
3155
+ }
2896
3156
  return wrapped;
2897
3157
  }
2898
3158
  function withStreamingSynthetics(inner, synthetics, opts) {
@@ -3113,73 +3373,24 @@ __export(features_exports, {
3113
3373
  var tax_exports = {};
3114
3374
  __export(tax_exports, {
3115
3375
  ORDINARY_OFFSET_CAP: () => ORDINARY_OFFSET_CAP,
3376
+ accrueCashInterest: () => accrueCashInterest,
3116
3377
  aggregateByYear: () => aggregateByYear,
3117
3378
  bucketByTerm: () => bucketByTerm,
3118
3379
  computeTaxBill: () => computeTaxBill,
3119
3380
  crossOffset: () => crossOffset,
3381
+ distributeDividend: () => distributeDividend,
3120
3382
  holdingPeriodDays: () => holdingPeriodDays,
3121
3383
  isLongTerm: () => isLongTerm,
3384
+ isQualifiedForLot: () => isQualifiedForLot,
3122
3385
  netWithinBucket: () => netWithinBucket,
3123
3386
  realize: () => realize,
3387
+ reinvestDividend: () => reinvestDividend,
3124
3388
  selectFIFO: () => selectFIFO,
3125
3389
  selectHIFO: () => selectHIFO,
3126
3390
  selectLIFO: () => selectLIFO,
3127
3391
  selectMinTax: () => selectMinTax
3128
3392
  });
3129
3393
 
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
3394
  // src/tax/aggregation.ts
3184
3395
  var ORDINARY_OFFSET_CAP = 3e3;
3185
3396
  function bucketByTerm(events) {
@@ -3314,6 +3525,7 @@ export {
3314
3525
  RoutingQuoteFeedError,
3315
3526
  RoutingStreamingDataFeed,
3316
3527
  RoutingStreamingDataFeedError,
3528
+ accrueCashInterest,
3317
3529
  aggregateByYear,
3318
3530
  applyFills,
3319
3531
  applyOrders,
@@ -3323,6 +3535,7 @@ export {
3323
3535
  computeTaxBill,
3324
3536
  crossOffset,
3325
3537
  defineFeature,
3538
+ distributeDividend,
3326
3539
  drawdown,
3327
3540
  ema,
3328
3541
  evaluateFeatureSpecs,
@@ -3333,6 +3546,7 @@ export {
3333
3546
  getFeatureCompute,
3334
3547
  holdingPeriodDays,
3335
3548
  isLongTerm,
3549
+ isQualifiedForLot,
3336
3550
  isRebalanceDay,
3337
3551
  netWithinBucket,
3338
3552
  paramsHash,
@@ -3341,6 +3555,7 @@ export {
3341
3555
  positionsByAsset,
3342
3556
  realize,
3343
3557
  reconcile,
3558
+ reinvestDividend,
3344
3559
  returnSeries,
3345
3560
  rsi,
3346
3561
  runBacktest,