Source: futuquant.js

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;