Exemplo n.º 1
0
 async def _insert_order(self, volume, end_time, strict_end_time, exit_immediately):
     volume_left = volume
     try:
         trade_chan = TqChan(self._api)
         self._order_task = InsertOrderUntilAllTradedTask(self._api, self._symbol, self._direction, self._offset,
                                                          volume=volume, price="PASSIVE", trade_chan=trade_chan,
                                                          trade_objs_chan=self._trade_objs_chan,
                                                          account=self._account)
         async with self._api.register_update_notify() as update_chan:
             async for _ in update_chan:
                 if _get_trade_timestamp(self._quote.datetime, float('nan')) > strict_end_time:
                     break
                 else:
                     while not trade_chan.empty():
                         v = await trade_chan.recv()
                         volume_left = volume_left - (v if self._direction == "BUY" else -v)
                     if exit_immediately and volume_left == 0:
                         break
     finally:
         self._order_task._task.cancel()
         await asyncio.gather(self._order_task._task, return_exceptions=True)
         while not trade_chan.empty():
             v = await trade_chan.recv()
             volume_left = volume_left - (v if self._direction == "BUY" else -v)
         await trade_chan.close()
         if volume_left > 0:
             await self._insert_order_active(volume_left)
Exemplo n.º 2
0
 async def _handle_req_data(self, pack):
     """
     处理所有下游发送的非 peek_message 数据包
     这里应该将发送的请求转发到指定的某个上游 channel
     """
     if self._is_self_trade_pack(pack):
         if pack["aid"] == "insert_order":
             symbol = pack["exchange_id"] + "." + pack["instrument_id"]
             if symbol not in self._quote_tasks:
                 quote_chan = TqChan(self._api)
                 order_chan = TqChan(self._api)
                 self._quote_tasks[symbol] = {
                     "quote_chan":
                     quote_chan,
                     "order_chan":
                     order_chan,
                     "task":
                     self._api.create_task(
                         self._quote_handler(symbol, quote_chan,
                                             order_chan))
                 }
             await self._quote_tasks[symbol]["order_chan"].send(pack)
         else:
             # pack 里只有 order_id 信息,发送到每一个合约的 order_chan, 交由 quote_task 判断是不是当前合约下的委托单
             for symbol in self._quote_tasks:
                 await self._quote_tasks[symbol]["order_chan"].send(pack)
     elif pack["aid"] == "subscribe_quote":
         # 这里只会增加订阅合约,不会退订合约
         await self._subscribe_quote(set(pack["ins_list"].split(",")))
     else:
         await self._md_send_chan.send(pack)
Exemplo n.º 3
0
 def _run(self, api, api_send_chan, api_recv_chan, ws_md_send_chan, ws_md_recv_chan):
     self._api = api
     log = ShinnyLoggerAdapter(self._api._logger.getChild("TqMultiAccount"))
     for index, account in enumerate(self._account_list):
         _send_chan = api_send_chan if index == len(self._account_list) - 1 else TqChan(self._api, logger=log)
         _recv_chan = api_recv_chan if index == len(self._account_list) - 1 else TqChan(self._api, logger=log)
         _send_chan._logger_bind(chan_name=f"send to account_{index}")
         _recv_chan._logger_bind(chan_name=f"recv from account_{index}")
         ws_md_send_chan._logger_bind(chan_from=f"account_{index}")
         ws_md_recv_chan._logger_bind(chan_to=f"account_{index}")
         if isinstance(account, BaseSim):
             # 启动模拟账户实例
             self._api.create_task(
                 account._run(self._api, _send_chan, _recv_chan, ws_md_send_chan, ws_md_recv_chan))
         else:
             # 连接交易服务器
             ws_td_send_chan, ws_td_recv_chan = self._connect_td(account, index)
             ws_td_send_chan._logger_bind(chan_from=f"account_{index}")
             ws_td_recv_chan._logger_bind(chan_to=f"account_{index}")
             # 账户处理消息
             self._api.create_task(
                 account._run(self._api, _send_chan, _recv_chan, ws_md_send_chan, ws_md_recv_chan, ws_td_send_chan,
                              ws_td_recv_chan)
             )
         ws_md_send_chan, ws_md_recv_chan = _send_chan, _recv_chan
Exemplo n.º 4
0
class TqTradingStatus(TqModule):
    """
    交易状态模块,建立 websocket 连接
    """

    async def _run(self, api, api_send_chan, api_recv_chan, md_send_chan, md_recv_chan):
        self._api = api
        self._logger = self._api._logger.getChild("TradingStatus")
        self._init_ts_ws = False
        self._api_send_chan = api_send_chan
        self._api_recv_chan = api_recv_chan
        self._md_send_chan = md_send_chan
        self._md_recv_chan = md_recv_chan
        self._ts_send_chan = TqChan(self._api, chan_name="send to ts_reconn")
        self._ts_recv_chan = TqChan(self._api, chan_name="recv from ts_reconn")
        await super(TqTradingStatus, self)._run(api, api_send_chan, api_recv_chan, md_send_chan, md_recv_chan, self._ts_send_chan, self._ts_recv_chan)

    async def _handle_recv_data(self, pack, chan):
        """
        处理所有上游收到的数据包
        """
        if pack['aid'] == 'rtn_data':
            if chan == self._md_recv_chan:  # 从行情收到的数据包
                self._diffs.extend(pack.get('data', []))
            elif chan == self._ts_recv_chan:  # 从交易状态服务收到的数据包
                diffs = pack.get('data', [])
                for d in diffs:
                    for symbol, ts in d.get('trading_status', {}).items():
                        if ts['trade_status'] not in ["AUCTIONORDERING", "CONTINOUS"]:
                            ts['trade_status'] = "NOTRADING"
                self._diffs.extend(diffs)
        else:
            await self._api_recv_chan.send(pack)

    async def _handle_req_data(self, pack):
        """处理所有下游发送的非 peek_message 数据包"""
        if pack['aid'] == 'subscribe_trading_status':
            if self._init_ts_ws is False:
                self._init_ts_ws = True
                self._create_ts_run()
            await self._ts_send_chan.send(pack)
        else:
            await self._md_send_chan.send(pack)

    def _create_ts_run(self):
        ts_url = "wss://trading-status.shinnytech.com/status"
        conn_logger = self._api._logger.getChild("TqConnect")
        ws_ts_send_chan = TqChan(self._api, chan_name="send to ts")
        ws_ts_recv_chan = TqChan(self._api, chan_name="recv from ts")
        ws_ts_send_chan._logger_bind(chan_from="ts_reconn", url=ts_url)
        ws_ts_recv_chan._logger_bind(chan_to="ts_reconn", url=ts_url)
        conn = TqConnect(logger=ShinnyLoggerAdapter(conn_logger, url=ts_url), conn_id="ts")
        self._api.create_task(conn._run(self._api, ts_url, ws_ts_send_chan, ws_ts_recv_chan), _caller_api=True)
        ts_reconnect = TsReconnectHandler(logger=ShinnyLoggerAdapter(self._logger.getChild("TsReconnect"), url=ts_url))
        self._ts_send_chan._logger_bind(chan_from="ts", url=ts_url)
        self._ts_recv_chan._logger_bind(chan_to="ts", url=ts_url)
        self._api.create_task(ts_reconnect._run(self._api, self._ts_send_chan, self._ts_recv_chan, ws_ts_send_chan, ws_ts_recv_chan), _caller_api=True)
Exemplo n.º 5
0
 async def _run(self, api, api_send_chan, api_recv_chan, md_send_chan, md_recv_chan):
     self._api = api
     self._logger = self._api._logger.getChild("TradingStatus")
     self._init_ts_ws = False
     self._api_send_chan = api_send_chan
     self._api_recv_chan = api_recv_chan
     self._md_send_chan = md_send_chan
     self._md_recv_chan = md_recv_chan
     self._ts_send_chan = TqChan(self._api, chan_name="send to ts_reconn")
     self._ts_recv_chan = TqChan(self._api, chan_name="recv from ts_reconn")
     await super(TqTradingStatus, self)._run(api, api_send_chan, api_recv_chan, md_send_chan, md_recv_chan, self._ts_send_chan, self._ts_recv_chan)
Exemplo n.º 6
0
 async def _run(self):
     try:
         self._send_chan = TqChan(self._api)
         self._send_chan.send_nowait({"aid": "ratio", "speed": self._default_speed})
         _senddata_task = self._api.create_task(self._senddata_handler())
         while True:
             await self._send_chan.send({"aid": "heartbeat"})
             await asyncio.sleep(30)
     finally:
         await self._send_chan.close()
         _senddata_task.cancel()
         await asyncio.gather(_senddata_task, return_exceptions=True)
Exemplo n.º 7
0
    def __init__(self,
                 api,
                 symbol,
                 direction,
                 offset,
                 volume,
                 limit_price=None,
                 order_chan=None,
                 trade_chan=None):
        """
        创建下单task实例

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 拟下单的合约symbol, 格式为 交易所代码.合约代码,  例如 "SHFE.cu1801"

            direction (str): "BUY" 或 "SELL"

            offset (str): "OPEN", "CLOSE" 或 "CLOSETODAY"

            volume (int): 需要下单的手数

            limit_price (float): [可选]下单价格, 默认市价单

            order_chan (TqChan): [可选]委托单通知channel, 当委托单状态发生时会将委托单信息发到该channel上

            trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上
        """
        self._api = api
        if symbol not in api._data.get("quotes", {}):
            raise Exception("代码 %s 不存在, 请检查合约代码是否填写正确" % (symbol))
        self._symbol = symbol
        if direction not in ("BUY", "SELL"):
            raise Exception("下单方向(direction) %s 错误, 请检查 direction 参数是否填写正确" %
                            (direction))
        self._direction = direction
        if offset not in ("OPEN", "CLOSE", "CLOSETODAY"):
            raise Exception("开平标志(offset) %s 错误, 请检查 offset 是否填写正确" % (offset))
        self._offset = offset
        self._volume = int(volume)
        self._limit_price = float(
            limit_price) if limit_price is not None else None
        self._order_chan = order_chan if order_chan is not None else TqChan(
            self._api)
        self._trade_chan = trade_chan if trade_chan is not None else TqChan(
            self._api)
        self._task = self._api.create_task(self._run())
Exemplo n.º 8
0
 async def _run(self):
     """负责追价下单的task"""
     async with self._api.register_update_notify() as update_chan:
         # 确保获得初始行情
         while self._quote.datetime == "":
             await update_chan.recv()
         while self._volume != 0:
             limit_price = self._get_price()
             insert_order_task = InsertOrderTask(
                 self._api,
                 self._symbol,
                 self._direction,
                 self._offset,
                 self._volume,
                 limit_price=limit_price,
                 trade_chan=self._trade_chan)
             order = await insert_order_task._order_chan.recv()
             check_chan = TqChan(self._api, last_only=True)
             check_task = self._api.create_task(
                 self._check_price(check_chan, limit_price, order))
             try:
                 await insert_order_task._task
                 order = insert_order_task._order_chan.recv_latest(order)
                 self._volume = order.volume_left
                 if self._volume != 0 and not check_task.done():
                     raise Exception(
                         "遇到错单: %s %s %s %d手 %f %s" %
                         (self._symbol, self._direction, self._offset,
                          self._volume, limit_price, order.last_msg))
             finally:
                 await check_chan.close()
                 await check_task
