spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
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