def __init__(self, api, symbol_list, backtest_timestamp, *args, **kwargs): self.__dict__["_api"] = api self.__dict__["_symbol_list"] = symbol_list self.__dict__["_backtest_timestamp"] = backtest_timestamp self.__dict__["_columns"] = [ "ins_class", "instrument_id", "instrument_name", "price_tick", "volume_multiple", "max_limit_order_volume", "max_market_order_volume", "underlying_symbol", "strike_price", "exchange_id", "product_id", "expired", "expire_datetime", "expire_rest_days", "delivery_year", "delivery_month", "last_exercise_datetime", "exercise_year", "exercise_month", "option_class", "upper_limit", "lower_limit", "pre_settlement", "pre_open_interest", "pre_close", "trading_time_day", "trading_time_night" ] default_quote = Quote(None) data = [{ k: (s if k == "instrument_id" else default_quote.get(k, None)) for k in self.__dict__["_columns"] } for s in symbol_list] super(TqSymbolDataFrame, self).__init__(data=data, columns=self.__dict__["_columns"], *args, **kwargs) self.__dict__["_task"] = api.create_task(self.async_update(), _caller_api=True)
def set_commission(self, symbol: str, commission: float=float('nan')): """ 设置指定合约模拟交易的每手手续费。 Args: symbol (str): 合约代码 commission (float): 每手手续费 Returns: float: 设置的每手手续费 Example:: from tqsdk import TqSim, TqApi, TqAuth sim = TqSim() api = TqApi(sim, auth=TqAuth("信易账户", "账户密码")) sim.set_commission("SHFE.cu2112", 50) print(sim.get_commission("SHFE.cu2112")) """ if commission != commission: raise Exception("合约手续费不可以设置为 float('nan')") quote = _get_obj(self._data, ["quotes", symbol], Quote(self._api if hasattr(self, "_api") else None)) quote["user_commission"] = commission if self._quote_tasks.get(symbol): self._quote_tasks[symbol]["quote_chan"].send_nowait({ "quotes": {symbol: {"user_commission": commission}} }) return commission
def __init__( self, account_id, init_balance, trade_class: Union[Type[SimTrade], Type[SimTradeStock]]) -> None: self._account_id = account_id super(BaseSim, self).__init__() self.trade_log = {} # 日期->交易记录及收盘时的权益及持仓 self.tqsdk_stat = {} # 回测结束后储存回测报告信息 self._init_balance = init_balance self._current_datetime = "1990-01-01 00:00:00.000000" # 当前行情时间(最新的 quote 时间) self._trading_day_end = "1990-01-01 18:00:00.000000" self._local_time_record = float("nan") # 记录获取最新行情时的本地时间 self._sim_trade = trade_class( account_key=self._account_key, account_id=self._account_id, init_balance=self._init_balance, get_trade_timestamp=self._get_trade_timestamp, is_in_trading_time=self._is_in_trading_time) self._data = Entity() self._data._instance_entity([]) self._prototype = { "quotes": { "#": Quote(self), # 行情的数据原型 } } self._quote_tasks = {}
def _symbols_to_quotes(symbols, keys=set(Quote(None).keys())): """将 symbols 转为 quotes,只输出 keys 包括的字段""" result = symbols.get("result", {}) quotes = {} for k in result: for symbol in result[k]: quote = quotes.setdefault(symbol["instrument_id"], {}) quote.update(_convert_symbol_to_quote(symbol, keys)) if symbol.get("underlying"): for edge in symbol["underlying"]["edges"]: underlying_symbol = edge["node"] if "underlying_symbol" in keys: quote["underlying_symbol"] = underlying_symbol["instrument_id"] underlying_quote = quotes.setdefault(underlying_symbol["instrument_id"], {}) underlying_quote.update(_convert_symbol_to_quote(underlying_symbol, keys)) # 为期权合约补充 delivery_year delivery_month 商品期权根据标的赋值;金融期权与 exercise_year exercise_month 相同 # 为期权补充 delivery_year delivery_month 完全是为了兼容旧版合约服务 for key in ["delivery_year", "delivery_month"]: if key in keys and symbol["class"] == "OPTION": if symbol["exchange_id"] in ["DCE", "CZCE", "SHFE"]: quote[key] = underlying_quote[key] if symbol["exchange_id"] == "CFFEX" and "last_exercise_datetime" in symbol: if key == "delivery_year": quote[key] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).year else: quote[key] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).month for k in quotes: if quotes[k].get("ins_class", "") == "COMBINE": # 为组合合约补充 volume_multiple leg1_quote = quotes.get(quotes[k].get("leg1_symbol", ""), {}) if leg1_quote: if leg1_quote.get("volume_multiple"): quotes[k]["volume_multiple"] = leg1_quote["volume_multiple"] return quotes
async def _ensure_quote(self, symbol, quote_chan): """quote收到行情后返回""" quote = _get_obj(self._data, ["quotes", symbol], Quote(self._api)) _register_update_chan(quote, quote_chan) if quote.get("datetime", ""): return quote.copy() async for _ in quote_chan: quote_chan.task_done() if quote.get("datetime", ""): return quote.copy()
async def _ensure_quote_info(self, symbol, quote_chan): """quote收到合约信息后返回""" quote = _get_obj(self._data, ["quotes", symbol], Quote(self._api)) if quote.get("price_tick") == quote.get("price_tick"): return quote.copy() if quote.get("price_tick") != quote.get("price_tick"): await self._md_send_chan.send(_query_for_quote(symbol)) async for _ in quote_chan: quote_chan.task_done() if quote.get("price_tick") == quote.get("price_tick"): return quote.copy()
async def _ensure_quote(self, symbol, quote_chan): """quote收到行情以及合约信息后返回""" quote = _get_obj(self._data, ["quotes", symbol], Quote(self._api)) _register_update_chan(quote, quote_chan) if quote.get("datetime", "") and quote.get("price_tick") == quote.get("price_tick"): return quote.copy() if quote.get("price_tick") != quote.get("price_tick"): # 对于没有合约信息的 quote,发送查询合约信息的请求 await self._md_send_chan.send(_query_for_quote(symbol)) async for _ in quote_chan: quote_chan.task_done() if quote.get("datetime", "") and quote.get("price_tick") == quote.get("price_tick"): return quote.copy()
def set_margin(self, symbol: str, margin: float = float('nan')): """ 设置指定合约模拟交易的每手保证金。 Args: symbol (str): 合约代码 (只支持期货合约) margin (float): 每手保证金 Returns: float: 设置的每手保证金 Example:: from tqsdk import TqSim, TqApi, TqAuth sim = TqSim() api = TqApi(sim, auth=TqAuth("信易账户", "账户密码")) sim.set_margin("SHFE.cu2112", 26000) print(sim.get_margin("SHFE.cu2112")) """ if margin != margin: raise Exception("合约手续费不可以设置为 float('nan')") quote = _get_obj(self._data, ["quotes", symbol], Quote(self._api if hasattr(self, "_api") else None)) quote["user_margin"] = margin if self._quote_tasks.get(symbol): self._quote_tasks[symbol]["quote_chan"].send_nowait( {"quotes": { symbol: { "user_margin": margin } }}) # 当用户设置保证金时,用户应该得到的效果是: # 在调用 sim.set_margin() 之后,立即调用 api.get_position(symbol),得到的 margin 字段应该按照新设置的保证金调整过,而且中间没有收到过行情更新包 # 以下代码可以保证这个效果,说明: # 1. 持仓已经调整过: # sim_trade 中持仓的 future_margin 字段更新,margin 会同时调整,那么 api 中持仓的 future_margin 更新时,margin 一定也已经更新 # 2. 中间没有收到过行情更新包: # 前提1:根据 diff 协议,sim 收到 peek_message 时,会将缓存的 diffs 发给用户,当缓存的 diffs 为空,会转发 peek_message; # 前提2:api.wait_update() 会等到所有 task 都执行到 pending 状态,然后发送 peek_message 给 sim # 当用户代码执行到 sim.set_margin(),立即向 quote_chan 中发送一个数据包,quote_task 就会到 ready 状态,此时调用 wait_update(), # 到所有 task 执行到 pending 状态时,sim 的 diffs 中有数据了,此时收到 api 发来 peek_message 不会转发给上游,用户会先收到 sim 本身的账户数据, # 在下一次 wait_update,sim 的 diffs 为空,才会收到行情数据 # 在回测时,以下代码应该只经历一次 wait_update while margin != self.get_position(symbol).get("future_margin"): self._api.wait_update() return margin
async def _run(self, api, sim_send_chan, sim_recv_chan, md_send_chan, md_recv_chan): """回测task""" self._api = api self._sim_send_chan = sim_send_chan self._sim_recv_chan = sim_recv_chan self._md_send_chan = md_send_chan self._md_recv_chan = md_recv_chan self._quotes_all_keys = set(Quote(None).keys()) self._quotes_all_keys = self._quotes_all_keys.union({'margin', 'commission'}) # 以下字段合约服务也会请求,但是不应该记在 quotes 中,quotes 中的这些字段应该有行情服务负责 self._quotes_all_keys.difference_update({'pre_open_interest', 'pre_close', 'upper_limit', 'lower_limit'}) sim_task = self._api.create_task(self._sim_handler()) try: async for pack in self._md_recv_chan: if pack.get("aid") == "rtn_data": data = pack.setdefault("data", []) # 对于收到的数据,全部转发给下游 # 对于合约服务信息,query_id 为 PYSDK_quote_xxx 开头的,一定是请求了合约的全部合约信息,需要转为 quotes 转发给下游 for d in data: for query_id, query_result in d.get("symbols", {}).items(): if query_result: if query_result.get("error", None): raise Exception(f"查询合约服务报错 {query_result['error']}") elif query_id.startswith("PYSDK_quote"): quotes = self._api._symbols_to_quotes(query_result, self._quotes_all_keys) for quote in quotes.values(): if not (quote["ins_class"] == "OPTION" and quote["exchange_id"] == "SSE"): # quotes 中的 pre_settlement 字段应该由行情服务负责,行情没有上交所期权的 pre_settlement,需要从合约服务取,其他合约不变 quote.pop("pre_settlement", None) data.append( {"quotes": quotes} ) self._md_send_chan.send_nowait({ "aid": "ins_query", "query_id": query_id, "query": "" }) await self._sim_recv_chan.send(pack) finally: sim_task.cancel() await asyncio.gather(sim_task, return_exceptions=True)
def _quotes_to_dataframe(self, quotes): default_quote = Quote(None) for col in self.__dict__["_columns"]: if col == "expire_rest_days": current_dt = self._api._get_current_datetime().timestamp() self.loc[:, col] = [ _get_expire_rest_days(quotes[s]['expire_datetime'], current_dt) if quotes[s].get('expire_datetime') else float('nan') for s in self.__dict__["_symbol_list"] ] elif col == "trading_time_day" or col == "trading_time_night": k = 'day' if col == "trading_time_day" else 'night' self.loc[:, col] = Series([ self._get_trading_time(quotes, s, k) for s in self.__dict__["_symbol_list"] ]) else: self.loc[:, col] = Series([ quotes[s].get(col, default_quote[col]) for s in self.__dict__["_symbol_list"] ])
def __init__(self, init_balance: float = 10000000.0, account_id: str = None) -> None: """ Args: init_balance (float): [可选]初始资金, 默认为一千万 account_id (str): [可选]帐号, 默认为 TQSIM Example:: # 修改TqSim模拟帐号的初始资金为100000 from tqsdk import TqApi, TqSim, TqAuth api = TqApi(TqSim(init_balance=100000), auth=TqAuth("信易账户", "账户密码")) """ self.trade_log = {} # 日期->交易记录及收盘时的权益及持仓 self.tqsdk_stat = {} # 回测结束后储存回测报告信息 self._account_id = "TQSIM" if account_id is None else account_id self._account_type = "FUTURE" self._broker_id = "TQSIM" if self._account_type == "FUTURE" else "TQSIM_STOCK" self._account_key = str(id(self)) self._init_balance = float(init_balance) if self._init_balance <= 0: raise Exception("初始资金(init_balance) %s 错误, 请检查 init_balance 是否填写正确" % (init_balance)) self._current_datetime = "1990-01-01 00:00:00.000000" # 当前行情时间(最新的 quote 时间) self._trading_day_end = "1990-01-01 18:00:00.000000" self._local_time_record = float("nan") # 记录获取最新行情时的本地时间 self._sim_trade = SimTrade(account_key=self._account_key, init_balance=self._init_balance, get_trade_timestamp=self._get_trade_timestamp, is_in_trading_time=self._is_in_trading_time) self._data = Entity() self._data._instance_entity([]) self._prototype = { "quotes": { "#": Quote(self), # 行情的数据原型 } } self._quote_tasks = {}
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)