|
| 1 | +# coding: utf-8 |
| 2 | +from __future__ import division |
| 3 | + |
| 4 | +import json |
| 5 | +import os |
| 6 | +import random |
| 7 | +import tempfile |
| 8 | +import urllib |
| 9 | + |
| 10 | +import demjson |
| 11 | +import requests |
| 12 | +import six |
| 13 | + |
| 14 | +from . import helpers |
| 15 | +from .log import log |
| 16 | +from .webtrader import NotLoginError |
| 17 | +from .webtrader import WebTrader |
| 18 | + |
| 19 | +class XCZQTrader(WebTrader): |
| 20 | + config_path = os.path.dirname(__file__) + '/config/xczq.json' |
| 21 | + |
| 22 | + def __init__(self): |
| 23 | + super(XCZQTrader, self).__init__() |
| 24 | + self.account_config = None |
| 25 | + self.s = requests.session() |
| 26 | + self.s.mount('https://', helpers.Ssl3HttpAdapter()) |
| 27 | + |
| 28 | + def login(self, throw=False): |
| 29 | + headers = { |
| 30 | + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko' |
| 31 | + } |
| 32 | + self.s.headers.update(headers) |
| 33 | + |
| 34 | + self.s.get(self.config['login_page']) |
| 35 | + |
| 36 | + verify_code = self.handle_recognize_code() |
| 37 | + if not verify_code: |
| 38 | + return False |
| 39 | + login_status, result = self.post_login_data(verify_code) |
| 40 | + if login_status is False and throw: |
| 41 | + raise NotLoginError(result) |
| 42 | + return login_status |
| 43 | + |
| 44 | + def handle_recognize_code(self): |
| 45 | + """获取并识别返回的验证码 |
| 46 | + :return:失败返回 False 成功返回 验证码""" |
| 47 | + # 获取验证码 |
| 48 | + verify_code_response = self.s.get(self.config['verify_code_api'], params=dict(randomStamp=random.random())) |
| 49 | + # 保存验证码 |
| 50 | + image_path = os.path.join(tempfile.gettempdir(), 'vcode_%d' % os.getpid()) |
| 51 | + with open(image_path, 'wb') as f: |
| 52 | + f.write(verify_code_response.content) |
| 53 | + |
| 54 | + verify_code = helpers.recognize_verify_code(image_path, 'xczq') |
| 55 | + # verify_code = helpers.input_verify_code_manual(image_path) |
| 56 | + log.debug('verify code detect result: %s' % verify_code) |
| 57 | + os.remove(image_path) |
| 58 | + |
| 59 | + ht_verify_code_length = 4 |
| 60 | + if len(verify_code) != ht_verify_code_length: |
| 61 | + return False |
| 62 | + return verify_code |
| 63 | + |
| 64 | + def post_login_data(self, verify_code): |
| 65 | + if six.PY2: |
| 66 | + password = urllib.unquote(self.account_config['password']) |
| 67 | + else: |
| 68 | + password = urllib.parse.unquote(self.account_config['password']) |
| 69 | + login_params = dict( |
| 70 | + self.config['login'], |
| 71 | + mac_addr=helpers.get_mac(), |
| 72 | + account_content=self.account_config['account'], |
| 73 | + password=password, |
| 74 | + validateCode=verify_code |
| 75 | + ) |
| 76 | + login_response = self.s.post(self.config['login_api'], params=login_params) |
| 77 | + log.debug('login response: %s' % login_response.text) |
| 78 | + |
| 79 | + if login_response.text.find('正常运行') != -1: |
| 80 | + # v = login_response.headers |
| 81 | + # self.sessionid = v['Set-Cookie'][11:43] |
| 82 | + return True, None |
| 83 | + return False, login_response.text |
| 84 | + |
| 85 | + def cancel_entrust(self, entrust_no, stock_code): |
| 86 | + """撤单 |
| 87 | + :param entrust_no: 委托单号 |
| 88 | + :param stock_code: 股票代码""" |
| 89 | + cancel_params = dict( |
| 90 | + self.config['cancel_entrust'], |
| 91 | + entrust_no=entrust_no, |
| 92 | + stock_code=stock_code |
| 93 | + ) |
| 94 | + return self.do(cancel_params) |
| 95 | + |
| 96 | + @property |
| 97 | + def current_deal(self): |
| 98 | + return self.get_current_deal() |
| 99 | + |
| 100 | + def get_current_deal(self): |
| 101 | + """获取当日成交列表""" |
| 102 | + """ |
| 103 | + [{'business_amount': '成交数量', |
| 104 | + 'business_price': '成交价格', |
| 105 | + 'entrust_amount': '委托数量', |
| 106 | + 'entrust_bs': '买卖方向', |
| 107 | + 'stock_account': '证券帐号', |
| 108 | + 'fund_account': '资金帐号', |
| 109 | + 'position_str': '定位串', |
| 110 | + 'business_status': '成交状态', |
| 111 | + 'date': '发生日期', |
| 112 | + 'business_type': '成交类别', |
| 113 | + 'business_time': '成交时间', |
| 114 | + 'stock_code': '证券代码', |
| 115 | + 'stock_name': '证券名称'}] |
| 116 | + """ |
| 117 | + return self.do(self.config['current_deal']) |
| 118 | + |
| 119 | + # TODO: 实现买入卖出的各种委托类型 |
| 120 | + def buy(self, stock_code, price, amount=0, volume=0, entrust_prop=0): |
| 121 | + """买入卖出股票 |
| 122 | + :param stock_code: 股票代码 |
| 123 | + :param price: 卖出价格 |
| 124 | + :param amount: 卖出股数 |
| 125 | + :param volume: 卖出总金额 由 volume / price 取整, 若指定 price 则此参数无效 |
| 126 | + :param entrust_prop: 委托类型,暂未实现,默认为限价委托 |
| 127 | + """ |
| 128 | + params = dict( |
| 129 | + self.config['buy'], |
| 130 | + entrust_bs=1, # 买入1 卖出2 |
| 131 | + entrust_amount=amount if amount else volume // price // 100 * 100 |
| 132 | + ) |
| 133 | + return self.__trade(stock_code, price, entrust_prop=entrust_prop, other=params) |
| 134 | + |
| 135 | + def sell(self, stock_code, price, amount=0, volume=0, entrust_prop=0): |
| 136 | + """卖出股票 |
| 137 | + :param stock_code: 股票代码 |
| 138 | + :param price: 卖出价格 |
| 139 | + :param amount: 卖出股数 |
| 140 | + :param volume: 卖出总金额 由 volume / price 取整, 若指定 amount 则此参数无效 |
| 141 | + :param entrust_prop: 委托类型,暂未实现,默认为限价委托 |
| 142 | + """ |
| 143 | + params = dict( |
| 144 | + self.config['sell'], |
| 145 | + entrust_bs=2, # 买入1 卖出2 |
| 146 | + entrust_amount=amount if amount else volume // price |
| 147 | + ) |
| 148 | + return self.__trade(stock_code, price, entrust_prop=entrust_prop, other=params) |
| 149 | + |
| 150 | + def get_ipo_limit(self, stock_code): |
| 151 | + """ |
| 152 | + 查询新股申购额度申购上限 |
| 153 | + :param stock_code: 申购代码!!! |
| 154 | + :return: high_amount(最高申购股数) enable_amount(申购额度) last_price(发行价) |
| 155 | + """ |
| 156 | + need_info = self.__get_trade_need_info(stock_code) |
| 157 | + params = dict( |
| 158 | + self.config['ipo_enable_amount'], |
| 159 | + CSRF_Token='undefined', |
| 160 | + timestamp=random.random(), |
| 161 | + stock_account=need_info['stock_account'], # '沪深帐号' |
| 162 | + exchange_type=need_info['exchange_type'], # '沪市1 深市2' |
| 163 | + entrust_prop=0, |
| 164 | + stock_code=stock_code |
| 165 | + ) |
| 166 | + data = self.do(params) |
| 167 | + if 'error_no' in data.keys() and data['error_no'] != "0": |
| 168 | + log.debug('查询错误: %s' % (data['error_info'])) |
| 169 | + return None |
| 170 | + return dict(high_amount=float(data['high_amount']), enable_amount=data['enable_amount'], |
| 171 | + last_price=float(data['last_price'])) |
| 172 | + |
| 173 | + def __trade(self, stock_code, price, entrust_prop, other): |
| 174 | + # 检查是否已经掉线 |
| 175 | + if not self.heart_thread.is_alive(): |
| 176 | + check_data = self.get_balance() |
| 177 | + if type(check_data) == dict: |
| 178 | + return check_data |
| 179 | + need_info = self.__get_trade_need_info(stock_code) |
| 180 | + return self.do(dict( |
| 181 | + other, |
| 182 | + stock_account=need_info['stock_account'], # '沪深帐号' |
| 183 | + exchange_type=need_info['exchange_type'], # '沪市1 深市2' |
| 184 | + entrust_prop=entrust_prop, # 委托方式 |
| 185 | + stock_code='{:0>6}'.format(stock_code), # 股票代码, 右对齐宽为6左侧填充0 |
| 186 | + elig_riskmatch_flag=1, # 用户风险等级 |
| 187 | + entrust_price=price, |
| 188 | + )) |
| 189 | + |
| 190 | + def __get_trade_need_info(self, stock_code): |
| 191 | + """获取股票对应的证券市场和帐号""" |
| 192 | + # 获取股票对应的证券市场 |
| 193 | + sh_exchange_type = 1 |
| 194 | + sz_exchange_type = 2 |
| 195 | + exchange_type = sh_exchange_type if helpers.get_stock_type(stock_code) == 'sh' else sz_exchange_type |
| 196 | + # 获取股票对应的证券帐号 |
| 197 | + if not hasattr(self, 'exchange_stock_account'): |
| 198 | + self.exchange_stock_account = dict() |
| 199 | + if exchange_type not in self.exchange_stock_account: |
| 200 | + stock_account_index = 0 |
| 201 | + response_data = self.do(dict( |
| 202 | + self.config['account4stock'], |
| 203 | + exchange_type=exchange_type, |
| 204 | + stock_code=stock_code |
| 205 | + ))[stock_account_index] |
| 206 | + self.exchange_stock_account[exchange_type] = response_data['stock_account'] |
| 207 | + return dict( |
| 208 | + exchange_type=exchange_type, |
| 209 | + stock_account=self.exchange_stock_account[exchange_type] |
| 210 | + ) |
| 211 | + |
| 212 | + def create_basic_params(self): |
| 213 | + basic_params = dict( |
| 214 | + timestamp=random.random(), |
| 215 | + ) |
| 216 | + return basic_params |
| 217 | + |
| 218 | + def request(self, params): |
| 219 | + r = self.s.get(self.trade_prefix, params=params) |
| 220 | + return r.text |
| 221 | + |
| 222 | + def format_response_data(self, data): |
| 223 | + # 获取 returnJSON |
| 224 | + return_json = json.loads(data)['returnJson'] |
| 225 | + raw_json_data = demjson.decode(return_json) |
| 226 | + fun_data = raw_json_data['Func%s' % raw_json_data['function_id']] |
| 227 | + header_index = 1 |
| 228 | + remove_header_data = fun_data[header_index:] |
| 229 | + return self.format_response_data_type(remove_header_data) |
| 230 | + |
| 231 | + def fix_error_data(self, data): |
| 232 | + error_index = 0 |
| 233 | + return data[error_index] if type(data) == list and data[error_index].get('error_no') is not None else data |
| 234 | + |
| 235 | + def check_login_status(self, return_data): |
| 236 | + if hasattr(return_data, 'get') and return_data.get('error_no') == '-1': |
| 237 | + raise NotLoginError |
| 238 | + |
| 239 | + def check_account_live(self, response): |
| 240 | + if hasattr(response, 'get') and response.get('error_no') == '-1': |
| 241 | + self.heart_active = False |
0 commit comments