@livefolio/sdk 0.5.0-rc.2 → 0.5.0-rc.3

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
@@ -1599,6 +1599,27 @@ declare class FeatureRuntime {
1599
1599
  compute(spec: FeatureSpec, asset: Asset): Promise<Series>;
1600
1600
  }
1601
1601
 
1602
+ /**
1603
+ * A scheduled cash injection or withdrawal. Applied at the start of the
1604
+ * matching session — BEFORE `universe`/`features`/`build` — by `runBacktest`
1605
+ * (see Task 10 wiring). Events with `t <= sessionT` that have not yet been
1606
+ * consumed are applied (and summed) on that session.
1607
+ */
1608
+ type CashEvent = {
1609
+ t: Date;
1610
+ /** Positive = deposit, negative = withdrawal. */
1611
+ delta: number;
1612
+ /**
1613
+ * Optional attribution tag for downstream metrics. `'deposit'`/`'withdrawal'`
1614
+ * are the natural tags for user-scheduled flows (this surface's main use case).
1615
+ * `'interest'`/`'dividend'` are accepted for callers who want to tag a
1616
+ * manually-scheduled flow as income, but the SDK's automatic per-session
1617
+ * interest/dividend hooks do NOT emit `CashEvent`s — they credit cash directly
1618
+ * and report via `BacktestSnapshot.interestIncome`/`dividendIncome`. User code
1619
+ * typically only sets `'deposit'`/`'withdrawal'`.
1620
+ */
1621
+ reason?: 'deposit' | 'withdrawal' | 'interest' | 'dividend';
1622
+ };
1602
1623
  /**
1603
1624
  * All inputs required to run a historical backtest.
1604
1625
  *
@@ -1653,6 +1674,16 @@ type RunBacktestOptions<F extends Features = Features, S = unknown> = {
1653
1674
  * runtime seed its buffer from the historical bars without refetching).
1654
1675
  */
1655
1676
  featureRuntime?: FeatureRuntime;
1677
+ /**
1678
+ * Optional scheduled deposits/withdrawals applied per-session before the
1679
+ * strategy runs. Matched by `t <= sessionT`; multiple due events are summed.
1680
+ * Defaults to none (today's behavior). See `BacktestSnapshot.cashFlow`.
1681
+ *
1682
+ * @remarks A withdrawal that exceeds available cash is allowed to drive cash
1683
+ * negative (logged via `console.warn`); automatic force-selling of holdings to
1684
+ * fund withdrawals is deferred to a later release, so this behavior may change.
1685
+ */
1686
+ cashEvents?: ReadonlyArray<CashEvent>;
1656
1687
  };
1657
1688
  /**
1658
1689
  * A point-in-time snapshot of the simulation at the end of a single trading session.
@@ -1669,6 +1700,10 @@ type BacktestSnapshot = {
1669
1700
  orders: ReadonlyArray<Order>;
1670
1701
  /** Fills returned by the executor for the orders above. */
1671
1702
  fills: ReadonlyArray<Fill>;
1703
+ /**
1704
+ * Net cash delta applied this session via `cashEvents`. Omitted when zero.
1705
+ */
1706
+ cashFlow?: number;
1672
1707
  };
1673
1708
  /**
1674
1709
  * The return value of `runBacktest`, containing the full simulation history
@@ -1820,6 +1855,18 @@ interface StreamingDataFeed {
1820
1855
  subscribe(assets: ReadonlyArray<Asset>): AsyncIterable<StreamingBar>;
1821
1856
  }
1822
1857
 
1858
+ /**
1859
+ * Mutable queue for scheduling {@link CashEvent}s into a running `runLive`
1860
+ * stream. Construct one, pass it via {@link RunLiveOptions.cashEventQueue}, and
1861
+ * `push` events from outside the generator; they are drained at each session
1862
+ * close. A non-breaking alternative to a returned scheduling handle.
1863
+ */
1864
+ declare class CashEventQueue {
1865
+ private pending;
1866
+ push(e: CashEvent): void;
1867
+ /** Remove and return events with `t <= now`, leaving later events queued. */
1868
+ drainDue(now: Date): CashEvent[];
1869
+ }
1823
1870
  /**
1824
1871
  * Unified event stream from {@link runLive}. Discriminated union of two variants:
1825
1872
  *
@@ -1861,6 +1908,25 @@ type LiveEvent<F extends Features = Features, _S = unknown> = {
1861
1908
  * `portfolio` + `prices`.
1862
1909
  */
1863
1910
  previewPortfolio: Portfolio;
