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)
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)
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
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)
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 _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 __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())
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
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
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 无法正常结束。" )
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()
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 } })
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
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)
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)
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, })
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())
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)
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()
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())
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)
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
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())
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()
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 更新
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)
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())
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())
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 更新
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())