@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.d.ts +410 -50
- package/dist/index.js +344 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 *
|
|
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() -
|
|
1071
|
-
if (dow === 0) return new Date(d.getTime() +
|
|
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() +
|
|
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() -
|
|
1114
|
-
if (dow === 0) return new Date(d.getTime() +
|
|
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)) *
|
|
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 *
|
|
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
|
|
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() +
|
|
1293
|
-
while (!this.isOpen(d)) d = new Date(d.getTime() +
|
|
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() -
|
|
1299
|
-
while (!this.isOpen(d)) d = new Date(d.getTime() -
|
|
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() +
|
|
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
|
|
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 *
|
|
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 *
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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
|
|
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 *
|
|
2299
|
-
if (dow === SUN2) return new Date(d.getTime() +
|
|
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() -
|
|
2305
|
-
if (dow === SUN2) return new Date(d.getTime() - 2 *
|
|
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
|
|
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() +
|
|
2606
|
+
return new Date(midnightUtc(t).getTime() + MS_PER_DAY6);
|
|
2507
2607
|
}
|
|
2508
2608
|
previous(t) {
|
|
2509
|
-
return new Date(midnightUtc(t).getTime() -
|
|
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() +
|
|
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() +
|
|
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
|