1911
+ } | {
1912
+ /**
1913
+ * A net cash injection/withdrawal applied to the committed portfolio at a
1914
+ * session close. Emitted BEFORE the `snapshot` for that session, so the
1915
+ * subsequent snapshot's `portfolio.cash` already reflects this delta.
1916
+ * Only emitted when the summed delta of due events is non-zero.
1917
+ */
1918
+ type: 'cash';
1919
+ /** Session-close timestamp at which the delta was applied. */
1920
+ t: Date;
1921
+ /** Net cash delta applied (sum of all due events). Positive = deposit. */
1922
+ delta: number;
1923
+ /**
1924
+ * Attribution tag of the FIRST contributing event. When multiple cash
1925
+ * events are due at the same session close they are summed into one delta
1926
+ * and the other reasons are dropped — use `cashEventQueue` and drain
1927
+ * manually if you need per-event attribution.
1928
+ */
1929
+ reason?: CashEvent['reason'];
1864
1930
  } | (BacktestSnapshot & {
1865
1931
  type: 'snapshot';
1866
1932
  });
@@ -1891,6 +1957,20 @@ type RunLiveOptions<F extends Features = Features, S = unknown> = {
1891
1957
  * seeded from `history.bars`.
1892
1958
  */
1893
1959
  streamingRuntime?: FeatureRuntime;
1960
+ /**
1961
+ * Cash injections/withdrawals known up front. Each is applied to the
1962
+ * committed portfolio at the first session close whose date is `>= e.t`,
1963
+ * summed with any other due events for that session. For dynamic scheduling
1964
+ * after the stream has started, use {@link RunLiveOptions.cashEventQueue}.
1965
+ */
1966
+ cashEvents?: ReadonlyArray<CashEvent>;
1967
+ /**
1968
+ * Optional mutable queue for scheduling cash events into the running stream
1969
+ * from outside the generator. Drained (alongside `cashEvents`) at each
1970
+ * session close. Construct a {@link CashEventQueue}, pass it here, and `push`
1971
+ * events as they arrive.
1972
+ */
1973
+ cashEventQueue?: CashEventQueue;
1894
1974
  };
1895
1975
  /**
1896
1976
  * Drives a {@link Strategy} against a streaming market-data source and yields
@@ -3977,4 +4057,4 @@ declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>)
3977
4057
  */
3978
4058
  declare function positionsByAsset(portfolio: Portfolio): Position[];
3979
4059
 
