def _trade_api(self, **kwargs): # 确保已经正确登录了融资融券账号 self._ensure_margin_flags() url = 'https://trade.gf.com.cn/entry' resq = self.client.post(url, params=kwargs) if len(resq.text) == 0: self.client.reset() resq = self.client.post(url, params=kwargs) data = resq.json() logger.debug('_trade_api() return: %s' % data) trade_status = data.pop('success', False) if trade_status == False: logger.error(data) error_info = data.get('error_info', data) raise TraderAPIError(error_info) df = pd.DataFrame(data['data']) df.rename(columns=RENAME_DICT, inplace=True) if 'symbol' in df.columns: df['symbol'] = df['symbol'].apply(code_to_symbols) # 去字段的并集,提高效率 cols = list(set(FLOAT_COLUMNS).intersection(set(df.columns))) for col in cols: df[col] = pd.to_numeric(df[col], errors='ignore') return df
def portfolio(self): logger.debug('call mystock_403 and mystock_405') p = self._worker.apply_async(self._trade_api, kwds={'request_id': 'mystock_403'}) b = self._worker.apply_async(self._trade_api, kwds={'request_id': 'mystock_405'}) p = p.get().copy() p.set_index('symbol', inplace=True) p = p[[ 'symbol_name', 'current_amount', 'enable_amount', 'lasttrade', 'market_value' ]] b = b.get().copy() money_type = b['money_type'].iloc[0] current_amount = b['enable_balance'].iloc[0] p.loc['cash', 'symbol_name'] = money_type p.loc['cash', 'current_amount'] = current_amount p.loc['cash', 'enable_amount'] = current_amount p.loc['cash', 'lasttrade'] = 1.0 p.loc['cash', 'market_value'] = current_amount p['weight'] = p['market_value'] / b['asset_balance'].iloc[0] p['weight'] = p['weight'].round(4) p = p.dropna(axis=0) return p
def pre_login(self): ''' 初始化session,以及需要的headers :return: ''' # session gfheader = { 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-Hans-CN, zh-Hans; q=0.5', 'Connection': 'Keep-Alive', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko', 'X-Requested-With': 'XMLHttpRequest' } session = requests.session() session.headers.update(gfheader) resq = session.get('https://trade.gf.com.cn/') resq.raise_for_status() logger.debug('get trade home pages sucess.') self._expire_at = 0 self._session = session return
def _ensure_margin_flags(self): '''确保已经登录了融资融券账户''' if self.client.margin_flags == False: margin_login_params = { 'classname': 'com.gf.etrade.control.RZRQUF2Control', 'method': 'ValidataLogin' } r = self.client.post(url='https://trade.gf.com.cn/entry', params=margin_login_params) data = r.json() logger.debug('ensure_margin_flags: %s' % data) trade_status = data.pop('success', False) if trade_status == False: logger.error(data) error_info = data.get('error_info', data) raise TraderAPIError(error_info) stockholders = data.get('stockholders', []) self._exchange_stock_account = {} for holders in stockholders: self._exchange_stock_account[ holders['exchange_type']] = holders['stock_account'] # 将session 设置为已经登录信用账户的状态 self.client.margin_flags = True return
def request(self, method, url, **kwargs): ''' 调用session的各类http方法 ''' logger.debug('Call params: %s' % kwargs) with self: resq = self.session.request(method=method, url=url, **kwargs) resq.raise_for_status() logger.debug('return: %s' % resq.text) self._expire_at = time.time() + _TIMEOUT return resq
def order(self, symbol, amount=0, volume=0, weight=0, portfolio=None): symbol = symbol.lower() logger.info( 'order: symbol(%s), amount(%.2f), volume(%.2f), weight(%.4f)' % (symbol, amount, volume, weight)) if amount == 0 and volume == 0 and weight == 0: return 0 if portfolio is None: portfolio = self.portfolio if amount != 0: if symbol in portfolio.index: volume = portfolio.loc[symbol, 'lasttrade'] * amount target_volume = portfolio.loc[symbol, 'market_value'] + volume target_weight = target_volume / portfolio['market_value'].sum() weight = target_weight - portfolio.loc[symbol, 'weight'] else: lasttrade = self.hq(symbol).loc[symbol, 'lasttrade'] target_volume = amount * lasttrade target_weight = target_volume / portfolio['market_value'].sum() weight = target_weight elif volume != 0: if symbol in portfolio.index: target_volume = portfolio.loc[symbol, 'market_value'] + volume target_weight = target_volume / portfolio['market_value'].sum() weight = target_weight - portfolio.loc[symbol, 'weight'] else: target_volume = volume target_weight = target_volume / portfolio['market_value'].sum() weight = target_weight else: if symbol in portfolio.index: target_weight = portfolio.loc[symbol, 'weight'] + weight else: target_weight = weight if target_weight > 1 or target_weight < 0: raise AttributeError( 'order: symbol(%s), amount(%.2f), volume(%.2f), weight(%.4f)' % (symbol, amount, volume, weight)) if weight > 0.001 or weight < -0.001: logger.debug('target_weight: %s' % round(target_weight, 4)) self._trade_api(symbol=symbol, target_percent=round(target_weight, 4), portfolio=portfolio) else: logger.info('权重变化过小,无需下单: %s' % weight) return 0
def request(self, method, url, **kwargs): with self: params = kwargs.get('params', {}) params.update({'dse_sessionId': self._dse_sessionId}) kwargs['params'] = params logger.debug('Call params: %s' % kwargs) resq = self._session.request(method=method, url=url, **kwargs) resq.raise_for_status() logger.debug('return: %s' % resq.text) self._expire_at = time.time() + TIMEOUT return resq
def keepalive(self, now=0): ''' 自动保持连接的函数 ''' if now == 0: now = time.time() logger.debug('keepalive checking. now: %s, expire_at: %s' % (now, self.expire_at)) if now + 60 > self.expire_at: self.portfolio logger.info('Reflash the expire time, expire_at timestamp is: %s' % self.expire_at) return
def login(self): self.pre_login() login_params = { "function_id": 200, "login_type": "stock", "version": 200, "identity_type": "", "remember_me": "", "input_content": 1, "content_type": 0, "loginPasswordType": "B64", "disk_serial_id": self.disk_serial_id, "cpuid": self.cpuid, "machinecode": self.machinecode, "mac_addr": self.mac_address, "account_content": self._account, "password": urllib.parse.unquote(self._password), "validateCode": self.vcode } logger.debug('login_params is: %s' % login_params) r = self._session.post( 'https://jy.yongjinbao.com.cn/winner_gj/gjzq/exchange.action', params=login_params) r.raise_for_status() logger.debug('Login respone: %s' % r.text) returnJson = r.json()['returnJson'] data = demjson.decode(returnJson) error_msg = dict() if data['msg_no'] != '0': if 'msg_info' in data.keys() and data['msg_info'] != '': error_msg['error_info'] = data['msg_info'] else: error_msg = data[data['error_grids']][1] if error_msg['error_info'].find('验证码') != -1: logger.warning('vcode error : %s' % error_msg['error_info']) raise VerifyCodeError(error_msg['error_info']) else: logger.error('login Failed :%s' % error_msg['error_info']) raise LoginFailedError(error_msg['error_info']) return
def vcode(self): r = self._session.get( 'https://jy.yongjinbao.com.cn/winner_gj/gjzq/user/extraCode.jsp', params={'randomStamp': random.random()}) r.raise_for_status() # 通过内存保存数据 img_buffer = BytesIO(r.content) img = Image.open(img_buffer) code = pytesseract.image_to_string(img) img.close() img_buffer.close() if self.code_rule.findall(code) == []: raise VerifyCodeError('Wrong verify code: %s' % code) else: logger.debug('Verify Code is: %s' % code) return code
def post_login(self): if self.margin_flags == True: margin_login_params = { 'classname': 'com.gf.etrade.control.RZRQUF2Control', 'method': 'ValidataLogin', 'dse_sessionId': self._dse_sessionId } r = self._session.post(url='https://trade.gf.com.cn/entry', params=margin_login_params) data = r.json() logger.debug('ensure_margin_flags: %s' % data) trade_status = data.pop('success', False) if trade_status == False: logger.error(data) error_info = data.get('error_info', data) raise TraderAPIError(error_info)
def portfolio(self): url = 'https://xueqiu.com/p/' + self.portfolio_code r = self.client.get(url) r.raise_for_status() # 查找持仓的字符串段 html = r.text pos_start = html.find('SNB.cubeInfo = ') + len('SNB.cubeInfo = ') pos_end = html.find('SNB.cubePieData') json_data = to_text(html[pos_start:pos_end - 2]) logger.debug(json_data) p_info = json.decode(json_data, encoding='utf-8') # 修复雪球持仓错误 positions = p_info['last_success_rebalancing']['holdings'] logger.debug(p_info) df = pd.DataFrame(positions) df.rename(columns=_RENAME_DICT, inplace=True) df['symbol'] = df['symbol'].str.lower() df = df.set_index('symbol') hq = self.hq(df.index) df['lasttrade'] = hq['lasttrade'] df.loc['cash', 'symbol_name'] = '人民币' df.loc['cash', 'current_amount'] = p_info['view_rebalancing']['cash_value'] df.loc['cash', 'lasttrade'] = 1.0 df['current_amount'] = df['current_amount'] * _BASE_MULTIPE df['enable_amount'] = df['current_amount'] df['market_value'] = df['current_amount'] * df['lasttrade'] net_value = df['market_value'].sum() df['weight'] = (df['market_value'] / net_value).round(4) return df[[ 'symbol_name', 'current_amount', 'enable_amount', 'lasttrade', 'market_value', 'weight' ]]
def _trade_api(self, **kwargs): ''' 底层交易接口 ''' logger.debug('call params: %s' % kwargs) r = self.client.get( url= 'https://jy.yongjinbao.com.cn/winner_gj/gjzq/stock/exchange.action', params=kwargs) logger.debug('return: %s' % r.text) # 解析返回的结果数据 returnJson = r.json()['returnJson'] if returnJson is None: return None data = demjson.decode(returnJson) if data['msg_no'] != '0': error_msg = data[data['error_grids']][1] logger.error('error no: %s,error info: %s' % (error_msg.get( 'error_no', ''), error_msg.get('error_info', ''))) raise TraderAPIError(error_msg.get('error_info', '')) data = data['Func%s' % data['function_id']] df = pd.DataFrame(data[1:]) # 替换表头的命名 df.rename(columns=RENAME_DICT, inplace=True) # 生成symbol if 'symbol' in df.columns: df['symbol'] = df['symbol'].apply(code_to_symbols) # FLOAT_COLUMNS和 df.columns取交集,以减少调用时间 cols = list(set(FLOAT_COLUMNS).intersection(set(df.columns))) for col in cols: df[col] = pd.to_numeric(df[col], errors='ignore') return df
def login(self): # 无论是否登录,都重新创建一个session对象 # self.pre_login() login_params = { "authtype": 2, "disknum": self.disknum, "loginType": 2, "origin": "web", 'mac': self.mac_address, 'username': self._account, 'password': self._password, 'tmp_yzm': self.vcode } resq = self._session.post(url='https://trade.gf.com.cn/login', params=login_params) resq.raise_for_status() logger.debug('login resq: %s' % resq.json()) data = resq.json() if data['success'] == True: v = resq.headers self._dse_sessionId = v['Set-Cookie'][-32:] # 等待服务器准备就绪 time.sleep(0.1) logger.info('Login success: %s' % self._dse_sessionId) return elif data['success'] == False and 'error_info' not in data.keys(): logger.warning('当前系统无法登陆') raise TraderAPIError(data) elif data['error_info'].find('验证码') != -1: self.dse_sessionId = None logger.warning('VerifyCode Error: %s' % data) raise VerifyCodeError(data['error_info']) else: self.dse_sessionId = None logger.warning('API Login Error: %s' % data) raise TraderAPIError(data['error_info'])
def vcode(self): # 获取校验码 r = self._session.get('https://trade.gf.com.cn/yzm.jpgx') r.raise_for_status() # 通过内存保存图片,进行识别 img_buffer = BytesIO(r.content) img = Image.open(img_buffer) if hasattr(img, "width"): width, height = img.width, img.height else: width, height = img.size for x in range(width): for y in range(height): if img.getpixel((x, y)) < (100, 100, 100): img.putpixel((x, y), (256, 256, 256)) gray = img.convert('L') two = gray.point(lambda x: 0 if 68 < x < 90 else 256) min_res = two.filter(ImageFilter.MinFilter) med_res = min_res.filter(ImageFilter.MedianFilter) for _ in range(1): med_res = med_res.filter(ImageFilter.MedianFilter) # 通过tesseract-ocr的工具进行校验码识别 vcode = pytesseract.image_to_string(med_res) img.close() img_buffer.close() vcode = vcode.replace(' ', '') if self.code_rule.findall(vcode) != []: logger.debug('vcode is: %s' % vcode) return vcode else: raise VerifyCodeError('verify code error: %s' % vcode)
def __new__(cls, account, password): ''' 创建loginSession类时,如果同一券商的账号密码都一样时,只创建一次 ''' logger.debug('LoginType: %s, account: %s, password: %s' % (type(cls), account, password)) # cls, account, password 是用MD5进行创建关键字 m = hashlib.md5() m.update(str(type(cls)).encode('utf-8')) m.update(account.encode('utf-8')) m.update(password.encode('utf-8')) keyword = m.hexdigest() obj = cls._objects.get(keyword, None) logger.debug('keyword: %s, obj: %s' % (keyword, obj)) if obj is None: # 如果没有缓存过此对象,就创建,并进行缓存 logger.debug('缓存内没有对象,重新创建一个对象') obj = super(LoginSession, cls).__new__(cls) cls._objects[keyword] = obj return obj
def orderlist(self): order_col = [ 'order_no', 'symbol', 'symbol_name', 'trade_side', 'order_price', 'order_amount', 'business_price', 'business_amount', 'order_status', 'order_time' ] p = {"cube_symbol": self.portfolio_code, 'count': 5, 'page': 1} resq = self.client.get( url='https://xueqiu.com/cubes/rebalancing/history.json', params=p, headers={}) logger.debug(resq.text) resq.raise_for_status() data = resq.json()['list'] logger.debug('get_entrust raw data: %s' % data) order_list = [] for xq_orders in data: status = xq_orders['status'] # 调仓状态 if status == 'pending': status = "已报" elif status == 'canceled': status = "已撤" elif status == 'failed': status = "废单" elif status == 'success': status = "已成" else: raise TraderAPIError('Unkown order status. %s' % status) for order in xq_orders['rebalancing_histories']: prev_target_volume = order['prev_target_volume'] if order[ 'prev_target_volume'] is not None else 0.0 target_volume = order['target_volume'] if order[ 'target_volume'] else 0.0 # 实际上应该是这里通常说得amount volume = abs(target_volume - prev_target_volume) * _BASE_MULTIPE price = order['price'] if order['price'] else 0.0 if volume > 0: order_list.append({ 'order_no': order['id'], 'symbol': order['stock_symbol'].lower(), 'symbol_name': order['stock_name'], 'trade_side': "买入" if target_volume > prev_target_volume else "卖出", 'order_status': status, 'order_time': time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(order['updated_at'] / 1000)), 'business_amount': volume * price if status == '已成' else 0, 'business_price': price if status == '已成' else 0, 'order_amount': volume * price, 'order_price': price, }) df = pd.DataFrame(order_list, columns=order_col) df = df.set_index('order_no') df[['order_price', 'order_amount', 'business_amount', 'business_price']] = \ df[['order_price', 'order_amount', 'business_amount', 'business_price']].round(3) return df
def cancel(self, order_no=0): portfolio = self.portfolio comment = '撤单\n 来自vxTrader' stock_infos = self._worker.imap( self._get_stock_info, portfolio.loc[portfolio.index != 'cash'].index) holdings = [] for stock in stock_infos: holding = { "code": stock['code'], "flag": stock['flag'], "type": stock['type'], "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'], # 注意此处雪球接受的是持仓百分比,例如:30.33 "weight": round(portfolio.loc[stock['code'].lower(), 'weight'] * 100, 2), "proactive": True, "price": str(stock['current']) } holdings.append(holding) params = { # 注意此处雪球接受的是持仓百分比,例如:30.33 'cash': round(portfolio.loc['cash', 'weight'] * 100, 2), 'holdings': str(json.encode(holdings)), 'cube_symbol': self.portfolio_code, 'segment': 1, 'comment': comment } self.client.headers.update( referer='https://xueqiu.com/p/update?action=holdings&symbol=%s' % self.portfolio_code) logger.debug(self.client.session.headers) try: r = self.client.post( url='https://xueqiu.com/cubes/rebalancing/create.json', params=params) r.raise_for_status() except Exception as err: logger.warning('order failed: %s' % err) raise TraderAPIError(str(err)) logger.debug('order success:%s %s' % (holdings, r.json())) return r.json()['id']
def _trade_api(self, symbol, target_percent, portfolio=None, comment=None): # 目标的持仓比例应该大于0 if target_percent < 0: raise TraderAPIError('wrong target_percent: %s' % target_percent) # symbol 转换成小写 symbol = symbol.lower() if comment is None: comment = '将股票( %s )的仓位调整至 %.2f%% . \n 来自vxTrader' % ( symbol, target_percent * 100) # 如果没有穿portfolio,就更新一下 if portfolio is None: portfolio = self.portfolio portfolio.loc[symbol, 'weight'] = target_percent market_weight = portfolio.loc[portfolio.index != 'cash', 'weight'].sum() if market_weight > 1.0: raise TraderAPIError( 'wrong target_percent: %s, market_weight is: %s' % (target_percent, market_weight)) portfolio.loc['cash', 'weight'] = 1.0 - market_weight stock_infos = self._worker.imap( self._get_stock_info, portfolio.loc[portfolio.index != 'cash'].index) holdings = [] for stock in stock_infos: proactive = (stock['code'].lower() == symbol) holding = { "code": stock['code'], "flag": stock['flag'], "type": stock['type'], "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'], # 注意此处雪球接受的是持仓百分比,例如:30.33 "weight": round(portfolio.loc[stock['code'].lower(), 'weight'] * 100, 2), "proactive": proactive, "price": str(stock['current']) } holdings.append(holding) params = { # 注意此处雪球接受的是持仓百分比,例如:30.33 'cash': round(portfolio.loc['cash', 'weight'] * 100, 2), 'holdings': str(json.encode(holdings)), 'cube_symbol': self.portfolio_code, 'segment': 1, 'comment': comment } self.client.headers.update( referer='https://xueqiu.com/p/update?action=holdings&symbol=%s' % self.portfolio_code) logger.debug(self.client.session.headers) try: r = self.client.post( url='https://xueqiu.com/cubes/rebalancing/create.json', params=params) r.raise_for_status() except Exception as err: logger.warning('order failed: %s' % err) raise TraderAPIError(str(err)) logger.debug('order success:%s %s' % (holdings, r.json())) return r.json()['id']
def order(self, symbol, amount=0, volume=0, wait=10): ''' 按数量下单 :return: order_no, left ''' logger.debug( 'order_amount: symbol: %s, amount: %s, volume: %s, wait: %s' % (symbol, amount, volume, wait)) if (amount == 0 and volume == 0): raise AttributeError('order_amount amount and volume can not be 0') # 下单 try: hq = self.hq(symbol) logger.debug('hq: %s' % hq.loc[symbol]) price = hq.loc[symbol, 'lasttrade'] amount = amount if amount else round(volume, 2) // price // 100 * 100 if amount == 0: return 0, 0 if amount > 0 or volume > 0: price = hq.loc[symbol, 'ask'] order_no = self.buy(symbol, price, amount=amount) logger.info('buy order send,order_no: %s' % order_no) elif amount < 0 or volume < 0: price = hq.loc[symbol, 'bid'] order_no = self.sell(symbol, price, amount=-amount) logger.info('sell order send,order_no: %s' % order_no) except TraderError as err: logger.debug('Order Error: %s' % err) raise err # 每隔2秒检查一下成交状态. # 如果是已成交,则返回order_no, 0 # 如果是已报、部成, 则再等2秒钟。 # 如果是其他状态,就报警 time.sleep(5) for i in range(int((wait + 1) / 2)): logger.info('Check Order Status %s times.' % i) orderlist = self.orderlist if order_no in orderlist.index: status = orderlist.loc[order_no, 'order_status'] else: # 如果记录的订单号不在orderlist里面,则认为已经成交了。 status = '已成' if status in ('已成'): logger.info('Order Success. %s' % orderlist.loc[order_no]) return order_no, 0 elif status in ('已报', '部成', '正常'): logger.info('Order not Complete. %s' % orderlist.loc[order_no]) time.sleep(5) elif status in ('未报'): logger.info('Not Allow to Send Order. %s' % orderlist.loc[order_no]) self.cancel(order_no) return order_no, amount else: logger.error('Order Status Invaild. %s' % orderlist.loc[order_no]) raise TraderAPIError('Order Status Invaild. %s' % orderlist.loc[order_no]) # 等待了足够时间,仍未全部成交,则撤单 try: logger.warning('Cancel order: %s' % order_no) self.cancel(order_no) time.sleep(0.3) orderlist = self.orderlist status = orderlist.loc[order_no, 'order_status'] if status in ('已撤', '部撤'): orderlist['left'] = orderlist['order_amount'] - orderlist[ 'business_amount'] left = orderlist.loc[order_no, 'left'] if amount < 0: left = -left return order_no, left else: raise TraderAPIError('Order Status Invaild. %s' % orderlist.loc[order_no]) except TraderError as err: logger.warning(err) logger.warning('Order Status Invaild. %s' % orderlist.loc[order_no])