@livefolio/sdk 0.4.0 → 0.4.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 CHANGED
@@ -1884,6 +1884,100 @@ type RunLiveOptions<F extends Features = Features, S = unknown> = {
1884
1884
  */
1885
1885
  declare function runLive<F extends Features = Features, S = unknown>(opts: RunLiveOptions<F, S>): AsyncIterable<LiveEvent<F, S>>;
1886
1886
 
1887
+ /**
1888
+ * A point-in-time quote for an asset. The `t` field is the vendor-stamped
1889
+ * quote time — callers should treat it as the staleness upper bound, not
1890
+ * "now". `price` is the last trade price, or the mid when the vendor only
1891
+ * exposes bid/ask. `bid` and `ask` surface Level 1 data when available.
1892
+ *
1893
+ * @example
1894
+ * ```ts
1895
+ * import type { Quote } from '@livefolio/sdk';
1896
+ *
1897
+ * const q: Quote = {
1898
+ * asset: { kind: 'equity', id: 'AAPL', symbol: 'AAPL' },
1899
+ * t: new Date('2024-06-03T13:30:00Z'),
1900
+ * price: 195.12,
1901
+ * bid: 195.11,
1902
+ * ask: 195.13,
1903
+ * currency: 'USD',
1904
+ * };
1905
+ * ```
1906
+ */
1907
+ type Quote = {
1908
+ asset: Asset;
1909
+ /** Vendor-stamped quote time. */
1910
+ t: Date;
1911
+ /** Last trade price, or mid if the vendor only exposes bid/ask. */
1912
+ price: number;
1913
+ /** Best bid, when the vendor exposes Level 1 data. */
1914
+ bid?: number;
1915
+ /** Best ask, when the vendor exposes Level 1 data. */
1916
+ ask?: number;
1917
+ /** Quote currency, when the vendor reports it. */
1918
+ currency?: string;
1919
+ };
1920
+ /**
1921
+ * One-shot current-price source. Sibling interface to {@link DataFeed} and
1922
+ * {@link StreamingDataFeed} — they are NOT a union and there is no
1923
+ * composition helper. Historical adapters implement `DataFeed.bars()`;
1924
+ * streaming adapters implement `StreamingDataFeed.subscribe()`; quote
1925
+ * adapters implement `QuoteFeed.quote()`. A vendor that offers all three
1926
+ * implements all three interfaces on one class.
1927
+ *
1928
+ * Implementations MUST guarantee:
1929
+ * - `quote` returns a freshly fetched {@link Quote} each call. Implementations
1930
+ * MAY cache for a short TTL to coalesce bursts; cache behavior MUST be
1931
+ * documented on the adapter.
1932
+ * - The returned `Quote.t` is the vendor's stamp, not the local clock.
1933
+ * - `quote` rejects with a typed error if the asset is unsupported or the
1934
+ * vendor is unreachable. It MUST NOT silently return a stale or fabricated
1935
+ * price.
1936
+ *
1937
+ * `quoteBatch` is optional. Vendors whose endpoints accept a symbol list
1938
+ * SHOULD implement it to avoid N-round-trip storms. Callers feature-detect:
1939
+ *
1940
+ * ```ts
1941
+ * const quotes = feed.quoteBatch
1942
+ * ? await feed.quoteBatch(assets)
1943
+ * : await Promise.all(assets.map((a) => feed.quote(a)));
1944
+ * ```
1945
+ *
1946
+ * When `quoteBatch` is implemented, the returned array MUST preserve request
1947
+ * order — `quotes[i]` corresponds to `assets[i]`.
1948
+ *
1949
+ * @example
1950
+ * ```ts
1951
+ * import type { QuoteFeed } from '@livefolio/sdk';
1952
+ *
1953
+ * const feed: QuoteFeed = {
1954
+ * async quote(asset) {
1955
+ * return { asset, t: new Date(), price: 195.12 };
1956
+ * },
1957
+ * };
1958
+ * ```
1959
+ */
1960
+ interface QuoteFeed {
1961
+ /**
1962
+ * Returns a freshly fetched quote for `asset`.
1963
+ *
1964
+ * @param asset - The instrument to quote.
1965
+ * @returns A {@link Quote} carrying the vendor-stamped time and price.
1966
+ */
1967
+ quote(asset: Asset): Promise<Quote>;
1968
+ /**
1969
+ * Returns quotes for `assets` in a single vendor round-trip. Optional —
1970
+ * adapters whose vendor does not expose a batch endpoint may omit this.
1971
+ *
1972
+ * Returned array MUST preserve request order: `result[i]` corresponds to
1973
+ * `assets[i]`.
1974
+ *
1975
+ * @param assets - The instruments to quote.
1976
+ * @returns An array of {@link Quote} objects in request order.
1977
+ */
1978
+ quoteBatch?(assets: ReadonlyArray<Asset>): Promise<ReadonlyArray<Quote>>;
1979
+ }
1980
+
1887
1981
  /**
1888
1982
  * In-process, Map-backed implementation of {@link FeatureCache}. Caches
1889
1983
  * computed indicator series in memory for the lifetime of the instance.
@@ -2095,6 +2189,49 @@ declare class RoutingStreamingDataFeed implements StreamingDataFeed {
2095
2189
  private merged;
2096
2190
  }
2097
2191
 
2192
+ /**
2193
+ * Error thrown by {@link RoutingQuoteFeed} when an asset cannot be routed.
2194
+ */
2195
+ declare class RoutingQuoteFeedError extends Error {
2196
+ constructor(message: string);
2197
+ }
2198
+ /** Function form of the routing rule. Returns the feed for `asset`, or `undefined` when no feed handles it. */
2199
+ type RoutingQuoteFeedRouteFn = (asset: Asset) => QuoteFeed | undefined;
2200
+ /** Map form of the routing rule. Keys are `Asset['kind']` discriminants. */
2201
+ type RoutingQuoteFeedRouteMap = Readonly<Partial<Record<Asset['kind'], QuoteFeed>>>;
2202
+ /**
2203
+ * A {@link QuoteFeed} that delegates each call to one of several underlying
2204
+ * feeds based on the asset. Use this to compose vendors — e.g. Alpaca for
2205
+ * equity quotes and a polling adapter for macro series — behind a single
2206
+ * `QuoteFeed` instance.
2207
+ *
2208
+ * Routing rules:
2209
+ * - **Map form:** `new RoutingQuoteFeed({ equity: alpaca, macro: fredPolling })`.
2210
+ * Keys are `asset.kind` discriminants. The 90% case.
2211
+ * - **Function form:** `new RoutingQuoteFeed((a) => a.kind === 'macro' ? fred : alpaca)`.
2212
+ * Use when routing depends on more than `kind` (e.g. allowlists).
2213
+ *
2214
+ * The router always implements `quoteBatch` — even if some inner feeds lack
2215
+ * it, the router falls back to per-asset `quote()` calls within that group,
2216
+ * preserving request order across the full result.
2217
+ *
2218
+ * @example
2219
+ * ```ts
2220
+ * import { RoutingQuoteFeed } from '@livefolio/sdk';
2221
+ *
2222
+ * const feed = new RoutingQuoteFeed({ equity: alpacaQuotes, macro: fredQuotes });
2223
+ * const quotes = await feed.quoteBatch([aaplAsset, dgs10Asset, msftAsset]);
2224
+ * // quotes[0] is for AAPL, quotes[1] for DGS10, quotes[2] for MSFT — request order preserved.
2225
+ * ```
2226
+ */
2227
+ declare class RoutingQuoteFeed implements QuoteFeed {
2228
+ private readonly route;
2229
+ constructor(routes: RoutingQuoteFeedRouteMap | RoutingQuoteFeedRouteFn);
2230
+ quote(asset: Asset): Promise<Quote>;
2231
+ quoteBatch(assets: ReadonlyArray<Asset>): Promise<ReadonlyArray<Quote>>;
2232
+ private resolve;
2233
+ }
2234
+
2098
2235
  type PollingSchedule = {
2099
2236
  kind: 'interval';
2100
2237
  intervalMs: number;
@@ -2915,6 +3052,67 @@ declare function evaluateFeatureSpecs(specs: ReadonlyArray<TacticalFeatureSpec>,
2915
3052
  * ```
2916
3053
  */
2917
3054
  declare function withSynthetics(dataFeed: DataFeed, synthetics: ReadonlyArray<SyntheticAsset>): DataFeed;
3055
+ /**
3056
+ * Options for {@link withStreamingSynthetics}.
3057
+ */
3058
+ interface WithStreamingSyntheticsOptions {
3059
+ /**
3060
+ * Last known close per asset id, used to seed `prevUnderlyingClose` and
3061
+ * `prevSynthClose` so the first live tick of a synthetic continues smoothly
3062
+ * from the end of its historical series.
3063
+ *
3064
+ * Build it from a {@link BacktestResult}:
3065
+ * ```ts
3066
+ * const seedLastCloses = new Map<AssetId, number>();
3067
+ * for (const [id, bars] of history.bars) {
3068
+ * const last = bars.at(-1)?.close;
3069
+ * if (last !== undefined) seedLastCloses.set(id, last);
3070
+ * }
3071
+ * ```
3072
+ *
3073
+ * Without seeding, the first synthesized tick lands on the underlying's
3074
+ * price and produces a visible jump in live preview.
3075
+ */
3076
+ seedLastCloses: ReadonlyMap<AssetId, number>;
3077
+ }
3078
+ /**
3079
+ * Streaming-feed counterpart to {@link withSynthetics}. Wraps a
3080
+ * {@link StreamingDataFeed} so that subscriptions for synthetic asset ids
3081
+ * resolve to upstream subscriptions on the underlying, with each underlying
3082
+ * tick re-emitted as a synthesized tick on the synthetic's id using the same
3083
+ * `(1 + leverage × r) × (1 − expense/252)` formula as the historical wrapper.
3084
+ *
3085
+ * Behavior:
3086
+ * - Non-synthetic ids in the `assets` argument pass through to the inner feed
3087
+ * unchanged.
3088
+ * - Underlyings that aren't directly in `assets` but are needed by a synthetic
3089
+ * are subscribed silently — only the synthesized ticks are yielded back to
3090
+ * the caller for those.
3091
+ * - Underlyings that the caller _did_ ask for in `assets` are yielded both as
3092
+ * the raw underlying tick and as the synthesized tick(s).
3093
+ *
3094
+ * Throws at construction time if `synthetics` contains duplicate `id` values.
3095
+ *
3096
+ * @example
3097
+ * ```ts
3098
+ * import { withStreamingSynthetics, runLive } from '@livefolio/sdk';
3099
+ *
3100
+ * const seedLastCloses = new Map<AssetId, number>();
3101
+ * for (const [id, bars] of history.bars) {
3102
+ * const last = bars.at(-1)?.close;
3103
+ * if (last !== undefined) seedLastCloses.set(id, last);
3104
+ * }
3105
+ *
3106
+ * const liveFeed = withStreamingSynthetics(rawStreamingFeed, spec.synthetics ?? [], {
3107
+ * seedLastCloses,
3108
+ * });
3109
+ *
3110
+ * for await (const event of runLive({ strategy, history, dataFeed: liveFeed, executor, calendar })) {
3111
+ * // …
3112
+ * }
3113
+ * ```
3114
+ */
3115
+ declare function withStreamingSynthetics(inner: StreamingDataFeed, synthetics: ReadonlyArray<SyntheticAsset>, opts: WithStreamingSyntheticsOptions): StreamingDataFeed;
2918
3116
 
2919
3117
  /** Test-only: reset the once-per-process deprecation gate. */
2920
3118
  declare function _resetTacticalDeprecationWarningForTesting(): void;
@@ -3018,15 +3216,17 @@ type index$1_TacticalFeatureSpec = TacticalFeatureSpec;
3018
3216
  type index$1_TacticalFeatures = TacticalFeatures;
3019
3217
  type index$1_TacticalSpec = TacticalSpec;
3020
3218
  type index$1_Tolerance = Tolerance;
3219
+ type index$1_WithStreamingSyntheticsOptions = WithStreamingSyntheticsOptions;
3021
3220
  declare const index$1__resetTacticalDeprecationWarningForTesting: typeof _resetTacticalDeprecationWarningForTesting;
3022
3221
  declare const index$1_evaluateFeatureSpecs: typeof evaluateFeatureSpecs;
3023
3222
  declare const index$1_evaluateRuleTree: typeof evaluateRuleTree;
3024
3223
  declare const index$1_fromSpec: typeof fromSpec;
3025
3224
  declare const index$1_isRebalanceDay: typeof isRebalanceDay;
3026
3225
  declare const index$1_periodKey: typeof periodKey;
3226
+ declare const index$1_withStreamingSynthetics: typeof withStreamingSynthetics;
3027
3227
  declare const index$1_withSynthetics: typeof withSynthetics;
3028
3228
  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 };
3229
+ 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
3230
  }
3031
3231
 
3032
3232
  /**
@@ -3393,4 +3593,4 @@ declare function applyFills(portfolio: Portfolio, fills: ReadonlyArray<Fill>, or
3393
3593
  */
3394
3594
  declare function applyOrders(portfolio: Portfolio, orders: ReadonlyArray<Order>): Portfolio;
3395
3595
 
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 };
3596
+ 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
@@ -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;
@@ -2830,6 +2968,8 @@ export {
2830
2968
  NYSEExchangeCalendar,
2831
2969
  RoutingDataFeed,
2832
2970
  RoutingDataFeedError,
2971
+ RoutingQuoteFeed,
2972
+ RoutingQuoteFeedError,
2833
2973
  RoutingStreamingDataFeed,
2834
2974
  RoutingStreamingDataFeedError,
2835
2975
  applyFills,
@@ -2858,6 +2998,7 @@ export {
2858
2998
  sma,
2859
2999
  tactical_exports as tactical,
2860
3000
  volatility,
3001
+ withStreamingSynthetics,
2861
3002
  withSynthetics
2862
3003
  };
2863
3004
  //# sourceMappingURL=index.js.map