@livefolio/sdk 0.4.4 → 0.5.0-rc.2

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
@@ -48,6 +48,42 @@ function reconcile(targets, portfolio, prices, assets) {
48
48
  return orders;
49
49
  }
50
50
 
51
+ // src/portfolio/ids.ts
52
+ var _lotCounter = 0;
53
+ function nextLotId() {
54
+ return `lot_${++_lotCounter}`;
55
+ }
56
+
57
+ // src/tax/holding-period.ts
58
+ var MS_PER_DAY = 864e5;
59
+ function holdingPeriodDays(lot, asOf) {
60
+ return (asOf.getTime() - lot.openDate.getTime()) / MS_PER_DAY;
61
+ }
62
+ function isLongTerm(days) {
63
+ return days > 365;
64
+ }
65
+ function realize(lot, qty, salePrice, asOf) {
66
+ if (qty <= 0) throw new RangeError(`realize: qty must be positive, got ${qty}`);
67
+ if (qty > lot.quantity) throw new RangeError(`realize: lot ${lot.id} has ${lot.quantity}, cannot sell ${qty}`);
68
+ const basisPerShare = lot.basis / lot.quantity;
69
+ const basis = basisPerShare * qty;
70
+ const proceeds = qty * salePrice;
71
+ const event = {
72
+ asset: lot.asset,
73
+ lotId: lot.id,
74
+ quantity: qty,
75
+ openDate: lot.openDate,
76
+ closeDate: asOf,
77
+ proceeds,
78
+ basis,
79
+ termType: isLongTerm(holdingPeriodDays(lot, asOf)) ? "long" : "short",
80
+ gain: proceeds - basis,
81
+ incomeKind: "capital-gain"
82
+ };
83
+ const remainingLot = qty === lot.quantity ? null : { ...lot, quantity: lot.quantity - qty, basis: lot.basis - basis };
84
+ return { event, remainingLot };
85
+ }
86
+
51
87
  // src/portfolio/apply.ts
