hft-js 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/trader.ts ADDED
@@ -0,0 +1,1520 @@
1
+ /*
2
+ * trader.ts
3
+ *
4
+ * Copyright (c) 2025 Xiongfei Shi
5
+ *
6
+ * Author: Xiongfei Shi <xiongfei.shi(a)icloud.com>
7
+ * License: Apache-2.0
8
+ *
9
+ * https://github.com/shixiongfei/hft.js
10
+ */
11
+
12
+ import Denque from "denque";
13
+ import ctp from "napi-ctp";
14
+ import { CTPProvider, CTPUserInfo } from "./provider.js";
15
+ import {
16
+ CommissionRate,
17
+ InstrumentData,
18
+ MarginRate,
19
+ OffsetType,
20
+ OrderData,
21
+ OrderFlag,
22
+ OrderStatus,
23
+ PositionData,
24
+ PositionDetail,
25
+ ProductType,
26
+ SideType,
27
+ TradeData,
28
+ TradingAccount,
29
+ Writeable,
30
+ } from "./typedef.js";
31
+ import {
32
+ ICancelOrderResultReceiver,
33
+ ICommissionRateReceiver,
34
+ IInstrumentReceiver,
35
+ IInstrumentsReceiver,
36
+ ILifecycleListener,
37
+ IMarginRateReceiver,
38
+ IOrderReceiver,
39
+ IOrdersReceiver,
40
+ IPlaceOrderResultReceiver,
41
+ IPositionDetailsReceiver,
42
+ IPositionReceiver,
43
+ IPositionsReceiver,
44
+ ITraderProvider,
45
+ ITradingAccountsReceiver,
46
+ } from "./interfaces.js";
47
+
48
+ type MarginRateQuery = { symbol: string; receiver: IMarginRateReceiver };
49
+
50
+ type CommissionRateQuery = {
51
+ symbol: string;
52
+ receiver: ICommissionRateReceiver;
53
+ };
54
+
55
+ type PositionInfo = Writeable<PositionData>;
56
+
57
+ export class Trader extends CTPProvider implements ITraderProvider {
58
+ private traderApi?: ctp.Trader;
59
+ private tradingDay: number;
60
+ private frontId: number;
61
+ private sessionId: number;
62
+ private orderRef: number;
63
+ private accountsQueryTime: number;
64
+ private positionDetailsChanged: boolean;
65
+ private readonly receivers: IOrderReceiver[];
66
+ private readonly instruments: ctp.InstrumentField[];
67
+ private readonly accounts: ctp.TradingAccountField[];
68
+ private readonly positionDetails: ctp.InvestorPositionDetailField[];
69
+ private readonly symbols: Map<string, string>;
70
+ private readonly positions: Map<string, PositionInfo>;
71
+ private readonly orders: Map<string, ctp.OrderField>;
72
+ private readonly trades: Map<string, ctp.TradeField[]>;
73
+ private readonly marginRates: Map<string, ctp.InstrumentMarginRateField>;
74
+ private readonly commRates: Map<string, ctp.InstrumentCommissionRateField>;
75
+ private readonly placeOrders: Map<number, IPlaceOrderResultReceiver>;
76
+ private readonly cancelOrders: Map<number, ICancelOrderResultReceiver>;
77
+ private readonly marginRatesQueue: Denque<MarginRateQuery>;
78
+ private readonly commRatesQueue: Denque<CommissionRateQuery>;
79
+ private readonly accountsQueue: Denque<ITradingAccountsReceiver>;
80
+ private readonly positionDetailsQueue: Denque<IPositionDetailsReceiver>;
81
+
82
+ constructor(
83
+ flowTdPath: string,
84
+ frontTdAddrs: string | string[],
85
+ userInfo: CTPUserInfo,
86
+ ) {
87
+ super(flowTdPath, frontTdAddrs, userInfo);
88
+ this.tradingDay = 0;
89
+ this.frontId = 0;
90
+ this.sessionId = 0;
91
+ this.orderRef = 0;
92
+ this.accountsQueryTime = 0;
93
+ this.positionDetailsChanged = true;
94
+ this.receivers = [];
95
+ this.instruments = [];
96
+ this.accounts = [];
97
+ this.positionDetails = [];
98
+ this.symbols = new Map();
99
+ this.positions = new Map();
100
+ this.orders = new Map();
101
+ this.trades = new Map();
102
+ this.marginRates = new Map();
103
+ this.commRates = new Map();
104
+ this.placeOrders = new Map();
105
+ this.cancelOrders = new Map();
106
+ this.marginRatesQueue = new Denque();
107
+ this.commRatesQueue = new Denque();
108
+ this.accountsQueue = new Denque();
109
+ this.positionDetailsQueue = new Denque();
110
+ }
111
+
112
+ open(lifecycle: ILifecycleListener) {
113
+ if (this.traderApi) {
114
+ return true;
115
+ }
116
+
117
+ this.traderApi = ctp.createTrader(this.flowPath, this.frontAddrs);
118
+
119
+ this.traderApi.on(ctp.TraderEvent.FrontConnected, () => {
120
+ this._withRetry(() => this.traderApi!.reqAuthenticate(this.userInfo));
121
+ });
122
+
123
+ this.traderApi.on(ctp.TraderEvent.FrontDisconnected, () => {
124
+ this.placeOrders.clear();
125
+ this.cancelOrders.clear();
126
+ });
127
+
128
+ this.traderApi.on<ctp.RspAuthenticateField>(
129
+ ctp.TraderEvent.RspAuthenticate,
130
+ (_, options) => {
131
+ if (this._isErrorResp(lifecycle, options, "login-error")) {
132
+ return;
133
+ }
134
+
135
+ this._withRetry(() => this.traderApi!.reqUserLogin(this.userInfo));
136
+ },
137
+ );
138
+
139
+ this.traderApi.on<ctp.RspUserLoginField>(
140
+ ctp.TraderEvent.RspUserLogin,
141
+ (rspUserLogin, options) => {
142
+ if (this._isErrorResp(lifecycle, options, "login-error")) {
143
+ return;
144
+ }
145
+
146
+ this.frontId = rspUserLogin.FrontID;
147
+ this.sessionId = rspUserLogin.SessionID;
148
+ this.orderRef = parseInt(rspUserLogin.MaxOrderRef);
149
+
150
+ const tradingDay = parseInt(this.traderApi!.getTradingDay());
151
+
152
+ if (this.tradingDay !== tradingDay) {
153
+ this.marginRates.clear();
154
+ this.commRates.clear();
155
+ this.tradingDay = tradingDay;
156
+ }
157
+
158
+ this._withRetry(() =>
159
+ this.traderApi!.reqSettlementInfoConfirm(this.userInfo),
160
+ );
161
+ },
162
+ );
163
+
164
+ this.traderApi.on<ctp.SettlementInfoConfirmField>(
165
+ ctp.TraderEvent.RspSettlementInfoConfirm,
166
+ (_, options) => {
167
+ if (this._isErrorResp(lifecycle, options, "login-error")) {
168
+ return;
169
+ }
170
+
171
+ this.orders.clear();
172
+ this._withRetry(() => this.traderApi!.reqQryOrder(this.userInfo));
173
+ },
174
+ );
175
+
176
+ this.traderApi.on<ctp.OrderField>(
177
+ ctp.TraderEvent.RspQryOrder,
178
+ (order, options) => {
179
+ if (this._isErrorResp(lifecycle, options, "query-order-error")) {
180
+ return;
181
+ }
182
+
183
+ if (order) {
184
+ const orderId = this._calcOrderId(order);
185
+ this.orders.set(orderId, order);
186
+ }
187
+
188
+ if (options.isLast) {
189
+ this.trades.clear();
190
+ this._withRetry(() => this.traderApi!.reqQryTrade(this.userInfo));
191
+ }
192
+ },
193
+ );
194
+
195
+ this.traderApi.on<ctp.TradeField>(
196
+ ctp.TraderEvent.RspQryTrade,
197
+ (trade, options) => {
198
+ if (this._isErrorResp(lifecycle, options, "query-trade-error")) {
199
+ return;
200
+ }
201
+
202
+ if (trade) {
203
+ const orderId = this._calcOrderId(trade);
204
+ const trades = this.trades.get(orderId);
205
+
206
+ if (trades) {
207
+ trades.push(trade);
208
+ } else {
209
+ this.trades.set(orderId, [trade]);
210
+ }
211
+ }
212
+
213
+ if (options.isLast) {
214
+ this.symbols.clear();
215
+ this.instruments.splice(0, this.instruments.length);
216
+ this._withRetry(() => this.traderApi!.reqQryInstrument());
217
+ }
218
+ },
219
+ );
220
+
221
+ this.traderApi.on<ctp.InstrumentField>(
222
+ ctp.TraderEvent.RspQryInstrument,
223
+ (instrument, options) => {
224
+ if (this._isErrorResp(lifecycle, options, "query-instrument-error")) {
225
+ return;
226
+ }
227
+
228
+ if (instrument) {
229
+ if (
230
+ instrument.ProductClass === ctp.ProductClassType.Futures ||
231
+ instrument.ProductClass === ctp.ProductClassType.Options
232
+ ) {
233
+ this.symbols.set(
234
+ instrument.InstrumentID,
235
+ `${instrument.InstrumentID}.${instrument.ExchangeID}`,
236
+ );
237
+
238
+ this.instruments.push(instrument);
239
+ }
240
+ }
241
+
242
+ if (options.isLast) {
243
+ this.positions.clear();
244
+
245
+ this._withRetry(() =>
246
+ this.traderApi!.reqQryInvestorPosition(this.userInfo),
247
+ );
248
+ }
249
+ },
250
+ );
251
+
252
+ let fired = false;
253
+
254
+ this.traderApi.on<ctp.InvestorPositionField>(
255
+ ctp.TraderEvent.RspQryInvestorPosition,
256
+ (position, options) => {
257
+ if (this._isErrorResp(lifecycle, options, "query-positions-error")) {
258
+ return;
259
+ }
260
+
261
+ if (position) {
262
+ const symbol = this.symbols.get(position.InstrumentID);
263
+
264
+ if (symbol) {
265
+ let posInfo = this._ensurePositionInfo(symbol);
266
+ const ExchangeSH = ["SHFE", "INE"];
267
+
268
+ switch (position.PosiDirection) {
269
+ case ctp.PosiDirectionType.Long:
270
+ if (position.PositionDate === ctp.PositionDateType.Today) {
271
+ if (ExchangeSH.includes(position.ExchangeID)) {
272
+ posInfo.today.long.position += position.TodayPosition;
273
+ } else {
274
+ posInfo.today.long.position += position.Position;
275
+ }
276
+ } else {
277
+ posInfo.history.long.position +=
278
+ position.Position - position.TodayPosition;
279
+ }
280
+ break;
281
+
282
+ case ctp.PosiDirectionType.Short:
283
+ if (position.PositionDate === ctp.PositionDateType.Today) {
284
+ if (ExchangeSH.includes(position.ExchangeID)) {
285
+ posInfo.today.short.position += position.TodayPosition;
286
+ } else {
287
+ posInfo.today.short.position += position.Position;
288
+ }
289
+ } else {
290
+ posInfo.history.short.position +=
291
+ position.Position - position.TodayPosition;
292
+ }
293
+ break;
294
+ }
295
+ }
296
+ }
297
+
298
+ if (options.isLast) {
299
+ if (!fired) {
300
+ fired = true;
301
+ lifecycle.onOpen();
302
+ }
303
+
304
+ if (this.accountsQueue.size() > 0) {
305
+ this._withRetry(() =>
306
+ this.traderApi!.reqQryTradingAccount(this.userInfo),
307
+ );
308
+ }
309
+
310
+ if (this.positionDetailsQueue.size() > 0) {
311
+ this._withRetry(() =>
312
+ this.traderApi!.reqQryInvestorPositionDetail(this.userInfo),
313
+ );
314
+ }
315
+
316
+ this._processMarginRatesQueue();
317
+ this._processCommissionRatesQueue();
318
+ }
319
+ },
320
+ );
321
+
322
+ this.traderApi.on<ctp.OrderField>(ctp.TraderEvent.RtnOrder, (order) => {
323
+ const orderId = this._calcOrderId(order);
324
+ const current = this.orders.get(orderId);
325
+
326
+ if (current) {
327
+ if (
328
+ order.OrderSubmitStatus === current.OrderSubmitStatus &&
329
+ order.OrderStatus === current.OrderStatus
330
+ ) {
331
+ return;
332
+ }
333
+ }
334
+
335
+ this.orders.set(orderId, order);
336
+
337
+ switch (this._calcOrderStatus(order)) {
338
+ case "submitted":
339
+ {
340
+ const orderData = this._toOrderData(order);
341
+ const symbol = this.symbols.get(order.InstrumentID);
342
+
343
+ if (symbol) {
344
+ if (orderData.offset === "open") {
345
+ this._recordPending(
346
+ symbol,
347
+ orderData.side,
348
+ orderData.offset,
349
+ orderData.volume,
350
+ );
351
+ } else {
352
+ this._freezePosition(
353
+ symbol,
354
+ orderData.side,
355
+ orderData.offset,
356
+ orderData.volume,
357
+ );
358
+ }
359
+ }
360
+
361
+ this.receivers.forEach((receiver) => receiver.onEntrust(orderData));
362
+ }
363
+ break;
364
+
365
+ case "canceled":
366
+ {
367
+ const orderData = this._toOrderData(order);
368
+ const symbol = this.symbols.get(order.InstrumentID);
369
+
370
+ if (symbol) {
371
+ if (orderData.offset === "open") {
372
+ this._recoverPending(
373
+ symbol,
374
+ orderData.side,
375
+ orderData.offset,
376
+ orderData.volume,
377
+ );
378
+ } else {
379
+ this._unfreezePosition(
380
+ symbol,
381
+ orderData.side,
382
+ orderData.offset,
383
+ orderData.volume,
384
+ );
385
+ }
386
+ }
387
+
388
+ this.receivers.forEach((receiver) => receiver.onCancel(orderData));
389
+ }
390
+ break;
391
+
392
+ case "rejected":
393
+ {
394
+ const orderData = this._toOrderData(order);
395
+ this.receivers.forEach((receiver) => receiver.onReject(orderData));
396
+ }
397
+ break;
398
+ }
399
+ });
400
+
401
+ this.traderApi.on<ctp.TradeField>(ctp.TraderEvent.RtnTrade, (trade) => {
402
+ const orderId = this._calcOrderId(trade);
403
+ const trades = this.trades.get(orderId);
404
+
405
+ if (trades) {
406
+ trades.push(trade);
407
+ } else {
408
+ this.trades.set(orderId, [trade]);
409
+ }
410
+
411
+ this.positionDetailsChanged = true;
412
+
413
+ const order = this.orders.get(orderId);
414
+
415
+ if (order) {
416
+ const orderData = this._toOrderData(order);
417
+ const tradeData = this._toTradeData(trade);
418
+ const symbol = this.symbols.get(order.InstrumentID);
419
+
420
+ if (symbol) {
421
+ this._calcPosition(
422
+ symbol,
423
+ orderData.side,
424
+ orderData.offset,
425
+ orderData.volume,
426
+ );
427
+ }
428
+
429
+ this.receivers.forEach((receiver) =>
430
+ receiver.onTrade(orderData, tradeData),
431
+ );
432
+ }
433
+ });
434
+
435
+ this.traderApi.on<ctp.InstrumentMarginRateField>(
436
+ ctp.TraderEvent.RspQryInstrumentMarginRate,
437
+ (marginRate, options) => {
438
+ const query = this.marginRatesQueue.shift();
439
+
440
+ if (this._isErrorResp(lifecycle, options, "query-margin-rate-error")) {
441
+ if (query) {
442
+ query.receiver.onMarginRate(undefined);
443
+ }
444
+
445
+ return;
446
+ }
447
+
448
+ if (marginRate) {
449
+ this.marginRates.set(marginRate.InstrumentID, marginRate);
450
+
451
+ if (query) {
452
+ query.receiver.onMarginRate(
453
+ this._toMarginRate(query.symbol, marginRate),
454
+ );
455
+ }
456
+ }
457
+
458
+ this._processMarginRatesQueue();
459
+ },
460
+ );
461
+
462
+ this.traderApi.on<ctp.InstrumentCommissionRateField>(
463
+ ctp.TraderEvent.RspQryInstrumentCommissionRate,
464
+ (commRate, options) => {
465
+ const query = this.commRatesQueue.shift();
466
+
467
+ if (
468
+ this._isErrorResp(lifecycle, options, "query-commission-rate-error")
469
+ ) {
470
+ if (query) {
471
+ query.receiver.onCommissionRate(undefined);
472
+ }
473
+
474
+ return;
475
+ }
476
+
477
+ if (commRate) {
478
+ this.commRates.set(commRate.InstrumentID, commRate);
479
+
480
+ if (query) {
481
+ query.receiver.onCommissionRate(
482
+ this._toCommissionRate(query.symbol, commRate),
483
+ );
484
+ }
485
+ }
486
+
487
+ this._processCommissionRatesQueue();
488
+ },
489
+ );
490
+
491
+ this.traderApi.on<ctp.TradingAccountField>(
492
+ ctp.TraderEvent.RspQryTradingAccount,
493
+ (account, options) => {
494
+ if (this._isErrorResp(lifecycle, options, "query-accounts-error")) {
495
+ const receivers = this.accountsQueue.toArray();
496
+
497
+ receivers.forEach((receiver) =>
498
+ receiver.onTradingAccounts(undefined),
499
+ );
500
+
501
+ this.accountsQueue.clear();
502
+ return;
503
+ }
504
+
505
+ if (account) {
506
+ this.accounts.push(account);
507
+ }
508
+
509
+ if (options.isLast) {
510
+ const accounts = this.accounts.map(this._toTradingAccount, this);
511
+ const receivers = this.accountsQueue.toArray();
512
+
513
+ receivers.forEach((receiver) => receiver.onTradingAccounts(accounts));
514
+ this.accountsQueue.clear();
515
+
516
+ this.accountsQueryTime = Date.now();
517
+ }
518
+ },
519
+ );
520
+
521
+ this.traderApi.on<ctp.InvestorPositionDetailField>(
522
+ ctp.TraderEvent.RspQryInvestorPositionDetail,
523
+ (positionDetail, options) => {
524
+ if (
525
+ this._isErrorResp(lifecycle, options, "query-position-details-error")
526
+ ) {
527
+ const receivers = this.positionDetailsQueue.toArray();
528
+
529
+ receivers.forEach((receiver) =>
530
+ receiver.onPositionDetails(undefined),
531
+ );
532
+ this.positionDetailsQueue.clear();
533
+
534
+ return;
535
+ }
536
+
537
+ if (positionDetail) {
538
+ this.positionDetails.push(positionDetail);
539
+ }
540
+
541
+ if (options.isLast) {
542
+ const positionDetails = this.positionDetails.map(
543
+ this._toPositionDetail,
544
+ );
545
+ const receivers = this.positionDetailsQueue.toArray();
546
+
547
+ this.positionDetailsChanged = false;
548
+
549
+ receivers.forEach((receiver) =>
550
+ receiver.onPositionDetails(positionDetails),
551
+ );
552
+ this.positionDetailsQueue.clear();
553
+ }
554
+ },
555
+ );
556
+
557
+ this.traderApi.on<ctp.InputOrderField>(
558
+ ctp.TraderEvent.RspOrderInsert,
559
+ (order, options) => {
560
+ if (options.rspInfo && order && options.requestId && options.isLast) {
561
+ const receiver = this.placeOrders.get(options.requestId);
562
+
563
+ if (receiver) {
564
+ this.placeOrders.delete(options.requestId);
565
+
566
+ receiver.onPlaceOrderError(
567
+ `${options.rspInfo.ErrorID}: ${options.rspInfo.ErrorMsg}`,
568
+ );
569
+ }
570
+ }
571
+ },
572
+ );
573
+
574
+ this.traderApi.on<ctp.InputOrderActionField>(
575
+ ctp.TraderEvent.RspOrderAction,
576
+ (order, options) => {
577
+ if (options.rspInfo && order && options.requestId && options.isLast) {
578
+ const receiver = this.cancelOrders.get(options.requestId);
579
+
580
+ if (receiver) {
581
+ this.cancelOrders.delete(options.requestId);
582
+
583
+ receiver.onCancelOrderError(
584
+ `${options.rspInfo.ErrorID}: ${options.rspInfo.ErrorMsg}`,
585
+ );
586
+ }
587
+ }
588
+ },
589
+ );
590
+
591
+ return true;
592
+ }
593
+
594
+ close(lifecycle: ILifecycleListener) {
595
+ if (!this.traderApi) {
596
+ return;
597
+ }
598
+
599
+ this.traderApi.close();
600
+ this.traderApi = undefined;
601
+
602
+ lifecycle.onClose();
603
+ }
604
+
605
+ addReceiver(receiver: IOrderReceiver) {
606
+ if (!this.receivers.includes(receiver)) {
607
+ this.receivers.push(receiver);
608
+ }
609
+ }
610
+
611
+ removeReceiver(receiver: IOrderReceiver) {
612
+ const index = this.receivers.indexOf(receiver);
613
+
614
+ if (index < 0) {
615
+ return;
616
+ }
617
+
618
+ this.receivers.splice(index, 1);
619
+ }
620
+
621
+ getTradingDay() {
622
+ return this.tradingDay;
623
+ }
624
+
625
+ queryCommissionRate(symbol: string, receiver: ICommissionRateReceiver) {
626
+ const [instrumentId] = this._parseSymbol(symbol);
627
+ const commRate = this.commRates.get(instrumentId);
628
+
629
+ if (commRate) {
630
+ receiver.onCommissionRate(this._toCommissionRate(symbol, commRate));
631
+ return;
632
+ }
633
+
634
+ this.commRatesQueue.push({ symbol, receiver });
635
+
636
+ if (this.commRatesQueue.size() === 1) {
637
+ this._withRetry(() =>
638
+ this.traderApi?.reqQryInstrumentCommissionRate({
639
+ ...this.userInfo,
640
+ InstrumentID: instrumentId,
641
+ }),
642
+ );
643
+ }
644
+ }
645
+
646
+ queryMarginRate(symbol: string, receiver: IMarginRateReceiver) {
647
+ const [instrumentId] = this._parseSymbol(symbol);
648
+ const marginRate = this.marginRates.get(instrumentId);
649
+
650
+ if (marginRate) {
651
+ receiver.onMarginRate(this._toMarginRate(symbol, marginRate));
652
+ return;
653
+ }
654
+
655
+ this.marginRatesQueue.push({ symbol, receiver });
656
+
657
+ if (this.marginRatesQueue.size() === 1) {
658
+ this._withRetry(() =>
659
+ this.traderApi?.reqQryInstrumentMarginRate({
660
+ ...this.userInfo,
661
+ HedgeFlag: ctp.HedgeFlagType.Speculation,
662
+ InstrumentID: instrumentId,
663
+ }),
664
+ );
665
+ }
666
+ }
667
+
668
+ queryInstrument(symbol: string, receiver: IInstrumentReceiver) {
669
+ const [instrumentId, exchangeId] = this._parseSymbol(symbol);
670
+
671
+ const instrument = this.instruments.find(
672
+ (instrument) =>
673
+ instrument.InstrumentID === instrumentId &&
674
+ instrument.ExchangeID === exchangeId,
675
+ );
676
+
677
+ receiver.onInstrument(
678
+ instrument ? this._toInstrumentData(instrument) : undefined,
679
+ );
680
+ }
681
+
682
+ queryPosition(symbol: string, receiver: IPositionReceiver) {
683
+ const position = this.positions.get(symbol);
684
+
685
+ if (position) {
686
+ receiver.onPosition(this._toPositionData(position));
687
+ return;
688
+ }
689
+
690
+ const [instrumentId] = this._parseSymbol(symbol);
691
+
692
+ if (!this.symbols.has(instrumentId)) {
693
+ receiver.onPosition(undefined);
694
+ return;
695
+ }
696
+
697
+ receiver.onPosition(
698
+ Object.freeze({
699
+ symbol: symbol,
700
+ today: Object.freeze({
701
+ long: Object.freeze({ position: 0, frozen: 0 }),
702
+ short: Object.freeze({ position: 0, frozen: 0 }),
703
+ }),
704
+ history: Object.freeze({
705
+ long: Object.freeze({ position: 0, frozen: 0 }),
706
+ short: Object.freeze({ position: 0, frozen: 0 }),
707
+ }),
708
+ pending: Object.freeze({ long: 0, short: 0 }),
709
+ }),
710
+ );
711
+ }
712
+
713
+ queryInstruments(receiver: IInstrumentsReceiver, type?: ProductType) {
714
+ switch (type) {
715
+ case "future":
716
+ receiver.onInstruments(
717
+ this.instruments
718
+ .filter(
719
+ (instrument) =>
720
+ instrument.ProductClass === ctp.ProductClassType.Futures,
721
+ )
722
+ .map(this._toInstrumentData, this),
723
+ );
724
+ break;
725
+
726
+ case "option":
727
+ receiver.onInstruments(
728
+ this.instruments
729
+ .filter(
730
+ (instrument) =>
731
+ instrument.ProductClass === ctp.ProductClassType.Options,
732
+ )
733
+ .map(this._toInstrumentData, this),
734
+ );
735
+ break;
736
+
737
+ default:
738
+ receiver.onInstruments(
739
+ this.instruments.map(this._toInstrumentData, this),
740
+ );
741
+ break;
742
+ }
743
+ }
744
+
745
+ queryTradingAccounts(receiver: ITradingAccountsReceiver) {
746
+ if (this.accountsQueue.size() > 0) {
747
+ this.accountsQueue.push(receiver);
748
+ return;
749
+ }
750
+
751
+ const elapsed = Date.now() - this.accountsQueryTime;
752
+
753
+ if (elapsed < 3000) {
754
+ receiver.onTradingAccounts(
755
+ this.accounts.map(this._toTradingAccount, this),
756
+ );
757
+ return;
758
+ }
759
+
760
+ this.accountsQueue.push(receiver);
761
+ this.accounts.splice(0, this.accounts.length);
762
+
763
+ this._withRetry(() => this.traderApi?.reqQryTradingAccount(this.userInfo));
764
+ }
765
+
766
+ queryPositionDetails(receiver: IPositionDetailsReceiver) {
767
+ if (this.positionDetailsQueue.size() > 0) {
768
+ this.positionDetailsQueue.push(receiver);
769
+ return;
770
+ }
771
+
772
+ if (!this.positionDetailsChanged) {
773
+ receiver.onPositionDetails(
774
+ this.positionDetails.map(this._toPositionDetail, this),
775
+ );
776
+ return;
777
+ }
778
+
779
+ this.positionDetailsQueue.push(receiver);
780
+ this.positionDetails.splice(0, this.positionDetails.length);
781
+
782
+ this._withRetry(() =>
783
+ this.traderApi?.reqQryInvestorPositionDetail(this.userInfo),
784
+ );
785
+ }
786
+
787
+ queryPositions(receiver: IPositionsReceiver) {
788
+ const positions: PositionData[] = [];
789
+
790
+ this.positions.forEach((position) =>
791
+ positions.push(this._toPositionData(position)),
792
+ );
793
+
794
+ receiver.onPositions(positions);
795
+ }
796
+
797
+ queryOrders(receiver: IOrdersReceiver) {
798
+ const orders: OrderData[] = [];
799
+
800
+ this.orders.forEach((order) => {
801
+ orders.push(this._toOrderData(order));
802
+ });
803
+
804
+ receiver.onOrders(orders);
805
+ }
806
+
807
+ placeOrder(
808
+ symbol: string,
809
+ offset: OffsetType,
810
+ side: SideType,
811
+ volume: number,
812
+ price: number,
813
+ flag: OrderFlag,
814
+ receiver: IPlaceOrderResultReceiver,
815
+ ) {
816
+ if (flag !== "limit") {
817
+ receiver.onPlaceOrderError("Only Supports Limit Order");
818
+ return;
819
+ }
820
+
821
+ const [instrumentId] = this._parseSymbol(symbol);
822
+
823
+ const instrument = this.instruments.find(
824
+ (instrument) => instrument.InstrumentID === instrumentId,
825
+ );
826
+
827
+ if (!instrument) {
828
+ receiver.onPlaceOrderError("Instrument Not Found");
829
+ return;
830
+ }
831
+
832
+ let orderRef = 0;
833
+
834
+ this._withRetry(() => {
835
+ orderRef = ++this.orderRef;
836
+
837
+ return this.traderApi?.reqOrderInsert({
838
+ ...this.userInfo,
839
+ OrderRef: `${orderRef}`,
840
+ InstrumentID: instrumentId,
841
+ ExchangeID: instrument.ExchangeID,
842
+ LimitPrice: price,
843
+ VolumeTotalOriginal: volume,
844
+ VolumeCondition: ctp.VolumeConditionType.AV,
845
+ TimeCondition: ctp.TimeConditionType.GFD,
846
+ Direction: this._toDirection(side),
847
+ OrderPriceType: ctp.OrderPriceTypeType.LimitPrice,
848
+ CombOffsetFlag: this._toOffsetFlag(offset),
849
+ CombHedgeFlag: ctp.HedgeFlagType.Speculation,
850
+ ContingentCondition: ctp.ContingentConditionType.Immediately,
851
+ ForceCloseReason: ctp.ForceCloseReasonType.NotForceClose,
852
+ IsAutoSuspend: 0,
853
+ UserForceClose: 0,
854
+ });
855
+ }).then((requestId) => {
856
+ if (!requestId) {
857
+ receiver.onPlaceOrderError("Request Failed");
858
+ return;
859
+ }
860
+
861
+ if (requestId < 0) {
862
+ receiver.onPlaceOrderError("Request Error");
863
+ return;
864
+ }
865
+
866
+ this.placeOrders.set(requestId, receiver);
867
+
868
+ const receiptId = `${this.frontId}:${this.sessionId}:${orderRef}`;
869
+
870
+ receiver.onPlaceOrderSent(receiptId);
871
+
872
+ return receiptId;
873
+ });
874
+ }
875
+
876
+ cancelOrder(order: OrderData, receiver: ICancelOrderResultReceiver) {
877
+ const current = this.orders.get(order.id);
878
+
879
+ if (!current) {
880
+ receiver.onCancelOrderError("Order Not Found");
881
+ return;
882
+ }
883
+
884
+ if (order.cancelTime) {
885
+ receiver.onCancelOrderError("Already Canceled");
886
+ return;
887
+ }
888
+
889
+ this._withRetry(() =>
890
+ this.traderApi?.reqOrderAction({
891
+ ...this.userInfo,
892
+ InstrumentID: current.InstrumentID,
893
+ FrontID: current.FrontID,
894
+ SessionID: current.SessionID,
895
+ OrderRef: current.OrderRef,
896
+ ExchangeID: current.ExchangeID,
897
+ OrderSysID: current.OrderSysID,
898
+ ActionFlag: ctp.ActionFlagType.Delete,
899
+ }),
900
+ ).then((requestId) => {
901
+ if (!requestId) {
902
+ receiver.onCancelOrderError("Request Failed");
903
+ return;
904
+ }
905
+
906
+ if (requestId < 0) {
907
+ receiver.onCancelOrderError("Request Error");
908
+ return;
909
+ }
910
+
911
+ this.cancelOrders.set(requestId, receiver);
912
+
913
+ receiver.onCancelOrderSent();
914
+ });
915
+ }
916
+
917
+ private _calcOrderId(orderOrTrade: ctp.OrderField | ctp.TradeField) {
918
+ const { ExchangeID, TraderID, OrderLocalID } = orderOrTrade;
919
+ return `${ExchangeID}:${TraderID}:${OrderLocalID}`;
920
+ }
921
+
922
+ private _calcReceiptId(order: ctp.OrderField | ctp.InputOrderActionField) {
923
+ return `${order.FrontID}:${order.SessionID}:${parseInt(order.OrderRef)}`;
924
+ }
925
+
926
+ private _calcOrderStatus(
927
+ order: ctp.OrderField,
928
+ traded?: number,
929
+ ): OrderStatus {
930
+ switch (order.OrderStatus) {
931
+ case ctp.OrderStatusType.Unknown:
932
+ return "submitted";
933
+
934
+ case ctp.OrderStatusType.AllTraded:
935
+ return "filled";
936
+
937
+ case ctp.OrderStatusType.Canceled:
938
+ switch (order.OrderSubmitStatus) {
939
+ case ctp.OrderSubmitStatusType.InsertRejected:
940
+ case ctp.OrderSubmitStatusType.CancelRejected:
941
+ case ctp.OrderSubmitStatusType.ModifyRejected:
942
+ return "rejected";
943
+
944
+ default:
945
+ return "canceled";
946
+ }
947
+
948
+ default:
949
+ return traded && order.VolumeTotalOriginal === traded
950
+ ? "filled"
951
+ : "partially-filled";
952
+ }
953
+ }
954
+
955
+ private _calcOrderFlag(orderPriceType: ctp.OrderPriceTypeType): OrderFlag {
956
+ switch (orderPriceType) {
957
+ case ctp.OrderPriceTypeType.LimitPrice:
958
+ return "limit";
959
+
960
+ default:
961
+ return "market";
962
+ }
963
+ }
964
+
965
+ private _calcSideType(direction: ctp.DirectionType): SideType {
966
+ switch (direction) {
967
+ case ctp.DirectionType.Buy:
968
+ return "long";
969
+
970
+ case ctp.DirectionType.Sell:
971
+ return "short";
972
+ }
973
+ }
974
+
975
+ private _toDirection(side: SideType) {
976
+ switch (side) {
977
+ case "long":
978
+ return ctp.DirectionType.Buy;
979
+
980
+ case "short":
981
+ return ctp.DirectionType.Sell;
982
+ }
983
+ }
984
+
985
+ private _calcOffsetType(offset: ctp.OffsetFlagType): OffsetType {
986
+ switch (offset) {
987
+ case ctp.OffsetFlagType.Open:
988
+ return "open";
989
+
990
+ case ctp.OffsetFlagType.CloseToday:
991
+ return "close-today";
992
+
993
+ default:
994
+ return "close";
995
+ }
996
+ }
997
+
998
+ private _toOffsetFlag(offset: OffsetType) {
999
+ switch (offset) {
1000
+ case "open":
1001
+ return ctp.OffsetFlagType.Open;
1002
+
1003
+ case "close":
1004
+ return ctp.OffsetFlagType.Close;
1005
+
1006
+ case "close-today":
1007
+ return ctp.OffsetFlagType.CloseToday;
1008
+ }
1009
+ }
1010
+
1011
+ private _calcProductType(productClass: ctp.ProductClassType): ProductType {
1012
+ switch (productClass) {
1013
+ case ctp.ProductClassType.Futures:
1014
+ return "future";
1015
+
1016
+ case ctp.ProductClassType.Options:
1017
+ return "option";
1018
+
1019
+ default:
1020
+ throw new Error(`Unsupported product class: ${productClass}`);
1021
+ }
1022
+ }
1023
+
1024
+ private _ensurePositionInfo(symbol: string) {
1025
+ let position = this.positions.get(symbol);
1026
+
1027
+ if (!position) {
1028
+ position = {
1029
+ symbol: symbol,
1030
+ today: {
1031
+ long: { position: 0, frozen: 0 },
1032
+ short: { position: 0, frozen: 0 },
1033
+ },
1034
+ history: {
1035
+ long: { position: 0, frozen: 0 },
1036
+ short: { position: 0, frozen: 0 },
1037
+ },
1038
+ pending: { long: 0, short: 0 },
1039
+ };
1040
+
1041
+ this.positions.set(symbol, position);
1042
+ }
1043
+
1044
+ return position;
1045
+ }
1046
+
1047
+ private _calcPosition(
1048
+ symbol: string,
1049
+ side: SideType,
1050
+ offset: OffsetType,
1051
+ volume: number,
1052
+ ) {
1053
+ const position = this._ensurePositionInfo(symbol);
1054
+
1055
+ switch (offset) {
1056
+ case "open":
1057
+ switch (side) {
1058
+ case "long":
1059
+ position.today.long.position += volume;
1060
+
1061
+ if (position.pending.long > volume) {
1062
+ position.pending.long -= volume;
1063
+ } else {
1064
+ position.pending.long = 0;
1065
+ }
1066
+ break;
1067
+
1068
+ case "short":
1069
+ position.today.short.position += volume;
1070
+
1071
+ if (position.pending.short > volume) {
1072
+ position.pending.short -= volume;
1073
+ } else {
1074
+ position.pending.short = 0;
1075
+ }
1076
+ break;
1077
+ }
1078
+ break;
1079
+
1080
+ case "close":
1081
+ switch (side) {
1082
+ case "long":
1083
+ if (position.history.long.position > volume) {
1084
+ position.history.long.position -= volume;
1085
+ } else {
1086
+ const rest = volume - position.history.long.position;
1087
+ position.history.long.position -= position.history.long.position;
1088
+
1089
+ if (rest > 0) {
1090
+ if (position.today.long.position > rest) {
1091
+ position.today.long.position -= rest;
1092
+ } else {
1093
+ position.today.long.position = 0;
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ if (position.history.long.frozen > volume) {
1099
+ position.history.long.frozen -= volume;
1100
+ } else {
1101
+ const rest = volume - position.history.long.frozen;
1102
+ position.history.long.frozen -= position.history.long.frozen;
1103
+
1104
+ if (rest > 0) {
1105
+ if (position.today.long.frozen > rest) {
1106
+ position.today.long.frozen -= rest;
1107
+ } else {
1108
+ position.today.long.frozen = 0;
1109
+ }
1110
+ }
1111
+ }
1112
+ break;
1113
+
1114
+ case "short":
1115
+ if (position.history.short.position > volume) {
1116
+ position.history.short.position -= volume;
1117
+ } else {
1118
+ const rest = volume - position.history.short.position;
1119
+ position.history.short.position -=
1120
+ position.history.short.position;
1121
+
1122
+ if (rest > 0) {
1123
+ if (position.today.short.position > rest) {
1124
+ position.today.short.position -= rest;
1125
+ } else {
1126
+ position.today.short.position = 0;
1127
+ }
1128
+ }
1129
+ }
1130
+
1131
+ if (position.history.short.frozen > volume) {
1132
+ position.history.short.frozen -= volume;
1133
+ } else {
1134
+ const rest = volume - position.history.short.frozen;
1135
+ position.history.short.frozen -= position.history.short.frozen;
1136
+
1137
+ if (rest > 0) {
1138
+ if (position.today.short.frozen > rest) {
1139
+ position.today.short.frozen -= rest;
1140
+ } else {
1141
+ position.today.short.frozen = 0;
1142
+ }
1143
+ }
1144
+ }
1145
+ break;
1146
+ }
1147
+ break;
1148
+
1149
+ case "close-today":
1150
+ switch (side) {
1151
+ case "long":
1152
+ if (position.today.long.position > volume) {
1153
+ position.today.long.position -= volume;
1154
+ } else {
1155
+ position.today.long.position = 0;
1156
+ }
1157
+
1158
+ if (position.today.long.frozen > volume) {
1159
+ position.today.long.frozen -= volume;
1160
+ } else {
1161
+ position.today.long.frozen = 0;
1162
+ }
1163
+ break;
1164
+
1165
+ case "short":
1166
+ if (position.today.short.position > volume) {
1167
+ position.today.short.position -= volume;
1168
+ } else {
1169
+ position.today.short.position = 0;
1170
+ }
1171
+
1172
+ if (position.today.short.frozen > volume) {
1173
+ position.today.short.frozen -= volume;
1174
+ } else {
1175
+ position.today.short.frozen = 0;
1176
+ }
1177
+ break;
1178
+ }
1179
+ break;
1180
+ }
1181
+ }
1182
+
1183
+ private _recordPending(
1184
+ symbol: string,
1185
+ side: SideType,
1186
+ offset: OffsetType,
1187
+ volume: number,
1188
+ ) {
1189
+ if (offset !== "open") {
1190
+ return;
1191
+ }
1192
+
1193
+ const position = this._ensurePositionInfo(symbol);
1194
+
1195
+ switch (side) {
1196
+ case "long":
1197
+ position.pending.long += volume;
1198
+ break;
1199
+
1200
+ case "short":
1201
+ position.pending.short += volume;
1202
+ break;
1203
+ }
1204
+ }
1205
+
1206
+ private _recoverPending(
1207
+ symbol: string,
1208
+ side: SideType,
1209
+ offset: OffsetType,
1210
+ volume: number,
1211
+ ) {
1212
+ if (offset !== "open") {
1213
+ return;
1214
+ }
1215
+
1216
+ const position = this.positions.get(symbol);
1217
+
1218
+ if (!position) {
1219
+ return;
1220
+ }
1221
+
1222
+ switch (side) {
1223
+ case "long":
1224
+ position.pending.long -= volume;
1225
+ break;
1226
+
1227
+ case "short":
1228
+ position.pending.short -= volume;
1229
+ break;
1230
+ }
1231
+ }
1232
+
1233
+ private _freezePosition(
1234
+ symbol: string,
1235
+ side: SideType,
1236
+ offset: OffsetType,
1237
+ volume: number,
1238
+ ) {
1239
+ const position = this.positions.get(symbol);
1240
+
1241
+ if (!position) {
1242
+ return;
1243
+ }
1244
+
1245
+ switch (offset) {
1246
+ case "close":
1247
+ switch (side) {
1248
+ case "long":
1249
+ position.history.long.frozen += volume;
1250
+ break;
1251
+
1252
+ case "short":
1253
+ position.history.short.frozen += volume;
1254
+ break;
1255
+ }
1256
+ break;
1257
+
1258
+ case "close-today":
1259
+ switch (side) {
1260
+ case "long":
1261
+ position.today.long.frozen += volume;
1262
+ break;
1263
+
1264
+ case "short":
1265
+ position.today.short.frozen += volume;
1266
+ break;
1267
+ }
1268
+ break;
1269
+ }
1270
+ }
1271
+
1272
+ private _unfreezePosition(
1273
+ symbol: string,
1274
+ side: SideType,
1275
+ offset: OffsetType,
1276
+ volume: number,
1277
+ ) {
1278
+ const position = this.positions.get(symbol);
1279
+
1280
+ if (!position) {
1281
+ return;
1282
+ }
1283
+
1284
+ switch (offset) {
1285
+ case "close":
1286
+ switch (side) {
1287
+ case "long":
1288
+ if (position.history.long.frozen > volume) {
1289
+ position.history.long.frozen -= volume;
1290
+ } else {
1291
+ position.history.long.frozen = 0;
1292
+ }
1293
+
1294
+ break;
1295
+
1296
+ case "short":
1297
+ if (position.history.short.frozen > volume) {
1298
+ position.history.short.frozen -= volume;
1299
+ } else {
1300
+ position.history.short.frozen = 0;
1301
+ }
1302
+ break;
1303
+ }
1304
+ break;
1305
+
1306
+ case "close-today":
1307
+ switch (side) {
1308
+ case "long":
1309
+ if (position.today.long.frozen > volume) {
1310
+ position.today.long.frozen -= volume;
1311
+ } else {
1312
+ position.today.long.frozen = 0;
1313
+ }
1314
+ break;
1315
+
1316
+ case "short":
1317
+ if (position.today.short.frozen > volume) {
1318
+ position.today.short.frozen -= volume;
1319
+ } else {
1320
+ position.today.short.frozen = 0;
1321
+ }
1322
+ break;
1323
+ }
1324
+ break;
1325
+ }
1326
+ }
1327
+
1328
+ private _toTradeData(trade: ctp.TradeField): TradeData {
1329
+ return Object.freeze({
1330
+ id: trade.TradeID,
1331
+ date: parseInt(trade.TradeDate),
1332
+ time: this._parseTime(trade.TradeTime),
1333
+ price: trade.Price,
1334
+ volume: trade.Volume,
1335
+ });
1336
+ }
1337
+
1338
+ private _toOrderData(order: ctp.OrderField): OrderData {
1339
+ const orderId = this._calcOrderId(order);
1340
+ const trades = this.trades.get(orderId) ?? [];
1341
+
1342
+ const traded = trades
1343
+ .map((trade) => trade.Volume)
1344
+ .reduce((a, b) => a + b, 0);
1345
+
1346
+ return Object.freeze({
1347
+ id: orderId,
1348
+ receiptId: this._calcReceiptId(order),
1349
+ symbol: `${order.InstrumentID}.${order.ExchangeID}`,
1350
+ date: parseInt(order.InsertDate),
1351
+ time: this._parseTime(order.InsertTime),
1352
+ flag: this._calcOrderFlag(order.OrderPriceType),
1353
+ side: this._calcSideType(order.Direction),
1354
+ offset: this._calcOffsetType(order.CombOffsetFlag as ctp.OffsetFlagType),
1355
+ price: order.LimitPrice,
1356
+ volume: order.VolumeTotalOriginal,
1357
+ traded: traded,
1358
+ status: this._calcOrderStatus(order, traded),
1359
+ trades: trades.map(this._toTradeData, this),
1360
+ cancelTime:
1361
+ order.CancelTime !== "" ? this._parseTime(order.CancelTime) : undefined,
1362
+ });
1363
+ }
1364
+
1365
+ private _toInstrumentData(instrument: ctp.InstrumentField): InstrumentData {
1366
+ return Object.freeze({
1367
+ symbol: `${instrument.InstrumentID}.${instrument.ExchangeID}`,
1368
+ id: instrument.InstrumentID,
1369
+ name: instrument.InstrumentName,
1370
+ exchangeId: instrument.ExchangeID,
1371
+ productId: instrument.ProductID,
1372
+ productType: this._calcProductType(instrument.ProductClass),
1373
+ deliveryTime: instrument.DeliveryYear * 100 + instrument.DeliveryMonth,
1374
+ createDate: parseInt(instrument.CreateDate),
1375
+ openDate: parseInt(instrument.OpenDate),
1376
+ expireDate: parseInt(instrument.ExpireDate),
1377
+ multiple: instrument.VolumeMultiple,
1378
+ priceTick: instrument.PriceTick,
1379
+ maxLimitOrderVolume: instrument.MaxLimitOrderVolume,
1380
+ minLimitOrderVolume: instrument.MinLimitOrderVolume,
1381
+ });
1382
+ }
1383
+
1384
+ private _toCommissionRate(
1385
+ symbol: string,
1386
+ commRate: ctp.InstrumentCommissionRateField,
1387
+ ): CommissionRate {
1388
+ return Object.freeze({
1389
+ symbol: symbol,
1390
+ open: Object.freeze({
1391
+ ratio: commRate.OpenRatioByMoney,
1392
+ amount: commRate.OpenRatioByVolume,
1393
+ }),
1394
+ close: Object.freeze({
1395
+ ratio: commRate.CloseRatioByMoney,
1396
+ amount: commRate.CloseRatioByVolume,
1397
+ }),
1398
+ closeToday: Object.freeze({
1399
+ ratio: commRate.CloseTodayRatioByMoney,
1400
+ amount: commRate.CloseTodayRatioByVolume,
1401
+ }),
1402
+ });
1403
+ }
1404
+
1405
+ private _toMarginRate(
1406
+ symbol: string,
1407
+ marginRate: ctp.InstrumentMarginRateField,
1408
+ ): MarginRate {
1409
+ return Object.freeze({
1410
+ symbol: symbol,
1411
+ long: Object.freeze({
1412
+ ratio: marginRate.LongMarginRatioByMoney,
1413
+ amount: marginRate.LongMarginRatioByVolume,
1414
+ }),
1415
+ short: Object.freeze({
1416
+ ratio: marginRate.ShortMarginRatioByMoney,
1417
+ amount: marginRate.ShortMarginRatioByVolume,
1418
+ }),
1419
+ });
1420
+ }
1421
+
1422
+ private _toTradingAccount(account: ctp.TradingAccountField): TradingAccount {
1423
+ return Object.freeze({
1424
+ id: account.AccountID,
1425
+ currency: account.CurrencyID,
1426
+ preBalance: account.PreBalance - account.Withdraw + account.Deposit,
1427
+ balance: account.Balance,
1428
+ cash: account.Available,
1429
+ margin: account.CurrMargin,
1430
+ commission: account.Commission,
1431
+ frozenMargin: account.FrozenMargin,
1432
+ frozenCash: account.FrozenCash,
1433
+ frozenCommission: account.FrozenCommission,
1434
+ });
1435
+ }
1436
+
1437
+ private _toPositionDetail(
1438
+ positionDetail: ctp.InvestorPositionDetailField,
1439
+ ): PositionDetail {
1440
+ return Object.freeze({
1441
+ symbol: this.symbols.get(positionDetail.InstrumentID)!,
1442
+ date: parseInt(positionDetail.OpenDate),
1443
+ side: this._calcSideType(positionDetail.Direction),
1444
+ price: positionDetail.OpenPrice,
1445
+ volume: positionDetail.Volume,
1446
+ margin: positionDetail.Margin,
1447
+ });
1448
+ }
1449
+
1450
+ private _toPositionData(position: PositionInfo): PositionData {
1451
+ return Object.freeze({
1452
+ symbol: position.symbol,
1453
+
1454
+ today: Object.freeze({
1455
+ long: Object.freeze({ ...position.today.long }),
1456
+ short: Object.freeze({ ...position.today.short }),
1457
+ }),
1458
+ history: Object.freeze({
1459
+ long: Object.freeze({ ...position.history.long }),
1460
+ short: Object.freeze({ ...position.history.short }),
1461
+ }),
1462
+ pending: Object.freeze({ ...position.pending }),
1463
+ });
1464
+ }
1465
+
1466
+ private _processMarginRatesQueue() {
1467
+ while (!this.marginRatesQueue.isEmpty()) {
1468
+ const nextQuery = this.marginRatesQueue.peekFront()!;
1469
+
1470
+ const [instrumentId] = this._parseSymbol(nextQuery.symbol);
1471
+ const marginRate = this.marginRates.get(instrumentId);
1472
+
1473
+ if (marginRate) {
1474
+ nextQuery.receiver.onMarginRate(
1475
+ this._toMarginRate(nextQuery.symbol, marginRate),
1476
+ );
1477
+
1478
+ this.marginRatesQueue.shift();
1479
+ } else {
1480
+ this._withRetry(() =>
1481
+ this.traderApi!.reqQryInstrumentMarginRate({
1482
+ ...this.userInfo,
1483
+ HedgeFlag: ctp.HedgeFlagType.Speculation,
1484
+ InstrumentID: instrumentId,
1485
+ }),
1486
+ );
1487
+ }
1488
+ }
1489
+ }
1490
+
1491
+ private _processCommissionRatesQueue() {
1492
+ while (!this.commRatesQueue.isEmpty()) {
1493
+ const nextQuery = this.commRatesQueue.peekFront()!;
1494
+
1495
+ const [instrumentId] = this._parseSymbol(nextQuery.symbol);
1496
+ const commRate = this.commRates.get(instrumentId);
1497
+
1498
+ if (commRate) {
1499
+ nextQuery.receiver.onCommissionRate(
1500
+ this._toCommissionRate(nextQuery.symbol, commRate),
1501
+ );
1502
+
1503
+ this.commRatesQueue.shift();
1504
+ } else {
1505
+ this._withRetry(() =>
1506
+ this.traderApi!.reqQryInstrumentCommissionRate({
1507
+ ...this.userInfo,
1508
+ InstrumentID: instrumentId,
1509
+ }),
1510
+ );
1511
+ }
1512
+ }
1513
+ }
1514
+ }
1515
+
1516
+ export const createTrader = (
1517
+ flowTdPath: string,
1518
+ frontTdAddrs: string | string[],
1519
+ userInfo: CTPUserInfo,
1520
+ ) => new Trader(flowTdPath, frontTdAddrs, userInfo);