const bunyan = require('bunyan');
const bunyanDebugStream = require('bunyan-debug-stream');
const Socket = require('./socket');
const sleep = async time => new Promise((resolve) => {
setTimeout(resolve, time);
});
/**
* 封装FutuQuant底层协议模块
*/
class FutuQuant {
/**
* Creates an instance of FutuQuant.
* @param {object} params 初始化参数
* @param {string} params.ip FutuOpenD服务IP
* @param {number} params.port FutuOpenD服务端口
* @param {number} params.userID 牛牛号
* @param {string} params.pwdMd5 解锁交易 md5
* @param {TrdMarket} [params.market] 市场环境,默认为港股环境,1港股2美股3大陆市场4香港A股通市场
* @param {TrdEnv} [params.env] 0为仿真环境,1为真实环境,2为回测环境,默认为1
* @param {object} [logger] 日志对象,若不传入,则使用bunyan.createLogger创建
* @memberof FutuQuant
*/
constructor(params, logger) {
if (typeof params !== 'object') throw new Error('传入参数类型错误');
// 处理参数
const {
ip,
port,
userID,
market,
pwdMd5,
env,
} = params;
if (!ip) throw new Error('必须指定FutuOpenD服务的ip');
if (!port) throw new Error('必须指定FutuOpenD服务的port');
if (!userID) throw new Error('必须指定FutuOpenD服务的牛牛号');
if (!pwdMd5) throw new Error('必须指定FutuOpenD服务的解锁 MD5');
this.logger = logger;
this.market = market || 1; // 当前市场环境,1港股2美股3大陆市场4香港A股通市场
this.userID = userID;
this.pwdMd5 = pwdMd5;
this.params = params;
this.env = env;
if (typeof this.env !== 'number') this.env = 1; // 0为仿真环境,1为真实环境,2为回测环境
// 处理日志
const methods = ['debug', 'info', 'warn', 'error', 'fatal', 'trace'];
if (this.logger) {
methods.forEach((key) => {
if (typeof this.logger[key] !== 'function') this.logger = null;
});
}
this.logger = this.logger || bunyan.createLogger({
name: 'sys',
streams: [{
level: 'debug',
type: 'raw',
serializers: bunyanDebugStream.serializers,
stream: bunyanDebugStream({ forceColor: true }),
}],
});
this.socket = new Socket(ip, port, this.logger); // 实例化的socket对象,所有行情拉取接口
this.inited = false; // 是否已经初始化
this.trdHeader = null; // 交易公共头部信息
this.timerKeepLive = null; // 保持心跳定时器
}
/**
* 初始化处理
*/
async init() {
if (this.inited) return;
await this.initConnect();
await this.limitExecTimes(30 * 1000, 10, async () => {
await this.trdUnlockTrade(true, this.pwdMd5); // 解锁交易密码
});
const { accID } = (await this.trdGetAccList())[0]; // 获取交易账户
await this.setCommonTradeHeader(this.env, accID, this.market); // 设置为港股的真实环境
this.inited = true;
}
/**
* 初始化连接,InitConnect.proto协议返回对象
* @typedef InitConnectResponse
* @property {number} serverVer FutuOpenD的版本号
* @property {number} loginUserID FutuOpenD登陆的牛牛用户ID
* @property {number} connID 此连接的连接ID,连接的唯一标识
* @property {string} connAESKey 此连接后续AES加密通信的Key,固定为16字节长字符串
* @property {number} keepAliveInterval 心跳保活间隔
*/
/**
* InitConnect.proto - 1001初始化连接
*
nodejs版本会根据返回的keepAliveInterval字段自动保持心跳连接,不再需要手动调用ft.keepLive()方法。
请求其它协议前必须等InitConnect协议先完成
若FutuOpenD配置了加密, “connAESKey”将用于后续协议加密
keepAliveInterval 为建议client发起心跳 KeepAlive 的间隔
* @async
* @param {object} params 初始化参数
* @param {number} params.clientVer 客户端版本号,clientVer = "."以前的数 * 100 + "."以后的,举例:1.1版本的clientVer为1 * 100 + 1 = 101,2.21版本为2 * 100 + 21 = 221
* @param {string} params.clientID 客户端唯一标识,无生具体生成规则,客户端自己保证唯一性即可
* @param {boolean} params.recvNotify 此连接是否接收市场状态、交易需要重新解锁等等事件通知,true代表接收,FutuOpenD就会向此连接推送这些通知,反之false代表不接收不推送
* @returns {InitConnectResponse}
*/
async initConnect(params) {
if (this.inited) throw new Error('请勿重复初始化连接');
return new Promise(async (resolve) => {
this.socket.onConnect(async () => {
const res = await this.socket.send('InitConnect', Object.assign({
clientVer: 101,
clientID: 'yisbug',
recvNotify: true,
}, params));
// 保持心跳
this.connID = res.connID;
this.connAESKey = res.connAESKey;
this.keepAliveInterval = res.keepAliveInterval;
if (this.timerKeepLive) {
clearInterval(this.timerKeepLive);
this.timerKeepLive = null;
}
this.timerKeepLive = setInterval(() => this.keepAlive(), 1000 * this.keepAliveInterval);
resolve(res);
});
await this.socket.init();
});
}
/**
* 断开连接
*/
close() {
if (this.timerKeepLive) {
clearInterval(this.timerKeepLive);
this.socket.close();
this.inited = false;
}
}
/**
* GetGlobalState.proto协议返回对象
* @typedef GetGlobalStateResponse
* @property {QotMarketState} marketHK Qot_Common.QotMarketState,港股主板市场状态
* @property {QotMarketState} marketUS Qot_Common.QotMarketState,美股Nasdaq市场状态
* @property {QotMarketState} marketSH Qot_Common.QotMarketState,沪市状态
* @property {QotMarketState} marketSZ Qot_Common.QotMarketState,深市状态
* @property {QotMarketState} marketHKFuture Qot_Common.QotMarketState,港股期货市场状态
* @property {boolean} qotLogined 是否登陆行情服务器
* @property {boolean} trdLogined 是否登陆交易服务器
* @property {number} serverVer 版本号
* @property {number} serverBuildNo buildNo
* @property {number} time 当前格林威治时间
*/
/**
* GetGlobalState.proto - 1002获取全局状态
* @async
* @returns {GetGlobalStateResponse}
*/
getGlobalState() {
return this.socket.send('GetGlobalState', {
userID: this.userID,
});
}
/**
* KeepAlive.proto - 1004保活心跳
* @returns {number} time 服务器回包时的格林威治时间戳,单位秒
*/
async keepAlive() {
const time = await this.socket.send('KeepAlive', {
time: Math.round(Date.now() / 1000),
});
return time;
}
/**
* Qot_Sub.proto - 3001订阅或者反订阅
*
股票结构参考 Security
订阅数据类型参考 SubType
复权类型参考 RehabType
为控制定阅产生推送数据流量,股票定阅总量有额度控制,订阅规则参考 高频数据接口
高频数据接口需要订阅之后才能使用,注册推送之后才可以收到数据更新推送
* @param {object} params
* @param {Security[]} params.securityList 股票
* @param {SubType[]} params.subTypeList Qot_Common.SubType,订阅数据类型
* @param {boolean} [params.isSubOrUnSub=true] ture表示订阅,false表示反订阅
* @param {boolean} [params.isRegOrUnRegPush=true] 是否注册或反注册该连接上面行情的推送,该参数不指定不做注册反注册操作
* @param {number} params.regPushRehabTypeList Qot_Common.RehabType,复权类型,注册推送并且是K线类型才生效,其他订阅类型忽略该参数,注册K线推送时该参数不指定默认前复权
* @param {boolean} [params.isFirstPush=true] 注册后如果本地已有数据是否首推一次已存在数据,该参数不指定则默认true
* @async
*/
qotSub(params) {
return this.socket.send('Qot_Sub', Object.assign({
securityList: [],
subTypeList: [],
isSubOrUnSub: true,
isRegOrUnRegPush: true,
regPushRehabTypeList: [],
isFirstPush: true,
}, params));
}
/**
* Qot_RegQotPush.proto - 3002注册行情推送
*
股票结构参考 Security
订阅数据类型参考 SubType
复权类型参考 RehabType
行情需要订阅成功才能注册推送
* @param {object} params Object
* @param {Security[]} params.securityList 股票
* @param {SubType[]} params.subTypeList Qot_Common.SubType,订阅数据类型
* @param {SubType[]} params.rehabTypeList Qot_Common.RehabType,复权类型,注册K线类型才生效,其他订阅类型忽略该参数,注册K线时该参数不指定默认前复权
* @param {boolean} [params.isRegOrUnReg=true] 注册或取消
* @param {boolean} [params.isFirstPush=true] 注册后如果本地已有数据是否首推一次已存在数据,该参数不指定则默认true
* @async
*/
qotRegQotPush(params) { // 3002注册行情推送
return this.socket.send('Qot_RegQotPush', Object.assign({
securityList: [],
subTypeList: [],
rehabTypeList: [],
isRegOrUnReg: true,
isFirstPush: true,
}, params));
}
/**
* Qot_GetSubInfo.proto协议返回对象
* @typedef QotGetSubInfoResponse
* @property {ConnSubInfo[]} connSubInfoList 订阅信息
* @property {number} totalUsedQuota FutuOpenD已使用的订阅额度
* @property {number} remainQuota FutuOpenD剩余订阅额度
*/
/**
* Qot_GetSubInfo.proto - 3003获取订阅信息
* @async
* @param {boolean} [isReqAllConn=false] 是否返回所有连接的订阅状态,默认false
* @returns {QotGetSubInfoResponse}
*/
qotGetSubInfo(isReqAllConn = false) { // 3003获取订阅信息
return this.socket.send('Qot_RegQotPush', {
isReqAllConn,
});
}
/**
* Qot_GetBasicQot.proto - 3004获取股票基本行情
*
股票结构参考 Security
基本报价结构参考 BasicQot
* @param {Security[]} securityList 股票列表
* @returns {BasicQot[]} basicQotList 股票基本报价
* @async
*/
async qotGetBasicQot(securityList) { // 3004获取股票基本行情
return (await this.socket.send('Qot_GetBasicQot', {
securityList,
})).basicQotList || [];
}
/**
* 注册股票基本报价通知,需要先调用订阅接口
* Qot_UpdateBasicQot.proto - 3005推送股票基本报价
* @async
* @param {function} callback 回调
* @returns {BasicQot[]} basicQotList
*/
subQotUpdateBasicQot(callback) { // 注册股票基本报价通知
return this.socket.subNotify(3005, data => callback(data.basicQotList || []));
}
/**
* Qot_GetKL.proto - 3006获取K线
*
复权类型参考 RehabType
K线类型参考 KLType
股票结构参考 Security
K线结构参考 KLine
请求K线目前最多最近1000根
* @param {object} params
* @param {RehabType} params.rehabType Qot_Common.RehabType,复权类型
* @param {KLType} params.klType Qot_Common.KLType,K线类型
* @param {Security} params.security 股票
* @param {number} params.reqNum 请求K线根数
* @async
* @returns {KLine[]} k线点
*/
async qotGetKL(params) { // 3006获取K线
return (await this.socket.send('Qot_GetKL', Object.assign({
rehabType: 1, // Qot_Common.RehabType,复权类型
klType: 1, // Qot_Common.KLType,K线类型
security: {}, // 股票
reqNum: 60, // 请求K线根数
}, params))).klList || [];
}
/**
* Qot_UpdateKL.proto协议返回对象
* @typedef QotUpdateKLResponse
* @property {RehabType} rehabType Qot_Common.RehabType,复权类型
* @property {KLType} klType Qot_Common.KLType,K线类型
* @property {Security} security 股票
* @property {KLine[]} klList 推送的k线点
*/
/**
* 注册K线推送,需要先调用订阅接口
* Qot_UpdateKL.proto - 3007推送K线
* @async
* @returns {QotUpdateKLResponse} 推送的k线点
*/
subQotUpdateKL(callback) { // 注册K线推送
return this.socket.subNotify(3007, callback);
}
/**
* Qot_GetRT.proto - 3008获取分时
* @async
* @param {Security} security 股票
* @returns {TimeShare[]} 分时点
*/
async qotGetRT(security) { // 获取分时
return (await this.socket.send('Qot_GetRT', {
security,
})).rtList || [];
}
/**
* 注册分时推送,需要先调用订阅接口
* Qot_UpdateRT.proto - 3009推送分时
* @async
* @returns {TimeShare[]} 分时点
*/
subQotUpdateRT(callback) { // 注册分时推送
return this.socket.subNotify(3009, data => callback(data.rtList || []));
}
/**
* Qot_GetTicker.proto - 3010获取逐笔
*
股票结构参考 Security
逐笔结构参考 Ticker
请求逐笔目前最多最近1000个
* @param {Security} security 股票
* @param {number} maxRetNum 最多返回的逐笔个数,实际返回数量不一定会返回这么多,最多返回1000个,默认100
* @returns {Ticker[]} 逐笔
* @async
*/
async qotGetTicker(security, maxRetNum = 100) { // 3010获取逐笔
return (await this.socket.send('Qot_GetTicker', {
security,
maxRetNum,
})).tickerList || [];
}
/**
* Qot_GetTicker.proto协议返回对象
* @typedef subQotUpdateTickerResponse
* @property {Security} security 股票
* @property {Ticker[]} tickerList 逐笔
*/
/**
* 注册逐笔推送,需要先调用订阅接口
* Qot_UpdateTicker.proto - 3011推送逐笔
* @async
* @param {function} callback 回调
* @returns {subQotUpdateTickerResponse} 逐笔
*/
subQotUpdateTicker(callback) { // 注册逐笔推送
return this.socket.subNotify(3011, callback);
}
/**
* Qot_GetOrderBook.proto协议返回对象
* @typedef QotGetOrderBookResponse
* @property {Security} security 股票
* @property {OrderBook[]} orderBookAskList 卖盘
* @property {OrderBook[]} sellList 卖盘,同orderBookAskList
* @property {OrderBook[]} orderBookBidList 买盘
* @property {OrderBook[]} buyList 买盘,同orderBookBidList
*/
/**
* Qot_GetOrderBook.proto - 3012获取买卖盘,需要先调用订阅接口
* @async
* @param {Security} security 股票
* @param {number} num 请求的摆盘个数(1-10),默认10
* @returns {QotGetOrderBookResponse}
*/
async qotGetOrderBook(security, num = 10) { // 3012获取买卖盘
const result = await this.socket.send('Qot_GetOrderBook', {
security,
num,
});
result.orderBookAskList = result.orderBookAskList || [];
result.orderBookBidList = result.orderBookBidList || [];
result.sellList = result.orderBookAskList;
result.buyList = result.orderBookBidList;
result.sellList.forEach((item) => { item.volume = Number(item.volume); });
result.buyList.forEach((item) => { item.volume = Number(item.volume); });
return result;
}
/**
* 注册买卖盘推送,需要先调用订阅接口
* Qot_UpdateOrderBook.proto - 3013推送买卖盘
* @async
* @param {function} callback 回调
* @const {QotGetOrderBookResponse}
*/
subQotUpdateOrderBook(callback) { // 注册买卖盘推送
return this.socket.subNotify(3013, (data) => {
data.sellList = data.orderBookAskList || [];
data.buyList = data.orderBookBidList || [];
data.sellList.forEach((item) => { item.volume = Number(item.volume); });
data.buyList.forEach((item) => { item.volume = Number(item.volume); });
callback(data);
});
}
/**
* Qot_GetBroker.proto协议返回对象
* @typedef QotGetBrokerResponse
* @property {Security} security 股票
* @property {Broker[]} brokerAskList 经纪Ask(卖)盘
* @property {Broker[]} sellList 经纪Ask(卖)盘,同brokerAskList
* @property {Broker[]} brokerBidList 经纪Bid(买)盘
* @property {Broker[]} buyList 经纪Bid(买)盘,同brokerBidList
*/
/**
* Qot_GetBroker.proto - 3014获取经纪队列
* @async
* @param {Security} security Object 股票
* @returns {QotGetBrokerResponse}
*/
async qotGetBroker(security) { // 3014获取经纪队列
const result = await this.socket.send('Qot_GetBroker', {
security,
});
result.brokerAskList = result.brokerAskList || [];
result.brokerBidList = result.brokerBidList || [];
result.sellList = result.brokerAskList;
result.buyList = result.brokerBidList;
return result;
}
/**
* 注册经纪队列推送,需要先调用订阅接口
* Qot_UpdateBroker.proto - 3015推送经纪队列
* @async
* @param {function} callback 回调
* @returns {QotGetBrokerResponse}
*/
subQotUpdateBroker(callback) { // 注册经纪队列推送
return this.socket.subNotify(3015, (result) => {
result.brokerAskList = result.brokerAskList || [];
result.brokerBidList = result.brokerBidList || [];
result.sellList = result.brokerAskList;
result.buyList = result.brokerBidList;
callback(result);
});
}
/**
* Qot_GetHistoryKL.proto - 3100获取单只股票一段历史K线
* @async
* @param {object} params
* @param {RehabType} params.rehabType Qot_Common.RehabType,复权类型
* @param {KLType} params.klType Qot_Common.KLType,K线类型
* @param {Security} params.security 股票市场以及股票代码
* @param {string} params.beginTime 开始时间字符串
* @param {string} params.endTime 结束时间字符串
* @param {number} [params.maxAckKLNum] 最多返回多少根K线,如果未指定表示不限制
* @param {number} [params.needKLFieldsFlag] 指定返回K线结构体特定某几项数据,KLFields枚举值或组合,如果未指定返回全部字段
* @returns {KLine[]}
*/
async qotGetHistoryKL(params) { // 3100获取单只股票一段历史K线
return (await this.socket.send('Qot_GetHistoryKL', Object.assign({
rehabType: 1, // Qot_Common.RehabType,复权类型
klType: 1, // Qot_Common.KLType,K线类型
security: {}, // 股票市场以及股票代码
beginTime: '', // 开始时间字符串
endTime: '', // 结束时间字符串
// maxAckKLNum: 60, // 最多返回多少根K线,如果未指定表示不限制
// needKLFieldsFlag: 512, // 指定返回K线结构体特定某几项数据,KLFields枚举值或组合,如果未指定返回全部字段
}, params))).klList || [];
}
/**
* 当请求时间点数据为空时,如何返回数据
*
NoDataMode_Null = 0; //直接返回空数据
NoDataMode_Forward = 1; //往前取值,返回前一个时间点数据
NoDataMode_Backward = 2; //向后取值,返回后一个时间点数据
* @typedef {number} NoDataMode
*/
/**
* 这个时间点返回数据的状态以及来源
*
DataStatus_Null = 0; //空数据
DataStatus_Current = 1; //当前时间点数据
DataStatus_Previous = 2; //前一个时间点数据
DataStatus_Back = 3; //后一个时间点数据
* @typedef {number} DataStatus
*/
/**
* K线数据
*
* @typedef HistoryPointsKL
* @property {DataStatus} status DataStatus,数据状态
* @property {string} reqTime 请求的时间
* @property {KLine} kl K线数据
*/
/**
* 多只股票的多点历史K线点
*
* @typedef SecurityHistoryKLPoints
* @property {Security} security 股票
* @property {HistoryPointsKL} klList K线数据
*/
/**
* Qot_GetHistoryKLPoints.proto - 3101获取多只股票多点历史K线
*
复权类型参考 RehabType
K线类型参考 KLType
股票结构参考 Security
K线结构参考 KLine
K线字段类型参考 KLFields
目前限制最多5个时间点,股票个数不做限制,但不建议传入过多股票,查询耗时过多会导致协议返回超时。
* @async
* @param {object} params
* @param {RehabType} params.rehabType Qot_Common.RehabType,复权类型
* @param {KLType} params.klType Qot_Common.KLType,K线类型
* @param {NoDataMode} params.noDataMode NoDataMode,当请求时间点数据为空时,如何返回数据
* @param {Security[]} params.securityList 股票市场以及股票代码
* @param {string[]} params.timeList 时间字符串
* @param {number} [params.maxReqSecurityNum] 最多返回多少只股票的数据,如果未指定表示不限制
* @param {KLFields} [params.needKLFieldsFlag] 指定返回K线结构体特定某几项数据,KLFields枚举值或组合,如果未指定返回全部字段
* @returns {SecurityHistoryKLPoints[]}
*/
qotGetHistoryKLPoints(params) { // 3101获取多只股票多点历史K线
return this.socket.send('Qot_GetHistoryKLPoints', Object.assign({
rehabType: 1, // Qot_Common.RehabType,复权类型
klType: 1, // Qot_Common.KLType,K线类型
noDataMode: 0, // NoDataMode,当请求时间点数据为空时,如何返回数据。0
securityList: [], // 股票市场以及股票代码
timeList: [], // 时间字符串
maxReqSecurityNum: 60, // 最多返回多少只股票的数据,如果未指定表示不限制
needKLFieldsFlag: 512, // 指定返回K线结构体特定某几项数据,KLFields枚举值或组合,如果未指定返回全部字段
}, params)).klPointList || [];
}
/**
* 公司行动组合,指定某些字段值是否有效
*
CompanyAct_None = 0; //无
CompanyAct_Split = 1; //拆股
CompanyAct_Join = 2; //合股
CompanyAct_Bonus = 4; //送股
CompanyAct_Transfer = 8; //转赠股
CompanyAct_Allot = 16; //配股
CompanyAct_Add = 32; //增发股
CompanyAct_Dividend = 64; //现金分红
CompanyAct_SPDividend = 128; //特别股息
* @typedef {number} CompanyAct
*/
/**
* 复权信息
*
* @typedef Rehab
* @property {string} time 时间字符串
* @property {CompanyAct} companyActFlag 公司行动组合,指定某些字段值是否有效
* @property {number} fwdFactorA 前复权因子A
* @property {number} fwdFactorB 前复权因子B
* @property {number} bwdFactorA 后复权因子A
* @property {number} bwdFactorB 后复权因子B
* @property {number} [splitBase] 拆股(eg.1拆5,Base为1,Ert为5)
* @property {number} [splitErt]
* @property {number} [joinBase] 合股(eg.50合1,Base为50,Ert为1)
* @property {number} [joinErt]
* @property {number} [bonusBase] 送股(eg.10送3, Base为10,Ert为3)
* @property {number} [bonusErt]
* @property {number} [transferBase] 转赠股(eg.10转3, Base为10,Ert为3)
* @property {number} [transferErt]
* @property {number} [allotBase] 配股(eg.10送2, 配股价为6.3元, Base为10, Ert为2, Price为6.3)
* @property {number} [allotErt]
* @property {number} [allotPrice]
* @property {number} [addBase] 增发股(eg.10送2, 增发股价为6.3元, Base为10, Ert为2, Price为6.3)
* @property {number} [addErt]
* @property {number} [addPrice]
* @property {number} [dividend] 现金分红(eg.每10股派现0.5元,则该字段值为0.05)
* @property {number} [spDividend] 特别股息(eg.每10股派特别股息0.5元,则该字段值为0.05)
*/
/**
* 股票复权信息
*
* @typedef SecurityRehab
* @property {Security} security 股票
* @property {Rehab[]} rehabList 复权信息
*/
/**
* Qot_GetRehab.proto - 3102获取复权信息
* @async
* @param {Security[]} securityList 股票列表
* @returns {SecurityRehab[]} securityRehabList 多支股票的复权信息
*/
qotGetRehab(securityList) { // 3102获取复权信息
return this.socket.send('Qot_GetRehab', {
securityList,
});
}
/**
* TradeDate
*
* @typedef TradeDate
* @property {string} time 时间字符串
*/
/**
* Qot_GetTradeDate.proto - 3200获取市场交易日
* @async
* @param {QotMarket} market Qot_Common.QotMarket,股票市场
* @param {string} beginTime 开始时间字符串 2018-01-01 00:00:00
* @param {string} endTime 结束时间字符串 2018-02-01 00:00:00
* @return {TradeDate[]} tradeDateList 交易日
*/
async qotGetTradeDate(market = 1, beginTime, endTime) { // 3200获取市场交易日
return (await this.socket.send('Qot_GetTradeDate', {
market,
beginTime,
endTime,
})).tradeDateList || [];
}
/**
* Qot_GetStaticInfo.proto - 3202获取股票静态信息
* @async
* @param {QotMarket} market Qot_Common.QotMarket,股票市场
* @param {SecurityType} secType Qot_Common.SecurityType,股票类型
* @returns {SecurityStaticInfo[]} 静态信息数组
*/
async qotGetStaticInfo(market = 1, secType) { // 3202获取股票静态信息
return (await this.socket.send('Qot_GetStaticInfo', {
market,
secType,
})).staticInfoList || [];
}
/**
* 正股类型额外数据
* @typedef EquitySnapshotExData
* @property {number} issuedShares 发行股本,即总股本
* @property {number} issuedMarketVal 总市值 =总股本*当前价格
* @property {number} netAsset 资产净值
* @property {number} netProfit 盈利(亏损)
* @property {number} earningsPershare 每股盈利
* @property {number} outstandingShares 流通股本
* @property {number} outstandingMarketVal 流通市值 =流通股本*当前价格
* @property {number} netAssetPershare 每股净资产
* @property {number} eyRate 收益率
* @property {number} peRate 市盈率
* @property {number} pbRate 市净率
*/
/**
* 涡轮类型额外数据
* @typedef WarrantSnapshotExData
* @property {number} conversionRate 换股比率
* @property {WarrantType} warrantType Qot_Common.WarrantType,涡轮类型
* @property {number} strikePrice 行使价
* @property {string} maturityTime 到期日时间字符串
* @property {string} endTradeTime 最后交易日时间字符串
* @property {Security} owner 所属正股
* @property {number} recoveryPrice 回收价
* @property {number} streetVolumn 街货量
* @property {number} issueVolumn 发行量
* @property {number} streetRate 街货占比
* @property {number} delta 对冲值
* @property {number} impliedVolatility 引申波幅
* @property {number} premium 溢价
*/
/**
* 基本快照数据
* @typedef SnapshotBasicData
* @property {Security} security 股票
* @property {SecurityType} type Qot_Common.SecurityType,股票类型
* @property {boolean} isSuspend 是否停牌
* @property {string} listTime 上市时间字符串
* @property {number} lotSize 每手数量
* @property {number} priceSpread 价差
* @property {string} updateTime 更新时间字符串
* @property {number} highPrice 最新价
* @property {number} openPrice 开盘价
* @property {number} lowPrice 最低价
* @property {number} lastClosePrice 昨收价
* @property {number} curPrice 最新价
* @property {number} volume 成交量
* @property {number} turnover 成交额
* @property {number} turnoverRate 换手率
*/
/**
* 快照
* @typedef Snapshot
* @property {SnapshotBasicData} basic 快照基本数据
* @property {EquitySnapshotExData} [equityExData] 正股快照额外数据
* @property {WarrantSnapshotExData} [warrantExData] 窝轮快照额外数据
*/
/**
* Qot_GetSecuritySnapshot.proto - 3203获取股票快照
*
股票结构参考 Security
限频接口:30秒内最多10次
最多可传入200只股票
* @async
* @param {Security[]} securityList 股票列表
* @returns {Snapshot[]} snapshotList 股票快照
*/
async qotGetSecuritySnapShot(securityList) { // 3203获取股票快照
const list = [].concat(securityList);
let snapshotList = [];
while (list.length) {
const res = await this.limitExecTimes(
30 * 1000, 10,
async () => {
const data = await this.socket.send('Qot_GetSecuritySnapshot', {
securityList: list.splice(-200),
});
return data.snapshotList;
},
);
snapshotList = snapshotList.concat(res);
}
return snapshotList;
}
/**
* 限制接口调用频率
* @param {Number} interval 限频间隔
* @param {Number} times 次数
* @param {Function} fn 要执行的函数
*/
async limitExecTimes(interval, times, fn) {
const now = Date.now();
const name = `${fn.toString()}_exec_time_array`;
const execArray = this[name] || [];
while (execArray[0] && now - execArray[0] > interval) {
execArray.shift();
}
if (execArray.length > times) {
await this.sleep(interval - (now - execArray[0]));
}
execArray.push(Date.now());
this[name] = execArray;
return fn();
}
/**
* PlateInfo
* @typedef PlateInfo
* @property {Security} plate 板块
* @property {string} name 板块名字
*/
/**
* Qot_GetPlateSet.proto - 3204获取板块集合下的板块
* @async
* @param {QotMarket} market Qot_Common.QotMarket,股票市场
* @param {PlateSetType} plateSetType Qot_Common.PlateSetType,板块集合的类型
* @returns {PlateInfo[]} 板块集合下的板块信息
*/
async qotGetPlateSet(market = 1, plateSetType) { // 3204获取板块集合下的板块
return (await this.socket.send('Qot_GetPlateSet', {
market,
plateSetType,
})).plateInfoList || [];
}
/**
* Qot_GetPlateSecurity.proto - 3205获取板块下的股票
* @async
* @param {Security} plate 板块
* @returns {SecurityStaticInfo[]} 板块下的股票静态信息
*/
async qotGetPlateSecurity(plate) { // 3205获取板块下的股票
return (await this.socket.send('Qot_GetPlateSecurity', {
plate,
})).staticInfoList || [];
}
/**
* 股票类型
*
ReferenceType_Unknow = 0;
ReferenceType_Warrant = 1; //正股相关的窝轮
* @typedef {number} ReferenceType
*/
/**
* Qot_GetReference.proto - 3206 获取正股相关股票
* @async
* @param {security} security 股票
* @param {ReferenceType} [referenceType] 相关类型,默认为1,获取正股相关的涡轮
*/
async qotGetReference(security, referenceType = 1) {
return (await this.socket.send('Qot_GetReference', {
security,
referenceType,
})).staticInfoList || [];
}
/**
* Trd_GetAccList.proto - 2001获取交易账户列表
* @async
* @returns {TrdAcc[]} 交易业务账户列表
*/
async trdGetAccList() { // 2001获取交易账户列表
const {
accList,
} = (await this.socket.send('Trd_GetAccList', {
userID: this.userID,
}));
return accList.filter(acc => acc.trdMarketAuthList.includes(this.market) && acc.trdEnv === this.env);
}
/**
* Trd_UnlockTrade.proto - 2005解锁或锁定交易
*
除2001协议外,所有交易协议请求都需要FutuOpenD先解锁交易
密码MD5方式获取请参考 FutuOpenD配置 内的login_pwd_md5字段
解锁或锁定交易针对与FutuOpenD,只要有一个连接解锁,其他连接都可以调用交易接口
强烈建议有实盘交易的用户使用加密通道,参考 加密通信流程
限频接口:30秒内最多10次
* @param {boolean} [unlock=true] true解锁交易,false锁定交易,默认true
* @param {string} [pwdMD5] 交易密码的MD5转16进制(全小写),解锁交易必须要填密码,锁定交易不需要验证密码,可不填
* @async
*/
trdUnlockTrade(unlock = true, pwdMD5 = '') { // 2005解锁或锁定交易
if (pwdMD5) this.pwdMD5 = pwdMD5;
return this.socket.send('Trd_UnlockTrade', {
unlock,
pwdMD5: pwdMD5 || this.pwdMD5,
});
}
/**
* Trd_SubAccPush.proto - 2008订阅接收交易账户的推送数据
* @async
* @param {number[]} accIDList 要接收推送数据的业务账号列表,全量非增量,即使用者请每次传需要接收推送数据的所有业务账号
*/
async trdSubAccPush(accIDList) { // 2008订阅接收交易账户的推送数据
return this.socket.send('Trd_SubAccPush', {
accIDList,
});
}
/**
* 设置交易模块的公共header,调用交易相关接口前必须先调用此接口。
* @param {TrdEnv} trdEnv 交易环境, 参见TrdEnv的枚举定义。0为仿真,1为真实,默认为1。
* @param {number} accID 业务账号, 业务账号与交易环境、市场权限需要匹配,否则会返回错误,默认为当前userID
* @param {TrdMarket} [trdMarket=1] 交易市场, 参见TrdMarket的枚举定义,默认为1,即香港市场。
*/
setCommonTradeHeader(trdEnv = 1, accID, trdMarket = 1) { // 设置交易模块的公共header,调用交易相关接口前必须先调用此接口。
this.market = trdMarket;
this.trdHeader = {
trdEnv,
accID,
trdMarket,
};
}
/**
* Trd_GetFunds.proto - 2101获取账户资金,需要先设置交易模块公共header
* @returns {Funds}
*/
async trdGetFunds() { // 2101获取账户资金
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_GetFunds', {
header: this.trdHeader,
})).funds;
}
/**
* Trd_GetPositionList.proto - 2102获取持仓列表
* @async
* @param {TrdFilterConditions} filterConditions 过滤条件
* @param {number} filterPLRatioMin 过滤盈亏比例下限,高于此比例的会返回,如0.1,返回盈亏比例大于10%的持仓
* @param {number} filterPLRatioMax 过滤盈亏比例上限,低于此比例的会返回,如0.2,返回盈亏比例小于20%的持仓
* @returns {Position[]} 持仓列表数组
*/
async trdGetPositionList(filterConditions, filterPLRatioMin, filterPLRatioMax) { // 2102获取持仓列表
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_GetPositionList', {
header: this.trdHeader, // 交易公共参数头
filterConditions, // 过滤条件
filterPLRatioMin, // 过滤盈亏比例下限,高于此比例的会返回,如0.1,返回盈亏比例大于10%的持仓
filterPLRatioMax, // 过滤盈亏比例上限,低于此比例的会返回,如0.2,返回盈亏比例小于20%的持仓
})).positionList || [];
}
/**
* Trd_GetMaxTrdQtys.proto - 2111获取最大交易数量
* @param {object} params
* @param {TrdHeader} [params.header] 交易公共参数头,默认不用填写
* @param {OrderType} params.orderType 订单类型, 参见Trd_Common.OrderType的枚举定义
* @param {string} params.code 代码
* @param {number} [params.price] 价格,3位精度(A股2位)
* @param {number} params.orderID 订单号,新下订单不需要,如果是修改订单就需要把原订单号带上才行,因为改单的最大买卖数量会包含原订单数量。
* 以下为调整价格使用,目前仅对港、A股有效,因为港股有价位,A股2位精度,美股不需要
* @param {boolean} [params.adjustPrice] 是否调整价格,如果价格不合法,是否调整到合法价位,true调整,false不调整
* @param {number} [params.adjustSideAndLimit] 调整方向和调整幅度百分比限制,正数代表向上调整,负数代表向下调整,具体值代表调整幅度限制,如:0.015代表向上调整且幅度不超过1.5%;-0.01代表向下调整且幅度不超过1%
* @returns {MaxTrdQtys} 最大交易数量结构体
*/
async trdGetMaxTrdQtys(params) {
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_GetMaxTrdQtys', Object.assign({
header: this.trdHeader, // 交易公共参数头
orderType: 1, // 订单类型, 参见Trd_Common.OrderType的枚举定义
code: '', // 代码
price: 0, // 价格,3位精度(A股2位)
orderID: 0, // 订单号,新下订单不需要,如果是修改订单就需要把原订单号带上才行,因为改单的最大买卖数量会包含原订单数量。
// 以下为调整价格使用,目前仅对港、A股有效,因为港股有价位,A股2位精度,美股不需要
adjustPrice: false, // 是否调整价格,如果价格不合法,是否调整到合法价位,true调整,false不调整
adjustSideAndLimit: 0, // 调整方向和调整幅度百分比限制,正数代表向上调整,负数代表向下调整,具体值代表调整幅度限制,如:0.015代表向上调整且幅度不超过1.5%;-0.01代表向下调整且幅度不超过1%
}, params))).maxTrdQtys;
}
/**
* Trd_GetOrderList.proto - 2201获取订单列表
* @async
* @param {TrdFilterConditions} filterConditions 过滤条件
* @param {OrderStatus[]} filterStatusList 需要过滤的订单状态列表
* @returns {Order[]} 订单列表
*/
async trdGetOrderList(filterConditions, filterStatusList) { // 2201获取订单列表
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_GetOrderList', {
header: this.trdHeader, // 交易公共参数头
filterConditions,
filterStatusList,
})).orderList || [];
}
/**
* Trd_PlaceOrder.proto - 2202下单
*
请求包标识结构参考 PacketID
交易公共参数头结构参考 TrdHeader
交易方向枚举参考 TrdSide
订单类型枚举参考 OrderType
限频接口:30秒内最多30次
* @async
* @param {object} params
* @param {PacketID} [params.packetID] 交易写操作防重放攻击,默认不用填写
* @param {TrdHeader} [params.header] 交易公共参数头,默认不用填写
* @param {TrdSide} params.trdSide 交易方向, 参见Trd_Common.TrdSide的枚举定义
* @param {OrderType} params.orderType 订单类型, 参见Trd_Common.OrderType的枚举定义
* @param {string} params.code 代码
* @param {number} params.qty 数量,2位精度,期权单位是"张"
* @param {number} [params.price] 价格,3位精度(A股2位)
* 以下为调整价格使用,目前仅对港、A股有效,因为港股有价位,A股2位精度,美股不需要
* @param {boolean} [params.adjustPrice] 是否调整价格,如果价格不合法,是否调整到合法价位,true调整,false不调整
* @param {number} [params.adjustSideAndLimit] 调整方向和调整幅度百分比限制,正数代表向上调整,负数代表向下调整,具体值代表调整幅度限制,如:0.015代表向上调整且幅度不超过1.5%;-0.01代表向下调整且幅度不超过1%
* @returns {number} orderID 订单号
*/
async trdPlaceOrder(params) { // 2202下单
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_PlaceOrder', Object.assign({
packetID: {
connID: this.connID,
serialNo: this.socket.requestId,
}, // 交易写操作防重放攻击
header: this.trdHeader, // 交易公共参数头
trdSide: 0, // 交易方向,1买入,2卖出
orderType: 1, // 订单类型, 参见Trd_Common.OrderType的枚举定义
code: '', // 代码
qty: 0, // 数量,2位精度,期权单位是"张"
price: 0, // 价格,3位精度(A股2位)
// 以下为调整价格使用,目前仅对港、A股有效,因为港股有价位,A股2位精度,美股不需要
adjustPrice: false, // 是否调整价格,如果价格不合法,是否调整到合法价位,true调整,false不调整
adjustSideAndLimit: 0, // 调整方向和调整幅度百分比限制,正数代表向上调整,负数代表向下调整,具体值代表调整幅度限制,如:0.015代表向上调整且幅度不超过1.5%;-0.01代表向下调整且幅度不超过1%
}, params))).orderID;
}
/**
* 2202市价下单,直到成功为止,返回买入/卖出的总价格
*
* @async
* @param {object} param
* @param {TrdSide} params.trdSide 交易方向, 参见Trd_Common.TrdSide的枚举定义
* @param {string} params.code 代码
* @param {number} params.qty 数量,2位精度,期权单位是"张"
* @returns {number} 卖出/买入总价
*/
async trdPlaceOrderMarket(param) { // 市价买入卖出
const { trdSide, code, qty } = param; // trdSide 1买入2卖出
let remainQty = qty;
let value = 0;
while (remainQty > 0) {
let orderID = null;
let order = null;
const orderBooks = await this.qotGetOrderBook({ market: this.market, code });// 获取盘口
const price = trdSide === 1 ? orderBooks.sellList[0].price : orderBooks.buyList[0].price;
if (orderID && order.orderStatus === 10) {
await this.trdModifyOrder({// 修改订单并设置订单为有效
modifyOrderOp: 4, orderID, price, qty: remainQty,
});
} else if (!orderID) {
orderID = await this.trdPlaceOrder({
trdSide, code, qty: remainQty, price,
}); // 下单
}
// eslint-disable-next-line
while (true) {
// 确认了不传入过滤条件会返回所有订单
const list = await this.trdGetOrderList({}, []);
// eslint-disable-next-line
order = list.filter(item => item.orderID === orderID);
if (order) {
if (order.orderStatus > 11) {
order = null;
orderID = null;
break;
} else if (order.orderStatus < 10) {
await sleep(50);
} else if (order.fillQty > 0) {
remainQty -= order.fillQty;
value += order.price * order.fillQty;
if (remainQty > 0 && order.orderStatus === 10) { // 部分成交,先设置为失效
await this.trdModifyOrder({ modifyOrderOp: 3, orderID }); // 失效
}
}
} else {
await sleep(60);
}
}
}
return value;
}
/**
* Trd_ModifyOrder.proto - 2205修改订单(改价、改量、改状态等)
*
请求包标识结构参考 PacketID
交易公共参数头结构参考 TrdHeader
修改操作枚举参考 ModifyOrderOp
限频接口:30秒内最多30次
* @async
* @param {object} params
* @param {PacketID} [params.packetID] 交易写操作防重放攻击,默认不用填写
* @param {TrdHeader} [params.header] 交易公共参数头,默认不用填写
* @param {number} params.orderID 订单号,forAll为true时,传0
* @param {ModifyOrderOp} params.modifyOrderOp 修改操作类型,参见Trd_Common.ModifyOrderOp的枚举定义
* @param {boolean} [params.forAll] 是否对此业务账户的全部订单操作,true是,false否(对单个订单),无此字段代表false,仅对单个订单
* 下面的字段仅在modifyOrderOp为ModifyOrderOp_Normal有效
* @param {number} [params.qty] 数量,2位精度,期权单位是"张"
* @param {number} [params.price] 价格,3位精度(A股2位)
* 以下为调整价格使用,目前仅对港、A股有效,因为港股有价位,A股2位精度,美股不需要
* @param {boolean} [params.adjustPrice] 是否调整价格,如果价格不合法,是否调整到合法价位,true调整,false不调整
* @param {number} [params.adjustSideAndLimit] 调整方向和调整幅度百分比限制,正数代表向上调整,负数代表向下调整,具体值代表调整幅度限制,如:0.015代表向上调整且幅度不超过1.5%;-0.01代表向下调整且幅度不超过1%
* @returns {number} orderID 订单号
*/
async trdModifyOrder(params) { // 2205修改订单(改价、改量、改状态等)
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_ModifyOrder', Object.assign({
packetID: {
connID: this.connID,
serialNo: this.socket.requestId,
}, // 交易写操作防重放攻击
header: this.trdHeader, // 交易公共参数头
orderID: 0, // 订单号,forAll为true时,传0
modifyOrderOp: 1, // //修改操作类型,参见Trd_Common.ModifyOrderOp的枚举定义
forAll: false, // /是否对此业务账户的全部订单操作,true是,false否(对单个订单),无此字段代表false,仅对单个订单
qty: 0, // 数量,2位精度,期权单位是"张"
price: 0, // 价格,3位精度(A股2位)
// 以下为调整价格使用,目前仅对港、A股有效,因为港股有价位,A股2位精度,美股不需要
adjustPrice: false, // 是否调整价格,如果价格不合法,是否调整到合法价位,true调整,false不调整
adjustSideAndLimit: 0, // 调整方向和调整幅度百分比限制,正数代表向上调整,负数代表向下调整,具体值代表调整幅度限制,如:0.015代表向上调整且幅度不超过1.5%;-0.01代表向下调整且幅度不超过1%
}, params))).orderID;
}
/**
* 注册订单更新通知
* Trd_UpdateOrder.proto - 2208推送订单更新
* @async
* @param {function} callback 回调
* @returns {Order} 订单结构
*/
async subTrdUpdateOrder(callback) { // 注册订单更新通知
return this.socket.subNotify(2208, data => callback(data.order));
}
/**
* 取消注册订单更新通知
* Trd_UpdateOrder.proto - 2208推送订单更新
* @async
*/
async unsubTrdUpdateOrder() { // 取消注册订单更新通知
return this.socket.unsubNotify(2208);
}
/**
* Trd_GetOrderFillList.proto - 2211获取成交列表
* @async
* @param {TrdFilterConditions} filterConditions 过滤条件
* @returns {OrderFill[]} 成交列表
*/
async trdGetOrderFillList(filterConditions) { // 2211获取成交列表
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_GetOrderFillList', {
header: this.trdHeader, // 交易公共参数头
filterConditions,
})).orderFillList || [];
}
/**
* 注册新成交通知
* Trd_UpdateOrderFill.proto - 2218推送新成交
* @param {function} callback 回调
* @returns {OrderFill} 成交结构
*/
async subTrdUpdateOrderFill(callback) { // 注册新成交通知
return this.socket.subNotify(2218, data => callback(data.orderFill || []));
}
/**
* Trd_GetHistoryOrderList.proto - 2221获取历史订单列表
*
交易公共参数头结构参考 TrdHeader
订单结构参考 Order
过滤条件结构参考 TrdFilterConditions
订单状态枚举参考 OrderStatus
限频接口:30秒内最多10次
* @async
* @param {TrdFilterConditions} filterConditions 过滤条件
* @param {OrderStatus} filterStatusList OrderStatus, 需要过滤的订单状态列表
* @returns {Order[]} 历史订单列表
*/
async trdGetHistoryOrderList(filterConditions, filterStatusList) { // 2221获取历史订单列表
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_GetHistoryOrderList', {
header: this.trdHeader, // 交易公共参数头
filterConditions,
filterStatusList,
})).orderList || [];
}
/**
* Trd_GetHistoryOrderFillList.proto - 2222获取历史成交列表
*
交易公共参数头结构参考 TrdHeader
成交结构参考 OrderFill
过滤条件结构参考 TrdFilterConditions
限频接口:30秒内最多10次
* @async
* @param {TrdFilterConditions} filterConditions 过滤条件
* @returns {OrderFill[]} 历史成交列表
*/
async trdGetHistoryOrderFillList(filterConditions) { // 2222获取历史成交列表
if (!this.trdHeader) throw new Error('请先调用setCommonTradeHeader接口设置交易公共header');
return (await this.socket.send('Trd_GetHistoryOrderFillList', {
header: this.trdHeader, // 交易公共参数头
filterConditions,
})).orderFillList || [];
}
}
module.exports = FutuQuant;