52
88
  var newPositionId = /* @__PURE__ */ (() => {
53
89
  let n = 0;
@@ -56,10 +92,49 @@ var newPositionId = /* @__PURE__ */ (() => {
56
92
  function findOrder(orders, id) {
57
93
  return orders.find((o) => o.id === id);
58
94
  }
95
+ function consumeLots(lots, realized, assetId, qty, price, fees, closeDate, preferLotId) {
96
+ const sortedLots = lots.filter((l) => l.asset.id === assetId && l.quantity > 0).sort((a, b) => {
97
+ if (preferLotId) {
98
+ if (a.id === preferLotId) return -1;
99
+ if (b.id === preferLotId) return 1;
100
+ }
101
+ return a.openDate.getTime() - b.openDate.getTime();
102
+ });
103
+ const held = sortedLots.reduce((s, x) => s + x.quantity, 0);
104
+ if (held < qty) {
105
+ throw new RangeError(`applyFills: cannot sell ${qty} of ${assetId} \u2014 lot ledger holds ${held}`);
106
+ }
107
+ let need = qty;
108
+ const totalQty = qty;
109
+ for (const l of sortedLots) {
110
+ if (need <= 0) break;
111
+ const take2 = Math.min(l.quantity, need);
112
+ const basisPerShare = l.basis / l.quantity;
113
+ const consumedBasis = basisPerShare * take2;
114
+ const proceeds = take2 * price - take2 / totalQty * fees;
115
+ realized.push({
116
+ asset: l.asset,
117
+ lotId: l.id,
118
+ quantity: take2,
119
+ openDate: l.openDate,
120
+ closeDate,
121
+ proceeds,
122
+ basis: consumedBasis,
123
+ termType: isLongTerm(holdingPeriodDays(l, closeDate)) ? "long" : "short",
124
+ gain: proceeds - consumedBasis,
125
+ incomeKind: "capital-gain"
126
+ });
127
+ l.quantity -= take2;
128
+ l.basis -= consumedBasis;
129
+ need -= take2;
130
+ }
131
+ }
59
132
  function applyFills(portfolio, fills, orders) {
60
133
  let positions = [...portfolio.positions];
61
134
  let cash = portfolio.cash;
62
135
  let t = portfolio.t;
136
+ const lots = (portfolio.lots ?? []).map((l) => ({ ...l }));
137
+ const realized = [...portfolio.realized ?? []];
63
138
  for (const fill of fills) {
64
139
  const order = findOrder(orders, fill.orderRef);
65
140
  if (!order) {
@@ -78,6 +153,16 @@ function applyFills(portfolio, fills, orders) {
78
153
  };
79
154
  positions.push(pos);
80
155
  cash -= fill.quantity * fill.price + fill.fees;
156
+ if (order.side === "long") {
157
+ lots.push({
158
+ id: nextLotId(),
159
+ asset: order.asset,
160
+ quantity: fill.quantity,
161
+ openDate: fill.t,
162
+ openPrice: fill.price,
163
+ basis: fill.quantity * fill.price + fill.fees
164
+ });
165
+ }
81
166
  break;
82
167
  }
83
168
  case "close": {
@@ -92,6 +177,9 @@ function applyFills(portfolio, fills, orders) {
92
177
  } else {
93
178
  positions[idx] = { ...pos, quantity: remaining };
94
179
  }
180
+ if (pos.side === "long") {
181
+ consumeLots(lots, realized, pos.asset.id, fill.quantity, fill.price, fill.fees, fill.t, fill.lotId);
182
+ }
95
183
  break;
96
184
  }
97
185
  case "adjust": {
@@ -125,6 +213,14 @@ function applyFills(portfolio, fills, orders) {
125
213
  basis: prev.basis + cost
126
214
  };
127
215
  }
216
+ lots.push({
217
+ id: nextLotId(),
218
+ asset: order.asset,
219
+ quantity: fill.quantity,
220
+ openDate: fill.t,
221
+ openPrice: fill.price,
222
+ basis: cost
223
+ });
128
224
  } else if (idx >= 0) {
129
225
  const prev = positions[idx];
130
226
  cash += fill.quantity * fill.price - fill.fees;
@@ -139,12 +235,16 @@ function applyFills(portfolio, fills, orders) {
139
235
  basis: basisPerShare * remaining
140
236
  };
141
237
  }
238
+ consumeLots(lots, realized, order.asset.id, fill.quantity, fill.price, fill.fees, fill.t, fill.lotId);
142
239
  }
143
240
  break;
144
241
  }
145
242
  }
243
+ for (let i = lots.length - 1; i >= 0; i--) {
244
+ if (lots[i].quantity <= 1e-9) lots.splice(i, 1);
245
+ }
146
246
  }
147
- return { cash, positions, t };
247
+ return { cash, positions, lots, realized, t };
148
248
  }