Exemplo n.º 9
0
 async def _download_data_series(self, rangeset):
     symbol = self._symbol_list[0]
     for start_dt, end_dt in rangeset:
         try:
             start_id, end_id = None, None
             temp_filename = os.path.join(
                 CACHE_DIR, f"{symbol}.{self._dur_nano}.temp")
             temp_file = open(temp_filename, "wb")
             data_chan = TqChan(self._api)
             task = self._api.create_task(
                 self._download_data(start_dt, end_dt, data_chan))
             async for item in data_chan:
                 temp_file.write(
                     struct.pack("@qq" + "d" * (len(item) - 2), *item))
                 if start_id is None:
                     start_id = item[0]
                 end_id = item[0]
         except Exception as e:
             temp_file.close()  # 这里如果遇到 exception,不应该再重命名文件,相当于下载失败
         else:
             temp_file.close()
             if start_id is not None and end_id is not None:
                 target_filename = os.path.join(
                     CACHE_DIR,
                     f"{symbol}.{self._dur_nano}.{start_id}.{end_id + 1}")
                 shutil.move(temp_filename, target_filename)
         finally:
             task.cancel()
             await task
Exemplo n.º 10
0
 async def _run(self):
     """负责追价下单的task"""
     self._quote = await self._api.get_quote(self._symbol)
     while self._volume != 0:
         limit_price = self._get_price(self._direction)
         if limit_price != limit_price:
             raise Exception("设置价格函数返回 nan,无法处理。请检查后重试。")
         # 当前下单手数
         if self._min_volume and self._max_volume and self._volume >= self._max_volume:
             this_volume = utils.RD.randint(self._min_volume,
                                            self._max_volume)
         else:
             this_volume = self._volume
         insert_order_task = InsertOrderTask(
             self._api,
             self._symbol,
             self._direction,
             self._offset,
             this_volume,
             limit_price=limit_price,
             trade_chan=self._trade_chan,
             trade_objs_chan=self._trade_objs_chan,
             account=self._account)
         order = await insert_order_task._order_chan.recv()
         check_chan = TqChan(self._api, last_only=True)
         check_task = self._api.create_task(
             self._check_price(check_chan, limit_price, order['order_id']))
         try:
             # 当父 task 被 cancel,子 task 如果正在执行,也会捕获 CancelError
             # 添加 asyncio.shield 后,如果父 task 被 cancel,asyncio.shield 也会被 cancel,但是子 task 不会收到 CancelError
             # 这里需要 asyncio.shield,是因为 insert_order_task._task 预期不会被 cancel, 应该等待到 order 状态是 FINISHED 才返回
             await asyncio.shield(insert_order_task._task)
             order = insert_order_task._order_chan.recv_latest(order)
             self._volume -= (this_volume - order['volume_left'])
             if order['volume_left'] != 0 and not check_task.done():
                 raise Exception(
                     "遇到错单: %s %s %s %d手 %f %s" %
                     (self._symbol, self._direction, self._offset,
                      this_volume, limit_price, order['last_msg']))
         finally:
             if self._api.get_order(
                     order['order_id'],
                     account=self._account).status == "ALIVE":
                 # 当 task 被 cancel 时,主动撤掉未成交的挂单
                 self._api.cancel_order(order['order_id'],
                                        account=self._account)
             await check_chan.close()
             await check_task
             # 在每次退出时,都等到 insert_order_task 执行完,此时 order 状态一定是 FINISHED;self._trade_chan 也一定会收到全部的成交手数
             try:
                 # 当用户调用 api.close(), 会主动 cancel 所有由 api 创建的 task,包括 TargetPosTask._target_pos_task,
                 # 此时,insert_order_task._task 如果有未完成委托单,会永远等待下去(因为网络连接已经断开),所以这里增加超时机制。
                 await asyncio.wait_for(insert_order_task._task, timeout=30)
             except asyncio.TimeoutError:
                 raise Exception(
                     f"InsertOrderTask 执行超时,30s 内报单未执行完。此错误产生可能的原因:"
                     f"可能是用户调用了 api.close() 之后,已经创建的 InsertOrderTask 无法正常结束。"
                 )
Exemplo n.º 11
0
 async def _ensure_query(self, pack):
     """一定收到了对应 query 返回的包"""
     query_pack = {"query": pack["query"]}
     if query_pack.items() <= self._data.get("symbols", {}).get(pack["query_id"], {}).items():
         return
     async with TqChan(self._api, last_only=True) as update_chan:
         self._data["_listener"].add(update_chan)
         while not query_pack.items() <= self._data.get("symbols", {}).get(pack["query_id"], {}).items():
             await update_chan.recv()
Exemplo n.º 12
0
 async def _send_snapshot(self):
     """发送初始合约信息"""
     async with TqChan(self._api, last_only=True) as update_chan:  # 等待与行情服务器连接成功
         self._data["_listener"].add(update_chan)
         while self._data.get("mdhis_more_data", True):
             await update_chan.recv()
     # 发送初始行情(合约信息截面)时
     quotes = {}
     for ins, quote in self._data["quotes"].items():
         if not ins.startswith("_"):
             trading_time = quote.get("trading_time", {})
             quotes[ins] = {
                 "open": None,  # 填写None: 删除api中的这个字段
                 "close": None,
                 "settlement": None,
                 "lower_limit": None,
                 "upper_limit": None,
                 "pre_open_interest": None,
                 "pre_settlement": None,
                 "pre_close": None,
                 "ins_class": quote.get("ins_class", ""),
                 "instrument_id": quote.get("instrument_id", ""),
                 "exchange_id": quote.get("exchange_id", ""),
                 "margin": quote.get("margin"),  # 用于内部实现模拟交易, 不作为api对外可用数据(即 Quote 类中无此字段)
                 "commission": quote.get("commission"),  # 用于内部实现模拟交易, 不作为api对外可用数据(即 Quote 类中无此字段)
                 "price_tick": quote["price_tick"],
                 "price_decs": quote["price_decs"],
                 "volume_multiple": quote["volume_multiple"],
                 "max_limit_order_volume": quote["max_limit_order_volume"],
                 "max_market_order_volume": quote["max_market_order_volume"],
                 "min_limit_order_volume": quote["min_limit_order_volume"],
                 "min_market_order_volume": quote["min_market_order_volume"],
                 "underlying_symbol": quote["underlying_symbol"],
                 "strike_price": quote["strike_price"],
                 "expired": quote.get('expire_datetime', float('nan')) <= self._trading_day_start,  # expired 默认值就是 False
                 "trading_time": {"day": trading_time.get("day", []), "night": trading_time.get("night", [])},
                 "expire_datetime": quote.get("expire_datetime"),
                 "delivery_month": quote.get("delivery_month"),
                 "delivery_year": quote.get("delivery_year"),
                 "option_class": quote.get("option_class", ""),
                 "product_id": quote.get("product_id", ""),
             }
     # 修改历史主连合约信息
     dt = datetime.fromtimestamp(self._trading_day_end / 1e9).strftime("%Y%m%d")
     quotes.update(self._get_history_cont_quotes(dt))
     self._diffs.append({
         "quotes": quotes,
         "ins_list": "",
         "mdhis_more_data": False,
         "_tqsdk_backtest": {
             "start_dt": self._start_dt,
             "current_dt": self._current_dt,
             "end_dt": self._end_dt
         }
     })
Exemplo n.º 13
0
    def _connect_td(self, account: Union[TqAccount, TqKq, TqKqStock] = None, index: int = 0):
        # 连接交易服务器
        td_logger = self._format_logger("TqConnect", account)
        conn_id = f"td_{index}"
        ws_td_send_chan = TqChan(self._api, chan_name=f"send to {conn_id}", logger=td_logger)
        ws_td_recv_chan = TqChan(self._api, chan_name=f"recv from {conn_id}", logger=td_logger)
        conn = TqConnect(td_logger, conn_id=conn_id)
        self._api.create_task(conn._run(self._api, account._td_url, ws_td_send_chan, ws_td_recv_chan))
        ws_td_send_chan._logger_bind(chan_from=f"td_reconn_{index}")
        ws_td_recv_chan._logger_bind(chan_to=f"td_reconn_{index}")

        td_handler_logger = self._format_logger("TdReconnect", account)
        td_reconnect = TdReconnectHandler(td_handler_logger)
        send_to_recon = TqChan(self._api, chan_name=f"send to td_reconn_{index}", logger=td_handler_logger)
        recv_from_recon = TqChan(self._api, chan_name=f"recv from td_reconn_{index}", logger=td_handler_logger)
        self._api.create_task(
            td_reconnect._run(self._api, send_to_recon, recv_from_recon, ws_td_send_chan, ws_td_recv_chan)
        )
        self._map_conn_id[conn_id] = account
        return send_to_recon, recv_from_recon
Exemplo n.º 14
0
 def _create_ts_run(self):
     ts_url = "wss://trading-status.shinnytech.com/status"
     conn_logger = self._api._logger.getChild("TqConnect")
     ws_ts_send_chan = TqChan(self._api, chan_name="send to ts")
     ws_ts_recv_chan = TqChan(self._api, chan_name="recv from ts")
     ws_ts_send_chan._logger_bind(chan_from="ts_reconn", url=ts_url)
     ws_ts_recv_chan._logger_bind(chan_to="ts_reconn", url=ts_url)
     conn = TqConnect(logger=ShinnyLoggerAdapter(conn_logger, url=ts_url), conn_id="ts")
     self._api.create_task(conn._run(self._api, ts_url, ws_ts_send_chan, ws_ts_recv_chan), _caller_api=True)
     ts_reconnect = TsReconnectHandler(logger=ShinnyLoggerAdapter(self._logger.getChild("TsReconnect"), url=ts_url))
     self._ts_send_chan._logger_bind(chan_from="ts", url=ts_url)
     self._ts_recv_chan._logger_bind(chan_to="ts", url=ts_url)
     self._api.create_task(ts_reconnect._run(self._api, self._ts_send_chan, self._ts_recv_chan, ws_ts_send_chan, ws_ts_recv_chan), _caller_api=True)
Exemplo n.º 15
0
 async def _ensure_quote(self, ins):
     # 在接新版合约服务器后,合约信息程序运行过程中查询得到的,这里不再能保证合约一定存在,需要添加 quote 默认值
     quote = _get_obj(self._data, ["quotes", ins], BtQuote(self._api))
     if math.isnan(quote.get("price_tick")):
         query_pack = _query_for_quote(ins)
         await self._md_send_chan.send(query_pack)
         async with TqChan(self._api, last_only=True) as update_chan:
             quote["_listener"].add(update_chan)
             while math.isnan(quote.get("price_tick")):
                 await update_chan.recv()
     if ins not in self._quotes or self._quotes[ins]["min_duration"] > 60000000000:
         await self._ensure_serial(ins, 60000000000)
