@livefolio/sdk 0.5.0-rc.1 → 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
@@ -54,6 +54,36 @@ function nextLotId() {
54
54
  return `lot_${++_lotCounter}`;
55
55
  }
56
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
+
57
87
  // src/portfolio/apply.ts
58
88
  var newPositionId = /* @__PURE__ */ (() => {
59
89
  let n = 0;
@@ -62,8 +92,6 @@ var newPositionId = /* @__PURE__ */ (() => {
62
92
  function findOrder(orders, id) {
63
93
  return orders.find((o) => o.id === id);
64
94
  }
65
- var MS_PER_DAY = 864e5;
66
- var isLong = (openDate, closeDate) => (closeDate.getTime() - openDate.getTime()) / MS_PER_DAY > 365;
67
95
  function consumeLots(lots, realized, assetId, qty, price, fees, closeDate, preferLotId) {
68
96
  const sortedLots = lots.filter((l) => l.asset.id === assetId && l.quantity > 0).sort((a, b) => {
69
97
  if (preferLotId) {
@@ -80,25 +108,25 @@ function consumeLots(lots, realized, assetId, qty, price, fees, closeDate, prefe
80
108
  const totalQty = qty;
81
109
  for (const l of sortedLots) {
82
110
  if (need <= 0) break;
83
- const take = Math.min(l.quantity, need);
111
+ const take2 = Math.min(l.quantity, need);
84
112
  const basisPerShare = l.basis / l.quantity;
85
- const consumedBasis = basisPerShare * take;
86
- const proceeds = take * price - take / totalQty * fees;
113
+ const consumedBasis = basisPerShare * take2;
114
+ const proceeds = take2 * price - take2 / totalQty * fees;
87
115
  realized.push({
88
116
  asset: l.asset,
89
117
  lotId: l.id,
90
- quantity: take,
118
+ quantity: take2,
91
119
  openDate: l.openDate,
92
120
  closeDate,
93
121
  proceeds,
94
122
  basis: consumedBasis,
95
- termType: isLong(l.openDate, closeDate) ? "long" : "short",
123
+ termType: isLongTerm(holdingPeriodDays(l, closeDate)) ? "long" : "short",
96
124
  gain: proceeds - consumedBasis,
97
125
  incomeKind: "capital-gain"
98
126
  });
99
- l.quantity -= take;
127
+ l.quantity -= take2;
100
128
  l.basis -= consumedBasis;
101
- need -= take;
129
+ need -= take2;
102
130
  }
103
131
  }
104
132
  function applyFills(portfolio, fills, orders) {
@@ -3039,6 +3067,164 @@ __export(features_exports, {
3039
3067
  volatility: () => volatility
3040
3068
  });
3041
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
+
3042
3228
  // src/portfolio/derived.ts
3043
3229
  function positionsByAsset(portfolio) {
3044
3230
  const byId = /* @__PURE__ */ new Map();
@@ -3078,16 +3264,21 @@ export {
3078
3264
  LSEExchangeCalendar,
3079
3265
  MemoryFeatureCache,
3080
3266
  NYSEExchangeCalendar,
3267
+ ORDINARY_OFFSET_CAP,
3081
3268
  RoutingDataFeed,
3082
3269
  RoutingDataFeedError,
3083
3270
  RoutingQuoteFeed,
3084
3271
  RoutingQuoteFeedError,
3085
3272
  RoutingStreamingDataFeed,
3086
3273
  RoutingStreamingDataFeedError,
3274
+ aggregateByYear,
3087
3275
  applyFills,
3088
3276
  applyOrders,
3089
3277
  barsToSeries,
3278
+ bucketByTerm,
3090
3279
  collectBars,
3280
+ computeTaxBill,
3281
+ crossOffset,
3091
3282
  defineFeature,
3092
3283
  drawdown,
3093
3284
  ema,
@@ -3097,19 +3288,28 @@ export {
3097
3288
  fromSpec,
3098
3289
  getCalendar,
3099
3290
  getFeatureCompute,
3291
+ holdingPeriodDays,
3292
+ isLongTerm,
3100
3293
  isRebalanceDay,
3294
+ netWithinBucket,
3101
3295
  paramsHash,
3102
3296
  periodKey,
3103
3297
  pollingStreamFromHistorical,
3104
3298
  positionsByAsset,
3299
+ realize,
3105
3300
  reconcile,
3106
3301
  returnSeries,
3107
3302
  rsi,
3108
3303
  runBacktest,
3109
3304
  runLive,
3305
+ selectFIFO,
3306
+ selectHIFO,
3307
+ selectLIFO,
3308
+ selectMinTax,
3110
3309
  seriesAt,
3111
3310
  sma,
3112
3311
  tactical_exports as tactical,
3312
+ tax_exports as tax,
3113
3313
  volatility,
3114
3314
  withStreamingSynthetics,
3115
3315
  withSynthetics