149
249
  function applyOrders(portfolio, orders) {
150
250
  let positions = [...portfolio.positions];
@@ -1037,7 +1137,7 @@ function pollingStreamFromHistorical(opts) {
1037
1137
  import { DateTime } from "luxon";
1038
1138
 
1039
1139
  // src/calendars/holiday-rules.ts
1040
- var MS_PER_DAY = 864e5;
1140
+ var MS_PER_DAY2 = 864e5;
1041
1141
  function nthWeekdayOfMonth(year, month, weekday, n) {
1042
1142
  const first = new Date(Date.UTC(year, month - 1, 1));
1043
1143
  const offset = (weekday - first.getUTCDay() + 7) % 7;
@@ -1046,7 +1146,7 @@ function nthWeekdayOfMonth(year, month, weekday, n) {
1046
1146
  function lastWeekdayOfMonth(year, month, weekday) {
1047
1147
  const last = new Date(Date.UTC(year, month, 0));
1048
1148
  const offset = (last.getUTCDay() - weekday + 7) % 7;
1049
- return new Date(last.getTime() - offset * MS_PER_DAY);
1149
+ return new Date(last.getTime() - offset * MS_PER_DAY2);
1050
1150
  }
1051
1151
  function easter(year) {
1052
1152
  const a = year % 19;
@@ -1067,8 +1167,8 @@ function easter(year) {
1067
1167
  }
1068
1168
  function observed(d) {
1069
1169
  const dow = d.getUTCDay();
1070
- if (dow === 6) return new Date(d.getTime() - MS_PER_DAY);
1071
- if (dow === 0) return new Date(d.getTime() + MS_PER_DAY);
1170
+ if (dow === 6) return new Date(d.getTime() - MS_PER_DAY2);
1171
+ if (dow === 0) return new Date(d.getTime() + MS_PER_DAY2);
1072
1172
  return d;
1073
1173
  }
1074
1174
  function resolveHolidays(rules, year) {
@@ -1106,21 +1206,21 @@ function resolveSpecialOpens(rules, year) {
1106
1206
  return out;
1107
1207
  }
1108
1208
  function sundayToMonday(d) {
1109
- return d.getUTCDay() === 0 ? new Date(d.getTime() + MS_PER_DAY) : d;
1209
+ return d.getUTCDay() === 0 ? new Date(d.getTime() + MS_PER_DAY2) : d;
1110
1210
  }
1111
1211
  function nearestWorkday(d) {
1112
1212
  const dow = d.getUTCDay();
1113
- if (dow === 6) return new Date(d.getTime() - MS_PER_DAY);
1114
- if (dow === 0) return new Date(d.getTime() + MS_PER_DAY);
1213
+ if (dow === 6) return new Date(d.getTime() - MS_PER_DAY2);
1214
+ if (dow === 0) return new Date(d.getTime() + MS_PER_DAY2);
1115
1215
  return d;
1116
1216
  }
1117
1217
  function firstMondayOnOrAfter(year, month, day, nth = 1) {
1118
1218
  const start = new Date(Date.UTC(year, month - 1, day));
1119
1219
  const offset = (1 - start.getUTCDay() + 7) % 7;
1120
- return new Date(start.getTime() + (offset + 7 * (nth - 1)) * MS_PER_DAY);
1220
+ return new Date(start.getTime() + (offset + 7 * (nth - 1)) * MS_PER_DAY2);
1121
1221
  }
1122
1222
  function easterPlus(year, dayDelta) {
1123
- return new Date(easter(year).getTime() + dayDelta * MS_PER_DAY);
1223
+ return new Date(easter(year).getTime() + dayDelta * MS_PER_DAY2);
1124
1224
  }
1125
1225
  function dropIfNotInDays(d, allowed) {
1126
1226
  if (d === null) return null;
@@ -1128,7 +1228,7 @@ function dropIfNotInDays(d, allowed) {
1128
1228
  }
1129
1229
 
1130
1230
  // src/calendars/exchange-calendar.ts
1131
- var MS_PER_DAY2 = 864e5;
1231
+ var MS_PER_DAY3 = 864e5;
1132
1232
  var DEFAULT_WEEKMASK = /* @__PURE__ */ new Set([1, 2, 3, 4, 5]);
1133
1233
  var EMPTY_ADHOC = /* @__PURE__ */ new Map();
1134
1234
  function ymdKey(d) {
@@ -1289,14 +1389,14 @@ var ExchangeCalendar = class {
1289
1389
  }
1290
1390
  /** Returns the first trading day strictly after `t`. */
1291
1391
  next(t) {
1292
- let d = new Date(this.normalize(t).getTime() + MS_PER_DAY2);
1293
- while (!this.isOpen(d)) d = new Date(d.getTime() + MS_PER_DAY2);
1392
+ let d = new Date(this.normalize(t).getTime() + MS_PER_DAY3);
1393
+ while (!this.isOpen(d)) d = new Date(d.getTime() + MS_PER_DAY3);
1294
1394
  return d;
1295
1395
  }
1296
1396
  /** Returns the first trading day strictly before `t`. */
1297
1397
  previous(t) {
1298
- let d = new Date(this.normalize(t).getTime() - MS_PER_DAY2);
1299
- while (!this.isOpen(d)) d = new Date(d.getTime() - MS_PER_DAY2);
1398
+ let d = new Date(this.normalize(t).getTime() - MS_PER_DAY3);
1399
+ while (!this.isOpen(d)) d = new Date(d.getTime() - MS_PER_DAY3);
1300
1400
  return d;
1301
1401
  }
1302
1402
  /**
@@ -1309,7 +1409,7 @@ var ExchangeCalendar = class {
1309
1409
  const end = this.normalize(range.to).getTime();
1310
1410
  while (d.getTime() < end) {
1311
1411
  if (this.isOpen(d)) out.push(d);
1312
- d = new Date(d.getTime() + MS_PER_DAY2);
1412
+ d = new Date(d.getTime() + MS_PER_DAY3);
1313
1413
  }
1314
1414
  return out;
1315
1415
  }
@@ -1360,7 +1460,7 @@ var ExchangeCalendar = class {
1360
1460
  };
1361
1461
 
1362
1462
  // src/calendars/nyse.ts
1363
- var MS_PER_DAY3 = 864e5;
1463
+ var MS_PER_DAY4 = 864e5;
1364
1464
  var SUN = 0;
1365
1465
  var MON = 1;
1366
1466
  var TUE = 2;
@@ -1524,7 +1624,7 @@ var REGULAR_HOLIDAYS = [
1524
1624
  resolve: (y) => {
1525
1625
  const start = utcDate(y, 11, 2);
1526
1626
  const offset = (TUE - start.getUTCDay() + 7) % 7;
1527
- return new Date(start.getTime() + offset * MS_PER_DAY3);
1627
+ return new Date(start.getTime() + offset * MS_PER_DAY4);
1528
1628
  }
1529
1629
  },
1530
1630
  // ── Veterans/Armistice Day (Nov 11, 1934-1953) ─────────────────────────────
@@ -1555,7 +1655,7 @@ var REGULAR_HOLIDAYS = [
1555
1655
  validUntil: 1941,
1556
1656
  resolve: (y) => {
1557
1657
  const last = lastWeekdayOfMonth(y, 11, THU);
1558
- return new Date(last.getTime() - 7 * MS_PER_DAY3);
1658
+ return new Date(last.getTime() - 7 * MS_PER_DAY4);
1559
1659
  }
1560
1660
  },
1561
1661
  // ── Christmas ──────────────────────────────────────────────────────────────
@@ -1912,7 +2012,7 @@ function* generateSummerSaturdays() {
1912
2012
  const end = /* @__PURE__ */ new Date(`${to}T00:00:00.000Z`);
1913
2013
  while (d.getTime() <= end.getTime()) {
1914
2014
  if (d.getUTCDay() === SAT) yield ymd(d);
1915
- d = new Date(d.getTime() + MS_PER_DAY3);
2015
+ d = new Date(d.getTime() + MS_PER_DAY4);
1916
2016
  }
1917
2017
  }
1918
2018
  }
@@ -1922,7 +2022,7 @@ function* generateWWIShutdown() {
1922
2022
  while (d.getTime() <= end.getTime()) {
1923
2023
  const dow = d.getUTCDay();
1924
2024
  if (dow !== SUN) yield ymd(d);
1925
- d = new Date(d.getTime() + MS_PER_DAY3);
2025
+ d = new Date(d.getTime() + MS_PER_DAY4);
1926
2026
  }
1927
2027
  }
1928
2028
  var ADHOC_HOLIDAYS = /* @__PURE__ */ new Set([
@@ -1940,7 +2040,7 @@ var SPECIAL_CLOSES = [
1940
2040
  closeAt: { h: 13, m: 0 },
1941
2041
  resolve: (y) => {
1942
2042
  const t = nthWeekdayOfMonth(y, 11, THU, 4);
1943
- return new Date(t.getTime() + MS_PER_DAY3);
2043
+ return new Date(t.getTime() + MS_PER_DAY4);
1944
2044
  }
1945
2045
  },
1946
2046
  {
@@ -1950,7 +2050,7 @@ var SPECIAL_CLOSES = [
1950
2050
  closeAt: { h: 14, m: 0 },
1951
2051
  resolve: (y) => {
1952
2052
  const t = nthWeekdayOfMonth(y, 11, THU, 4);
1953
- return new Date(t.getTime() + MS_PER_DAY3);
2053
+ return new Date(t.getTime() + MS_PER_DAY4);
1954
2054
  }
1955
2055
  },
1956
2056
  {
@@ -2111,7 +2211,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2111
2211
  if (dow >= MON && dow <= FRI) {
2112
2212
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2113
2213
  }
2114
- d = new Date(d.getTime() + MS_PER_DAY3);
2214
+ d = new Date(d.getTime() + MS_PER_DAY4);
2115
2215
  }
2116
2216
  })();
2117
2217
  (() => {
@@ -2122,7 +2222,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2122
2222
  if (dow >= MON && dow <= FRI) {
2123
2223
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2124
2224
  }
2125
- d = new Date(d.getTime() + MS_PER_DAY3);
2225
+ d = new Date(d.getTime() + MS_PER_DAY4);
2126
2226
  }
2127
2227
  })();
2128
2228
  (() => {
@@ -2133,7 +2233,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2133
2233
  if (dow >= MON && dow <= FRI) {
2134
2234
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2135
2235
  }
2136
- d = new Date(d.getTime() + MS_PER_DAY3);
2236
+ d = new Date(d.getTime() + MS_PER_DAY4);
2137
2237
  }
2138
2238
  })();
2139
2239
  (() => {
@@ -2144,7 +2244,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2144
2244
  if (dow >= MON && dow <= FRI) {
2145
2245
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
2146
2246
  }
2147
- d = new Date(d.getTime() + MS_PER_DAY3);
2247
+ d = new Date(d.getTime() + MS_PER_DAY4);
2148
2248
  }
2149
2249
  })();
2150
2250
  (() => {
@@ -2155,7 +2255,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2155
2255
  if (dow >= MON && dow <= FRI) {
2156
2256
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 30 });
2157
2257
  }
2158
- d = new Date(d.getTime() + MS_PER_DAY3);
2258
+ d = new Date(d.getTime() + MS_PER_DAY4);
2159
2259
  }
2160
2260
  })();
2161
2261
  (() => {
@@ -2166,7 +2266,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
2166
2266
  if (dow >= MON && dow <= FRI) {
2167
2267
  SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 15, m: 0 });
2168
2268
  }
2169
- d = new Date(d.getTime() + MS_PER_DAY3);
2269
+ d = new Date(d.getTime() + MS_PER_DAY4);
2170
2270
  }