Exemplo n.º 16
0
 async def _send_snapshot(self):
     """发送初始合约信息"""
     async with TqChan(self._api,
                       last_only=True) as update_chan:  # 等待与行情服务器连接成功
         self._data["_listener"].add(update_chan)
         while self._data.get("mdhis_more_data", True):
             await update_chan.recv()
     # 发送合约信息截面
     quotes = {}
     for ins, quote in self._data["quotes"].items():
         if not ins.startswith("_"):
             quotes[ins] = {
                 "open": None,  # 填写None: 删除api中的这个字段
                 "close": None,
                 "settlement": None,
                 "lower_limit": None,
                 "upper_limit": None,
                 "pre_open_interest": None,
                 "pre_settlement": None,
                 "pre_close": None,
                 "ins_class": quote.get("ins_class", ""),
                 "instrument_id": quote.get("instrument_id", ""),
                 "exchange_id": quote.get("exchange_id", ""),
                 "margin": quote.get(
                     "margin"),  # 用于内部实现模拟交易, 不作为api对外可用数据(即 Quote 类中无此字段)
                 "commission":
                 quote.get("commission"
                           ),  # 用于内部实现模拟交易, 不作为api对外可用数据(即 Quote 类中无此字段)
                 "price_tick": quote["price_tick"],
                 "price_decs": quote["price_decs"],
                 "volume_multiple": quote["volume_multiple"],
                 "max_limit_order_volume": quote["max_limit_order_volume"],
                 "max_market_order_volume":
                 quote["max_market_order_volume"],
                 "min_limit_order_volume": quote["min_limit_order_volume"],
                 "min_market_order_volume":
                 quote["min_market_order_volume"],
                 "underlying_symbol": quote["underlying_symbol"],
                 "strike_price": quote["strike_price"],
                 "expired": None,
                 "trading_time": quote.get("trading_time"),
                 "expire_datetime": quote.get("expire_datetime"),
                 "delivery_month": quote.get("delivery_month"),
                 "delivery_year": quote.get("delivery_year"),
                 "option_class": quote.get("option_class", ""),
                 "product_id": quote.get("product_id", ""),
             }
     self._diffs.append({
         "quotes": quotes,
         "ins_list": "",
         "mdhis_more_data": False,
     })
Exemplo n.º 17
0
    def __init__(self,
                 api,
                 symbol,
                 direction,
                 offset,
                 volume,
                 limit_price=None,
                 order_chan=None,
                 trade_chan=None,
                 trade_objs_chan=None,
                 account: Optional[Union[TqAccount, TqKq, TqSim]] = None):
        """
        创建下单task实例

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 拟下单的合约symbol, 格式为 交易所代码.合约代码,  例如 "SHFE.cu1801"

            direction (str): "BUY" 或 "SELL"

            offset (str): "OPEN", "CLOSE" 或 "CLOSETODAY"

            volume (int): 需要下单的手数

            limit_price (float): [可选]下单价格, 默认市价单

            order_chan (TqChan): [可选]委托单通知channel, 当委托单状态发生时会将委托单信息发到该channel上

            trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上

            trade_objs_chan (TqChan): [可选]成交对象通知channel, 当有成交发生时会将成交对象发送到该channel上

            account (TqAccount/TqKq/TqSim): [可选]指定发送下单指令的账户实例, 多账户模式下,该参数必须指定
        """
        self._api = api
        self._account = account
        self._symbol = symbol
        self._direction = _check_direction(direction)
        self._offset = _check_offset(offset)
        self._volume = _check_volume(volume)
        self._offset = offset
        self._volume = int(volume)
        self._limit_price = float(
            limit_price) if limit_price is not None else None
        self._order_chan = order_chan if order_chan is not None else TqChan(
            self._api)
        self._trade_chan = trade_chan
        self._trade_objs_chan = trade_objs_chan
        self._task = self._api.create_task(self._run())
Exemplo n.º 18
0
 async def _insert_order_active(self, volume):
     try:
         trade_chan = TqChan(self._api)
         self._order_task = InsertOrderUntilAllTradedTask(self._api, self._symbol, self._direction, self._offset,
                                                          volume=volume, price="ACTIVE", trade_chan=trade_chan,
                                                          trade_objs_chan=self._trade_objs_chan,
                                                          account=self._account)
         async for v in trade_chan:
             volume = volume - (v if self._direction == "BUY" else -v)
             if volume == 0:
                 break
     finally:
         await trade_chan.close()
         self._order_task._task.cancel()
         await asyncio.gather(self._order_task._task, return_exceptions=True)
Exemplo n.º 19
0
 async def _update_time_from_md(self):
     """监听行情更新并记录当时本地时间的task"""
     try:
         chan = TqChan(self._api, last_only=True)
         self._api.register_update_notify(self._quote,
                                          chan)  # quote有更新时: 更新记录的时间
         if isinstance(self._api._backtest, TqBacktest):
             # 回测情况下,在收到回测时间有更新的时候,也需要更新记录的时间
             self._api.register_update_notify(
                 _get_obj(self._api._data, ["_tqsdk_backtest"]), chan)
         async for _ in chan:
             self._local_time_record = time.time() - 0.005  # 更新最新行情时间时的本地时间
             self._local_time_record_update_chan.send_nowait(
                 True)  # 通知记录的时间有更新
     finally:
         await chan.close()
Exemplo n.º 20
0
    def __init__(self,
                 api,
                 symbol,
                 direction,
                 offset,
                 volume,
                 price="ACTIVE",
                 trade_chan=None):
        """
        创建追价下单task实例

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 拟下单的合约symbol, 格式为 交易所代码.合约代码,  例如 "SHFE.cu1801"

            direction (str): "BUY" 或 "SELL"

            offset (str): "OPEN", "CLOSE" 或 "CLOSETODAY"

            volume (int): 需要下单的手数

            price (str): [可选]下单方式, ACTIVE=对价下单, PASSIVE=挂价下单

            trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上
        """
        self._api = api
        if symbol not in api._data.get("quotes", {}):
            raise Exception("代码 %s 不存在, 请检查合约代码是否填写正确" % (symbol))
        self._symbol = symbol
        if direction not in ("BUY", "SELL"):
            raise Exception("下单方向(direction) %s 错误, 请检查 direction 参数是否填写正确" %
                            (direction))
        self._direction = direction
        if offset not in ("OPEN", "CLOSE", "CLOSETODAY"):
            raise Exception("开平标志(offset) %s 错误, 请检查 offset 是否填写正确" % (offset))
        self._offset = offset
        self._volume = int(volume)
        if self._volume <= 0:
            raise Exception("下单手数(volume) %s 错误, 请检查 volume 是否填写正确" % (volume))
        if price not in ("ACTIVE", "PASSIVE"):
            raise Exception("下单方式(price) %s 错误, 请检查 price 参数是否填写正确" % (price))
        self._price = price
        self._trade_chan = trade_chan if trade_chan is not None else TqChan(
            self._api)
        self._quote = self._api.get_quote(self._symbol)
        self._task = self._api.create_task(self._run())
Exemplo n.º 21
0
 async def connection_handler(self, request):
     ws = web.WebSocketResponse()
     await ws.prepare(request)
     send_msg = self.get_send_msg(self._data)
     await ws.send_str(send_msg)
     conn_chan = TqChan(self._api, last_only=True)
     self._conn_diff_chans.add(conn_chan)
     try:
         async for msg in ws:
             pack = simplejson.loads(msg.data)
             if pack["aid"] == 'peek_message':
                 last_diff = await conn_chan.recv()
                 send_msg = self.get_send_msg(last_diff)
                 await ws.send_str(send_msg)
     except Exception as e:
         await conn_chan.close()
         self._conn_diff_chans.remove(conn_chan)
Exemplo n.º 22
0
class TqReplay(object):
    """天勤复盘类"""
    def __init__(self, replay_dt: date):
        """
        除了传统的回测模式以外,TqSdk 提供独具特色的复盘模式,它与回测模式有以下区别

        1.复盘模式为时间驱动,回测模式为事件驱动

        复盘模式下,你可以指定任意一天交易日,后端行情服务器会传输用户订阅合约的当天的所有历史行情数据,重演当天行情,而在回测模式下,我们根据用户订阅的合约周期数据来进行推送

        因此在复盘模式下K线更新和实盘一模一样,而回测模式下就算订阅了 Tick 数据,回测中任意周期 K 线最后一根的 close 和其他数据也不会随着 Tick 更新而更新,而是随着K线频率生成和结束时更新一次

        2.复盘和回测的行情速度

        因为两者的驱动机制不同,回测会更快,但是我们在复盘模式下也提供行情速度调节功能,可以结合web_gui来实现

        3.复盘目前只支持单日复盘

        因为复盘提供对应合约当日全部历史行情数据,对后端服务器会有较大压力,目前只支持复盘模式下选择单日进行复盘

        Args:
            replay_dt (date): 指定复盘交易日
        """
        if isinstance(replay_dt, date):
            self._replay_dt = replay_dt
        else:
            raise Exception("复盘时间(dt)类型 %s 错误, 请检查 dt 数据类型是否填写正确" %
                            (type(replay_dt)))
        if self._replay_dt.weekday() >= 5:
            # 0~6, 检查周末[5,6] 提前抛错退出
            raise Exception("无法创建复盘服务器,请检查复盘日期后重试。")
        self._default_speed = 1
        self._api = None

    def _create_server(self, api):
        self._api = api
        self._logger = api._logger.getChild("TqReplay")  # 调试信息输出
        self._logger.debug('replay prepare', replay_dt=self._replay_dt)

        session = self._prepare_session()
        self._session_url = "http://%s:%d/t/rmd/replay/session/%s" % (
            session["ip"], session["session_port"], session["session"])
        self._ins_url = "http://%s:%d/t/rmd/replay/session/%s/symbol" % (
            session["ip"], session["session_port"], session["session"])
        self._md_url = "ws://%s:%d/t/rmd/front/mobile" % (
            session["ip"], session["gateway_web_port"])

        self._server_status = None
        self._server_status = self._wait_server_status("running", 60)
        if self._server_status == "running":
            self._logger.debug('replay start successed',
                               replay_dt=self._replay_dt)
            return self._ins_url, self._md_url
        else:
            self._logger.debug('replay start failed',
                               replay_dt=self._replay_dt)
            raise Exception("无法创建复盘服务器,请检查复盘日期后重试。")

    async def _run(self):
        try:
            self._send_chan = TqChan(self._api)
            self._send_chan.send_nowait({
                "aid": "ratio",
                "speed": self._default_speed
            })
            _senddata_task = self._api.create_task(self._senddata_handler())
            while True:
                await self._send_chan.send({"aid": "heartbeat"})
                await asyncio.sleep(30)
        finally:
            await self._send_chan.close()
            _senddata_task.cancel()
            await asyncio.gather(_senddata_task, return_exceptions=True)

    def _prepare_session(self):
        create_session_url = "http://replay.api.shinnytech.com/t/rmd/replay/create_session"
        response = requests.post(
            create_session_url,
            headers=self._api._base_headers,
            data=json.dumps({'dt': self._replay_dt.strftime("%Y%m%d")}),
            timeout=5)
        if response.status_code == 200:
            return json.loads(response.content)
        else:
            raise Exception("创建复盘服务器失败,请检查复盘日期后重试。")

    def _wait_server_status(self, target_status, timeout):
        """等服务器状态为 target_status,超时时间 timeout 秒"""
        deadline = time.time() + timeout
        server_status = self._get_server_status()
        while deadline > time.time():
            if target_status == server_status:
                break
            else:
                time.sleep(1)
                server_status = self._get_server_status()
        return server_status

    def _get_server_status(self):
        try:
            response = requests.get(self._session_url,
                                    headers=self._api._base_headers,
                                    timeout=5)
            if response.status_code == 200:
                return json.loads(response.content)["status"]
            else:
                raise Exception("无法创建复盘服务器,请检查复盘日期后重试。")
        except requests.exceptions.ConnectionError as e:
            # 刚开始 _session_url 还不能访问的时候~
            return None

    async def _senddata_handler(self):
        try:
            session = aiohttp.ClientSession(headers=self._api._base_headers)
            async for data in self._send_chan:
                await session.post(self._session_url, data=json.dumps(data))
        finally:
            await session.post(self._session_url,
                               data=json.dumps({"aid": "terminate"}))
            await session.close()

    def set_replay_speed(self, speed: float = 10.0) -> None:
        """
        调整复盘服务器行情推进速度

        Args:
            speed (float): 复盘服务器行情推进速度, 默认为 10.0

        Example::

            from datetime import date
            from tqsdk import TqApi, TqAuth, TqReplay
            replay = TqReplay(date(2020, 9, 10))
            api = TqApi(backtest=replay, auth=("信易账户,账户密码"))
            replay.set_replay_speed(3.0)
            quote = api.get_quote("SHFE.cu2012")
            while True:
                api.wait_update()
                if api.is_changing(quote):
                    print("最新价", quote.datetime, quote.last_price)

        """
        if self._api:
            self._send_chan.send_nowait({"aid": "ratio", "speed": speed})
        else:
            # _api 未初始化,只记录用户设定的速度,在复盘服务器启动完成后,发动请求
            self._default_speed = speed
