@livefolio/sdk 0.4.2 → 0.4.4

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 CHANGED
@@ -2760,12 +2760,27 @@ type FeatureRef = {
2760
2760
  * - `'lt'` — strictly less than (`l < r`)
2761
2761
  * - `'gte'` — greater than or equal to (`l >= r`)
2762
2762
  * - `'lte'` — less than or equal to (`l <= r`)
2763
+ * - `'eq'` — equality. Without {@link Tolerance}, this is strict `l === r`
2764
+ * (no epsilon) — intended for comparing integer-valued features (e.g.
2765
+ * calendar features like `dayOfWeek`) against integer literals. With
2766
+ * {@link Tolerance}, this is "within the symmetric band around `r`" —
2767
+ * `true` while `l ∈ [r − tol, r + tol]`, `false` outside. State is still
2768
+ * persisted via {@link RuleTreeState} but, because entry and exit share
2769
+ * the same band edges, the per-step result is effectively stateless.
2763
2770
  */
2764
- type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte';
2771
+ type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte' | 'eq';
2765
2772
  /**
2766
- * Hysteresis band applied to a {@link Comparison} with `op: 'gt'` or `op: 'lt'`.
2767
- * Once the comparison has flipped, it will not flip back until the left operand
2768
- * exits the tolerance band around the right operand.
2773
+ * Tolerance band applied to a {@link Comparison} with `op: 'gt'`, `op: 'lt'`,
2774
+ * or `op: 'eq'`.
2775
+ *
2776
+ * For `gt` / `lt`, the band implements **hysteresis**: once the comparison
2777
+ * has flipped, it will not flip back until the left operand exits the band
2778
+ * around the right operand. Entry and exit thresholds differ.
2779
+ *
2780
+ * For `eq`, the band defines a **symmetric range** around `right`: the
2781
+ * comparison is `true` while `l ∈ [r − value, r + value]`. Entry and exit
2782
+ * share the same edges, so behavior is stateless in practice even though the
2783
+ * outcome is still recorded in {@link RuleTreeState}.
2769
2784
  *
2770
2785
  * `mode: 'absolute'` defines a ±`value` band around `right`.
2771
2786
  * `mode: 'relative'` defines a ±`value`% band (i.e. `value` is a percentage).
@@ -2774,7 +2789,7 @@ type ComparisonOp = 'gt' | 'lt' | 'gte' | 'lte';
2774
2789
  * so the runtime can persist the last-known state across rebalance periods.
2775
2790
  */
2776
2791
  type Tolerance = {
2777
- /** Half-width of the hysteresis band. */
2792
+ /** Half-width of the band. */
2778
2793
  value: number;
2779
2794
  /** `'absolute'` uses raw units; `'relative'` uses a percentage of `right`. */
2780
2795
  mode: 'absolute' | 'relative';
@@ -2809,8 +2824,9 @@ type Comparison = {
2809
2824
  /** Right-hand operand — a feature reference or a literal number. */
2810
2825
  right: FeatureRef | number;
2811
2826
  /**
2812
- * Optional hysteresis band. Requires `op` to be `'gt'` or `'lt'` and
2813
- * requires `id` to be set.
2827
+ * Optional tolerance band. Requires `op` to be `'gt'`, `'lt'`, or `'eq'`,
2828
+ * and requires `id` to be set. For `gt`/`lt` the band implements
2829
+ * hysteresis; for `eq` it defines a symmetric range around `right`.
2814
2830
  */
2815
2831
  tolerance?: Tolerance;
2816
2832
  /**
@@ -3528,7 +3544,10 @@ declare namespace index {
3528
3544
  * - `'close'` — removes shares from an existing position and credits cash.
3529
3545
  * - `'adjust'` — updates the position's `quantity`; only fees are debited.
3530
3546
  * - `'rebalance'` — buys or sells shares in the long position for `asset`;
3531
- * creates or removes the position as needed.
3547
+ * creates the position on a positive `delta`, removes it
3548
+ * when fully reduced. A reduce against a non-existent
3549
+ * long position is silently ignored (matching the same
3550
+ * case in {@link applyOrders}).
3532
3551
  *
3533
3552
  * The returned `portfolio.t` is updated to the maximum fill timestamp.
3534
3553
  *
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ function reconcile(targets, portfolio, prices, assets) {
27
27
  if (price === void 0) {
28
28
  throw new Error(`reconcile: missing price for target asset ${assetId}`);
29
29
  }
30
- const targetShares = Math.floor(totalValue * weight / price);
30
+ const targetShares = Math.max(0, Math.floor(totalValue * weight / price));
31
31
  const held = longByAsset.get(assetId);
32
32
  const currentShares = held?.quantity ?? 0;
33
33
  const delta = targetShares - currentShares;
@@ -125,10 +125,7 @@ function applyFills(portfolio, fills, orders) {
125
125
  basis: prev.basis + cost
126
126
  };
127
127
  }
128
- } else {
129
- if (idx < 0) {
130
- throw new Error(`applyFills: rebalance reduce on ${order.asset.id} but no long position exists`);
131
- }
128
+ } else if (idx >= 0) {
132
129
  const prev = positions[idx];
133
130
  cash += fill.quantity * fill.price - fill.fees;
134
131
  const remaining = prev.quantity - fill.quantity;
@@ -2568,6 +2565,8 @@ function rawCompare(op, l, r) {
2568
2565
  return l >= r;
2569
2566
  case "lte":
2570
2567
  return l <= r;
2568
+ case "eq":
2569
+ return l === r;
2571
2570
  }
2572
2571
  }
2573
2572
  function band(right, tol) {
@@ -2586,13 +2585,15 @@ function evalComparison(cond, values, state, outState) {
2586
2585
  if (cond.id === void 0) {
2587
2586
  throw new Error("evaluateRuleTree: comparison with tolerance requires id");
2588
2587
  }
2589
- if (cond.op !== "gt" && cond.op !== "lt") {
2590
- throw new Error(`evaluateRuleTree: tolerance is only supported for op gt/lt, got ${cond.op}`);
2588
+ if (cond.op !== "gt" && cond.op !== "lt" && cond.op !== "eq") {
2589
+ throw new Error(`evaluateRuleTree: tolerance is only supported for op gt/lt/eq, got ${cond.op}`);
2591
2590
  }
2592
2591
  const prev = state.get(cond.id);
2593
2592
  const { lower, upper } = band(r, cond.tolerance);
2594
2593
  let result;
2595
- if (prev === void 0) {
2594
+ if (cond.op === "eq") {
2595
+ result = l >= lower && l <= upper ? 1 : 0;
2596
+ } else if (prev === void 0) {
2596
2597
  result = rawCompare(cond.op, l, r) ? 1 : 0;
2597
2598
  } else if (cond.op === "gt") {
2598
2599
  if (prev === 1) result = l < lower ? 0 : 1;