2171
2271
  })();
2172
2272
  var SPECIAL_OPENS = [];
@@ -2283,7 +2383,7 @@ var NYSEExchangeCalendar = class extends ExchangeCalendar {
2283
2383
  };
2284
2384
 
2285
2385
  // src/calendars/lse.ts
2286
- var MS_PER_DAY4 = 864e5;
2386
+ var MS_PER_DAY5 = 864e5;
2287
2387
  var SUN2 = 0;
2288
2388
  var MON2 = 1;
2289
2389
  var TUE2 = 2;
@@ -2295,14 +2395,14 @@ var WEEKDAYS_MON_FRI2 = /* @__PURE__ */ new Set([1, 2, 3, 4, 5]);
2295
2395
  var MON_TUE = /* @__PURE__ */ new Set([MON2, TUE2]);
2296
2396
  function weekendToMonday(d) {
2297
2397
  const dow = d.getUTCDay();
2298
- if (dow === SAT2) return new Date(d.getTime() + 2 * MS_PER_DAY4);
2299
- if (dow === SUN2) return new Date(d.getTime() + MS_PER_DAY4);
2398
+ if (dow === SAT2) return new Date(d.getTime() + 2 * MS_PER_DAY5);
2399
+ if (dow === SUN2) return new Date(d.getTime() + MS_PER_DAY5);
2300
2400
  return d;
2301
2401
  }
