@livefolio/sdk 0.4.0 → 0.4.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 CHANGED
@@ -577,6 +577,11 @@ type PriceMap = ReadonlyMap<AssetId, number>;
577
577
  * value and existing share counts.
578
578
  * @param prices - Current prices for all assets that appear in `targets` or are
579
579
  * currently held. Throws `Error` if a target asset is missing from this map.
580
+ * @param assets - Optional canonical {@link Asset} metadata keyed by id. When
581
+ * `reconcile` needs to emit an order for an asset that is not yet held in the
582
+ * portfolio, it consults this map for the proper `symbol`/`kind`. If the
583
+ * asset is missing here too, the order falls back to a synthesized
584
+ * `{ kind: 'equity', id, symbol: id }` — lossless but display-unfriendly.
580
585
  * @returns A readonly array of `RebalanceOrder` objects. The array may be empty
581
586
  * if the portfolio is already at the target allocation. Order IDs are
582
587
  * deterministic within a single call (`rebal_<assetId>_<counter>`).
@@ -603,7 +608,7 @@ type PriceMap = ReadonlyMap<AssetId, number>;
603
608
  * // ]
604
609
  * ```
605
610
  */
606
- declare function reconcile(targets: TargetWeights, portfolio: Portfolio, prices: PriceMap): ReadonlyArray<RebalanceOrder>;
611
+ declare function reconcile(targets: TargetWeights, portfolio: Portfolio, prices: PriceMap, assets?: ReadonlyMap<AssetId, Asset>): ReadonlyArray<RebalanceOrder>;
607
612
 
608
613
  /**
609
614
  * A flat record of fundamental data points for an asset at a point in time.
@@ -1884,6 +1889,100 @@ type RunLiveOptions<F extends Features = Features, S = unknown> = {
1884
1889
  */
1885
1890
  declare function runLive<F extends Features = Features, S = unknown>(opts: RunLiveOptions<F, S>): AsyncIterable<LiveEvent<F, S>>;
1886
1891
 
1892
+ /**
1893
+ * A point-in-time quote for an asset. The `t` field is the vendor-stamped
1894
+ * quote time — callers should treat it as the staleness upper bound, not
1895
+ * "now". `price` is the last trade price, or the mid when the vendor only
1896
+ * exposes bid/ask. `bid` and `ask` surface Level 1 data when available.
1897
+ *
1898
+ * @example
1899
+ * ```ts
1900
+ * import type { Quote } from '@livefolio/sdk';
1901
+ *
1902
+ * const q: Quote = {
1903
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
1904
+ * t: new Date('2024-06-03T13:30:00Z'),
1905
+ * price: 195.12,
1906
+ * bid: 195.11,
1907
+ * ask: 195.13,
1908
+ * currency: 'USD',
1909
+ * };
1910
+ * ```
1911
+ */
1912
+ type Quote = {
1913
+ asset: Asset;
1914
+ /** Vendor-stamped quote time. */
1915
+ t: Date;
1916
+ /** Last trade price, or mid if the vendor only exposes bid/ask. */
1917
+ price: number;
1918
+ /** Best bid, when the vendor exposes Level 1 data. */
1919
+ bid?: number;
1920
+ /** Best ask, when the vendor exposes Level 1 data. */
1921
+ ask?: number;
1922
+ /** Quote currency, when the vendor reports it. */
1923
+ currency?: string;
1924
+ };
1925
+ /**
1926
+ * One-shot current-price source. Sibling interface to {@link DataFeed} and
1927
+ * {@link StreamingDataFeed} — they are NOT a union and there is no
1928
+ * composition helper. Historical adapters implement `DataFeed.bars()`;
1929
+ * streaming adapters implement `StreamingDataFeed.subscribe()`; quote
1930
+ * adapters implement `QuoteFeed.quote()`. A vendor that offers all three
1931
+ * implements all three interfaces on one class.
1932
+ *
1933
+ * Implementations MUST guarantee:
1934
+ * - `quote` returns a freshly fetched {@link Quote} each call. Implementations
1935
+ * MAY cache for a short TTL to coalesce bursts; cache behavior MUST be
1936
+ * documented on the adapter.
1937
+ * - The returned `Quote.t` is the vendor's stamp, not the local clock.
1938
+ * - `quote` rejects with a typed error if the asset is unsupported or the
1939
+ * vendor is unreachable. It MUST NOT silently return a stale or fabricated
1940
+ * price.
1941
+ *
1942
+ * `quoteBatch` is optional. Vendors whose endpoints accept a symbol list
1943
+ * SHOULD implement it to avoid N-round-trip storms. Callers feature-detect:
1944
+ *
1945
+ * ```ts
1946
+ * const quotes = feed.quoteBatch
1947
+ * ? await feed.quoteBatch(assets)
1948
+ * : await Promise.all(assets.map((a) => feed.quote(a)));
1949
+ * ```
1950
+ *
1951
+ * When `quoteBatch` is implemented, the returned array MUST preserve request
1952
+ * order — `quotes[i]` corresponds to `assets[i]`.
1953
+ *
1954
+ * @example
1955
+ * ```ts
1956
+ * import type { QuoteFeed } from '@livefolio/sdk';
1957
+ *
1958
+ * const feed: QuoteFeed = {
1959
+ * async quote(asset) {
1960
+ * return { asset, t: new Date(), price: 195.12 };
1961
+ * },
1962
+ * };
1963
+ * ```
1964
+ */
1965
+ interface QuoteFeed {
1966
+ /**
1967
+ * Returns a freshly fetched quote for `asset`.
1968
+ *
1969
+ * @param asset - The instrument to quote.
1970
+ * @returns A {@link Quote} carrying the vendor-stamped time and price.
1971
+ */
1972
+ quote(asset: Asset): Promise<Quote>;
1973
+ /**
1974
+ * Returns quotes for `assets` in a single vendor round-trip. Optional —
1975
+ * adapters whose vendor does not expose a batch endpoint may omit this.
1976
+ *
1977
+ * Returned array MUST preserve request order: `result[i]` corresponds to
1978
+ * `assets[i]`.
1979
+ *
1980
+ * @param assets - The instruments to quote.
1981
+ * @returns An array of {@link Quote} objects in request order.
1982
+ */
1983
+ quoteBatch?(assets: ReadonlyArray<Asset>): Promise<ReadonlyArray<Quote>>;
1984
+ }
1985
+
1887
1986
  /**
1888
1987
  * In-process, Map-backed implementation of {@link FeatureCache}. Caches
1889
1988
  * computed indicator series in memory for the lifetime of the instance.
@@ -2095,6 +2194,49 @@ declare class RoutingStreamingDataFeed implements StreamingDataFeed {
2095
2194
  private merged;
2096
2195
  }
2097
2196
 
2197
+ /**
2198
+ * Error thrown by {@link RoutingQuoteFeed} when an asset cannot be routed.
2199
+ */
2200
+ declare class RoutingQuoteFeedError extends Error {
2201
+ constructor(message: string);
2202
+ }
2203
+ /** Function form of the routing rule. Returns the feed for `asset`, or `undefined` when no feed handles it. */
2204
+ type RoutingQuoteFeedRouteFn = (asset: Asset) => QuoteFeed | undefined;
2205
+ /** Map form of the routing rule. Keys are `Asset['kind']` discriminants. */
2206
+ type RoutingQuoteFeedRouteMap = Readonly<Partial<Record<Asset['kind'], QuoteFeed>>>;
2207
+ /**
2208
+ * A {@link QuoteFeed} that delegates each call to one of several underlying
2209
+ * feeds based on the asset. Use this to compose vendors — e.g. Alpaca for
2210
+ * equity quotes and a polling adapter for macro series — behind a single
2211
+ * `QuoteFeed` instance.
2212
+ *
2213
+ * Routing rules:
2214
+ * - **Map form:** `new RoutingQuoteFeed({ equity: alpaca, macro: fredPolling })`.
2215
+ * Keys are `asset.kind` discriminants. The 90% case.
2216
+ * - **Function form:** `new RoutingQuoteFeed((a) => a.kind === 'macro' ? fred : alpaca)`.
2217
+ * Use when routing depends on more than `kind` (e.g. allowlists).
2218
+ *
2219
+ * The router always implements `quoteBatch` — even if some inner feeds lack
2220
+ * it, the router falls back to per-asset `quote()` calls within that group,
2221
+ * preserving request order across the full result.
2222
+ *
2223
+ * @example
2224
+ * ```ts
2225
+ * import { RoutingQuoteFeed } from '@livefolio/sdk';
2226
+ *
2227
+ * const feed = new RoutingQuoteFeed({ equity: alpacaQuotes, macro: fredQuotes });
2228
+ * const quotes = await feed.quoteBatch([aaplAsset, dgs10Asset, msftAsset]);
2229
+ * // quotes[0] is for AAPL, quotes[1] for DGS10, quotes[2] for MSFT — request order preserved.
2230
+ * ```
2231
+ */
2232
+ declare class RoutingQuoteFeed implements QuoteFeed {
2233
+ private readonly route;
2234
+ constructor(routes: RoutingQuoteFeedRouteMap | RoutingQuoteFeedRouteFn);
2235
+ quote(asset: Asset): Promise<Quote>;
2236
+ quoteBatch(assets: ReadonlyArray<Asset>): Promise<ReadonlyArray<Quote>>;
2237
+ private resolve;
2238
+ }
2239
+
2098
2240
  type PollingSchedule = {
2099
2241
  kind: 'interval';
2100
2242
  intervalMs: number;
@@ -2915,6 +3057,67 @@ declare function evaluateFeatureSpecs(specs: ReadonlyArray<TacticalFeatureSpec>,
2915
3057
  * ```