Exemplo n.º 23
0
    def __init__(self, api: TqApi, symbol: str, direction: str, offset: str, volume: int, duration: float,
                 min_volume_each_order: int, max_volume_each_order: int,
                 account: Optional[Union[TqAccount, TqKq, TqSim]] = None):
        """
        创建 Twap 实例

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 拟下单的合约symbol, 格式为 交易所代码.合约代码,  例如 "SHFE.cu1801"

            direction (str): "BUY" 或 "SELL"

            offset (str): "OPEN", "CLOSE","CLOSETODAY"

            volume (int): 需要下单的总手数

            duration (int): 算法执行的时长,以秒为单位,时长可以跨非交易时间段,但是不可以跨交易日
            * 设置为 60*10, 可以是 10:10~10:15 + 10:30~10:35

            min_volume_each_order (int):单笔最小委托单,每笔委托单数默认在最小和最大值中产生

            max_volume_each_order (int):单笔最大委托单,每笔委托单数默认在最小和最大值中产生

            account (TqAccount/TqKq/TqSim): [可选]指定发送下单指令的账户实例, 多账户模式下,该参数必须指定

        Example1::

          from tqsdk import TqApi
          from tqsdk.algorithm import Twap

          api = TqApi(auth="信易账户,用户密码")
          # 设置twap任务参数
          target_twap = Twap(api,"SHFE.rb2012","BUY","OPEN",500,300,10,25)
          # 启动循环
          while True:
            api.wait_update()
            if target_twap.is_finished():
                break
          api.close()

        Example2::

          from tqsdk import TqApi
          from tqsdk.algorithm import Twap

          api = TqApi(auth="信易账户,用户密码")
          target_twap = Twap(api,"SHFE.rb2012","BUY","OPEN",500,300,10,25)

          num_of_trades = 0

          while True:
            api.wait_update()

            if num_of_trades < len(target_twap.trades):
              # 最新的成交
              for i in range(num_of_trades - len(target_twap.trades), 0):
                print("新的成交", target_twap.trades[i])
              print(target_twap.average_trade_price)  # 打印出当前已经成交的平均价格
              num_of_trades = len(target_twap.trades)

            if target_twap.is_finished():
                break

          print("打印出 twap 全部成交以及成交均价")
          print(target_twap.trades)
          print(target_twap.average_trade_price)
          api.close()
        """
        if symbol.startswith("CZCE.CJ"):
            raise Exception("红枣期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 4 手,这些函数还未支持该规则!")
        if symbol.startswith("CZCE.ZC"):
            raise Exception("动力煤期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 4 手,这些函数还未支持该规则!")
        if symbol.startswith("CZCE.WH"):
            raise Exception("强麦期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 10 手,这些函数还未支持该规则!")
        self._api = api
        self._account = api._account._check_valid(account)
        if self._account is None:
            raise Exception(f"多账户模式下, 需要指定账户实例 account")
        self._symbol = symbol
        self._direction = direction
        self._offset = offset
        self._volume = int(volume)
        self._duration = duration
        self._min_volume_each_order = int(min_volume_each_order)
        self._max_volume_each_order = int(max_volume_each_order)
        if self._max_volume_each_order <= 0 or self._min_volume_each_order <= 0:
            raise Exception("请调整参数, min_volume_each_order、max_volume_each_order 必须是大于 0 的整数。")
        if self._min_volume_each_order > self._max_volume_each_order:
            raise Exception("请调整参数, min_volume_each_order 必须小于 max_volume_each_order。")
        # 得到有效的手数序列和时间间隔序列
        volume_list, interval_list = self._get_volume_list()
        self._task = self._api.create_task(self._run(volume_list, interval_list))
        self._order_task = None

        self.trades = []  # 所有的 trade 列表
        self._trade_sum_volume = 0  # 所有 trade 的成交的总手数
        self._trade_sum_amount = 0  # 所有 trade 的成交的总支出 (手数*价格)
        self._trade_objs_chan = TqChan(self._api)
        self._trade_recv_task = self._api.create_task(self._trade_recv())