2302
2402
  function previousFriday(d) {
2303
2403
  const dow = d.getUTCDay();
2304
- if (dow === SAT2) return new Date(d.getTime() - MS_PER_DAY4);
2305
- if (dow === SUN2) return new Date(d.getTime() - 2 * MS_PER_DAY4);
2404
+ if (dow === SAT2) return new Date(d.getTime() - MS_PER_DAY5);
2405
+ if (dow === SUN2) return new Date(d.getTime() - 2 * MS_PER_DAY5);
2306
2406
  return d;
2307
2407
  }
2308
2408
  var REGULAR_HOLIDAYS2 = [
@@ -2494,7 +2594,7 @@ function getCalendar(name) {
2494
2594
  }
2495
2595
 
2496
2596
  // src/calendars/crypto-24x7.ts
2497
- var MS_PER_DAY5 = 864e5;
2597
+ var MS_PER_DAY6 = 864e5;
2498
2598
  function midnightUtc(d) {
2499
2599
  return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
2500
2600
  }
@@ -2503,10 +2603,10 @@ var Crypto24x7Calendar = class {
2503
2603
  return true;
2504
2604
  }
2505
2605
  next(t) {
2506
- return new Date(midnightUtc(t).getTime() + MS_PER_DAY5);
2606
+ return new Date(midnightUtc(t).getTime() + MS_PER_DAY6);
2507
2607
  }
2508
2608
  previous(t) {
2509
- return new Date(midnightUtc(t).getTime() - MS_PER_DAY5);
2609
+ return new Date(midnightUtc(t).getTime() - MS_PER_DAY6);
2510
2610
  }
2511
2611
  sessions(range) {
2512
2612
  const out = [];
@@ -2514,7 +2614,7 @@ var Crypto24x7Calendar = class {
2514
2614
  const end = range.to.getTime();
2515
2615
  while (cursor.getTime() < end) {
2516
2616
  out.push(cursor);
2517
- cursor = new Date(cursor.getTime() + MS_PER_DAY5);
2617
+ cursor = new Date(cursor.getTime() + MS_PER_DAY6);
2518
2618
  }
2519
2619
  return out;
2520
2620
  }
@@ -2522,7 +2622,7 @@ var Crypto24x7Calendar = class {
2522
2622
  return this.sessions(range).map((date) => ({
2523
2623
  date,
2524
2624
  open: date,
2525
- close: new Date(date.getTime() + MS_PER_DAY5)
2625
+ close: new Date(date.getTime() + MS_PER_DAY6)
2526
2626
  }));
2527
2627
  }
2528
2628
  isEarlyClose(_t) {
@@ -2966,6 +3066,196 @@ __export(features_exports, {
2966
3066
  sma: () => sma,
2967
3067
  volatility: () => volatility
2968
3068
  });
3069
+
3070
+ // src/tax/index.ts
3071
+ var tax_exports = {};
3072
+ __export(tax_exports, {
3073
+ ORDINARY_OFFSET_CAP: () => ORDINARY_OFFSET_CAP,
3074
+ aggregateByYear: () => aggregateByYear,
3075
+ bucketByTerm: () => bucketByTerm,
3076
+ computeTaxBill: () => computeTaxBill,
3077
+ crossOffset: () => crossOffset,
3078
+ holdingPeriodDays: () => holdingPeriodDays,
3079
+ isLongTerm: () => isLongTerm,
3080
+ netWithinBucket: () => netWithinBucket,
3081
+ realize: () => realize,
3082
+ selectFIFO: () => selectFIFO,
3083
+ selectHIFO: () => selectHIFO,
3084
+ selectLIFO: () => selectLIFO,
3085
+ selectMinTax: () => selectMinTax
3086
+ });
3087
+
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
+ // src/tax/aggregation.ts
3142
+ var ORDINARY_OFFSET_CAP = 3e3;
3143
+ function bucketByTerm(events) {
3144
+ const short = [];
3145
+ const long = [];
3146
+ for (const e of events) {
3147
+ if (e.incomeKind !== "capital-gain") continue;
3148
+ (e.termType === "long" ? long : short).push(e);
3149
+ }
3150
+ return { short, long };
3151
+ }
3152
+ function netWithinBucket(events) {
3153
+ let gains = 0;
3154
+ let losses = 0;
3155
+ for (const e of events) {
3156
+ if (e.gain >= 0) gains += e.gain;
3157
+ else losses += -e.gain;
3158
+ }
3159
+ return { gains, losses, net: gains - losses };
3160
+ }
3161
+ function crossOffset(netShort, netLong) {
3162
+ if (netShort >= 0 && netLong >= 0) {
3163
+ return { taxableShort: netShort, taxableLong: netLong, ordinaryOffset: 0, carryForward: 0 };
3164
+ }
3165
+ if (netShort < 0 && netLong < 0) {
3166
+ const totalLoss = -(netShort + netLong);
3167
+ const ordinaryOffset2 = Math.min(ORDINARY_OFFSET_CAP, totalLoss);
3168
+ return { taxableShort: 0, taxableLong: 0, ordinaryOffset: ordinaryOffset2, carryForward: totalLoss - ordinaryOffset2 };
3169
+ }
3170
+ const combined = netShort + netLong;
3171
+ if (combined >= 0) {
3172
+ const taxableShort = netShort > 0 ? combined : 0;
3173
+ const taxableLong = netLong > 0 ? combined : 0;
3174
+ return { taxableShort, taxableLong, ordinaryOffset: 0, carryForward: 0 };
3175
+ }
3176
+ const loss = -combined;
3177
+ const ordinaryOffset = Math.min(ORDINARY_OFFSET_CAP, loss);
3178
+ return { taxableShort: 0, taxableLong: 0, ordinaryOffset, carryForward: loss - ordinaryOffset };
3179
+ }
3180
+ function aggregateByYear(events) {
3181
+ const out = /* @__PURE__ */ new Map();
3182
+ const blank = () => ({
3183
+ shortTermGains: 0,
3184
+ shortTermLosses: 0,
3185
+ longTermGains: 0,
3186
+ longTermLosses: 0,
3187
+ qualifiedDividends: 0,
3188
+ ordinaryDividends: 0,
3189
+ interestIncome: 0
3190
+ });
3191
+ for (const e of events) {
3192
+ const year = e.closeDate.getUTCFullYear();
3193
+ const acc = out.get(year) ?? blank();
3194
+ switch (e.incomeKind) {
3195
+ case "capital-gain":
3196
+ if (e.termType === "long") {
3197
+ if (e.gain >= 0) acc.longTermGains += e.gain;
3198
+ else acc.longTermLosses += -e.gain;
3199
+ } else {
3200
+ if (e.gain >= 0) acc.shortTermGains += e.gain;
3201
+ else acc.shortTermLosses += -e.gain;
3202
+ }
3203
+ break;
3204
+ case "qualified-dividend":
3205
+ acc.qualifiedDividends += e.proceeds;
3206
+ break;
3207
+ case "ordinary-dividend":
3208
+ acc.ordinaryDividends += e.proceeds;
3209
+ break;
3210
+ case "interest":
3211
+ acc.interestIncome += e.proceeds;
3212
+ break;
3213
+ }
3214
+ out.set(year, acc);
3215
+ }
3216
+ return out;
3217
+ }
3218
+ function computeTaxBill(income, rates) {
3219
+ const netShort = income.shortTermGains - income.shortTermLosses;
3220
+ const netLong = income.longTermGains - income.longTermLosses;
3221
+ const off = crossOffset(netShort, netLong);
3222
+ const ordinaryPortion = (off.taxableShort + income.ordinaryDividends + income.interestIncome) * rates.shortTerm;
3223
+ const ltPortion = (off.taxableLong + income.qualifiedDividends) * rates.longTerm;
3224
+ const total = ordinaryPortion + ltPortion;
3225
+ return { total, breakdown: { ordinaryPortion, ltPortion, carryForward: off.carryForward } };
3226
+ }
3227
+
3228
+ // src/portfolio/derived.ts
3229
+ function positionsByAsset(portfolio) {
3230
+ const byId = /* @__PURE__ */ new Map();
3231
+ for (const lot of portfolio.lots ?? []) {
3232
+ const cur = byId.get(lot.asset.id);
3233
+ if (cur) {
3234
+ cur.quantity += lot.quantity;
3235
+ cur.basis += lot.basis;
3236
+ if (lot.openDate < cur.openDate) {
3237
+ cur.openDate = lot.openDate;
3238
+ cur.openPrice = lot.openPrice;
3239
+ }
3240
+ } else {
3241
+ byId.set(lot.asset.id, {
3242
+ asset: lot.asset,
3243
+ quantity: lot.quantity,
3244
+ basis: lot.basis,
3245
+ openDate: lot.openDate,
3246
+ openPrice: lot.openPrice
3247
+ });
3248
+ }
3249
+ }
3250
+ return Array.from(byId.values()).map((agg) => ({
3251
+ id: `lot_view_${agg.asset.id}`,
3252
+ asset: agg.asset,
3253
+ side: "long",
3254
+ quantity: agg.quantity,
3255
+ entry: { date: agg.openDate, price: agg.openPrice },
3256
+ basis: agg.basis
3257
+ }));
3258
+ }
2969
3259
  export {
2970
3260
  BacktestExecutor,
2971
3261
  Crypto24x7Calendar,
@@ -2974,16 +3264,21 @@ export {
2974
3264
  LSEExchangeCalendar,
2975
3265
  MemoryFeatureCache,
2976
3266
  NYSEExchangeCalendar,
3267
+ ORDINARY_OFFSET_CAP,
2977
3268
  RoutingDataFeed,
2978
3269
  RoutingDataFeedError,
2979
3270
  RoutingQuoteFeed,
2980
3271
  RoutingQuoteFeedError,
2981
3272
  RoutingStreamingDataFeed,
2982
3273
  RoutingStreamingDataFeedError,
3274
+ aggregateByYear,
2983
3275
  applyFills,
2984
3276
  applyOrders,
2985
3277
  barsToSeries,
3278
+ bucketByTerm,
2986
3279
  collectBars,
3280
+ computeTaxBill,
3281
+ crossOffset,
2987
3282
  defineFeature,
2988
3283
  drawdown,
2989
3284
  ema,
@@ -2993,18 +3288,28 @@ export {
2993
3288
  fromSpec,
2994
3289
  getCalendar,
2995
3290
  getFeatureCompute,
3291
+ holdingPeriodDays,
3292
+ isLongTerm,
2996
3293
  isRebalanceDay,
3294
+ netWithinBucket,
2997
3295
  paramsHash,
2998
3296
  periodKey,
2999
3297
  pollingStreamFromHistorical,
3298
+ positionsByAsset,
3299
+ realize,
3000
3300
  reconcile,
3001
3301
  returnSeries,
3002
3302
  rsi,
3003
3303
  runBacktest,
3004
3304
  runLive,
3305
+ selectFIFO,
3306
+ selectHIFO,
3307
+ selectLIFO,
3308
+ selectMinTax,
3005
3309
  seriesAt,
3006
3310
  sma,
3007
3311
  tactical_exports as tactical,
3312
+ tax_exports as tax,
3008
3313
  volatility,
3009
3314
  withStreamingSynthetics,
3010
3315
  withSynthetics