2916
3058
  */
2917
3059
  declare function withSynthetics(dataFeed: DataFeed, synthetics: ReadonlyArray<SyntheticAsset>): DataFeed;
3060
+ /**
3061
+ * Options for {@link withStreamingSynthetics}.
3062
+ */
3063
+ interface WithStreamingSyntheticsOptions {
3064
+ /**
3065
+ * Last known close per asset id, used to seed `prevUnderlyingClose` and
3066
+ * `prevSynthClose` so the first live tick of a synthetic continues smoothly
3067
+ * from the end of its historical series.
3068
+ *
3069
+ * Build it from a {@link BacktestResult}:
3070
+ * ```ts
3071
+ * const seedLastCloses = new Map<AssetId, number>();
3072
+ * for (const [id, bars] of history.bars) {
3073
+ * const last = bars.at(-1)?.close;
3074
+ * if (last !== undefined) seedLastCloses.set(id, last);
3075
+ * }
3076
+ * ```
3077
+ *
3078
+ * Without seeding, the first synthesized tick lands on the underlying's
3079
+ * price and produces a visible jump in live preview.
3080
+ */
3081
+ seedLastCloses: ReadonlyMap<AssetId, number>;
3082
+ }
3083
+ /**
3084
+ * Streaming-feed counterpart to {@link withSynthetics}. Wraps a
3085
+ * {@link StreamingDataFeed} so that subscriptions for synthetic asset ids
3086
+ * resolve to upstream subscriptions on the underlying, with each underlying
3087
+ * tick re-emitted as a synthesized tick on the synthetic's id using the same
3088
+ * `(1 + leverage × r) × (1 − expense/252)` formula as the historical wrapper.
3089
+ *
3090
+ * Behavior:
3091
+ * - Non-synthetic ids in the `assets` argument pass through to the inner feed
3092
+ * unchanged.
3093
+ * - Underlyings that aren't directly in `assets` but are needed by a synthetic
3094
+ * are subscribed silently — only the synthesized ticks are yielded back to
3095
+ * the caller for those.
3096
+ * - Underlyings that the caller _did_ ask for in `assets` are yielded both as
3097
+ * the raw underlying tick and as the synthesized tick(s).
3098
+ *
3099
+ * Throws at construction time if `synthetics` contains duplicate `id` values.
3100
+ *
3101
+ * @example
3102
+ * ```ts
3103
+ * import { withStreamingSynthetics, runLive } from '@livefolio/sdk';
3104
+ *
3105
+ * const seedLastCloses = new Map<AssetId, number>();
3106
+ * for (const [id, bars] of history.bars) {
3107
+ * const last = bars.at(-1)?.close;
3108
+ * if (last !== undefined) seedLastCloses.set(id, last);
3109
+ * }
3110
+ *
3111
+ * const liveFeed = withStreamingSynthetics(rawStreamingFeed, spec.synthetics ?? [], {
3112
+ * seedLastCloses,
3113
+ * });
3114
+ *
3115
+ * for await (const event of runLive({ strategy, history, dataFeed: liveFeed, executor, calendar })) {
3116
+ * // …
3117
+ * }
3118
+ * ```
3119
+ */
3120
+ declare function withStreamingSynthetics(inner: StreamingDataFeed, synthetics: ReadonlyArray<SyntheticAsset>, opts: WithStreamingSyntheticsOptions): StreamingDataFeed;
2918
3121
 
