@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.d.ts +315 -50
- package/dist/index.js +209 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
111
|
+
const take2 = Math.min(l.quantity, need);
|
|
84
112
|
const basisPerShare = l.basis / l.quantity;
|
|
85
|
-
const consumedBasis = basisPerShare *
|
|
86
|
-
const proceeds =
|
|
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:
|
|
118
|
+
quantity: take2,
|
|
91
119
|
openDate: l.openDate,
|
|
92
120
|
closeDate,
|
|
93
121
|
proceeds,
|
|
94
122
|
basis: consumedBasis,
|
|
95
|
-
termType:
|
|
123
|
+
termType: isLongTerm(holdingPeriodDays(l, closeDate)) ? "long" : "short",
|
|
96
124
|
gain: proceeds - consumedBasis,
|
|
97
125
|
incomeKind: "capital-gain"
|
|
98
126
|
});
|
|
99
|
-
l.quantity -=
|
|
127
|
+
l.quantity -= take2;
|
|
100
128
|
l.basis -= consumedBasis;
|
|
101
|
-
need -=
|
|
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
|