import { Client } from "./client"; import { Region } from "./collections"; import { v4 as uuidv4 } from "uuid"; import { LivePriceFeed } from "./live-price-web-socket"; export type MessageType = "pr" | "state_change" | "heartbeat" | "ob"; // Stream Message Wrapper - v2 formatter wraps all messages in this structure export interface StreamMessage { t: MessageType; d: T; } export interface BISTStockPriceData { s: string; // Symbol ch: number; // DailyPercentChange p: number; // ClosePrice d: number; // Date } export interface USStockPriceData { s: string; // Symbol p: number; // Price pc: number; // PercentChange ac: number; // AmountChange d: number; // Date } export type BISTStockStreamData = StreamMessage; export type USStockStreamData = StreamMessage; export type BISTBidAskStreamData = StreamMessage; export enum OrderbookLevelSide { Bid = "bid", Ask = "ask" } export interface OrderbookLevel { level: number; vol: number; orders: number; p: number; side: OrderbookLevelSide; } export interface BISTBidAskData { d: number; s: string; ask: number; bid: number; } export interface OrderbookDeletedLevel { level: number; side: OrderbookLevelSide; } export interface OrderbookLiveData { updated?: OrderbookLevel[]; deleted?: OrderbookDeletedLevel[]; symbol: string; } export enum PriceDataType { Live = "live", Delayed = "delayed", Orderbook = "orderbook", Bids = "bids", } interface WebSocketUrlResponse { url: string; } interface WebSocketUsageResponse { externalUserID: string; firstConnectionTime: Date; uniqueDeviceCount: number; } export interface SendWebsocketEventRequest { externalUserID?: string; event: Record; transient?: boolean; broadCastToAll?: boolean; } export interface ILivePriceClient { close(): void; receive(): AsyncIterable; subscribe(symbols: string[]): Promise; } class LivePriceClientImpl implements ILivePriceClient { private client: Client; private region: Region; private dataType: PriceDataType; private symbols: string[] = []; private closed = false; private currentStream: AsyncIterable | null = null; private cancelFn: (() => void) | null = null; constructor(client: Client, region: Region, dataType: PriceDataType) { this.client = client; this.region = region; this.dataType = dataType; } close(): void { if (this.closed) return; this.closed = true; if (this.cancelFn) { this.cancelFn(); } } receive(): AsyncIterable { if (!this.currentStream) { throw new Error("Not subscribed. Call subscribe() first."); } return this.currentStream; } async subscribe(symbols: string[]): Promise { // Cancel existing connection if (this.cancelFn) { this.cancelFn(); } const streamId = uuidv4(); let url: string; switch (this.dataType) { case PriceDataType.Live: url = `${ this.client["baseUrl"] }/api/v2/stock/price/live?filter=${symbols.join(",")}®ion=${ this.region }&stream=${streamId}`; break; case PriceDataType.Delayed: url = `${ this.client["baseUrl"] }/api/v1/stock/price/delayed?filter=${symbols.join(",")}®ion=${ this.region }&stream=${streamId}`; break; case PriceDataType.Orderbook: url = `${ this.client["baseUrl"] }/api/v1/stock/orderbook/live?filter=${symbols.join(",")}®ion=${ this.region }&stream=${streamId}`; break; case PriceDataType.Bids: url = `${ this.client["baseUrl"] }/api/v1/stock/price/bids?filter=${symbols.join(",")}®ion=${ this.region }&stream=${streamId}`; break; } const { events, cancel } = this.client.sendSSERequest(url); this.currentStream = events; this.cancelFn = cancel; this.symbols = symbols; this.closed = false; } } export function getLivePrice( client: Client, symbols: string[], region: Region ): ILivePriceClient { if (!client) { throw new Error("Client cannot be null"); } const livePriceClient = new LivePriceClientImpl(client, region, PriceDataType.Live); livePriceClient.subscribe(symbols).catch((error) => { console.error("Failed to initialize live price client", error); }); return livePriceClient; } export function getDelayedPrice( client: Client, symbols: string[], region: Region, ): ILivePriceClient { if (!client) { throw new Error("Client cannot be null"); } const livePriceClient = new LivePriceClientImpl(client, region, PriceDataType.Delayed); livePriceClient.subscribe(symbols).catch((error) => { console.error("Failed to initialize live price client", error); }); return livePriceClient; } function getOrderbook( client: Client, symbols: string[], region: Region, ): ILivePriceClient { if (!client) { throw new Error("Client cannot be null"); } const orderbookClient = new LivePriceClientImpl(client, region, PriceDataType.Orderbook); orderbookClient.subscribe(symbols).catch((error) => { console.error("Failed to initialize orderbook client", error); }); return orderbookClient; } function getBidAsk( client: Client, symbols: string[], region: Region ): ILivePriceClient { if (!client) { throw new Error("Client cannot be null"); } const bidAskClient = new LivePriceClientImpl( client, region, PriceDataType.Bids ); bidAskClient.subscribe(symbols).catch((error) => { console.error("Failed to initialize bist bid ask client", error); }); return bidAskClient; } export function getLivePriceForBIST( client: Client, symbols: string[] ): ILivePriceClient { return getLivePrice(client, symbols, Region.Tr); } export function getLivePriceForUS( client: Client, symbols: string[] ): ILivePriceClient { return getLivePrice(client, symbols, Region.Us); } export function getDelayedPriceForBIST( client: Client, symbols: string[] ): ILivePriceClient { return getDelayedPrice(client, symbols, Region.Tr); } export function getOrderbookForBIST( client: Client, symbols: string[] ): ILivePriceClient { return getOrderbook(client, symbols, Region.Tr); } export function getBidAskForBIST( client: Client, symbols: string[] ): ILivePriceClient { return getBidAsk(client, symbols, Region.Tr); } export class LivePriceClient extends Client { getLivePriceForBIST(symbols: string[]): ILivePriceClient { return getLivePriceForBIST(this, symbols); } getLivePriceForUS(symbols: string[]): ILivePriceClient { return getLivePriceForUS(this, symbols); } getDelayedPriceForBIST( symbols: string[], ): ILivePriceClient { return getDelayedPriceForBIST(this, symbols); } getOrderbookForBIST(symbols: string[]): ILivePriceClient { return getOrderbookForBIST(this, symbols); } getBidAskForBIST(symbols: string[]): ILivePriceClient { return getBidAskForBIST(this, symbols); } async getClientWebsocketUrl( externalUserId: string, feeds: LivePriceFeed[] ): Promise { const response = await this.sendRequest({ method: "POST", url: "/api/v2/ws/url", data: { externalUserId, feeds }, }); return response.url; } async getWebsocketUsageForMonth( month: number, year: number, feedType: LivePriceFeed, ): Promise { return this.sendRequest({ method: "GET", url: "/api/v1/ws/report", params: { month, year, feedType }, }); } async sendWebsocketEvent( request: SendWebsocketEventRequest ): Promise { await this.sendRequest({ method: "POST", url: "/api/v1/ws/event", data: request, }); } async revokeWebsocketConnection(id: string): Promise { await this.sendRequest({ method: "POST", url: `/api/v1/ws/user/revoke/${id}`, }); } }