hft-js 0.1.8 → 0.2.0

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/market.ts CHANGED
@@ -12,8 +12,8 @@
12
12
  import ctp from "napi-ctp";
13
13
  import { CTPProvider } from "./provider.js";
14
14
  import { InstrumentData, OrderBook, TickData } from "./typedef.js";
15
+ import { isValidPrice, isValidVolume, parseSymbol } from "./utils.js";
15
16
  import { calcTapeData } from "./tape.js";
16
- import { parseSymbol } from "./utils.js";
17
17
  import {
18
18
  ILifecycleListener,
19
19
  IMarketProvider,
@@ -23,14 +23,15 @@ import {
23
23
  ITickReceiver,
24
24
  } from "./interfaces.js";
25
25
 
26
- const isValidPrice = (x: number) => x !== Number.MAX_VALUE && x !== 0;
27
- const isValidVolume = (x: number) => x !== Number.MAX_VALUE && x !== 0;
28
-
29
26
  export interface IMarketListener {
30
27
  onSubscribed: (symbol: string) => void;
31
28
  onUnsubscribed: (symbol: string) => void;
32
29
  }
33
30
 
31
+ export type MarketOptions = {
32
+ listener?: IMarketListener;
33
+ };
34
+
34
35
  export class Market
35
36
  extends CTPProvider
36
37
  implements IMarketProvider, IMarketRecorderProvider
@@ -48,15 +49,18 @@ export class Market
48
49
  constructor(
49
50
  flowMdPath: string,
50
51
  frontMdAddrs: string | string[],
51
- listener?: IMarketListener,
52
+ options?: MarketOptions,
52
53
  ) {
53
54
  super(flowMdPath, frontMdAddrs);
54
- this.listener = listener;
55
55
  this.tradingDay = 0;
56
56
  this.recordings = new Set();
57
57
  this.symbols = new Map();
58
58
  this.lastTicks = new Map();
59
59
  this.subscribers = new Map();
60
+
61
+ if (options?.listener) {
62
+ this.listener = options.listener;
63
+ }
60
64
  }
61
65
 
62
66
  getRecorder() {
@@ -75,6 +79,10 @@ export class Market
75
79
  this.recorderSymbols = symbols;
76
80
  }
77
81
 
82
+ getLastTick(instrumentId: string) {
83
+ return this.lastTicks.get(instrumentId);
84
+ }
85
+
78
86
  open(lifecycle: ILifecycleListener) {
79
87
  if (this.marketApi) {
80
88
  return true;
@@ -423,5 +431,5 @@ export class Market
423
431
  export const createMarket = (
424
432
  flowMdPath: string,
425
433
  frontMdAddrs: string | string[],
426
- listener?: IMarketListener,
427
- ) => new Market(flowMdPath, frontMdAddrs, listener);
434
+ options?: MarketOptions,
435
+ ) => new Market(flowMdPath, frontMdAddrs, options);
package/src/provider.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * https://github.com/shixiongfei/hft.js
10
10
  */
11
11
 
12
+ import fs from "node:fs";
12
13
  import ctp from "napi-ctp";
13
14
  import { ErrorType, ILifecycleListener } from "./interfaces.js";
14
15
 
@@ -19,6 +20,12 @@ export class CTPProvider {
19
20
  constructor(flowPath: string, frontAddrs: string | string[]) {
20
21
  this.flowPath = flowPath;
21
22
  this.frontAddrs = frontAddrs;
23
+
24
+ try {
25
+ fs.accessSync(this.flowPath);
26
+ } catch {
27
+ fs.mkdirSync(this.flowPath, { recursive: true });
28
+ }
22
29
  }
23
30
 
24
31
  private _sleep(ms: number) {
@@ -35,11 +42,12 @@ export class CTPProvider {
35
42
  return ctp.getLastRequestId();
36
43
  }
37
44
 
38
- if (-2 !== retval && -3 !== retval) {
39
- return retval;
45
+ if (-2 === retval || -3 === retval) {
46
+ await this._sleep(ms);
47
+ continue;
40
48
  }
41
49
 
42
- await this._sleep(ms);
50
+ return retval;
43
51
  }
44
52
  }
45
53
 
package/src/trader.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  import Denque from "denque";
13
13
  import ctp from "napi-ctp";
14
14
  import { CTPProvider } from "./provider.js";
15
- import { parseSymbol } from "./utils.js";
15
+ import { isValidPrice, parseSymbol } from "./utils.js";
16
16
  import {
17
17
  CommissionRate,
18
18
  InstrumentData,
@@ -24,8 +24,10 @@ import {
24
24
  OrderStatus,
25
25
  PositionData,
26
26
  PositionDetail,
27
+ PriceRange,
27
28
  ProductType,
28
29
  SideType,
30
+ TickData,
29
31
  TradeData,
30
32
  TradingAccount,
31
33
  Writeable,
@@ -47,15 +49,26 @@ import {
47
49
  ITradingAccountsReceiver,
48
50
  } from "./interfaces.js";
49
51
 
50
- type MarginRateQuery = { symbol: string; receiver: IMarginRateReceiver };
52
+ type PositionInfo = Writeable<PositionData>;
53
+ type OrderStat = Writeable<OrderStatistic>;
54
+
55
+ type MarginRateQuery = {
56
+ symbol: string;
57
+ receiver: IMarginRateReceiver;
58
+ };
51
59
 
52
60
  type CommissionRateQuery = {
53
61
  symbol: string;
54
62
  receiver: ICommissionRateReceiver;
55
63
  };
56
64
 
57
- type PositionInfo = Writeable<PositionData>;
58
- type OrderStat = Writeable<OrderStatistic>;
65
+ type MarketOrder = {
66
+ symbol: string;
67
+ offset: OffsetType;
68
+ side: SideType;
69
+ volume: number;
70
+ receiver: IPlaceOrderResultReceiver;
71
+ };
59
72
 
60
73
  export type CTPUserInfo = {
61
74
  BrokerID: string;
@@ -67,6 +80,14 @@ export type CTPUserInfo = {
67
80
  AppID: string;
68
81
  };
69
82
 
83
+ export type FastQueryLastTickFunc = (
84
+ instrumentId: string,
85
+ ) => TickData | undefined;
86
+
87
+ export type TraderOptions = {
88
+ fastQueryLastTick?: FastQueryLastTickFunc;
89
+ };
90
+
70
91
  export class Trader extends CTPProvider implements ITraderProvider {
71
92
  private traderApi?: ctp.Trader;
72
93
  private tradingDay: number;
@@ -75,6 +96,7 @@ export class Trader extends CTPProvider implements ITraderProvider {
75
96
  private orderRef: number;
76
97
  private accountsQueryTime: number;
77
98
  private positionDetailsChanged: boolean;
99
+ private readonly fastQueryLastTick?: FastQueryLastTickFunc;
78
100
  private readonly userInfo: CTPUserInfo;
79
101
  private readonly receivers: IOrderReceiver[];
80
102
  private readonly accounts: ctp.TradingAccountField[];
@@ -87,6 +109,8 @@ export class Trader extends CTPProvider implements ITraderProvider {
87
109
  private readonly commRates: Map<string, ctp.InstrumentCommissionRateField>;
88
110
  private readonly placeOrders: Map<number, IPlaceOrderResultReceiver>;
89
111
  private readonly cancelOrders: Map<number, ICancelOrderResultReceiver>;
112
+ private readonly marketOrdersQueue: Map<string, Denque<MarketOrder>>;
113
+ private readonly priceLimit: Map<string, PriceRange>;
90
114
  private readonly orderStatistics: Map<string, OrderStat>;
91
115
  private readonly marginRatesQueue: Denque<MarginRateQuery>;
92
116
  private readonly commRatesQueue: Denque<CommissionRateQuery>;
@@ -97,6 +121,7 @@ export class Trader extends CTPProvider implements ITraderProvider {
97
121
  flowTdPath: string,
98
122
  frontTdAddrs: string | string[],
99
123
  userInfo: CTPUserInfo,
124
+ options?: TraderOptions,
100
125
  ) {
101
126
  super(flowTdPath, frontTdAddrs);
102
127
  this.tradingDay = 0;
@@ -117,11 +142,17 @@ export class Trader extends CTPProvider implements ITraderProvider {
117
142
  this.commRates = new Map();
118
143
  this.placeOrders = new Map();
119
144
  this.cancelOrders = new Map();
145
+ this.marketOrdersQueue = new Map();
146
+ this.priceLimit = new Map();
120
147
  this.orderStatistics = new Map();
121
148
  this.marginRatesQueue = new Denque();
122
149
  this.commRatesQueue = new Denque();
123
150
  this.accountsQueue = new Denque();
124
151
  this.positionDetailsQueue = new Denque();
152
+
153
+ if (options?.fastQueryLastTick) {
154
+ this.fastQueryLastTick = options.fastQueryLastTick;
155
+ }
125
156
  }
126
157
 
127
158
  open(lifecycle: ILifecycleListener) {
@@ -136,6 +167,7 @@ export class Trader extends CTPProvider implements ITraderProvider {
136
167
  });
137
168
 
138
169
  this.traderApi.on(ctp.TraderEvent.FrontDisconnected, () => {
170
+ this._clearAllMarketOrders();
139
171
  this.placeOrders.clear();
140
172
  this.cancelOrders.clear();
141
173
  });
@@ -168,6 +200,7 @@ export class Trader extends CTPProvider implements ITraderProvider {
168
200
  this.marginRates.clear();
169
201
  this.commRates.clear();
170
202
  this.orderStatistics.clear();
203
+ this.priceLimit.clear();
171
204
  this.tradingDay = tradingDay;
172
205
  }
173
206
 
@@ -627,6 +660,75 @@ export class Trader extends CTPProvider implements ITraderProvider {
627
660
  },
628
661
  );
629
662
 
663
+ this.traderApi.on<ctp.DepthMarketDataField>(
664
+ ctp.TraderEvent.RspQryDepthMarketData,
665
+ (depthMarketData, options) => {
666
+ if (
667
+ this._isErrorResp(lifecycle, options, "query-depth-market-data-error")
668
+ ) {
669
+ this._clearAllMarketOrders();
670
+ return;
671
+ }
672
+
673
+ const isLimitPrice =
674
+ !isValidPrice(depthMarketData.BandingUpperPrice) ||
675
+ !isValidPrice(depthMarketData.BandingLowerPrice);
676
+
677
+ if (isLimitPrice) {
678
+ this.priceLimit.set(depthMarketData.InstrumentID, {
679
+ upper: depthMarketData.UpperLimitPrice,
680
+ lower: depthMarketData.LowerLimitPrice,
681
+ });
682
+ }
683
+
684
+ const queue = this.marketOrdersQueue.get(depthMarketData.InstrumentID);
685
+
686
+ if (!queue) {
687
+ return;
688
+ }
689
+
690
+ if (!queue.isEmpty()) {
691
+ const upperPrice = isLimitPrice
692
+ ? depthMarketData.UpperLimitPrice
693
+ : depthMarketData.BandingUpperPrice;
694
+
695
+ const lowerPrice = isLimitPrice
696
+ ? depthMarketData.LowerLimitPrice
697
+ : depthMarketData.BandingLowerPrice;
698
+
699
+ const orders = queue.toArray();
700
+
701
+ orders.forEach((order) => {
702
+ switch (order.side) {
703
+ case "long":
704
+ this._placeLimitOrder(
705
+ order.symbol,
706
+ order.offset,
707
+ order.side,
708
+ order.volume,
709
+ upperPrice,
710
+ order.receiver,
711
+ );
712
+ break;
713
+
714
+ case "short":
715
+ this._placeLimitOrder(
716
+ order.symbol,
717
+ order.offset,
718
+ order.side,
719
+ order.volume,
720
+ lowerPrice,
721
+ order.receiver,
722
+ );
723
+ break;
724
+ }
725
+ });
726
+ }
727
+
728
+ this.marketOrdersQueue.delete(depthMarketData.InstrumentID);
729
+ },
730
+ );
731
+
630
732
  return true;
631
733
  }
632
734
 
@@ -862,21 +964,15 @@ export class Trader extends CTPProvider implements ITraderProvider {
862
964
  receiver.onOrders(orders);
863
965
  }
864
966
 
865
- placeOrder(
967
+ private _placeLimitOrder(
866
968
  symbol: string,
867
969
  offset: OffsetType,
868
970
  side: SideType,
869
971
  volume: number,
870
972
  price: number,
871
- flag: OrderFlag,
872
973
  receiver: IPlaceOrderResultReceiver,
873
974
  ) {
874
- if (flag !== "limit") {
875
- receiver.onPlaceOrderError("Only Supports Limit Order");
876
- return;
877
- }
878
-
879
- const [instrumentId] = parseSymbol(symbol);
975
+ const [instrumentId, exchangeId] = parseSymbol(symbol);
880
976
  const instrument = this.instruments.get(instrumentId);
881
977
 
882
978
  if (!instrument) {
@@ -884,6 +980,11 @@ export class Trader extends CTPProvider implements ITraderProvider {
884
980
  return;
885
981
  }
886
982
 
983
+ if (exchangeId !== instrument.ExchangeID) {
984
+ receiver.onPlaceOrderError("Exchange Id Error");
985
+ return;
986
+ }
987
+
887
988
  let orderRef = 0;
888
989
 
889
990
  this._withRetry(() => {
@@ -908,12 +1009,7 @@ export class Trader extends CTPProvider implements ITraderProvider {
908
1009
  UserForceClose: 0,
909
1010
  });
910
1011
  }).then((requestId) => {
911
- if (!requestId) {
912
- receiver.onPlaceOrderError("Request Failed");
913
- return;
914
- }
915
-
916
- if (requestId < 0) {
1012
+ if (!requestId || requestId < 0) {
917
1013
  receiver.onPlaceOrderError("Request Error");
918
1014
  return;
919
1015
  }
@@ -932,6 +1028,173 @@ export class Trader extends CTPProvider implements ITraderProvider {
932
1028
  });
933
1029
  }
934
1030
 
1031
+ private _clearAllMarketOrders() {
1032
+ this.marketOrdersQueue.forEach((queue) => {
1033
+ const orders = queue.toArray();
1034
+
1035
+ orders.forEach((order) => {
1036
+ order.receiver.onPlaceOrderError("Request Error");
1037
+ });
1038
+ });
1039
+
1040
+ this.marketOrdersQueue.clear();
1041
+ }
1042
+
1043
+ private _placeMarketOrder(
1044
+ symbol: string,
1045
+ offset: OffsetType,
1046
+ side: SideType,
1047
+ volume: number,
1048
+ receiver: IPlaceOrderResultReceiver,
1049
+ ) {
1050
+ const [instrumentId, exchangeId] = parseSymbol(symbol);
1051
+ const instrument = this.instruments.get(instrumentId);
1052
+
1053
+ if (!instrument) {
1054
+ receiver.onPlaceOrderError("Instrument Not Found");
1055
+ return;
1056
+ }
1057
+
1058
+ if (exchangeId !== instrument.ExchangeID) {
1059
+ receiver.onPlaceOrderError("Exchange Id Error");
1060
+ return;
1061
+ }
1062
+
1063
+ const priceRange = this.priceLimit.get(instrumentId);
1064
+
1065
+ if (priceRange) {
1066
+ switch (side) {
1067
+ case "long":
1068
+ this._placeLimitOrder(
1069
+ symbol,
1070
+ offset,
1071
+ side,
1072
+ volume,
1073
+ priceRange.upper,
1074
+ receiver,
1075
+ );
1076
+ break;
1077
+
1078
+ case "short":
1079
+ this._placeLimitOrder(
1080
+ symbol,
1081
+ offset,
1082
+ side,
1083
+ volume,
1084
+ priceRange.lower,
1085
+ receiver,
1086
+ );
1087
+ break;
1088
+ }
1089
+
1090
+ return;
1091
+ }
1092
+
1093
+ if (this.fastQueryLastTick) {
1094
+ const lastTick = this.fastQueryLastTick(instrumentId);
1095
+
1096
+ if (lastTick && lastTick.tradingDay === this.tradingDay) {
1097
+ const isLimitPrice =
1098
+ !isValidPrice(lastTick.bandings.upper) ||
1099
+ !isValidPrice(lastTick.bandings.lower);
1100
+
1101
+ if (isLimitPrice) {
1102
+ this.priceLimit.set(instrumentId, { ...lastTick.limits });
1103
+ }
1104
+
1105
+ const upperPrice = isLimitPrice
1106
+ ? lastTick.limits.upper
1107
+ : lastTick.bandings.upper;
1108
+
1109
+ const lowerPrice = isLimitPrice
1110
+ ? lastTick.limits.lower
1111
+ : lastTick.bandings.lower;
1112
+
1113
+ switch (side) {
1114
+ case "long":
1115
+ this._placeLimitOrder(
1116
+ symbol,
1117
+ offset,
1118
+ side,
1119
+ volume,
1120
+ upperPrice,
1121
+ receiver,
1122
+ );
1123
+ break;
1124
+
1125
+ case "short":
1126
+ this._placeLimitOrder(
1127
+ symbol,
1128
+ offset,
1129
+ side,
1130
+ volume,
1131
+ lowerPrice,
1132
+ receiver,
1133
+ );
1134
+ break;
1135
+ }
1136
+
1137
+ return;
1138
+ }
1139
+ }
1140
+
1141
+ let queue = this.marketOrdersQueue.get(instrumentId);
1142
+
1143
+ if (queue) {
1144
+ queue.push({ symbol, offset, side, volume, receiver });
1145
+ return;
1146
+ }
1147
+
1148
+ queue = new Denque();
1149
+ this.marketOrdersQueue.set(instrumentId, queue);
1150
+
1151
+ this._withRetry(() =>
1152
+ this.traderApi?.reqQryDepthMarketData({
1153
+ ExchangeID: exchangeId,
1154
+ InstrumentID: instrumentId,
1155
+ }),
1156
+ ).then((requestId) => {
1157
+ if (!requestId || requestId < 0) {
1158
+ const orders = queue.toArray();
1159
+
1160
+ orders.forEach((order) => {
1161
+ order.receiver.onPlaceOrderError("Request Error");
1162
+ });
1163
+
1164
+ this.marketOrdersQueue.delete(instrumentId);
1165
+
1166
+ return;
1167
+ }
1168
+
1169
+ queue.push({ symbol, offset, side, volume, receiver });
1170
+ });
1171
+ }
1172
+
1173
+ placeOrder(
1174
+ symbol: string,
1175
+ offset: OffsetType,
1176
+ side: SideType,
1177
+ volume: number,
1178
+ price: number,
1179
+ flag: OrderFlag,
1180
+ receiver: IPlaceOrderResultReceiver,
1181
+ ) {
1182
+ switch (flag) {
1183
+ case "limit":
1184
+ return this._placeLimitOrder(
1185
+ symbol,
1186
+ offset,
1187
+ side,
1188
+ volume,
1189
+ price,
1190
+ receiver,
1191
+ );
1192
+
1193
+ case "market":
1194
+ return this._placeMarketOrder(symbol, offset, side, volume, receiver);
1195
+ }
1196
+ }
1197
+
935
1198
  cancelOrder(order: OrderData, receiver: ICancelOrderResultReceiver) {
936
1199
  const current = this.orders.get(order.id);
937
1200
 
@@ -957,12 +1220,7 @@ export class Trader extends CTPProvider implements ITraderProvider {
957
1220
  ActionFlag: ctp.ActionFlagType.Delete,
958
1221
  }),
959
1222
  ).then((requestId) => {
960
- if (!requestId) {
961
- receiver.onCancelOrderError("Request Failed");
962
- return;
963
- }
964
-
965
- if (requestId < 0) {
1223
+ if (!requestId || requestId < 0) {
966
1224
  receiver.onCancelOrderError("Request Error");
967
1225
  return;
968
1226
  }
@@ -1605,4 +1863,5 @@ export const createTrader = (
1605
1863
  flowTdPath: string,
1606
1864
  frontTdAddrs: string | string[],
1607
1865
  userInfo: CTPUserInfo,
1608
- ) => new Trader(flowTdPath, frontTdAddrs, userInfo);
1866
+ options?: TraderOptions,
1867
+ ) => new Trader(flowTdPath, frontTdAddrs, userInfo, options);
package/src/utils.ts CHANGED
@@ -17,6 +17,9 @@ import {
17
17
  IStrategy,
18
18
  } from "./interfaces.js";
19
19
 
20
+ export const isValidPrice = (x: number) => x !== Number.MAX_VALUE && x !== 0;
21
+ export const isValidVolume = (x: number) => x !== Number.MAX_VALUE && x !== 0;
22
+
20
23
  export const parseSymbol = (symbol: string): [string, string] => {
21
24
  const [instrumentId, exchangeId] = symbol.split(".");
22
25
  return [instrumentId, exchangeId];
@@ -130,7 +133,7 @@ export const buyOpen = (
130
133
  "long",
131
134
  volume,
132
135
  price,
133
- flag,
136
+ price > 0 && flag === "limit" ? "limit" : "market",
134
137
  receiver,
135
138
  );
136
139
 
@@ -151,7 +154,7 @@ export const buyClose = (
151
154
  "long",
152
155
  volume,
153
156
  price,
154
- flag,
157
+ price > 0 && flag === "limit" ? "limit" : "market",
155
158
  receiver,
156
159
  );
157
160
 
@@ -171,7 +174,7 @@ export const sellOpen = (
171
174
  "short",
172
175
  volume,
173
176
  price,
174
- flag,
177
+ price > 0 && flag === "limit" ? "limit" : "market",
175
178
  receiver,
176
179
  );
177
180
 
@@ -192,6 +195,6 @@ export const sellClose = (
192
195
  "short",
193
196
  volume,
194
197
  price,
195
- flag,
198
+ price > 0 && flag === "limit" ? "limit" : "market",
196
199
  receiver,
197
200
  );