@ariva-mds/mds
Version:
Stock market data
483 lines (482 loc) • 20.6 kB
JavaScript
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);
}
}