Skip to content

Commit caaecd5

Browse files
dericshidenggui
authored andcommitted
添加对湘财证券的支持 (shidenggui#183)
* start workingon XiangCaiZhengQuan * upate .gitignore file * enable login for XiangCaiZhengQuan * removed unused code * code cleanse * update search_stock_url for xq.json * update xueqiu * code cleanse * revert back changes to xue qiu
1 parent 656719f commit caaecd5

File tree

7 files changed

+310
-3
lines changed

7 files changed

+310
-3
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,6 @@ docs/_build/
6666
target/
6767

6868
# cache
69-
tmp/
69+
tmp/
70+
71+
secrets/

easytrader/api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .xqtrader import XueQiuTrader
1111
from .yhtrader import YHTrader
1212
from .yjbtrader import YJBTrader
13+
from .xczqtrader import XCZQTrader
1314

1415

1516
def use(broker, debug=True, **kwargs):
@@ -41,7 +42,8 @@ def use(broker, debug=True, **kwargs):
4142
if broker.lower() in ['yh_client', '银河客户端']:
4243
from .yh_clienttrader import YHClientTrader
4344
return YHClientTrader()
44-
45+
if broker.lower() in ['xczq', '湘财证券']:
46+
return XCZQTrader()
4547

4648
def follower(platform, **kwargs):
4749
"""用于生成特定的券商对象

easytrader/config/xczq.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"login_page": "https://webtrade.xcsc.com/winner/xcsc/",
3+
"login_api": "https://webtrade.xcsc.com/winner/xcsc/user/exchange.action",
4+
"verify_code_api": "https://webtrade.xcsc.com/winner/xcsc/user/extraCode.jsp",
5+
"prefix": "https://webtrade.xcsc.com/winner/xcsc/user/exchange.action",
6+
"logout_api": "https://webtrade.xcsc.com/winner/xcsc/exchange.action?function_id=20&login_type=stock",
7+
"login": {
8+
"function_id": 200,
9+
"login_type": "stock",
10+
"version": 200,
11+
"identity_type": "",
12+
"remember_me": "",
13+
"input_content": 1,
14+
"content_type": 0
15+
},
16+
"buy": {
17+
"service_type": "stock",
18+
"request_id": "buystock_302"
19+
},
20+
"sell": {
21+
"service_type": "stock",
22+
"request_id": "sellstock_302"
23+
},
24+
"position": {
25+
"request_id": "mystock_403"
26+
},
27+
"balance": {
28+
"request_id": "mystock_405"
29+
},
30+
"entrust": {
31+
"request_id": "trust_401",
32+
"sort_direction": 1,
33+
"deliver_type": "",
34+
"service_type": "stock"
35+
},
36+
"cancel_entrust": {
37+
"request_id": "chedan_304"
38+
},
39+
"current_deal": {
40+
"request_id": "bargain_402",
41+
"sort_direction": 1,
42+
"service_type": "stock"
43+
},
44+
"ipo_enable_amount": {
45+
"request_id": "buystock_300"
46+
},
47+
"exchangetype4stock": {
48+
"service_type": "stock",
49+
"function_id": "105"
50+
},
51+
"account4stock": {
52+
"service_type": "stock",
53+
"function_id": "407",
54+
"window_id": "StockMarketTrade"
55+
}
56+
}

easytrader/config/xq.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"rebalance_url": "https://xueqiu.com/cubes/rebalancing/create.json",
77
"history_url": "https://xueqiu.com/cubes/rebalancing/history.json",
88
"referer": "https://xueqiu.com/p/update?action=holdings&symbol=%s"
9-
}
9+
}

easytrader/helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ def recognize_verify_code(image_path, broker='ht'):
8383
return detect_gf_result(image_path)
8484
elif broker == 'yh':
8585
return detect_yh_result(image_path)
86+
elif broker == 'xczq':
87+
return default_verify_code_detect(image_path)
8688
elif broker == 'yh_client':
8789
return detect_yh_client_result(image_path)
8890
# 调用 tesseract 识别

easytrader/xczq.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"account": "客户号",
3+
"password": "密码"
4+
}

easytrader/xczqtrader.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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

Comments
 (0)