UNPKG

@ariva-mds/mds

Version:

Stock market data

483 lines (482 loc) 20.6 kB
import ReconnectingWebSocket from 'reconnecting-websocket'; import { MdsConnectionState } from './MdsConnectionState'; import { RunningRequest } from './RunningRequest'; import { Observable, map, retry, repeat } from 'rxjs'; import { MarketstateUpdate } from './MarketstateUpdate'; import { AfuCompanyProfileFromJSON, BidAskHistoryDataFromJSON, ListingdataWithIdAndQualityFromJSON, MarketdepthWithIdFromJSON, MarketstateResponseSearchFromJSON, MarketstateResponseSearchToJSON, MasterdataMergedSummaryWithIdFromJSON, PerformanceDataForTimeRangeFromJSON, SourcedataFromJSON, TickHistoryDataFromJSON, TimeseriesBidAskDataFromJSON, TimeseriesDataFromJSON, TopflopListFromJSON, TradetickerEventFromJSON, TradingtimeFromJSON, TradingtimeInfoReplyFromJSON } from '../models'; import { LruCache } from '../lrucache'; export class MdsConnection { /** * construct an MdsConnection */ constructor(websocketUrl, authdataCallback, options = { debug: false }) { this.waitingForReconnect = []; this.state = new MdsConnectionState(); this.waitingForAuthentification = []; this.nextGeneratedRequestId = 0; this.runningRequests = new Map(); this.isDebug = false; this.reconnectTimer = undefined; // ############################################################################################# /** * Sources * information about sources, access to other data that is linked to a source */ /** * gets the basic information about a source */ this.sourceCache = undefined; /** * gets the trading time for a source * */ this.tradingtimeCache = undefined; /** * gets the tradingtime for a source * */ this.tradingtimeAnalysisCache = undefined; // ############################################################################################# /** * Instruments * information about instruments, access to other data that is linked to an instrument */ /** * Returns one Instrument */ this.masterdataCache = undefined; this.authdataCallback = authdataCallback; this.isDebug = !!options.debug; this.websocket = new ReconnectingWebSocket(websocketUrl, [], options.wsOptions); const outer = this; // open-handler -> resend open requests from previous connection this.websocket.onopen = () => { outer.debug(`websocket open, resending ${outer.waitingForReconnect.length} items`); outer.state.connectionOpened(); for (const request of outer.waitingForReconnect) { outer.sendAsSoonAsAuthenticationPermits(request.request); } }; // close handler -> store open requests for resending on reconnect this.websocket.onclose = () => { outer.debug(`websocket closed, queuing ${outer.waitingForReconnect.length} items`); outer.state.connectionClosed(); outer.waitingForReconnect = Array.from(outer.runningRequests.values()); }; this.websocket.onmessage = (e) => { outer.processWebsocketMessageEvent(e); }; this.stayAuthenticated(); this.reconnectTimer = setInterval(() => { if (!outer.state.isTimedOut()) { outer.debug('websocket timed out, calling reconnect'); outer.state.forcedDisconnect(); outer.websocket.reconnect(); } }, 1000); } close() { if (this.reconnectTimer) { clearInterval(this.reconnectTimer); } this.websocket.close(); } /** * create a hearbeat observable that can be subscribed to */ heartbeat() { return this.observable({ heartbeat: {} }).pipe(map((x) => x.dataHeartbeat)); } /** * sets the priority for sources */ priority(sources) { return this.promise({ priority: { sources: sources } }); } source(sourceId) { if (this.sourceCache == undefined) { const outer = this; this.sourceCache = new LruCache(sourceId => outer.promise({ getSource: { sourceId: sourceId } }).then(state => SourcedataFromJSON(state.dataSource)), 3000); } return this.sourceCache.get(sourceId); } tradingtime(sourceId) { if (this.tradingtimeCache == undefined) { const outer = this; this.tradingtimeCache = new LruCache(sourceId => outer.promise({ getTradingtime: { sourceId: sourceId } }).then(state => TradingtimeFromJSON(state.dataTradingtime)), 3000); } return this.tradingtimeCache.get(sourceId); } tradingtimeAnalysis(sourceId, date) { if (this.tradingtimeAnalysisCache == undefined) { const outer = this; this.tradingtimeAnalysisCache = new LruCache(parameter => outer.promise({ getTradingtimeAnalysis: { sourceId: parameter.sourceId, date: parameter.date ? new Date(Date.UTC(parameter.date.getFullYear(), parameter.date.getMonth(), parameter.date.getDate())) : undefined } }).then(state => TradingtimeInfoReplyFromJSON(state.dataTradingtimeAnalysis)), 3000); } return this.tradingtimeAnalysisCache.get({ sourceId: sourceId, date: date }); } /** * Returns a list or stream of the current trades from this source. * */ tradeticker(sourceId, quality, onlyWithTurnover, preloadSize) { return this.observable({ subscribeSourceTradeticker: { sourceId: sourceId, preloadSize: preloadSize, onlyWithTurnover: onlyWithTurnover, quality: quality } }).pipe(map(state => TradetickerEventFromJSON(state.dataTradeticker))); } masterdata(instrumentId) { if (this.masterdataCache == undefined) { const outer = this; this.masterdataCache = new LruCache(instrumentId => outer.promise({ getInstrumentMasterdata: { instrumentId: instrumentId } }).then(state => MasterdataMergedSummaryWithIdFromJSON(state.dataMasterdata)), 3000); } return this.masterdataCache.get(instrumentId); } /** * Returns a list of all Listings for the Instrument * */ instrumentsListings(instrumentId, sourceId) { return this.observable({ listInstrumentListings: { instrumentId: instrumentId, sourceId: sourceId } }).pipe(map(state => ListingdataWithIdAndQualityFromJSON(state.dataListing))); } /** * Returns the AFU company profile */ afuCompanyProfile(instrumentId) { return this.promise({ getAfuCompanyProfile: { instrumentId: instrumentId } }).then(state => AfuCompanyProfileFromJSON(state.dataAfuCompanyProfile)); } // ############################################################################################# /** * Marketstates * information about listings, access to other data that is linked to a listing */ /** * create a MarketstateUpdate observable that can be subscribed to, contains both full state and delta for every marketstate */ marketstates(marketstateQueries, quality) { const full = new Map(); return this.marketstateUpdates(marketstateQueries, quality).pipe(map((update) => { let existingEntry = update.marketstateId ? full.get(update.marketstateId) : undefined; if (update.marketstateId) { if (existingEntry) { existingEntry = this.updateExistingMarketstateWithDeltaUpdate(existingEntry, update); } else { existingEntry = update; } full.set(update.marketstateId, existingEntry); } return new MarketstateUpdate(existingEntry, update); })); } /** * create a Marketstate for a snapshot request */ marketstateSnapshot(marketstateQueries, quality) { return this.observable({ listMarketstates: { marketstateQueries: marketstateQueries, quality: quality } }).pipe(map((x) => MarketstateResponseSearchFromJSON(x.dataMarketstate))); } /** * create a market state observable that can be subscribed to, contains only delta for every marketstate */ marketstateUpdates(marketstateQueries, quality) { return this.observable({ subscribeMarketstates: { marketstateQueries: marketstateQueries, quality: quality } }).pipe(map((x) => MarketstateResponseSearchFromJSON(x.dataMarketstate))); } /** * create a market state observable that can be subscribed to, contains only full state for every marketstate */ marketstatesStates(marketstateQueries, quality) { return this.marketstates(marketstateQueries, quality).pipe(map((x) => x.state)); } /** * Returns the Open-High-Low-Close Timeseries for the specified Instrument on the Source. * */ timeseries(marketstateId, resolution, start, end, cleanSplits, cleanDividends, cleanDistributions, cleanSubscriptions, quality) { return this.observable({ subscribeTimeseries: { resolution: resolution, marketstateId: marketstateId, start: start instanceof Date ? start?.toISOString() : start, end: end instanceof Date ? end?.toISOString() : end, cleanSplits: cleanSplits, cleanDividends: cleanDividends, cleanDistributions: cleanDistributions, cleanSubscriptions: cleanSubscriptions, quality: quality } }).pipe(map(state => TimeseriesDataFromJSON(state.dataTimeseries))); } /** * Returns the Open-High-Low-Close Timeseries for the specified Instrument on the Source. * */ timeseriesSnapshot(marketstateId, resolution, start, end, cleanSplits, cleanDividends, cleanDistributions, cleanSubscriptions, quality) { return this.observable({ listTimeseries: { resolution: resolution, marketstateId: marketstateId, start: start instanceof Date ? start?.toISOString() : start, end: end instanceof Date ? end?.toISOString() : end, cleanSplits: cleanSplits, cleanDividends: cleanDividends, cleanDistributions: cleanDistributions, cleanSubscriptions: cleanSubscriptions, quality: quality } }).pipe(map(state => TimeseriesDataFromJSON(state.dataTimeseries))); } /** * Returns the Orderbook for the specified Marketstate. * */ marketdepth(marketstateId, quality, orderBookVariant) { return this.observable({ subscribeMarketdepth: { marketstateId: marketstateId, quality: quality, orderBookVariant: orderBookVariant } }).pipe(map(state => MarketdepthWithIdFromJSON(state.dataMarketdepth))); } /** * Returns the performance for a specified listing of an instrument. */ performance(marketstateId, quality, ...timeRanges) { return this.observable({ listPerformance: { marketstateId: marketstateId, timeRange: timeRanges, quality: quality } }).pipe(map(state => PerformanceDataForTimeRangeFromJSON(state.dataPerformance))); } /** * Returns the bid/ask history for the specified Instrument on the Source. */ bidAskHistory(marketstateId, start, end, quality, offset, limit) { return this.observable({ listBidAskHistory: { marketstateId: marketstateId, start: start instanceof Date ? start?.toISOString() : start, end: end instanceof Date ? end?.toISOString() : end, quality: quality, offset: offset, limit: limit } }).pipe(map(state => { const bidaskHistory = BidAskHistoryDataFromJSON(state.dataBidAskHistory); bidaskHistory.totalResultCount = state.totalResultCount; return bidaskHistory; })); } /** * Returns the tick history for the specified Instrument on the Source. */ tickHistory(marketstateId, start, end, filterByVolume, quality, offset, limit) { return this.observable({ listTickHistory: { marketstateId: marketstateId, start: start instanceof Date ? start?.toISOString() : start, end: end instanceof Date ? end?.toISOString() : end, filterByVolume: filterByVolume, quality: quality, offset: offset, limit: limit } }).pipe(map(state => { const tickHistory = TickHistoryDataFromJSON(state.dataTickHistory); tickHistory.totalResultCount = state.totalResultCount; return tickHistory; })); } timeseriesBidAsk(marketstateId, resolution, start, end, quality) { return this.observable({ subscribeTimeseriesBidAsk: { resolution: resolution, marketstateId: marketstateId, start: start instanceof Date ? start?.toISOString() : start, end: end instanceof Date ? end?.toISOString() : end, quality: quality } }).pipe(map(state => TimeseriesBidAskDataFromJSON(state.dataTimeseriesBidAsk))); } /** * Returns the Open-High-Low-Close Timeseries for the specified Instrument on the Source. * */ timeseriesBidAskSnapshot(marketstateId, resolution, start, end, quality) { return this.observable({ listTimeseriesBidAsk: { resolution: resolution, marketstateId: marketstateId, start: start instanceof Date ? start?.toISOString() : start, end: end instanceof Date ? end?.toISOString() : end, quality: quality } }).pipe(map(state => TimeseriesBidAskDataFromJSON(state.dataTimeseriesBidAsk))); } /** * Returns the Orderbook for the specified Marketstate. * */ topflop(listId, sourcePriorities, quality, topSize, flopSize) { return this.observable({ subscribeListTopflop: { listId: listId, sourcePriorities: sourcePriorities, topSize: topSize, flopSize: flopSize, quality: quality } }).pipe(map(state => TopflopListFromJSON(state.dataTopflopList))); } // ############################################################################################# updateExistingMarketstateWithDeltaUpdate(existingEntry, update) { const existingJson = MarketstateResponseSearchToJSON(existingEntry); const updateJson = MarketstateResponseSearchToJSON(update); for (const key in updateJson) { if (updateJson[key]) { existingJson[key] = updateJson[key]; } } return MarketstateResponseSearchFromJSON(existingJson); } sendAsSoonAsAuthenticationPermits(request) { if (this.state.isAuthenticated || request.subscribeAuthentication) { this.websocket.send(JSON.stringify(request)); } else { this.waitingForAuthentification.push(request); } } stayAuthenticated() { const outer = this; new Observable((subscriber) => { this.authdataCallback().then((mdsAuthdata) => outer.observable({ 'subscribeAuthentication': mdsAuthdata })).then((observable) => observable.subscribe({ next(x) { const seconds = x?.dataAuthentication?.secondsToLive; if (seconds && seconds > 30) { subscriber.next(x); for (const requestParameter of outer.waitingForAuthentification) { outer.websocket.send(JSON.stringify(requestParameter)); } outer.waitingForAuthentification = []; outer.state.authenticationAccepted(); } }, complete() { outer.state.authenticationEnded(); subscriber.complete(); }, error(e) { outer.error(e); outer.state.authenticationEnded(); subscriber.complete(); } })).catch((error) => { outer.error(error); outer.state.authenticationEnded(); subscriber.error(error); }); }).pipe(retry({ delay: 2000 }), repeat({ delay: 2000 })).subscribe({ next(n) { outer.debug(`auth outer ${JSON.stringify(n)}`); }, error(e) { outer.error(`auth error ${JSON.stringify(e)}`); }, complete() { outer.debug('auth completed'); } }); } generateNextRequestId() { return 'request-' + this.nextGeneratedRequestId++; } observable(req) { const requestId = this.generateNextRequestId(); req.requestId = requestId; const outer = this; return new Observable((subscriber) => { outer.runningRequests.set(requestId, RunningRequest.withObservable(req, subscriber)); outer.sendAsSoonAsAuthenticationPermits(req); // Provide a way of canceling and disposing the resources return function unsubscribe() { const runningRequest = outer.runningRequests.get(requestId); if (runningRequest?.observableSubscriber) { outer.runningRequests.delete(requestId); outer.websocket.send(JSON.stringify({ cancel: { requestId: requestId } })); } }; }); } promise(req) { const requestId = this.generateNextRequestId(); req.requestId = requestId; const outer = this; return new Promise((resolve, reject) => { outer.runningRequests.set(requestId, RunningRequest.withPromise(req, resolve, reject)); outer.sendAsSoonAsAuthenticationPermits(req); }); } processWebsocketMessageEvent(e) { if (e?.data) { const msg = JSON.parse(e.data); this.debug('we received ' + JSON.stringify(msg)); this.state.messageReceived(); if (msg.requestId) { const request = this.runningRequests.get(msg.requestId); if (request) { if (request.promiseResolve && request.promiseReject) { if (msg.isError) { request.promiseReject(msg.errorMessage); } else if (msg.isComplete) { request.promiseResolve(msg); } else { request.promiseReject(msg.errorMessage); } this.runningRequests.delete(msg.requestId); } else if (request.observableSubscriber) { if (msg.isError) { this.runningRequests.delete(msg.requestId); request.observableSubscriber.error(msg.errorMessage); } else if (msg.isComplete) { this.runningRequests.delete(msg.requestId); request.observableSubscriber.complete(); } else { request.observableSubscriber.next(msg); } } } } } } debug(s) { if (this.isDebug) console.log(s); } error(s) { console.error(s); } }