2919
3122
  /** Test-only: reset the once-per-process deprecation gate. */
2920
3123
  declare function _resetTacticalDeprecationWarningForTesting(): void;
@@ -3018,15 +3221,17 @@ type index$1_TacticalFeatureSpec = TacticalFeatureSpec;
3018
3221
  type index$1_TacticalFeatures = TacticalFeatures;
3019
3222
  type index$1_TacticalSpec = TacticalSpec;
3020
3223
  type index$1_Tolerance = Tolerance;
3224
+ type index$1_WithStreamingSyntheticsOptions = WithStreamingSyntheticsOptions;
3021
3225
  declare const index$1__resetTacticalDeprecationWarningForTesting: typeof _resetTacticalDeprecationWarningForTesting;
3022
3226
  declare const index$1_evaluateFeatureSpecs: typeof evaluateFeatureSpecs;
3023
3227
  declare const index$1_evaluateRuleTree: typeof evaluateRuleTree;
3024
3228
  declare const index$1_fromSpec: typeof fromSpec;
3025
3229
  declare const index$1_isRebalanceDay: typeof isRebalanceDay;
3026
3230
  declare const index$1_periodKey: typeof periodKey;
3231
+ declare const index$1_withStreamingSynthetics: typeof withStreamingSynthetics;
3027
3232
  declare const index$1_withSynthetics: typeof withSynthetics;