Exemplo n.º 24
0
class TargetPosTask(object, metaclass=TargetPosTaskSingleton):
    """目标持仓 task, 该 task 可以将指定合约调整到目标头寸"""
    def __init__(
            self,
            api: TqApi,
            symbol: str,
            price: Union[str, Callable[[str], Union[float, int]]] = "ACTIVE",
            offset_priority: str = "今昨,开",
            min_volume: Optional[int] = None,
            max_volume: Optional[int] = None,
            trade_chan: Optional[TqChan] = None,
            trade_objs_chan: Optional[TqChan] = None,
            account: Optional[Union[TqAccount, TqKq, TqSim]] = None) -> None:
        """
        创建目标持仓task实例,负责调整归属于该task的持仓 **(默认为整个账户的该合约净持仓)**.

        **注意:**
            1. TargetPosTask 在 set_target_volume 时并不下单或撤单, 它的下单和撤单动作, 是在之后的每次 wait_update 时执行的. 因此, **需保证 set_target_volume 后还会继续调用wait_update()** 。

            2. 请勿在使用 TargetPosTask 的同时使用 insert_order() 函数, 否则将导致 TargetPosTask 报错或错误下单。

            3. TargetPosTask 如果同时设置 min_volume(每笔最小下单手数),max_volume(每笔最大下单的手数)两个参数,表示采用 **大单拆分模式** 下单。

                在 **大单拆分模式** 下,每次下单的手数为随机生成的正整数,值介于 min_volume、max_volume 之间。

                具体说明:调用 set_target_volume 后,首先会根据目标持仓手数、开平仓顺序计算出,需要平今、平昨、开仓的目标下单手数及顺序。

                + 如果在调整持仓的目标下单手数小于 max_volume,则直接以目标下单手数下单。

                + 如果在调整持仓的目标下单手数大于等于 max_volume,则会以 min_volume、max_volume 之间的随机手数下一笔委托单,手数全部成交后,会接着处理剩余的手数;\
                继续以随机手数下一笔委托单,全部成交后,继续处理剩余的手数,直至剩余手数小于 max_volume 时,直接以剩余手数下单。

                当使用大单拆分模式下单时,必须同时填写 min_volume、max_volume,且需要满足 max_volume >= min_volume > 0。

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 负责调整的合约代码

            price (str / Callable): [可选]下单方式, 默认为 "ACTIVE"。
                * "ACTIVE":对价下单,在持仓调整过程中,若下单方向为买,对价为卖一价;若下单方向为卖,对价为买一价。
                * "PASSIVE":排队价下单,在持仓调整过程中,若下单方向为买,对价为买一价;若下单方向为卖,对价为卖一价。
                * Callable[[str], Union[float, int]]: 函数参数为下单方向,函数返回值是下单价格。如果返回 nan,程序会抛错。

            offset_priority (str): [可选]开平仓顺序,昨=平昨仓,今=平今仓,开=开仓,逗号=等待之前操作完成

                                   对于下单指令区分平今/昨的交易所(如上期所),按照今/昨仓的数量计算是否能平今/昨仓
                                   对于下单指令不区分平今/昨的交易所(如中金所),按照“先平当日新开仓,再平历史仓”的规则计算是否能平今/昨仓,如果这些交易所设置为"昨开"在有当日新开仓和历史仓仓的情况下,会自动跳过平昨仓进入到下一步

                                   * "今昨,开" 表示先平今仓,再平昨仓,等待平仓完成后开仓,对于没有单向大边的品种避免了开仓保证金不足
                                   * "今昨开" 表示先平今仓,再平昨仓,并开仓,所有指令同时发出,适合有单向大边的品种
                                   * "昨开" 表示先平昨仓,再开仓,禁止平今仓,适合股指这样平今手续费较高的品种
                                   * "开" 表示只开仓,不平仓,适合需要进行锁仓操作的品种

            min_volume (int): [可选] **大单拆分模式下** 每笔最小下单的手数,默认不启用 **大单拆分模式**

            max_volume (int): [可选] **大单拆分模式下** 每笔最大下单的手数,默认不启用 **大单拆分模式**

            trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上

            trade_objs_chan (TqChan): [可选]成交对象通知channel, 当有成交发生时会将成交对象发送到该channel上

            account (TqAccount/TqKq/TqSim): [可选]指定发送下单指令的账户实例, 多账户模式下,该参数必须指定

        **注意**

        当 price 参数为函数类型时,该函数应该返回一个有效的价格值,应该避免返回 nan。以下为 price 参数是函数类型时的示例。

        Example1::

            # ... 用户代码 ...
            quote = api.get_quote("SHFE.cu2012")
            def get_price(direction):
                # 在 BUY 时使用买一价加一档价格,SELL 时使用卖一价减一档价格
                if direction == "BUY":
                    price = quote.bid_price1 + quote.price_tick
                else:
                    price = quote.ask_price1 - quote.price_tick
                # 如果 price 价格是 nan,使用最新价报单
                if price != price:
                    price = quote.last_price
                return price

            target_pos = TargetPosTask(api, "SHFE.cu2012", price=get_price)
            # ... 用户代码 ...


        Example2::

            # ... 用户代码 ...
            quote1 = api.get_quote("SHFE.cu2012")
            quote2 = api.get_quote("SHFE.au2012")

            def get_price(direction, quote):
                # 在 BUY 时使用买一价加一档价格,SELL 时使用卖一价减一档价格
                if direction == "BUY":
                    price = quote.bid_price1 + quote.price_tick
                else:
                    price = quote.ask_price1 - quote.price_tick
                # 如果 price 价格是 nan,使用最新价报单
                if price != price:
                    price = quote.last_price
                return price

            target_pos1 = TargetPosTask(api, "SHFE.cu2012", price=lambda direction: get_price(direction, quote1))
            target_pos2 = TargetPosTask(api, "SHFE.au2012", price=lambda direction: get_price(direction, quote2))
            # ... 用户代码 ...

        Example3::

            # 大单拆分模式用法示例

            from tqsdk import TqApi, TqAuth, TargetPosTask
            api = TqApi(auth=TqAuth("信易账户", "账户密码"))
            position = api.get_position('SHFE.rb2106')

            # 同时设置 min_volume、max_volume 两个参数,表示使用大单拆分模式
            t = TargetPosTask(api, 'SHFE.rb2106', min_volume=2, max_volume=10)
            t.set_target_volume(50)
            while True:
                api.wait_update()
                if position.pos_long == 50:
                    break
            api.close()

            # 说明:
            # 以上代码使用 TqSim 交易,开始时用户没有 SHFE.cu2012 合约的任何持仓,那么在 t.set_target_volume(50) 之后应该开多仓 50 手
            # 根据用户参数,下单使用大单拆分模式,每次下单手数在 2~10 之间,打印出的成交通知可能是这样的:
            # 2021-03-15 11:29:48 -     INFO - 模拟交易成交记录
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.516138, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.519699, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 8, 价格: 4687.000,手续费: 37.64
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.522848, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 10, 价格: 4687.000,手续费: 47.05
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.525617, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 8, 价格: 4687.000,手续费: 37.64
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.528151, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.530930, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.533515, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 3, 价格: 4687.000,手续费: 14.12

        """
        if symbol.startswith("CZCE.CJ"):
            raise Exception(
                "红枣期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 4 手,这些函数还未支持该规则!"
            )
        if symbol.startswith("CZCE.ZC"):
            raise Exception(
                "动力煤期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 2 手,这些函数还未支持该规则!"
            )
        super(TargetPosTask, self).__init__()
        self._api = api
        self._account = api._account._check_valid(account)
        self._symbol = symbol
        self._exchange = symbol.split(".")[0]
        self._offset_priority = _check_offset_priority(offset_priority)
        self._min_volume, self._max_volume = _check_volume_limit(
            min_volume, max_volume)
        self._price = _check_price(price)
        self._pos = self._account.get_position(self._symbol)
        self._pos_chan = TqChan(self._api, last_only=True)
        self._trade_chan = trade_chan
        self._trade_objs_chan = trade_objs_chan
        self._task = self._api.create_task(self._target_pos_task())
        self._time_update_task = self._api.create_task(
            self._update_time_from_md())  # 监听行情更新并记录当时本地时间的task
        self._local_time_record = time.time() - 0.005  # 更新最新行情时间时的本地时间
        self._local_time_record_update_chan = TqChan(
            self._api, last_only=True)  # 监听 self._local_time_record 更新

    def set_target_volume(self, volume: int) -> None:
        """
        设置目标持仓手数

        Args:
            volume (int): 目标持仓手数,正数表示多头,负数表示空头,0表示空仓

        Example1::

            # 设置 rb1810 持仓为多头5手
            from tqsdk import TqApi, TqAuth, TargetPosTask

            api = TqApi(auth=TqAuth("信易账户", "账户密码"))
            target_pos = TargetPosTask(api, "SHFE.rb1810")
            target_pos.set_target_volume(5)
            while True:
                # 需在 set_target_volume 后调用wait_update()以发出指令
                api.wait_update()

        Example2::

            # 多账户模式下使用 TargetPosTask
            from tqsdk import TqApi, TqMultiAccount, TqAuth, TargetPosTask

            account1 = TqAccount("H海通期货", "123456", "123456")
            account2 = TqAccount("H宏源期货", "654321", "123456")
            api = TqApi(TqMultiAccount([account1, account2]), auth=TqAuth("信易账户", "账户密码"))
            symbol1 = "DCE.m2105"
            symbol2 = "DCE.i2101"
            # 多账户模式下, 调仓工具需要指定账户实例
            target_pos1 = TargetPosTask(api, symbol1, account=account1)
            target_pos2 = TargetPosTask(api, symbol2, account=account2)
            target_pos1.set_target_volume(30)
            target_pos2.set_target_volume(80)
            while True:
                api.wait_update()

            api.close()

        """
        if self._task.done():
            raise Exception("已经结束的 TargetPosTask 实例不可以再设置手数。")
        self._pos_chan.send_nowait(int(volume))

    def _get_order(self, offset, vol, pending_frozen):
        """
        根据指定的offset和预期下单手数vol, 返回符合要求的委托单最大报单手数
        :param offset: "昨" / "今" / "开"
        :param vol: int, <0表示SELL, >0表示BUY
        :return: order_offset: "CLOSE"/"CLOSETODAY"/"OPEN"; order_dir: "BUY"/"SELL"; "order_volume": >=0, 报单手数
        """
        if vol > 0:  # 买单(增加净持仓)
            order_dir = "BUY"
            pos_all = self._pos.pos_short
        else:  # 卖单
            order_dir = "SELL"
            pos_all = self._pos.pos_long
        if offset == "昨":
            order_offset = "CLOSE"
            if self._exchange == "SHFE" or self._exchange == "INE":
                if vol > 0:
                    pos_all = self._pos.pos_short_his
                else:
                    pos_all = self._pos.pos_long_his
                frozen_volume = sum([
                    order.volume_left for order in self._pos.orders.values()
                    if not order.is_dead and order.offset == order_offset
                    and order.direction == order_dir
                ])
            else:
                frozen_volume = pending_frozen + sum([
                    order.volume_left
                    for order in self._pos.orders.values() if not order.is_dead
                    and order.offset != "OPEN" and order.direction == order_dir
                ])
                # 判断是否有未冻结的今仓手数: 若有则不平昨仓
                if (self._pos.pos_short_today if vol > 0 else
                        self._pos.pos_long_today) - frozen_volume > 0:
                    pos_all = frozen_volume
            order_volume = min(abs(vol), max(0, pos_all - frozen_volume))
        elif offset == "今":
            if self._exchange == "SHFE" or self._exchange == "INE":
                order_offset = "CLOSETODAY"
                if vol > 0:
                    pos_all = self._pos.pos_short_today
                else:
                    pos_all = self._pos.pos_long_today
                frozen_volume = sum([
                    order.volume_left for order in self._pos.orders.values()
                    if not order.is_dead and order.offset == order_offset
                    and order.direction == order_dir
                ])
            else:
                order_offset = "CLOSE"
                frozen_volume = pending_frozen + sum([
                    order.volume_left
                    for order in self._pos.orders.values() if not order.is_dead
                    and order.offset != "OPEN" and order.direction == order_dir
                ])
                pos_all = self._pos.pos_short_today if vol > 0 else self._pos.pos_long_today
            order_volume = min(abs(vol), max(0, pos_all - frozen_volume))
        elif offset == "开":
            order_offset = "OPEN"
            order_volume = abs(vol)
        else:
            order_offset = ""
            order_volume = 0
        return order_offset, order_dir, order_volume

    async def _update_time_from_md(self):
        """监听行情更新并记录当时本地时间的task"""
        try:
            chan = TqChan(self._api, last_only=True)
            self._quote = await self._api.get_quote(self._symbol)
            self._api.register_update_notify(self._quote,
                                             chan)  # quote有更新时: 更新记录的时间
            if isinstance(self._api._backtest, TqBacktest):
                # 回测情况下,在收到回测时间有更新的时候,也需要更新记录的时间
                self._api.register_update_notify(
                    _get_obj(self._api._data, ["_tqsdk_backtest"]), chan)
            async for _ in chan:
                self._local_time_record = time.time() - 0.005  # 更新最新行情时间时的本地时间
                self._local_time_record_update_chan.send_nowait(
                    True)  # 通知记录的时间有更新
        finally:
            await chan.close()

    async def _target_pos_task(self):
        """负责调整目标持仓的task"""
        all_tasks = []
        try:
            self._quote = await self._api.get_quote(self._symbol)
            async for target_pos in self._pos_chan:
                # lib 中对于时间判断的方案:
                #   如果当前时间(模拟交易所时间)不在交易时间段内,则:等待直到行情更新
                #   行情更新(即下一交易时段开始)后:获取target_pos最新的目标仓位, 开始调整仓位

                # 如果不在可交易时间段内(回测时用 backtest 下发的时间判断,实盘使用 quote 行情判断): 等待更新
                while True:
                    if isinstance(self._api._backtest, TqBacktest):
                        cur_timestamp = self._api._data.get(
                            "_tqsdk_backtest", {}).get("current_dt",
                                                       float("nan"))
                        cur_dt = _timestamp_nano_to_str(cur_timestamp)
                        time_record = float("nan")
                    else:
                        cur_dt = self._quote["datetime"]
                        time_record = self._local_time_record
                    if _is_in_trading_time(self._quote, cur_dt, time_record):
                        break
                    await self._local_time_record_update_chan.recv()

                target_pos = self._pos_chan.recv_latest(
                    target_pos)  # 获取最后一个target_pos目标仓位
                # 确定调仓增减方向
                delta_volume = target_pos - self._pos.pos
                pending_forzen = 0
                for each_priority in self._offset_priority + ",":  # 按不同模式的优先级顺序报出不同的offset单,股指(“昨开”)平昨优先从不平今就先报平昨,原油平今优先("今昨开")就报平今
                    if each_priority == ",":
                        await gather(*[each._task for each in all_tasks])
                        pending_forzen = 0
                        all_tasks = []
                        continue
                    order_offset, order_dir, order_volume = self._get_order(
                        each_priority, delta_volume, pending_forzen)
                    if order_volume == 0:  # 如果没有则直接到下一种offset
                        continue
                    elif order_offset != "OPEN":
                        pending_forzen += order_volume
                    order_task = InsertOrderUntilAllTradedTask(
                        self._api,
                        self._symbol,
                        order_dir,
                        offset=order_offset,
                        volume=order_volume,
                        min_volume=self._min_volume,
                        max_volume=self._max_volume,
                        price=self._price,
                        trade_chan=self._trade_chan,
                        trade_objs_chan=self._trade_objs_chan,
                        account=self._account)
                    all_tasks.append(order_task)
                    delta_volume -= order_volume if order_dir == "BUY" else -order_volume
        finally:
            # 执行 task.cancel() 时, 删除掉该 symbol 对应的 TargetPosTask 实例
            # self._account 类型为 TqSim/TqKq/TqAccount,都包括 _account_key 变量
            TargetPosTaskSingleton._instances.pop(
                self._account._account_key + "#" + self._symbol, None)
            await self._pos_chan.close()
            self._time_update_task.cancel()
            await asyncio.gather(*([t._task for t in all_tasks] +
                                   [self._time_update_task]),
                                 return_exceptions=True)

    def cancel(self):
        """
        取消当前 TargetPosTask 实例,会将该实例已经发出但还是未成交的委托单撤单,并且如果后续调用此实例的 set_target_volume 函数会报错。

        任何时刻,每个账户下一个合约只能有一个 TargetPosTask 实例,并且其构造参数不能修改。

        如果对于同一个合约要构造不同参数的 TargetPosTask 实例,需要调用 cancel 方法销毁,才能创建新的 TargetPosTask 实例

        Example1::

            from datetime import datetime, time
            from tqsdk import TqApi, TargetPosTask

            api = TqApi(auth=TqAuth("信易账户", "账户密码"))
            quote = api.get_quote("SHFE.rb2110")
            target_pos_passive = TargetPosTask(api, "SHFE.rb2110", price="PASSIVE")

            while datetime.strptime(quote.datetime, "%Y-%m-%d %H:%M:%S.%f").time() < time(14, 50):
                api.wait_update()
                # ... 策略代码 ...

            # 取消 TargetPosTask 实例
            target_pos_passive.cancel()

            while not target_pos_passive.is_finished():  # 此循环等待 target_pos_passive 处理 cancel 结束
                api.wait_update()  # 调用wait_update(),会对已经发出但还是未成交的委托单撤单

            # 创建新的 TargetPosTask 实例
            target_pos_active = TargetPosTask(api, "SHFE.rb2110", price="ACTIVE")
            target_pos_active.set_target_volume(0)  # 平所有仓位

            while True:
                api.wait_update()
                # ... 策略代码 ...

            api.close()

        """
        self._task.cancel()

    def is_finished(self) -> bool:
        """
        返回当前 TargetPosTask 实例是否已经结束。即如果后续调用此实例的 set_target_volume 函数会报错,此实例不会再下单或者撤单。

        Returns:
            bool: 当前 TargetPosTask 实例是否已经结束
        """
        return self._task.done()
