@livefolio/sdk 0.4.3 → 0.5.0-rc.1
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 +119 -8
- package/dist/index.js +151 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -176,6 +176,54 @@ type Position = {
|
|
|
176
176
|
/** Optional key-value labels for strategy attribution or downstream reporting. */
|
|
177
177
|
tags?: Record<string, unknown>;
|
|
178
178
|
};
|
|
179
|
+
/**
|
|
180
|
+
* Tax classification of a {@link RealizedEvent}. Drives which rate bucket the
|
|
181
|
+
* income falls into during year-level tax aggregation.
|
|
182
|
+
*/
|
|
183
|
+
type IncomeKind = 'capital-gain' | 'qualified-dividend' | 'ordinary-dividend' | 'interest';
|
|
184
|
+
/**
|
|
185
|
+
* A single tax lot — one acquisition of `quantity` shares of `asset` at a
|
|
186
|
+
* point in time. The cost-basis source of truth for long holdings. Partial
|
|
187
|
+
* sales reduce `quantity` and pro-rate `basis`.
|
|
188
|
+
*/
|
|
189
|
+
type Lot = {
|
|
190
|
+
/** Opaque id assigned by {@link nextLotId} on creation. */
|
|
191
|
+
id: string;
|
|
192
|
+
asset: Asset;
|
|
193
|
+
/** Shares remaining in this lot after any partial sales. */
|
|
194
|
+
quantity: number;
|
|
195
|
+
/** Date the lot was opened (a DRIP lot's clock starts at its pay date). */
|
|
196
|
+
openDate: Date;
|
|
197
|
+
/** Per-share open price, excluding fees. */
|
|
198
|
+
openPrice: number;
|
|
199
|
+
/** Total cost basis for `quantity` shares incl. entry fees; pro-rated on partial sale and bumped by wash-sale §1091. */
|
|
200
|
+
basis: number;
|
|
201
|
+
/** Running total of disallowed wash-sale losses absorbed into `basis`. */
|
|
202
|
+
washSaleAdjustment?: number;
|
|
203
|
+
/** Set when this lot was created via dividend reinvestment; references the lot whose dividend funded it. */
|
|
204
|
+
dripParent?: string;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* An append-only record of realized taxable activity: a capital gain/loss from
|
|
208
|
+
* closing (part of) a lot, or dividend/interest income. `quantity` is `0` for
|
|
209
|
+
* income events (dividends, interest), which carry `basis: 0` and `gain = proceeds`.
|
|
210
|
+
*/
|
|
211
|
+
type RealizedEvent = {
|
|
212
|
+
asset: Asset;
|
|
213
|
+
/** The lot this event closed against. For income events, a reference token (e.g. the paying lot, or `'cash'` for interest). */
|
|
214
|
+
lotId: string;
|
|
215
|
+
/** Shares closed; `0` for dividend and interest income events. */
|
|
216
|
+
quantity: number;
|
|
217
|
+
openDate: Date;
|
|
218
|
+
closeDate: Date;
|
|
219
|
+
proceeds: number;
|
|
220
|
+
basis: number;
|
|
221
|
+
termType: 'short' | 'long';
|
|
222
|
+
gain: number;
|
|
223
|
+
incomeKind: IncomeKind;
|
|
224
|
+
/** When `> 0`, this much of a (negative) capital gain was disallowed by the wash-sale rule and rolled into a replacement lot's basis. */
|
|
225
|
+
washSaleDisallowed?: number;
|
|
226
|
+
};
|
|
179
227
|
/**
|
|
180
228
|
* A point-in-time snapshot of the full portfolio state.
|
|
181
229
|
*
|
|
@@ -199,6 +247,17 @@ type Portfolio = {
|
|
|
199
247
|
cash: number;
|
|
200
248
|
/** All currently open positions. Immutable array — replaced (not mutated) by apply functions. */
|
|
201
249
|
positions: ReadonlyArray<Position>;
|
|
250
|
+
/**
|
|
251
|
+
* Long-side tax ledger — the cost-basis source of truth for lot accounting.
|
|
252
|
+
* Maintained in parallel with `positions` by `applyFills`; defaults to `[]`
|
|
253
|
+
* when absent. Short positions and `adjust` orders do not participate.
|
|
254
|
+
*/
|
|
255
|
+
lots?: ReadonlyArray<Lot>;
|
|
256
|
+
/**
|
|
257
|
+
* Append-only log of realized capital gains and dividend/interest income
|
|
258
|
+
* accumulated during a run. Defaults to `[]` when absent.
|
|
259
|
+
*/
|
|
260
|
+
realized?: ReadonlyArray<RealizedEvent>;
|
|
202
261
|
/** Logical timestamp of the most recent fill (or the portfolio's start date). */
|
|
203
262
|
t: Date;
|
|
204
263
|
};
|
|
@@ -380,6 +439,13 @@ type Fill = {
|
|
|
380
439
|
price: number;
|
|
381
440
|
/** Total transaction fees in the portfolio's base currency. */
|
|
382
441
|
fees: number;
|
|
442
|
+
/**
|
|
443
|
+
* Optional id of the specific {@link Lot} this fill draws from on a sell.
|
|
444
|
+
* Set by {@link BacktestExecutor} when a `lotMethod` is configured so
|
|
445
|
+
* {@link applyFills} consumes the chosen lot rather than defaulting to FIFO.
|
|
446
|
+
* Absent on buys and on default-FIFO sells.
|
|
447
|
+
*/
|
|
448
|
+
lotId?: string;
|
|
383
449
|
};
|
|
384
450
|
|
|
385
451
|
/**
|
|
@@ -2760,12 +2826,27 @@ type FeatureRef = {
|
|
|
2760
2826
|
* - `'lt'` — strictly less than (`l < r`)
|
|
2761
2827
|
* - `'gte'` — greater than or equal to (`l >= r`)
|
|
2762
2828
|
* - `'lte'` — less than or equal to (`l <= r`)
|
|
2829
|
+
* - `'eq'` — equality. Without {@link Tolerance}, this is strict `l === r`
|
|
2830
|
+
* (no epsilon) — intended for comparing integer-valued features (e.g.
|
|
2831
|
+
* calendar features like `dayOfWeek`) against integer literals. With
|
|
2832
|
+
* {@link Tolerance}, this is "within the symmetric band around `r`" —
|
|
2833
|
+
* `true` while `l ∈ [r − tol, r + tol]`, `false` outside. State is still
|
|
2834
|
+
* persisted via {@link RuleTreeState} but, because entry and exit share
|
|
2835
|
+
* the same band edges, the per-step result is effectively stateless.
|
|
2763
2836
|
*/
|
|
2764
|
-
type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte';
|
|
2837
|
+
type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte' | 'eq';
|
|
2765
2838
|
/**
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
*
|
|
2839
|
+
* Tolerance band applied to a {@link Comparison} with `op: 'gt'`, `op: 'lt'`,
|
|
2840
|
+
* or `op: 'eq'`.
|
|
2841
|
+
*
|
|
2842
|
+
* For `gt` / `lt`, the band implements **hysteresis**: once the comparison
|
|
2843
|
+
* has flipped, it will not flip back until the left operand exits the band
|
|
2844
|
+
* around the right operand. Entry and exit thresholds differ.
|
|
2845
|
+
*
|
|
2846
|
+
* For `eq`, the band defines a **symmetric range** around `right`: the
|
|
2847
|
+
* comparison is `true` while `l ∈ [r − value, r + value]`. Entry and exit
|
|
2848
|
+
* share the same edges, so behavior is stateless in practice even though the
|
|
2849
|
+
* outcome is still recorded in {@link RuleTreeState}.
|
|
2769
2850
|
*
|
|
2770
2851
|
* `mode: 'absolute'` defines a ±`value` band around `right`.
|
|
2771
2852
|
* `mode: 'relative'` defines a ±`value`% band (i.e. `value` is a percentage).
|
|
@@ -2774,7 +2855,7 @@ type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte';
|
|
|
2774
2855
|
* so the runtime can persist the last-known state across rebalance periods.
|
|
2775
2856
|
*/
|
|
2776
2857
|
type Tolerance = {
|
|
2777
|
-
/** Half-width of the
|
|
2858
|
+
/** Half-width of the band. */
|
|
2778
2859
|
value: number;
|
|
2779
2860
|
/** `'absolute'` uses raw units; `'relative'` uses a percentage of `right`. */
|
|
2780
2861
|
mode: 'absolute' | 'relative';
|
|
@@ -2809,8 +2890,9 @@ type Comparison = {
|
|
|
2809
2890
|
/** Right-hand operand — a feature reference or a literal number. */
|
|
2810
2891
|
right: FeatureRef | number;
|
|
2811
2892
|
/**
|
|
2812
|
-
* Optional
|
|
2813
|
-
* requires `id` to be set.
|
|
2893
|
+
* Optional tolerance band. Requires `op` to be `'gt'`, `'lt'`, or `'eq'`,
|
|
2894
|
+
* and requires `id` to be set. For `gt`/`lt` the band implements
|
|
2895
|
+
* hysteresis; for `eq` it defines a symmetric range around `right`.
|
|
2814
2896
|
*/
|
|
2815
2897
|
tolerance?: Tolerance;
|
|
2816
2898
|
/**
|
|
@@ -3535,6 +3617,14 @@ declare namespace index {
|
|
|
3535
3617
|
*
|
|
3536
3618
|
* The returned `portfolio.t` is updated to the maximum fill timestamp.
|
|
3537
3619
|
*
|
|
3620
|
+
* In parallel with `positions`/`cash`, a long-side tax ledger is maintained:
|
|
3621
|
+
* buys (`open` long, `rebalance` delta>0) append a {@link Lot}; sells
|
|
3622
|
+
* (`close` of a long, `rebalance` delta<0) consume lots — by `fill.lotId` if
|
|
3623
|
+
* present, else FIFO oldest-first — pro-rating basis and appending one
|
|
3624
|
+
* {@link RealizedEvent} per consumed slice. Short positions and `adjust`
|
|
3625
|
+
* orders do not participate. The `positions`/`cash` outputs are unaffected by
|
|
3626
|
+
* this ledger.
|
|
3627
|
+
*
|
|
3538
3628
|
* @param portfolio - The current portfolio state before this batch.
|
|
3539
3629
|
* @param fills - Execution confirmations returned by {@link Executor.submit}.
|
|
3540
3630
|
* Each fill's `orderRef` MUST match an `id` in `orders`.
|
|
@@ -3572,6 +3662,11 @@ declare function applyFills(portfolio: Portfolio, fills: ReadonlyArray<Fill>, or
|
|
|
3572
3662
|
* - `cash` is left unchanged (no price is available at projection time).
|
|
3573
3663
|
* - Newly opened positions have `basis: 0` and `entry.price: 0` as
|
|
3574
3664
|
* provisional values. A price-aware projection is planned for a later phase.
|
|
3665
|
+
* - The long-side tax ledger (`lots` / `realized`) is **not** advanced — it is
|
|
3666
|
+
* carried through unchanged, so it will be stale relative to the projected
|
|
3667
|
+
* `positions`. Use {@link applyFills} to settle the ledger after confirmed
|
|
3668
|
+
* execution; do not read `lots` from an `applyOrders` result expecting it to
|
|
3669
|
+
* reflect the projected positions.
|
|
3575
3670
|
*
|
|
3576
3671
|
* Use {@link applyFills} (not this function) to settle the portfolio after
|
|
3577
3672
|
* confirmed execution.
|
|
@@ -3601,4 +3696,20 @@ declare function applyFills(portfolio: Portfolio, fills: ReadonlyArray<Fill>, or
|
|
|
3601
3696
|
*/
|
|
3602
3697
|
declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>): Portfolio;
|
|
3603
3698
|
|
|
3604
|
-
|
|
3699
|
+
/**
|
|
3700
|
+
* Aggregates a portfolio's tax {@link Lot}s into a per-asset {@link Position}
|
|
3701
|
+
* view (`side: 'long'`). The lot-level analogue of `portfolio.positions`,
|
|
3702
|
+
* offered for consumers that want a single position per asset derived from the
|
|
3703
|
+
* cost-basis ledger. `reconcile` continues to read `portfolio.positions`.
|
|
3704
|
+
*
|
|
3705
|
+
* The returned ids are synthetic view keys (`lot_view_<assetId>`) — they are
|
|
3706
|
+
* NOT stable `PositionId`s and must not be passed to `CloseOrder.positionId`
|
|
3707
|
+
* or compared against `portfolio.positions[*].id`.
|
|
3708
|
+
*
|
|
3709
|
+
* @param portfolio - Source portfolio; reads `portfolio.lots` (treated as `[]` when absent).
|
|
3710
|
+
* @returns One {@link Position} per distinct asset id, summing quantity and basis,
|
|
3711
|
+
* with `entry` taken from the earliest lot. Empty when there are no lots.
|
|
3712
|
+
*/
|
|
3713
|
+
declare function positionsByAsset(portfolio: Portfolio): Position[];
|
|
3714
|
+
|
|
3715
|
+
export { type AdhocTimeOverrides, type AdjustOrder, type AllocateNode, type Asset, type AssetId, type AssetRef, BacktestExecutor, type BacktestExecutorOptions, type BacktestResult, type BacktestSnapshot, type Bar, type BarField, type Calendar, type CloseOrder, type Comparison, type ComparisonOp, type ComputeFn, Crypto24x7Calendar, type DataEvent, type DataFeed, type DateRange, type EquityAsset, type EventKind, ExchangeCalendar, type ExchangeName, type Executor, type FeatureCache, type FeatureKey, type FeatureKind, type FeatureRef, FeatureRuntime, type FeatureRuntimeOptions, type FeatureScope, type FeatureSpec, type Features, type Fill, type Frequency, type FromSpecOptions, type Fundamentals, type HolidayRule, type IfNode, type IncomeKind, LSEExchangeCalendar, type LiveEvent, type Lot, type MacroAsset, MemoryFeatureCache, NYSEExchangeCalendar, type NextOpenFn, type OpenOrder, type Order, type PollingSchedule, type PollingStreamOptions, type Portfolio, type Position, type PositionId, type PriceMap, type Quote, type QuoteFeed, type RealizedEvent, type RebalanceConfig, type RebalanceFrequency, type RebalanceOrder, type ReturnMode, RoutingDataFeed, RoutingDataFeedError, type RoutingDataFeedRouteFn, type RoutingDataFeedRouteMap, RoutingQuoteFeed, RoutingQuoteFeedError, type RoutingQuoteFeedRouteFn, type RoutingQuoteFeedRouteMap, RoutingStreamingDataFeed, RoutingStreamingDataFeedError, type RoutingStreamingDataFeedRouteFn, type RoutingStreamingDataFeedRouteMap, type RuleNode, type RuleTreeState, type RunBacktestOptions, type RunLiveOptions, type Series, type Session, type SpecialClose, type SpecialOpen, type Strategy, type StreamingBar, type StreamingDataFeed, type SyntheticAsset, type TacticalFeatureKind, type TacticalFeatureSpec, type TacticalFeatures, type TacticalSpec, type TargetWeights, type TimeOfDay, type Tolerance, type WithStreamingSyntheticsOptions, applyFills, applyOrders, barsToSeries, collectBars, defineFeature, drawdown, ema, evaluateFeatureSpecs, evaluateRuleTree, index as features, fromSpec, getCalendar, getFeatureCompute, isRebalanceDay, paramsHash, periodKey, pollingStreamFromHistorical, positionsByAsset, reconcile, returnSeries, rsi, runBacktest, runLive, seriesAt, sma, index$1 as tactical, volatility, withStreamingSynthetics, withSynthetics };
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,12 @@ 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
|
+
|
|
51
57
|
// src/portfolio/apply.ts
|
|
52
58
|
var newPositionId = /* @__PURE__ */ (() => {
|
|
53
59
|
let n = 0;
|
|
@@ -56,10 +62,51 @@ var newPositionId = /* @__PURE__ */ (() => {
|
|
|
56
62
|
function findOrder(orders, id) {
|
|
57
63
|
return orders.find((o) => o.id === id);
|
|
58
64
|
}
|
|
65
|
+
var MS_PER_DAY = 864e5;
|
|
66
|
+
var isLong = (openDate, closeDate) => (closeDate.getTime() - openDate.getTime()) / MS_PER_DAY > 365;
|
|
67
|
+
function consumeLots(lots, realized, assetId, qty, price, fees, closeDate, preferLotId) {
|
|
68
|
+
const sortedLots = lots.filter((l) => l.asset.id === assetId && l.quantity > 0).sort((a, b) => {
|
|
69
|
+
if (preferLotId) {
|
|
70
|
+
if (a.id === preferLotId) return -1;
|
|
71
|
+
if (b.id === preferLotId) return 1;
|
|
72
|
+
}
|
|
73
|
+
return a.openDate.getTime() - b.openDate.getTime();
|
|
74
|
+
});
|
|
75
|
+
const held = sortedLots.reduce((s, x) => s + x.quantity, 0);
|
|
76
|
+
if (held < qty) {
|
|
77
|
+
throw new RangeError(`applyFills: cannot sell ${qty} of ${assetId} \u2014 lot ledger holds ${held}`);
|
|
78
|
+
}
|
|
79
|
+
let need = qty;
|
|
80
|
+
const totalQty = qty;
|
|
81
|
+
for (const l of sortedLots) {
|
|
82
|
+
if (need <= 0) break;
|
|
83
|
+
const take = Math.min(l.quantity, need);
|
|
84
|
+
const basisPerShare = l.basis / l.quantity;
|
|
85
|
+
const consumedBasis = basisPerShare * take;
|
|
86
|
+
const proceeds = take * price - take / totalQty * fees;
|
|
87
|
+
realized.push({
|
|
88
|
+
asset: l.asset,
|
|
89
|
+
lotId: l.id,
|
|
90
|
+
quantity: take,
|
|
91
|
+
openDate: l.openDate,
|
|
92
|
+
closeDate,
|
|
93
|
+
proceeds,
|
|
94
|
+
basis: consumedBasis,
|
|
95
|
+
termType: isLong(l.openDate, closeDate) ? "long" : "short",
|
|
96
|
+
gain: proceeds - consumedBasis,
|
|
97
|
+
incomeKind: "capital-gain"
|
|
98
|
+
});
|
|
99
|
+
l.quantity -= take;
|
|
100
|
+
l.basis -= consumedBasis;
|
|
101
|
+
need -= take;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
59
104
|
function applyFills(portfolio, fills, orders) {
|
|
60
105
|
let positions = [...portfolio.positions];
|
|
61
106
|
let cash = portfolio.cash;
|
|
62
107
|
let t = portfolio.t;
|
|
108
|
+
const lots = (portfolio.lots ?? []).map((l) => ({ ...l }));
|
|
109
|
+
const realized = [...portfolio.realized ?? []];
|
|
63
110
|
for (const fill of fills) {
|
|
64
111
|
const order = findOrder(orders, fill.orderRef);
|
|
65
112
|
if (!order) {
|
|
@@ -78,6 +125,16 @@ function applyFills(portfolio, fills, orders) {
|
|
|
78
125
|
};
|
|
79
126
|
positions.push(pos);
|
|
80
127
|
cash -= fill.quantity * fill.price + fill.fees;
|
|
128
|
+
if (order.side === "long") {
|
|
129
|
+
lots.push({
|
|
130
|
+
id: nextLotId(),
|
|
131
|
+
asset: order.asset,
|
|
132
|
+
quantity: fill.quantity,
|
|
133
|
+
openDate: fill.t,
|
|
134
|
+
openPrice: fill.price,
|
|
135
|
+
basis: fill.quantity * fill.price + fill.fees
|
|
136
|
+
});
|
|
137
|
+
}
|
|
81
138
|
break;
|
|
82
139
|
}
|
|
83
140
|
case "close": {
|
|
@@ -92,6 +149,9 @@ function applyFills(portfolio, fills, orders) {
|
|
|
92
149
|
} else {
|
|
93
150
|
positions[idx] = { ...pos, quantity: remaining };
|
|
94
151
|
}
|
|
152
|
+
if (pos.side === "long") {
|
|
153
|
+
consumeLots(lots, realized, pos.asset.id, fill.quantity, fill.price, fill.fees, fill.t, fill.lotId);
|
|
154
|
+
}
|
|
95
155
|
break;
|
|
96
156
|
}
|
|
97
157
|
case "adjust": {
|
|
@@ -125,6 +185,14 @@ function applyFills(portfolio, fills, orders) {
|
|
|
125
185
|
basis: prev.basis + cost
|
|
126
186
|
};
|
|
127
187
|
}
|
|
188
|
+
lots.push({
|
|
189
|
+
id: nextLotId(),
|
|
190
|
+
asset: order.asset,
|
|
191
|
+
quantity: fill.quantity,
|
|
192
|
+
openDate: fill.t,
|
|
193
|
+
openPrice: fill.price,
|
|
194
|
+
basis: cost
|
|
195
|
+
});
|
|
128
196
|
} else if (idx >= 0) {
|
|
129
197
|
const prev = positions[idx];
|
|
130
198
|
cash += fill.quantity * fill.price - fill.fees;
|
|
@@ -139,12 +207,16 @@ function applyFills(portfolio, fills, orders) {
|
|
|
139
207
|
basis: basisPerShare * remaining
|
|
140
208
|
};
|
|
141
209
|
}
|
|
210
|
+
consumeLots(lots, realized, order.asset.id, fill.quantity, fill.price, fill.fees, fill.t, fill.lotId);
|
|
142
211
|
}
|
|
143
212
|
break;
|
|
144
213
|
}
|
|
145
214
|
}
|
|
215
|
+
for (let i = lots.length - 1; i >= 0; i--) {
|
|
216
|
+
if (lots[i].quantity <= 1e-9) lots.splice(i, 1);
|
|
217
|
+
}
|
|
146
218
|
}
|
|
147
|
-
return { cash, positions, t };
|
|
219
|
+
return { cash, positions, lots, realized, t };
|
|
148
220
|
}
|
|
149
221
|
function applyOrders(portfolio, orders) {
|
|
150
222
|
let positions = [...portfolio.positions];
|
|
@@ -1037,7 +1109,7 @@ function pollingStreamFromHistorical(opts) {
|
|
|
1037
1109
|
import { DateTime } from "luxon";
|
|
1038
1110
|
|
|
1039
1111
|
// src/calendars/holiday-rules.ts
|
|
1040
|
-
var
|
|
1112
|
+
var MS_PER_DAY2 = 864e5;
|
|
1041
1113
|
function nthWeekdayOfMonth(year, month, weekday, n) {
|
|
1042
1114
|
const first = new Date(Date.UTC(year, month - 1, 1));
|
|
1043
1115
|
const offset = (weekday - first.getUTCDay() + 7) % 7;
|
|
@@ -1046,7 +1118,7 @@ function nthWeekdayOfMonth(year, month, weekday, n) {
|
|
|
1046
1118
|
function lastWeekdayOfMonth(year, month, weekday) {
|
|
1047
1119
|
const last = new Date(Date.UTC(year, month, 0));
|
|
1048
1120
|
const offset = (last.getUTCDay() - weekday + 7) % 7;
|
|
1049
|
-
return new Date(last.getTime() - offset *
|
|
1121
|
+
return new Date(last.getTime() - offset * MS_PER_DAY2);
|
|
1050
1122
|
}
|
|
1051
1123
|
function easter(year) {
|
|
1052
1124
|
const a = year % 19;
|
|
@@ -1067,8 +1139,8 @@ function easter(year) {
|
|
|
1067
1139
|
}
|
|
1068
1140
|
function observed(d) {
|
|
1069
1141
|
const dow = d.getUTCDay();
|
|
1070
|
-
if (dow === 6) return new Date(d.getTime() -
|
|
1071
|
-
if (dow === 0) return new Date(d.getTime() +
|
|
1142
|
+
if (dow === 6) return new Date(d.getTime() - MS_PER_DAY2);
|
|
1143
|
+
if (dow === 0) return new Date(d.getTime() + MS_PER_DAY2);
|
|
1072
1144
|
return d;
|
|
1073
1145
|
}
|
|
1074
1146
|
function resolveHolidays(rules, year) {
|
|
@@ -1106,21 +1178,21 @@ function resolveSpecialOpens(rules, year) {
|
|
|
1106
1178
|
return out;
|
|
1107
1179
|
}
|
|
1108
1180
|
function sundayToMonday(d) {
|
|
1109
|
-
return d.getUTCDay() === 0 ? new Date(d.getTime() +
|
|
1181
|
+
return d.getUTCDay() === 0 ? new Date(d.getTime() + MS_PER_DAY2) : d;
|
|
1110
1182
|
}
|
|
1111
1183
|
function nearestWorkday(d) {
|
|
1112
1184
|
const dow = d.getUTCDay();
|
|
1113
|
-
if (dow === 6) return new Date(d.getTime() -
|
|
1114
|
-
if (dow === 0) return new Date(d.getTime() +
|
|
1185
|
+
if (dow === 6) return new Date(d.getTime() - MS_PER_DAY2);
|
|
1186
|
+
if (dow === 0) return new Date(d.getTime() + MS_PER_DAY2);
|
|
1115
1187
|
return d;
|
|
1116
1188
|
}
|
|
1117
1189
|
function firstMondayOnOrAfter(year, month, day, nth = 1) {
|
|
1118
1190
|
const start = new Date(Date.UTC(year, month - 1, day));
|
|
1119
1191
|
const offset = (1 - start.getUTCDay() + 7) % 7;
|
|
1120
|
-
return new Date(start.getTime() + (offset + 7 * (nth - 1)) *
|
|
1192
|
+
return new Date(start.getTime() + (offset + 7 * (nth - 1)) * MS_PER_DAY2);
|
|
1121
1193
|
}
|
|
1122
1194
|
function easterPlus(year, dayDelta) {
|
|
1123
|
-
return new Date(easter(year).getTime() + dayDelta *
|
|
1195
|
+
return new Date(easter(year).getTime() + dayDelta * MS_PER_DAY2);
|
|
1124
1196
|
}
|
|
1125
1197
|
function dropIfNotInDays(d, allowed) {
|
|
1126
1198
|
if (d === null) return null;
|
|
@@ -1128,7 +1200,7 @@ function dropIfNotInDays(d, allowed) {
|
|
|
1128
1200
|
}
|
|
1129
1201
|
|
|
1130
1202
|
// src/calendars/exchange-calendar.ts
|
|
1131
|
-
var
|
|
1203
|
+
var MS_PER_DAY3 = 864e5;
|
|
1132
1204
|
var DEFAULT_WEEKMASK = /* @__PURE__ */ new Set([1, 2, 3, 4, 5]);
|
|
1133
1205
|
var EMPTY_ADHOC = /* @__PURE__ */ new Map();
|
|
1134
1206
|
function ymdKey(d) {
|
|
@@ -1289,14 +1361,14 @@ var ExchangeCalendar = class {
|
|
|
1289
1361
|
}
|
|
1290
1362
|
/** Returns the first trading day strictly after `t`. */
|
|
1291
1363
|
next(t) {
|
|
1292
|
-
let d = new Date(this.normalize(t).getTime() +
|
|
1293
|
-
while (!this.isOpen(d)) d = new Date(d.getTime() +
|
|
1364
|
+
let d = new Date(this.normalize(t).getTime() + MS_PER_DAY3);
|
|
1365
|
+
while (!this.isOpen(d)) d = new Date(d.getTime() + MS_PER_DAY3);
|
|
1294
1366
|
return d;
|
|
1295
1367
|
}
|
|
1296
1368
|
/** Returns the first trading day strictly before `t`. */
|
|
1297
1369
|
previous(t) {
|
|
1298
|
-
let d = new Date(this.normalize(t).getTime() -
|
|
1299
|
-
while (!this.isOpen(d)) d = new Date(d.getTime() -
|
|
1370
|
+
let d = new Date(this.normalize(t).getTime() - MS_PER_DAY3);
|
|
1371
|
+
while (!this.isOpen(d)) d = new Date(d.getTime() - MS_PER_DAY3);
|
|
1300
1372
|
return d;
|
|
1301
1373
|
}
|
|
1302
1374
|
/**
|
|
@@ -1309,7 +1381,7 @@ var ExchangeCalendar = class {
|
|
|
1309
1381
|
const end = this.normalize(range.to).getTime();
|
|
1310
1382
|
while (d.getTime() < end) {
|
|
1311
1383
|
if (this.isOpen(d)) out.push(d);
|
|
1312
|
-
d = new Date(d.getTime() +
|
|
1384
|
+
d = new Date(d.getTime() + MS_PER_DAY3);
|
|
1313
1385
|
}
|
|
1314
1386
|
return out;
|
|
1315
1387
|
}
|
|
@@ -1360,7 +1432,7 @@ var ExchangeCalendar = class {
|
|
|
1360
1432
|
};
|
|
1361
1433
|
|
|
1362
1434
|
// src/calendars/nyse.ts
|
|
1363
|
-
var
|
|
1435
|
+
var MS_PER_DAY4 = 864e5;
|
|
1364
1436
|
var SUN = 0;
|
|
1365
1437
|
var MON = 1;
|
|
1366
1438
|
var TUE = 2;
|
|
@@ -1524,7 +1596,7 @@ var REGULAR_HOLIDAYS = [
|
|
|
1524
1596
|
resolve: (y) => {
|
|
1525
1597
|
const start = utcDate(y, 11, 2);
|
|
1526
1598
|
const offset = (TUE - start.getUTCDay() + 7) % 7;
|
|
1527
|
-
return new Date(start.getTime() + offset *
|
|
1599
|
+
return new Date(start.getTime() + offset * MS_PER_DAY4);
|
|
1528
1600
|
}
|
|
1529
1601
|
},
|
|
1530
1602
|
// ── Veterans/Armistice Day (Nov 11, 1934-1953) ─────────────────────────────
|
|
@@ -1555,7 +1627,7 @@ var REGULAR_HOLIDAYS = [
|
|
|
1555
1627
|
validUntil: 1941,
|
|
1556
1628
|
resolve: (y) => {
|
|
1557
1629
|
const last = lastWeekdayOfMonth(y, 11, THU);
|
|
1558
|
-
return new Date(last.getTime() - 7 *
|
|
1630
|
+
return new Date(last.getTime() - 7 * MS_PER_DAY4);
|
|
1559
1631
|
}
|
|
1560
1632
|
},
|
|
1561
1633
|
// ── Christmas ──────────────────────────────────────────────────────────────
|
|
@@ -1912,7 +1984,7 @@ function* generateSummerSaturdays() {
|
|
|
1912
1984
|
const end = /* @__PURE__ */ new Date(`${to}T00:00:00.000Z`);
|
|
1913
1985
|
while (d.getTime() <= end.getTime()) {
|
|
1914
1986
|
if (d.getUTCDay() === SAT) yield ymd(d);
|
|
1915
|
-
d = new Date(d.getTime() +
|
|
1987
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
1916
1988
|
}
|
|
1917
1989
|
}
|
|
1918
1990
|
}
|
|
@@ -1922,7 +1994,7 @@ function* generateWWIShutdown() {
|
|
|
1922
1994
|
while (d.getTime() <= end.getTime()) {
|
|
1923
1995
|
const dow = d.getUTCDay();
|
|
1924
1996
|
if (dow !== SUN) yield ymd(d);
|
|
1925
|
-
d = new Date(d.getTime() +
|
|
1997
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
1926
1998
|
}
|
|
1927
1999
|
}
|
|
1928
2000
|
var ADHOC_HOLIDAYS = /* @__PURE__ */ new Set([
|
|
@@ -1940,7 +2012,7 @@ var SPECIAL_CLOSES = [
|
|
|
1940
2012
|
closeAt: { h: 13, m: 0 },
|
|
1941
2013
|
resolve: (y) => {
|
|
1942
2014
|
const t = nthWeekdayOfMonth(y, 11, THU, 4);
|
|
1943
|
-
return new Date(t.getTime() +
|
|
2015
|
+
return new Date(t.getTime() + MS_PER_DAY4);
|
|
1944
2016
|
}
|
|
1945
2017
|
},
|
|
1946
2018
|
{
|
|
@@ -1950,7 +2022,7 @@ var SPECIAL_CLOSES = [
|
|
|
1950
2022
|
closeAt: { h: 14, m: 0 },
|
|
1951
2023
|
resolve: (y) => {
|
|
1952
2024
|
const t = nthWeekdayOfMonth(y, 11, THU, 4);
|
|
1953
|
-
return new Date(t.getTime() +
|
|
2025
|
+
return new Date(t.getTime() + MS_PER_DAY4);
|
|
1954
2026
|
}
|
|
1955
2027
|
},
|
|
1956
2028
|
{
|
|
@@ -2111,7 +2183,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
|
|
|
2111
2183
|
if (dow >= MON && dow <= FRI) {
|
|
2112
2184
|
SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
|
|
2113
2185
|
}
|
|
2114
|
-
d = new Date(d.getTime() +
|
|
2186
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
2115
2187
|
}
|
|
2116
2188
|
})();
|
|
2117
2189
|
(() => {
|
|
@@ -2122,7 +2194,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
|
|
|
2122
2194
|
if (dow >= MON && dow <= FRI) {
|
|
2123
2195
|
SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
|
|
2124
2196
|
}
|
|
2125
|
-
d = new Date(d.getTime() +
|
|
2197
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
2126
2198
|
}
|
|
2127
2199
|
})();
|
|
2128
2200
|
(() => {
|
|
@@ -2133,7 +2205,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
|
|
|
2133
2205
|
if (dow >= MON && dow <= FRI) {
|
|
2134
2206
|
SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
|
|
2135
2207
|
}
|
|
2136
|
-
d = new Date(d.getTime() +
|
|
2208
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
2137
2209
|
}
|
|
2138
2210
|
})();
|
|
2139
2211
|
(() => {
|
|
@@ -2144,7 +2216,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
|
|
|
2144
2216
|
if (dow >= MON && dow <= FRI) {
|
|
2145
2217
|
SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 0 });
|
|
2146
2218
|
}
|
|
2147
|
-
d = new Date(d.getTime() +
|
|
2219
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
2148
2220
|
}
|
|
2149
2221
|
})();
|
|
2150
2222
|
(() => {
|
|
@@ -2155,7 +2227,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
|
|
|
2155
2227
|
if (dow >= MON && dow <= FRI) {
|
|
2156
2228
|
SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 14, m: 30 });
|
|
2157
2229
|
}
|
|
2158
|
-
d = new Date(d.getTime() +
|
|
2230
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
2159
2231
|
}
|
|
2160
2232
|
})();
|
|
2161
2233
|
(() => {
|
|
@@ -2166,7 +2238,7 @@ var SPECIAL_CLOSES_ADHOC = /* @__PURE__ */ new Map([
|
|
|
2166
2238
|
if (dow >= MON && dow <= FRI) {
|
|
2167
2239
|
SPECIAL_CLOSES_ADHOC.set(ymd(d), { h: 15, m: 0 });
|
|
2168
2240
|
}
|
|
2169
|
-
d = new Date(d.getTime() +
|
|
2241
|
+
d = new Date(d.getTime() + MS_PER_DAY4);
|
|
2170
2242
|
}
|
|
2171
2243
|
})();
|
|
2172
2244
|
var SPECIAL_OPENS = [];
|
|
@@ -2283,7 +2355,7 @@ var NYSEExchangeCalendar = class extends ExchangeCalendar {
|
|
|
2283
2355
|
};
|
|
2284
2356
|
|
|
2285
2357
|
// src/calendars/lse.ts
|
|
2286
|
-
var
|
|
2358
|
+
var MS_PER_DAY5 = 864e5;
|
|
2287
2359
|
var SUN2 = 0;
|
|
2288
2360
|
var MON2 = 1;
|
|
2289
2361
|
var TUE2 = 2;
|
|
@@ -2295,14 +2367,14 @@ var WEEKDAYS_MON_FRI2 = /* @__PURE__ */ new Set([1, 2, 3, 4, 5]);
|
|
|
2295
2367
|
var MON_TUE = /* @__PURE__ */ new Set([MON2, TUE2]);
|
|
2296
2368
|
function weekendToMonday(d) {
|
|
2297
2369
|
const dow = d.getUTCDay();
|
|
2298
|
-
if (dow === SAT2) return new Date(d.getTime() + 2 *
|
|
2299
|
-
if (dow === SUN2) return new Date(d.getTime() +
|
|
2370
|
+
if (dow === SAT2) return new Date(d.getTime() + 2 * MS_PER_DAY5);
|
|
2371
|
+
if (dow === SUN2) return new Date(d.getTime() + MS_PER_DAY5);
|
|
2300
2372
|
return d;
|
|
2301
2373
|
}
|
|
2302
2374
|
function previousFriday(d) {
|
|
2303
2375
|
const dow = d.getUTCDay();
|
|
2304
|
-
if (dow === SAT2) return new Date(d.getTime() -
|
|
2305
|
-
if (dow === SUN2) return new Date(d.getTime() - 2 *
|
|
2376
|
+
if (dow === SAT2) return new Date(d.getTime() - MS_PER_DAY5);
|
|
2377
|
+
if (dow === SUN2) return new Date(d.getTime() - 2 * MS_PER_DAY5);
|
|
2306
2378
|
return d;
|
|
2307
2379
|
}
|
|
2308
2380
|
var REGULAR_HOLIDAYS2 = [
|
|
@@ -2494,7 +2566,7 @@ function getCalendar(name) {
|
|
|
2494
2566
|
}
|
|
2495
2567
|
|
|
2496
2568
|
// src/calendars/crypto-24x7.ts
|
|
2497
|
-
var
|
|
2569
|
+
var MS_PER_DAY6 = 864e5;
|
|
2498
2570
|
function midnightUtc(d) {
|
|
2499
2571
|
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
2500
2572
|
}
|
|
@@ -2503,10 +2575,10 @@ var Crypto24x7Calendar = class {
|
|
|
2503
2575
|
return true;
|
|
2504
2576
|
}
|
|
2505
2577
|
next(t) {
|
|
2506
|
-
return new Date(midnightUtc(t).getTime() +
|
|
2578
|
+
return new Date(midnightUtc(t).getTime() + MS_PER_DAY6);
|
|
2507
2579
|
}
|
|
2508
2580
|
previous(t) {
|
|
2509
|
-
return new Date(midnightUtc(t).getTime() -
|
|
2581
|
+
return new Date(midnightUtc(t).getTime() - MS_PER_DAY6);
|
|
2510
2582
|
}
|
|
2511
2583
|
sessions(range) {
|
|
2512
2584
|
const out = [];
|
|
@@ -2514,7 +2586,7 @@ var Crypto24x7Calendar = class {
|
|
|
2514
2586
|
const end = range.to.getTime();
|
|
2515
2587
|
while (cursor.getTime() < end) {
|
|
2516
2588
|
out.push(cursor);
|
|
2517
|
-
cursor = new Date(cursor.getTime() +
|
|
2589
|
+
cursor = new Date(cursor.getTime() + MS_PER_DAY6);
|
|
2518
2590
|
}
|
|
2519
2591
|
return out;
|
|
2520
2592
|
}
|
|
@@ -2522,7 +2594,7 @@ var Crypto24x7Calendar = class {
|
|
|
2522
2594
|
return this.sessions(range).map((date) => ({
|
|
2523
2595
|
date,
|
|
2524
2596
|
open: date,
|
|
2525
|
-
close: new Date(date.getTime() +
|
|
2597
|
+
close: new Date(date.getTime() + MS_PER_DAY6)
|
|
2526
2598
|
}));
|
|
2527
2599
|
}
|
|
2528
2600
|
isEarlyClose(_t) {
|
|
@@ -2565,6 +2637,8 @@ function rawCompare(op, l, r) {
|
|
|
2565
2637
|
return l >= r;
|
|
2566
2638
|
case "lte":
|
|
2567
2639
|
return l <= r;
|
|
2640
|
+
case "eq":
|
|
2641
|
+
return l === r;
|
|
2568
2642
|
}
|
|
2569
2643
|
}
|
|
2570
2644
|
function band(right, tol) {
|
|
@@ -2583,13 +2657,15 @@ function evalComparison(cond, values, state, outState) {
|
|
|
2583
2657
|
if (cond.id === void 0) {
|
|
2584
2658
|
throw new Error("evaluateRuleTree: comparison with tolerance requires id");
|
|
2585
2659
|
}
|
|
2586
|
-
if (cond.op !== "gt" && cond.op !== "lt") {
|
|
2587
|
-
throw new Error(`evaluateRuleTree: tolerance is only supported for op gt/lt, got ${cond.op}`);
|
|
2660
|
+
if (cond.op !== "gt" && cond.op !== "lt" && cond.op !== "eq") {
|
|
2661
|
+
throw new Error(`evaluateRuleTree: tolerance is only supported for op gt/lt/eq, got ${cond.op}`);
|
|
2588
2662
|
}
|
|
2589
2663
|
const prev = state.get(cond.id);
|
|
2590
2664
|
const { lower, upper } = band(r, cond.tolerance);
|
|
2591
2665
|
let result;
|
|
2592
|
-
if (
|
|
2666
|
+
if (cond.op === "eq") {
|
|
2667
|
+
result = l >= lower && l <= upper ? 1 : 0;
|
|
2668
|
+
} else if (prev === void 0) {
|
|
2593
2669
|
result = rawCompare(cond.op, l, r) ? 1 : 0;
|
|
2594
2670
|
} else if (cond.op === "gt") {
|
|
2595
2671
|
if (prev === 1) result = l < lower ? 0 : 1;
|
|
@@ -2962,6 +3038,38 @@ __export(features_exports, {
|
|
|
2962
3038
|
sma: () => sma,
|
|
2963
3039
|
volatility: () => volatility
|
|
2964
3040
|
});
|
|
3041
|
+
|
|
3042
|
+
// src/portfolio/derived.ts
|
|
3043
|
+
function positionsByAsset(portfolio) {
|
|
3044
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3045
|
+
for (const lot of portfolio.lots ?? []) {
|
|
3046
|
+
const cur = byId.get(lot.asset.id);
|
|
3047
|
+
if (cur) {
|
|
3048
|
+
cur.quantity += lot.quantity;
|
|
3049
|
+
cur.basis += lot.basis;
|
|
3050
|
+
if (lot.openDate < cur.openDate) {
|
|
3051
|
+
cur.openDate = lot.openDate;
|
|
3052
|
+
cur.openPrice = lot.openPrice;
|
|
3053
|
+
}
|
|
3054
|
+
} else {
|
|
3055
|
+
byId.set(lot.asset.id, {
|
|
3056
|
+
asset: lot.asset,
|
|
3057
|
+
quantity: lot.quantity,
|
|
3058
|
+
basis: lot.basis,
|
|
3059
|
+
openDate: lot.openDate,
|
|
3060
|
+
openPrice: lot.openPrice
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
return Array.from(byId.values()).map((agg) => ({
|
|
3065
|
+
id: `lot_view_${agg.asset.id}`,
|
|
3066
|
+
asset: agg.asset,
|
|
3067
|
+
side: "long",
|
|
3068
|
+
quantity: agg.quantity,
|
|
3069
|
+
entry: { date: agg.openDate, price: agg.openPrice },
|
|
3070
|
+
basis: agg.basis
|
|
3071
|
+
}));
|
|
3072
|
+
}
|
|
2965
3073
|
export {
|
|
2966
3074
|
BacktestExecutor,
|
|
2967
3075
|
Crypto24x7Calendar,
|
|
@@ -2993,6 +3101,7 @@ export {
|
|
|
2993
3101
|
paramsHash,
|
|
2994
3102
|
periodKey,
|
|
2995
3103
|
pollingStreamFromHistorical,
|
|
3104
|
+
positionsByAsset,
|
|
2996
3105
|
reconcile,
|
|
2997
3106
|
returnSeries,
|
|
2998
3107
|
rsi,
|