3028
3233
  declare namespace index$1 {
3029
- export { type index$1_AllocateNode as AllocateNode, type index$1_AssetRef as AssetRef, type index$1_Comparison as Comparison, type index$1_ComparisonOp as ComparisonOp, type index$1_FeatureRef as FeatureRef, type index$1_FromSpecOptions as FromSpecOptions, type index$1_IfNode as IfNode, type index$1_RebalanceConfig as RebalanceConfig, type index$1_RebalanceFrequency as RebalanceFrequency, type index$1_RuleNode as RuleNode, type index$1_RuleTreeState as RuleTreeState, type index$1_SyntheticAsset as SyntheticAsset, type index$1_TacticalFeatureKind as TacticalFeatureKind, type index$1_TacticalFeatureSpec as TacticalFeatureSpec, type index$1_TacticalFeatures as TacticalFeatures, type index$1_TacticalSpec as TacticalSpec, type index$1_Tolerance as Tolerance, index$1__resetTacticalDeprecationWarningForTesting as _resetTacticalDeprecationWarningForTesting, index$1_evaluateFeatureSpecs as evaluateFeatureSpecs, index$1_evaluateRuleTree as evaluateRuleTree, index$1_fromSpec as fromSpec, index$1_isRebalanceDay as isRebalanceDay, index$1_periodKey as periodKey, index$1_withSynthetics as withSynthetics };
3234
+ export { type index$1_AllocateNode as AllocateNode, type index$1_AssetRef as AssetRef, type index$1_Comparison as Comparison, type index$1_ComparisonOp as ComparisonOp, type index$1_FeatureRef as FeatureRef, type index$1_FromSpecOptions as FromSpecOptions, type index$1_IfNode as IfNode, type index$1_RebalanceConfig as RebalanceConfig, type index$1_RebalanceFrequency as RebalanceFrequency, type index$1_RuleNode as RuleNode, type index$1_RuleTreeState as RuleTreeState, type index$1_SyntheticAsset as SyntheticAsset, type index$1_TacticalFeatureKind as TacticalFeatureKind, type index$1_TacticalFeatureSpec as TacticalFeatureSpec, type index$1_TacticalFeatures as TacticalFeatures, type index$1_TacticalSpec as TacticalSpec, type index$1_Tolerance as Tolerance, type index$1_WithStreamingSyntheticsOptions as WithStreamingSyntheticsOptions, index$1__resetTacticalDeprecationWarningForTesting as _resetTacticalDeprecationWarningForTesting, index$1_evaluateFeatureSpecs as evaluateFeatureSpecs, index$1_evaluateRuleTree as evaluateRuleTree, index$1_fromSpec as fromSpec, index$1_isRebalanceDay as isRebalanceDay, index$1_periodKey as periodKey, index$1_withStreamingSynthetics as withStreamingSynthetics, index$1_withSynthetics as withSynthetics };
3030
3235
  }
