import {z} from 'zod' export type GetApiV1QuotesArgs = {tickers?: string | undefined}; export type GetApiV1QuotesOptions = {baseUrl?: string | undefined, ApiKey?: string | undefined, fetch?: typeof fetch | undefined}; export const listingNotice = z.object({status: z.string().nullable().optional(), ticker: z.string().nullable().optional(), delistedOn: z.string().nullable().optional(), successorTicker: z.string().nullable().optional()}); export const liveQuoteDto = z.object({ticker: z.string().nullable().optional(), currency: z.string().nullable().optional(), price: z.number().nullable().optional(), bidPrice: z.number().nullable().optional(), askPrice: z.number().nullable().optional(), asOf: z.string().optional(), sessionDate: z.string().optional(), expectedSessionDate: z.string().optional(), stale: z.boolean().optional(), ageSeconds: z.number().int().optional(), delayed: z.boolean().optional()}); export const liveQuoteDtoListResponse = z.object({listingNotice: listingNotice.optional(), data: z.array(liveQuoteDto).nullable().optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Latest available intraday quotes for up to 25 active U.S. listings (consolidated SIP) and Canadian or Euronext listings named as MIC:TICKER in one call. Requires a paid plan: for U.S. listings Pro receives real-time trade and bid/ask and Plus the same fields 15 minutes delayed; for Canadian listings both receive the last price in the listing's currency, real time on XTSE and XTSX and 15 minutes behind on XCNQ and NEOE, and for Euronext listings the last trade 15 minutes behind. Free receives 403 because it covers end-of-day data only. Tickers with no reading are omitted rather than returned with a placeholder price, so compare the returned tickers against the ones you asked for. * * GET /v1/quotes */ export const getApiV1Quotes = async (args: GetApiV1QuotesArgs, options?: GetApiV1QuotesOptions) => { const {baseUrl = "https://api.equibles.com", fetch: fetchFn = fetch, ApiKey} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") if (args.tickers != null) query.append("tickers", String(args.tickers)) if (ApiKey !== undefined) headers.set('authorization', `Bearer ${ApiKey}`) const url = `${baseUrl.replace(/\/+$/, '')}/v1/quotes${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "GET", headers}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(liveQuoteDtoListResponse, await res.json()) };