hft-js
Version:
High-Frequency Trading in Node.js
287 lines • 12 kB
JavaScript
/*
* market.ts
*
* Copyright (c) 2025 Xiongfei Shi
*
* Author: Xiongfei Shi <xiongfei.shi(a)icloud.com>
* License: Apache-2.0
*
* https://github.com/shixiongfei/hft.js
*/
import ctp, {} from "napi-ctp";
import { CTPProvider } from "./provider.js";
import { isValidPrice, isValidVolume, parseSymbol } from "./utils.js";
import { calcTapeData } from "./tape.js";
export class Market extends CTPProvider {
marketApi;
recorder;
recorderSymbols;
tradingDay;
listener;
recordings;
symbols;
lastTicks;
subscribers;
constructor(flowMdPath, frontMdAddrs, options) {
super(flowMdPath, frontMdAddrs);
this.tradingDay = 0;
this.recordings = new Set();
this.symbols = new Map();
this.lastTicks = new Map();
this.subscribers = new Map();
if (options?.listener) {
this.listener = options.listener;
}
}
getRecorder() {
return this;
}
isRecorderReady() {
return !!this.recorder;
}
setRecorder(receiver, symbols) {
this.recorder = receiver;
this.recorderSymbols = symbols;
}
getLastTick(instrumentId) {
return this.lastTicks.get(instrumentId);
}
open(lifecycle) {
if (this.marketApi) {
return true;
}
this.marketApi = ctp.createMarketData(this.flowPath, this.frontAddrs);
this.marketApi.on(ctp.MarketDataEvent.FrontConnected, () => {
this._withRetry(() => this.marketApi.reqUserLogin());
});
let fired = false;
this.marketApi.on(ctp.MarketDataEvent.RspUserLogin, (_, options) => {
if (this._isErrorResp(lifecycle, options, "login-error")) {
return;
}
const tradingDay = parseInt(this.marketApi.getTradingDay());
if (this.tradingDay !== tradingDay) {
this.lastTicks.clear();
this.tradingDay = tradingDay;
}
const instrumentIds = new Set([
...Array.from(this.recordings),
...Object.keys(this.subscribers),
]);
if (instrumentIds.size > 0) {
this._withRetry(() => this.marketApi.subscribeMarketData(Array.from(instrumentIds)));
}
if (!fired) {
fired = true;
lifecycle.onOpen();
}
});
this.marketApi.on(ctp.MarketDataEvent.RspSubMarketData, (instrument) => {
if (!this.listener) {
return;
}
const symbol = this.symbols.get(instrument.InstrumentID);
this.listener.onSubscribed(symbol ?? instrument.InstrumentID);
});
this.marketApi.on(ctp.MarketDataEvent.RspUnSubMarketData, (instrument) => {
if (!this.listener) {
return;
}
const symbol = this.symbols.get(instrument.InstrumentID);
this.listener.onUnsubscribed(symbol ?? instrument.InstrumentID);
});
this.marketApi.on(ctp.MarketDataEvent.RtnDepthMarketData, (depthMarketData) => {
const instrumentId = depthMarketData.InstrumentID;
if (this.recorder && this.recordings.has(instrumentId)) {
this.recorder.onMarketData(depthMarketData);
}
const symbol = this.symbols.get(instrumentId);
if (!symbol) {
return;
}
const orderBook = {
asks: { price: [], volume: [] },
bids: { price: [], volume: [] },
};
if (isValidPrice(depthMarketData.AskPrice1) &&
isValidVolume(depthMarketData.AskVolume1)) {
orderBook.asks.price.push(depthMarketData.AskPrice1);
orderBook.asks.volume.push(depthMarketData.AskVolume1);
if (isValidPrice(depthMarketData.AskPrice2) &&
isValidVolume(depthMarketData.AskVolume2)) {
orderBook.asks.price.push(depthMarketData.AskPrice2);
orderBook.asks.volume.push(depthMarketData.AskVolume2);
if (isValidPrice(depthMarketData.AskPrice3) &&
isValidVolume(depthMarketData.AskVolume3)) {
orderBook.asks.price.push(depthMarketData.AskPrice3);
orderBook.asks.volume.push(depthMarketData.AskVolume3);
if (isValidPrice(depthMarketData.AskPrice4) &&
isValidVolume(depthMarketData.AskVolume4)) {
orderBook.asks.price.push(depthMarketData.AskPrice4);
orderBook.asks.volume.push(depthMarketData.AskVolume4);
if (isValidPrice(depthMarketData.AskPrice5) &&
isValidVolume(depthMarketData.AskVolume5)) {
orderBook.asks.price.push(depthMarketData.AskPrice5);
orderBook.asks.volume.push(depthMarketData.AskVolume5);
}
}
}
}
}
if (isValidPrice(depthMarketData.BidPrice1) &&
isValidVolume(depthMarketData.BidVolume1)) {
orderBook.bids.price.push(depthMarketData.BidPrice1);
orderBook.bids.volume.push(depthMarketData.BidVolume1);
if (isValidPrice(depthMarketData.BidPrice2) &&
isValidVolume(depthMarketData.BidVolume2)) {
orderBook.bids.price.push(depthMarketData.BidPrice2);
orderBook.bids.volume.push(depthMarketData.BidVolume2);
if (isValidPrice(depthMarketData.BidPrice3) &&
isValidVolume(depthMarketData.BidVolume3)) {
orderBook.bids.price.push(depthMarketData.BidPrice3);
orderBook.bids.volume.push(depthMarketData.BidVolume3);
if (isValidPrice(depthMarketData.BidPrice4) &&
isValidVolume(depthMarketData.BidVolume4)) {
orderBook.bids.price.push(depthMarketData.BidPrice4);
orderBook.bids.volume.push(depthMarketData.BidVolume4);
if (isValidPrice(depthMarketData.BidPrice5) &&
isValidVolume(depthMarketData.BidVolume5)) {
orderBook.bids.price.push(depthMarketData.BidPrice5);
orderBook.bids.volume.push(depthMarketData.BidVolume5);
}
}
}
}
}
const time = this._parseTime(depthMarketData.UpdateTime);
const tick = Object.freeze({
symbol: symbol,
date: parseInt(depthMarketData.ActionDay),
time: time + depthMarketData.UpdateMillisec / 1000,
tradingDay: parseInt(depthMarketData.TradingDay),
preOpenInterest: depthMarketData.PreOpenInterest,
preClose: depthMarketData.PreClosePrice,
openInterest: depthMarketData.OpenInterest,
openPrice: depthMarketData.OpenPrice,
highPrice: depthMarketData.HighestPrice,
lowPrice: depthMarketData.LowestPrice,
lastPrice: depthMarketData.LastPrice,
volume: depthMarketData.Volume,
amount: depthMarketData.Turnover,
limits: Object.freeze({
upper: depthMarketData.UpperLimitPrice,
lower: depthMarketData.LowerLimitPrice,
}),
bandings: Object.freeze({
upper: depthMarketData.BandingUpperPrice,
lower: depthMarketData.BandingLowerPrice,
}),
orderBook: Object.freeze(orderBook),
});
const lastTick = this.lastTicks.get(instrumentId);
const receivers = this.subscribers.get(instrumentId);
this.lastTicks.set(instrumentId, tick);
if (receivers && receivers.length > 0) {
const tape = calcTapeData(tick, lastTick);
receivers.forEach((receiver) => receiver.onTick(tick, tape));
}
});
return true;
}
close(lifecycle) {
if (!this.marketApi) {
return;
}
this.marketApi.close();
this.marketApi = undefined;
lifecycle.onClose();
}
startRecorder(instrument) {
if (!this.recorderSymbols) {
return;
}
const symbols = this.recorderSymbols(instrument);
const instrumentIds = new Set();
symbols.forEach((symbol) => {
const [instrumentId] = parseSymbol(symbol);
if (this.recordings.has(instrumentId)) {
return;
}
this.recordings.add(instrumentId);
if (!this.subscribers.has(instrumentId)) {
this.symbols.set(instrumentId, symbol);
instrumentIds.add(instrumentId);
}
});
if (instrumentIds.size > 0) {
this._withRetry(() => this.marketApi?.subscribeMarketData(Array.from(instrumentIds)));
}
}
stopRecorder() {
if (this.recordings.size === 0) {
return;
}
const instrumentIds = new Set();
this.recordings.forEach((instrumentId) => {
if (!this.subscribers.has(instrumentId)) {
this.symbols.delete(instrumentId);
instrumentIds.add(instrumentId);
}
});
this.recordings.clear();
if (instrumentIds.size > 0) {
this._withRetry(() => this.marketApi?.unsubscribeMarketData(Array.from(instrumentIds)));
}
}
subscribe(symbols, receiver) {
const instrumentIds = new Set();
symbols.forEach((symbol) => {
const [instrumentId] = parseSymbol(symbol);
const receivers = this.subscribers.get(instrumentId);
if (receivers) {
if (!receivers.includes(receiver)) {
receivers.push(receiver);
}
}
else {
this.subscribers.set(instrumentId, [receiver]);
if (!this.recordings.has(instrumentId)) {
this.symbols.set(instrumentId, symbol);
instrumentIds.add(instrumentId);
}
}
});
if (instrumentIds.size > 0) {
this._withRetry(() => this.marketApi?.subscribeMarketData(Array.from(instrumentIds)));
}
}
unsubscribe(symbols, receiver) {
const instrumentIds = new Set();
symbols.forEach((symbol) => {
const [instrumentId] = parseSymbol(symbol);
const receivers = this.subscribers.get(instrumentId);
if (!receivers) {
return;
}
if (receivers.length > 0) {
const index = receivers.indexOf(receiver);
if (index < 0) {
return;
}
receivers.splice(index, 1);
}
if (receivers.length === 0) {
this.subscribers.delete(instrumentId);
if (!this.recordings.has(instrumentId)) {
this.symbols.delete(instrumentId);
instrumentIds.add(instrumentId);
}
}
});
if (instrumentIds.size > 0) {
this._withRetry(() => this.marketApi?.unsubscribeMarketData(Array.from(instrumentIds)));
}
}
}
export const createMarket = (flowMdPath, frontMdAddrs, options) => new Market(flowMdPath, frontMdAddrs, options);
//# sourceMappingURL=market.js.map