3980
- 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 LotSlice, type MacroAsset, MemoryFeatureCache, NYSEExchangeCalendar, type NextOpenFn, ORDINARY_OFFSET_CAP, type OpenOrder, type Order, type PollingSchedule, type PollingStreamOptions, type Portfolio, type Position, type PositionId, type PriceMap, type Quote, type QuoteFeed, type RealizeResult, 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 TaxRates, type TaxableIncome, type TimeOfDay, type Tolerance, type WithStreamingSyntheticsOptions, aggregateByYear, applyFills, applyOrders, barsToSeries, bucketByTerm, collectBars, computeTaxBill, crossOffset, defineFeature, drawdown, ema, evaluateFeatureSpecs, evaluateRuleTree, index$1 as features, fromSpec, getCalendar, getFeatureCompute, holdingPeriodDays, isLongTerm, isRebalanceDay, netWithinBucket, paramsHash, periodKey, pollingStreamFromHistorical, positionsByAsset, realize, reconcile, returnSeries, rsi, runBacktest, runLive, selectFIFO, selectHIFO, selectLIFO, selectMinTax, seriesAt, sma, index$2 as tactical, index as tax, volatility, withStreamingSynthetics, withSynthetics };
4060
+ 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 CashEvent, CashEventQueue, 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 LotSlice, type MacroAsset, MemoryFeatureCache, NYSEExchangeCalendar, type NextOpenFn, ORDINARY_OFFSET_CAP, type OpenOrder, type Order, type PollingSchedule, type PollingStreamOptions, type Portfolio, type Position, type PositionId, type PriceMap, type Quote, type QuoteFeed, type RealizeResult, 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 TaxRates, type TaxableIncome, type TimeOfDay, type Tolerance, type WithStreamingSyntheticsOptions, aggregateByYear, applyFills, applyOrders, barsToSeries, bucketByTerm, collectBars, computeTaxBill, crossOffset, defineFeature, drawdown, ema, evaluateFeatureSpecs, evaluateRuleTree, index$1 as features, fromSpec, getCalendar, getFeatureCompute, holdingPeriodDays, isLongTerm, isRebalanceDay, netWithinBucket, paramsHash, periodKey, pollingStreamFromHistorical, positionsByAsset, realize, reconcile, returnSeries, rsi, runBacktest, runLive, selectFIFO, selectHIFO, selectLIFO, selectMinTax, seriesAt, sma, index$2 as tactical, index as tax, volatility, withStreamingSynthetics, withSynthetics };
package/dist/index.js CHANGED
@@ -331,7 +331,24 @@ async function runBacktest(opts) {
331
331
  let portfolio = opts.initialPortfolio;
332
332
  let state = initialStateValue;
333
333
  const snapshots = [];
334
+ const cashEvents = [...opts.cashEvents ?? []].sort((a, b) => a.t.getTime() - b.t.getTime());
335
+ let eventCursor = 0;
336
+ let warnedNegativeCash = false;
334
337
  for (const t of sessions) {
338
+ let cashFlow = 0;
339
+ while (eventCursor < cashEvents.length && cashEvents[eventCursor].t.getTime() <= t.getTime()) {
340
+ cashFlow += cashEvents[eventCursor].delta;
341
+ eventCursor++;
342
+ }
343
+ if (cashFlow !== 0) {
344
+ portfolio = { ...portfolio, cash: portfolio.cash + cashFlow };
345
+ if (portfolio.cash < 0 && !warnedNegativeCash) {
346
+ warnedNegativeCash = true;
347
+ console.warn(
348
+ `[runBacktest] cash went negative at ${t.toISOString()}: ${portfolio.cash}. Withdrawals exceed available cash (force-sell is deferred); further occurrences this run are suppressed.`
349
+ );
350
+ }
351
+ }
335
352
  const universe = opts.strategy.universe(t, portfolio);
336
353
  const features = await opts.strategy.features(universe, portfolio, t);
337
354
  const buildResult = opts.strategy.build(features, portfolio, state, t);
@@ -344,7 +361,7 @@ async function runBacktest(opts) {
344
361
  }
345
362
  const fills = await opts.executor.submit(orders, t, portfolio);
346
363
  portfolio = applyFills(portfolio, fills, orders);
347
- snapshots.push({ t, portfolio, orders, fills });
364
+ snapshots.push({ t, portfolio, orders, fills, ...cashFlow !== 0 ? { cashFlow } : {} });
348
365
  }
349
366
  const bars = opts.featureRuntime?.getAllBars() ?? /* @__PURE__ */ new Map();
350
367
  return { snapshots, finalPortfolio: portfolio, finalState: state, bars };
@@ -757,6 +774,18 @@ var MemoryFeatureCache = class {
757
774
  };
758
775
 
759
776
  // src/strategy/run-live.ts
777
+ var CashEventQueue = class {
778
+ pending = [];
779
+ push(e) {
780
+ this.pending.push(e);
781
+ }
782
+ /** Remove and return events with `t <= now`, leaving later events queued. */
783
+ drainDue(now) {
784
+ const due = this.pending.filter((e) => e.t.getTime() <= now.getTime());
785
+ if (due.length > 0) this.pending = this.pending.filter((e) => e.t.getTime() > now.getTime());
786
+ return due;
787
+ }
788
+ };
760
789
  function snapshotState(state) {
761
790
  if (state === void 0) return void 0;
762
791
  return structuredClone(state);
@@ -775,6 +804,8 @@ async function* runLive(opts) {
775
804
  });
776
805
  let portfolio = history.finalPortfolio;
777
806
  let state = history.finalState;
807
+ const seedQueue = new CashEventQueue();
808
+ for (const e of opts.cashEvents ?? []) seedQueue.push(e);
778
809
  const anchorTime = history.snapshots.length > 0 ? history.snapshots[history.snapshots.length - 1].t : /* @__PURE__ */ new Date(0);
779
810
  const universe = strategy.universe(anchorTime, portfolio);
780
811
  let currentSession = history.snapshots.length > 0 ? calendar.next(history.snapshots[history.snapshots.length - 1].t) : null;
@@ -826,6 +857,17 @@ async function* runLive(opts) {
826
857
  }
827
858
  const fills = await executor.submit(orders, currentSession, portfolio);
828
859
  portfolio = applyFills(portfolio, fills, orders);
860
+ const dueCash = [...seedQueue.drainDue(currentSession), ...opts.cashEventQueue?.drainDue(currentSession) ?? []];
861
+ const cashDelta = dueCash.reduce((s, e) => s + e.delta, 0);
862
+ if (cashDelta !== 0) {
863
+ portfolio = { ...portfolio, cash: portfolio.cash + cashDelta };
864
+ yield {
865
+ type: "cash",
866
+ t: currentSession,
867
+ delta: cashDelta,
868
+ reason: dueCash[0]?.reason
869
+ };
870
+ }
829
871
  yield {
830
872
  type: "snapshot",
831
873
  t: currentSession,
@@ -3258,6 +3300,7 @@ function positionsByAsset(portfolio) {
3258
3300
  }
3259
3301
  export {
3260
3302
  BacktestExecutor,
3303
+ CashEventQueue,
3261
3304
  Crypto24x7Calendar,
3262
3305
  ExchangeCalendar,
3263
3306
  FeatureRuntime,