Exemplo n.º 25
0
    def __init__(
            self,
            api: TqApi,
            symbol: str,
            price: Union[str, Callable[[str], Union[float, int]]] = "ACTIVE",
            offset_priority: str = "今昨,开",
            min_volume: Optional[int] = None,
            max_volume: Optional[int] = None,
            trade_chan: Optional[TqChan] = None,
            trade_objs_chan: Optional[TqChan] = None,
            account: Optional[Union[TqAccount, TqKq, TqSim]] = None) -> None:
        """
        创建目标持仓task实例,负责调整归属于该task的持仓 **(默认为整个账户的该合约净持仓)**.

        **注意:**
            1. TargetPosTask 在 set_target_volume 时并不下单或撤单, 它的下单和撤单动作, 是在之后的每次 wait_update 时执行的. 因此, **需保证 set_target_volume 后还会继续调用wait_update()** 。

            2. 请勿在使用 TargetPosTask 的同时使用 insert_order() 函数, 否则将导致 TargetPosTask 报错或错误下单。

            3. TargetPosTask 如果同时设置 min_volume(每笔最小下单手数),max_volume(每笔最大下单的手数)两个参数,表示采用 **大单拆分模式** 下单。

                在 **大单拆分模式** 下,每次下单的手数为随机生成的正整数,值介于 min_volume、max_volume 之间。

                具体说明:调用 set_target_volume 后,首先会根据目标持仓手数、开平仓顺序计算出,需要平今、平昨、开仓的目标下单手数及顺序。

                + 如果在调整持仓的目标下单手数小于 max_volume,则直接以目标下单手数下单。

                + 如果在调整持仓的目标下单手数大于等于 max_volume,则会以 min_volume、max_volume 之间的随机手数下一笔委托单,手数全部成交后,会接着处理剩余的手数;\
                继续以随机手数下一笔委托单,全部成交后,继续处理剩余的手数,直至剩余手数小于 max_volume 时,直接以剩余手数下单。

                当使用大单拆分模式下单时,必须同时填写 min_volume、max_volume,且需要满足 max_volume >= min_volume > 0。

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 负责调整的合约代码

            price (str / Callable): [可选]下单方式, 默认为 "ACTIVE"。
                * "ACTIVE":对价下单,在持仓调整过程中,若下单方向为买,对价为卖一价;若下单方向为卖,对价为买一价。
                * "PASSIVE":排队价下单,在持仓调整过程中,若下单方向为买,对价为买一价;若下单方向为卖,对价为卖一价。
                * Callable[[str], Union[float, int]]: 函数参数为下单方向,函数返回值是下单价格。如果返回 nan,程序会抛错。

            offset_priority (str): [可选]开平仓顺序,昨=平昨仓,今=平今仓,开=开仓,逗号=等待之前操作完成

                                   对于下单指令区分平今/昨的交易所(如上期所),按照今/昨仓的数量计算是否能平今/昨仓
                                   对于下单指令不区分平今/昨的交易所(如中金所),按照“先平当日新开仓,再平历史仓”的规则计算是否能平今/昨仓,如果这些交易所设置为"昨开"在有当日新开仓和历史仓仓的情况下,会自动跳过平昨仓进入到下一步

                                   * "今昨,开" 表示先平今仓,再平昨仓,等待平仓完成后开仓,对于没有单向大边的品种避免了开仓保证金不足
                                   * "今昨开" 表示先平今仓,再平昨仓,并开仓,所有指令同时发出,适合有单向大边的品种
                                   * "昨开" 表示先平昨仓,再开仓,禁止平今仓,适合股指这样平今手续费较高的品种
                                   * "开" 表示只开仓,不平仓,适合需要进行锁仓操作的品种

            min_volume (int): [可选] **大单拆分模式下** 每笔最小下单的手数,默认不启用 **大单拆分模式**

            max_volume (int): [可选] **大单拆分模式下** 每笔最大下单的手数,默认不启用 **大单拆分模式**

            trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上

            trade_objs_chan (TqChan): [可选]成交对象通知channel, 当有成交发生时会将成交对象发送到该channel上

            account (TqAccount/TqKq/TqSim): [可选]指定发送下单指令的账户实例, 多账户模式下,该参数必须指定

        **注意**

        当 price 参数为函数类型时,该函数应该返回一个有效的价格值,应该避免返回 nan。以下为 price 参数是函数类型时的示例。

        Example1::

            # ... 用户代码 ...
            quote = api.get_quote("SHFE.cu2012")
            def get_price(direction):
                # 在 BUY 时使用买一价加一档价格,SELL 时使用卖一价减一档价格
                if direction == "BUY":
                    price = quote.bid_price1 + quote.price_tick
                else:
                    price = quote.ask_price1 - quote.price_tick
                # 如果 price 价格是 nan,使用最新价报单
                if price != price:
                    price = quote.last_price
                return price

            target_pos = TargetPosTask(api, "SHFE.cu2012", price=get_price)
            # ... 用户代码 ...


        Example2::

            # ... 用户代码 ...
            quote1 = api.get_quote("SHFE.cu2012")
            quote2 = api.get_quote("SHFE.au2012")

            def get_price(direction, quote):
                # 在 BUY 时使用买一价加一档价格,SELL 时使用卖一价减一档价格
                if direction == "BUY":
                    price = quote.bid_price1 + quote.price_tick
                else:
                    price = quote.ask_price1 - quote.price_tick
                # 如果 price 价格是 nan,使用最新价报单
                if price != price:
                    price = quote.last_price
                return price

            target_pos1 = TargetPosTask(api, "SHFE.cu2012", price=lambda direction: get_price(direction, quote1))
            target_pos2 = TargetPosTask(api, "SHFE.au2012", price=lambda direction: get_price(direction, quote2))
            # ... 用户代码 ...

        Example3::

            # 大单拆分模式用法示例

            from tqsdk import TqApi, TqAuth, TargetPosTask
            api = TqApi(auth=TqAuth("信易账户", "账户密码"))
            position = api.get_position('SHFE.rb2106')

            # 同时设置 min_volume、max_volume 两个参数,表示使用大单拆分模式
            t = TargetPosTask(api, 'SHFE.rb2106', min_volume=2, max_volume=10)
            t.set_target_volume(50)
            while True:
                api.wait_update()
                if position.pos_long == 50:
                    break
            api.close()

            # 说明:
            # 以上代码使用 TqSim 交易,开始时用户没有 SHFE.cu2012 合约的任何持仓,那么在 t.set_target_volume(50) 之后应该开多仓 50 手
            # 根据用户参数,下单使用大单拆分模式,每次下单手数在 2~10 之间,打印出的成交通知可能是这样的:
            # 2021-03-15 11:29:48 -     INFO - 模拟交易成交记录
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.516138, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.519699, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 8, 价格: 4687.000,手续费: 37.64
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.522848, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 10, 价格: 4687.000,手续费: 47.05
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.525617, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 8, 价格: 4687.000,手续费: 37.64
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.528151, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.530930, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94
            # 2021-03-15 11:29:48 -     INFO - 时间: 2021-03-15 11:29:47.533515, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 3, 价格: 4687.000,手续费: 14.12

        """
        if symbol.startswith("CZCE.CJ"):
            raise Exception(
                "红枣期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 4 手,这些函数还未支持该规则!"
            )
        if symbol.startswith("CZCE.ZC"):
            raise Exception(
                "动力煤期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 2 手,这些函数还未支持该规则!"
            )
        super(TargetPosTask, self).__init__()
        self._api = api
        self._account = api._account._check_valid(account)
        self._symbol = symbol
        self._exchange = symbol.split(".")[0]
        self._offset_priority = _check_offset_priority(offset_priority)
        self._min_volume, self._max_volume = _check_volume_limit(
            min_volume, max_volume)
        self._price = _check_price(price)
        self._pos = self._account.get_position(self._symbol)
        self._pos_chan = TqChan(self._api, last_only=True)
        self._trade_chan = trade_chan
        self._trade_objs_chan = trade_objs_chan
        self._task = self._api.create_task(self._target_pos_task())
        self._time_update_task = self._api.create_task(
            self._update_time_from_md())  # 监听行情更新并记录当时本地时间的task
        self._local_time_record = time.time() - 0.005  # 更新最新行情时间时的本地时间
        self._local_time_record_update_chan = TqChan(
            self._api, last_only=True)  # 监听 self._local_time_record 更新
Exemplo n.º 26
0
 async def _run(self, api, api_send_chan, api_recv_chan, md_send_chan, md_recv_chan):
     """模拟交易task"""
     self._api = api
     self._tqsdk_backtest = {}  # 储存可能的回测信息
     self._tqsdk_stat = {}  # 回测结束后储存回测报告信息
     self._logger = api._logger.getChild("TqSim")  # 调试信息输出
     self._api_send_chan = api_send_chan
     self._api_recv_chan = api_recv_chan
     self._md_send_chan = md_send_chan
     self._md_recv_chan = md_recv_chan
     self._pending_peek = False
     self._diffs = []
     self._account = {
         "currency": "CNY",
         "pre_balance": self._init_balance,
         "static_balance": self._init_balance,
         "balance": self._init_balance,
         "available": self._init_balance,
         "float_profit": 0.0,
         "position_profit": 0.0,  # 期权没有持仓盈亏
         "close_profit": 0.0,
         "frozen_margin": 0.0,
         "margin": 0.0,
         "frozen_commission": 0.0,
         "commission": 0.0,
         "frozen_premium": 0.0,
         "premium": 0.0,
         "deposit": 0.0,
         "withdraw": 0.0,
         "risk_ratio": 0.0,
         "market_value": 0.0,
         "ctp_balance": float("nan"),
         "ctp_available": float("nan"),
     }
     self._positions = {}
     self._orders = {}
     self._data = Entity()
     self._data._instance_entity([])
     self._prototype = {
         "quotes": {
             "#": Quote(self),  # 行情的数据原型
         }
     }
     self._quote_tasks = {}
     self._all_subscribe = set()  # 客户端+模拟交易模块订阅的合约集合
     # 是否已经发送初始账户信息
     self._has_send_init_account = False
     md_task = self._api.create_task(self._md_handler())  # 将所有 md_recv_chan 上收到的包投递到 api_send_chan 上
     try:
         async for pack in self._api_send_chan:
             self._logger.debug("TqSim message received: %s", pack)
             if "_md_recv" in pack:
                 if pack["aid"] == "rtn_data":
                     self._md_recv(pack)  # md_recv 中会发送 wait_count 个 quotes 包给各个 quote_chan
                     await asyncio.gather(*[quote_task["quote_chan"].join() for quote_task in self._quote_tasks.values()])
                     await self._send_diff()
             elif pack["aid"] == "subscribe_quote":
                 await self._subscribe_quote(set(pack["ins_list"].split(",")))
             elif pack["aid"] == "peek_message":
                 self._pending_peek = True
                 await self._send_diff()
                 if self._pending_peek:  # 控制"peek_message"发送: 当没有新的事件需要用户处理时才推进到下一个行情
                     await self._md_send_chan.send(pack)
             elif pack["aid"] == "insert_order":
                 symbol = pack["exchange_id"] + "." + pack["instrument_id"]
                 if symbol not in self._quote_tasks:
                     quote_chan = TqChan(self._api)
                     order_chan = TqChan(self._api)
                     self._quote_tasks[symbol] = {
                         "quote_chan": quote_chan,
                         "order_chan": order_chan,
                         "task": self._api.create_task(self._quote_handler(symbol, quote_chan, order_chan))
                     }
                 await self._quote_tasks[symbol]["order_chan"].send(pack)
             elif pack["aid"] == "cancel_order":
                 # pack 里只有 order_id 信息,发送到每一个合约的 order_chan, 交由 quote_task 判断是不是当前合约下的委托单
                 for symbol in self._quote_tasks:
                     await self._quote_tasks[symbol]["order_chan"].send(pack)
             else:
                 await self._md_send_chan.send(pack)
             if self._tqsdk_backtest != {} and self._tqsdk_backtest["current_dt"] >= self._tqsdk_backtest["end_dt"] \
                     and not self._tqsdk_stat:
                 # 回测情况下,把 _send_stat_report 在循环中回测结束时执行
                 await self._send_stat_report()
     finally:
         if not self._tqsdk_stat:
             await self._send_stat_report()
         md_task.cancel()
         tasks = [md_task]
         for symbol in self._quote_tasks:
             self._quote_tasks[symbol]["task"].cancel()
             tasks.append(self._quote_tasks[symbol]["task"])
         await asyncio.gather(*tasks, return_exceptions=True)
