def read_config(self, path): try: self.account_config = file2dict(path) except ValueError: logger.error("配置文件格式有误,请勿使用记事本编辑,推荐 sublime text") for value in self.account_config: if isinstance(value, int): logger.warning("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题")
def extract_day_trades(self, run_id): ret_json = self.client.get_day_trades(run_id) if ret_json["code"] != 200: logger.error( "fetch day trades from run_id %s fail, msg %s", run_id, ret_json["msg"], ) raise RuntimeError(ret_json["msg"]) return ret_json["resp"]["trades"]
def extract_strategy_name(self, run_id): ret_json = self.client.get_positions(run_id) if ret_json["code"] != 200: logger.error( "fetch data from run_id %s fail, msg %s", run_id, ret_json["msg"], ) raise RuntimeError(ret_json["msg"]) return ret_json["resp"]["name"]
def check_login(self, sleepy=30): logger.setLevel(logging.ERROR) try: response = self.heartbeat() self.check_account_live(response) except requests.exceptions.ConnectionError: pass except requests.exceptions.RequestException as e: logger.setLevel(self.log_level) logger.error("心跳线程发现账户出现错误: %s %s, 尝试重新登陆", e.__class__, e) self.autologin() finally: logger.setLevel(self.log_level) time.sleep(sleepy)
def follow( self, users, strategies, track_interval=1, trade_cmd_expire_seconds=120, cmd_cache=True, entrust_prop="limit", send_interval=0, ): """跟踪joinquant对应的模拟交易,支持多用户多策略 :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户 :param strategies: joinquant 的模拟交易地址,支持使用 [] 指定多个模拟交易, 地址类似 https://www.joinquant.com/algorithm/live/index?backtestId=xxx :param track_interval: 轮训模拟交易时间,单位为秒 :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒 :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令 :param entrust_prop: 委托方式, 'limit' 为限价,'market' 为市价, 仅在银河实现 :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足 """ users = self.warp_list(users) strategies = self.warp_list(strategies) if cmd_cache: self.load_expired_cmd_cache() self.start_trader_thread(users, trade_cmd_expire_seconds, entrust_prop, send_interval) workers = [] for strategy_url in strategies: try: strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: logger.error("抽取交易id和策略名失败, 无效的模拟交易url: %s", strategy_url) raise strategy_worker = Thread( target=self.track_strategy_worker, args=[strategy_id, strategy_name], kwargs={"interval": track_interval}, ) strategy_worker.start() workers.append(strategy_worker) logger.info("开始跟踪策略: %s", strategy_name) for worker in workers: worker.join()
def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): """ 调仓 :param security: :param price: :param amount: :param volume: :param entrust_bs: :return: """ stock = self._search_stock_info(security) balance = self.get_balance()[0] if stock is None: raise exceptions.TradeError(u"没有查询要操作的股票信息") if not volume: volume = int(float(price) * amount) # 可能要取整数 if balance["current_balance"] < volume and entrust_bs == "buy": raise exceptions.TradeError(u"没有足够的现金进行操作") if stock["flag"] != 1: raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") if volume == 0: raise exceptions.TradeError(u"操作金额不能为零") # 计算调仓调仓份额 weight = volume / balance["asset_balance"] * 100 weight = round(weight, 2) # 获取原有仓位信息 position_list = self._get_position() # 调整后的持仓 is_have = False for position in position_list: if position["stock_id"] == stock["stock_id"]: is_have = True position["proactive"] = True old_weight = position["weight"] if entrust_bs == "buy": position["weight"] = weight + old_weight else: if weight > old_weight: raise exceptions.TradeError(u"操作数量大于实际可卖出数量") else: position["weight"] = old_weight - weight position["weight"] = round(position["weight"], 2) if not is_have: if entrust_bs == "buy": position_list.append({ "code": stock["code"], "name": stock["name"], "enName": stock["enName"], "hasexist": stock["hasexist"], "flag": stock["flag"], "type": stock["type"], "current": stock["current"], "chg": stock["chg"], "percent": str(stock["percent"]), "stock_id": stock["stock_id"], "ind_id": stock["ind_id"], "ind_name": stock["ind_name"], "ind_color": stock["ind_color"], "textname": stock["name"], "segment_name": stock["ind_name"], "weight": round(weight, 2), "url": "/S/" + stock["code"], "proactive": True, "price": str(stock["current"]), }) else: raise exceptions.TradeError(u"没有持有要卖出的股票") if entrust_bs == "buy": cash = ((balance["current_balance"] - volume) / balance["asset_balance"] * 100) else: cash = ((balance["current_balance"] + volume) / balance["asset_balance"] * 100) cash = round(cash, 2) logger.info("weight:%f, cash:%f", weight, cash) data = { "cash": cash, "holdings": str(json.dumps(position_list)), "cube_symbol": str(self.account_config["portfolio_code"]), "segment": 1, "comment": "", } try: resp = self.s.post(self.config["rebalance_url"], data=data) # pylint: disable=broad-except except Exception as e: logger.warning("调仓失败: %s ", e) return None else: logger.info("调仓 %s%s: %d", entrust_bs, stock["name"], resp.status_code) resp_json = json.loads(resp.text) if "error_description" in resp_json and resp.status_code != 200: logger.error("调仓错误: %s", resp_json["error_description"]) return [{ "error_no": resp_json["error_code"], "error_info": resp_json["error_description"], }] return [{ "entrust_no": resp_json["id"], "init_date": self._time_strftime(resp_json["created_at"]), "batch_no": "委托批号", "report_no": "申报号", "seat_no": "席位编号", "entrust_time": self._time_strftime(resp_json["updated_at"]), "entrust_price": price, "entrust_amount": amount, "stock_code": security, "entrust_bs": "买入", "entrust_type": "雪球虚拟委托", "entrust_status": "-", }]
def adjust_weight(self, stock_code, weight): """ 雪球组合调仓, weight 为调整后的仓位比例 :param stock_code: str 股票代码 :param weight: float 调整之后的持仓百分比, 0 - 100 之间的浮点数 """ stock = self._search_stock_info(stock_code) if stock is None: raise exceptions.TradeError(u"没有查询要操作的股票信息") if stock["flag"] != 1: raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") # 仓位比例向下取两位数 weight = round(weight, 2) # 获取原有仓位信息 position_list = self._get_position() # 调整后的持仓 for position in position_list: if position["stock_id"] == stock["stock_id"]: position["proactive"] = True position["weight"] = weight if weight != 0 and stock["stock_id"] not in [ k["stock_id"] for k in position_list ]: position_list.append({ "code": stock["code"], "name": stock["name"], "enName": stock["enName"], "hasexist": stock["hasexist"], "flag": stock["flag"], "type": stock["type"], "current": stock["current"], "chg": stock["chg"], "percent": str(stock["percent"]), "stock_id": stock["stock_id"], "ind_id": stock["ind_id"], "ind_name": stock["ind_name"], "ind_color": stock["ind_color"], "textname": stock["name"], "segment_name": stock["ind_name"], "weight": weight, "url": "/S/" + stock["code"], "proactive": True, "price": str(stock["current"]), }) remain_weight = 100 - sum(i.get("weight") for i in position_list) cash = round(remain_weight, 2) logger.info("调仓比例:%f, 剩余持仓 :%f", weight, remain_weight) data = { "cash": cash, "holdings": str(json.dumps(position_list)), "cube_symbol": str(self.account_config["portfolio_code"]), "segment": "true", "comment": "", } try: resp = self.s.post(self.config["rebalance_url"], data=data) # pylint: disable=broad-except except Exception as e: logger.warning("调仓失败: %s ", e) return None logger.info("调仓 %s: 持仓比例%d", stock["name"], weight) resp_json = json.loads(resp.text) if "error_description" in resp_json and resp.status_code != 200: logger.error("调仓错误: %s", resp_json["error_description"]) return [{ "error_no": resp_json["error_code"], "error_info": resp_json["error_description"], }] logger.info("调仓成功 %s: 持仓比例%d", stock["name"], weight) return None
def _execute_trade_cmd(self, trade_cmd, users, expire_seconds, entrust_prop, send_interval): """分发交易指令到对应的 user 并执行 :param trade_cmd: :param users: :param expire_seconds: :param entrust_prop: :param send_interval: :return: """ for user in users: # check expire now = datetime.datetime.now() expire = (now - trade_cmd["datetime"]).total_seconds() if expire > expire_seconds: logger.warning( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 超过设置的最大过期时间 %s 秒, 被丢弃", trade_cmd["strategy_name"], trade_cmd["stock_code"], trade_cmd["action"], trade_cmd["amount"], trade_cmd["price"], trade_cmd["datetime"], now, expire_seconds, ) break # check price price = trade_cmd["price"] if not self._is_number(price) or price <= 0: logger.warning( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 价格无效 , 被丢弃", trade_cmd["strategy_name"], trade_cmd["stock_code"], trade_cmd["action"], trade_cmd["amount"], trade_cmd["price"], trade_cmd["datetime"], now, ) break # check amount if trade_cmd["amount"] <= 0: logger.warning( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 买入股数无效 , 被丢弃", trade_cmd["strategy_name"], trade_cmd["stock_code"], trade_cmd["action"], trade_cmd["amount"], trade_cmd["price"], trade_cmd["datetime"], now, ) break actual_price = self._calculate_price_by_slippage( trade_cmd["action"], trade_cmd["price"]) args = { "security": trade_cmd["stock_code"], "price": actual_price, "amount": trade_cmd["amount"], "entrust_prop": entrust_prop, } try: response = getattr(user, trade_cmd["action"])(**args) except exceptions.TradeError as e: trader_name = type(user).__name__ err_msg = "{}: {}".format(type(e).__name__, e.args) logger.error( "%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 失败, 错误信息: %s", trader_name, trade_cmd["strategy_name"], trade_cmd["stock_code"], trade_cmd["action"], trade_cmd["amount"], actual_price, trade_cmd["datetime"], err_msg, ) else: logger.info( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 执行成功, 返回: %s", trade_cmd["strategy_name"], trade_cmd["stock_code"], trade_cmd["action"], trade_cmd["amount"], actual_price, trade_cmd["datetime"], response, )
def follow( # type: ignore self, users, strategies, total_assets=10000, initial_assets=None, adjust_sell=False, adjust_buy=False, track_interval=10, trade_cmd_expire_seconds=120, cmd_cache=True, slippage: float = 0.0, ): """跟踪 joinquant 对应的模拟交易,支持多用户多策略 :param users: 支持 easytrader 的用户对象,支持使用 [] 指定多个用户 :param strategies: 雪球组合名, 类似 ZH123450 :param total_assets: 雪球组合对应的总资产, 格式 [组合1对应资金, 组合2对应资金] 若 strategies=['ZH000001', 'ZH000002'], 设置 total_assets=[10000, 10000], 则表明每个组合对应的资产为 1w 元 假设组合 ZH000001 加仓 价格为 p 股票 A 10%, 则对应的交易指令为 买入 股票 A 价格 P 股数 1w * 10% / p 并按 100 取整 :param adjust_sell: 是否根据用户的实际持仓数调整卖出股票数量, 当卖出股票数大于实际持仓数时,调整为实际持仓数。目前仅在银河客户端测试通过。 当 users 为多个时,根据第一个 user 的持仓数决定 :type adjust_sell: bool :param initial_assets: 雪球组合对应的初始资产, 格式 [ 组合1对应资金, 组合2对应资金 ] 总资产由 初始资产 × 组合净值 算得, total_assets 会覆盖此参数 :param track_interval: 轮训模拟交易时间,单位为秒 :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒 :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令 :param slippage: 滑点,0.0 表示无滑点, 0.05 表示滑点为 5% """ super().follow( users=users, strategies=strategies, track_interval=track_interval, trade_cmd_expire_seconds=trade_cmd_expire_seconds, cmd_cache=cmd_cache, slippage=slippage, ) self._adjust_sell = adjust_sell self._adjust_buy = adjust_buy self._expire_seconds = trade_cmd_expire_seconds self._users = self.warp_list(users) strategies = self.warp_list(strategies) total_assets = self.warp_list(total_assets) initial_assets = self.warp_list(initial_assets) if cmd_cache: self.load_expired_cmd_cache() self.start_trader_thread(self._users, trade_cmd_expire_seconds) for strategy_url, strategy_total_assets, strategy_initial_assets in zip( strategies, total_assets, initial_assets ): assets = self.calculate_assets( strategy_url, strategy_total_assets, strategy_initial_assets ) try: strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: logger.error("抽取交易id和策略名失败, 无效模拟交易url: %s", strategy_url) raise strategy_worker = Thread( target=self.track_strategy_worker, args=[strategy_id, strategy_name], kwargs={"interval": track_interval, "assets": assets}, ) strategy_worker.start() logger.info("开始跟踪策略: %s", strategy_name)
def project_transactions(self, transactions, assets): logger.info("总额:" + str(assets)) # 打开数据库连接 db = pymysql.connect("localhost", "root", "sa123$", "xueqiu") sql_operation = "" for transaction in transactions: weight_diff = self.none_to_zero( transaction["target_weight"]) - self.none_to_zero( transaction["prev_weight_adjusted"]) start_amount = 0 initial_amount = 0 crrut_amount = 0 if (abs(weight_diff) != 0): initial_amount = abs( weight_diff) / 100 * assets / transaction["price"] if (self.none_to_zero(transaction["prev_weight_adjusted"]) != 0): start_amount = abs( self.none_to_zero(transaction["prev_weight_adjusted"]) ) / 100 * assets / transaction["price"] if (self.none_to_zero(transaction["target_weight"]) != 0): crrut_amount = abs( self.none_to_zero(transaction["target_weight"]) ) / 100 * assets / transaction["price"] transaction["datetime"] = datetime.fromtimestamp( transaction["updated_at"] // 1000) transaction["stock_code"] = transaction["stock_symbol"].lower() transaction["action"] = "买入" if weight_diff > 0 else "卖出" transaction["amount"] = int(round(initial_amount, -2)) if transaction["action"] == "卖出" and self._adjust_sell: transaction["amount"] = self._adjust_sell_amount( transaction["stock_code"], transaction["amount"]) logger.info("动作:" + str(transaction["action"])) logger.info("股票类型:" + str(transaction["stock_label"])) logger.info("股票名称:" + str(transaction["stock_name"])) logger.info("股票号:" + str(transaction["stock_symbol"])) logger.info("调仓前百分比:" + str(transaction["prev_weight_adjusted"])) logger.info("调仓后百分比:" + str(transaction["target_weight"])) logger.info("百分比:" + str(abs(weight_diff))) logger.info("数量:" + str(int(round(initial_amount, -2)))) logger.info("价格:" + str(transaction["price"])) logger.info("成本:" + str(transaction["price"] * float(round(initial_amount, -2)))) logger.info("时间:" + str(transaction["datetime"])) # 使用cursor()方法获取操作游标 cursor = db.cursor() # 类似于其他语言的 query 函数,execute 是 python 中的执行查询函数 cursor.execute("SELECT * FROM xq_operation where STOCK_CODE = '" + str(transaction["stock_symbol"]) + "' and OPERATION_TIME = '" + str(transaction["datetime"]) + "' ") # 使用 fetchall 函数,将结果集(多维元组)存入 rows 里面 rows = cursor.fetchall() if (len(rows) <= 0): sql_operation = """INSERT IGNORE INTO xq_operation( ACCOUNT_ID,STOCK_NAME, STOCK_CODE, STOCK_OPERATION, STOCK_PRICE, STOCK_COUNT, COST,START_REPERTORY, END_REPERTORY, OPERATION_TIME, IS_DEL) VALUES ('1','{}','{}','{}',{},{},{},{},{},'{}','0')""".format( str(transaction["stock_name"]), str(transaction["stock_symbol"]), str(transaction["action"]), Decimal.from_float(transaction["price"]), int(round(initial_amount, -2)), transaction["price"] * int(round(initial_amount, -2)), transaction["prev_weight_adjusted"], transaction["target_weight"], str(transaction["datetime"])) try: if (sql_operation != ""): logger.info(sql_operation) cursor.execute(sql_operation) # 执行sql语句 except Exception: logger.error("发生异常1", Exception) # db.rollback() # 如果发生错误则回滚 # 类似于其他语言的 query 函数,execute 是 python 中的执行查询函数 cursor.execute("SELECT * FROM xq_history where STOCK_CODE = '" + str(transaction["stock_symbol"]) + "' ") # 使用 fetchall 函数,将结果集(多维元组)存入 rows 里面 rowss = cursor.fetchall() IS_HAS = 0 if (str(transaction["target_weight"]) is "0.00" or str(transaction["target_weight"]) is "0" or str(transaction["target_weight"]) is "0.0" or crrut_amount == 0): IS_HAS = 1 if (len(rowss) > 0): # 计算平均价格 cursor.execute( "SELECT AVG(STOCK_PRICE) FROM xq_operation WHERE stock_code='" + str(transaction["stock_symbol"]) + "' ") # 使用 fetchall 函数,将结果集(多维元组)存入 rows 里面 rowsxq = cursor.fetchall() ave_price = 0 for row in rowsxq: ave_price = row[0] sql_xq_history = """update xq_history set STOCK_COUNT = {},AVE_PRICE = {}, START_REPERTORY = {}, HISTORY_TIME = '{}', IS_HAS = '{}' where STOCK_CODE = '{}' """.format( decimal.Decimal(int(round(crrut_amount, -2))), ave_price, str(transaction["target_weight"]), str(transaction["datetime"]), IS_HAS, str(transaction["stock_symbol"])) try: if (sql_xq_history != ""): # logger.info(sql_xq_history) cursor.execute(sql_xq_history) # 执行sql语句 except Exception: logger.error("发生异常2", Exception) else: sql_operation = """INSERT IGNORE INTO xq_history( ACCOUNT_ID,STOCK_NAME, STOCK_CODE, STOCK_COUNT, AVE_PRICE, START_REPERTORY, HISTORY_TIME, IS_HAS, IS_DEL) VALUES ('1','{}','{}',{},{},'{}','{}',{},'0')""".format( str(transaction["stock_name"]), str(transaction["stock_symbol"]), int(round(initial_amount, -2)), transaction["price"], transaction["target_weight"], str(transaction["datetime"]), IS_HAS, '0') try: if (sql_operation != ""): # logger.info(sql_operation) cursor.execute(sql_operation) # 执行sql语句 except Exception: logger.error("发生异常3", Exception) db.commit() # 提交到数据库执行 cursor.execute( "update xq_account set TOTAL_BALANCE = {} where ACCOUNT_ID = '1' " .format(assets)) # 使用 fetchall 函数,将结果集(多维元组)存入 rows 里面 rows = cursor.fetchall() # 计算当前持仓盈亏 cursor.execute("SELECT * FROM xq_history where IS_HAS = '0' ") xq_history_rows = cursor.fetchall() for row in xq_history_rows: # 查询股票当前价格 self.account_config = { "cookies": self._cookies, "portfolio_code": row[3], "portfolio_market": "cn", } # logger.info("account_config:" + str(self.account_config)) data = { "code": row[3], "size": "300", "key": "47bce5c74f", "market": self.account_config["portfolio_market"], } cur = self.s.get(self.SEARCH_STOCK_URL, params=data) stockss = json.loads(cur.text) stockss = stockss["stocks"] curstock = None if len(stockss) > 0: curstock = stockss[0] # logger.info("testsssssss:" + str(curstock["current"])) # logger.info("testsssssss:" + str(row[4])) update_xq_history = """update xq_history set PROFIT_LOSS = {} where STOCK_CODE = '{}' """.format( Decimal(row[4] * curstock["current"]) - Decimal(row[4] * row[10]), row[3]) try: if (update_xq_history != ""): # logger.info(update_xq_history) cursor.execute(update_xq_history) # 执行sql语句 except Exception: logger.error("发生异常2", Exception) db.commit() # 提交到数据库执行 # 关闭数据库连接 db.close()