@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.d.ts +275 -10
- package/dist/index.js +317 -102
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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({
|
|
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 {
|
|
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 {
|
|
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
|
|
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 *
|
|
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() -
|
|
1213
|
-
if (dow === 0) return new Date(d.getTime() +
|
|
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() +
|
|
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() -
|
|
1256
|
-
if (dow === 0) return new Date(d.getTime() +
|
|
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)) *
|
|
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 *
|
|
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
|
|
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() +
|
|
1435
|
-
while (!this.isOpen(d)) d = new Date(d.getTime() +
|
|
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() -
|
|
1441
|
-
while (!this.isOpen(d)) d = new Date(d.getTime() -
|
|
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() +
|
|
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
|
|
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 *
|
|
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 *
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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() +
|
|
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
|
|
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 *
|
|
2441
|
-
if (dow === SUN2) return new Date(d.getTime() +
|
|
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() -
|
|
2447
|
-
if (dow === SUN2) return new Date(d.getTime() - 2 *
|
|
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
|
|
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() +
|
|
2903
|
+
return new Date(midnightUtc(t).getTime() + MS_PER_DAY7);
|
|
2649
2904
|
}
|
|
2650
2905
|
previous(t) {
|
|
2651
|
-
return new Date(midnightUtc(t).getTime() -
|
|
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() +
|
|
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() +
|
|
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
|
-
|
|
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,
|