Exemplo n.º 27
0
 async def _gen_serial(self, ins, dur):
     """k线/tick 序列的 async generator, yield 出来的行情数据带有时间戳, 因此 _send_diff 可以据此归并"""
     # 先定位左端点, focus_datetime 是 lower_bound ,这里需要的是 upper_bound
     # 因此将 view_width 和 focus_position 设置成一样,这样 focus_datetime 所对应的 k线刚好位于屏幕外
     chart_info = {
         "aid": "set_chart",
         "chart_id": _generate_uuid("PYSDK_backtest"),
         "ins_list": ins,
         "duration": dur,
         "view_width":
         8964,  # 设为8964原因:可满足用户所有的订阅长度,并在backtest中将所有的 相同合约及周期 的K线用同一个serial存储
         "focus_datetime": int(self._current_dt),
         "focus_position": 8964,
     }
     chart = _get_obj(self._data, ["charts", chart_info["chart_id"]])
     current_id = None  # 当前数据指针
     serial = _get_obj(
         self._data,
         ["klines", ins, str(dur)] if dur != 0 else ["ticks", ins])
     async with TqChan(self._api, last_only=True) as update_chan:
         serial["_listener"].add(update_chan)
         chart["_listener"].add(update_chan)
         await self._md_send_chan.send(chart_info.copy())
         try:
             async for _ in update_chan:
                 if not (chart_info.items() <= _get_obj(chart,
                                                        ["state"]).items()):
                     # 当前请求还没收齐回应, 不应继续处理
                     continue
                 left_id = chart.get("left_id", -1)
                 right_id = chart.get("right_id", -1)
                 last_id = serial.get("last_id", -1)
                 if (left_id == -1 and right_id == -1) or last_id == -1:
                     # 定位信息还没收到, 或数据序列还没收到
                     continue
                 if self._data.get("mdhis_more_data", True):
                     self._data["_listener"].add(update_chan)
                     continue
                 else:
                     self._data["_listener"].discard(update_chan)
                 if current_id is None:
                     current_id = max(left_id, 0)
                 while True:
                     if current_id > last_id:
                         # 当前 id 已超过 last_id
                         return
                     if current_id - chart_info.get("left_kline_id",
                                                    left_id) > 5000:
                         # 当前 id 已超出订阅范围, 需重新订阅后续数据
                         chart_info["left_kline_id"] = current_id
                         chart_info.pop("focus_datetime", None)
                         chart_info.pop("focus_position", None)
                         await self._md_send_chan.send(chart_info.copy())
                     # 将订阅的8964长度的窗口中的数据都遍历完后,退出循环,然后再次进入并处理下一窗口数据
                     # (因为在处理过5000条数据的同时向服务器订阅从当前id开始的新一窗口的数据,在当前窗口剩下的3000条数据处理完后,下一窗口数据也已经收到)
                     if current_id > right_id:
                         break
                     item = {
                         k: v
                         for k, v in serial["data"].get(
                             str(current_id), {}).items()
                     }
                     if dur == 0:
                         diff = {
                             "ticks": {
                                 ins: {
                                     "last_id": current_id,
                                     "data": {
                                         str(current_id): item,
                                         str(current_id - 8964): None,
                                     }
                                 }
                             }
                         }
                         if item["datetime"] > self._end_dt:  # 超过结束时间
                             return
                         yield item[
                             "datetime"], diff, self._get_quotes_from_tick(
                                 item)
                     else:
                         diff = {
                             "klines": {
                                 ins: {
                                     str(dur): {
                                         "last_id": current_id,
                                         "data": {
                                             str(current_id): {
                                                 "datetime":
                                                 item["datetime"],
                                                 "open": item["open"],
                                                 "high": item["open"],
                                                 "low": item["open"],
                                                 "close": item["open"],
                                                 "volume": 0,
                                                 "open_oi": item["open_oi"],
                                                 "close_oi":
                                                 item["open_oi"],
                                             },
                                             str(current_id - 8964): None,
                                         }
                                     }
                                 }
                             }
                         }
                         timestamp = item[
                             "datetime"] if dur < 86400000000000 else _get_trading_day_start_time(
                                 item["datetime"])
                         if timestamp > self._end_dt:  # 超过结束时间
                             return
                         yield timestamp, diff, self._get_quotes_from_kline_open(
                             self._data["quotes"][ins], timestamp,
                             item)  # K线刚生成时的数据都为开盘价
                         diff = {
                             "klines": {
                                 ins: {
                                     str(dur): {
                                         "data": {
                                             str(current_id): item,
                                         }
                                     }
                                 }
                             }
                         }
                         timestamp = item[
                             "datetime"] + dur - 1000 if dur < 86400000000000 else _get_trading_day_end_time(
                                 item["datetime"]) - 999
                         if timestamp > self._end_dt:  # 超过结束时间
                             return
                         yield timestamp, diff, self._get_quotes_from_kline(
                             self._data["quotes"][ins], timestamp,
                             item)  # K线结束时生成quote数据
                     current_id += 1
         finally:
             # 释放chart资源
             chart_info["ins_list"] = ""
             await self._md_send_chan.send(chart_info.copy())
Exemplo n.º 28
0
    def __init__(
            self,
            api: TqApi,
            symbol: str,
            time_table: DataFrame,
            offset_priority: str = "今昨,开",
            min_volume: Optional[int] = None,
            max_volume: Optional[int] = None,
            trade_chan: Optional[TqChan] = None,
            trade_objs_chan: Optional[TqChan] = None,
            account: Optional[Union[TqAccount, TqKq, TqSim]] = None) -> None:
        """
        创建算法执行引擎实例,根据设定的目标持仓任务列表,调用 TargetPosTask 来调整指定合约到目标头寸。

        **注意:**
            1. TargetPosScheduler 创建后不会立即不下单或撤单, 它的下单和撤单动作, 是在之后的每次 wait_update 时执行的. 因此, **需保证后续还会调用wait_update()** 。

            2. 请勿同时使用 TargetPosScheduler、TargetPosTask、insert_order() 函数, 否则将导致报错或错误下单。

            3. `symbol`,`offset_priority`,`min_volume`,`max_volume`,`trade_chan`,`trade_objs_chan`,`account` 这几个参数会直接传给 TargetPosTask,请按照 TargetPosTask 的说明设置参数。

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 负责调整的合约代码

            time_table (DataFrame): 目标持仓任务列表,每一行表示一项目标持仓任务,其应该包含以下几列:
                + interval: 当前这项任务的持续时间长度,单位为秒,经过这么多秒之后,此项任务应该退出,剩余未调整到的目标持仓,会留到下一项任务中
                    * 注意1:对于最后一项任务,会按照当前项参数,调整到目标持仓后立即退出(时间参数不对最后一项任务起作用)
                    * 注意2:时间长度可以跨非交易时间段(可以跨小节等待),但是不可以跨交易日
                + target_pos: 当前这项任务的目标净持仓手数
                + price: 当前这项任务的下单价格模式,此列中非 None 的项,会作为创建 TargetPosTask 实例的 price 参数,支持以下几种参数:
                    * None: 不下单,表示暂停一段时间
                    * "PASSIVE" : 排队价下单
                    * "ACTIVE": 对价下单
                    * Callable (direction: str) -> Union[float, int]: 传入函数作为价格参数,函数参数为下单方向,函数返回值是下单价格。如果返回 nan,程序会抛错。

            offset_priority (str): [可选]开平仓顺序,昨=平昨仓,今=平今仓,开=开仓,逗号=等待之前操作完成

                                   对于下单指令区分平今/昨的交易所(如上期所),按照今/昨仓的数量计算是否能平今/昨仓
                                   对于下单指令不区分平今/昨的交易所(如中金所),按照“先平当日新开仓,再平历史仓”的规则计算是否能平今/昨仓,如果这些交易所设置为"昨开"在有当日新开仓和历史仓仓的情况下,会自动跳过平昨仓进入到下一步

                                   * "今昨,开" 表示先平今仓,再平昨仓,等待平仓完成后开仓,对于没有单向大边的品种避免了开仓保证金不足
                                   * "今昨开" 表示先平今仓,再平昨仓,并开仓,所有指令同时发出,适合有单向大边的品种
                                   * "昨开" 表示先平昨仓,再开仓,禁止平今仓,适合股指这样平今手续费较高的品种
                                   * "开" 表示只开仓,不平仓,适合需要进行锁仓操作的品种

            min_volume (int): [可选] **大单拆分模式下** 每笔最小下单的手数,默认不启用 **大单拆分模式**

            max_volume (int): [可选] **大单拆分模式下** 每笔最大下单的手数,默认不启用 **大单拆分模式**

            trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上

            trade_objs_chan (TqChan): [可选]成交对象通知channel, 当有成交发生时会将成交对象发送到该channel上

            account (TqAccount/TqKq/TqSim): [可选]指定发送下单指令的账户实例, 多账户模式下,该参数必须指定

        Example::

            from pandas import DataFrame
            from tqsdk import TqApi, TargetPosScheduler

            api = TqApi(auth=TqAuth("信易账户", "账户密码"))
            time_table = DataFrame([
                [25, 10, "PASSIVE"],
                [5, 10, "ACTIVE"],
                [25, 20, "PASSIVE"],
                [5, 20, "ACTIVE"],
            ], columns=['interval', 'target_pos', 'price'])

            scheduler = TargetPosScheduler(api, 'SHFE.cu2112', time_table=time_table)
            while True:
                api.wait_update()
                if scheduler.is_finished():
                    break

            print("打印出 scheduler 全部成交以及成交均价")
            print(scheduler.trades_df)
            average_trade_price = sum(scheduler.trades_df['price'] * scheduler.trades_df['volume']) / sum(scheduler.trades_df['volume'])
            print("成交均价:", average_trade_price)
            api.close()
        """
        self._api = api
        self._account = api._account._check_valid(account)

        # 这些参数直接传给 TargetPosTask,由 TargetPosTask 来检查其合法性
        self._symbol = symbol
        self._offset_priority = offset_priority
        self._min_volume = min_volume
        self._max_volume = max_volume
        self._trade_chan = trade_chan

        self._trade_objs_chan = trade_objs_chan if trade_objs_chan else TqChan(
            self._api)
        self._time_table = _check_time_table(time_table)
        self._task = self._api.create_task(self._run())

        self._trade_keys = list(Trade(None).keys())
        self.trades_df = DataFrame(columns=self._trade_keys)  # 所有的 trade 列表
        self._trade_recv_task = self._api.create_task(self._trade_recv())
