UNPKG

hft-js

Version:

High-Frequency Trading in Node.js

1,241 lines (1,240 loc) 52.1 kB
/* * trader.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 Denque from "denque"; import ctp, {} from "napi-ctp"; import { CTPProvider } from "./provider.js"; import { isValidPrice, parseSymbol } from "./utils.js"; export class Trader extends CTPProvider { traderApi; tradingDay; frontId; sessionId; orderRef; accountsQueryTime; positionDetailsChanged; fastQueryLastTick; userInfo; receivers; accounts; positionDetails; instruments; positions; orders; trades; marginRates; commRates; placeOrders; cancelOrders; marketOrdersQueue; priceLimit; orderStatistics; marginRatesQueue; commRatesQueue; accountsQueue; positionDetailsQueue; constructor(flowTdPath, frontTdAddrs, userInfo, options) { super(flowTdPath, frontTdAddrs); this.tradingDay = 0; this.frontId = 0; this.sessionId = 0; this.orderRef = 0; this.accountsQueryTime = 0; this.positionDetailsChanged = true; this.userInfo = userInfo; this.receivers = []; this.accounts = []; this.positionDetails = []; this.instruments = new Map(); this.positions = new Map(); this.orders = new Map(); this.trades = new Map(); this.marginRates = new Map(); this.commRates = new Map(); this.placeOrders = new Map(); this.cancelOrders = new Map(); this.marketOrdersQueue = new Map(); this.priceLimit = new Map(); this.orderStatistics = new Map(); this.marginRatesQueue = new Denque(); this.commRatesQueue = new Denque(); this.accountsQueue = new Denque(); this.positionDetailsQueue = new Denque(); if (options?.fastQueryLastTick) { this.fastQueryLastTick = options.fastQueryLastTick; } } open(lifecycle) { if (this.traderApi) { return true; } this.traderApi = ctp.createTrader(this.flowPath, this.frontAddrs); this.traderApi.on(ctp.TraderEvent.FrontConnected, () => { this._withRetry(() => this.traderApi.reqAuthenticate(this.userInfo)); }); this.traderApi.on(ctp.TraderEvent.FrontDisconnected, () => { this._clearAllMarketOrders(); this.placeOrders.clear(); this.cancelOrders.clear(); }); this.traderApi.on(ctp.TraderEvent.RspAuthenticate, (_, options) => { if (this._isErrorResp(lifecycle, options, "login-error")) { return; } this._withRetry(() => this.traderApi.reqUserLogin(this.userInfo)); }); this.traderApi.on(ctp.TraderEvent.RspUserLogin, (rspUserLogin, options) => { if (this._isErrorResp(lifecycle, options, "login-error")) { return; } this.frontId = rspUserLogin.FrontID; this.sessionId = rspUserLogin.SessionID; this.orderRef = parseInt(rspUserLogin.MaxOrderRef); const tradingDay = parseInt(this.traderApi.getTradingDay()); if (this.tradingDay !== tradingDay) { this.marginRates.clear(); this.commRates.clear(); this.orderStatistics.clear(); this.priceLimit.clear(); this.tradingDay = tradingDay; } this._withRetry(() => this.traderApi.reqSettlementInfoConfirm(this.userInfo)); }); this.traderApi.on(ctp.TraderEvent.RspSettlementInfoConfirm, (_, options) => { if (this._isErrorResp(lifecycle, options, "login-error")) { return; } this.orders.clear(); this._withRetry(() => this.traderApi.reqQryOrder(this.userInfo)); }); this.traderApi.on(ctp.TraderEvent.RspQryOrder, (order, options) => { if (this._isErrorResp(lifecycle, options, "query-order-error")) { return; } if (order) { const orderId = this._calcOrderId(order); this.orders.set(orderId, order); } if (options.isLast) { this.trades.clear(); this._withRetry(() => this.traderApi.reqQryTrade(this.userInfo)); } }); this.traderApi.on(ctp.TraderEvent.RspQryTrade, (trade, options) => { if (this._isErrorResp(lifecycle, options, "query-trade-error")) { return; } if (trade) { const orderId = this._calcOrderId(trade); const trades = this.trades.get(orderId); if (trades) { trades.push(trade); } else { this.trades.set(orderId, [trade]); } } if (options.isLast) { this.instruments.clear(); this._withRetry(() => this.traderApi.reqQryInstrument()); } }); this.traderApi.on(ctp.TraderEvent.RspQryInstrument, (instrument, options) => { if (this._isErrorResp(lifecycle, options, "query-instrument-error")) { return; } if (instrument) { if (instrument.ProductClass === ctp.ProductClassType.Futures || instrument.ProductClass === ctp.ProductClassType.Options) { this.instruments.set(instrument.InstrumentID, instrument); } } if (options.isLast) { this.positions.clear(); this._withRetry(() => this.traderApi.reqQryInvestorPosition(this.userInfo)); } }); let fired = false; this.traderApi.on(ctp.TraderEvent.RspQryInvestorPosition, (position, options) => { if (this._isErrorResp(lifecycle, options, "query-positions-error")) { return; } if (position) { const symbol = this._toSymbol(position.InstrumentID); if (symbol) { let posInfo = this._ensurePositionInfo(symbol); const ExchangeSH = ["SHFE", "INE"]; switch (position.PosiDirection) { case ctp.PosiDirectionType.Long: if (position.PositionDate === ctp.PositionDateType.Today) { if (ExchangeSH.includes(position.ExchangeID)) { posInfo.today.long.position += position.TodayPosition; } else { posInfo.today.long.position += position.Position; } } else { posInfo.history.long.position += position.Position - position.TodayPosition; } break; case ctp.PosiDirectionType.Short: if (position.PositionDate === ctp.PositionDateType.Today) { if (ExchangeSH.includes(position.ExchangeID)) { posInfo.today.short.position += position.TodayPosition; } else { posInfo.today.short.position += position.Position; } } else { posInfo.history.short.position += position.Position - position.TodayPosition; } break; } } } if (options.isLast) { if (!fired) { fired = true; lifecycle.onOpen(); } if (this.accountsQueue.size() > 0) { this._withRetry(() => this.traderApi.reqQryTradingAccount(this.userInfo)); } if (this.positionDetailsQueue.size() > 0) { this._withRetry(() => this.traderApi.reqQryInvestorPositionDetail(this.userInfo)); } this._processMarginRatesQueue(); this._processCommissionRatesQueue(); } }); this.traderApi.on(ctp.TraderEvent.RtnOrder, (order) => { const orderId = this._calcOrderId(order); const current = this.orders.get(orderId); if (current) { if (order.OrderSubmitStatus === current.OrderSubmitStatus && order.OrderStatus === current.OrderStatus) { return; } } this.orders.set(orderId, order); switch (this._calcOrderStatus(order)) { case "submitted": { const orderData = this._toOrderData(order); const symbol = this._toSymbol(order.InstrumentID); if (symbol) { if (orderData.offset === "open") { this._recordPending(symbol, orderData.side, orderData.offset, orderData.volume); } else { this._freezePosition(symbol, orderData.side, orderData.offset, orderData.volume); } const statistic = this._ensureOrderStatistic(symbol); statistic.entrusts += 1; } this.receivers.forEach((receiver) => receiver.onEntrust(orderData)); } break; case "filled": { const symbol = this._toSymbol(order.InstrumentID); if (symbol) { const statistic = this._ensureOrderStatistic(symbol); statistic.filleds += 1; } } break; case "canceled": { const orderData = this._toOrderData(order); const symbol = this._toSymbol(order.InstrumentID); if (symbol) { if (orderData.offset === "open") { this._recoverPending(symbol, orderData.side, orderData.offset, orderData.volume); } else { this._unfreezePosition(symbol, orderData.side, orderData.offset, orderData.volume); } const statistic = this._ensureOrderStatistic(symbol); statistic.cancels += 1; } this.receivers.forEach((receiver) => receiver.onCancel(orderData)); } break; case "rejected": { const orderData = this._toOrderData(order); const symbol = this._toSymbol(order.InstrumentID); if (symbol) { const statistic = this._ensureOrderStatistic(symbol); statistic.rejects += 1; } this.receivers.forEach((receiver) => receiver.onReject(orderData)); } break; } }); this.traderApi.on(ctp.TraderEvent.RtnTrade, (trade) => { const orderId = this._calcOrderId(trade); const trades = this.trades.get(orderId); if (trades) { trades.push(trade); } else { this.trades.set(orderId, [trade]); } this.positionDetailsChanged = true; const order = this.orders.get(orderId); if (order) { const orderData = this._toOrderData(order); const tradeData = this._toTradeData(trade); const symbol = this._toSymbol(order.InstrumentID); if (symbol) { this._calcPosition(symbol, orderData.side, orderData.offset, tradeData.volume); } this.receivers.forEach((receiver) => receiver.onTrade(orderData, tradeData)); } }); this.traderApi.on(ctp.TraderEvent.RspQryInstrumentMarginRate, (marginRate, options) => { const query = this.marginRatesQueue.shift(); if (this._isErrorResp(lifecycle, options, "query-margin-rate-error")) { if (query) { query.receiver.onMarginRate(undefined); } return; } if (marginRate) { this.marginRates.set(marginRate.InstrumentID, marginRate); if (query) { query.receiver.onMarginRate(this._toMarginRate(query.symbol, marginRate)); } } this._processMarginRatesQueue(); }); this.traderApi.on(ctp.TraderEvent.RspQryInstrumentCommissionRate, (commRate, options) => { const query = this.commRatesQueue.shift(); if (this._isErrorResp(lifecycle, options, "query-commission-rate-error")) { if (query) { query.receiver.onCommissionRate(undefined); } return; } if (commRate) { this.commRates.set(commRate.InstrumentID, commRate); if (query) { query.receiver.onCommissionRate(this._toCommissionRate(query.symbol, commRate)); } } this._processCommissionRatesQueue(); }); this.traderApi.on(ctp.TraderEvent.RspQryTradingAccount, (account, options) => { if (this._isErrorResp(lifecycle, options, "query-accounts-error")) { const receivers = this.accountsQueue.toArray(); receivers.forEach((receiver) => receiver.onTradingAccounts(undefined)); this.accountsQueue.clear(); return; } if (account) { this.accounts.push(account); } if (options.isLast) { const accounts = this.accounts.map(this._toTradingAccount, this); const receivers = this.accountsQueue.toArray(); receivers.forEach((receiver) => receiver.onTradingAccounts(accounts)); this.accountsQueue.clear(); this.accountsQueryTime = Date.now(); } }); this.traderApi.on(ctp.TraderEvent.RspQryInvestorPositionDetail, (positionDetail, options) => { if (this._isErrorResp(lifecycle, options, "query-position-details-error")) { const receivers = this.positionDetailsQueue.toArray(); receivers.forEach((receiver) => receiver.onPositionDetails(undefined)); this.positionDetailsQueue.clear(); return; } if (positionDetail) { this.positionDetails.push(positionDetail); } if (options.isLast) { const positionDetails = this.positionDetails.map(this._toPositionDetail, this); const receivers = this.positionDetailsQueue.toArray(); this.positionDetailsChanged = false; receivers.forEach((receiver) => receiver.onPositionDetails(positionDetails)); this.positionDetailsQueue.clear(); } }); this.traderApi.on(ctp.TraderEvent.RspOrderInsert, (order, options) => { if (options.rspInfo && order && options.requestId && options.isLast) { const receiver = this.placeOrders.get(options.requestId); if (receiver) { this.placeOrders.delete(options.requestId); receiver.onPlaceOrderError(`${options.rspInfo.ErrorID}: ${options.rspInfo.ErrorMsg}`); } } }); this.traderApi.on(ctp.TraderEvent.RspOrderAction, (order, options) => { if (options.rspInfo && order && options.requestId && options.isLast) { const receiver = this.cancelOrders.get(options.requestId); if (receiver) { this.cancelOrders.delete(options.requestId); receiver.onCancelOrderError(`${options.rspInfo.ErrorID}: ${options.rspInfo.ErrorMsg}`); } } }); this.traderApi.on(ctp.TraderEvent.RspQryDepthMarketData, (depthMarketData, options) => { if (this._isErrorResp(lifecycle, options, "query-depth-market-data-error")) { this._clearAllMarketOrders(); return; } const isLimitPrice = !isValidPrice(depthMarketData.BandingUpperPrice) || !isValidPrice(depthMarketData.BandingLowerPrice); if (isLimitPrice) { this.priceLimit.set(depthMarketData.InstrumentID, { upper: depthMarketData.UpperLimitPrice, lower: depthMarketData.LowerLimitPrice, }); } const queue = this.marketOrdersQueue.get(depthMarketData.InstrumentID); if (!queue) { return; } if (!queue.isEmpty()) { const upperPrice = isLimitPrice ? depthMarketData.UpperLimitPrice : depthMarketData.BandingUpperPrice; const lowerPrice = isLimitPrice ? depthMarketData.LowerLimitPrice : depthMarketData.BandingLowerPrice; const orders = queue.toArray(); orders.forEach((order) => { switch (order.side) { case "long": this._placeLimitOrder(order.symbol, order.offset, order.side, order.volume, upperPrice, order.receiver); break; case "short": this._placeLimitOrder(order.symbol, order.offset, order.side, order.volume, lowerPrice, order.receiver); break; } }); } this.marketOrdersQueue.delete(depthMarketData.InstrumentID); }); return true; } close(lifecycle) { if (!this.traderApi) { return; } this.traderApi.close(); this.traderApi = undefined; lifecycle.onClose(); } addOrderReceiver(receiver) { if (!this.receivers.includes(receiver)) { this.receivers.push(receiver); } } removeOrderReceiver(receiver) { const index = this.receivers.indexOf(receiver); if (index < 0) { return; } this.receivers.splice(index, 1); } getTradingDay() { return this.tradingDay; } getOrderStatistics() { const statistics = Array.from(this.orderStatistics.values()); return statistics.map((stat) => Object.freeze({ ...stat })); } getOrderStatistic(symbol) { const statistic = this.orderStatistics.get(symbol); if (!statistic) { return Object.freeze({ symbol: symbol, places: 0, entrusts: 0, filleds: 0, cancels: 0, rejects: 0, }); } return Object.freeze({ ...statistic }); } queryCommissionRate(symbol, receiver) { const [instrumentId] = parseSymbol(symbol); const commRate = this.commRates.get(instrumentId); if (commRate) { receiver.onCommissionRate(this._toCommissionRate(symbol, commRate)); return; } this.commRatesQueue.push({ symbol, receiver }); if (this.commRatesQueue.size() === 1) { this._withRetry(() => this.traderApi?.reqQryInstrumentCommissionRate({ ...this.userInfo, InstrumentID: instrumentId, })); } } queryMarginRate(symbol, receiver) { const [instrumentId] = parseSymbol(symbol); const marginRate = this.marginRates.get(instrumentId); if (marginRate) { receiver.onMarginRate(this._toMarginRate(symbol, marginRate)); return; } this.marginRatesQueue.push({ symbol, receiver }); if (this.marginRatesQueue.size() === 1) { this._withRetry(() => this.traderApi?.reqQryInstrumentMarginRate({ ...this.userInfo, HedgeFlag: ctp.HedgeFlagType.Speculation, InstrumentID: instrumentId, })); } } queryInstrument(symbol, receiver) { const [instrumentId, exchangeId] = parseSymbol(symbol); const instrument = this.instruments.get(instrumentId); receiver.onInstrument(instrument && instrument.ExchangeID === exchangeId ? this._toInstrumentData(instrument) : undefined); } queryPosition(symbol, receiver) { const position = this.positions.get(symbol); if (position) { receiver.onPosition(this._toPositionData(position)); return; } const [instrumentId] = parseSymbol(symbol); if (!this.instruments.has(instrumentId)) { receiver.onPosition(undefined); return; } receiver.onPosition(Object.freeze({ symbol: symbol, today: Object.freeze({ long: Object.freeze({ position: 0, frozen: 0 }), short: Object.freeze({ position: 0, frozen: 0 }), }), history: Object.freeze({ long: Object.freeze({ position: 0, frozen: 0 }), short: Object.freeze({ position: 0, frozen: 0 }), }), pending: Object.freeze({ long: 0, short: 0 }), })); } queryInstruments(receiver, type) { const instruments = Array.from(this.instruments.values()); switch (type) { case "futures": receiver.onInstruments(instruments .filter((instrument) => instrument.ProductClass === ctp.ProductClassType.Futures) .map(this._toInstrumentData, this)); break; case "options": receiver.onInstruments(instruments .filter((instrument) => instrument.ProductClass === ctp.ProductClassType.Options) .map(this._toInstrumentData, this)); break; default: receiver.onInstruments(instruments.map(this._toInstrumentData, this)); break; } } queryTradingAccounts(receiver) { if (this.accountsQueue.size() > 0) { this.accountsQueue.push(receiver); return; } const elapsed = Date.now() - this.accountsQueryTime; if (elapsed < 3000) { receiver.onTradingAccounts(this.accounts.map(this._toTradingAccount, this)); return; } this.accountsQueue.push(receiver); this.accounts.splice(0, this.accounts.length); this._withRetry(() => this.traderApi?.reqQryTradingAccount(this.userInfo)); } queryPositionDetails(receiver) { if (this.positionDetailsQueue.size() > 0) { this.positionDetailsQueue.push(receiver); return; } if (!this.positionDetailsChanged) { receiver.onPositionDetails(this.positionDetails.map(this._toPositionDetail, this)); return; } this.positionDetailsQueue.push(receiver); this.positionDetails.splice(0, this.positionDetails.length); this._withRetry(() => this.traderApi?.reqQryInvestorPositionDetail(this.userInfo)); } queryPositions(receiver) { const positions = []; this.positions.forEach((position) => positions.push(this._toPositionData(position))); receiver.onPositions(positions); } queryOrders(receiver) { const orders = []; this.orders.forEach((order) => { orders.push(this._toOrderData(order)); }); receiver.onOrders(orders); } _placeLimitOrder(symbol, offset, side, volume, price, receiver) { const [instrumentId, exchangeId] = parseSymbol(symbol); const instrument = this.instruments.get(instrumentId); if (!instrument) { receiver.onPlaceOrderError("Instrument Not Found"); return; } if (exchangeId !== instrument.ExchangeID) { receiver.onPlaceOrderError("Exchange Id Error"); return; } let orderRef = 0; this._withRetry(() => { orderRef = ++this.orderRef; return this.traderApi?.reqOrderInsert({ ...this.userInfo, OrderRef: `${orderRef}`, InstrumentID: instrumentId, ExchangeID: instrument.ExchangeID, LimitPrice: price, VolumeTotalOriginal: volume, VolumeCondition: ctp.VolumeConditionType.AV, TimeCondition: ctp.TimeConditionType.GFD, Direction: this._toDirection(side), OrderPriceType: ctp.OrderPriceTypeType.LimitPrice, CombOffsetFlag: this._toOffsetFlag(offset), CombHedgeFlag: ctp.HedgeFlagType.Speculation, ContingentCondition: ctp.ContingentConditionType.Immediately, ForceCloseReason: ctp.ForceCloseReasonType.NotForceClose, IsAutoSuspend: 0, UserForceClose: 0, }); }).then((requestId) => { if (!requestId || requestId < 0) { receiver.onPlaceOrderError("Request Error"); return; } const statistic = this._ensureOrderStatistic(symbol); statistic.places += 1; this.placeOrders.set(requestId, receiver); const receiptId = `${this.frontId}:${this.sessionId}:${orderRef}`; receiver.onPlaceOrderSent(receiptId); return receiptId; }); } _clearAllMarketOrders() { this.marketOrdersQueue.forEach((queue) => { const orders = queue.toArray(); orders.forEach((order) => { order.receiver.onPlaceOrderError("Request Error"); }); }); this.marketOrdersQueue.clear(); } _placeMarketOrder(symbol, offset, side, volume, receiver) { const [instrumentId, exchangeId] = parseSymbol(symbol); const instrument = this.instruments.get(instrumentId); if (!instrument) { receiver.onPlaceOrderError("Instrument Not Found"); return; } if (exchangeId !== instrument.ExchangeID) { receiver.onPlaceOrderError("Exchange Id Error"); return; } const priceRange = this.priceLimit.get(instrumentId); if (priceRange) { switch (side) { case "long": this._placeLimitOrder(symbol, offset, side, volume, priceRange.upper, receiver); break; case "short": this._placeLimitOrder(symbol, offset, side, volume, priceRange.lower, receiver); break; } return; } if (this.fastQueryLastTick) { const lastTick = this.fastQueryLastTick(instrumentId); if (lastTick && lastTick.tradingDay === this.tradingDay) { const isLimitPrice = !isValidPrice(lastTick.bandings.upper) || !isValidPrice(lastTick.bandings.lower); if (isLimitPrice) { this.priceLimit.set(instrumentId, { ...lastTick.limits }); } const upperPrice = isLimitPrice ? lastTick.limits.upper : lastTick.bandings.upper; const lowerPrice = isLimitPrice ? lastTick.limits.lower : lastTick.bandings.lower; switch (side) { case "long": this._placeLimitOrder(symbol, offset, side, volume, upperPrice, receiver); break; case "short": this._placeLimitOrder(symbol, offset, side, volume, lowerPrice, receiver); break; } return; } } let queue = this.marketOrdersQueue.get(instrumentId); if (queue) { queue.push({ symbol, offset, side, volume, receiver }); return; } queue = new Denque(); this.marketOrdersQueue.set(instrumentId, queue); this._withRetry(() => this.traderApi?.reqQryDepthMarketData({ ExchangeID: exchangeId, InstrumentID: instrumentId, })).then((requestId) => { if (!requestId || requestId < 0) { const orders = queue.toArray(); orders.forEach((order) => { order.receiver.onPlaceOrderError("Request Error"); }); this.marketOrdersQueue.delete(instrumentId); return; } queue.push({ symbol, offset, side, volume, receiver }); }); } placeOrder(symbol, offset, side, volume, price, flag, receiver) { if (volume <= 0) { receiver.onPlaceOrderError("Invalid Volume"); return; } switch (flag) { case "limit": return this._placeLimitOrder(symbol, offset, side, volume, price, receiver); case "market": return this._placeMarketOrder(symbol, offset, side, volume, receiver); } } cancelOrder(order, receiver) { const current = this.orders.get(order.id); if (!current) { receiver.onCancelOrderError("Order Not Found"); return; } if (order.cancelTime) { receiver.onCancelOrderError("Already Canceled"); return; } this._withRetry(() => this.traderApi?.reqOrderAction({ ...this.userInfo, InstrumentID: current.InstrumentID, FrontID: current.FrontID, SessionID: current.SessionID, OrderRef: current.OrderRef, ExchangeID: current.ExchangeID, OrderSysID: current.OrderSysID, ActionFlag: ctp.ActionFlagType.Delete, })).then((requestId) => { if (!requestId || requestId < 0) { receiver.onCancelOrderError("Request Error"); return; } this.cancelOrders.set(requestId, receiver); receiver.onCancelOrderSent(); }); } _toSymbol(instrumentId) { const instrument = this.instruments.get(instrumentId); if (!instrument) { return undefined; } return `${instrument.InstrumentID}.${instrument.ExchangeID}`; } _calcOrderId(orderOrTrade) { const { ExchangeID, TraderID, OrderLocalID } = orderOrTrade; return `${ExchangeID}:${TraderID}:${OrderLocalID}`; } _calcReceiptId(order) { return `${order.FrontID}:${order.SessionID}:${parseInt(order.OrderRef)}`; } _calcOrderStatus(order, traded) { switch (order.OrderStatus) { case ctp.OrderStatusType.Unknown: return "submitted"; case ctp.OrderStatusType.AllTraded: return "filled"; case ctp.OrderStatusType.Canceled: switch (order.OrderSubmitStatus) { case ctp.OrderSubmitStatusType.InsertRejected: case ctp.OrderSubmitStatusType.CancelRejected: case ctp.OrderSubmitStatusType.ModifyRejected: return "rejected"; default: return "canceled"; } default: return traded && order.VolumeTotalOriginal === traded ? "filled" : "partially-filled"; } } _calcOrderFlag(orderPriceType) { switch (orderPriceType) { case ctp.OrderPriceTypeType.LimitPrice: return "limit"; default: return "market"; } } _calcSideType(direction) { switch (direction) { case ctp.DirectionType.Buy: return "long"; case ctp.DirectionType.Sell: return "short"; } } _toDirection(side) { switch (side) { case "long": return ctp.DirectionType.Buy; case "short": return ctp.DirectionType.Sell; } } _calcOffsetType(offset) { switch (offset) { case ctp.OffsetFlagType.Open: return "open"; case ctp.OffsetFlagType.CloseToday: return "close-today"; default: return "close"; } } _toOffsetFlag(offset) { switch (offset) { case "open": return ctp.OffsetFlagType.Open; case "close": return ctp.OffsetFlagType.Close; case "close-today": return ctp.OffsetFlagType.CloseToday; } } _calcProductType(productClass) { switch (productClass) { case ctp.ProductClassType.Futures: return "futures"; case ctp.ProductClassType.Options: return "options"; case ctp.ProductClassType.Spot: return "spot"; case ctp.ProductClassType.SpotOption: return "spot-options"; default: throw new Error(`Unsupported product class: ${productClass}`); } } _calcOptionsType(optionsType) { switch (optionsType) { case ctp.OptionsTypeType.CallOptions: return "call"; case ctp.OptionsTypeType.PutOptions: return "put"; default: return undefined; } } _ensurePositionInfo(symbol) { let position = this.positions.get(symbol); if (!position) { position = { symbol: symbol, today: { long: { position: 0, frozen: 0 }, short: { position: 0, frozen: 0 }, }, history: { long: { position: 0, frozen: 0 }, short: { position: 0, frozen: 0 }, }, pending: { long: 0, short: 0 }, }; this.positions.set(symbol, position); } return position; } _ensureOrderStatistic(symbol) { let statistic = this.orderStatistics.get(symbol); if (!statistic) { statistic = { symbol: symbol, places: 0, entrusts: 0, filleds: 0, cancels: 0, rejects: 0, }; this.orderStatistics.set(symbol, statistic); } return statistic; } _calcPosition(symbol, side, offset, volume) { const position = this._ensurePositionInfo(symbol); switch (offset) { case "open": switch (side) { case "long": position.today.long.position += volume; if (position.pending.long >= volume) { position.pending.long -= volume; } else { position.pending.long = 0; } break; case "short": position.today.short.position += volume; if (position.pending.short >= volume) { position.pending.short -= volume; } else { position.pending.short = 0; } break; } break; case "close": switch (side) { case "long": if (position.history.short.position >= volume) { position.history.short.position -= volume; } else { const rest = volume - position.history.short.position; position.history.short.position -= position.history.short.position; if (rest > 0) { if (position.today.short.position >= rest) { position.today.short.position -= rest; } else { position.today.short.position = 0; } } } if (position.history.short.frozen >= volume) { position.history.short.frozen -= volume; } else { const rest = volume - position.history.short.frozen; position.history.short.frozen -= position.history.short.frozen; if (rest > 0) { if (position.today.short.frozen >= rest) { position.today.short.frozen -= rest; } else { position.today.short.frozen = 0; } } } break; case "short": if (position.history.long.position >= volume) { position.history.long.position -= volume; } else { const rest = volume - position.history.long.position; position.history.long.position -= position.history.long.position; if (rest > 0) { if (position.today.long.position >= rest) { position.today.long.position -= rest; } else { position.today.long.position = 0; } } } if (position.history.long.frozen >= volume) { position.history.long.frozen -= volume; } else { const rest = volume - position.history.long.frozen; position.history.long.frozen -= position.history.long.frozen; if (rest > 0) { if (position.today.long.frozen >= rest) { position.today.long.frozen -= rest; } else { position.today.long.frozen = 0; } } } break; } break; case "close-today": switch (side) { case "long": if (position.today.short.position >= volume) { position.today.short.position -= volume; } else { position.today.short.position = 0; } if (position.today.short.frozen >= volume) { position.today.short.frozen -= volume; } else { position.today.short.frozen = 0; } break; case "short": if (position.today.long.position >= volume) { position.today.long.position -= volume; } else { position.today.long.position = 0; } if (position.today.long.frozen >= volume) { position.today.long.frozen -= volume; } else { position.today.long.frozen = 0; } break; } break; } } _recordPending(symbol, side, offset, volume) { if (offset !== "open") { return; } const position = this._ensurePositionInfo(symbol); switch (side) { case "long": position.pending.long += volume; break; case "short": position.pending.short += volume; break; } } _recoverPending(symbol, side, offset, volume) { if (offset !== "open") { return; } const position = this.positions.get(symbol); if (!position) { return; } switch (side) { case "long": position.pending.long -= volume; break; case "short": position.pending.short -= volume; break; } } _freezePosition(symbol, side, offset, volume) { const position = this.positions.get(symbol); if (!position) { return; } switch (offset) { case "close": switch (side) { case "long": position.history.short.frozen += volume; break; case "short": position.history.long.frozen += volume; break; } break; case "close-today": switch (side) { case "long": position.today.short.frozen += volume; break; case "short": position.today.long.frozen += volume; break; } break; } } _unfreezePosition(symbol, side, offset, volume) { const position = this.positions.get(symbol); if (!position) { return; } switch (offset) { case "close": switch (side) { case "long": if (position.history.short.frozen >= volume) { position.history.short.frozen -= volume; } else { position.history.short.frozen = 0; } break; case "short": if (position.history.long.frozen >= volume) { position.history.long.frozen -= volume; } else { position.history.long.frozen = 0; } break; } break; case "close-today": switch (side) { case "long": if (position.today.short.frozen >= volume) { position.today.short.frozen -= volume; } else { position.today.short.frozen = 0; } break; case "short": if (position.today.long.frozen >= volume) { position.today.long.frozen -= volume; } else { position.today.long.frozen = 0; } break; } break; } } _toTradeData(trade) { return Object.freeze({ id: trade.TradeID, date: parseInt(trade.TradeDate), time: this._parseTime(trade.TradeTime), price: trade.Price, volume: trade.Volume, }); } _toOrderData(order) { const orderId = this._calcOrderId(order); const trades = this.trades.get(orderId) ?? []; const traded = trades .map((trade) => trade.Volume) .reduce((a, b) => a + b, 0); return Object.freeze({ id: orderId, receiptId: this._calcReceiptId(order), symbol: `${order.InstrumentID}.${order.ExchangeID}`, date: parseInt(order.InsertDate), time: this._parseTime(order.InsertTime), flag: this._calcOrderFlag(order.OrderPriceType), side: this._calcSideType(order.Direction), offset: this._calcOffsetType(order.CombOffsetFlag), price: order.LimitPrice, volume: order.VolumeTotalOriginal, traded: traded, status: this._calcOrderStatus(order, traded), trades: trades.map(this._toTradeData, this), cancelTime: order.CancelTime !== "" ? this._parseTime(order.CancelTime) : undefined, }); } _toInstrumentData(instrument) { return Object.freeze({ symbol: `${instrument.InstrumentID}.${instrument.ExchangeID}`, id: instrument.InstrumentID, name: instrument.InstrumentName, exchangeId: instrument.ExchangeID, productId: instrument.ProductID, productType: this._calcProductType(instrument.ProductClass), deliveryTime: instrument.DeliveryYear * 100 + instrument.DeliveryMonth, createDate: parseInt(instrument.CreateDate), openDate: parseInt(instrument.OpenDate), expireDate: parseInt(instrument.ExpireDate), multiple: instrument.VolumeMultiple, priceTick: instrument.PriceTick, maxLimitOrderVolume: instrument.MaxLimitOrderVolume, minLimitOrderVolume: instrument.MinLimitOrderVolume, strikePrice: instrument.StrikePrice, optionsType: this._calcOptionsType(instrument.OptionsType), }); } _toCommissionRate(symbol, commRate) { return Object.freeze({ symbol: symbol, open: Object.freeze({ ratio: commRate.OpenRatioByMoney, amount: commRate.OpenRatioByVolume, }), close: Object.freeze({ ratio: commRate.CloseRatioByMoney, amount: commRate.CloseRatioByVolume, }), closeToday: Object.freeze({ ratio: commRate.CloseTodayRatioByMoney, amount: commRate.CloseTodayRatioByVolume, }), }); } _toMarginRate(symbol, marginRate) { return Object.freeze({ symbol: symbol, long: Object.freeze({ ratio: marginRate.LongMarginRatioByMoney, amount: marginRate.LongMarginRatioByVolume, }), short: Object.freeze({ ratio: marginRate.ShortMarginRatioByMoney, amount: marginRate.ShortMarginRatioByVolume, }), }); } _toTradingAccount(account) { return Object.freeze({ id: account.AccountID, currency: account.CurrencyID, preBalance: account.PreBalance - account.Withdraw + account.Deposit, preMargin: account.PreMargin, balance: account.Balance, cash: account.Available, margin: account.CurrMargin, commission: account.Commission, frozenMargin: account.FrozenMargin, frozenCash: account.FrozenCash, frozenCommission: account.FrozenCommission, }); } _toPositionDetail(positionDetail) { return Object.freeze({ symbol: this._toSymbol(positionDetail.InstrumentID), date: parseInt(positionDetail.OpenDate), side: this._calcSideType(positionDetail.Direction), price: positionDetail.OpenPrice, volume: positionDetail.Volume, margin: positionDetail.Margin, }); } _toPositionData(position) { return Object.freeze({ symbol: position.symbol,