def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) # ctrl+s 保存 grid 内容为 xls 文件 self._set_foreground( grid) # setFocus buggy, instead of SetForegroundWindow grid.type_keys("^s", set_foreground=False) count = 10 while count > 0: if self._trader.is_exist_pop_dialog(): break self._trader.wait(0.2) count -= 1 temp_path = tempfile.mktemp(suffix=".csv") logger.info("temp_path-->" + temp_path) self._set_foreground(self._trader.app.top_window()) if (platform.system() == 'Windows'): tmp_file_path = temp_path else: tmp_file_path = self.normalize_path(temp_path) # alt+s保存,alt+y替换已存在的文件 self._trader.app.top_window().Edit1.set_edit_text(tmp_file_path) self._trader.wait(0.1) self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False) # Wait until file save complete otherwise pandas can not find file self._trader.wait(0.2) if self._trader.is_exist_pop_dialog(): self._trader.app.top_window().Button2.click() self._trader.wait(0.2) return self._format_grid_data(temp_path)
def record_trades(self, trade_record_path): time.sleep(0.5) trades_list = self.today_trades index_name = self._config.TRADE_RECORD_INDEX_NAME if len(trades_list) > 0: df_td = pd.DataFrame.from_dict(trades_list, orient='columns') df_td["日期"] = pd.to_datetime(date.today()) df_td.set_index(["日期", index_name], drop=True, inplace=True) else: df_td = pd.DataFrame([]) if df_td.shape[0] > 0: if trade_record_path.exists(): df_his = pd.read_csv(trade_record_path, dtype={ index_name: str, "证券代码": str }, parse_dates=["日期"]) df_his.set_index(["日期", index_name], drop=True, inplace=True) for ind in df_td.index: df_his.loc[ind, :] = df_td.loc[ind, :] df_his.to_csv(trade_record_path, header=True, index=True, float_format="%.3f") logger.info("append the trades to the trades record file.") else: df_his = df_td df_his.to_csv(trade_record_path, header=True, index=True, float_format="%.3f") logger.info("append the trades to the trades record file.")
def _get_clipboard_data(self) -> str: if Copy._need_captcha_reg: if ( self._trader.app.top_window() .window(class_name="Static", title_re="验证码") .exists(timeout=1) ): file_path = "tmp.png" count = 5 found = False while count > 0: self._trader.app.top_window().window( control_id=0x965, class_name="Static" ).capture_as_image().save( file_path ) # 保存验证码 captcha_num = captcha_recognize(file_path) # 识别验证码 logger.info("captcha result-->" + captcha_num) if len(captcha_num) == 4: self._trader.app.top_window().window( control_id=0x964, class_name="Edit" ).set_text( captcha_num ) # 模拟输入验证码 self._trader.app.top_window().set_focus() pywinauto.keyboard.SendKeys("{ENTER}") # 模拟发送enter,点击确定 try: logger.info( self._trader.app.top_window() .window(control_id=0x966, class_name="Static") .window_text() ) except Exception as ex: # 窗体消失 logger.exception(ex) found = True break count -= 1 self._trader.wait(0.1) self._trader.app.top_window().window( control_id=0x965, class_name="Static" ).click() if not found: self._trader.app.top_window().Button2.click() # 点击取消 else: Copy._need_captcha_reg = False count = 5 while count > 0: try: return pywinauto.clipboard.GetData() # pylint: disable=broad-except except Exception as e: count -= 1 logger.exception("%s, retry ......", e)
def get_all_transactions(): all_trans = [] for num in range(1, 50): try: tmp = xq_follower.custom_query_strategy_transaction( strategy='ZH974199', page=num) print(num, len(tmp)) except Exception as e: logger.info(e) continue all_trans.extend(tmp) write_txt(all_trans, 'all_trans.txt') return all_trans
def extract_transactions(self, history): if history["count"] <= 0: return [] rebalancing_index = 0 raw_transactions = history["list"][rebalancing_index][ "rebalancing_histories"] transactions = [] for transaction in raw_transactions: if transaction["price"] is None: logger.info("该笔交易无法获取价格,疑似未成交,跳过。交易详情: %s", transaction) continue transactions.append(transaction) return transactions
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 _adjust_buy_amount(self, transaction, stock_code, amount, price, assets): if amount == 0: return amount price = price * (1 + self.slippage) stock_code = stock_code[-6:] # 511880这个是货币基金,买入这个相当于无效调仓 if stock_code == "511880": return 0 # user = self._users[0] # weight是5的倍数,且diff小于3.5,则认为是无效调仓 # weight不是5的倍数 # 进行下面的判断 # 判断账户的仓位realWeight是否大于weight, # 如果realWeight >= weight,无效调仓 # 如果realWeight < weight,则weight先往上靠,得到新的5的倍数仓位adjustWeight,实际调仓为adjustWeight - realWeight weight = self.none_to_zero(transaction['weight']) prevWeight = self.none_to_zero(transaction["prev_weight_adjusted"]) startWeight = round(self.none_to_zero(transaction["prev_weight"])) diff = weight - prevWeight # 新开仓, 直接半仓买入 if startWeight == 0: amount = round(assets / 2 / price, -2) logger.info("买:指令时间:%s 股票名称 %s: 策略从 %s -> %s,改为买入半仓,数量 %s, 金额为 %s", transaction["datetime"], transaction["stock_name"], prevWeight, weight, amount, round(amount * price)) return amount return 0 # diff小于3.5,且不是调仓到5的整数倍,大概率就是微调,直接放弃 if weight % 5 == 0 and diff < 3.5: # logger.info("买:指令时间:%s 股票名称 %s: 策略调仓从 %s 调到 %s, 调仓幅度 %s, 小于3.5丢弃。", transaction["datetime"], transaction["stock_name"], prevWeight, # weight, diff) return 0 # 0 -> 1, 5 -> 6 if startWeight % 5 == 0 and weight % 5 != 0 and diff < 4.5: diff = 5 amount = round(diff / 100 * assets / price, -2) logger.info("买:指令时间:%s 股票名称 %s: 策略从 %s -> %s 改为调仓5,调整后的买入数量为:%s, 买入金额为 %s", transaction["datetime"], transaction["stock_name"], prevWeight, weight, amount, round(amount * price)) return amount if weight % 5 != 0 and startWeight % 5 != 0: logger.info("买:指令时间:%s 股票名称 %s: 策略调仓从 %s -> %s, 放弃调仓", transaction["datetime"], transaction["stock_name"], prevWeight, weight) return 0 # 0 -> 5, 11 -> 15, 11 -> 20 if weight % 5 == 0 and diff >= 4: logger.info("买:指令时间:%s 股票名称 %s: 策略从 %s -> %s, 按实际值进行调仓", transaction["datetime"], transaction["stock_name"], prevWeight, weight) return amount
def track_strategy_worker(self, strategy, name, interval=10, **kwargs): """跟踪下单worker :param strategy: 策略id :param name: 策略名字 :param interval: 轮询策略的时间间隔,单位为秒""" while True: try: transactions = self.query_strategy_transaction( strategy, **kwargs ) # pylint: disable=broad-except except Exception as e: logger.exception("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e) time.sleep(3) continue for transaction in transactions: trade_cmd = { "strategy": strategy, "strategy_name": name, "action": transaction["action"], "stock_code": transaction["stock_code"], "amount": transaction["amount"], "price": transaction["price"], "datetime": transaction["datetime"], } if self.is_cmd_expired(trade_cmd): continue if trade_cmd["amount"] != 0: logger.info( "策略 [%s] 发送指令到交易队列, 股票: %s 动作: %s 数量: %s 价格: %s 信号产生时间: %s", name, trade_cmd["stock_code"], trade_cmd["action"], trade_cmd["amount"], trade_cmd["price"], trade_cmd["datetime"], ) self.trade_queue.put(trade_cmd) self.add_cmd_to_expired_cmds(trade_cmd) try: for _ in range(interval): time.sleep(1) except KeyboardInterrupt: logger.info("程序退出") break
def record_position(self, position_record_path): time.sleep(0.5) posi_list = self.position if len(posi_list) > 0: df_td = pd.DataFrame.from_dict(posi_list) df_td["日期"] = pd.to_datetime(date.today()) df_td_copy = df_td.copy().set_index("证券代码") dict_td = df_td_copy[ self._config.POSITION_RECORD_COMPARE_ITEMS].to_dict( orient="index") df_td.set_index(["日期", "证券代码"], drop=True, inplace=True) else: df_td = pd.DataFrame([]) dict_td = {} if df_td.shape[0] > 0: if position_record_path.exists(): df_his = pd.read_csv(position_record_path, dtype={"证券代码": str}, parse_dates=["日期"]) max_dt = max(df_his["日期"]) df_his_last = df_his[df_his["日期"] == max_dt] df_his_last.set_index("证券代码", inplace=True) dict_his = df_his_last[ self._config.POSITION_RECORD_COMPARE_ITEMS].to_dict( orient="index") df_his.set_index(["日期", "证券代码"], drop=True, inplace=True) if dict_his != dict_td: for ind in df_td.index: df_his.loc[ind, :] = df_td.loc[ind, :] df_his.to_csv(position_record_path, header=True, index=True, float_format="%.3f") logger.info( "append the positions to the position record file.") else: df_his = df_td df_his.to_csv(position_record_path, header=True, index=True, float_format="%.3f") logger.info( "append the positions to the position record file.")
def login(self, user=None, password=None, **kwargs): """ 登陆接口 :param user: 用户名 :param password: 密码 :param kwargs: 其他参数 :return: """ headers = self._generate_headers() self.s.headers.update(headers) # init cookie self.s.get(self.LOGIN_PAGE) # post for login params = self.create_login_params(user, password, **kwargs) rep = self.s.post(self.LOGIN_API, data=params) self.check_login_success(rep) logger.info("登录成功")
def extract_transactions(self, history): if history["count"] <= 0: return [] rebalancing_index = 0 raw_transactions = history["list"][rebalancing_index]["rebalancing_histories"] transactions = [] for transaction in raw_transactions: if transaction["price"] is None: logger.info("该笔交易无法获取价格,疑似未成交,跳过。交易详情: %s", transaction) continue trans_date = datetime.fromtimestamp( transaction["created_at"] // 1000 ) now = datetime.now() expire = (now - trans_date).total_seconds() if expire > self._expire_seconds: continue transactions.append(transaction) return transactions
def login(self, user=None, password=None, **kwargs): """ 雪球登陆, 需要设置 cookies :param cookies: 雪球登陆需要设置 cookies, 具体见 https://smalltool.github.io/2016/08/02/cookie/ :return: """ cookies = kwargs.get("cookies") if cookies is None: raise TypeError("雪球登陆需要设置 cookies, 具体见" "https://smalltool.github.io/2016/08/02/cookie/") headers = self._generate_headers() self.s.headers.update(headers) self.s.get(self.LOGIN_PAGE) self._cookies = cookies cookie_dict = parse_cookies_str(cookies) self.s.cookies.update(cookie_dict) logger.info("xq_follower登录成功")
def _adjust_sell_amount(self, stock_code, amount): """ 根据实际持仓值计算雪球卖出股数 因为雪球的交易指令是基于持仓百分比,在取近似值的情况下可能出现不精确的问题。 导致如下情况的产生,计算出的指令为买入 1049 股,取近似值买入 1000 股。 而卖出的指令计算出为卖出 1051 股,取近似值卖出 1100 股,超过 1000 股的买入量, 导致卖出失败 :param stock_code: 证券代码 :type stock_code: str :param amount: 卖出股份数 :type amount: int :return: 考虑实际持仓之后的卖出股份数 :rtype: int """ # stock_code = stock_code[-6:] user = self._users[0] position = user.position # logger.info(position) # logger.info(stock_code) try: stock = next(s for s in position if s["stock_code"].upper() == stock_code.upper()) except StopIteration: logger.info("根据持仓调整 %s 卖出额,发现未持有股票 %s, 不做任何调整", stock_code, stock_code) return amount available_amount = stock["enable_amount"] if available_amount >= amount: return amount adjust_amount = available_amount // 100 * 100 logger.info( "股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s", stock_code, available_amount, amount, adjust_amount, ) return adjust_amount
def follow( self, users, run_id, track_interval=1, trade_cmd_expire_seconds=120, cmd_cache=True, entrust_prop="limit", send_interval=0, ): """跟踪ricequant对应的模拟交易,支持多用户多策略 :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户 :param run_id: ricequant 的模拟交易ID,支持使用 [] 指定多个模拟交易 :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) run_ids = self.warp_list(run_id) if cmd_cache: self.load_expired_cmd_cache() self.start_trader_thread(users, trade_cmd_expire_seconds, entrust_prop, send_interval) workers = [] for id_ in run_ids: strategy_name = self.extract_strategy_name(id_) strategy_worker = Thread( target=self.track_strategy_worker, args=[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 record_balance(self, balance_record_path): time.sleep(0.5) dict_td = self.balance df_td = pd.DataFrame.from_dict(dict_td, orient="index").T index_td = pd.to_datetime(date.today()) df_td.index = [index_td] df_td.loc[index_td, "记录时间"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if balance_record_path.exists(): df_his = pd.read_csv(balance_record_path, dtype={"证券代码": str}, parse_dates=["日期", "记录时间"]) df_his.set_index("日期", inplace=True) df_his.loc[index_td, :] = df_td.loc[index_td, :] else: df_his = df_td df_his.to_csv(balance_record_path, header=True, index=True, float_format="%.2f") logger.info("updated today's balance to the balance record file.")
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()
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 _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_sell_amount(self, stock_code, amount, transaction, assets): """ 根据实际持仓值计算雪球卖出股数 因为雪球的交易指令是基于持仓百分比,在取近似值的情况下可能出现不精确的问题。 导致如下情况的产生,计算出的指令为买入 1049 股,取近似值买入 1000 股。 而卖出的指令计算出为卖出 1051 股,取近似值卖出 1100 股,超过 1000 股的买入量, 导致卖出失败 :param stock_code: 证券代码 :type stock_code: str :param amount: 卖出股份数 :type amount: int :return: 考虑实际持仓之后的卖出股份数 :rtype: int """ return 0 if amount == 0: return 0 stock_code = stock_code[-6:] if stock_code == "511880": return 0 weight = self.none_to_zero(transaction['weight']) prevWeight = self.none_to_zero(transaction["prev_weight_adjusted"]) # startWeight = round(self.none_to_zero(transaction["prev_weight"])) diff = prevWeight - weight # 微小调仓,忽略 5.5 -> 5, 6 -> 5 if weight % 5 == 0 and diff < 3.5: return 0 try: user = self._users[0] position = user.position stock = next(s for s in position if s["证券代码"] == stock_code) except StopIteration: logger.info("卖:指令时间:%s 根据持仓调整 %s 卖出额,发现未持有股票 %s, 调仓无效。", transaction["datetime"], transaction["stock_name"], transaction["stock_name"]) return 0 except Exception as e: logger.info("未知错误:%s", e) return 0 available_amount = stock["可用余额"] logger.info("卖:指令时间:%s 股票:%s 策略从 %s -> %s, 清空股票", transaction["datetime"], transaction["stock_name"], transaction["prev_target_weight"], transaction["weight"]) return available_amount totalMoney = stock["市值"] # 策略调仓到0,那直接全卖 if self.none_to_zero(transaction['weight']) == 0: # 当天不可卖仓位 leftWeight = round(stock["冻结数量"] * stock["市价"] / assets, 2) logger.info("卖:指令时间:%s 股票:%s 策略从 %s -> %s, 清空股票, 账户当前股票仓位为 %s, 不可买仓位为 %s", transaction["datetime"], transaction["stock_name"], transaction["prev_target_weight"], transaction["weight"], round(totalMoney / assets * 100, 2), leftWeight) return available_amount if available_amount >= amount: currentWeight = round(totalMoney/assets * 100, 2) diff = self.none_to_zero(transaction["prev_target_weight"]) - self.none_to_zero(transaction["weight"]) logger.info("卖:指令时间:%s 股票:%s 策略从 %s -> %s, 账户当前股票仓位为 %s, 卖出仓位 %s, 剩余仓位 %s", transaction["datetime"], transaction["stock_name"], transaction["prev_target_weight"], transaction["weight"], currentWeight, diff, currentWeight - diff ) return amount // 100 * 100 adjust_amount = available_amount // 100 * 100 logger.info( "卖:指令时间:%s 股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s。", transaction["datetime"], transaction["stock_name"], available_amount, amount, adjust_amount ) return adjust_amount