3031
3236
 
3032
3237
  /**
@@ -3393,4 +3598,4 @@ declare function applyFills(portfolio: Portfolio, fills: ReadonlyArray<Fill>, or
3393
3598
  */
3394
3599
  declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>): Portfolio;
3395
3600
 
3396
- 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, LSEExchangeCalendar, type LiveEvent, type MacroAsset, MemoryFeatureCache, NYSEExchangeCalendar, type NextOpenFn, type OpenOrder, type Order, type PollingSchedule, type PollingStreamOptions, type Portfolio, type Position, type PositionId, type PriceMap, type RebalanceConfig, type RebalanceFrequency, type RebalanceOrder, type ReturnMode, RoutingDataFeed, RoutingDataFeedError, type RoutingDataFeedRouteFn, type RoutingDataFeedRouteMap, 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, applyFills, applyOrders, barsToSeries, collectBars, defineFeature, drawdown, ema, evaluateFeatureSpecs, evaluateRuleTree, index as features, fromSpec, getCalendar, getFeatureCompute, isRebalanceDay, paramsHash, periodKey, pollingStreamFromHistorical, reconcile, returnSeries, rsi, runBacktest, runLive, seriesAt, sma, index$1 as tactical, volatility, withSynthetics };
3601
+ 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, LSEExchangeCalendar, type LiveEvent, 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 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, reconcile, returnSeries, rsi, runBacktest, runLive, seriesAt, sma, index$1 as tactical, volatility, withStreamingSynthetics, withSynthetics };
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/strategy/reconcile.ts
8
- function reconcile(targets, portfolio, prices) {
8
+ function reconcile(targets, portfolio, prices, assets) {
9
9
  const longByAsset = /* @__PURE__ */ new Map();
10
10
  for (const p of portfolio.positions) {
11
11
  if (p.side !== "long") continue;
@@ -33,10 +33,10 @@ function reconcile(targets, portfolio, prices) {
33
33
  const delta = targetShares - currentShares;
34
34
  seen.add(assetId);
35
35
  if (delta !== 0) {
36
- const asset = held?.asset ?? {
36
+ const asset = held?.asset ?? assets?.get(assetId) ?? {
37
37
  kind: "equity",
38
38
  id: assetId,
39
- symbol: assetId.split(":").pop() ?? assetId
39
+ symbol: assetId
40
40
  };
41
41
  orders.push({ id: nextId(assetId), kind: "rebalance", asset, delta });
42
42
  }
@@ -925,6 +925,58 @@ async function* mergeIterators(iters) {
925
925
  }
926
926
  }
927
927
 
928
+ // src/reference/routing-quote-feed.ts
929
+ var RoutingQuoteFeedError = class extends Error {
930
+ constructor(message) {
931
+ super(message);
932
+ this.name = "RoutingQuoteFeedError";
933
+ }
934
+ };
935
+ var RoutingQuoteFeed = class {
936
+ route;
937
+ constructor(routes) {
938
+ if (typeof routes === "function") {
939
+ this.route = routes;
940
+ } else {
941
+ this.route = (asset) => routes[asset.kind];
942
+ }
943
+ }
944
+ async quote(asset) {
945
+ return this.resolve(asset).quote(asset);
946
+ }
947
+ async quoteBatch(assets) {
948
+ if (assets.length === 0) return [];
949
+ const groups = /* @__PURE__ */ new Map();
950
+ for (let i = 0; i < assets.length; i++) {
951
+ const asset = assets[i];
952
+ const feed = this.resolve(asset);
953
+ const bucket = groups.get(feed) ?? [];
954
+ bucket.push({ asset, index: i });
955
+ groups.set(feed, bucket);
956
+ }
957
+ const output = new Array(assets.length);
958
+ await Promise.all(
959
+ [...groups.entries()].map(async ([feed, bucket]) => {
960
+ const bucketAssets = bucket.map((b) => b.asset);
961
+ const results = typeof feed.quoteBatch === "function" ? await feed.quoteBatch(bucketAssets) : await Promise.all(bucketAssets.map((a) => feed.quote(a)));
962
+ for (let i = 0; i < bucket.length; i++) {
963
+ output[bucket[i].index] = results[i];
964
+ }
965
+ })
966
+ );
967
+ return output;
968
+ }
969
+ resolve(asset) {
970
+ const feed = this.route(asset);
971
+ if (feed === void 0) {
972
+ throw new RoutingQuoteFeedError(
973
+ `RoutingQuoteFeed: no feed registered for asset.kind="${asset.kind}" (id="${asset.id}")`
974
+ );
975
+ }
976
+ return feed;
977
+ }
978
+ };
979
+
928
980
  // src/reference/polling-stream-from-historical.ts
929
981
  function pollingStreamFromHistorical(opts) {
930
982
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
@@ -2490,6 +2542,7 @@ __export(tactical_exports, {
2490
2542
  fromSpec: () => fromSpec,
2491
2543
  isRebalanceDay: () => isRebalanceDay,
2492
2544
  periodKey: () => periodKey,
2545
+ withStreamingSynthetics: () => withStreamingSynthetics,
2493
2546
  withSynthetics: () => withSynthetics
2494
2547
  });
2495
2548
 
@@ -2642,19 +2695,27 @@ async function evaluateFeatureSpecs(specs, runtime, t) {
2642
2695
 
2643
2696
  // src/tactical/synthetics.ts
2644
2697
  var TRADING_DAYS_PER_YEAR = 252;
2645
- async function* synthesize(underlyingBars, leverage, expense) {
2698
+ function nextSynthClose(opts) {
2699
+ const { prevSynthClose, prevUnderlyingClose, underlyingClose, leverage, expense } = opts;
2700
+ if (prevSynthClose === void 0 || prevUnderlyingClose === void 0) {
2701
+ return underlyingClose;
2702
+ }
2646
2703
  const drag = (expense ?? 0) / TRADING_DAYS_PER_YEAR;
2704
+ const safe = Number.isFinite(prevUnderlyingClose) && prevUnderlyingClose !== 0;
2705
+ const r = safe ? (underlyingClose - prevUnderlyingClose) / prevUnderlyingClose : 0;
2706
+ return prevSynthClose * (1 + leverage * r) * (1 - drag);
2707
+ }
2708
+ async function* synthesize(underlyingBars, leverage, expense) {
2647
2709
  let prevUnderlyingClose;
2648
2710
  let prevSynthClose;
2649
2711
  for await (const u of underlyingBars) {
2650
- let close;
2651
- if (prevSynthClose === void 0 || prevUnderlyingClose === void 0) {
2652
- close = u.close;
2653
- } else {
2654
- const safe = Number.isFinite(prevUnderlyingClose) && prevUnderlyingClose !== 0;
2655
- const r = safe ? (u.close - prevUnderlyingClose) / prevUnderlyingClose : 0;
2656
- close = prevSynthClose * (1 + leverage * r) * (1 - drag);
2657
- }
2712
+ const close = nextSynthClose({
2713
+ prevSynthClose,
2714
+ prevUnderlyingClose,
2715
+ underlyingClose: u.close,
2716
+ leverage,
2717
+ expense
2718
+ });
2658
2719
  yield {
2659
2720
  t: u.t,
2660
2721
  open: close,
@@ -2691,6 +2752,83 @@ function withSynthetics(dataFeed, synthetics) {
2691
2752
  }
2692
2753
  return wrapped;
2693
2754
  }
2755
+ function withStreamingSynthetics(inner, synthetics, opts) {
2756
+ const synthById = /* @__PURE__ */ new Map();
2757
+ for (const s of synthetics) {
2758
+ if (synthById.has(s.id)) {
2759
+ throw new Error(`withStreamingSynthetics: duplicate synthetic asset id "${s.id}"`);
2760
+ }
2761
+ synthById.set(s.id, s);
2762
+ }
2763
+ return {
2764
+ async *subscribe(assets) {
2765
+ const passthroughIds = /* @__PURE__ */ new Set();
2766
+ const requestedSynths = [];
2767
+ const upstream = [];
2768
+ const upstreamSeen = /* @__PURE__ */ new Set();
2769
+ for (const a of assets) {
2770
+ const synth = synthById.get(a.id);
2771
+ if (synth) {
2772
+ requestedSynths.push(synth);
2773
+ if (!upstreamSeen.has(synth.underlying.id)) {
2774
+ upstreamSeen.add(synth.underlying.id);
2775
+ upstream.push(resolveAssetRef(synth.underlying));
2776
+ }
2777
+ } else {
2778
+ passthroughIds.add(a.id);
2779
+ if (!upstreamSeen.has(a.id)) {
2780
+ upstreamSeen.add(a.id);
2781
+ upstream.push(a);
2782
+ }
2783
+ }
2784
+ }
2785
+ const synthsByUnderlying = /* @__PURE__ */ new Map();
2786
+ for (const s of requestedSynths) {
2787
+ const st = {
2788
+ synth: s,
2789
+ asset: resolveAssetRef({ id: s.id, symbol: s.symbol }),
2790
+ prevUnderlyingClose: opts.seedLastCloses.get(s.underlying.id),
2791
+ prevSynthClose: opts.seedLastCloses.get(s.id)
2792
+ };
2793
+ const list = synthsByUnderlying.get(s.underlying.id) ?? [];
2794
+ list.push(st);
2795
+ synthsByUnderlying.set(s.underlying.id, list);
2796
+ }
2797
+ for await (const tick of inner.subscribe(upstream)) {
2798
+ if (passthroughIds.has(tick.asset.id)) {
2799
+ yield tick;
2800
+ }
2801
+ const states = synthsByUnderlying.get(tick.asset.id);
2802
+ if (!states) continue;
2803
+ const underlyingClose = tick.bar.close;
2804
+ for (const st of states) {
2805
+ const synthClose = nextSynthClose({
2806
+ prevSynthClose: st.prevSynthClose,
2807
+ prevUnderlyingClose: st.prevUnderlyingClose,
2808
+ underlyingClose,
2809
+ leverage: st.synth.leverage,
2810
+ expense: st.synth.expense
2811
+ });
2812
+ yield {
2813
+ asset: st.asset,
2814
+ bar: {
2815
+ t: tick.bar.t,
2816
+ open: synthClose,
2817
+ high: synthClose,
2818
+ low: synthClose,
2819
+ close: synthClose,
2820
+ volume: tick.bar.volume
2821
+ }
2822
+ };
2823
+ st.prevSynthClose = synthClose;
2824
+ }
2825
+ for (const st of states) {
2826
+ st.prevUnderlyingClose = underlyingClose;
2827
+ }
2828
+ }
2829
+ }
2830
+ };
2831
+ }
2694
2832
 
2695
2833
  // src/tactical/from-spec.ts
2696
2834
  var _warnedV0 = false;
@@ -2752,6 +2890,13 @@ function fromSpec(spec, opts) {
2752
2890
  }
2753
2891
  validateSynthetics(spec);
2754
2892
  const universe = spec.universe.map(resolveAssetRef);
2893
+ const assetsById = /* @__PURE__ */ new Map();
2894
+ for (const a of universe) assetsById.set(a.id, a);
2895
+ for (const s of spec.synthetics ?? []) {
2896
+ if (!assetsById.has(s.id)) {
2897
+ assetsById.set(s.id, { kind: "equity", id: s.id, symbol: s.symbol });
2898
+ }
2899
+ }
2755
2900
  const { runtime, calendar } = opts;
2756
2901
  const cadence = spec.rebalance?.frequency ?? "Daily";
2757
2902
  return {
@@ -2796,7 +2941,7 @@ function fromSpec(spec, opts) {
2796
2941
  }
2797
2942
  }
2798
2943
  return {
2799
- orders: reconcile(evaluated.weights, portfolio, features.prices),
2944
+ orders: reconcile(evaluated.weights, portfolio, features.prices, assetsById),
2800
2945
  state: evaluated.state
2801
2946
  };
2802
2947
  }
@@ -2830,6 +2975,8 @@ export {
2830
2975
  NYSEExchangeCalendar,
2831
2976
  RoutingDataFeed,
2832
2977
  RoutingDataFeedError,
2978
+ RoutingQuoteFeed,
2979
+ RoutingQuoteFeedError,
2833
2980
  RoutingStreamingDataFeed,
2834
2981
  RoutingStreamingDataFeedError,
2835
2982
  applyFills,
@@ -2858,6 +3005,7 @@ export {
2858
3005
  sma,
2859
3006
  tactical_exports as tactical,
2860
3007
  volatility,
3008
+ withStreamingSynthetics,
2861
3009
  withSynthetics
2862
3010
  };
2863
3011
  //# sourceMappingURL=index.js.map