Exemplo n.º 29
0
    def __init__(self,
                 api: TqApi,
                 symbol: str,
                 price: str = "ACTIVE",
                 offset_priority: str = "今昨,开",
                 trade_chan: Optional[TqChan] = None) -> None:
        """
        创建目标持仓task实例,负责调整归属于该task的持仓 **(默认为整个账户的该合约净持仓)**.

        **注意:**
            1. TargetPosTask 在 set_target_volume 时并不下单或撤单, 它的下单和撤单动作, 是在之后的每次 wait_update 时执行的. 因此, **需保证 set_target_volume 后还会继续调用wait_update()** 。

            2. 请勿在使用 TargetPosTask 的同时使用 insert_order() 函数, 否则将导致 TargetPosTask 报错或错误下单。

        Args:
            api (TqApi): TqApi实例,该task依托于指定api下单/撤单

            symbol (str): 负责调整的合约代码

            price (str): [可选]下单方式, ACTIVE=对价下单, PASSIVE=挂价下单.

                * 在持仓调整过程中,若下单方向为买: 对价为卖一价, 挂价为买一价
                * 在持仓调整过程中,若下单方向为卖: 对价为买一价, 挂价为卖一价

            offset_priority (str): [可选]开平仓顺序,昨=平昨仓,今=平今仓,开=开仓,逗号=等待之前操作完成

                                   对于下单指令区分平今/昨的交易所(如上期所),按照今/昨仓的数量计算是否能平今/昨仓
                                   对于下单指令不区分平今/昨的交易所(如中金所),按照“先平当日新开仓,再平历史仓”的规则计算是否能平今/昨仓

                                   * "今昨,开" 表示先平今仓,再平昨仓,等待平仓完成后开仓,对于没有单向大边的品种避免了开仓保证金不足
                                   * "今昨开" 表示先平今仓,再平昨仓,并开仓,所有指令同时发出,适合有单向大边的品种
                                   * "昨开" 表示先平昨仓,再开仓,禁止平今仓,适合股指这样平今手续费较高的品种

            trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上
        """
        super(TargetPosTask, self).__init__()
        self._api = api
        if symbol not in api._data.get("quotes", {}):
            raise Exception("代码 %s 不存在, 请检查合约代码是否填写正确" % (symbol))
        self._symbol = symbol
        self._exchange = symbol.split(".")[0]
        if price not in ("ACTIVE", "PASSIVE"):
            raise Exception("下单方式(price) %s 错误, 请检查 price 参数是否填写正确" % (price))
        self._price = price
        if len(
                offset_priority.replace(",", "").replace("今", "", 1).replace(
                    "昨", "", 1).replace("开", "", 1)) > 0:
            raise Exception(
                "开平仓顺序(offset_priority) %s 错误, 请检查 offset_priority 参数是否填写正确" %
                (offset_priority))
        self._offset_priority = offset_priority
        self._pos = self._api.get_position(self._symbol)
        self._pos_chan = TqChan(self._api, last_only=True)
        self._trade_chan = trade_chan if trade_chan is not None else TqChan(
            self._api)
        self._task = self._api.create_task(self._target_pos_task())

        self._quote = self._api.get_quote(self._symbol)
        self._time_update_task = self._api.create_task(
            self._update_time_from_md())  # 监听行情更新并记录当时本地时间的task
        self._local_time_record = time.time() - 0.005  # 更新最新行情时间时的本地时间
        self._local_time_record_update_chan = TqChan(
            self._api, last_only=True)  # 监听 self._local_time_record 更新
Exemplo n.º 30
0
 async def _gen_serial(self, ins, dur):
     """k线/tick 序列的 async generator, yield 出来的行情数据带有时间戳, 因此 _send_diff 可以据此归并"""
     # 先定位左端点, focus_datetime 是 lower_bound ,这里需要的是 upper_bound
     # 因此将 view_width 和 focus_position 设置成一样,这样 focus_datetime 所对应的 k线刚好位于屏幕外
     # 使用两个长度为 8964 的 chart,去缓存/回收下游需要的数据
     chart_id_a = _generate_uuid("PYSDK_backtest")
     chart_id_b = _generate_uuid("PYSDK_backtest")
     chart_info = {
         "aid": "set_chart",
         "chart_id": chart_id_a,
         "ins_list": ins,
         "duration": dur,
         "view_width": 8964,  # 设为8964原因:可满足用户所有的订阅长度,并在backtest中将所有的 相同合约及周期 的K线用同一个serial存储
         "focus_datetime": int(self._current_dt),
         "focus_position": 8964,
     }
     chart_a = _get_obj(self._data, ["charts", chart_id_a])
     chart_b = _get_obj(self._data, ["charts", chart_id_b])
     symbol_list = ins.split(',')
     current_id = None  # 当前数据指针
     if dur == 0:
         serials = [_get_obj(self._data, ["ticks", symbol_list[0]])]
     else:
         serials = [_get_obj(self._data, ["klines", s, str(dur)]) for s in symbol_list]
     async with TqChan(self._api, last_only=True) as update_chan:
         for serial in serials:
             serial["_listener"].add(update_chan)
         chart_a["_listener"].add(update_chan)
         chart_b["_listener"].add(update_chan)
         await self._md_send_chan.send(chart_info.copy())
         try:
             async for _ in update_chan:
                 chart = _get_obj(self._data, ["charts", chart_info["chart_id"]])
                 if not (chart_info.items() <= _get_obj(chart, ["state"]).items()):
                     # 当前请求还没收齐回应, 不应继续处理
                     continue
                 left_id = chart.get("left_id", -1)
                 right_id = chart.get("right_id", -1)
                 if (left_id == -1 and right_id == -1) or chart.get("more_data", True):
                     continue  # 定位信息还没收到, 数据没有完全收到
                 last_id = serials[0].get("last_id", -1)
                 if last_id == -1:
                     continue  # 数据序列还没收到
                 if self._data.get("mdhis_more_data", True):
                     self._data["_listener"].add(update_chan)
                     continue
                 else:
                     self._data["_listener"].discard(update_chan)
                 if current_id is None:
                     current_id = max(left_id, 0)
                 # 发送下一段 chart 8964 根 kline
                 chart_info["chart_id"] = chart_id_b if chart_info["chart_id"] == chart_id_a else chart_id_a
                 chart_info["left_kline_id"] = right_id
                 chart_info.pop("focus_datetime", None)
                 chart_info.pop("focus_position", None)
                 await self._md_send_chan.send(chart_info.copy())
                 while True:
                     if current_id > last_id:
                         # 当前 id 已超过 last_id
                         return
                     # 将订阅的8964长度的窗口中的数据都遍历完后,退出循环,然后再次进入并处理下一窗口数据
                     if current_id > right_id:
                         break
                     item = {k: v for k, v in serials[0]["data"].get(str(current_id), {}).items()}
                     if dur == 0:
                         diff = {
                             "ticks": {
                                 ins: {
                                     "last_id": current_id,
                                     "data": {
                                         str(current_id): item,
                                         str(current_id - 8964): None,
                                     }
                                 }
                             }
                         }
                         if item["datetime"] > self._end_dt:  # 超过结束时间
                             return
                         yield item["datetime"], diff, self._get_quotes_from_tick(item)
                     else:
                         timestamp = item["datetime"] if dur < 86400000000000 else _get_trading_day_start_time(
                             item["datetime"])
                         if timestamp > self._end_dt:  # 超过结束时间
                             return
                         binding = serials[0].get("binding", {})
                         diff = {
                             "klines": {
                                 symbol_list[0]: {
                                     str(dur): {
                                         "last_id": current_id,
                                         "data": {
                                             str(current_id): {
                                                 "datetime": item["datetime"],
                                                 "open": item["open"],
                                                 "high": item["open"],
                                                 "low": item["open"],
                                                 "close": item["open"],
                                                 "volume": 0,
                                                 "open_oi": item["open_oi"],
                                                 "close_oi": item["open_oi"],
                                             }
                                         }
                                     }
                                 }
                             }
                         }
                         for chart_id in self._serials[(ins, dur)]["chart_id_set"]:
                             diff["charts"] = {
                                 chart_id: {
                                     "right_id": current_id  # api 中处理多合约 kline 需要 right_id 信息
                                 }
                             }
                         for i, symbol in enumerate(symbol_list):
                             if i == 0:
                                 diff_binding = diff["klines"][symbol_list[0]][str(dur)].setdefault("binding", {})
                                 continue
                             other_id = binding.get(symbol, {}).get(str(current_id), -1)
                             if other_id >= 0:
                                 diff_binding[symbol] = {str(current_id): str(other_id)}
                                 other_item = serials[i]["data"].get(str(other_id), {})
                                 diff["klines"][symbol] = {
                                     str(dur): {
                                         "last_id": other_id,
                                         "data": {
                                             str(other_id): {
                                                 "datetime": other_item["datetime"],
                                                 "open": other_item["open"],
                                                 "high": other_item["open"],
                                                 "low": other_item["open"],
                                                 "close": other_item["open"],
                                                 "volume": 0,
                                                 "open_oi": other_item["open_oi"],
                                                 "close_oi": other_item["open_oi"],
                                             }
                                         }
                                     }
                                 }
                         yield timestamp, diff, self._get_quotes_from_kline_open(
                             self._data["quotes"][symbol_list[0]],
                             timestamp,
                             item)  # K线刚生成时的数据都为开盘价
                         timestamp = item["datetime"] + dur - 1000 \
                             if dur < 86400000000000 else _get_trading_day_start_time(item["datetime"] + dur) - 1000
                         if timestamp > self._end_dt:  # 超过结束时间
                             return
                         diff = {
                             "klines": {
                                 symbol_list[0]: {
                                     str(dur): {
                                         "data": {
                                             str(current_id): item,
                                         }
                                     }
                                 }
                             }
                         }
                         for i, symbol in enumerate(symbol_list):
                             if i == 0:
                                 continue
                             other_id = binding.get(symbol, {}).get(str(current_id), -1)
                             if other_id >= 0:
                                 diff["klines"][symbol] = {
                                     str(dur): {
                                         "data": {
                                             str(other_id): {k: v for k, v in
                                                             serials[i]["data"].get(str(other_id), {}).items()}
                                         }
                                     }
                                 }
                         yield timestamp, diff, self._get_quotes_from_kline(self._data["quotes"][symbol_list[0]],
                                                                            timestamp,
                                                                            item)  # K线结束时生成quote数据
                     current_id += 1
         finally:
             # 释放chart资源
             chart_info["ins_list"] = ""
             await self._md_send_chan.send(chart_info.copy())
             chart_info["chart_id"] = chart_id_b if chart_info["chart_id"] == chart_id_a else chart_id_a
             await self._md_send_chan.send(chart_info.copy())