SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
5.1 KB · 103 lines typescript
Raw Blame History
1import type { InstrumentHint, NormalizedObservation, ObservationType, RealtimeStatus, RightsStatus, TimestampTrust } from "@market-atlas/market-model";2import { cryptoHint, type TickerFields } from "./crypto.js";3import { conventionalPair, fxHint } from "../ecb-frankfurter/index.js";45export const FIAT = new Set(["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "NZD", "SGD", "HKD", "BRL", "MXN", "TRY", "ZAR", "ARS", "AED", "PLN", "SEK", "NOK", "DKK", "CZK", "HUF", "RON", "UAH", "INR", "KRW", "CNY"]);6export const STABLES = new Set(["USDT", "USDC", "USD1", "FDUSD", "TUSD", "DAI"]);78export interface PairPlan {9  /** Instrument hint to resolve/create. */10  hint: InstrumentHint;11  /** Nature of the LAST_PRICE value. */12  type: ObservationType;13  /** Multiply venue prices by -1 exponent: when true, canonical value = 1 / venue price. */14  invert: boolean;15  currency: string;16}1718/**19 * Decide what a venue pair *means* for Market Atlas:20 *  - fiat/fiat (Kraken EUR/USD, Bitstamp EURUSD)  → FOREX instrument, TRADE (a real fiat market)21 *  - stable/fiat or fiat/stable (USDT-EUR, EURUSDT) → FOREX instrument, STABLECOIN_PROXY (USDT ≈ USD)22 *  - anything else                                  → CRYPTO instrument, TRADE23 */24export function planPair(base: string, quote: string, exchangeId: string): PairPlan {25  const b = base.toUpperCase();26  const q = quote.toUpperCase();27  if (FIAT.has(b) && FIAT.has(q)) {28    const [cb, cq] = conventionalPair(b, q);29    return { hint: fxHint(cb, cq), type: "TRADE", invert: cb !== b, currency: cq };30  }31  const bStable = STABLES.has(b);32  const qStable = STABLES.has(q);33  if ((bStable && FIAT.has(q) && q !== "USD") || (qStable && FIAT.has(b) && b !== "USD")) {34    const fb = bStable ? "USD" : b;35    const fq = qStable ? "USD" : q;36    const [cb, cq] = conventionalPair(fb, fq);37    return { hint: fxHint(cb, cq), type: "STABLECOIN_PROXY", invert: cb !== fb, currency: cq };38  }39  return { hint: cryptoHint(b, q, exchangeId), type: "TRADE", invert: false, currency: q };40}4142const num = (v: unknown): number | null => {43  if (v == null || v === "") return null;44  const n = typeof v === "number" ? v : Number(v);45  return Number.isFinite(n) ? n : null;46};4748/**49 * Venue ticker → observations for whatever the pair means (crypto, live FX, stablecoin proxy).50 * Inverted pairs swap bid/ask and drop venue-specific volumes (they would be in the wrong unit).51 */52export function pairObservations(53  symbol: string,54  base: string,55  quote: string,56  exchangeId: string,57  f: TickerFields,58  meta: { sourceTimestamp: number | null; timestampTrust: TimestampTrust; sequence?: number | string | null; rightsStatus: RightsStatus; realtimeStatus: RealtimeStatus },59): NormalizedObservation[] {60  const plan = planPair(base, quote, exchangeId);61  const out: NormalizedObservation[] = [];62  const conv = (v: number) => (plan.invert ? 1 / v : v);63  const push = (field: NormalizedObservation["field"], v: unknown, type: ObservationType) => {64    const n = num(v);65    if (n == null) return;66    if (field !== "VOLUME" && field !== "BID_SIZE" && field !== "ASK_SIZE" && n <= 0) return;67    const value = field === "VOLUME" || field === "BID_SIZE" || field === "ASK_SIZE" ? n : Number(conv(n).toPrecision(10));68    out.push({ symbol, instrumentHint: plan.hint, field, value, currency: plan.currency, observationType: type, sourceTimestamp: meta.sourceTimestamp, timestampTrust: meta.timestampTrust, sequence: meta.sequence ?? null, rightsStatus: meta.rightsStatus, realtimeStatus: meta.realtimeStatus, meta: plan.type === "STABLECOIN_PROXY" ? { venue_pair: `${base}/${quote}`, proxy: true } : undefined });69  };70  const quoteType: ObservationType = plan.type === "STABLECOIN_PROXY" ? "STABLECOIN_PROXY" : "QUOTE";71  push("LAST_PRICE", f.last, plan.type);72  if (plan.invert) {73    // 1/x flips the order: venue ask becomes our bid.74    push("BID", f.ask, quoteType);75    push("ASK", f.bid, quoteType);76    push("HIGH", f.low, plan.type);77    push("LOW", f.high, plan.type);78    push("OPEN", f.open, plan.type);79  } else {80    push("BID", f.bid, quoteType);81    push("ASK", f.ask, quoteType);82    push("HIGH", f.high, plan.type);83    push("LOW", f.low, plan.type);84    push("OPEN", f.open, plan.type);85    push("BID_SIZE", f.bidSize, quoteType);86    push("ASK_SIZE", f.askSize, quoteType);87    if (plan.type !== "STABLECOIN_PROXY") push("VOLUME", f.volume, plan.type);88    if (plan.type === "TRADE" && plan.hint.assetClass === "CRYPTO") push("VWAP", f.vwap, plan.type);89  }90  return out;91}9293/** Split a venue symbol using an explicit separator or a list of known quote currencies. */94export function splitVenueSymbol(symbol: string, sep: string | null, quotes: string[] = ["USDT", "USDC", "USD", "EUR", "GBP", "JPY", "BTC", "ETH", "TRY", "BRL", "MXN", "ZAR", "ARS", "AUD", "SGD", "AED", "CAD", "CHF"]): [string, string] | null {95  if (sep) {96    const [b, q] = symbol.split(sep);97    return b && q ? [b.toUpperCase(), q.toUpperCase()] : null;98  }99  const s = symbol.toUpperCase();100  for (const q of quotes.sort((a, b) => b.length - a.length)) if (s.endsWith(q) && s.length > q.length) return [s.slice(0, -q.length), q];101  return null;102}103