From d2c81291cfea8b1f0aff0421bfc793b3639c17f5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 20 Mar 2017 20:10:25 +0800 Subject: [PATCH 001/276] setup: add missing setup.py alter --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 966976aa..4c410a7e 100644 --- a/setup.py +++ b/setup.py @@ -101,6 +101,6 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'License :: OSI Approved :: BSD License'], - packages=['easytrader', 'easytrader.config', 'easytrader.thirdlibrary'], + packages=['easytrader', 'easytrader.config'], package_data={'': ['*.jar', '*.json'], 'config': ['config/*.json'], 'thirdlibrary': ['thirdlibrary/*.jar']}, ) From 33ee0cd35f029af36ec11a007866bde628ef5307 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 20 Mar 2017 21:16:37 +0800 Subject: [PATCH 002/276] =?UTF-8?q?yh:=20=E6=B7=BB=E5=8A=A0=E9=93=B6?= =?UTF-8?q?=E6=B2=B3=20=E4=BA=94=E6=A1=A3=E5=8D=B3=E6=88=90=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E6=92=A4=E9=94=80=20=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/__init__.py | 2 +- easytrader/yhtrader.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 97fb5384..bc983ca3 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .joinquant_follower import JoinQuantFollower from .ricequant_follower import RiceQuantFollower -__version__ = '0.11.9' +__version__ = '0.11.10' __author__ = 'shidenggui' diff --git a/easytrader/yhtrader.py b/easytrader/yhtrader.py index dead616c..f427782c 100644 --- a/easytrader/yhtrader.py +++ b/easytrader/yhtrader.py @@ -279,12 +279,14 @@ def buy(self, stock_code, price, amount=0, volume=0, entrust_prop='limit'): :param price: 买入价格 :param amount: 买入股数 :param volume: 买入总金额 由 volume / price 取整, 若指定 price 则此参数无效 - :param entrust_prop: 委托类型 'limit' 限价单 , 'market' 市价单 + :param entrust_prop: 委托类型 'limit' 限价单 , 'market' 市价单, 'market_cancel' 五档即时成交剩余转限制 """ market_type = helpers.get_stock_type(stock_code) bsflag = None if entrust_prop == 'limit': bsflag = '0B' + elif entrust_prop == 'market_cancel': + bsflag = '0d' elif market_type == 'sh': bsflag = '0q' elif market_type == 'sz': @@ -304,12 +306,14 @@ def sell(self, stock_code, price, amount=0, volume=0, entrust_prop='limit'): :param price: 卖出价格 :param amount: 卖出股数 :param volume: 卖出总金额 由 volume / price 取整, 若指定 amount 则此参数无效 - :param entrust_prop: str 委托类型 'limit' 限价单 , 'market' 市价单 + :param entrust_prop: str 委托类型 'limit' 限价单 , 'market' 市价单, 'market_cancel' 五档即时成交剩余转限制 """ market_type = helpers.get_stock_type(stock_code) bsflag = None if entrust_prop == 'limit': bsflag = '0S' + elif entrust_prop == 'market_cancel': + bsflag = '0i' elif market_type == 'sh': bsflag = '0r' elif market_type == 'sz': From ae1772f0dbf12322b6056f4a09b1656a5cde3596 Mon Sep 17 00:00:00 2001 From: Reno <396708962@qq.com> Date: Tue, 18 Apr 2017 15:26:06 +0800 Subject: [PATCH 003/276] Update doc for INSTALL4Windows (#195) --- docs/other/INSTALL4Windows.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/other/INSTALL4Windows.md b/docs/other/INSTALL4Windows.md index e73c9421..d15e344f 100644 --- a/docs/other/INSTALL4Windows.md +++ b/docs/other/INSTALL4Windows.md @@ -15,7 +15,10 @@ * unzip pytesser_v0.0.1.zip * Put tesseract.exe tessdata\ under C:\Users\xxxx\AppData\Local\Programs\Python\Python35\Scripts\ Config json file like gf.json -* Open https://trade.gf.com.cn +* Open https://trade.gf.com.cn in IE +* Input Account and Password (select 'save account') +* Login (for creating 'userId' Cookie) +* Logout * Input Account and Password * F12 | Console * Copy userId from Cookie and paste into gf.json | username From f31bb68eecf924d2f934d894728ce0660f46b9be Mon Sep 17 00:00:00 2001 From: yin000shi Date: Thu, 20 Apr 2017 00:02:57 +0800 Subject: [PATCH 004/276] =?UTF-8?q?xq=5Ffollower.py=E4=B8=AD=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3login=20api=E7=9A=84url=EF=BC=8C=E5=B9=B6=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E8=B0=83=E4=BB=93=E4=B8=BA0=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E5=88=A4=E6=96=AD=20(#197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/xq_follower.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index 5cdb47db..fc40082e 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -14,7 +14,7 @@ class XueQiuFollower(BaseFollower): LOGIN_PAGE = 'https://www.xueqiu.com' - LOGIN_API = 'https://xueqiu.com/user/login' + LOGIN_API = 'https://xueqiu.com/snowman/login' TRANSACTION_API = 'https://xueqiu.com/cubes/rebalancing/history.json' PORTFOLIO_URL = 'https://xueqiu.com/p/' WEB_REFERER = 'https://www.xueqiu.com' @@ -114,9 +114,15 @@ def create_query_transaction_params(self, strategy): return params # noinspection PyMethodOverriding + def none_to_zero(self,data): + if data==None: + return 0 + else: + return data + def project_transactions(self, transactions, assets): for t in transactions: - weight_diff = t['weight'] - t['prev_weight'] + weight_diff = self.none_to_zero(t['weight']) - self.none_to_zero(t['prev_weight']) initial_amount = abs(weight_diff) / 100 * assets / t['price'] t['amount'] = int(round(initial_amount, -2)) From 034c241c754c10cea9431bdfddbcb97d73bdc1a2 Mon Sep 17 00:00:00 2001 From: xxx975 <905266420@qq.com> Date: Fri, 5 May 2017 10:14:21 +0800 Subject: [PATCH 005/276] Update yhtrader.py (#200) --- easytrader/yhtrader.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/easytrader/yhtrader.py b/easytrader/yhtrader.py index dead616c..96892553 100644 --- a/easytrader/yhtrader.py +++ b/easytrader/yhtrader.py @@ -244,6 +244,32 @@ def get_current_deal(self, date=None): """ return self.do(self.config['current_deal']) + + def get_his_deal(self, bgd, edd): + """ + <905266420@qq.com> + 获取历史时间段内的全部成交列表 + e.g.: get_deal( bgd="2016-07-14", edd="2016-08-14" ) + 遇到提示“系统超时请重新登录”或者https返回状态码非200或者其他异常情况会返回False + "" + data = { + "sdate": bgd, + "edate": edd + + try: + response = self.s.post("https://www.chinastock.com.cn/trade/webtrade/stock/stock_cj_query.jsp", data=data, + cookies=self.cookie) + if response.status_code != 200: + return False + if response.text.find("重新登录") != -1: + return False + res = self.format_response_data(response.text) + return res + except Exception as e: + log.warning("撤单出错".format(e)) + return False + + def get_deal(self, date=None): """ @Contact: Emptyset <21324784@qq.com> From 8591b80ab69a6895d013b2160fa6476e548958c8 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 7 May 2017 22:24:50 +0800 Subject: [PATCH 006/276] update doc && fix bugs --- README.md | 6 ++++++ cli.py | 7 ++++--- docs/index.md | 5 +++++ docs/usage.md | 8 ++++++++ easytrader/__init__.py | 2 +- easytrader/webtrader.py | 5 ++++- easytrader/xq_follower.py | 2 -- easytrader/yhtrader.py | 22 ++++++++++++++++++++++ 8 files changed, 50 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 76c1ca84..11a165cf 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 * 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) 或者 银河开户可以加群找我 +## 公众号 + +请扫码关注“易量化”的微信公众号,不定时更新`easytrader`的最新动态及量化方面的相关文章 + +![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) + **开发环境** : `Ubuntu 16.04` / `Python 3.5` diff --git a/cli.py b/cli.py index 7e6a678e..10fd4583 100644 --- a/cli.py +++ b/cli.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import json +import better_exceptions import click import dill @@ -28,10 +29,10 @@ def main(prepare, use, do, get, params, debug): with open(ACCOUNT_OBJECT_FILE, 'rb') as f: user = dill.load(f) - if len(params) > 0: - result = getattr(user, do)(*params) - else: + if get is not None: result = getattr(user, do) + else: + result = getattr(user, do)(*params) json_result = json.dumps(result, indent=4, ensure_ascii=False, sort_keys=True) click.echo(json_result) diff --git a/docs/index.md b/docs/index.md index a3efe09e..9b2b0b5c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,6 +8,11 @@ * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 * 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) 或者 银河开户可以加群找我 +## 公众号 + +请扫码关注“易量化”的微信公众号,不定时更新`easytrader`的最新动态及量化方面的相关文章 + +![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) **开发环境** : `OSX 10.12.3` / `Python 3.5` diff --git a/docs/usage.md b/docs/usage.md index 8f2aec0b..c90d68d0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -212,6 +212,14 @@ user.sell('162411', price=0.55, amount=100) {'orderid': 'xxxxxxxx', 'ordersno': '1111'} ``` +#### 一键打新 + +##### 银河 + +```python +user.auto_ipo() +``` + #### 撤单 ##### 银河 diff --git a/easytrader/__init__.py b/easytrader/__init__.py index bc983ca3..03aeda29 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .joinquant_follower import JoinQuantFollower from .ricequant_follower import RiceQuantFollower -__version__ = '0.11.10' +__version__ = '0.11.16' __author__ = 'shidenggui' diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index 6dc659fc..9955b65a 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -6,6 +6,7 @@ from threading import Thread import six +import requests from . import helpers from .log import log @@ -109,9 +110,11 @@ def check_login(self, sleepy=30): try: response = self.heartbeat() self.check_account_live(response) + except requests.exceptions.ConnectionError: + pass except Exception as e: log.setLevel(self.log_level) - log.error('心跳线程发现账户出现错误: {}, 尝试重新登陆'.format(e)) + log.error('心跳线程发现账户出现错误: {} {}, 尝试重新登陆'.format(e.__class__, e)) self.autologin() finally: log.setLevel(self.log_level) diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index fc40082e..98370c18 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -87,8 +87,6 @@ def calculate_assets(self, strategy_url, total_assets=None, initial_assets=None) @staticmethod def extract_strategy_id(strategy_url): - if len(strategy_url) != 8: - raise ValueError('雪球组合名格式不对, 类似 ZH123456, 设置值: {}'.format(strategy_url)) return strategy_url def extract_strategy_name(self, strategy_url): diff --git a/easytrader/yhtrader.py b/easytrader/yhtrader.py index f427782c..66563785 100644 --- a/easytrader/yhtrader.py +++ b/easytrader/yhtrader.py @@ -6,6 +6,7 @@ import os import random import re +import time import requests @@ -421,6 +422,7 @@ def __trade(self, stock_code, price, entrust_prop, other): log.debug("{}".format(self.config['trade_api'])) log.debug("{}".format(trade_params)) log.debug('trade response: %s' % trade_response.text) + time.sleep(0.5) # 避免银河 '请求频繁,请稍后再试' 的错误 return trade_response.json() def __get_trade_need_info(self, stock_code): @@ -575,3 +577,23 @@ def get_ipo_limit(self, stock_code): ser = df.iloc[0] return dict(high_amount=int(ser['申购上限']), enable_amount=int(ser['账户额度']), last_price=float(ser['价格'])) + + def auto_ipo(self): + """ + 自动打新 + :return: list(dict) dict 格式为 {'申购股票': 申购返回结果} + """ + ipo_info, _ = self.get_ipo_info() + ipo_info.fillna(0, inplace=True) + + res = [] + for _, row in ipo_info.iterrows(): + if row['账户额度'] <= 0: + continue + + ipo_amount = min(row['账户额度'], row['申购上限']) + response = self.buy(row['代码'], row['价格'], ipo_amount) + res.append({ + row['名称']: response + }) + return res From 59868b9bdc5af866371a8bf157a2806166aed38c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 7 May 2017 22:37:58 +0800 Subject: [PATCH 007/276] fix bug --- easytrader/__init__.py | 2 +- easytrader/yhtrader.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 03aeda29..7301a811 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .joinquant_follower import JoinQuantFollower from .ricequant_follower import RiceQuantFollower -__version__ = '0.11.16' +__version__ = '0.11.17' __author__ = 'shidenggui' diff --git a/easytrader/yhtrader.py b/easytrader/yhtrader.py index f5127573..3ceebf16 100644 --- a/easytrader/yhtrader.py +++ b/easytrader/yhtrader.py @@ -245,18 +245,18 @@ def get_current_deal(self, date=None): """ return self.do(self.config['current_deal']) - def get_his_deal(self, bgd, edd): """ <905266420@qq.com> 获取历史时间段内的全部成交列表 e.g.: get_deal( bgd="2016-07-14", edd="2016-08-14" ) 遇到提示“系统超时请重新登录”或者https返回状态码非200或者其他异常情况会返回False - "" + """ data = { "sdate": bgd, "edate": edd - + } + try: response = self.s.post("https://www.chinastock.com.cn/trade/webtrade/stock/stock_cj_query.jsp", data=data, cookies=self.cookie) @@ -269,7 +269,6 @@ def get_his_deal(self, bgd, edd): except Exception as e: log.warning("撤单出错".format(e)) return False - def get_deal(self, date=None): """ From 8c907f0a1179f2acd80ebecf5f70cc5d65c23133 Mon Sep 17 00:00:00 2001 From: heheqiao <614400597@qq.com> Date: Wed, 31 May 2017 18:27:14 +0800 Subject: [PATCH 008/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dget=5Fentrust?= =?UTF-8?q?=E4=B8=BA=E8=BF=94=E5=9B=9E=E6=89=80=E6=9C=89=E5=A7=94=E6=89=98?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=20(#203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/config/gf.json | 7 ++++ easytrader/gftrader.py | 49 ++++++++++++++++++++++++- test_gftrader_get_entrust.py | 69 ++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 test_gftrader_get_entrust.py diff --git a/easytrader/config/gf.json b/easytrader/config/gf.json index 461fb487..1dc2ba39 100644 --- a/easytrader/config/gf.json +++ b/easytrader/config/gf.json @@ -30,6 +30,13 @@ "start": 0, "limit": 10 }, + "entrust_pos":{ + "classname": "com.gf.etrade.control.StockUF2Control", + "method": "queryDRWT", + "request_num": 100, + "query_direction": 0, + "query_mode": 0 + }, "cancel_entrust": { "classname": "com.gf.etrade.control.StockUF2Control", "method": "cancel", diff --git a/easytrader/gftrader.py b/easytrader/gftrader.py index 06a34788..06df081d 100644 --- a/easytrader/gftrader.py +++ b/easytrader/gftrader.py @@ -558,7 +558,7 @@ def exit(self): log.debug(self.do(params)) self.heart_active = False - def get_entrust(self, action_in=0): + def get_entrust_without_pos(self, action_in=0): ''' :param action_in: 当值为0,返回全部委托;当值为1时,返回可撤委托 @@ -569,3 +569,50 @@ def get_entrust(self, action_in=0): "action_in": action_in, }) return self.do(params) + + def get_entrust(self, action_in): + ''' + + :param action_in: 当值为0,返回全部委托;当值为1时,返回可撤委托 + :return: 字典形式的返回值 + ''' + data, total = self.get_value(action_in) + return {u'data': data, u'total': total, u'success': True} + + def get_entrust_with_pos(self, postion_str): + ''' + + :param position_str: 用于标记查询委托单号的起点 + :return: 字典形式的返回值 + ''' + params = self.config['entrust_pos'].copy() + params.update({ + "postion_str": postion_str, + }) + return self.do(params) + + def get_value(self, action_in): + ''' + 1.委托数量在100单以下,直接返回值 + 2.查询委托的数量等于100单,调用带position_str参数的委托查询方法 + 3.直到最后一次的查询返回值小于100单,结束循环,构造返回值 + + :param action_in: 当值为0,返回全部委托;当值为1时,返回可撤委托 + :return:(数据列表,数据总数)构成的元组 + ''' + data = [] + total = 0 + + result = self.get_entrust_without_pos(action_in) + + while True: + data += result[u'data'] + total += result[u'total'] + + if result[u'total'] < 100: + break + result = self.get_entrust_with_pos( + action_in, result[u'data'][-1]['position_str'] + ) + + return data, total diff --git a/test_gftrader_get_entrust.py b/test_gftrader_get_entrust.py new file mode 100644 index 00000000..0bb7f7f5 --- /dev/null +++ b/test_gftrader_get_entrust.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# +# Author: heheqiao(614400597@qq.com) +# +'''Test for ``trader.FixedTrader`` +''' +import mock +from nose.tools import assert_equal +from easytrader import gftrader + + +@mock.patch.object(gftrader.GFTrader, 'get_value') +def test_get_entrust(mock_get_value): + '''UnitTest of ``gftrader.GFTrader.get_entrust`` + ''' + mock_get_value.return_value = ( + [u'test', u'test'], 100 + ) + + assert_equal( + gftrader.GFTrader().get_entrust(0), + {u'data': [u'test', u'test'], u'total': 100, u'success': True} + ) + + +@mock.patch.object(gftrader.GFTrader, 'do') +def test_get_entrust_with_pos(mock_do): + '''UnitTest of ``gftrader.GFTrader.get_entrust_with_pos`` + ''' + gftrader.GFTrader().get_entrust_with_pos(u'test') + + mock_do.assert_called_with({ + u'classname': u'com.gf.etrade.control.StockUF2Control', + u'query_mode': 0, u'query_direction': 0, + u'postion_str': u'test', + u'request_num': 100, + u'method': u'queryDRWT' + }) + + +@mock.patch.object(gftrader.GFTrader, 'get_entrust_without_pos') +@mock.patch.object(gftrader.GFTrader, 'get_entrust_with_pos') +def test_get_value(mock_get_pos, mock_get): + '''UnitTest of ``gftrader.GFTrader.get_value`` + ''' + # Case1:result[u'total'] < 100 + mock_get.return_value = { + u'data': [u'test'], u'total': 1 + } + + assert_equal( + gftrader.GFTrader().get_value(0), + ([u'test'], 1) + ) + mock_get_pos.assert_not_called() + + # Case2:result[u'total'] >= 100 + mock_get.return_value = { + u'data': [{u'position_str': u'test'}], u'total': 100 + } + mock_get_pos.return_value = { + u'data': [u'test'], u'total': 50 + } + + assert_equal( + gftrader.GFTrader().get_value(0), + ([{u'position_str': u'test'}, u'test'], 150) + ) + mock_get_pos.assert_called_with(0, u'test') From 7196f9e7537e1d82f8b352c9ef0455d9fe4c1399 Mon Sep 17 00:00:00 2001 From: Conner Mo Date: Wed, 14 Jun 2017 14:10:45 +0800 Subject: [PATCH 009/276] =?UTF-8?q?=E5=A4=84=E7=90=86=E6=9C=89=E4=BA=9B?= =?UTF-8?q?=E9=9D=9E=E8=B5=84=E9=87=91=E8=B4=A6=E6=88=B7=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5=EF=BC=8C=E6=94=AF=E6=8C=81=E5=9C=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E4=B8=AD=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?orgin=E5=92=8Cinputtype=E3=80=82=20(#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/yhtrader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/easytrader/yhtrader.py b/easytrader/yhtrader.py index 3ceebf16..1c6e885d 100644 --- a/easytrader/yhtrader.py +++ b/easytrader/yhtrader.py @@ -100,6 +100,12 @@ def post_login_data(self, verify_code): trdpwd=self.account_config['trdpwd'], checkword=verify_code ) + + if self.account_config.get('orgid'): + login_params['orgid'] = self.account_config.get('orgid') + if self.account_config.get('inputtype'): + login_params['inputtype'] = self.account_config.get('inputtype') + log.debug('login params: %s' % login_params) login_response = self.s.post(self.config['login_api'], params=login_params) log.debug('login response: %s' % login_response.text) From d3e3d2239e67dbecbb54e404148daba95575a069 Mon Sep 17 00:00:00 2001 From: Conner Mo Date: Wed, 14 Jun 2017 19:10:31 +0800 Subject: [PATCH 010/276] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=9C=A8=E9=93=B6?= =?UTF-8?q?=E6=B2=B3=E8=AF=81=E5=88=B8web=E7=99=BB=E5=BD=95=E4=B8=AD?= =?UTF-8?q?=E5=8A=A0=E5=85=A5orgid=E5=92=8Cinputtype=E7=AD=89=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E5=8F=98=E9=87=8F=20(#208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 处理有些非资金账户登录的情况,支持在配置文件中添加orgin和inputtype。 * 支持在银河证券登录中加入orgid和inputtype等其他变量。 --- easytrader/yhtrader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/easytrader/yhtrader.py b/easytrader/yhtrader.py index 1c6e885d..ff72c50c 100644 --- a/easytrader/yhtrader.py +++ b/easytrader/yhtrader.py @@ -119,6 +119,7 @@ def _prepare_account(self, user, password, **kwargs): 'inputaccount': user, 'trdpwd': password } + self.account_config.update(**kwargs) def check_available_cancels(self, parsed=True): """ From 59bf910fa7e959d0aa42e51160c75c357a0685a2 Mon Sep 17 00:00:00 2001 From: shenxingbei Date: Mon, 26 Jun 2017 10:44:11 +0800 Subject: [PATCH 011/276] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8D=8E=E6=B3=B0?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=94=AF=E6=8C=81=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0balance=E6=96=B9=E6=B3=95=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 增加华泰客户端支持,添加获取账户余额的方法 * 增加华泰客户端使用方法 --- docs/usage.md | 22 +++ easytrader/api.py | 3 + easytrader/ht_clienttrader.py | 352 ++++++++++++++++++++++++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 easytrader/ht_clienttrader.py diff --git a/docs/usage.md b/docs/usage.md index c90d68d0..b5497827 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -17,6 +17,10 @@ user = easytrader.use('yh') # 银河支持 ['yh', 'YH', '银河'] ```python user = easytrader.use('yh_client') # 银河客户端支持 ['yh_client', 'YH_CLIENT', '银河客户端'] ``` +** 华泰客户端** +```python +user = easytrader.use('ht_client') # 华泰客户端支持 ['ht_client', 'HT_CLIENT', '华泰客户端'] +``` ** 广发** @@ -61,6 +65,9 @@ user = easytrader.use('xczq') # 湘财证券支持 ['xczq', '湘财证券'] 银河客户端直接使用明文的账号和密码即可 +**华泰客户端** + +华泰客户端直接使用明文账号、交易密码及通讯密码 # 登录帐号 @@ -72,6 +79,10 @@ user = easytrader.use('xczq') # 湘财证券支持 ['xczq', '湘财证券'] user.prepare(user='用户名', password='银河,广发web端需要券商加密后的密码, 雪球、银河客户端为明文密码') ``` +``` +user.prepare(user='用户名', password='华泰交易密码',commpasswd='华泰通讯密码') +``` + **注:**雪球额外有个 account 参数,见上文介绍 ** 使用配置文件** @@ -103,6 +114,17 @@ user.prepare('/path/to/your/yh.json') // 或者 zq.json 或者 yh_client.json ``` +华泰客户端 + +``` +{ +  "user": "华泰用户名", +  "password": "华泰明文密码" +  "commpasswd": "华泰通讯密码" +} + +``` + 广发 ``` diff --git a/easytrader/api.py b/easytrader/api.py index d636d181..e2b63541 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -35,6 +35,9 @@ def use(broker, debug=True, **kwargs): elif broker.lower() in ['yh_client', '银河客户端']: from .yh_clienttrader import YHClientTrader return YHClientTrader() + elif broker.lower() in ['ht_client', '华泰客户端']: + from .ht_clienttrader import HTClientTrader + return HTClientTrader() elif broker.lower() in ['xczq', '湘财证券']: return XCZQTrader() diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py new file mode 100644 index 00000000..e333e210 --- /dev/null +++ b/easytrader/ht_clienttrader.py @@ -0,0 +1,352 @@ +# coding:utf8 +from __future__ import division + +import os +import subprocess +import tempfile +import time +import traceback +import win32api +import win32gui +from io import StringIO + +import pandas as pd +import pyperclip +import win32com.client +import win32con +from PIL import ImageGrab + +from . import helpers +from .log import log + + +class HTClientTrader(): + def __init__(self): + self.Title = '网上股票交易系统5.0' + + def prepare(self, config_path=None, user=None, password=None, commpasswd=None, exe_path='C:\htwt\Xiadan.exe'): + """ + 登陆银河客户端 + :param config_path: 华泰登陆配置文件,跟参数登陆方式二选一 + :param user: 华泰账号 + :param password: 华泰明文密码 + :param commpasswd: 华泰通讯密码 + :param exe_path: 华泰客户端路径 + :return: + """ + if config_path is not None: + account = helpers.file2dict(config_path) + user = account['user'] + password = account['password'] + commpasswd = account['commpasswd'] + self.login(user, password, commpasswd, exe_path) + + def login(self, user, password, commpasswd, exe_path): + if self._has_main_window(): + self._get_handles() + log.info('检测到交易客户端已启动,连接完毕') + return + if not self._has_login_window(): + if not os.path.exists(exe_path): + raise FileNotFoundError('在 {} 未找到应用程序,请用 exe_path 指定应用程序目录'.format(exe_path)) + subprocess.Popen(exe_path) + # 检测登陆窗口 + for _ in range(30): + if self._has_login_window(): + break + time.sleep(1) + else: + raise Exception('启动客户端失败,无法检测到登陆窗口') + log.info('成功检测到客户端登陆窗口') + + # 登陆 + # self._set_trade_mode() + self._set_login_name(user) + self._set_login_password(password) + self._set_login_commpassword(commpasswd) + for _ in range(10): + # self._set_login_verify_code() + self._click_login_button() + time.sleep(3) + if not self._has_login_window(): + break + # self._click_login_verify_code() + + for _ in range(60): + if self._has_main_window(): + # self._get_handles() + break + time.sleep(1) + else: + raise Exception('启动交易客户端失败') + log.info('客户端登陆成功') + + # def _set_login_verify_code(self): + # verify_code_image = self._grab_verify_code() + # image_path = tempfile.mktemp() + '.jpg' + # verify_code_image.save(image_path) + # result = helpers.recognize_verify_code(image_path, 'yh_client') + # time.sleep(0.2) + # self._input_login_verify_code(result) + # time.sleep(0.4) + + # def _set_trade_mode(self): + # input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x4f4d) + # win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) + + def _set_login_name(self, user): + time.sleep(0.5) + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F3) + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, user) + + def _set_login_password(self, password): + time.sleep(0.5) + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F4) + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, password) + + def _set_login_commpassword(self, commpasswd): + time.sleep(0.5) + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3E9) + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, commpasswd) + def _has_login_window(self): + for title in ['用户登录']: + self.login_hwnd = win32gui.FindWindow(None, title) + if self.login_hwnd != 0: + return True + return False + + # def _input_login_verify_code(self, code): + # input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56b9) + # win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, code) + + # def _click_login_verify_code(self): + # input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56ba) + # rect = win32gui.GetWindowRect(input_hwnd) + # self._mouse_click(rect[0] + 5, rect[1] + 5) + + @staticmethod + def _mouse_click(x, y): + win32api.SetCursorPos((x, y)) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) + + def _click_login_button(self): + time.sleep(1) + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3EE) + win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) + + def _has_main_window(self): + try: + self._get_handles() + except: + return False + return True + + # def _grab_verify_code(self): + # verify_code_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56ba) + # self._set_foreground_window(self.login_hwnd) + # time.sleep(1) + # rect = win32gui.GetWindowRect(verify_code_hwnd) + # return ImageGrab.grab(rect) + + def _get_handles(self): + trade_main_hwnd = win32gui.FindWindow(0, self.Title) # 交易窗口 + operate_frame_hwnd = win32gui.GetDlgItem(trade_main_hwnd, 59648) # 操作窗口框架 + operate_frame_afx_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59648) # 操作窗口框架 + hexin_hwnd = win32gui.GetDlgItem(operate_frame_afx_hwnd, 129) + scroll_hwnd = win32gui.GetDlgItem(hexin_hwnd, 200) # 左部折叠菜单控件 + tree_view_hwnd = win32gui.GetDlgItem(scroll_hwnd, 129) # 左部折叠菜单控件 + + # 获取委托窗口所有控件句柄 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F1, 0) + time.sleep(0.5) + + # 买入相关 + entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 委托窗口框架 + self.buy_stock_code_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x408) # 买入代码输入框 + self.buy_price_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x409) # 买入价格输入框 + self.buy_amount_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x40A) # 买入数量输入框 + self.buy_btn_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x3EE) # 买入确认按钮 + self.refresh_entrust_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x8016) # 刷新持仓按钮 + entrust_frame_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x417) # 持仓显示框架 + entrust_sub_frame_hwnd = win32gui.GetDlgItem(entrust_frame_hwnd, 200) # 持仓显示框架 ?? + self.position_list_hwnd = win32gui.GetDlgItem(entrust_sub_frame_hwnd, 1047) # 持仓列表 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F2, 0) + time.sleep(0.5) + + # 卖出相关 + sell_entrust_frame_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 + self.sell_stock_code_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1032) # 卖出代码输入框 + self.sell_price_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1033) # 卖出价格输入框 + self.sell_amount_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1034) # 卖出数量输入框 + self.sell_btn_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1006) # 卖出确认按钮 + + # 撤单窗口 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F3, 0) + time.sleep(0.5) + cancel_entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 撤单窗口框架 + self.cancel_stock_code_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0xD14) # 卖出代码输入框 + self.cancel_query_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0xD15) # 查询代码按钮 + self.cancel_buy_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0x7532) # 撤买 + self.cancel_sell_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0x7533) # 撤卖 + + chexin_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0x417) + chexin_sub_hwnd = win32gui.GetDlgItem(chexin_hwnd, 200) + self.entrust_list_hwnd = win32gui.GetDlgItem(chexin_sub_hwnd, 1047) # 委托列表 + + + # 资金股票 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F4, 0) + time.sleep(0.5) + capital_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 资金股票窗口框架 + self.capital_balance_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F4) # 资金余额 + self.capital_frozen_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F5) # 冻结资金 + self.capital_available_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F8) # 可用金额 + self.capital_withdrawable_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F9) # 可取金额 + self.market_value_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F6) # 股票市值 + self.total_assets_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F7) # 总资产 + self.capital_window_hwnd = capital_window_hwnd + + + def buy(self, stock_code, price, amount, **kwargs): + """ + 买入股票 + :param stock_code: 股票代码 + :param price: 买入价格 + :param amount: 买入股数 + :return: bool: 买入信号是否成功发出 + """ + amount = str(amount // 100 * 100) + price = str(price) + + try: + win32gui.SendMessage(self.buy_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入买入代码 + time.sleep(0.1) + win32gui.SendMessage(self.buy_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入买入价格 + time.sleep(0.1) + win32gui.SendMessage(self.buy_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入买入数量 + time.sleep(0.1) + win32gui.SendMessage(self.buy_btn_hwnd, win32con.BM_CLICK, None, None) # 买入确定 + time.sleep(0.2) + except: + traceback.print_exc() + return False + return True + + def sell(self, stock_code, price, amount, **kwargs): + """ + 买出股票 + :param stock_code: 股票代码 + :param price: 卖出价格 + :param amount: 卖出股数 + :return: bool 卖出操作是否成功 + """ + amount = str(amount // 100 * 100) + price = str(price) + + try: + win32gui.SendMessage(self.sell_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入卖出代码 + time.sleep(0.1) + win32gui.SendMessage(self.sell_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入卖出价格 + time.sleep(0.1) + win32gui.SendMessage(self.sell_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入卖出数量 + time.sleep(0.1) + win32gui.SendMessage(self.sell_btn_hwnd, win32con.BM_CLICK, None, None) # 卖出确定 + time.sleep(0.2) + except: + traceback.print_exc() + return False + return True + + def cancel_entrust(self, stock_code, direction): + """ + 撤单 + :param stock_code: str 股票代码 + :param direction: str 'buy' 撤买, 'sell' 撤卖 + :return: bool 撤单信号是否发出 + """ + direction = 0 if direction == 'buy' else 1 + + try: + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 + time.sleep(0.2) + win32gui.SendMessage(self.cancel_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入撤单 + win32gui.SendMessage(self.cancel_query_hwnd, win32con.BM_CLICK, None, None) # 查询代码 + time.sleep(0.2) + if direction == 0: + win32gui.SendMessage(self.cancel_buy_hwnd, win32con.BM_CLICK, None, None) # 撤买 + elif direction == 1: + win32gui.SendMessage(self.cancel_sell_hwnd, win32con.BM_CLICK, None, None) # 撤卖 + except: + traceback.print_exc() + return False + time.sleep(0.3) + return True + + @property + def position(self): + return self.get_position() + + def get_position(self): + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 + time.sleep(0.1) + self._set_foreground_window(self.position_list_hwnd) + time.sleep(0.1) + data = self._read_clipboard() + return self.project_copy_data(data) + + @property + def balance(self): + return self.get_balance() + + def get_balance(self): + self._set_foreground_window(self.capital_window_hwnd) + time.sleep(0.3) + data = {} + data['资金余额'] = win32gui.GetWindowText(self.capital_balance_hwnd) + data['冻结余额'] = win32gui.GetWindowText(self.capital_frozen_hwnd) + data['可用金额'] = win32gui.GetWindowText(self.capital_available_hwnd) + data['可取余额'] = win32gui.GetWindowText(self.capital_withdrawable_hwnd) + data['股票市值'] = win32gui.GetWindowText(self.market_value_hwnd) + data['总资产'] = win32gui.GetWindowText(self.total_assets_hwnd) + return data + + + @staticmethod + def project_copy_data(copy_data): + reader = StringIO(copy_data) + df = pd.read_csv(reader, sep='\t') + return df.to_dict('records') + + def _read_clipboard(self): + for _ in range(15): + try: + win32api.keybd_event(17, 0, 0, 0) + win32api.keybd_event(67, 0, 0, 0) + win32api.keybd_event(67, 0, win32con.KEYEVENTF_KEYUP, 0) + win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0) + time.sleep(0.2) + return pyperclip.paste() + except Exception as e: + log.error('open clipboard failed: {}, retry...'.format(e)) + time.sleep(1) + else: + raise Exception('read clipbord failed') + + @staticmethod + def _set_foreground_window(hwnd): + shell = win32com.client.Dispatch('WScript.Shell') + shell.SendKeys('%') + win32gui.SetForegroundWindow(hwnd) + + @property + def entrust(self): + return self.get_entrust() + + def get_entrust(self): + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 + time.sleep(0.2) + self._set_foreground_window(self.entrust_list_hwnd) + time.sleep(0.2) + data = self._read_clipboard() + return self.project_copy_data(data) From dea1c225f756672bfc1a163fd2da0dfc265f79b7 Mon Sep 17 00:00:00 2001 From: Conner Mo Date: Wed, 28 Jun 2017 10:45:55 +0800 Subject: [PATCH 012/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=81=94=E9=80=9A?= =?UTF-8?q?=E7=BA=BF=E8=B7=AF=E6=A3=80=E6=B5=8B=E4=B8=8D=E5=88=B0=E7=99=BB?= =?UTF-8?q?=E9=99=86=E7=AA=97=E5=8F=A3=E7=9A=84Bug=20(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 处理有些非资金账户登录的情况,支持在配置文件中添加orgin和inputtype。 * 支持在银河证券登录中加入orgid和inputtype等其他变量。 * 修复联通线路检测不到登陆窗口的Bug --- easytrader/yh_clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 077fde16..0e50e799 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -102,7 +102,7 @@ def _set_login_password(self, password): win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, password) def _has_login_window(self): - for title in [' - 北京电信', ' - 北京电信 - 北京电信']: + for title in [' - 北京电信', ' - 北京电信 - 北京电信', ' - 北京联通1']: self.login_hwnd = win32gui.FindWindow(None, title) if self.login_hwnd != 0: return True From 8c0ed111f8fa35562c5ee3f952cfd7067bded1c5 Mon Sep 17 00:00:00 2001 From: Conner Mo Date: Wed, 28 Jun 2017 06:20:32 -0500 Subject: [PATCH 013/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dpythoncom=E5=9C=A8?= =?UTF-8?q?=E5=A4=9A=E7=BA=BF=E7=A8=8B=E6=9C=AA=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E7=9A=84Bug=E3=80=82=20(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/yh_clienttrader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 0e50e799..3739f944 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -302,6 +302,8 @@ def _project_position_str(raw): @staticmethod def _set_foreground_window(hwnd): + import pythoncom + pythoncom.CoInitialize() shell = win32com.client.Dispatch('WScript.Shell') shell.SendKeys('%') win32gui.SetForegroundWindow(hwnd) From a79b6b0a3855125310297ddecce15bf7878a95eb Mon Sep 17 00:00:00 2001 From: XuYimin Date: Thu, 29 Jun 2017 23:06:40 +0800 Subject: [PATCH 014/276] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=93=B6=E6=B2=B3?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=8C=81=E4=BB=93=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E5=9C=A8=E8=AF=81=E5=88=B8=E5=90=8D=E7=A7=B0=E5=90=AB?= =?UTF-8?q?=E6=9C=89=E7=A9=BA=E6=A0=BC=E6=97=B6=E5=88=86=E5=89=B2=E9=94=99?= =?UTF-8?q?=E8=AF=AF=EF=BC=8C=E5=A6=82=EF=BC=9A=20=E4=BA=94=20=20=E7=B2=AE?= =?UTF-8?q?=20=20=E6=B6=B2=20(#216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修正银河客户端持仓字符串在证券名称含有空格时分割错误,如: 五 粮 液 * 更改分割字符串 --- easytrader/yh_clienttrader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 3739f944..f6e60c72 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -276,7 +276,7 @@ def get_position(self): @staticmethod def project_copy_data(copy_data): reader = StringIO(copy_data) - df = pd.read_csv(reader, delim_whitespace=True) + df = pd.read_csv(reader, sep = '\t') return df.to_dict('records') def _read_clipboard(self): @@ -297,7 +297,7 @@ def _read_clipboard(self): @staticmethod def _project_position_str(raw): reader = StringIO(raw) - df = pd.read_csv(reader, delim_whitespace=True) + df = pd.read_csv(reader, sep = '\t') return df @staticmethod From 28365a9fe6989524e4fe23096774fe1d97f3b2e5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 29 Jun 2017 23:09:14 +0800 Subject: [PATCH 015/276] yh_client: add balance api --- easytrader/__init__.py | 2 +- easytrader/yh_clienttrader.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 7301a811..5479db2a 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .joinquant_follower import JoinQuantFollower from .ricequant_follower import RiceQuantFollower -__version__ = '0.11.17' +__version__ = '0.11.19' __author__ = 'shidenggui' diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 077fde16..48613eb7 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -187,6 +187,20 @@ def _get_handles(self): chexin_sub_hwnd = win32gui.GetDlgItem(chexin_hwnd, 200) self.entrust_list_hwnd = win32gui.GetDlgItem(chexin_sub_hwnd, 1047) # 委托列表 + # 资金股票 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F4, 0) + time.sleep(0.5) + self.capital_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 资金股票窗口框架 + + def balance(self): + return self.get_balance() + + def get_balance(self): + self._set_foreground_window(self.capital_window_hwnd) + time.sleep(0.3) + data = self._read_clipboard() + return self.project_copy_data(data)[0] + def buy(self, stock_code, price, amount, **kwargs): """ 买入股票 From 24c66674e88ddfd85ade2dc0c9d4833050bf9369 Mon Sep 17 00:00:00 2001 From: Yu Ling Date: Mon, 10 Jul 2017 13:54:14 +0800 Subject: [PATCH 016/276] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=BD=E9=87=91?= =?UTF-8?q?=E5=85=A8=E8=83=BD=E8=A1=8C=E5=AE=A2=E6=88=B7=E7=AB=AF=20(#221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add gj_clienttrader * update gj_clienttrader with yh_clienttrader * add demo json --- easytrader/gj_clienttrader.py | 358 ++++++++++++++++++++++++++++++++++ gj_client.json | 4 + 2 files changed, 362 insertions(+) create mode 100644 easytrader/gj_clienttrader.py create mode 100644 gj_client.json diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py new file mode 100644 index 00000000..ab734260 --- /dev/null +++ b/easytrader/gj_clienttrader.py @@ -0,0 +1,358 @@ +# coding:utf8 +from __future__ import division +import sys +import os +import subprocess +import tempfile +import time +import traceback +import win32api +import win32gui +from io import StringIO + +import pandas as pd +import pyperclip +import win32com.client +import win32con +from PIL import ImageGrab + +from . import helpers +from .log import log + + +def findWindowSubwindowEqualText(tt): + hwnds = [] + def findSub(hwnd,param): + if win32gui.FindWindowEx(hwnd,0,None,tt)!=0: + param.append(hwnd) + win32gui.EnumWindows(findSub, hwnds) + return hwnds + +class GJClientTrader(): + def __init__(self): + self.LoginTitle = ['用户登录'] + self.Title = '网上股票交易系统5.0' + + def prepare(self, config_path=None, user=None, password=None, exe_path='C:\\全能行证券交易终端\\xiadan.exe'): + """ + 登陆银河客户端 + :param config_path: 银河登陆配置文件,跟参数登陆方式二选一 + :param user: 银河账号 + :param password: 银河明文密码 + :param exe_path: 银河客户端路径 + :return: + """ + if config_path is not None: + account = helpers.file2dict(config_path) + user = account['user'] + password = account['password'] + self.login(user, password, exe_path) + + def login(self, user, password, exe_path): + if self._has_main_window(): + self._get_handles() + log.info('检测到交易客户端已启动,连接完毕') + return + if not self._has_login_window(): + if not os.path.exists(exe_path): + raise FileNotFoundError('在 {} 未找到应用程序,请用 exe_path 指定应用程序目录'.format(exe_path)) + subprocess.Popen(exe_path) + # 检测登陆窗口 + for _ in range(30): + if self._has_login_window(): + break + time.sleep(1) + else: + raise Exception('启动客户端失败,无法检测到登陆窗口') + log.info('成功检测到客户端登陆窗口') + + # 登陆 + # self._set_trade_mode() + self._set_login_name(user) + self._set_login_password(password) + for _ in range(10): + self._set_login_verify_code() + self._click_login_button() + time.sleep(3) + self._check_verify_code_wrong() + if not self._has_login_window(): + log.info('no login window, login success') + break + self._click_login_verify_code() + + for _ in range(60): + if self._has_main_window(): + self._get_handles() + break + time.sleep(1) + else: + raise Exception('启动交易客户端失败') + log.info('客户端登陆成功') + + def _set_login_verify_code(self): + verify_code_image = self._grab_verify_code() + image_path = tempfile.mktemp() + '.jpg' + verify_code_image.save(image_path) + result = helpers.recognize_verify_code(image_path, 'gj_client') + time.sleep(0.2) + self._input_login_verify_code(result) + time.sleep(0.4) + + def _set_trade_mode(self): + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x4f4d) + win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) + + def _set_login_name(self, user): + time.sleep(0.5) + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F3) + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, user) + + def _set_login_password(self, password): + time.sleep(0.5) + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F4) + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, password) + + def _has_login_window(self): + for title in self.LoginTitle: + self.login_hwnd = win32gui.FindWindow(None, title) + if self.login_hwnd != 0: + return True + return False + + def _input_login_verify_code(self, code): + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3eb) + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, code) + + def _click_login_verify_code(self): + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5db) + rect = win32gui.GetWindowRect(input_hwnd) + self._mouse_click(rect[0] + 5, rect[1] + 5) + + @staticmethod + def _mouse_click(x, y): + win32api.SetCursorPos((x, y)) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) + + def _click_login_button(self): + time.sleep(1) + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3ee) + win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) + + def _check_verify_code_wrong(self): + hwnds = findWindowSubwindowEqualText("提示") + if len(hwnds)==0: + log.info('Right verify code') + elif len(hwnds)==1: + win32gui.SetForegroundWindow(hwnds[0]) + button = win32gui.FindWindowEx(hwnds[0],0,None,"确定") + win32gui.SendMessage(button, win32con.BM_CLICK, None, None) + log.info('Wrong verify code') + else: + raise Exception("_check_verify_code_wrong too many windows %d"%len(hwnds)) + + def _has_main_window(self): + try: + self._get_handles() + except: + return False + return True + + def _grab_verify_code(self): + verify_code_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5db) + self._set_foreground_window(self.login_hwnd) + time.sleep(1) + rect = win32gui.GetWindowRect(verify_code_hwnd) + return ImageGrab.grab(rect) + + def _get_handles(self): + trade_main_hwnd = win32gui.FindWindow(0, self.Title) # 交易窗口 + operate_frame_hwnd = win32gui.GetDlgItem(trade_main_hwnd, 59648) # 操作窗口框架 + operate_frame_afx_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59648) # 操作窗口框架 + hexin_hwnd = win32gui.GetDlgItem(operate_frame_afx_hwnd, 129) + scroll_hwnd = win32gui.GetDlgItem(hexin_hwnd, 200) # 左部折叠菜单控件 + tree_view_hwnd = win32gui.GetDlgItem(scroll_hwnd, 129) # 左部折叠菜单控件 + + # 获取委托窗口所有控件句柄 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F1, 0) + time.sleep(0.5) + + # 买入相关 + entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 + self.buy_stock_code_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1032) # 买入代码输入框 + self.buy_price_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1033) # 买入价格输入框 + self.buy_amount_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1034) # 买入数量输入框 + self.buy_btn_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1006) # 买入确认按钮 + self.refresh_entrust_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 32790) # 刷新持仓按钮 + entrust_frame_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1047) # 持仓显示框架 + entrust_sub_frame_hwnd = win32gui.GetDlgItem(entrust_frame_hwnd, 200) # 持仓显示框架 + self.position_list_hwnd = win32gui.GetDlgItem(entrust_sub_frame_hwnd, 1047) # 持仓列表 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F2, 0) + time.sleep(0.5) + + # 卖出相关 + sell_entrust_frame_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 + self.sell_stock_code_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1032) # 卖出代码输入框 + self.sell_price_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1033) # 卖出价格输入框 + self.sell_amount_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1034) # 卖出数量输入框 + self.sell_btn_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1006) # 卖出确认按钮 + + # 撤单窗口 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F3, 0) + time.sleep(0.5) + cancel_entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 撤单窗口框架 + self.cancel_stock_code_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3348) # 卖出代码输入框 + self.cancel_query_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3349) # 查询代码按钮 + self.cancel_buy_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30002) # 撤买 + self.cancel_sell_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30003) # 撤卖 + + chexin_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 1047) + chexin_sub_hwnd = win32gui.GetDlgItem(chexin_hwnd, 200) + self.entrust_list_hwnd = win32gui.GetDlgItem(chexin_sub_hwnd, 1047) # 委托列表 + + # 资金股票 + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F4, 0) + time.sleep(0.5) + self.capital_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 资金股票窗口框架 + + def balance(self): + return self.get_balance() + + def get_balance(self): + self._set_foreground_window(self.capital_window_hwnd) + time.sleep(0.3) + data = self._read_clipboard() + return self.project_copy_data(data)[0] + + def buy(self, stock_code, price, amount, **kwargs): + """ + 买入股票 + :param stock_code: 股票代码 + :param price: 买入价格 + :param amount: 买入股数 + :return: bool: 买入信号是否成功发出 + """ + amount = str(amount // 100 * 100) + price = str(price) + + try: + win32gui.SendMessage(self.buy_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入买入代码 + win32gui.SendMessage(self.buy_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入买入价格 + time.sleep(0.2) + win32gui.SendMessage(self.buy_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入买入数量 + time.sleep(0.2) + win32gui.SendMessage(self.buy_btn_hwnd, win32con.BM_CLICK, None, None) # 买入确定 + time.sleep(0.3) + except: + traceback.print_exc() + return False + return True + + def sell(self, stock_code, price, amount, **kwargs): + """ + 买出股票 + :param stock_code: 股票代码 + :param price: 卖出价格 + :param amount: 卖出股数 + :return: bool 卖出操作是否成功 + """ + amount = str(amount // 100 * 100) + price = str(price) + + try: + win32gui.SendMessage(self.sell_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入卖出代码 + win32gui.SendMessage(self.sell_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入卖出价格 + win32gui.SendMessage(self.sell_price_hwnd, win32con.BM_CLICK, None, None) # 输入卖出价格 + time.sleep(0.2) + win32gui.SendMessage(self.sell_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入卖出数量 + time.sleep(0.2) + win32gui.SendMessage(self.sell_btn_hwnd, win32con.BM_CLICK, None, None) # 卖出确定 + time.sleep(0.3) + except: + traceback.print_exc() + return False + return True + + def cancel_entrust(self, stock_code, direction): + """ + 撤单 + :param stock_code: str 股票代码 + :param direction: str 'buy' 撤买, 'sell' 撤卖 + :return: bool 撤单信号是否发出 + """ + direction = 0 if direction == 'buy' else 1 + + try: + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 + time.sleep(0.2) + win32gui.SendMessage(self.cancel_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入撤单 + win32gui.SendMessage(self.cancel_query_hwnd, win32con.BM_CLICK, None, None) # 查询代码 + time.sleep(0.2) + if direction == 0: + win32gui.SendMessage(self.cancel_buy_hwnd, win32con.BM_CLICK, None, None) # 撤买 + elif direction == 1: + win32gui.SendMessage(self.cancel_sell_hwnd, win32con.BM_CLICK, None, None) # 撤卖 + except: + traceback.print_exc() + return False + time.sleep(0.3) + return True + + @property + def position(self): + return self.get_position() + + def get_position(self): + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 + time.sleep(0.1) + self._set_foreground_window(self.position_list_hwnd) + time.sleep(0.1) + data = self._read_clipboard() + return self.project_copy_data(data) + + @staticmethod + def project_copy_data(copy_data): + reader = StringIO(copy_data) + df = pd.read_csv(reader, sep = '\t') + return df.to_dict('records') + + def _read_clipboard(self): + for _ in range(15): + try: + win32api.keybd_event(17, 0, 0, 0) + win32api.keybd_event(67, 0, 0, 0) + win32api.keybd_event(67, 0, win32con.KEYEVENTF_KEYUP, 0) + win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0) + time.sleep(0.2) + return pyperclip.paste() + except Exception as e: + log.error('open clipboard failed: {}, retry...'.format(e)) + time.sleep(1) + else: + raise Exception('read clipbord failed') + + @staticmethod + def _project_position_str(raw): + reader = StringIO(raw) + df = pd.read_csv(reader, sep = '\t') + return df + + @staticmethod + def _set_foreground_window(hwnd): + import pythoncom + pythoncom.CoInitialize() + shell = win32com.client.Dispatch('WScript.Shell') + shell.SendKeys('%') + win32gui.SetForegroundWindow(hwnd) + + @property + def entrust(self): + return self.get_entrust() + + def get_entrust(self): + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 + time.sleep(0.2) + self._set_foreground_window(self.entrust_list_hwnd) + time.sleep(0.2) + data = self._read_clipboard() + return self.project_copy_data(data) diff --git a/gj_client.json b/gj_client.json new file mode 100644 index 00000000..e20ebf09 --- /dev/null +++ b/gj_client.json @@ -0,0 +1,4 @@ +{ + "user": "国金用户名", + "password": "国金明文密码" +} \ No newline at end of file From 30ee5bf436e3efe5a5c56f90209ba0af7340ffb0 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 24 Aug 2017 19:59:17 +0800 Subject: [PATCH 017/276] refactor: remove no use files && code --- docs/index.md | 1 - docs/usage.md | 106 +------ easytrader/api.py | 3 - easytrader/config/yh.json | 71 ----- easytrader/helpers.py | 59 +--- easytrader/yhtrader.py | 631 -------------------------------------- test_easytrader.py | 57 +--- yh.json | 5 - 8 files changed, 6 insertions(+), 927 deletions(-) delete mode 100644 easytrader/config/yh.json delete mode 100644 easytrader/yhtrader.py delete mode 100644 yh.json diff --git a/docs/index.md b/docs/index.md index 9b2b0b5c..d3a9c9e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,6 @@ ### 支持券商 -* 银河 * 广发 * 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 * 湘财证券 diff --git a/docs/usage.md b/docs/usage.md index b5497827..ad7ab803 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,12 +6,6 @@ import easytrader **设置账户**: -**银河** - -```python -user = easytrader.use('yh') # 银河支持 ['yh', 'YH', '银河'] -``` - ** 银河客户端** ```python @@ -39,24 +33,10 @@ user = easytrader.use('xczq') # 湘财证券支持 ['xczq', '湘财证券'] 使用 `easytrader` 的广发,银河 `web` 版本时,需要抓取对应券商的加密密码 -** 银河 web 版本** - -**推荐获取方法:** - -在 IE 浏览器中打开下面这个网页, [一键获取银河加密密码](http://htmlpreview.github.io/?https://github.com/shidenggui/assets/blob/master/easytrader/get_yh_password.html), 若有弹框的话选择允许控件运行,按步骤操作就可以获得密码 - -**其他方式** - -* [银河web获取加密密码的图文教程, 其他类似](https://shimo.im/doc/kvazIHNTRvYr7iqe)(需要安装 fildder 软件) -* [如何获取配置所需信息, 也可参考此文章](https://www.jisilu.cn/question/42707) - **广发** 参考此文档 [INSTALL4Windows.md](other/INSTALL4Windows.md) -参考银河的获取密码的其他方式 - - **雪球** 雪球配置中 `username` 为邮箱, `account` 为手机, 填两者之一即可,另一项改为 `""`, 密码直接填写登录的明文密码即可,不需要抓取 `POST` 的密码 @@ -76,7 +56,7 @@ user = easytrader.use('xczq') # 湘财证券支持 ['xczq', '湘财证券'] ** 参数登录(推荐)** ``` -user.prepare(user='用户名', password='银河,广发web端需要券商加密后的密码, 雪球、银河客户端为明文密码') +user.prepare(user='用户名', password='广发web端需要券商加密后的密码, 雪球、银河客户端为明文密码') ``` ``` @@ -88,22 +68,13 @@ user.prepare(user='用户名', password='华泰交易密码',commpasswd='华泰 ** 使用配置文件** ```python -user.prepare('/path/to/your/yh.json') // 或者 zq.json 或者 yh_client.json 等配置文件路径 +user.prepare('/path/to/your/yh_client.json') // 配置文件路径 ``` **注**: 使用配置文件模式, 配置文件需要自己用编辑器编辑生成, 请勿使用记事本, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) *格式如下* -银河 - -``` -{ - "inputaccount": "客户号", - "trdpwd": "加密后的密码" -} -``` - 银河客户端 ``` @@ -322,35 +293,9 @@ user.entrust '证券名称': '华宝油气'}] ``` -#### ipo 打新 - -*银河* - -```python -user.get_ipo_info() -``` - -**return** - - -```python -(df_taoday_ipo, df_ipo_limit), 分别是当日新股申购列表信息, 申购额度。 - df_today_ipo - 代码 名称 价格 账户额度 申购下限 申购上限 证券账号 交易所 发行日期 - 0 2830 名雕股份 16.53 17500 500 xxxxx xxxxxxxx 深A 20161201 - 1 732098 森特申购 9.18 27000 1000 xxxxx xxxxxxx 沪A 20161201 - - df_ipo_limit: - 市场 证券账号 账户额度 - 0 深圳 xxxxxxx xxxxx - 1 上海 xxxxxxx xxxxx -``` - -然后使用 `user.buy` 接口按返回的价格数量买入对应新股就可以了 - #### 查询交割单 -需要注意通常券商只会返回有限天数最新的交割单,如查询2015年整年数据, 华泰只会返回年末的90天的交割单 +需要注意通常券商只会返回有限天数最新的交割单,如查询2015年整年数据 ```python user.exchangebill # 查询最近30天的交割单 @@ -380,49 +325,6 @@ user.get_exchangebill('开始日期', '截止日期') # 指定查询时间段, ``` -#### 基金申购 - -##### 银河 - -``` -user.fundpurchase(stock_code, amount): -``` - -#### 基金赎回 - -##### 银河 - -``` -user.fundredemption(stock_code, amount): -``` - -#### 基金认购 - -##### 银河 - -``` -user.fundsubscribe(stock_code, amount): -``` - - -#### 基金分拆 - -##### 银河 - -``` -user.fundsplit(stock_code, amount): -``` - -#### 基金合并 - -##### 银河 - -``` -user.fundmerge(stock_code, amount): -``` - - - #### 查询今天可以申购的新股信息 @@ -544,7 +446,7 @@ follower.follow(***, send_interval=30) # 设置下单间隔为 30 s #### 登录 ``` - python cli.py --use yh --prepare yh.json + python cli.py --use yh --prepare gf.json ``` 注: 此时会生成 `account.session` 文件保存生成的 `user` 对象 diff --git a/easytrader/api.py b/easytrader/api.py index e2b63541..38ea9a56 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -7,7 +7,6 @@ from .log import log from .xq_follower import XueQiuFollower from .xqtrader import XueQiuTrader -from .yhtrader import YHTrader from .xczqtrader import XCZQTrader @@ -26,8 +25,6 @@ def use(broker, debug=True, **kwargs): """ if not debug: log.setLevel(logging.INFO) - if broker.lower() in ['yh', '银河']: - return YHTrader(debug=debug) elif broker.lower() in ['xq', '雪球']: return XueQiuTrader(**kwargs) elif broker.lower() in ['gf', '广发']: diff --git a/easytrader/config/yh.json b/easytrader/config/yh.json deleted file mode 100644 index d794ab82..00000000 --- a/easytrader/config/yh.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "login_page": "https://www.chinastock.com.cn/trade/webtrade/login.jsp", - "login_api": "https://www.chinastock.com.cn/trade/LoginServlet?ajaxFlag=mainlogin", - "heart_beat": "https://www.chinastock.com.cn/trade/AjaxServlet?ajaxFlag=heartbeat", - "unlock": "https://www.chinastock.com.cn/trade/AjaxServlet?ajaxFlag=unlockscreen", - "trade_api": "https://www.chinastock.com.cn/trade/AjaxServlet", - "trade_info_page": "https://www.chinastock.com.cn/trade/webtrade/tradeindex.jsp", - "verify_code_api": "https://www.chinastock.com.cn/trade/webtrade/verifyCodeImage.jsp", - "prefix": "https://www.chinastock.com.cn", - "logout_api": "https://www.chinastock.com.cn/trade/webtrade/commons/keepalive.jsp?type=go", - "ipo_api": "https://www.chinastock.com.cn/trade/webtrade/stock/newStockList.jsp", - "login": { - "logintype_rzrq": 0, - "orgid": "", - "inputtype": "C", - "identifytype": 0, - "isonlytrade": 1, - "trdpwdjtws": "", - "Authplain9320": "", - "Authsign9321": "", - "certdata9322": "", - "ftype": "bsn" - }, - "position": { - "service_jsp": "/trade/webtrade/stock/stock_zjgf_query.jsp", - "service_type": 1 - }, - "balance": { - "service_jsp": "/trade/webtrade/stock/stock_zjgf_query.jsp", - "service_type": 2 - }, - "entrust": { - "service_jsp": "/trade/webtrade/stock/stock_wt_query.jsp" - }, - "buy": { - "marktype": "", - "ajaxFlag": "wt" - }, - "sell": { - "ajaxFlag": "wt" - }, - "fundpurchase": { - "ajaxFlag": "wt", - "bsflag": 3 - }, - "fundredemption": { - "ajaxFlag": "wt", - "bsflag": 4 - }, - "fundsubscribe": { - "ajaxFlag": "wt", - "bsflag": 5 - }, - "fundsplit": { - "ajaxFlag": "wt", - "bsflag": 85 - }, - "fundmerge": { - "ajaxFlag": "wt", - "bsflag": 86 - }, - "cancel_entrust": { - "ajaxFlag": "stock_cancel" - }, - "current_deal": { - "service_jsp": "/trade/webtrade/stock/stock_cj_query.jsp" - }, - "account4stock": { - "service_jsp": "/trade/webtrade/zhgl/holderQuery.jsp" - } -} diff --git a/easytrader/helpers.py b/easytrader/helpers.py index 18438c51..03f7c330 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -3,10 +3,8 @@ import datetime import json -import os import re import ssl -import sys import uuid import requests @@ -15,8 +13,6 @@ from requests.packages.urllib3.poolmanager import PoolManager from six.moves import input -from .log import log - if six.PY2: from io import open @@ -74,14 +70,8 @@ def recognize_verify_code(image_path, broker='ht'): :param broker: 券商 ['ht', 'yjb', 'gf', 'yh'] :return recognized: verify code string""" - if broker == 'ht': - return detect_ht_result(image_path) - elif broker == 'yjb': - return detect_yjb_result(image_path) - elif broker == 'gf': + if broker == 'gf': return detect_gf_result(image_path) - elif broker == 'yh': - return detect_yh_result(image_path) elif broker == 'xczq': return default_verify_code_detect(image_path) elif broker == 'yh_client': @@ -111,37 +101,6 @@ def input_verify_code_manual(image_path): return code -def detect_verify_code_by_java(image_path, broker): - jars = { - 'ht': ('getcode_jdk1.5.jar', ''), - 'yjb': ('yjb_verify_code.jar', 'guojin') - } - verify_code_tool, param = jars[broker] - # 检查 java 环境,若有则调用 jar 包处理 (感谢空中园的贡献) - # noinspection PyGlobalUndefined - if six.PY2: - if sys.platform == 'win32': - from subprocess import PIPE, Popen, STDOUT - - def getoutput(cmd, input=None, cwd=None, env=None): - pipe = Popen(cmd, shell=True, cwd=cwd, env=env, stdout=PIPE, stderr=STDOUT) - (output, err_out) = pipe.communicate(input=input) - return output.decode().rstrip('\r\n') - else: - import commands - getoutput = commands.getoutput - else: - from subprocess import getoutput - out_put = getoutput('java -version') - log.debug('java detect result: %s' % out_put) - if out_put.find('java version') != -1 or out_put.find('openjdk') != -1: - tool_path = os.path.join(os.path.dirname(__file__), 'thirdlibrary', verify_code_tool) - out_put = getoutput('java -jar "{}" {} {}'.format(tool_path, param, image_path)) - log.debug('recognize output: %s' % out_put) - verify_code_start = -4 - return out_put[verify_code_start:] - - def default_verify_code_detect(image_path): from PIL import Image img = Image.open(image_path) @@ -168,22 +127,6 @@ def detect_gf_result(image_path): return invoke_tesseract_to_recognize(med_res) -def detect_yh_result(image_path): - """封装了tesseract的中文识别,部署在阿里云上,服务端源码地址为: https://github.com/shidenggui/yh_verify_code_docker""" - api = 'http://123.56.157.162:5000/yh' - with open(image_path, 'rb') as f: - try: - rep = requests.post(api, files={ - 'image': f - }) - if rep.status_code != 200: - raise Exception('request {} error'.format(api)) - except Exception as e: - log.error('自动识别银河验证码失败: {}, 请手动输入验证码'.format(e)) - return input_verify_code_manual(image_path) - return rep.text - - def invoke_tesseract_to_recognize(img): import pytesseract try: diff --git a/easytrader/yhtrader.py b/easytrader/yhtrader.py deleted file mode 100644 index ff72c50c..00000000 --- a/easytrader/yhtrader.py +++ /dev/null @@ -1,631 +0,0 @@ -# coding: utf-8 -from __future__ import division, unicode_literals - -import math -import logging -import os -import random -import re -import time - -import requests - -from . import helpers -from .log import log -from .webtrader import WebTrader, NotLoginError - -VERIFY_CODE_POS = 0 -TRADE_MARKET = 1 -HOLDER_NAME = 0 - - -# 用于将一个list按一定步长切片,返回这个list切分后的list -def slice_list(step=None, num=None, data_list=None): - if not ((step is None) & (num is None)): - if num is not None: - step = math.ceil(len(data_list) / num) - return [data_list[i: i + step] for i in range(0, len(data_list), step)] - else: - print("step和num不能同时为空") - return False - - -class YHTrader(WebTrader): - config_path = os.path.dirname(__file__) + '/config/yh.json' - - def __init__(self, debug=True): - super(YHTrader, self).__init__(debug=debug) - self.cookie = None - self.account_config = None - self.s = None - self.exchange_stock_account = dict() - - def login(self, throw=False): - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko', - 'Referer': 'https://www.chinastock.com.cn/trade/webtrade/login.jsp' - } - if self.s is not None: - self.s.get(self.config['logout_api']) - self.s = requests.Session() - self.s.headers.update(headers) - data = self.s.get(self.config['login_page']) - - # 查找验证码 - verify_code = self.handle_recognize_code() - - if not verify_code: - return False - - login_status, result = self.post_login_data(verify_code) - if login_status is False and throw: - raise NotLoginError(result) - - accounts = self.do(self.config['account4stock']) - if accounts is False: - return False - if len(accounts) < 2: - raise Exception('无法获取沪深 A 股账户: %s' % accounts) - for account in accounts: - if account['交易市场'] == '深A' and account['股东代码'].startswith('0'): - self.exchange_stock_account['0'] = account['股东代码'][0:10] - elif account['交易市场'] == '沪A' and account['股东代码'].startswith('A'): - self.exchange_stock_account['1'] = account['股东代码'][0:10] - return login_status - - def handle_recognize_code(self): - """获取并识别返回的验证码 - :return:失败返回 False 成功返回 验证码""" - # 获取验证码 - verify_code_response = self.s.get(self.config['verify_code_api'], params=dict(updateverify=random.random())) - # 保存验证码 - image_path = os.path.join(os.getcwd(), 'vcode') - with open(image_path, 'wb') as f: - f.write(verify_code_response.content) - - verify_code = helpers.recognize_verify_code(image_path, 'yh') - log.debug('verify code detect result: %s' % verify_code) - - ht_verify_code_length = 4 - if len(verify_code) != ht_verify_code_length: - return False - return verify_code - - def post_login_data(self, verify_code): - login_params = dict( - self.config['login'], - mac=helpers.get_mac(), - clientip='', - inputaccount=self.account_config['inputaccount'], - trdpwd=self.account_config['trdpwd'], - checkword=verify_code - ) - - if self.account_config.get('orgid'): - login_params['orgid'] = self.account_config.get('orgid') - if self.account_config.get('inputtype'): - login_params['inputtype'] = self.account_config.get('inputtype') - - log.debug('login params: %s' % login_params) - login_response = self.s.post(self.config['login_api'], params=login_params) - log.debug('login response: %s' % login_response.text) - - if login_response.text.find('success') != -1: - return True, None - return False, login_response.text - - def _prepare_account(self, user, password, **kwargs): - self.account_config = { - 'inputaccount': user, - 'trdpwd': password - } - self.account_config.update(**kwargs) - - def check_available_cancels(self, parsed=True): - """ - @Contact: Emptyset <21324784@qq.com> - 检查撤单列表 - """ - try: - response = self.s.get("https://www.chinastock.com.cn/trade/webtrade/stock/StockEntrustCancel.jsp", - cookies=self.cookie) - if response.status_code != 200: - return False - html = response.text.replace("\t", "").replace("\n", "").replace("\r", "") - if html.find("请重新登录") != -1: - return False - pattern = r'(.+)' - result1 = re.findall(pattern, html)[0] - pattern = r'([\S]+)' - parsed_data = re.findall(pattern, result1) - cancel_list = slice_list(step=12, data_list=parsed_data) - except Exception as e: - return [] - result = list() - if parsed: - for item in cancel_list: - if len(item) == 12: - item_dict = { - "time": item[0], - "code": item[1], - "name": item[2], - "status": item[3], - "iotype": item[4], - "price": float(item[5]), - "volume": int(item[6]), - "entrust_num": item[7], - "trans_vol": int(item[8]), - "canceled_vol": int(item[9]), - "investor_code": item[10], - "account": item[11] - } - elif len(item) == 11: - item_dict = { - "time": item[0], - "code": item[1], - "name": item[2], - "status": item[3], - "iotype": "", - "price": float(item[4]), - "volume": int(item[5]), - "entrust_num": item[6], - "trans_vol": int(item[7]), - "canceled_vol": int(item[8]), - "investor_code": item[9], - "account": item[10] - } - else: - continue - result.append(item_dict) - return result - - def cancel_entrust(self, entrust_no, stock_code): - """撤单 - :param entrust_no: 委托单号 - :param stock_code: 股票代码""" - need_info = self.__get_trade_need_info(stock_code) - cancel_params = dict( - self.config['cancel_entrust'], - orderSno=entrust_no, - secuid=need_info['stock_account'] - ) - cancel_response = self.s.post(self.config['trade_api'], params=cancel_params) - log.debug('cancel trust: %s' % cancel_response.text) - return cancel_response.json() - - def cancel_entrusts(self, entrust_no): - """ - @Contact: Emptyset <21324784@qq.com> - 批量撤单 - @param - entrust_no: string类型 - 委托单号,用逗号隔开 - e.g:"8000,8001,8002" - @return - 返回格式是list,比如一个22个单子的批量撤单 - e.g.: - [{"success":15, "failed":0},{"success":7, "failed":0}] - """ - import time - list_entrust_no = entrust_no.split(",") - # 一次批量撤单不能超过15个 - list_entrust_no = slice_list(step=15, data_list=list_entrust_no) - result = list() - for item in list_entrust_no: - if item[-1] == "": - num = len(item) - 1 - else: - num = len(item) - cancel_data = { - "ajaxFlag": "stock_batch_cancel", - "num": num, - "orderSno": ",".join(item) - } - while True: - try: - cancel_response = self.s.post( - "https://www.chinastock.com.cn/trade/AjaxServlet", - data=cancel_data, - timeout=1, - ) - if cancel_response.status_code == 200: - cancel_response_json = cancel_response.json() - # 如果出现“系统超时请重新登录”之类的错误信息,直接返回False - if "result_type" in cancel_response_json and "result_type" == 'error': - return False - result.append(cancel_response_json) - break - else: - log.debug('{}'.format(cancel_response)) - except Exception as e: - log.debug('{}'.format(e)) - time.sleep(0.2) - return result - - @property - def current_deal(self): - return self.get_current_deal() - - def get_current_deal(self, date=None): - """ - 获取当日成交列表. - """ - return self.do(self.config['current_deal']) - - def get_his_deal(self, bgd, edd): - """ - <905266420@qq.com> - 获取历史时间段内的全部成交列表 - e.g.: get_deal( bgd="2016-07-14", edd="2016-08-14" ) - 遇到提示“系统超时请重新登录”或者https返回状态码非200或者其他异常情况会返回False - """ - data = { - "sdate": bgd, - "edate": edd - } - - try: - response = self.s.post("https://www.chinastock.com.cn/trade/webtrade/stock/stock_cj_query.jsp", data=data, - cookies=self.cookie) - if response.status_code != 200: - return False - if response.text.find("重新登录") != -1: - return False - res = self.format_response_data(response.text) - return res - except Exception as e: - log.warning("撤单出错".format(e)) - return False - - def get_deal(self, date=None): - """ - @Contact: Emptyset <21324784@qq.com> - 获取历史日成交列表 - e.g.: get_deal( "2016-07-14" ) - 如果不传递日期则取的是当天成交列表 - 返回值格式与get_current_deal相同 - 遇到提示“系统超时请重新登录”或者https返回状态码非200或者其他异常情况会返回False - """ - if date is None: - data = {} - else: - data = { - "sdate": date, - "edate": date - } - try: - response = self.s.post("https://www.chinastock.com.cn/trade/webtrade/stock/stock_cj_query.jsp", data=data, - cookies=self.cookie) - if response.status_code != 200: - return False - if response.text.find("重新登录") != -1: - return False - res = self.format_response_data(response.text) - return res - except Exception as e: - log.warning("撤单出错".format(e)) - return False - - def buy(self, stock_code, price, amount=0, volume=0, entrust_prop='limit'): - """买入股票 - :param stock_code: 股票代码 - :param price: 买入价格 - :param amount: 买入股数 - :param volume: 买入总金额 由 volume / price 取整, 若指定 price 则此参数无效 - :param entrust_prop: 委托类型 'limit' 限价单 , 'market' 市价单, 'market_cancel' 五档即时成交剩余转限制 - """ - market_type = helpers.get_stock_type(stock_code) - bsflag = None - if entrust_prop == 'limit': - bsflag = '0B' - elif entrust_prop == 'market_cancel': - bsflag = '0d' - elif market_type == 'sh': - bsflag = '0q' - elif market_type == 'sz': - bsflag = '0a' - assert bsflag is not None - - params = dict( - self.config['buy'], - bsflag=bsflag, - qty=int(amount) if amount else volume // price // 100 * 100 - ) - return self.__trade(stock_code, price, entrust_prop=entrust_prop, other=params) - - def sell(self, stock_code, price, amount=0, volume=0, entrust_prop='limit'): - """卖出股票 - :param stock_code: 股票代码 - :param price: 卖出价格 - :param amount: 卖出股数 - :param volume: 卖出总金额 由 volume / price 取整, 若指定 amount 则此参数无效 - :param entrust_prop: str 委托类型 'limit' 限价单 , 'market' 市价单, 'market_cancel' 五档即时成交剩余转限制 - """ - market_type = helpers.get_stock_type(stock_code) - bsflag = None - if entrust_prop == 'limit': - bsflag = '0S' - elif entrust_prop == 'market_cancel': - bsflag = '0i' - elif market_type == 'sh': - bsflag = '0r' - elif market_type == 'sz': - bsflag = '0f' - assert bsflag is not None - - params = dict( - self.config['sell'], - bsflag=bsflag, - qty=amount if amount else volume // price - ) - return self.__trade(stock_code, price, entrust_prop=entrust_prop, other=params) - - def fundpurchase(self, stock_code, amount=0): - """基金申购 - :param stock_code: 基金代码 - :param amount: 申购份额 - """ - params = dict( - self.config['fundpurchase'], - price=1, # 价格默认为1 - qty=amount - ) - return self.__tradefund(stock_code, other=params) - - def fundredemption(self, stock_code, amount=0): - """基金赎回 - :param stock_code: 基金代码 - :param amount: 赎回份额 - """ - params = dict( - self.config['fundredemption'], - price=1, # 价格默认为1 - qty=amount - ) - return self.__tradefund(stock_code, other=params) - - def fundsubscribe(self, stock_code, amount=0): - """基金认购 - :param stock_code: 基金代码 - :param amount: 认购份额 - """ - params = dict( - self.config['fundsubscribe'], - price=1, # 价格默认为1 - qty=amount - ) - return self.__tradefund(stock_code, other=params) - - def fundsplit(self, stock_code, amount=0): - """基金分拆 - :param stock_code: 母份额基金代码 - :param amount: 分拆份额 - """ - params = dict( - self.config['fundsplit'], - qty=amount - ) - return self.__tradefund(stock_code, other=params) - - def fundmerge(self, stock_code, amount=0): - """基金合并 - :param stock_code: 母份额基金代码 - :param amount: 合并份额 - """ - params = dict( - self.config['fundmerge'], - qty=amount - ) - return self.__tradefund(stock_code, other=params) - - def __tradefund(self, stock_code, other): - # 检查是否已经掉线 - if not self.heart_thread.is_alive(): - check_data = self.get_balance() - if type(check_data) == dict: - return check_data - need_info = self.__get_trade_need_info(stock_code) - trade_params = dict( - other, - stockCode=stock_code, - market=need_info['exchange_type'], - secuid=need_info['stock_account'] - ) - - trade_response = self.s.post(self.config['trade_api'], params=trade_params) - log.debug('trade response: %s' % trade_response.text) - return trade_response.json() - - def __trade(self, stock_code, price, entrust_prop, other): - # 检查是否已经掉线 - if not self.heart_thread.is_alive(): - check_data = self.get_balance() - if type(check_data) == dict: - return check_data - need_info = self.__get_trade_need_info(stock_code) - trade_params = dict( - other, - stockCode=stock_code[-6:], - price=price, - market=need_info['exchange_type'], - secuid=need_info['stock_account'] - ) - trade_response = self.s.post(self.config['trade_api'], params=trade_params) - log.debug("{}".format(self.config['trade_api'])) - log.debug("{}".format(trade_params)) - log.debug('trade response: %s' % trade_response.text) - time.sleep(0.5) # 避免银河 '请求频繁,请稍后再试' 的错误 - return trade_response.json() - - def __get_trade_need_info(self, stock_code): - """获取股票对应的证券市场和帐号""" - sh_exchange_type = '1' - sz_exchange_type = '0' - exchange_type = sh_exchange_type if helpers.get_stock_type(stock_code) == 'sh' else sz_exchange_type - return dict( - exchange_type=exchange_type, - stock_account=self.exchange_stock_account[exchange_type] - ) - - def create_basic_params(self): - basic_params = dict( - CSRF_Token='undefined', - timestamp=random.random(), - ) - return basic_params - - def request(self, params): - url = self.trade_prefix + params['service_jsp'] - r = self.s.get(url, cookies=self.cookie) - if r.status_code != 200: - return False - if r.text.find('系统超时请重新登录') != -1: - return False - if params['service_jsp'] == '/trade/webtrade/stock/stock_zjgf_query.jsp': - if params['service_type'] == 2: - rptext = r.text[0:r.text.find('操作')] - return rptext - else: - rbtext = r.text[r.text.find('操作'):] - rbtext += 'yhposition' - return rbtext - else: - return r.text - - def format_response_data(self, data): - if not data: - return False - # 需要对于银河持仓情况特殊处理 - if data.find('yhposition') != -1: - search_result_name = re.findall(r'(.*)', data) - search_result_content = [] - search_result_content_tmp = re.findall(r'(.*)', data) - for item in search_result_content_tmp: - s = item[-1] if type(item) is not str else item - k = re.findall(">(.*)<", s) - if len(k) > 0: - s = k[-1] - search_result_content.append(s) - else: - # 获取原始data的html源码并且解析得到一个可读json格式 - search_result_name = re.findall(r'(.*)', data) - search_result_content = re.findall(r'([^~]*?)', data) - search_result_content = list(map(lambda x: x.replace(' ', '').replace(';', ''), search_result_content)) - - col_len = len(search_result_name) - if col_len == 0 or len(search_result_content) % col_len != 0: - if len(search_result_content) == 0: - return list() - raise Exception("Get Data Error: col_num: {}, Data: {}".format(col_len, search_result_content)) - else: - row_len = len(search_result_content) // col_len - res = list() - for row in range(row_len): - item = dict() - for col in range(col_len): - col_name = search_result_name[col] - item[col_name] = search_result_content[row * col_len + col] - res.append(item) - - return self.format_response_data_type(res) - - def check_account_live(self, response): - if hasattr(response, 'get'): - if response.get('error_no') == '-1' or response.get('result_type') == 'error': - self.heart_active = False - raise NotLoginError(response.get('result_msg')) - - def heartbeat(self): - heartbeat_params = dict( - ftype='bsn' - ) - res = self.s.post(self.config['heart_beat'], params=heartbeat_params) - - def unlockscreen(self): - unlock_params = dict( - password=self.account_config['trdpwd'], - mainAccount=self.account_config['inputaccount'], - ftype='bsn' - ) - log.debug('unlock params: %s' % unlock_params) - unlock_resp = self.s.post(self.config['unlock'], params=unlock_params) - log.debug('unlock resp: %s' % unlock_resp.text) - - def get_ipo_info(self): - """ - 查询新股申购信息 - :return: (df_taoday_ipo, df_ipo_limit), 分别是当日新股申购列表信息, 申购额度。 - df_today_ipo - 代码 名称 价格 账户额度 申购下限 申购上限 证券账号 交易所 发行日期 - 0 2830 名雕股份 16.53 17500 500 xxxxx xxxxxxxx 深A 20161201 - 1 732098 森特申购 9.18 27000 1000 xxxxx xxxxxxx 沪A 20161201 - - df_ipo_limit: - 市场 证券账号 账户额度 - 0 深圳 xxxxxxx xxxxx - 1 上海 xxxxxxx xxxxx - - """ - import pandas as pd - from bs4 import BeautifulSoup - - ipo_response = self.s.get( - self.config['ipo_api'], - params=dict(), - headers={ - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "zh-CN", - "Connection": "Keep-Alive", - "Host": "www.chinastock.com.cn", - "Referer": "https://www.chinastock.com.cn/trade/webtrade/login.jsp", - "User-Agent": "Mozilla/4.0(compatible;MSIE,7.0;Windows NT 10.0; WOW64;Trident / 7.0;.NET4.0C;.NET4.0E;.NET CLR2.0.50727;.NET CLR 3.0.30729;.NET CLR 3.5.30729;InfoPath.3)" - }) - if ipo_response.status_code != 200: - return None, None - html = ipo_response.content - soup = BeautifulSoup(html, 'lxml') - tables = soup.findAll('table', attrs={'class': 'fee'}) - df_ipo_limit = pd.read_html(str(tables[0]), flavor='lxml', header=0, encoding='utf-8')[0] - df_today_ipo = pd.read_html(str(tables[1]), flavor='lxml', header=0, encoding='utf-8')[0] - df_today_ipo[['代码']] = df_today_ipo[['代码']].applymap(lambda x: '{:0>6}'.format(x)) - return df_today_ipo, df_ipo_limit - - def get_ipo_limit(self, stock_code): - """ - 查询当日某只新股申购额度、申购上限、价格。 - 仅为了兼容佣金宝同名方法。 不需要兼容,最好使用get_ipo_info()[0] - :param stock_code: 申购代码!!! - :return: high_amount(最高申购股数) enable_amount(申购额度) last_price(发行价) - - """ - (df1, df2) = self.get_ipo_info() - if df1 is None: - log.debug('查询错误: %s') - return None - df = df1[df1['代码'] == stock_code] - if len(df) == 0: - return dict() - ser = df.iloc[0] - return dict(high_amount=int(ser['申购上限']), enable_amount=int(ser['账户额度']), - last_price=float(ser['价格'])) - - def auto_ipo(self): - """ - 自动打新 - :return: list(dict) dict 格式为 {'申购股票': 申购返回结果} - """ - ipo_info, _ = self.get_ipo_info() - ipo_info.fillna(0, inplace=True) - - res = [] - for _, row in ipo_info.iterrows(): - if row['账户额度'] <= 0: - continue - - ipo_amount = min(row['账户额度'], row['申购上限']) - response = self.buy(row['代码'], row['价格'], ipo_amount) - res.append({ - row['名称']: response - }) - return res diff --git a/test_easytrader.py b/test_easytrader.py index 3625c911..99b15650 100644 --- a/test_easytrader.py +++ b/test_easytrader.py @@ -1,9 +1,8 @@ # coding: utf-8 import unittest -from unittest import mock from datetime import datetime -import time +from unittest import mock import easytrader from easytrader import JoinQuantFollower, RiceQuantFollower @@ -12,7 +11,6 @@ class TestEasytrader(unittest.TestCase): - def test_helpers(self): result = helpers.get_stock_type('162411') self.assertEqual(result, 'sz') @@ -23,56 +21,6 @@ def test_helpers(self): result = helpers.get_stock_type('sz162411') self.assertEqual(result, 'sz') - def test_format_response_data_type(self): - user = easytrader.use('ht') - - test_data = [{ - 'current_amount': '187.00', - 'current_balance': '200.03', - 'stock_code': '000001' - }] - result = user.format_response_data_type(test_data) - - self.assertIs(type(result[0]['current_amount']), int) - self.assertIs(type(result[0]['current_balance']), float) - self.assertIs(type(result[0]['stock_code']), str) - - test_data = [{'position_str': '', - 'date': '', - 'fund_account': '', - 'stock_account': '', - 'stock_code': '', - 'entrust_bs': '', - 'business_price': '', - 'business_amount': '', - 'business_time': '', - 'stock_name': '', - 'business_status': '', - 'business_type': ''}] - result = user.format_response_data_type(test_data) - - def test_ht_fix_error_data(self): - user = easytrader.use('ht') - test_data = { - 'cssweb_code': 'error', - 'cssweb_type': 'GET_STOCK_POSITON' - } - - return_data = user.fix_error_data(test_data) - self.assertEqual(test_data, return_data) - - test_data = [{ - 'stock_code': '162411', - 'entrust_bs': '2'}, - {'no_use_index': 'hello'}] - - normal_return_data = [{ - 'stock_code': '162411', - 'entrust_bs': '2'}] - - return_data = user.fix_error_data(test_data) - self.assertEqual(return_data, normal_return_data) - def test_helpers_grep_comma(self): test_data = '123' normal_data = '123' @@ -120,7 +68,6 @@ def test_gf_check_account_live(self): class TestXueQiuTrader(unittest.TestCase): - def test_set_initial_assets(self): # default set to 1e6 xq_user = easytrader.use('xq') @@ -141,7 +88,6 @@ def test_set_initial_assets(self): class TestJoinQuantFollower(unittest.TestCase): - def test_extract_strategy_id(self): cases = [('https://www.joinquant.com/algorithm/live/index?backtestId=aaaabbbbcccc', 'aaaabbbbcccc')] @@ -184,7 +130,6 @@ def test_project_transactions(self): class TestFollower(unittest.TestCase): - def test_is_number(self): cases = [('1', True), ('--', False)] diff --git a/yh.json b/yh.json deleted file mode 100644 index 57b56167..00000000 --- a/yh.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "inputaccount": "客户号", - "trdpwd": "加密后的密码" -} - From c2d05c3b8cdc30b12dbbd9c509b76eb635d93542 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 12:13:50 +0800 Subject: [PATCH 018/276] refactor: yh_client use pywinauto --- easytrader/__init__.py | 4 +- easytrader/config/client.py | 41 +++ easytrader/exceptions.py | 4 + easytrader/yh_clienttrader.py | 543 ++++++++++++++++------------------ requirements.txt | 1 + test_easytrader.py | 168 ++--------- 6 files changed, 333 insertions(+), 428 deletions(-) create mode 100644 easytrader/config/client.py create mode 100644 easytrader/exceptions.py diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 5479db2a..9afa322a 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,10 +1,10 @@ # coding: utf-8 from .api import * from .webtrader import WebTrader -from .yhtrader import YHTrader from .gftrader import GFTrader from .joinquant_follower import JoinQuantFollower from .ricequant_follower import RiceQuantFollower +from . import exceptions -__version__ = '0.11.19' +__version__ = '0.12.0' __author__ = 'shidenggui' diff --git a/easytrader/config/client.py b/easytrader/config/client.py new file mode 100644 index 00000000..f29fb7bb --- /dev/null +++ b/easytrader/config/client.py @@ -0,0 +1,41 @@ +# coding:utf8 + +def create(broker): + if broker == 'yh': + return YH + raise NotImplemented + + +class YH: + TITLE = '网上股票交易系统5.0' + + TRADE_SECURITY_CONTROL_ID = 1032 + TRADE_PRICE_CONTROL_ID = 1033 + TRADE_AMOUNT_CONTROL_ID = 1034 + + TRADE_SUBMIT_CONTROL_ID = 1006 + + COMMON_GRID_CONTROL_ID = 1047 + BALANCE_GRID_CONTROL_ID = 1047 + + POP_DIALOD_TITLE_CONTROL_ID = 1365 + + GRID_DTYPE = { + '操作日期': str, + '委托编号': str, + '申请编号': str, + '合同编号': str, + '证券代码': str, + '股东代码': str, + '资金帐号': str, + '资金帐户': str, + '发生日期': str + } + + CANCEL_ENTRUST_ENTRUST_FIELD = '合同编号' + CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50 + CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30 + CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16 + + AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 + AUTO_IPO_BUTTON_CONTROL_ID = 1006 diff --git a/easytrader/exceptions.py b/easytrader/exceptions.py new file mode 100644 index 00000000..be29946c --- /dev/null +++ b/easytrader/exceptions.py @@ -0,0 +1,4 @@ +# coding:utf8 + +class TradeError(IOError): + pass diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 62467469..f526c02b 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -1,30 +1,31 @@ # coding:utf8 from __future__ import division +import functools +import io import os -import subprocess +import re import tempfile import time -import traceback -import win32api -import win32gui -from io import StringIO +import easyutils import pandas as pd -import pyperclip -import win32com.client -import win32con -from PIL import ImageGrab +import pywinauto +import pywinauto.clipboard +from . import exceptions from . import helpers +from .config import client from .log import log class YHClientTrader(): + BROKER = 'yh' + def __init__(self): - self.Title = '网上股票交易系统5.0' + self._config = client.create(self.BROKER) - def prepare(self, config_path=None, user=None, password=None, exe_path='C:\中国银河证券双子星3.2\Binarystar.exe'): + def prepare(self, config_path=None, user=None, password=None, exe_path=r'C:\中国银河证券双子星3.2\Binarystar.exe'): """ 登陆银河客户端 :param config_path: 银河登陆配置文件,跟参数登陆方式二选一 @@ -33,6 +34,7 @@ def prepare(self, config_path=None, user=None, password=None, exe_path='C:\中 :param exe_path: 银河客户端路径 :return: """ + if config_path is not None: account = helpers.file2dict(config_path) user = account['user'] @@ -40,296 +42,257 @@ def prepare(self, config_path=None, user=None, password=None, exe_path='C:\中 self.login(user, password, exe_path) def login(self, user, password, exe_path): - if self._has_main_window(): - self._get_handles() - log.info('检测到交易客户端已启动,连接完毕') - return - if not self._has_login_window(): - if not os.path.exists(exe_path): - raise FileNotFoundError('在 {} 未找到应用程序,请用 exe_path 指定应用程序目录'.format(exe_path)) - subprocess.Popen(exe_path) - # 检测登陆窗口 - for _ in range(30): - if self._has_login_window(): - break - time.sleep(1) - else: - raise Exception('启动客户端失败,无法检测到登陆窗口') - log.info('成功检测到客户端登陆窗口') - - # 登陆 - self._set_trade_mode() - self._set_login_name(user) - self._set_login_password(password) - for _ in range(10): - self._set_login_verify_code() - self._click_login_button() - time.sleep(3) - if not self._has_login_window(): - break - self._click_login_verify_code() - - for _ in range(60): - if self._has_main_window(): - self._get_handles() - break - time.sleep(1) - else: - raise Exception('启动交易客户端失败') - log.info('客户端登陆成功') - - def _set_login_verify_code(self): - verify_code_image = self._grab_verify_code() - image_path = tempfile.mktemp() + '.jpg' - verify_code_image.save(image_path) - result = helpers.recognize_verify_code(image_path, 'yh_client') - time.sleep(0.2) - self._input_login_verify_code(result) - time.sleep(0.4) - - def _set_trade_mode(self): - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x4f4d) - win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) - - def _set_login_name(self, user): - time.sleep(0.5) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5523) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, user) - - def _set_login_password(self, password): - time.sleep(0.5) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5534) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, password) - - def _has_login_window(self): - for title in [' - 北京电信', ' - 北京电信 - 北京电信', ' - 北京联通1']: - self.login_hwnd = win32gui.FindWindow(None, title) - if self.login_hwnd != 0: - return True - return False - - def _input_login_verify_code(self, code): - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56b9) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, code) - - def _click_login_verify_code(self): - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56ba) - rect = win32gui.GetWindowRect(input_hwnd) - self._mouse_click(rect[0] + 5, rect[1] + 5) - - @staticmethod - def _mouse_click(x, y): - win32api.SetCursorPos((x, y)) - win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) - win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) - - def _click_login_button(self): - time.sleep(1) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x1) - win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) - - def _has_main_window(self): try: - self._get_handles() - except: - return False - return True - - def _grab_verify_code(self): - verify_code_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56ba) - self._set_foreground_window(self.login_hwnd) - time.sleep(1) - rect = win32gui.GetWindowRect(verify_code_hwnd) - return ImageGrab.grab(rect) - - def _get_handles(self): - trade_main_hwnd = win32gui.FindWindow(0, self.Title) # 交易窗口 - operate_frame_hwnd = win32gui.GetDlgItem(trade_main_hwnd, 59648) # 操作窗口框架 - operate_frame_afx_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59648) # 操作窗口框架 - hexin_hwnd = win32gui.GetDlgItem(operate_frame_afx_hwnd, 129) - scroll_hwnd = win32gui.GetDlgItem(hexin_hwnd, 200) # 左部折叠菜单控件 - tree_view_hwnd = win32gui.GetDlgItem(scroll_hwnd, 129) # 左部折叠菜单控件 - - # 获取委托窗口所有控件句柄 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F1, 0) - time.sleep(0.5) - - # 买入相关 - entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 - self.buy_stock_code_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1032) # 买入代码输入框 - self.buy_price_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1033) # 买入价格输入框 - self.buy_amount_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1034) # 买入数量输入框 - self.buy_btn_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1006) # 买入确认按钮 - self.refresh_entrust_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 32790) # 刷新持仓按钮 - entrust_frame_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1047) # 持仓显示框架 - entrust_sub_frame_hwnd = win32gui.GetDlgItem(entrust_frame_hwnd, 200) # 持仓显示框架 - self.position_list_hwnd = win32gui.GetDlgItem(entrust_sub_frame_hwnd, 1047) # 持仓列表 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F2, 0) - time.sleep(0.5) - - # 卖出相关 - sell_entrust_frame_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 - self.sell_stock_code_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1032) # 卖出代码输入框 - self.sell_price_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1033) # 卖出价格输入框 - self.sell_amount_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1034) # 卖出数量输入框 - self.sell_btn_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1006) # 卖出确认按钮 - - # 撤单窗口 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F3, 0) - time.sleep(0.5) - cancel_entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 撤单窗口框架 - self.cancel_stock_code_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3348) # 卖出代码输入框 - self.cancel_query_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3349) # 查询代码按钮 - self.cancel_buy_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30002) # 撤买 - self.cancel_sell_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30003) # 撤卖 - - chexin_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 1047) - chexin_sub_hwnd = win32gui.GetDlgItem(chexin_hwnd, 200) - self.entrust_list_hwnd = win32gui.GetDlgItem(chexin_sub_hwnd, 1047) # 委托列表 - - # 资金股票 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F4, 0) - time.sleep(0.5) - self.capital_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 资金股票窗口框架 + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) + except RuntimeError: + self._app = pywinauto.Application().start(exe_path) + self._wait(1) - def balance(self): - return self.get_balance() + self._app.top_window().Edit1.type_keys(user) + self._app.top_window().Edit2.type_keys(password) - def get_balance(self): - self._set_foreground_window(self.capital_window_hwnd) - time.sleep(0.3) - data = self._read_clipboard() - return self.project_copy_data(data)[0] + while self._app.is_process_running(): + self._app.top_window().Edit3.type_keys( + self._handle_verify_code() + ) - def buy(self, stock_code, price, amount, **kwargs): - """ - 买入股票 - :param stock_code: 股票代码 - :param price: 买入价格 - :param amount: 买入股数 - :return: bool: 买入信号是否成功发出 - """ - amount = str(amount // 100 * 100) - price = str(price) + self._app.top_window()['登录'].click() + self._wait(2) - try: - win32gui.SendMessage(self.buy_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入买入代码 - win32gui.SendMessage(self.buy_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入买入价格 - time.sleep(0.2) - win32gui.SendMessage(self.buy_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入买入数量 - time.sleep(0.2) - win32gui.SendMessage(self.buy_btn_hwnd, win32con.BM_CLICK, None, None) # 买入确定 - time.sleep(0.3) - except: - traceback.print_exc() - return False - return True - - def sell(self, stock_code, price, amount, **kwargs): - """ - 买出股票 - :param stock_code: 股票代码 - :param price: 卖出价格 - :param amount: 卖出股数 - :return: bool 卖出操作是否成功 - """ - amount = str(amount // 100 * 100) - price = str(price) + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) + self._close_prompt_windows() + self._main = self._app.top_window() - try: - win32gui.SendMessage(self.sell_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入卖出代码 - win32gui.SendMessage(self.sell_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入卖出价格 - win32gui.SendMessage(self.sell_price_hwnd, win32con.BM_CLICK, None, None) # 输入卖出价格 - time.sleep(0.2) - win32gui.SendMessage(self.sell_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入卖出数量 - time.sleep(0.2) - win32gui.SendMessage(self.sell_btn_hwnd, win32con.BM_CLICK, None, None) # 卖出确定 - time.sleep(0.3) - except: - traceback.print_exc() - return False - return True - - def cancel_entrust(self, stock_code, direction): - """ - 撤单 - :param stock_code: str 股票代码 - :param direction: str 'buy' 撤买, 'sell' 撤卖 - :return: bool 撤单信号是否发出 - """ - direction = 0 if direction == 'buy' else 1 + def _run_exe_path(self, exe_path): + return os.path.join( + os.path.dirname(exe_path), 'xiadan.exe' + ) - try: - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.2) - win32gui.SendMessage(self.cancel_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入撤单 - win32gui.SendMessage(self.cancel_query_hwnd, win32con.BM_CLICK, None, None) # 查询代码 - time.sleep(0.2) - if direction == 0: - win32gui.SendMessage(self.cancel_buy_hwnd, win32con.BM_CLICK, None, None) # 撤买 - elif direction == 1: - win32gui.SendMessage(self.cancel_sell_hwnd, win32con.BM_CLICK, None, None) # 撤卖 - except: - traceback.print_exc() - return False - time.sleep(0.3) - return True + def _wait(self, seconds): + time.sleep(seconds) + + def exit(self): + self._app.kill() + + def _handle_verify_code(self): + control = self._app.top_window().window(control_id=22202) + control.click() + control.draw_outline() + + file_path = tempfile.mktemp() + control.capture_as_image().save(file_path, 'jpeg') + vcode = helpers.recognize_verify_code(file_path, 'yh_client') + return ''.join(re.findall('\d+', vcode)) + + def _close_prompt_windows(self): + self._wait(1) + for w in self._app.windows(class_name='#32770'): + if w.window_text() != self._config.TITLE: + w.close() + + @property + def balance(self): + self._switch_left_menus(['查询[F4]', '资金股票']) + + return self._get_grid_data(self._config.BALANCE_GRID_CONTROL_ID) @property def position(self): - return self.get_position() - - def get_position(self): - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.1) - self._set_foreground_window(self.position_list_hwnd) - time.sleep(0.1) - data = self._read_clipboard() - return self.project_copy_data(data) - - @staticmethod - def project_copy_data(copy_data): - reader = StringIO(copy_data) - df = pd.read_csv(reader, sep = '\t') - return df.to_dict('records') + self._switch_left_menus(['查询[F4]', '资金股票']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + + @property + def today_entrusts(self): + self._switch_left_menus(['查询[F4]', '单日委托']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + + @property + def today_trades(self): + self._switch_left_menus(['查询[F4]', '单日成交']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - def _read_clipboard(self): - for _ in range(15): + def buy(self, security, price, amount): + self._switch_left_menus(['买入[F1]']) + + return self.trade(security, price, amount) + + def sell(self, security, price, amount): + self._switch_left_menus(['卖出[F2]']) + + return self.trade(security, price, amount) + + @property + def cancel_entrusts(self): + self._refresh() + self._switch_left_menus(['撤单[F3]']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + + def cancel_entrust(self, entrust_no): + self._refresh() + self._switch_left_menus(['买入[F1]']) + for i, entrust in enumerate(self.cancel_entrusts): + if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: + self._cancel_entrust_by_double_click(i) + return self._handle_cancel_entrust_pop_dialog() + else: + return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} + + def auto_ipo(self): + self._switch_left_menus(['新股申购', '一键打新']) + + self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) + self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) + + return self._handle_auto_ipo_pop_dialog() + + def _handle_auto_ipo_pop_dialog(self): + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + title = self._get_pop_dialog_title() + if '提示信息' in title: + self._app.top_window().type_keys('%Y') + elif '提示' in title: + data = self._app.top_window().Static.window_text() + self._app.top_window()['确定'].click() + return {'message': data} + else: + data = self._app.top_window().Static.window_text() + self._app.top_window().close() + return {'message': 'unkown message: {}'.find(data)} + self._wait(0.1) + + def _click(self, control_id): + self._app.top_window().window( + control_id=control_id, + class_name='Button' + ).click() + + def trade(self, security, price, amount): + self._set_trade_params(security, price, amount) + + self._submit_trade() + + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + pop_title = self._get_pop_dialog_title() + if pop_title == '委托确认': + self._app.top_window().type_keys('%Y') + elif pop_title == '提示信息': + if '超出涨跌停' in self._app.top_window().Static.window_text(): + self._app.top_window().type_keys('%Y') + elif pop_title == '提示': + content = self._app.top_window().Static.window_text() + if '成功' in content: + entrust_id = self._extract_entrust_id(content) + self._app.top_window()['确定'].click() + return {'entrust_id': entrust_id} + else: + self._app.top_window()['确定'].click() + self._wait(0.05) + raise exceptions.TradeError(content) + else: + self._app.top_window().close() + self._wait(0.1) # wait next dialog display + + def _extract_entrust_id(self, content): + return re.search(r'\d+', content).group() + + def _submit_trade(self): + time.sleep(0.05) + self._app.top_window().window( + control_id=self._config.TRADE_SUBMIT_CONTROL_ID, + class_name='Button' + ).click() + + def _get_pop_dialog_title(self): + return self._app.top_window().window( + control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID + ).window_text() + + def _set_trade_params(self, security, price, amount): + self._type_keys( + self._config.TRADE_SECURITY_CONTROL_ID, + security + ) + self._type_keys( + self._config.TRADE_PRICE_CONTROL_ID, + easyutils.round_price_by_code(price, security) + ) + self._type_keys( + self._config.TRADE_AMOUNT_CONTROL_ID, + str(int(amount)) + ) + + def _get_grid_data(self, control_id): + grid = self._app.top_window().window( + control_id=control_id, + class_name='CVirtualGridCtrl' + ) + grid.type_keys('^A^C') + return self._format_grid_data( + self._get_clipboard_data() + ) + + def _type_keys(self, control_id, text): + self._app.top_window().window( + control_id=control_id, + class_name='Edit' + ).type_keys(text) + + def _get_clipboard_data(self): + while True: try: - win32api.keybd_event(17, 0, 0, 0) - win32api.keybd_event(67, 0, 0, 0) - win32api.keybd_event(67, 0, win32con.KEYEVENTF_KEYUP, 0) - win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0) - time.sleep(0.2) - return pyperclip.paste() + return pywinauto.clipboard.GetData() except Exception as e: - log.error('open clipboard failed: {}, retry...'.format(e)) - time.sleep(1) - else: - raise Exception('read clipbord failed') - - @staticmethod - def _project_position_str(raw): - reader = StringIO(raw) - df = pd.read_csv(reader, sep = '\t') - return df - - @staticmethod - def _set_foreground_window(hwnd): - import pythoncom - pythoncom.CoInitialize() - shell = win32com.client.Dispatch('WScript.Shell') - shell.SendKeys('%') - win32gui.SetForegroundWindow(hwnd) + log.warning('{}, retry ......'.format(e)) + + def _switch_left_menus(self, path, sleep=0.2): + self._get_left_menus_handle().get_item(path).click() + self._wait(sleep) + + def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): + self._app.top_window().type_keys(shortcut) + self._wait(sleep) + + @functools.lru_cache() + def _get_left_menus_handle(self): + return self._app.top_window().window( + control_id=129, + class_name='SysTreeView32' + ) + + def _format_grid_data(self, data): + df = pd.read_csv(io.StringIO(data), + delimiter='\t', + dtype=self._config.GRID_DTYPE, + na_filter=False, + ) + return df.to_dict('records') - @property - def entrust(self): - return self.get_entrust() - - def get_entrust(self): - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.2) - self._set_foreground_window(self.entrust_list_hwnd) - time.sleep(0.2) - data = self._read_clipboard() - return self.project_copy_data(data) + def _handle_cancel_entrust_pop_dialog(self): + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + title = self._get_pop_dialog_title() + if '提示信息' in title: + self._app.top_window().type_keys('%Y') + elif '提示' in title: + data = self._app.top_window().Static.window_text() + self._app.top_window()['确定'].click() + return {'message': data} + else: + data = self._app.top_window().Static.window_text() + self._app.top_window().close() + return {'message': 'unkown message: {}'.find(data)} + self._wait(0.2) + + def _cancel_entrust_by_double_click(self, row): + x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN + y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row + self._app.top_window().window( + control_id=self._config.COMMON_GRID_CONTROL_ID, + class_name='CVirtualGridCtrl' + ).double_click(coords=(x, y)) + + def _refresh(self): + self._switch_left_menus(['买入[F1]'], sleep=0.05) diff --git a/requirements.txt b/requirements.txt index 66d30707..82cbeda1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +pywinauto bs4 requests demjson diff --git a/test_easytrader.py b/test_easytrader.py index 99b15650..619900c4 100644 --- a/test_easytrader.py +++ b/test_easytrader.py @@ -1,155 +1,51 @@ # coding: utf-8 +import os +import sys +import time import unittest -from datetime import datetime -from unittest import mock -import easytrader -from easytrader import JoinQuantFollower, RiceQuantFollower -from easytrader import helpers -from easytrader.follower import BaseFollower - - -class TestEasytrader(unittest.TestCase): - def test_helpers(self): - result = helpers.get_stock_type('162411') - self.assertEqual(result, 'sz') - - result = helpers.get_stock_type('691777') - self.assertEqual(result, 'sh') - - result = helpers.get_stock_type('sz162411') - self.assertEqual(result, 'sz') - - def test_helpers_grep_comma(self): - test_data = '123' - normal_data = '123' - result = helpers.grep_comma(test_data) - self.assertEqual(result, normal_data) - - test_data = '4,000' - normal_data = '4000' - result = helpers.grep_comma(test_data) - self.assertEqual(result, normal_data) - - def test_helpers_str2num(self): - test_data = '123' - normal_data = 123 - result = helpers.str2num(test_data, 'int') - self.assertEqual(result, normal_data) - - test_data = '1,000' - normal_data = 1000 - result = helpers.str2num(test_data, 'int') - self.assertEqual(result, normal_data) +sys.path.append('.') - test_data = '123.05' - normal_data = 123.05 - result = helpers.str2num(test_data, 'float') - self.assertAlmostEqual(result, normal_data) - - test_data = '1,023.05' - normal_data = 1023.05 - result = helpers.str2num(test_data, 'float') - self.assertAlmostEqual(result, normal_data) - - def test_gf_check_account_live(self): - user = easytrader.use('gf') - - test_data = None - with self.assertRaises(easytrader.webtrader.NotLoginError): - user.check_account_live(test_data) - self.assertFalse(user.heart_active) - - test_data = {'success': False, 'data': [{}], 'total': 1} - with self.assertRaises(easytrader.webtrader.NotLoginError): - user.check_account_live(test_data) - self.assertFalse(user.heart_active) - - -class TestXueQiuTrader(unittest.TestCase): - def test_set_initial_assets(self): - # default set to 1e6 - xq_user = easytrader.use('xq') - self.assertEqual(xq_user.multiple, 1e6) - - xq_user = easytrader.use('xq', initial_assets=1000) - self.assertEqual(xq_user.multiple, 1000) +import easytrader - # cant low than 1000 - with self.assertRaises(ValueError): - xq_user = easytrader.use('xq', initial_assets=999) - # initial_assets must be number - cases = [None, '', b'', bool] - for v in cases: - with self.assertRaises(TypeError): - xq_user = easytrader.use('xq', initial_assets=v) +class TestYhClientTrader(unittest.TestCase): + @classmethod + def setUpClass(cls): + # input your test account and password + cls._ACCOUNT = os.environ.get('EZ_TEST_YH_ACCOUNT') or 'your account' + cls._PASSWORD = os.environ.get('EZ_TEST_YH_password') or 'your password' + cls._user = easytrader.use('yh_client') + cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD) -class TestJoinQuantFollower(unittest.TestCase): - def test_extract_strategy_id(self): - cases = [('https://www.joinquant.com/algorithm/live/index?backtestId=aaaabbbbcccc', - 'aaaabbbbcccc')] - for test, result in cases: - extracted_id = JoinQuantFollower.extract_strategy_id(test) - self.assertEqual(extracted_id, result) + def test_balance(self): + time.sleep(3) + result = self._user.balance - def test_stock_shuffle_to_prefix(self): - cases = [('123456.XSHG', 'sh123456'), - ('000001.XSHE', 'sz000001')] - for test, result in cases: - self.assertEqual( - JoinQuantFollower.stock_shuffle_to_prefix(test), - result - ) + def test_today_entrusts(self): + result = self._user.today_entrusts - with self.assertRaises(AssertionError): - JoinQuantFollower.stock_shuffle_to_prefix('1234') + def test_today_trades(self): + result = self._user.today_trades - def test_project_transactions(self): - cases = [([{'type': '市价单', 'price': 8.11, 'commission': 9.98, 'gains': 0, 'time': '14:50', 'date': '2016-11-18', - 'security': '股票', 'stock': '华纺股份(600448.XSHG)', 'transaction': '买', 'total': 33251, - 'status': '全部成交', - 'amount': "4100股"}], - [{'type': '市价单', 'price': 8.11, 'commission': 9.98, 'gains': 0, 'time': '14:50', 'date': '2016-11-18', - 'security': '股票', 'stock': '华纺股份(600448.XSHG)', 'transaction': '买', 'total': 33251, - 'status': '全部成交', - 'amount': 4100, - 'action': 'buy', - 'stock_code': 'sh600448', - 'datetime': - datetime.strptime('2016-11-18 14:50', '%Y-%m-%d %H:%M') - }])] - for test, result in cases: - JoinQuantFollower().project_transactions(test), - self.assertListEqual( - test, - result - ) + def test_cancel_entrusts(self): + result = self._user.cancel_entrusts + def test_cancel_entrust(self): + result = self._user.cancel_entrust('123456789') -class TestFollower(unittest.TestCase): - def test_is_number(self): - cases = [('1', True), - ('--', False)] - for string, result in cases: - test = BaseFollower._is_number(string) - self.assertEqual(test, result) + def test_invalid_buy(self): + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.buy('511990', 1, 1e10) - @mock.patch.object(BaseFollower, 'trade_worker', autospec=True) - def test_send_interval(self, mock_trade_worker): - cases = [(1, 1), (2, 2)] - for follower_cls in [JoinQuantFollower, RiceQuantFollower]: - for test_data, truth in cases: - follower = follower_cls() - try: - follower.follow(None, None, send_interval=test_data) - except: - pass - print(test_data, truth) - self.assertEqual(mock_trade_worker.call_args[1]['send_interval'], truth) + def test_invalid_sell(self): + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.buy('162411', 200, 1e10) + def test_auto_ipo(self): + self._user.auto_ipo() if __name__ == '__main__': unittest.main() From 96b856dd894c692317e60ed37e279b66a802feb6 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 13:30:22 +0800 Subject: [PATCH 019/276] fix yh_client balance grid control id typo --- easytrader/config/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/config/client.py b/easytrader/config/client.py index f29fb7bb..e6ab274e 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -16,7 +16,7 @@ class YH: TRADE_SUBMIT_CONTROL_ID = 1006 COMMON_GRID_CONTROL_ID = 1047 - BALANCE_GRID_CONTROL_ID = 1047 + BALANCE_GRID_CONTROL_ID = 1308 POP_DIALOD_TITLE_CONTROL_ID = 1365 From 567e3c17b5572e16d21586de9a7ad5b32d973980 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 13:31:03 +0800 Subject: [PATCH 020/276] fix entrust_no typo --- easytrader/yh_clienttrader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index f526c02b..9094609f 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -185,9 +185,9 @@ def trade(self, security, price, amount): elif pop_title == '提示': content = self._app.top_window().Static.window_text() if '成功' in content: - entrust_id = self._extract_entrust_id(content) + entrust_no = self._extract_entrust_id(content) self._app.top_window()['确定'].click() - return {'entrust_id': entrust_id} + return {'entrust_no': entrust_no} else: self._app.top_window()['确定'].click() self._wait(0.05) From e3c7c459d0edf7494772fd6e23c872b0d6cbd32d Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 13:47:07 +0800 Subject: [PATCH 021/276] remove security prefix if has avoid trade error --- easytrader/yh_clienttrader.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 9094609f..24bdf592 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -212,13 +212,15 @@ def _get_pop_dialog_title(self): ).window_text() def _set_trade_params(self, security, price, amount): + code = security[-6:] + self._type_keys( self._config.TRADE_SECURITY_CONTROL_ID, - security + code ) self._type_keys( self._config.TRADE_PRICE_CONTROL_ID, - easyutils.round_price_by_code(price, security) + easyutils.round_price_by_code(price, code) ) self._type_keys( self._config.TRADE_AMOUNT_CONTROL_ID, From 2e62775380e4040af2760e4f1662089f61eb3410 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 13:47:42 +0800 Subject: [PATCH 022/276] adjust cancel_entrust action --- easytrader/yh_clienttrader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 24bdf592..601f48fd 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -133,7 +133,6 @@ def cancel_entrusts(self): def cancel_entrust(self, entrust_no): self._refresh() - self._switch_left_menus(['买入[F1]']) for i, entrust in enumerate(self.cancel_entrusts): if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: self._cancel_entrust_by_double_click(i) @@ -194,7 +193,7 @@ def trade(self, security, price, amount): raise exceptions.TradeError(content) else: self._app.top_window().close() - self._wait(0.1) # wait next dialog display + self._wait(0.2) # wait next dialog display def _extract_entrust_id(self, content): return re.search(r'\d+', content).group() From ef7048e696e58b30222bdd480a770f9555d1f965 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 14:15:59 +0800 Subject: [PATCH 023/276] fix connect exception handle when start client --- easytrader/yh_clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 601f48fd..e0960131 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -44,7 +44,7 @@ def prepare(self, config_path=None, user=None, password=None, exe_path=r'C:\中 def login(self, user, password, exe_path): try: self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) - except RuntimeError: + except Exception: self._app = pywinauto.Application().start(exe_path) self._wait(1) From 9f75ae3e9673202958868e0b9480b0cebe5777c9 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 14:16:10 +0800 Subject: [PATCH 024/276] fix test typo --- test_easytrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_easytrader.py b/test_easytrader.py index 619900c4..f08fd998 100644 --- a/test_easytrader.py +++ b/test_easytrader.py @@ -42,7 +42,7 @@ def test_invalid_buy(self): def test_invalid_sell(self): with self.assertRaises(easytrader.exceptions.TradeError): - result = self._user.buy('162411', 200, 1e10) + result = self._user.sell('162411', 200, 1e10) def test_auto_ipo(self): self._user.auto_ipo() From 9a875fdaf78b252b3336d638118923df14bc7a52 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 17:00:25 +0800 Subject: [PATCH 025/276] fix typo --- easytrader/yh_clienttrader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index e0960131..4143bfb2 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -104,13 +104,13 @@ def position(self): @property def today_entrusts(self): - self._switch_left_menus(['查询[F4]', '单日委托']) + self._switch_left_menus(['查询[F4]', '当日委托']) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @property def today_trades(self): - self._switch_left_menus(['查询[F4]', '单日成交']) + self._switch_left_menus(['查询[F4]', '当日成交']) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) From 07bc015103983c71ccfc8b0e57c978a8a3dc478f Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 19:46:39 +0800 Subject: [PATCH 026/276] remove hardcode time slepp for speed up client login --- easytrader/yh_clienttrader.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 4143bfb2..9b2902a6 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -46,7 +46,14 @@ def login(self, user, password, exe_path): self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) except Exception: self._app = pywinauto.Application().start(exe_path) - self._wait(1) + + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait('ready') + break + except RuntimeError: + pass self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) @@ -57,7 +64,12 @@ def login(self, user, password, exe_path): ) self._app.top_window()['登录'].click() - self._wait(2) + + # detect login is success or not + try: + self._app.top_window().wait_not('exists', 2) + except: + pass self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) self._close_prompt_windows() From 1d0667ce495ecd2bbd75bd93ef1626a54cf110a6 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 8 Sep 2017 19:46:55 +0800 Subject: [PATCH 027/276] fix some time cant find left menu handle ready --- easytrader/yh_clienttrader.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 9b2902a6..54b884d7 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -271,10 +271,17 @@ def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): @functools.lru_cache() def _get_left_menus_handle(self): - return self._app.top_window().window( - control_id=129, - class_name='SysTreeView32' - ) + while True: + try: + handle = self._app.top_window().window( + control_id=129, + class_name='SysTreeView32' + ) + # sometime can't find handle ready, must retry + handle.wait('ready', 2) + return handle + except: + pass def _format_grid_data(self, data): df = pd.read_csv(io.StringIO(data), From 5d728125c259c7cf98dfb8338b1b01387870957c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 9 Sep 2017 08:02:30 +0800 Subject: [PATCH 028/276] optimize handle verify code process --- easytrader/yh_clienttrader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 54b884d7..e137786e 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -58,7 +58,7 @@ def login(self, user, password, exe_path): self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) - while self._app.is_process_running(): + while True: self._app.top_window().Edit3.type_keys( self._handle_verify_code() ) @@ -68,6 +68,7 @@ def login(self, user, password, exe_path): # detect login is success or not try: self._app.top_window().wait_not('exists', 2) + break except: pass From a78378b5c055aa4e0d6a8c8ef2302bb5bc64caf8 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 9 Sep 2017 17:53:20 +0800 Subject: [PATCH 029/276] add base class ClientTrader for HTClientTrader and YHClientTrader --- docs/index.md | 3 +- docs/other/INSTALL4Windows.md | 29 -- docs/usage.md | 137 +------- easytrader/api.py | 8 +- easytrader/clienttrader.py | 90 +++++ easytrader/config/client.py | 47 +++ easytrader/config/xczq.json | 72 ---- easytrader/helpers.py | 2 - easytrader/ht_clienttrader.py | 596 ++++++++++++++++------------------ easytrader/xczqtrader.py | 302 ----------------- easytrader/yh_clienttrader.py | 31 +- mkdocs.yml | 1 - test_easytrader.py | 50 +++ 13 files changed, 495 insertions(+), 873 deletions(-) delete mode 100644 docs/other/INSTALL4Windows.md create mode 100644 easytrader/clienttrader.py delete mode 100644 easytrader/config/xczq.json delete mode 100644 easytrader/xczqtrader.py diff --git a/docs/index.md b/docs/index.md index d3a9c9e3..d753a699 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,9 +18,8 @@ ### 支持券商 -* 广发 * 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 -* 湘财证券 +* 华泰客户端(同花顺版本) ### 模拟交易 diff --git a/docs/other/INSTALL4Windows.md b/docs/other/INSTALL4Windows.md deleted file mode 100644 index d15e344f..00000000 --- a/docs/other/INSTALL4Windows.md +++ /dev/null @@ -1,29 +0,0 @@ - -**Python installation** - -* Install windows python3 from python.org - -** Python module installation** - -* pip install -r requirements.txt -* pip install Pillow -* pip install pytesseract - - **Tesseract installation** - -* wget https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/pytesser/pytesser_v0.0.1.zip -* unzip pytesser_v0.0.1.zip -* Put tesseract.exe tessdata\ under C:\Users\xxxx\AppData\Local\Programs\Python\Python35\Scripts\ - Config json file like gf.json -* Open https://trade.gf.com.cn in IE -* Input Account and Password (select 'save account') -* Login (for creating 'userId' Cookie) -* Logout -* Input Account and Password -* F12 | Console -* Copy userId from Cookie and paste into gf.json | username -* var e=document.getElementById("SecurePassword") -* e.GetPassword() -* Copy password and paste into gf.json | password - Run -* python cli.py --use gf --prepare gf.json diff --git a/docs/usage.md b/docs/usage.md index ad7ab803..627040f6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,34 +9,13 @@ import easytrader ** 银河客户端** ```python -user = easytrader.use('yh_client') # 银河客户端支持 ['yh_client', 'YH_CLIENT', '银河客户端'] +user = easytrader.use('yh_client') # 银河客户端支持 ['yh_client', '银河客户端'] ``` ** 华泰客户端** ```python -user = easytrader.use('ht_client') # 华泰客户端支持 ['ht_client', 'HT_CLIENT', '华泰客户端'] +user = easytrader.use('ht_client') # 华泰客户端支持 ['ht_client', '华泰客户端'] ``` -** 广发** - -```python -user = easytrader.use('gf') # 广发支持 ['gf', 'GF', '广发'] -``` - -**湘财证券** - -```python -user = easytrader.use('xczq') # 湘财证券支持 ['xczq', '湘财证券'] -``` - - -# 抓取登陆所需的密码 - -使用 `easytrader` 的广发,银河 `web` 版本时,需要抓取对应券商的加密密码 - -**广发** - -参考此文档 [INSTALL4Windows.md](other/INSTALL4Windows.md) - **雪球** 雪球配置中 `username` 为邮箱, `account` 为手机, 填两者之一即可,另一项改为 `""`, 密码直接填写登录的明文密码即可,不需要抓取 `POST` 的密码 @@ -56,11 +35,7 @@ user = easytrader.use('xczq') # 湘财证券支持 ['xczq', '湘财证券'] ** 参数登录(推荐)** ``` -user.prepare(user='用户名', password='广发web端需要券商加密后的密码, 雪球、银河客户端为明文密码') -``` - -``` -user.prepare(user='用户名', password='华泰交易密码',commpasswd='华泰通讯密码') +user.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码,其他券商不用') ``` **注:**雪球额外有个 account 参数,见上文介绍 @@ -91,27 +66,9 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 {  "user": "华泰用户名",  "password": "华泰明文密码" -  "commpasswd": "华泰通讯密码" -} - -``` - -广发 - -``` -{ - "username": "加密的客户号", - "password": "加密的密码" +  "comm_password": "华泰通讯密码" } -``` - -湘菜证券 -``` -{ - "account": "客户号", - "password": "密码" -} ``` ### 交易相关 @@ -159,28 +116,6 @@ user.position '证券名称': '工商银行'}] ``` -#### 获取今日委托单 -```python -user.entrust -``` - -**return** - -```python -[{'business_amount': '成交数量', - 'business_price': '成交价格', - 'entrust_amount': '委托数量', - 'entrust_bs': '买卖方向', - 'entrust_no': '委托编号', - 'entrust_price': '委托价格', - 'entrust_status': '委托状态', # 废单 / 已报 - 'report_time': '申报时间', - 'stock_code': '证券代码', - 'stock_name': '证券名称'}] - -``` - - #### 买入: ```python @@ -190,7 +125,7 @@ user.buy('162411', price=0.55, amount=100) **return** ```python -{'orderid': 'xxxxxxxx', 'ordersno': '1111'} +{'entrust_no': 'xxxxxxxx'} ``` #### 卖出: @@ -202,42 +137,32 @@ user.sell('162411', price=0.55, amount=100) **return** ```python -{'orderid': 'xxxxxxxx', 'ordersno': '1111'} +{'entrust_no': 'xxxxxxxx'} ``` #### 一键打新 -##### 银河 - ```python user.auto_ipo() ``` #### 撤单 -##### 银河 - ```python -user.cancel_entrust('委托单号', '股票代码') +user.cancel_entrust('buy/sell 获取的 entrust_no') ``` **return** ``` -{'msgok': '撤单申报成功'} +{'message': '撤单申报成功'} ``` -##### 银河客户端 - - -```python -user.cancel_entrust('股票6位代码,不带前缀', "撤单方向,可使用 ['buy', 'sell']" -``` -#### 查询当日成交 +#### 当日成交 ```python -user.current_deal +user.today_trades ``` **return** @@ -256,10 +181,10 @@ user.current_deal '证券名称': '华宝油气'}] ``` -#### 今日委托 +#### 当日委托 ```python -user.entrust +user.today_entrusts ``` **return** @@ -293,38 +218,6 @@ user.entrust '证券名称': '华宝油气'}] ``` -#### 查询交割单 - -需要注意通常券商只会返回有限天数最新的交割单,如查询2015年整年数据 - -```python -user.exchangebill # 查询最近30天的交割单 - -user.get_exchangebill('开始日期', '截止日期') # 指定查询时间段, 日期格式为 "20160214" -``` -**return** -```python -{["entrust_bs": "操作", # "1":"买入", "2":"卖出", " ":"其他" - "business_balance": "成交金额", - "stock_name": "证券名称", - "fare1": "印花税", - "occur_balance": "发生金额", - "stock_account": "股东帐户", - "business_name": "摘要", # "证券买入", "证券卖出", "基金拆分", "基金合并", "交收证券冻结", "交收证券冻结取消", "开放基金赎回", "开放基金赎回返款", "基金资金拨入", "基金资金拨出", "交收资金冻结取消", "开放基金申购" - "farex": "", - "fare0": "手续费", - "stock_code": "证券代码", - "occur_amount": "成交数量", - "date": "成交日期", - "post_balance": "本次余额", - "fare2": "其他杂费", - "fare3": "", - "entrust_no": "合同编号", - "business_price": "成交均价", -]} - -``` - #### 查询今天可以申购的新股信息 @@ -343,6 +236,12 @@ print(ipo_data) 'apply_code': '申购代码'}] ``` +#### 退出客户端软件 + +``` +user.exit() +``` + #### 雪球组合调仓 ```python diff --git a/easytrader/api.py b/easytrader/api.py index 38ea9a56..1054b7ef 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -3,11 +3,10 @@ from .gftrader import GFTrader from .joinquant_follower import JoinQuantFollower -from .ricequant_follower import RiceQuantFollower from .log import log +from .ricequant_follower import RiceQuantFollower from .xq_follower import XueQiuFollower from .xqtrader import XueQiuTrader -from .xczqtrader import XCZQTrader def use(broker, debug=True, **kwargs): @@ -35,8 +34,9 @@ def use(broker, debug=True, **kwargs): elif broker.lower() in ['ht_client', '华泰客户端']: from .ht_clienttrader import HTClientTrader return HTClientTrader() - elif broker.lower() in ['xczq', '湘财证券']: - return XCZQTrader() + elif broker.lower() in ['gj_client', '国金客户端']: + from .gj_clienttrader import GJClientTrader + return GJClientTrader() def follower(platform, **kwargs): diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py new file mode 100644 index 00000000..5006a136 --- /dev/null +++ b/easytrader/clienttrader.py @@ -0,0 +1,90 @@ +# coding:utf-8 + +import os +import time +from abc import abstractmethod + +from . import helpers +from .config import client + + +class ClientTrader: + def __init__(self): + self._config = client.create(self.broker_type) + + def prepare(self, config_path=None, user=None, password=None, exe_path=None, comm_password=None, + **kwargs): + """ + 登陆客户端 + :param config_path: 登陆配置文件,跟参数登陆方式二选一 + :param user: 账号 + :param password: 明文密码 + :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe' + :param comm_password: 通讯密码 + :return: + """ + if config_path is not None: + account = helpers.file2dict(config_path) + user = account['user'] + password = account['password'] + self.login(user, password, exe_path or self._config.DEFAULT_EXE_PATH, comm_password, **kwargs) + + @abstractmethod + def login(self, user, password, exe_path, comm_password=None, **kwargs): + pass + + @property + @abstractmethod + def broker_type(self): + pass + + @property + @abstractmethod + def balance(self): + pass + + @property + @abstractmethod + def position(self): + pass + + @property + @abstractmethod + def cancel_entrusts(self): + pass + + @property + @abstractmethod + def today_entrusts(self): + pass + + @property + @abstractmethod + def today_trades(self): + pass + + @abstractmethod + def cancel_entrust(self, entrust_no): + pass + + @abstractmethod + def buy(self, security, price, amount): + pass + + @abstractmethod + def sell(self, security, price, amount): + pass + + def auto_ipo(self): + raise NotImplementedError + + def _run_exe_path(self, exe_path): + return os.path.join( + os.path.dirname(exe_path), 'xiadan.exe' + ) + + def _wait(self, seconds): + time.sleep(seconds) + + def exit(self): + self._app.kill() diff --git a/easytrader/config/client.py b/easytrader/config/client.py index e6ab274e..daf948da 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -3,10 +3,13 @@ def create(broker): if broker == 'yh': return YH + elif broker == 'ht': + return HT raise NotImplemented class YH: + DEFAULT_EXE_PATH = r'C:\中国银河证券双子星3.2\Binarystar.exe' TITLE = '网上股票交易系统5.0' TRADE_SECURITY_CONTROL_ID = 1032 @@ -39,3 +42,47 @@ class YH: AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 AUTO_IPO_BUTTON_CONTROL_ID = 1006 + + +class HT: + DEFAULT_EXE_PATH = r'C:\htzqzyb2\xiadan.exe' + TITLE = '网上股票交易系统5.0' + + TRADE_SECURITY_CONTROL_ID = 1032 + TRADE_PRICE_CONTROL_ID = 1033 + TRADE_AMOUNT_CONTROL_ID = 1034 + + TRADE_SUBMIT_CONTROL_ID = 1006 + + COMMON_GRID_CONTROL_ID = 1047 + + BALANCE_CONTROL_ID_GROUP = { + '资金余额': 1012, + '冻结资金': 1013, + '可用金额': 1016, + '可取金额': 1017, + '股票市值': 1014, + '总资产': 1015 + } + + POP_DIALOD_TITLE_CONTROL_ID = 1365 + + GRID_DTYPE = { + '操作日期': str, + '委托编号': str, + '申请编号': str, + '合同编号': str, + '证券代码': str, + '股东代码': str, + '资金帐号': str, + '资金帐户': str, + '发生日期': str + } + + CANCEL_ENTRUST_ENTRUST_FIELD = '合同编号' + CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50 + CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30 + CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16 + + AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 + AUTO_IPO_BUTTON_CONTROL_ID = 1006 diff --git a/easytrader/config/xczq.json b/easytrader/config/xczq.json deleted file mode 100644 index e4b2fb0e..00000000 --- a/easytrader/config/xczq.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "login_page": "https://webtrade.xcsc.com/winner/xcsc/", - "login_api": "https://webtrade.xcsc.com/winner/xcsc/user/exchange.action", - "verify_code_api": "https://webtrade.xcsc.com/winner/xcsc/user/extraCode.jsp", - "prefix": "https://webtrade.xcsc.com/winner/xcsc/user/exchange.action", - "logout_api": "https://webtrade.xcsc.com/winner/xcsc/exchange.action?function_id=20&login_type=stock", - "login": { - "function_id": 200, - "login_type": "stock", - "version": 200, - "identity_type": "", - "remember_me": "", - "input_content": 1, - "content_type": 0 - }, - "buy": { - "service_type": "stock", - "request_id": "buystock_302" - }, - "buymarket" :{ - "service_type": "stock", - "request_id": "buystockbymaketvalue_302" - }, - "sell": { - "service_type": "stock", - "request_id": "sellstock_302" - }, - "position": { - "request_id": "mystock_403" - }, - "balance": { - "request_id": "mystock_405" - }, - "entrust": { - "request_id": "trust_401", - "sort_direction": 1, - "deliver_type": "", - "service_type": "stock" - }, - "cancel_entrust": { - "request_id": "chedan_304" - }, - "can_cancel": { - "request_id": "chedan_401", - "sort_direction": 1, - "service_type": "stock", - "action_in":1 - }, - "current_deal": { - "request_id": "bargain_402", - "sort_direction": 1, - "service_type": "stock" - }, - "ipo_enable_amount": { - "request_id": "buystock_301" - }, - "exchangetype4stock": { - "service_type": "stock", - "function_id": "105" - }, - "account4stock": { - "service_type": "stock", - "function_id": "407", - "window_id": "StockMarketTrade" - }, - "exchangebill": { - "request_id": "prompt_308", - "sort_direction": 0, - "service_type": "stock", - "deliver_type":1 - } -} diff --git a/easytrader/helpers.py b/easytrader/helpers.py index 03f7c330..f28430e2 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -72,8 +72,6 @@ def recognize_verify_code(image_path, broker='ht'): if broker == 'gf': return detect_gf_result(image_path) - elif broker == 'xczq': - return default_verify_code_detect(image_path) elif broker == 'yh_client': return detect_yh_client_result(image_path) # 调用 tesseract 识别 diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index e333e210..48a8b589 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,352 +1,304 @@ # coding:utf8 from __future__ import division +import functools +import io import os -import subprocess -import tempfile +import re import time -import traceback -import win32api -import win32gui -from io import StringIO +import easyutils import pandas as pd -import pyperclip -import win32com.client -import win32con -from PIL import ImageGrab +import pywinauto +import pywinauto.clipboard -from . import helpers +from . import exceptions +from .clienttrader import ClientTrader from .log import log -class HTClientTrader(): - def __init__(self): - self.Title = '网上股票交易系统5.0' +class HTClientTrader(ClientTrader): + @property + def broker_type(self): + return 'ht' - def prepare(self, config_path=None, user=None, password=None, commpasswd=None, exe_path='C:\htwt\Xiadan.exe'): + def login(self, user, password, exe_path, comm_password=None, **kwargs): """ - 登陆银河客户端 - :param config_path: 华泰登陆配置文件,跟参数登陆方式二选一 - :param user: 华泰账号 - :param password: 华泰明文密码 - :param commpasswd: 华泰通讯密码 - :param exe_path: 华泰客户端路径 + :param user: 用户名 + :param password: 密码 + :param exe_path: 客户端路径, 类似 + :param comm_password: + :param kwargs: :return: """ - if config_path is not None: - account = helpers.file2dict(config_path) - user = account['user'] - password = account['password'] - commpasswd = account['commpasswd'] - self.login(user, password, commpasswd, exe_path) - - def login(self, user, password, commpasswd, exe_path): - if self._has_main_window(): - self._get_handles() - log.info('检测到交易客户端已启动,连接完毕') - return - if not self._has_login_window(): - if not os.path.exists(exe_path): - raise FileNotFoundError('在 {} 未找到应用程序,请用 exe_path 指定应用程序目录'.format(exe_path)) - subprocess.Popen(exe_path) - # 检测登陆窗口 - for _ in range(30): - if self._has_login_window(): - break - time.sleep(1) - else: - raise Exception('启动客户端失败,无法检测到登陆窗口') - log.info('成功检测到客户端登陆窗口') - - # 登陆 - # self._set_trade_mode() - self._set_login_name(user) - self._set_login_password(password) - self._set_login_commpassword(commpasswd) - for _ in range(10): - # self._set_login_verify_code() - self._click_login_button() - time.sleep(3) - if not self._has_login_window(): - break - # self._click_login_verify_code() - - for _ in range(60): - if self._has_main_window(): - # self._get_handles() - break - time.sleep(1) - else: - raise Exception('启动交易客户端失败') - log.info('客户端登陆成功') - - # def _set_login_verify_code(self): - # verify_code_image = self._grab_verify_code() - # image_path = tempfile.mktemp() + '.jpg' - # verify_code_image.save(image_path) - # result = helpers.recognize_verify_code(image_path, 'yh_client') - # time.sleep(0.2) - # self._input_login_verify_code(result) - # time.sleep(0.4) - - # def _set_trade_mode(self): - # input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x4f4d) - # win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) - - def _set_login_name(self, user): - time.sleep(0.5) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F3) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, user) - - def _set_login_password(self, password): - time.sleep(0.5) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F4) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, password) - - def _set_login_commpassword(self, commpasswd): - time.sleep(0.5) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3E9) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, commpasswd) - def _has_login_window(self): - for title in ['用户登录']: - self.login_hwnd = win32gui.FindWindow(None, title) - if self.login_hwnd != 0: - return True - return False - - # def _input_login_verify_code(self, code): - # input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56b9) - # win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, code) - - # def _click_login_verify_code(self): - # input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56ba) - # rect = win32gui.GetWindowRect(input_hwnd) - # self._mouse_click(rect[0] + 5, rect[1] + 5) - - @staticmethod - def _mouse_click(x, y): - win32api.SetCursorPos((x, y)) - win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) - win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) - - def _click_login_button(self): - time.sleep(1) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3EE) - win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) - - def _has_main_window(self): - try: - self._get_handles() - except: - return False - return True - - # def _grab_verify_code(self): - # verify_code_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x56ba) - # self._set_foreground_window(self.login_hwnd) - # time.sleep(1) - # rect = win32gui.GetWindowRect(verify_code_hwnd) - # return ImageGrab.grab(rect) - - def _get_handles(self): - trade_main_hwnd = win32gui.FindWindow(0, self.Title) # 交易窗口 - operate_frame_hwnd = win32gui.GetDlgItem(trade_main_hwnd, 59648) # 操作窗口框架 - operate_frame_afx_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59648) # 操作窗口框架 - hexin_hwnd = win32gui.GetDlgItem(operate_frame_afx_hwnd, 129) - scroll_hwnd = win32gui.GetDlgItem(hexin_hwnd, 200) # 左部折叠菜单控件 - tree_view_hwnd = win32gui.GetDlgItem(scroll_hwnd, 129) # 左部折叠菜单控件 - - # 获取委托窗口所有控件句柄 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F1, 0) - time.sleep(0.5) - - # 买入相关 - entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 委托窗口框架 - self.buy_stock_code_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x408) # 买入代码输入框 - self.buy_price_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x409) # 买入价格输入框 - self.buy_amount_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x40A) # 买入数量输入框 - self.buy_btn_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x3EE) # 买入确认按钮 - self.refresh_entrust_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x8016) # 刷新持仓按钮 - entrust_frame_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 0x417) # 持仓显示框架 - entrust_sub_frame_hwnd = win32gui.GetDlgItem(entrust_frame_hwnd, 200) # 持仓显示框架 ?? - self.position_list_hwnd = win32gui.GetDlgItem(entrust_sub_frame_hwnd, 1047) # 持仓列表 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F2, 0) - time.sleep(0.5) - - # 卖出相关 - sell_entrust_frame_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 - self.sell_stock_code_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1032) # 卖出代码输入框 - self.sell_price_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1033) # 卖出价格输入框 - self.sell_amount_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1034) # 卖出数量输入框 - self.sell_btn_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1006) # 卖出确认按钮 - - # 撤单窗口 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F3, 0) - time.sleep(0.5) - cancel_entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 撤单窗口框架 - self.cancel_stock_code_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0xD14) # 卖出代码输入框 - self.cancel_query_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0xD15) # 查询代码按钮 - self.cancel_buy_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0x7532) # 撤买 - self.cancel_sell_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0x7533) # 撤卖 - - chexin_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 0x417) - chexin_sub_hwnd = win32gui.GetDlgItem(chexin_hwnd, 200) - self.entrust_list_hwnd = win32gui.GetDlgItem(chexin_sub_hwnd, 1047) # 委托列表 - - - # 资金股票 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F4, 0) - time.sleep(0.5) - capital_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 资金股票窗口框架 - self.capital_balance_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F4) # 资金余额 - self.capital_frozen_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F5) # 冻结资金 - self.capital_available_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F8) # 可用金额 - self.capital_withdrawable_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F9) # 可取金额 - self.market_value_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F6) # 股票市值 - self.total_assets_hwnd = win32gui.GetDlgItem(capital_window_hwnd, 0x3F7) # 总资产 - self.capital_window_hwnd = capital_window_hwnd - - - def buy(self, stock_code, price, amount, **kwargs): - """ - 买入股票 - :param stock_code: 股票代码 - :param price: 买入价格 - :param amount: 买入股数 - :return: bool: 买入信号是否成功发出 - """ - amount = str(amount // 100 * 100) - price = str(price) + if comm_password is None: + raise ValueError('华泰必须设置通讯密码') try: - win32gui.SendMessage(self.buy_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入买入代码 - time.sleep(0.1) - win32gui.SendMessage(self.buy_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入买入价格 - time.sleep(0.1) - win32gui.SendMessage(self.buy_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入买入数量 - time.sleep(0.1) - win32gui.SendMessage(self.buy_btn_hwnd, win32con.BM_CLICK, None, None) # 买入确定 - time.sleep(0.2) - except: - traceback.print_exc() - return False - return True - - def sell(self, stock_code, price, amount, **kwargs): - """ - 买出股票 - :param stock_code: 股票代码 - :param price: 卖出价格 - :param amount: 卖出股数 - :return: bool 卖出操作是否成功 - """ - amount = str(amount // 100 * 100) - price = str(price) + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) + except Exception: + self._app = pywinauto.Application().start(exe_path) - try: - win32gui.SendMessage(self.sell_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入卖出代码 - time.sleep(0.1) - win32gui.SendMessage(self.sell_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入卖出价格 - time.sleep(0.1) - win32gui.SendMessage(self.sell_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入卖出数量 - time.sleep(0.1) - win32gui.SendMessage(self.sell_btn_hwnd, win32con.BM_CLICK, None, None) # 卖出确定 - time.sleep(0.2) - except: - traceback.print_exc() - return False - return True - - def cancel_entrust(self, stock_code, direction): - """ - 撤单 - :param stock_code: str 股票代码 - :param direction: str 'buy' 撤买, 'sell' 撤卖 - :return: bool 撤单信号是否发出 - """ - direction = 0 if direction == 'buy' else 1 + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait('ready') + break + except RuntimeError: + pass - try: - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.2) - win32gui.SendMessage(self.cancel_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入撤单 - win32gui.SendMessage(self.cancel_query_hwnd, win32con.BM_CLICK, None, None) # 查询代码 - time.sleep(0.2) - if direction == 0: - win32gui.SendMessage(self.cancel_buy_hwnd, win32con.BM_CLICK, None, None) # 撤买 - elif direction == 1: - win32gui.SendMessage(self.cancel_sell_hwnd, win32con.BM_CLICK, None, None) # 撤卖 - except: - traceback.print_exc() - return False - time.sleep(0.3) - return True + self._app.top_window().Edit1.type_keys(user) + self._app.top_window().Edit2.type_keys(password) + + self._app.top_window().Edit3.type_keys(comm_password) + + self._app.top_window().type_keys('%Y') + + # detect login is success or not + self._app.top_window().wait_not('exists', 2) + + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) + self._close_prompt_windows() + self._main = self._app.top_window() + + def _run_exe_path(self, exe_path): + return os.path.join( + os.path.dirname(exe_path), 'xiadan.exe' + ) + + def _wait(self, seconds): + time.sleep(seconds) + + def exit(self): + self._app.kill() + + def _close_prompt_windows(self): + self._wait(1) + for w in self._app.windows(class_name='#32770'): + if w.window_text() != self._config.TITLE: + w.close() + + @property + def balance(self): + self._switch_left_menus(['查询[F4]', '资金股票']) + + return self._get_balance_from_statics() + + def _get_balance_from_statics(self): + result = {} + for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): + result[key] = float( + self._app.top_window().window( + control_id=control_id, + class_name='Static', + ).window_text() + ) + return result @property def position(self): - return self.get_position() + self._switch_left_menus(['查询[F4]', '资金股票']) - def get_position(self): - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.1) - self._set_foreground_window(self.position_list_hwnd) - time.sleep(0.1) - data = self._read_clipboard() - return self.project_copy_data(data) + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @property - def balance(self): - return self.get_balance() - - def get_balance(self): - self._set_foreground_window(self.capital_window_hwnd) - time.sleep(0.3) - data = {} - data['资金余额'] = win32gui.GetWindowText(self.capital_balance_hwnd) - data['冻结余额'] = win32gui.GetWindowText(self.capital_frozen_hwnd) - data['可用金额'] = win32gui.GetWindowText(self.capital_available_hwnd) - data['可取余额'] = win32gui.GetWindowText(self.capital_withdrawable_hwnd) - data['股票市值'] = win32gui.GetWindowText(self.market_value_hwnd) - data['总资产'] = win32gui.GetWindowText(self.total_assets_hwnd) - return data - - - @staticmethod - def project_copy_data(copy_data): - reader = StringIO(copy_data) - df = pd.read_csv(reader, sep='\t') - return df.to_dict('records') + def today_entrusts(self): + self._switch_left_menus(['查询[F4]', '当日委托']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + + @property + def today_trades(self): + self._switch_left_menus(['查询[F4]', '当日成交']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + + def buy(self, security, price, amount): + self._switch_left_menus(['买入[F1]']) + + return self.trade(security, price, amount) - def _read_clipboard(self): - for _ in range(15): + def sell(self, security, price, amount): + self._switch_left_menus(['卖出[F2]']) + + return self.trade(security, price, amount) + + @property + def cancel_entrusts(self): + self._refresh() + self._switch_left_menus(['撤单[F3]']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + + def cancel_entrust(self, entrust_no): + self._refresh() + for i, entrust in enumerate(self.cancel_entrusts): + if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: + self._cancel_entrust_by_double_click(i) + return self._handle_cancel_entrust_pop_dialog() + else: + return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} + + def auto_ipo(self): + self._switch_left_menus(['新股申购', '批量新股申购']) + + self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) + self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) + + return self._handle_auto_ipo_pop_dialog() + + def _handle_auto_ipo_pop_dialog(self): + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + title = self._get_pop_dialog_title() + if '提示信息' in title: + self._app.top_window().type_keys('%Y') + elif '提示' in title: + data = self._app.top_window().Static.window_text() + self._app.top_window()['确定'].click() + return {'message': data} + else: + data = self._app.top_window().Static.window_text() + self._app.top_window().close() + return {'message': 'unkown message: {}'.find(data)} + self._wait(0.1) + + def _click(self, control_id): + self._app.top_window().window( + control_id=control_id, + class_name='Button' + ).click() + + def trade(self, security, price, amount): + self._set_trade_params(security, price, amount) + + self._submit_trade() + + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + pop_title = self._get_pop_dialog_title() + if pop_title == '委托确认': + self._app.top_window().type_keys('%Y') + elif pop_title == '提示信息': + if '超出涨跌停' in self._app.top_window().Static.window_text(): + self._app.top_window().type_keys('%Y') + elif pop_title == '提示': + content = self._app.top_window().Static.window_text() + if '成功' in content: + entrust_no = self._extract_entrust_id(content) + self._app.top_window()['确定'].click() + return {'entrust_no': entrust_no} + else: + self._app.top_window()['确定'].click() + self._wait(0.05) + raise exceptions.TradeError(content) + else: + self._app.top_window().close() + self._wait(0.2) # wait next dialog display + + def _extract_entrust_id(self, content): + return re.search(r'\d+', content).group() + + def _submit_trade(self): + self._main.window( + control_id=self._config.TRADE_SUBMIT_CONTROL_ID, + class_name='Button' + ).click() + + def _get_pop_dialog_title(self): + return self._app.top_window().window( + control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID + ).window_text() + + def _set_trade_params(self, security, price, amount): + code = security[-6:] + + self._type_keys( + self._config.TRADE_SECURITY_CONTROL_ID, + code + ) + self._type_keys( + self._config.TRADE_PRICE_CONTROL_ID, + easyutils.round_price_by_code(price, code) + ) + self._type_keys( + self._config.TRADE_AMOUNT_CONTROL_ID, + str(int(amount)) + ) + + def _get_grid_data(self, control_id): + grid = self._app.top_window().window( + control_id=control_id, + class_name='CVirtualGridCtrl' + ) + grid.type_keys('^A^C') + return self._format_grid_data( + self._get_clipboard_data() + ) + + def _type_keys(self, control_id, text): + self._app.top_window().window( + control_id=control_id, + class_name='Edit' + ).type_keys(text) + + def _get_clipboard_data(self): + while True: try: - win32api.keybd_event(17, 0, 0, 0) - win32api.keybd_event(67, 0, 0, 0) - win32api.keybd_event(67, 0, win32con.KEYEVENTF_KEYUP, 0) - win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0) - time.sleep(0.2) - return pyperclip.paste() + return pywinauto.clipboard.GetData() except Exception as e: - log.error('open clipboard failed: {}, retry...'.format(e)) - time.sleep(1) - else: - raise Exception('read clipbord failed') + log.warning('{}, retry ......'.format(e)) - @staticmethod - def _set_foreground_window(hwnd): - shell = win32com.client.Dispatch('WScript.Shell') - shell.SendKeys('%') - win32gui.SetForegroundWindow(hwnd) + def _switch_left_menus(self, path, sleep=0.2): + self._get_left_menus_handle().get_item(path).click() + self._wait(sleep) - @property - def entrust(self): - return self.get_entrust() - - def get_entrust(self): - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.2) - self._set_foreground_window(self.entrust_list_hwnd) - time.sleep(0.2) - data = self._read_clipboard() - return self.project_copy_data(data) + def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): + self._app.top_window().type_keys(shortcut) + self._wait(sleep) + + @functools.lru_cache() + def _get_left_menus_handle(self): + while True: + try: + handle = self._app.top_window().window( + control_id=129, + class_name='SysTreeView32' + ) + # sometime can't find handle ready, must retry + handle.wait('ready', 2) + return handle + except: + pass + + def _format_grid_data(self, data): + df = pd.read_csv(io.StringIO(data), + delimiter='\t', + dtype=self._config.GRID_DTYPE, + na_filter=False, + ) + return df.to_dict('records') + + def _handle_cancel_entrust_pop_dialog(self): + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + title = self._get_pop_dialog_title() + if '提示信息' in title: + self._app.top_window().type_keys('%Y') + elif '提示' in title: + data = self._app.top_window().Static.window_text() + self._app.top_window()['确定'].click() + return {'message': data} + else: + data = self._app.top_window().Static.window_text() + self._app.top_window().close() + return {'message': 'unkown message: {}'.find(data)} + self._wait(0.2) + + def _cancel_entrust_by_double_click(self, row): + x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN + y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row + self._app.top_window().window( + control_id=self._config.COMMON_GRID_CONTROL_ID, + class_name='CVirtualGridCtrl' + ).double_click(coords=(x, y)) + + def _refresh(self): + self._switch_left_menus(['买入[F1]'], sleep=0.05) diff --git a/easytrader/xczqtrader.py b/easytrader/xczqtrader.py deleted file mode 100644 index 6550b207..00000000 --- a/easytrader/xczqtrader.py +++ /dev/null @@ -1,302 +0,0 @@ -# coding: utf-8 -from __future__ import division - -import json -import os -import random -import tempfile -import urllib - -import demjson -import requests -import six - -from . import helpers -from .log import log -from .webtrader import NotLoginError -from .webtrader import WebTrader - -# handle error: SSLError: [SSL: SSL_NEGATIVE_LENGTH] dh key too small (_ssl.c:720) -requests.packages.urllib3.disable_warnings() -requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += 'HIGH:!DH:!aNULL' - - -class XCZQTrader(WebTrader): - config_path = os.path.dirname(__file__) + '/config/xczq.json' - - def __init__(self): - super(XCZQTrader, self).__init__() - self.account_config = None - self.s = requests.session() - self.s.mount('https://', helpers.Ssl3HttpAdapter()) - - def login(self, throw=False): - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko' - } - self.s.headers.update(headers) - - self.s.get(self.config['login_page'], verify=False) - - - verify_code = self.handle_recognize_code() - if not verify_code: - return False - login_status, result = self.post_login_data(verify_code) - if login_status is False and throw: - raise NotLoginError(result) - return login_status - - def handle_recognize_code(self): - """获取并识别返回的验证码 - :return:失败返回 False 成功返回 验证码""" - # 获取验证码 - verify_code_response = self.s.get(self.config['verify_code_api'], params=dict(randomStamp=random.random())) - # 保存验证码 - image_path = os.path.join(tempfile.gettempdir(), 'vcode_%d' % os.getpid()) - with open(image_path, 'wb') as f: - f.write(verify_code_response.content) - - # 自动识别验证码, 如果自动识别验证码无法使用,可以切换成手动识别。 - verify_code = helpers.recognize_verify_code(image_path, 'xczq') - # 手动输入验证码 - # verify_code = helpers.input_verify_code_manual(image_path) - log.debug('verify code detect result: %s' % verify_code) - os.remove(image_path) - - ht_verify_code_length = 4 - if len(verify_code) != ht_verify_code_length: - return False - return verify_code - - def post_login_data(self, verify_code): - if six.PY2: - password = urllib.unquote(self.account_config['password']) - else: - password = urllib.parse.unquote(self.account_config['password']) - login_params = dict( - self.config['login'], - mac_addr=helpers.get_mac(), - account_content=self.account_config['account'], - password=password, - validateCode=verify_code - ) - login_response = self.s.post(self.config['login_api'], params=login_params) - log.debug('login response: %s' % login_response.text) - - if login_response.text.find('湘财证券股份有限公司') != -1: - # v = login_response.headers - # self.sessionid = v['Set-Cookie'][11:43] - return True, None - return False, login_response.text - - def cancel_entrust(self, entrust_no, stock_code): - """撤单 - :param entrust_no: 委托单号 - :param stock_code: 股票代码""" - cancel_params = dict( - self.config['cancel_entrust'], - entrust_no=entrust_no, - stock_code=stock_code - ) - return self.do(cancel_params) - - @property - def can_cancel(self): - return self.get_can_cancel() - - def get_can_cancel(self): - """获取可撤单的股票""" - """ - [{ - 'bs_name' : '买入', - 'business_amount': '0', - 'entrust_amount': '委托数量', - 'entrust_price': '价格', - 'entrust_prop_name': '买卖', - 'entrust_time': '时间' - 'position_str': '定位串', - 'status_name': '已报', - 'stock_code': '证券代码', - 'stock_name': '证券名称'}] - """ - - return self.do(self.config['can_cancel']) - - @property - def current_deal(self): - return self.get_current_deal() - - def get_current_deal(self): - """获取当日成交列表""" - """ - [{'business_amount': '成交数量', - 'business_price': '成交价格', - 'entrust_amount': '委托数量', - 'entrust_bs': '买卖方向', - 'stock_account': '证券帐号', - 'fund_account': '资金帐号', - 'position_str': '定位串', - 'business_status': '成交状态', - 'date': '发生日期', - 'business_type': '成交类别', - 'business_time': '成交时间', - 'stock_code': '证券代码', - 'stock_name': '证券名称'}] - """ - return self.do(self.config['current_deal']) - - # TODO: 实现买入卖出的各种委托类型 - def buy(self, stock_code, price, amount=0, volume=0, order_type='limit'): - """买入卖出股票 - :param stock_code: 股票代码 - :param price: 卖出价格 - :param amount: 卖出股数 - :param volume: 卖出总金额 由 volume / price 取整, 若指定 price 则此参数无效 - :param order_type: 委托类型,默认为limit - 限价委托, 也可以为makret - 最优五档即时成交剩余转限价。 - """ - if order_type == 'limit': - entrust_prop = 0 - params = dict( - self.config['buy'], - entrust_bs=1, # 买入1 卖出2 - entrust_amount=amount if amount else volume // price // 100 * 100 - ) - elif order_type == 'market': - entrust_prop = 'R' - params = dict( - self.config['buymarket'], - entrust_bs=1, # 买入1 卖出2 - entrust_amount=amount if amount else volume // price // 100 * 100 - ) - else: - log.debug('订单类型不支持: %s' % (order_type)) - return None - return self.__trade(stock_code, price, entrust_prop=entrust_prop, other=params) - - def sell(self, stock_code, price, amount=0, volume=0, entrust_prop=0): - """卖出股票 - :param stock_code: 股票代码 - :param price: 卖出价格 - :param amount: 卖出股数 - :param volume: 卖出总金额 由 volume / price 取整, 若指定 amount 则此参数无效 - :param entrust_prop: 委托类型,暂未实现,默认为限价委托 - """ - params = dict( - self.config['sell'], - entrust_bs=2, # 买入1 卖出2 - entrust_amount=amount if amount else volume // price - ) - return self.__trade(stock_code, price, entrust_prop=entrust_prop, other=params) - - def get_ipo_limit(self, stock_code): - """ - 查询新股申购额度申购上限 - :param stock_code: 申购代码!!! - :return: high_amount(最高申购股数) enable_amount(申购额度) last_price(发行价) - """ - need_info = self.__get_trade_need_info(stock_code) - params = dict( - self.config['ipo_enable_amount'], - timestamp=random.random(), - stock_account=need_info['stock_account'], # '沪深帐号' - exchange_type=need_info['exchange_type'], # '沪市1 深市2' - entrust_prop=0, - stock_code=stock_code - ) - data = self.do(params) - if 'error_no' in data.keys() and data['error_no'] != "0": - log.debug('查询错误: %s' % (data['error_info'])) - return None - return dict(high_amount=float(data['high_amount']), enable_amount=data['enable_amount'], - last_price=float(data['last_price'])) - - def __trade(self, stock_code, price, entrust_prop, other): - # 检查是否已经掉线 - if not self.heart_thread.is_alive(): - check_data = self.get_balance() - if type(check_data) == dict: - return check_data - need_info = self.__get_trade_need_info(stock_code) - return self.do(dict( - other, - stock_account=need_info['stock_account'], # '沪深帐号' - exchange_type=need_info['exchange_type'], # '沪市1 深市2' - entrust_prop=entrust_prop, # 委托方式 - stock_code='{:0>6}'.format(stock_code), # 股票代码, 右对齐宽为6左侧填充0 - elig_riskmatch_flag=1, # 用户风险等级 - entrust_price=price, - )) - - def __get_trade_need_info(self, stock_code): - """获取股票对应的证券市场和帐号""" - # 获取股票对应的证券市场 - sh_exchange_type = 1 - sz_exchange_type = 2 - exchange_type = sh_exchange_type if helpers.get_stock_type(stock_code) == 'sh' else sz_exchange_type - # 获取股票对应的证券帐号 - if not hasattr(self, 'exchange_stock_account'): - self.exchange_stock_account = dict() - if exchange_type not in self.exchange_stock_account: - stock_account_index = 0 - response_data = self.do(dict( - self.config['account4stock'], - exchange_type=exchange_type, - stock_code=stock_code - ))[stock_account_index] - self.exchange_stock_account[exchange_type] = response_data['stock_account'] - return dict( - exchange_type=exchange_type, - stock_account=self.exchange_stock_account[exchange_type] - ) - - def create_basic_params(self): - basic_params = dict( - timestamp=random.random(), - ) - return basic_params - - def request(self, params): - r = self.s.get(self.trade_prefix, params=params) - return r.text - - def format_response_data(self, data): - # 获取 returnJSON - return_json = json.loads(data)['returnJson'] - raw_json_data = demjson.decode(return_json) - fun_data = raw_json_data['Func%s' % raw_json_data['function_id']] - header_index = 1 - remove_header_data = fun_data[header_index:] - return self.format_response_data_type(remove_header_data) - - def fix_error_data(self, data): - error_index = 0 - return data[error_index] if type(data) == list and data[error_index].get('error_no') is not None else data - - def check_login_status(self, return_data): - if hasattr(return_data, 'get') and return_data.get('error_no') == '-1': - raise NotLoginError - - def check_account_live(self, response): - if hasattr(response, 'get') and response.get('error_no') == '-1': - self.heart_active = False - - @property - def exchangebill(self): - start_date, end_date = helpers.get_30_date() - return self.get_exchangebill(start_date, end_date) - - def get_exchangebill(self, start_date, end_date): - """ - 查询指定日期内的交割单 - :param start_date: 20160211 - :param end_date: 20160211 - :return: - """ - params = self.config['exchangebill'].copy() - params.update({ - "start_date": start_date, - "end_date": end_date, - }) - return self.do(params) - diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index e137786e..07281785 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -15,33 +15,24 @@ from . import exceptions from . import helpers -from .config import client +from .clienttrader import ClientTrader from .log import log -class YHClientTrader(): - BROKER = 'yh' - - def __init__(self): - self._config = client.create(self.BROKER) +class YHClientTrader(ClientTrader): + @property + def broker_type(self): + return 'yh' - def prepare(self, config_path=None, user=None, password=None, exe_path=r'C:\中国银河证券双子星3.2\Binarystar.exe'): + def login(self, user, password, exe_path, comm_password=None, **kwargs): """ - 登陆银河客户端 - :param config_path: 银河登陆配置文件,跟参数登陆方式二选一 - :param user: 银河账号 - :param password: 银河明文密码 - :param exe_path: 银河客户端路径 + 登陆客户端 + :param user: 账号 + :param password: 明文密码 + :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' + :param comm_password: 通讯密码, 华泰需要,可不设 :return: """ - - if config_path is not None: - account = helpers.file2dict(config_path) - user = account['user'] - password = account['password'] - self.login(user, password, exe_path) - - def login(self, user, password, exe_path): try: self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) except Exception: diff --git a/mkdocs.yml b/mkdocs.yml index 3e8fe06c..ce98f4a8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,5 +5,4 @@ pages: - [usage.md, 使用] - [help.md, 常见问题] - [other/xueqiu.md, 其他, '雪球模拟组合说明'] -- [other/INSTALL4Windows.md, 其他, 'windows下配置广发'] theme: readthedocs diff --git a/test_easytrader.py b/test_easytrader.py index f08fd998..a1834bc3 100644 --- a/test_easytrader.py +++ b/test_easytrader.py @@ -9,10 +9,16 @@ import easytrader +TEST_CLIENTS = os.environ.get('EZ_TEST_CLIENTS', 'yh') + +@unittest.skipUnless('yh' in TEST_CLIENTS, 'skip yh test') class TestYhClientTrader(unittest.TestCase): @classmethod def setUpClass(cls): + if 'yh' not in TEST_CLIENTS: + return + # input your test account and password cls._ACCOUNT = os.environ.get('EZ_TEST_YH_ACCOUNT') or 'your account' cls._PASSWORD = os.environ.get('EZ_TEST_YH_password') or 'your password' @@ -47,5 +53,49 @@ def test_invalid_sell(self): def test_auto_ipo(self): self._user.auto_ipo() + +@unittest.skipUnless('ht' in TEST_CLIENTS, 'skip ht test') +class TestHTClientTrader(unittest.TestCase): + @classmethod + def setUpClass(cls): + if 'ht' not in TEST_CLIENTS: + return + + # input your test account and password + cls._ACCOUNT = os.environ.get('EZ_TEST_HT_ACCOUNT') or 'your account' + cls._PASSWORD = os.environ.get('EZ_TEST_HT_password') or 'your password' + cls._COMM_PASSWORD = os.environ.get('EZ_TEST_HT_comm_password') or 'your comm password' + + cls._user = easytrader.use('ht_client') + cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD) + + def test_balance(self): + time.sleep(3) + result = self._user.balance + + def test_today_entrusts(self): + result = self._user.today_entrusts + + def test_today_trades(self): + result = self._user.today_trades + + def test_cancel_entrusts(self): + result = self._user.cancel_entrusts + + def test_cancel_entrust(self): + result = self._user.cancel_entrust('123456789') + + def test_invalid_buy(self): + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.buy('511990', 1, 1e10) + + def test_invalid_sell(self): + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.sell('162411', 200, 1e10) + + def test_auto_ipo(self): + self._user.auto_ipo() + + if __name__ == '__main__': unittest.main() From 6e7aa8a8d590e5d1487921fb636239404fd06233 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 9 Sep 2017 19:36:15 +0800 Subject: [PATCH 030/276] buy/sell can accpet kwargs for compatibility --- easytrader/__init__.py | 2 +- easytrader/clienttrader.py | 4 ++-- easytrader/ht_clienttrader.py | 4 ++-- easytrader/yh_clienttrader.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 9afa322a..b9408712 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.0' +__version__ = '0.12.1' __author__ = 'shidenggui' diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 5006a136..0caa841e 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -68,11 +68,11 @@ def cancel_entrust(self, entrust_no): pass @abstractmethod - def buy(self, security, price, amount): + def buy(self, security, price, amount, **kwargs): pass @abstractmethod - def sell(self, security, price, amount): + def sell(self, security, price, amount, **kwargs): pass def auto_ipo(self): diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 48a8b589..21072689 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -113,12 +113,12 @@ def today_trades(self): return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - def buy(self, security, price, amount): + def buy(self, security, price, amount, **kwargs): self._switch_left_menus(['买入[F1]']) return self.trade(security, price, amount) - def sell(self, security, price, amount): + def sell(self, security, price, amount, **kwargs): self._switch_left_menus(['卖出[F2]']) return self.trade(security, price, amount) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 07281785..5b952e0f 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -118,12 +118,12 @@ def today_trades(self): return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - def buy(self, security, price, amount): + def buy(self, security, price, amount, **kwargs): self._switch_left_menus(['买入[F1]']) return self.trade(security, price, amount) - def sell(self, security, price, amount): + def sell(self, security, price, amount, **kwargs): self._switch_left_menus(['卖出[F2]']) return self.trade(security, price, amount) From 43a0e6ddd44f1264d61c43918097b6f057115aea Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 10 Sep 2017 16:02:39 +0800 Subject: [PATCH 031/276] update README.md --- README.md | 15 +++++++-------- docs/index.md | 7 +++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 11a165cf..4df6ed01 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ * 支持跟踪 `joinquant`, `ricequant` 的模拟交易 * 支持跟踪 雪球组合 调仓 * 支持命令行调用,方便其他语言适配 -* 支持 Python3 / Python2, Linux / Win, 推荐使用 `Python3` +* 支持 Python3, Linux / Win, 推荐使用 `Python3` * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 * 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) 或者 银河开户可以加群找我 @@ -20,19 +20,18 @@ ### 相关 -[量化交流论坛](http://www.celuetan.com) - [获取新浪免费实时行情的类库: easyquotation](https://github.com/shidenggui/easyquotation) [简单的股票量化交易框架 使用 easytrader 和 easyquotation](https://github.com/shidenggui/easyquant) - ### 支持券商 -* 银河 -* 广发 -* 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 -* 湘财证券 +* 银河客户端, 须在 `windows` 平台下载 `银河双子星` 客户端 +* 华泰客户端(同花顺版本) + +### 实盘易 + +如果有对其他券商或者通达信版本的需求,可以查看 [实盘易](http://6du.in/0s15Iru) ### 模拟交易 diff --git a/docs/index.md b/docs/index.md index d753a699..8fb97f87 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,11 @@ * 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 * 华泰客户端(同花顺版本) + +### 实盘易 + +如果有对其他券商或者通达信版本的需求,可以查看 [实盘易](http://6du.in/0s15Iru) + ### 模拟交易 * 雪球组合 by @[haogefeifei](https://github.com/haogefeifei)([说明](other/xueqiu.md)) @@ -29,8 +34,6 @@ ### 相关 -[量化交流论坛](http://www.celuetan.com) - [获取新浪免费实时行情的类库: easyquotation](https://github.com/shidenggui/easyquotation) [简单的股票量化交易框架 使用 easytrader 和 easyquotation](https://github.com/shidenggui/easyquant) From 876c4f707d4ffc5001847e5414ea6221c73d3cec Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 10 Sep 2017 17:43:08 +0800 Subject: [PATCH 032/276] update docs --- docs/install.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 838e5bc2..8e816324 100644 --- a/docs/install.md +++ b/docs/install.md @@ -4,9 +4,8 @@ * `tesseract` : 非 `pytesseract`, 需要单独安装, [地址](https://github.com/tesseract-ocr/tesseract/wiki),保证在命令行下 `tesseract` 可用 -##### 银河客户端设置 +##### 客户端设置 -* 系统设置 > 快速交易: 关闭所有的买卖,撤单等确认选项 * 系统设置 > 界面设置: 界面不操作超时时间设为 0 * 系统设置 > 交易设置: 默认买入价格/买入数量/卖出价格/卖出数量 都设置为 空 From 01fcdea6671b892496c45cc7a87ae6608f8313a9 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 10 Sep 2017 17:52:16 +0800 Subject: [PATCH 033/276] use self._main replace self._app.top_window() fix some time cant find right mian window --- easytrader/ht_clienttrader.py | 2 +- easytrader/yh_clienttrader.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 21072689..6a1d5cd7 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -225,7 +225,7 @@ def _set_trade_params(self, security, price, amount): ) def _get_grid_data(self, control_id): - grid = self._app.top_window().window( + grid = self._main.window( control_id=control_id, class_name='CVirtualGridCtrl' ) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 5b952e0f..6b0e7e0d 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -204,7 +204,7 @@ def _extract_entrust_id(self, content): def _submit_trade(self): time.sleep(0.05) - self._app.top_window().window( + self._main.window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name='Button' ).click() @@ -231,7 +231,7 @@ def _set_trade_params(self, security, price, amount): ) def _get_grid_data(self, control_id): - grid = self._app.top_window().window( + grid = self._main.window( control_id=control_id, class_name='CVirtualGridCtrl' ) From 77abb3bf4abbe4c4279fc7fe0af98cd06f23f285 Mon Sep 17 00:00:00 2001 From: Yu Ling Date: Sun, 10 Sep 2017 19:26:56 +0800 Subject: [PATCH 034/276] =?UTF-8?q?pywinauto=E9=87=8D=E6=9E=84gj=5Fclientt?= =?UTF-8?q?rader=20(#231)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add gj_clienttrader * update gj_clienttrader with yh_clienttrader * add demo json * update gj trader with pywinauto * fixx login bug --- easytrader/config/client.py | 41 ++++ easytrader/gj_clienttrader.py | 415 +++++++--------------------------- 2 files changed, 117 insertions(+), 339 deletions(-) diff --git a/easytrader/config/client.py b/easytrader/config/client.py index daf948da..504a2855 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -5,6 +5,8 @@ def create(broker): return YH elif broker == 'ht': return HT + elif broker == 'gj': + return GJ raise NotImplemented @@ -86,3 +88,42 @@ class HT: AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 AUTO_IPO_BUTTON_CONTROL_ID = 1006 + +class GJ: + DEFAULT_EXE_PATH = 'C:\\全能行证券交易终端\\xiadan.exe' + TITLE = '网上股票交易系统5.0' + + TRADE_SECURITY_CONTROL_ID = 1032 + TRADE_PRICE_CONTROL_ID = 1033 + TRADE_AMOUNT_CONTROL_ID = 1034 + + TRADE_SUBMIT_CONTROL_ID = 1006 + + COMMON_GRID_CONTROL_ID = 1047 + BALANCE_GRID_CONTROL_ID = 1047 + + POP_DIALOD_TITLE_CONTROL_ID = 1365 + + GRID_DTYPE = { + '操作日期': str, + '委托编号': str, + '申请编号': str, + '合同编号': str, + '证券代码': str, + '股东代码': str, + '资金帐号': str, + '资金帐户': str, + '发生日期': str + } + + CANCEL_ENTRUST_ENTRUST_FIELD = '合同编号' + CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50 + CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30 + CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16 + + AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 + AUTO_IPO_BUTTON_CONTROL_ID = 1006 + + ENABLE_BALANCE_TEXT_ID = 0x3f8 + TOTAL_BALANCE_TEXT_ID = 0x3f7 + \ No newline at end of file diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index ab734260..e9e16377 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -1,358 +1,95 @@ # coding:utf8 from __future__ import division -import sys + +import functools +import io import os -import subprocess +import re import tempfile import time -import traceback -import win32api -import win32gui -from io import StringIO - -import pandas as pd -import pyperclip -import win32com.client -import win32con -from PIL import ImageGrab +import sys +import easyutils +# import pandas as pd +import pywinauto +import pywinauto.clipboard +from . import exceptions from . import helpers +from .yh_clienttrader import YHClientTrader from .log import log -def findWindowSubwindowEqualText(tt): - hwnds = [] - def findSub(hwnd,param): - if win32gui.FindWindowEx(hwnd,0,None,tt)!=0: - param.append(hwnd) - win32gui.EnumWindows(findSub, hwnds) - return hwnds - -class GJClientTrader(): - def __init__(self): - self.LoginTitle = ['用户登录'] - self.Title = '网上股票交易系统5.0' +class GJClientTrader(YHClientTrader): + @property + def broker_type(self): + return 'gj' - def prepare(self, config_path=None, user=None, password=None, exe_path='C:\\全能行证券交易终端\\xiadan.exe'): + def login(self, user, password, exe_path, comm_password=None, **kwargs): """ - 登陆银河客户端 - :param config_path: 银河登陆配置文件,跟参数登陆方式二选一 - :param user: 银河账号 - :param password: 银河明文密码 - :param exe_path: 银河客户端路径 + 登陆客户端 + :param user: 账号 + :param password: 明文密码 + :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' + :param comm_password: 通讯密码, 华泰需要,可不设 :return: """ - if config_path is not None: - account = helpers.file2dict(config_path) - user = account['user'] - password = account['password'] - self.login(user, password, exe_path) - - def login(self, user, password, exe_path): - if self._has_main_window(): - self._get_handles() - log.info('检测到交易客户端已启动,连接完毕') - return - if not self._has_login_window(): - if not os.path.exists(exe_path): - raise FileNotFoundError('在 {} 未找到应用程序,请用 exe_path 指定应用程序目录'.format(exe_path)) - subprocess.Popen(exe_path) - # 检测登陆窗口 - for _ in range(30): - if self._has_login_window(): - break - time.sleep(1) - else: - raise Exception('启动客户端失败,无法检测到登陆窗口') - log.info('成功检测到客户端登陆窗口') - - # 登陆 - # self._set_trade_mode() - self._set_login_name(user) - self._set_login_password(password) - for _ in range(10): - self._set_login_verify_code() - self._click_login_button() - time.sleep(3) - self._check_verify_code_wrong() - if not self._has_login_window(): - log.info('no login window, login success') - break - self._click_login_verify_code() - - for _ in range(60): - if self._has_main_window(): - self._get_handles() - break - time.sleep(1) - else: - raise Exception('启动交易客户端失败') - log.info('客户端登陆成功') - - def _set_login_verify_code(self): - verify_code_image = self._grab_verify_code() - image_path = tempfile.mktemp() + '.jpg' - verify_code_image.save(image_path) - result = helpers.recognize_verify_code(image_path, 'gj_client') - time.sleep(0.2) - self._input_login_verify_code(result) - time.sleep(0.4) - - def _set_trade_mode(self): - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x4f4d) - win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) - - def _set_login_name(self, user): - time.sleep(0.5) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F3) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, user) - - def _set_login_password(self, password): - time.sleep(0.5) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F4) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, password) - - def _has_login_window(self): - for title in self.LoginTitle: - self.login_hwnd = win32gui.FindWindow(None, title) - if self.login_hwnd != 0: - return True - return False - - def _input_login_verify_code(self, code): - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3eb) - win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, code) - - def _click_login_verify_code(self): - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5db) - rect = win32gui.GetWindowRect(input_hwnd) - self._mouse_click(rect[0] + 5, rect[1] + 5) - - @staticmethod - def _mouse_click(x, y): - win32api.SetCursorPos((x, y)) - win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) - win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) - - def _click_login_button(self): - time.sleep(1) - input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3ee) - win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) - - def _check_verify_code_wrong(self): - hwnds = findWindowSubwindowEqualText("提示") - if len(hwnds)==0: - log.info('Right verify code') - elif len(hwnds)==1: - win32gui.SetForegroundWindow(hwnds[0]) - button = win32gui.FindWindowEx(hwnds[0],0,None,"确定") - win32gui.SendMessage(button, win32con.BM_CLICK, None, None) - log.info('Wrong verify code') - else: - raise Exception("_check_verify_code_wrong too many windows %d"%len(hwnds)) - - def _has_main_window(self): - try: - self._get_handles() - except: - return False - return True - - def _grab_verify_code(self): - verify_code_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5db) - self._set_foreground_window(self.login_hwnd) - time.sleep(1) - rect = win32gui.GetWindowRect(verify_code_hwnd) - return ImageGrab.grab(rect) - - def _get_handles(self): - trade_main_hwnd = win32gui.FindWindow(0, self.Title) # 交易窗口 - operate_frame_hwnd = win32gui.GetDlgItem(trade_main_hwnd, 59648) # 操作窗口框架 - operate_frame_afx_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59648) # 操作窗口框架 - hexin_hwnd = win32gui.GetDlgItem(operate_frame_afx_hwnd, 129) - scroll_hwnd = win32gui.GetDlgItem(hexin_hwnd, 200) # 左部折叠菜单控件 - tree_view_hwnd = win32gui.GetDlgItem(scroll_hwnd, 129) # 左部折叠菜单控件 - - # 获取委托窗口所有控件句柄 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F1, 0) - time.sleep(0.5) - - # 买入相关 - entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 - self.buy_stock_code_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1032) # 买入代码输入框 - self.buy_price_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1033) # 买入价格输入框 - self.buy_amount_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1034) # 买入数量输入框 - self.buy_btn_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1006) # 买入确认按钮 - self.refresh_entrust_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 32790) # 刷新持仓按钮 - entrust_frame_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1047) # 持仓显示框架 - entrust_sub_frame_hwnd = win32gui.GetDlgItem(entrust_frame_hwnd, 200) # 持仓显示框架 - self.position_list_hwnd = win32gui.GetDlgItem(entrust_sub_frame_hwnd, 1047) # 持仓列表 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F2, 0) - time.sleep(0.5) - - # 卖出相关 - sell_entrust_frame_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 - self.sell_stock_code_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1032) # 卖出代码输入框 - self.sell_price_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1033) # 卖出价格输入框 - self.sell_amount_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1034) # 卖出数量输入框 - self.sell_btn_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1006) # 卖出确认按钮 - - # 撤单窗口 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F3, 0) - time.sleep(0.5) - cancel_entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 撤单窗口框架 - self.cancel_stock_code_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3348) # 卖出代码输入框 - self.cancel_query_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3349) # 查询代码按钮 - self.cancel_buy_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30002) # 撤买 - self.cancel_sell_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30003) # 撤卖 - - chexin_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 1047) - chexin_sub_hwnd = win32gui.GetDlgItem(chexin_hwnd, 200) - self.entrust_list_hwnd = win32gui.GetDlgItem(chexin_sub_hwnd, 1047) # 委托列表 - - # 资金股票 - win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F4, 0) - time.sleep(0.5) - self.capital_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 资金股票窗口框架 - - def balance(self): - return self.get_balance() - - def get_balance(self): - self._set_foreground_window(self.capital_window_hwnd) - time.sleep(0.3) - data = self._read_clipboard() - return self.project_copy_data(data)[0] - - def buy(self, stock_code, price, amount, **kwargs): - """ - 买入股票 - :param stock_code: 股票代码 - :param price: 买入价格 - :param amount: 买入股数 - :return: bool: 买入信号是否成功发出 - """ - amount = str(amount // 100 * 100) - price = str(price) - try: - win32gui.SendMessage(self.buy_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入买入代码 - win32gui.SendMessage(self.buy_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入买入价格 - time.sleep(0.2) - win32gui.SendMessage(self.buy_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入买入数量 - time.sleep(0.2) - win32gui.SendMessage(self.buy_btn_hwnd, win32con.BM_CLICK, None, None) # 买入确定 - time.sleep(0.3) - except: - traceback.print_exc() - return False - return True - - def sell(self, stock_code, price, amount, **kwargs): - """ - 买出股票 - :param stock_code: 股票代码 - :param price: 卖出价格 - :param amount: 卖出股数 - :return: bool 卖出操作是否成功 - """ - amount = str(amount // 100 * 100) - price = str(price) - - try: - win32gui.SendMessage(self.sell_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入卖出代码 - win32gui.SendMessage(self.sell_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入卖出价格 - win32gui.SendMessage(self.sell_price_hwnd, win32con.BM_CLICK, None, None) # 输入卖出价格 - time.sleep(0.2) - win32gui.SendMessage(self.sell_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入卖出数量 - time.sleep(0.2) - win32gui.SendMessage(self.sell_btn_hwnd, win32con.BM_CLICK, None, None) # 卖出确定 - time.sleep(0.3) - except: - traceback.print_exc() - return False - return True - - def cancel_entrust(self, stock_code, direction): - """ - 撤单 - :param stock_code: str 股票代码 - :param direction: str 'buy' 撤买, 'sell' 撤卖 - :return: bool 撤单信号是否发出 - """ - direction = 0 if direction == 'buy' else 1 - - try: - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.2) - win32gui.SendMessage(self.cancel_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入撤单 - win32gui.SendMessage(self.cancel_query_hwnd, win32con.BM_CLICK, None, None) # 查询代码 - time.sleep(0.2) - if direction == 0: - win32gui.SendMessage(self.cancel_buy_hwnd, win32con.BM_CLICK, None, None) # 撤买 - elif direction == 1: - win32gui.SendMessage(self.cancel_sell_hwnd, win32con.BM_CLICK, None, None) # 撤卖 - except: - traceback.print_exc() - return False - time.sleep(0.3) - return True + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) + except Exception: + self._app = pywinauto.Application().start(exe_path) + + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait('ready') + break + except RuntimeError: + pass + + self._app.top_window().Edit1.type_keys(user) + self._app.top_window().Edit2.type_keys(password) + edit3 = self._app.top_window().window(control_id=0x3eb) + while True: + try: + code = self._handle_verify_code() + print('verify code=',code) + edit3.type_keys( + code + ) + time.sleep(1) + self._app.top_window()['确定(Y)'].click() + # detect login is success or not + try: + self._app.top_window().wait_not('exists', 5) + break + except: + self._app.top_window()['确定'].click() + pass + except Exception as e: + print("Exception,",e) + pass + print('connect start') + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) + print('connect end') + self._main = self._app.top_window() + + def _handle_verify_code(self): + control = self._app.top_window().window(control_id=0x5db) + control.click() + time.sleep(0.2) + file_path = tempfile.mktemp()+'.jpg' + control.capture_as_image().save(file_path) + time.sleep(0.2) + vcode = helpers.recognize_verify_code(file_path, 'gj_client') + return ''.join(re.findall('[a-zA-Z0-9]+', vcode)) @property - def position(self): - return self.get_position() - - def get_position(self): - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.1) - self._set_foreground_window(self.position_list_hwnd) - time.sleep(0.1) - data = self._read_clipboard() - return self.project_copy_data(data) - - @staticmethod - def project_copy_data(copy_data): - reader = StringIO(copy_data) - df = pd.read_csv(reader, sep = '\t') - return df.to_dict('records') - - def _read_clipboard(self): - for _ in range(15): - try: - win32api.keybd_event(17, 0, 0, 0) - win32api.keybd_event(67, 0, 0, 0) - win32api.keybd_event(67, 0, win32con.KEYEVENTF_KEYUP, 0) - win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0) - time.sleep(0.2) - return pyperclip.paste() - except Exception as e: - log.error('open clipboard failed: {}, retry...'.format(e)) - time.sleep(1) - else: - raise Exception('read clipbord failed') - - @staticmethod - def _project_position_str(raw): - reader = StringIO(raw) - df = pd.read_csv(reader, sep = '\t') - return df + def balance(self): + self._switch_left_menus(['查询[F4]', '资金股票']) + retv={} + retv['enable_balance'] = self._app.top_window().window(control_id=0x3f8).window_text() + retv['total_balance'] = self._app.top_window().window(control_id=0x3f7).window_text() + return [retv] - @staticmethod - def _set_foreground_window(hwnd): - import pythoncom - pythoncom.CoInitialize() - shell = win32com.client.Dispatch('WScript.Shell') - shell.SendKeys('%') - win32gui.SetForegroundWindow(hwnd) - @property - def entrust(self): - return self.get_entrust() - - def get_entrust(self): - win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 - time.sleep(0.2) - self._set_foreground_window(self.entrust_list_hwnd) - time.sleep(0.2) - data = self._read_clipboard() - return self.project_copy_data(data) + \ No newline at end of file From 0e5425572b2d84b66dc676ae7c5a31624927cc0b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 10 Sep 2017 18:00:27 +0800 Subject: [PATCH 035/276] Revert "use self._main replace self._app.top_window() fix some time cant find right mian window" This reverts commit 01fcdea6671b892496c45cc7a87ae6608f8313a9. --- easytrader/ht_clienttrader.py | 2 +- easytrader/yh_clienttrader.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 6a1d5cd7..21072689 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -225,7 +225,7 @@ def _set_trade_params(self, security, price, amount): ) def _get_grid_data(self, control_id): - grid = self._main.window( + grid = self._app.top_window().window( control_id=control_id, class_name='CVirtualGridCtrl' ) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 6b0e7e0d..5b952e0f 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -204,7 +204,7 @@ def _extract_entrust_id(self, content): def _submit_trade(self): time.sleep(0.05) - self._main.window( + self._app.top_window().window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name='Button' ).click() @@ -231,7 +231,7 @@ def _set_trade_params(self, security, price, amount): ) def _get_grid_data(self, control_id): - grid = self._main.window( + grid = self._app.top_window().window( control_id=control_id, class_name='CVirtualGridCtrl' ) From 7bbd1fbfcf157e0bac17936e842c2fafcc838a3a Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 10 Sep 2017 20:27:45 +0800 Subject: [PATCH 036/276] add more sleep when close after start dialog to get proper top_window handle --- easytrader/__init__.py | 2 +- easytrader/ht_clienttrader.py | 1 + easytrader/yh_clienttrader.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index b9408712..e5ff9162 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.1' +__version__ = '0.12.2' __author__ = 'shidenggui' diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 21072689..48b01552 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -77,6 +77,7 @@ def _close_prompt_windows(self): for w in self._app.windows(class_name='#32770'): if w.window_text() != self._config.TITLE: w.close() + self._wait(1) @property def balance(self): diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 5b952e0f..9c68b47f 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -93,6 +93,7 @@ def _close_prompt_windows(self): for w in self._app.windows(class_name='#32770'): if w.window_text() != self._config.TITLE: w.close() + self._wait(1) @property def balance(self): From 03e70530b559ebb55a41ebe80bbec7c6018a6a3c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 11 Sep 2017 08:41:00 +0800 Subject: [PATCH 037/276] add issue template --- .github/ISSUE_TEMPLATE.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..27e0269b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ +## env + +OS: win / mac / linux +PYTHON_VERSION: 3.6 +EASYTRADER_VERSION: 0.xx.xx + +## problem + + From 994fce1428e05a9a0780e028a9fc5e494b91f971 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 11 Sep 2017 08:44:24 +0800 Subject: [PATCH 038/276] alter issue template --- .github/ISSUE_TEMPLATE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 27e0269b..173c9c67 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,9 +1,12 @@ ## env OS: win / mac / linux -PYTHON_VERSION: 3.6 +PYTHON_VERSION: 3.x EASYTRADER_VERSION: 0.xx.xx ## problem +## how to repeat + + From e91dc64ca96a95c82cc3d3f87ad192ed4762939c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 11 Sep 2017 09:18:47 +0800 Subject: [PATCH 039/276] fix some time top_window can't find proper element --- easytrader/__init__.py | 2 +- easytrader/clienttrader.py | 8 ++++++++ easytrader/gj_clienttrader.py | 2 +- easytrader/ht_clienttrader.py | 11 ++--------- easytrader/yh_clienttrader.py | 13 +++---------- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index e5ff9162..db9e8c7e 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.2' +__version__ = '0.12.3' __author__ = 'shidenggui' diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 0caa841e..5d99fd57 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -88,3 +88,11 @@ def _wait(self, seconds): def exit(self): self._app.kill() + + def _close_prompt_windows(self): + self._wait(1) + for w in self._app.windows(class_name='#32770'): + if w.window_text() != self._config.TITLE: + w.close() + self._wait(1) + diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index e9e16377..4ab32e11 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -71,7 +71,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): print('connect start') self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) print('connect end') - self._main = self._app.top_window() + self._main = self._app.window(title='网上股票交易系统5.0') def _handle_verify_code(self): control = self._app.top_window().window(control_id=0x5db) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 48b01552..d02cff19 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -59,7 +59,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) self._close_prompt_windows() - self._main = self._app.top_window() + self._main = self._app.window(title='网上股票交易系统5.0') def _run_exe_path(self, exe_path): return os.path.join( @@ -72,13 +72,6 @@ def _wait(self, seconds): def exit(self): self._app.kill() - def _close_prompt_windows(self): - self._wait(1) - for w in self._app.windows(class_name='#32770'): - if w.window_text() != self._config.TITLE: - w.close() - self._wait(1) - @property def balance(self): self._switch_left_menus(['查询[F4]', '资金股票']) @@ -89,7 +82,7 @@ def _get_balance_from_statics(self): result = {} for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): result[key] = float( - self._app.top_window().window( + self._main.window( control_id=control_id, class_name='Static', ).window_text() diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 9c68b47f..e3f0580b 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -65,7 +65,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) self._close_prompt_windows() - self._main = self._app.top_window() + self._main = self._app.window(title='网上股票交易系统5.0') def _run_exe_path(self, exe_path): return os.path.join( @@ -88,13 +88,6 @@ def _handle_verify_code(self): vcode = helpers.recognize_verify_code(file_path, 'yh_client') return ''.join(re.findall('\d+', vcode)) - def _close_prompt_windows(self): - self._wait(1) - for w in self._app.windows(class_name='#32770'): - if w.window_text() != self._config.TITLE: - w.close() - self._wait(1) - @property def balance(self): self._switch_left_menus(['查询[F4]', '资金股票']) @@ -232,7 +225,7 @@ def _set_trade_params(self, security, price, amount): ) def _get_grid_data(self, control_id): - grid = self._app.top_window().window( + grid = self._main.window( control_id=control_id, class_name='CVirtualGridCtrl' ) @@ -242,7 +235,7 @@ def _get_grid_data(self, control_id): ) def _type_keys(self, control_id, text): - self._app.top_window().window( + self._main.window( control_id=control_id, class_name='Edit' ).type_keys(text) From afc0c1aa008204674aaac1e030de116f5a33ef33 Mon Sep 17 00:00:00 2001 From: algony-tony Date: Fri, 22 Sep 2017 20:24:37 +0800 Subject: [PATCH 040/276] fix xqtrader (#235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修正雪球获取委托单中状态和价格的错误; 2. 删除登陆不需要的 telephone 和 remember_me; --- easytrader/xqtrader.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index f2798a1e..19d41f36 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -89,8 +89,8 @@ def post_login_data(self): login_post_data = { 'username': self.account_config.get('username', ''), 'areacode': '86', - 'telephone': self.account_config['account'], - 'remember_me': '0', + # 'telephone': self.account_config['account'], + # 'remember_me': '0', 'password': self.account_config['password'] } login_response = self.session.post(self.config['login_api'], data=login_post_data) @@ -219,7 +219,7 @@ def __get_xq_history(self): """ data = { "cube_symbol": str(self.account_config['portfolio_code']), - 'count': 5, + 'count': 20, 'page': 1 } r = self.session.get(self.config['history_url'], params=data) @@ -232,33 +232,35 @@ def history(self): def get_entrust(self): """ - 获取委托单(目前返回5次调仓的结果) + 获取委托单(目前返回20次调仓的结果) 操作数量都按1手模拟换算的 :return: """ xq_entrust_list = self.__get_xq_history() entrust_list = [] + replace_none = lambda s: s or 0 for xq_entrusts in xq_entrust_list: status = xq_entrusts['status'] # 调仓状态 if status == 'pending': status = "已报" - elif status == 'canceled': + elif status in ['canceled','failed']: status = "废单" else: status = "已成" for entrust in xq_entrusts['rebalancing_histories']: - volume = abs(entrust['target_weight'] - entrust['weight']) * self.multiple / 10000 + volume = abs(entrust['target_weight'] - replace_none(entrust['prev_weight'])) * self.multiple / 10000 + price = entrust['price'] entrust_list.append({ 'entrust_no': entrust['id'], - 'entrust_bs': u"买入" if entrust['target_weight'] > entrust['weight'] else u"卖出", + 'entrust_bs': u"买入" if entrust['target_weight'] > replace_none(entrust['prev_weight']) else u"卖出", 'report_time': self.__time_strftime(entrust['updated_at']), 'entrust_status': status, 'stock_code': entrust['stock_symbol'], 'stock_name': entrust['stock_name'], 'business_amount': 100, - 'business_price': volume, + 'business_price': price, 'entrust_amount': 100, - 'entrust_price': volume, + 'entrust_price': price, }) return entrust_list From be4f6daa2b10167812b0443df5b21783a4383a9c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 9 Oct 2017 16:23:43 +0800 Subject: [PATCH 041/276] fix: use self._main replace self._app.top_window() to stably get handle fix #243 --- easytrader/yh_clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index e3f0580b..b3162285 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -259,7 +259,7 @@ def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): def _get_left_menus_handle(self): while True: try: - handle = self._app.top_window().window( + handle = self._main.window( control_id=129, class_name='SysTreeView32' ) From 961459acd8b4cb98edb3bfdee3d2ebb484f43592 Mon Sep 17 00:00:00 2001 From: shidenggui Date: Sun, 29 Oct 2017 03:54:47 -0500 Subject: [PATCH 042/276] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4df6ed01..55ddbea7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ ### 实盘易 -如果有对其他券商或者通达信版本的需求,可以查看 [实盘易](http://6du.in/0s15Iru) +如果有对其他券商或者通达信版本的需求,可以查看 [实盘易](http://www.iguuu.com/e?x=19828) ### 模拟交易 From 8fa475b2e4e2d85804a39f3f73d3a70b2b5e239b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 8 Jan 2018 14:53:39 +0800 Subject: [PATCH 043/276] fix: switch window to normal window after login --- easytrader/ht_clienttrader.py | 2 +- easytrader/yh_clienttrader.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index d02cff19..b0c56ce5 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -55,7 +55,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().type_keys('%Y') # detect login is success or not - self._app.top_window().wait_not('exists', 2) + self._app.top_window().wait_not('exists', 10) self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) self._close_prompt_windows() diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index b3162285..4a1fcfee 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -58,7 +58,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): # detect login is success or not try: - self._app.top_window().wait_not('exists', 2) + self._app.top_window().wait_not('exists', 10) break except: pass From 79d142cc3f0d83f3f74408e97f62bd8c9bf090c7 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 8 Jan 2018 14:56:17 +0800 Subject: [PATCH 044/276] fix: fix sometime elementNotFound error --- easytrader/yh_clienttrader.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 4a1fcfee..104e25e7 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -66,6 +66,20 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) self._close_prompt_windows() self._main = self._app.window(title='网上股票交易系统5.0') + try: + self._main.window( + control_id=129, + class_name='SysTreeView32' + ).wait('ready', 2) + except: + self._wait(2) + self._switch_window_to_normal_mode() + + def _switch_window_to_normal_mode(self): + self._app.top_window().window( + control_id=32812, + class_name='Button' + ).click() def _run_exe_path(self, exe_path): return os.path.join( From 4429b77bd05c4759c79a95fb80acda1e6e0fa0ac Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 8 Jan 2018 15:24:58 +0800 Subject: [PATCH 045/276] update version --- easytrader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index db9e8c7e..694968ae 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.3' +__version__ = '0.12.4' __author__ = 'shidenggui' From 2947e54b0c26a34d3149f6a996d183d3a06949f5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 9 Jan 2018 17:36:57 +0800 Subject: [PATCH 046/276] feature: support remote controll client --- README.md | 1 + docs/index.md | 1 + docs/usage.md | 19 ++++++ easytrader/__init__.py | 2 +- easytrader/remoteclient.py | 94 +++++++++++++++++++++++++++ easytrader/server.py | 127 +++++++++++++++++++++++++++++++++++++ 6 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 easytrader/remoteclient.py create mode 100644 easytrader/server.py diff --git a/README.md b/README.md index 55ddbea7..916870f8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ * 实现自动登录 * 支持跟踪 `joinquant`, `ricequant` 的模拟交易 * 支持跟踪 雪球组合 调仓 +* 支持通过 webserver 远程操作客户端 * 支持命令行调用,方便其他语言适配 * 支持 Python3, Linux / Win, 推荐使用 `Python3` * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 diff --git a/docs/index.md b/docs/index.md index 8fb97f87..b71c6d36 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,7 @@ * 支持跟踪 `joinquant`, `ricequant` 的模拟交易 * 支持跟踪 雪球组合 调仓, 实盘雪球组合 * 支持命令行调用,方便其他语言适配 +* 支持通过 webserver 远程操作客户端 * 支持 Python3 , Linux / Win / Mac * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 * 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) 或者 银河开户可以加群找我 diff --git a/docs/usage.md b/docs/usage.md index 627040f6..6eb99d07 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -339,6 +339,25 @@ follower.follow(***, entrust_prop='market') follower.follow(***, send_interval=30) # 设置下单间隔为 30 s ``` +### 远端服务器模式 + +#### 在服务器上启动服务 + +```python +from easytrader import server + +server.run(port=1430) # 默认端口为 1430 +``` + +#### 远程客户端调用 + +```python +from easytrader import remoteclient + +user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client 等', host='服务器ip', port='服务器端口,默认为1430') + +其他用法同上 +``` ### 命令行模式 diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 694968ae..ede85904 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.4' +__version__ = '0.12.5' __author__ = 'shidenggui' diff --git a/easytrader/remoteclient.py b/easytrader/remoteclient.py new file mode 100644 index 00000000..d1d4b6c7 --- /dev/null +++ b/easytrader/remoteclient.py @@ -0,0 +1,94 @@ +# coding:utf8 +import requests + +from . import helpers + + +def use(broker, host, port=1430, **kwargs): + return RemoteClient(broker, host, port) + + +class RemoteClient: + def __init__(self, broker, host, port=1430, **kwargs): + self._s = requests.session() + self._api = 'http://{}:{}'.format(host, port) + self._broker = broker + + def prepare(self, config_path=None, user=None, password=None, exe_path=None, comm_password=None, + **kwargs): + """ + 登陆客户端 + :param config_path: 登陆配置文件,跟参数登陆方式二选一 + :param user: 账号 + :param password: 明文密码 + :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe' + :param comm_password: 通讯密码 + :return: + """ + params = locals().copy() + params.pop('self') + + if config_path is not None: + account = helpers.file2dict(config_path) + params['user'] = account['user'] + password['password'] = account['password'] + + params['broker'] = self._broker + + response = self._s.post(self._api + '/prepare', json=params) + if response.status_code >= 300: + raise Exception(response.json()['error']) + return response.json() + + @property + def balance(self): + return self.common_get('balance') + + @property + def position(self): + return self.common_get('position') + + @property + def today_entrusts(self): + return self.common_get('today_entrusts') + + @property + def today_trades(self): + return self.common_get('today_trades') + + @property + def cancel_entrusts(self): + return self.common_get('cancel_entrusts') + + def common_get(self, endpoint): + response = self._s.get(self._api + '/' + endpoint) + if response.status_code >= 300: + raise Exception(response.json()['error']) + return response.json() + + def buy(self, security, price, amount, **kwargs): + params = locals().copy() + params.pop('self') + + response = self._s.post(self._api + '/buy', json=params) + if response.status_code >= 300: + raise Exception(response.json()['error']) + return response.json() + + def sell(self, security, price, amount, **kwargs): + params = locals().copy() + params.pop('self') + + response = self._s.post(self._api + '/sell', json=params) + if response.status_code >= 300: + raise Exception(response.json()['error']) + return response.json() + + def cancel_entrust(self, entrust_no): + params = locals().copy() + params.pop('self') + + response = self._s.post(self._api + '/cancel_entrust', json=params) + if response.status_code >= 300: + raise Exception(response.json()['error']) + return response.json() diff --git a/easytrader/server.py b/easytrader/server.py new file mode 100644 index 00000000..e4463e9b --- /dev/null +++ b/easytrader/server.py @@ -0,0 +1,127 @@ +from flask import Flask, request, jsonify + +from . import api + +app = Flask(__name__) + +global_store = {} + + +@app.route('/prepare', methods=['POST']) +def post_prepare(): + json_data = request.get_json(force=True) + + try: + user = api.use(json_data.pop('broker')) + user.prepare(**json_data) + except Exception as e: + return jsonify({'error': str(e)}), 400 + + global_store['user'] = user + return jsonify({'msg': 'login success'}), 201 + + +@app.route('/balance', methods=['GET']) +def get_balance(): + try: + user = global_store['user'] + balance = user.balance + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(balance), 200 + + +@app.route('/position', methods=['GET']) +def get_position(): + try: + user = global_store['user'] + position = user.position + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(position), 200 + + +@app.route('/today_entrusts', methods=['GET']) +def get_today_entrusts(): + try: + user = global_store['user'] + today_entrusts = user.today_entrusts + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(today_entrusts), 200 + + +@app.route('/today_trades', methods=['GET']) +def get_today_trades(): + try: + user = global_store['user'] + today_trades = user.today_trades + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(today_trades), 200 + + +@app.route('/cancel_entrusts', methods=['GET']) +def get_cancel_entrusts(): + try: + user = global_store['user'] + cancel_entrusts = user.cancel_entrusts + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(cancel_entrusts), 200 + + +@app.route('/buy', methods=['POST']) +def post_buy(): + json_data = request.get_json(force=True) + try: + user = global_store['user'] + res = user.buy(**json_data) + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(res), 201 + + +@app.route('/sell', methods=['POST']) +def post_sell(): + json_data = request.get_json(force=True) + try: + user = global_store['user'] + res = user.sell(**json_data) + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(res), 201 + + +@app.route('/cancel_entrust', methods=['POST']) +def post_cancel_entrust(): + json_data = request.get_json(force=True) + try: + user = global_store['user'] + res = user.cancel_entrust(**json_data) + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(res), 201 + + +@app.route('/exit', methods=['GET']) +def get_exit(): + try: + user = global_store['user'] + user.exit() + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify({'msg': 'exit success'}), 200 + + +def run(port=1430): + app.run(host='0.0.0.0', port=port) From d5571fb5906b3c1e2e2e7790435a2603ae0ac006 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 9 Jan 2018 17:41:12 +0800 Subject: [PATCH 047/276] remove invalid gftrader support --- docs/install.md | 2 +- easytrader/__init__.py | 1 - easytrader/api.py | 5 +- easytrader/gftrader.py | 618 ----------------------------------- test_gftrader_get_entrust.py | 69 ---- 5 files changed, 2 insertions(+), 693 deletions(-) delete mode 100644 easytrader/gftrader.py delete mode 100644 test_gftrader_get_entrust.py diff --git a/docs/install.md b/docs/install.md index 8e816324..b0fc6a8e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,6 +1,6 @@ ### requirements -银河可以直接自动登录, 广发的自动登录需要安装 tesseract: +银河可以直接自动登录, 其他券商如果登陆需要识别验证码的话需要安装 tesseract: * `tesseract` : 非 `pytesseract`, 需要单独安装, [地址](https://github.com/tesseract-ocr/tesseract/wiki),保证在命令行下 `tesseract` 可用 diff --git a/easytrader/__init__.py b/easytrader/__init__.py index ede85904..6a15a825 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,7 +1,6 @@ # coding: utf-8 from .api import * from .webtrader import WebTrader -from .gftrader import GFTrader from .joinquant_follower import JoinQuantFollower from .ricequant_follower import RiceQuantFollower from . import exceptions diff --git a/easytrader/api.py b/easytrader/api.py index 1054b7ef..67b6f76e 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -1,7 +1,6 @@ # coding=utf-8 import logging -from .gftrader import GFTrader from .joinquant_follower import JoinQuantFollower from .log import log from .ricequant_follower import RiceQuantFollower @@ -11,7 +10,7 @@ def use(broker, debug=True, **kwargs): """用于生成特定的券商对象 - :param broker:券商名支持 ['yh', 'YH', '银河'] ['gf', 'GF', '广发'] + :param broker:券商名支持 ['yh_client', '银河客户端'] ['ht_client', '华泰客户端'] :param debug: 控制 debug 日志的显示, 默认为 True :param initial_assets: [雪球参数] 控制雪球初始资金,默认为一百万 :return the class of trader @@ -26,8 +25,6 @@ def use(broker, debug=True, **kwargs): log.setLevel(logging.INFO) elif broker.lower() in ['xq', '雪球']: return XueQiuTrader(**kwargs) - elif broker.lower() in ['gf', '广发']: - return GFTrader(debug=debug) elif broker.lower() in ['yh_client', '银河客户端']: from .yh_clienttrader import YHClientTrader return YHClientTrader() diff --git a/easytrader/gftrader.py b/easytrader/gftrader.py deleted file mode 100644 index 06df081d..00000000 --- a/easytrader/gftrader.py +++ /dev/null @@ -1,618 +0,0 @@ -# coding: utf-8 -from __future__ import division - -import json -import os -import re -import tempfile -import urllib - -import requests -import six - -from . import helpers -from .log import log -from .webtrader import NotLoginError -from .webtrader import WebTrader - -VERIFY_CODE_POS = 0 -TRADE_MARKET = 1 -SESSIONIDPOS = 32 -HOLDER_POS = 11 -SH = 0 -SZ = 1 - - -class GFTrader(WebTrader): - config_path = os.path.dirname(__file__) + '/config/gf.json' - - def __init__(self, debug=True): - super(GFTrader, self).__init__(debug=debug) - self.cookie = None - self.account_config = None - self.s = None - self.exchange_stock_account = dict() - self.sessionid = '' - self.holdername = list() - - def _prepare_account(self, user, password, **kwargs): - self.account_config = { - 'username': user, - 'password': password - } - - def __handle_recognize_code(self): - """获取并识别返回的验证码 - :return:失败返回 False 成功返回 验证码""" - # 获取验证码 - verify_code_response = self.s.get(self.config['verify_code_api']) - # 保存验证码 - image_path = tempfile.mktemp() - with open(image_path, 'wb') as f: - f.write(bytes(verify_code_response.content)) - - verify_code = helpers.recognize_verify_code(image_path, broker='gf') - log.debug('verify code detect result: %s' % verify_code) - os.remove(image_path) - - ht_verify_code_length = 5 - if len(verify_code) != ht_verify_code_length: - return False - return verify_code - - def __go_login_page(self): - """访问登录页面获取 cookie""" - if self.s is not None: - self.s.get(self.config['logout_api']) - self.s = requests.session() - self.s.get(self.config['login_page']) - - def login(self, throw=False): - """实现广发证券的自动登录""" - self.__go_login_page() - verify_code = self.__handle_recognize_code() - - if not verify_code: - return False - - login_status, result = self.post_login_data(verify_code) - if login_status is False: - return False - return True - - def post_login_data(self, verify_code): - login_params = dict( - self.config['login'], - mac=helpers.get_mac(), - username=self.account_config['username'], - password=self.account_config['password'], - tmp_yzm=verify_code - ) - login_response = self.s.post(self.config['login_api'], params=login_params) - log.info('login response: {}'.format(login_response.text)) - if login_response.json()['success'] is True: - v = login_response.headers - self.sessionid = v['Set-Cookie'][-SESSIONIDPOS:] - self.__set_trade_need_info() - return True, None - return False, login_response.text - - def create_basic_params(self): - basic_params = dict( - dse_sessionId=self.sessionid - ) - return basic_params - - def request(self, params): - if six.PY2: - params_str = urllib.urlencode(params) - unquote_str = urllib.unquote(params_str) - else: - params_str = urllib.parse.urlencode(params) - unquote_str = urllib.parse.unquote(params_str) - url = self.trade_prefix + '?' + unquote_str - r = self.s.post(url) - log.debug('raw response: {}'.format(r.text)) - return r.content - - def format_response_data(self, data): - if six.PY2: - return_data = json.loads(data.encode('utf-8')) - else: - return_data = json.loads(str(data, 'utf-8')) - return return_data - - def check_login_status(self, response): - if response is None or (not response.get('success') == True): - self.heart_active = False - raise NotLoginError - - def check_account_live(self, response): - if response is None or (not response.get('success') == True): - self.heart_active = False - raise NotLoginError - if hasattr(response, 'data') and response.get('error_no') == '-1': - self.heart_active = False - - def __set_trade_need_info(self): - """设置交易所需的一些基本参数 - """ - account_params = dict( - self.config['accountinfo'] - ) - if six.PY2: - params_str = urllib.urlencode(account_params) - unquote_str = urllib.unquote(params_str) - else: - params_str = urllib.parse.urlencode(account_params) - unquote_str = urllib.parse.unquote(params_str) - url = self.trade_prefix + '?' + unquote_str - r = self.s.get(url) - log.debug('get account info: {}'.format(r.text)) - jslist = r.text.split(';') - jsholder = jslist[HOLDER_POS] - jsholder = re.findall(r'\[(.*)\]', jsholder) - jsholder = eval(jsholder[0]) - for jsholder_sh in jsholder: - if jsholder_sh['exchange_name'] == '上海': - self.holdername.append(jsholder_sh) - for jsholder_sz in jsholder: - if jsholder_sz['exchange_name'] == '深圳': - self.holdername.append(jsholder_sz) - - def __get_trade_need_info(self, stock_code): - """获取股票对应的证券市场和帐号""" - # 获取股票对应的证券市场 - exchange_type = self.holdername[SH]['exchange_type'] if helpers.get_stock_type(stock_code) == 'sh' \ - else self.holdername[SZ]['exchange_type'] - # 获取股票对应的证券帐号 - stock_account = self.holdername[SH]['stock_account'] if exchange_type == '1' \ - else self.holdername[SZ]['stock_account'] - return dict( - exchange_type=exchange_type, - stock_account=stock_account - ) - - def buy(self, stock_code, price, amount=0, volume=0, entrust_prop=0): - """买入 - :param stock_code: 股票代码 - :param price: 买入价格 - :param amount: 买入股数 - :param volume: 买入总金额 由 volume / price 取 100 的整数, 若指定 amount 则此参数无效 - :param entrust_prop: 委托类型,暂未实现,默认为限价委托 - """ - params = dict( - self.config['buy'], - entrust_amount=amount if amount else volume // price // 100 * 100, - entrust_prop=0 - ) - return self.__trade(stock_code, price, other=params) - - def sell(self, stock_code, price, amount=0, volume=0, entrust_prop=0): - """卖出 - :param stock_code: 股票代码 - :param price: 卖出价格 - :param amount: 卖出股数 - :param volume: 卖出总金额 由 volume / price 取整, 若指定 amount 则此参数无效 - :param entrust_prop: 委托类型,暂未实现,默认为限价委托 - """ - params = dict( - self.config['sell'], - entrust_amount=amount if amount else volume // price, - entrust_prop=entrust_prop - ) - return self.__trade(stock_code, price, other=params) - - def cnjj_apply(self, stock_code, amount): - """场内基金申购 - :param stock_code: 基金代码 - :param amount: 申购金额 - """ - params = dict( - self.config['cnjj_apply'], - entrust_amount=amount - ) - return self.__trade(stock_code, 0, other=params) - - def cnjj_redemption(self, stock_code, amount=0): - """场内基金赎回 - :param stock_code: 基金代码 - :param amount: 赎回份额 - """ - params = dict( - self.config['cnjj_redeem'], - entrust_amount=amount - ) - return self.__trade(stock_code, 1, other=params) - - def fund_subscribe(self, stock_code, price=0, entrust_prop='LFS'): - """基金认购 - :param stock_code: 基金代码 - :param price: 认购金额 - """ - params = dict( - self.config['fundsubscribe'], - entrust_amount=1, - entrust_prop=entrust_prop - ) - return self.__trade(stock_code, price, other=params) - - def fund_purchase(self, stock_code, price=0, entrust_prop='LFC'): - """基金申购 - :param stock_code: 基金代码 - :param amount: 申购金额 - """ - params = dict( - self.config['fundpurchase'], - entrust_amount=1, - entrust_prop=entrust_prop - ) - return self.__trade(stock_code, price, other=params) - - def fund_redemption(self, stock_code, amount=0, entrust_prop='LFR'): - """基金赎回 - :param stock_code: 基金代码 - :param amount: 赎回份额 - """ - params = dict( - self.config['fundredemption'], - entrust_amount=amount, - entrust_prop=entrust_prop - ) - return self.__trade(stock_code, 1, other=params) - - def fund_merge(self, stock_code, amount=0, entrust_prop='LFM'): - """基金合并 - :param stock_code: 母份额基金代码 - :param amount: 合并份额 - """ - params = dict( - self.config['fundmerge'], - entrust_amount=amount, - entrust_prop=entrust_prop - ) - return self.__trade(stock_code, 1, other=params) - - def fund_split(self, stock_code, amount=0, entrust_prop='LFP'): - """基金分拆 - :param stock_code: 母份额基金代码 - :param amount: 分拆份额 - """ - params = dict( - self.config['fundsplit'], - entrust_amount=amount, - entrust_prop=entrust_prop - ) - return self.__trade(stock_code, 1, other=params) - - def nxbQueryPrice(self, fund_code): - """牛熊宝查询 - """ - params = dict( - self.config['nxbQueryPrice'], - fund_code=fund_code - ) - return self.do(params) - - def nxbentrust(self, fund_code, amount, price, bs, auto_deal="true"): - """牛熊宝单项申报 - :param fund_code: 转换代码 - :param amount: 转入数量, like n*1000, min 1000 - :param price: 转换比例 like 0.8 - :param bs: 转换方向,1为母转子,2为子转母 - """ - # TODO: What's auto_deal - params = dict( - self.config['nxbentrust'], - fund_code=fund_code, - entrust_amount=amount, - entrust_price=price, - entrust_bs=bs, - auto_deal=auto_deal - ) - return self.do(params) - - def nxbentrustcancel(self, entrust_no): - """牛熊宝撤单,撤单后再次调用nxbQueryEntrust确认撤单成功 - param: entrust_no: 单号,通过调用nxbQueryEntrust查询 - """ - params = dict( - self.config['nxbentrustcancel'], - entrust_no=entrust_no - ) - return self.do(params) - - def nxbQueryEntrust(self, start_date="0", end_date="0", query_type="1"): - """当日委托 - :param start_date: 开始日期20160515,0为当天 - :param end_date: 结束日期20160522,0为当天 - :param query_type: 委托查询类型,0为历史查询,1为当日查询 - """ - params = dict( - self.config['nxbQueryEntrust'], - query_type=query_type, - prodta_no="98", - entrust_no="0", - fund_code="", - start_date=start_date, - end_date=end_date, - position_str="0", - limit="10", - start="0" - ) - if query_type == "1": - params['query_mode'] = "1" - return self.do(params) - - def nxbQueryDeliverOfToday(self): - """当日转换 - """ - params = dict( - self.config['nxbQueryDeliver'], - query_type="2", - prodta_no="98", - fund_code="", - position_str="0", - limit="10", - start="0" - ) - return self.do(params) - - def nxbQueryHisDeliver(self, start_date, end_date): - """历史转换 - """ - params = dict( - self.config['nxbQueryHisDeliver'], - query_type="2", - prodta_no="98", - fund_code="", - position_str="0", - limit="50", - start="0", - start_date=start_date, - end_date=end_date - ) - return self.do(params) - - def queryOfStkCodes(self): - """牛熊宝代码查询? - """ - params = dict( - self.config['queryOfStkCodes'], - prodta_no="98", - business_type="2" - ) - return self.do(params) - - def queryNXBOfStock(self): - """牛熊宝持仓查询 - """ - params = dict( - self.config['queryNXBOfStock'], - fund_company="98", - query_mode="0", - start="0", - limit="10" - ) - return self.do(params) - - def __trade(self, stock_code, price, other): - # 检查是否已经掉线 - self.check_login(1) - need_info = self.__get_trade_need_info(stock_code) - trade_param = dict( - other, - stock_account=need_info['stock_account'], - exchange_type=need_info['exchange_type'], - stock_code=stock_code[-6:], - entrust_price=price, - dse_sessionId=self.sessionid - ) - return self.do(trade_param) - - def cancel_entrust(self, entrust_no): - """撤单 - :param entrust_no: 委单号""" - cancel_params = dict( - self.config['cancel_entrust'], - entrust_no=entrust_no, - dse_sessionId=self.sessionid - ) - return self.do(cancel_params) - - @property - def exchangebill(self): - start_date, end_date = helpers.get_30_date() - return self.get_exchangebill(start_date, end_date) - - def getStockQuotation(self, stockcode): - exchange_info = self.__get_trade_need_info(stockcode) - params = dict( - self.config['queryStockInfo'], - exchange_type=exchange_info['exchange_type'], - stock_code=stockcode - ) - request_params = self.create_basic_params() - request_params.update(params) - response_data = self.request(request_params) - response_data = str(response_data) - response_data = response_data[response_data.find('hq') + 3:response_data.find('hqtype') - 1] - response_data = response_data.replace('\\x', '\\u00') - return json.loads(response_data) - - def get_exchangebill(self, start_date, end_date): - """ - 查询指定日期内的交割单 - :param start_date: 20160211 - :param end_date: 20160211 - :return: - """ - params = self.config['exchangebill'].copy() - params.update({ - "start_date": start_date, - "end_date": end_date, - }) - return self.do(params) - - @property - def today_ipo_list(self): - ''' - - 查询今日ipo的股票列表 - :return: - ''' - params = self.config['today_ipo_list'].copy() - return self.do(params) - - def today_ipo_limit(self): - ''' - - 查询今日账户新股申购额度 - :return: - ''' - params = self.config['today_ipo_limit'].copy() - return self.do(params) - - def login_rzrq(self): - ''' - - 登录融资融券平台 - :return: - ''' - params = self.config['rzrq'].copy() - return self.do(params) - - def rzrq_exchangebill(self, start_date, end_date): - """ - 查询指定日期内的融资融券交割单 - :param start_date: 20160211 - :param end_date: 20160211 - :return: - """ - params = self.config['rzrq_exchangebill'].copy() - params.update({ - "start_date": start_date, - "end_date": end_date, - }) - return self.do(params) - - def entrust_his(self, start_date, end_date): - """ - 查询指定日期内的历史委托单 - :param start_date: 20160211 - :param end_date: 20160211 - :return: - """ - params = self.config['entrust_his'].copy() - params.update({ - "start_date": start_date, - "end_date": end_date, - }) - return self.do(params) - - def rzrq_entrust_his(self, start_date, end_date): - """ - 查询指定日期内的融资融券历史委托单 - :param start_date: 20160211 - :param end_date: 20160211 - :return: - """ - params = self.config['rzrq_entrust_his'].copy() - params.update({ - "start_date": start_date, - "end_date": end_date, - }) - return self.do(params) - - def do_job(self, request_type, **kwargs): - ''' - 直接输入请求类型,以及相关参数列表,返回执行结果 - :param request_type:请求类型,这个请求类型必须在config/gf.json里面,例如position - :param kwargs:请求相关的参数 - :return:返回请求结果。 - ''' - params = self.config[request_type].copy() - params.update(kwargs) - return self.do(params) - - def capitalflow(self, start_date, end_date): - """ - 查询指定日期内的资金流水 - :param start_date: 开始时间,例如:20160211 - :param end_date: 技术时间,例如:20160211 - :return: 指定时间段内的资金流水数据 - """ - params = self.config['capitalflow'].copy() - params.update({ - "start_date": start_date, - "end_date": end_date, - }) - return self.do(params) - - def exit(self): - ''' - 退出系统 - :return: - ''' - params = self.config['exit'].copy() - log.debug(self.do(params)) - self.heart_active = False - - def get_entrust_without_pos(self, action_in=0): - ''' - - :param action_in: 当值为0,返回全部委托;当值为1时,返回可撤委托 - :return: - ''' - params = self.config['entrust'].copy() - params.update({ - "action_in": action_in, - }) - return self.do(params) - - def get_entrust(self, action_in): - ''' - - :param action_in: 当值为0,返回全部委托;当值为1时,返回可撤委托 - :return: 字典形式的返回值 - ''' - data, total = self.get_value(action_in) - return {u'data': data, u'total': total, u'success': True} - - def get_entrust_with_pos(self, postion_str): - ''' - - :param position_str: 用于标记查询委托单号的起点 - :return: 字典形式的返回值 - ''' - params = self.config['entrust_pos'].copy() - params.update({ - "postion_str": postion_str, - }) - return self.do(params) - - def get_value(self, action_in): - ''' - 1.委托数量在100单以下,直接返回值 - 2.查询委托的数量等于100单,调用带position_str参数的委托查询方法 - 3.直到最后一次的查询返回值小于100单,结束循环,构造返回值 - - :param action_in: 当值为0,返回全部委托;当值为1时,返回可撤委托 - :return:(数据列表,数据总数)构成的元组 - ''' - data = [] - total = 0 - - result = self.get_entrust_without_pos(action_in) - - while True: - data += result[u'data'] - total += result[u'total'] - - if result[u'total'] < 100: - break - result = self.get_entrust_with_pos( - action_in, result[u'data'][-1]['position_str'] - ) - - return data, total diff --git a/test_gftrader_get_entrust.py b/test_gftrader_get_entrust.py deleted file mode 100644 index 0bb7f7f5..00000000 --- a/test_gftrader_get_entrust.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Author: heheqiao(614400597@qq.com) -# -'''Test for ``trader.FixedTrader`` -''' -import mock -from nose.tools import assert_equal -from easytrader import gftrader - - -@mock.patch.object(gftrader.GFTrader, 'get_value') -def test_get_entrust(mock_get_value): - '''UnitTest of ``gftrader.GFTrader.get_entrust`` - ''' - mock_get_value.return_value = ( - [u'test', u'test'], 100 - ) - - assert_equal( - gftrader.GFTrader().get_entrust(0), - {u'data': [u'test', u'test'], u'total': 100, u'success': True} - ) - - -@mock.patch.object(gftrader.GFTrader, 'do') -def test_get_entrust_with_pos(mock_do): - '''UnitTest of ``gftrader.GFTrader.get_entrust_with_pos`` - ''' - gftrader.GFTrader().get_entrust_with_pos(u'test') - - mock_do.assert_called_with({ - u'classname': u'com.gf.etrade.control.StockUF2Control', - u'query_mode': 0, u'query_direction': 0, - u'postion_str': u'test', - u'request_num': 100, - u'method': u'queryDRWT' - }) - - -@mock.patch.object(gftrader.GFTrader, 'get_entrust_without_pos') -@mock.patch.object(gftrader.GFTrader, 'get_entrust_with_pos') -def test_get_value(mock_get_pos, mock_get): - '''UnitTest of ``gftrader.GFTrader.get_value`` - ''' - # Case1:result[u'total'] < 100 - mock_get.return_value = { - u'data': [u'test'], u'total': 1 - } - - assert_equal( - gftrader.GFTrader().get_value(0), - ([u'test'], 1) - ) - mock_get_pos.assert_not_called() - - # Case2:result[u'total'] >= 100 - mock_get.return_value = { - u'data': [{u'position_str': u'test'}], u'total': 100 - } - mock_get_pos.return_value = { - u'data': [u'test'], u'total': 50 - } - - assert_equal( - gftrader.GFTrader().get_value(0), - ([{u'position_str': u'test'}, u'test'], 150) - ) - mock_get_pos.assert_called_with(0, u'test') From 7411f3606dedd84ee7cf6c57d1971d781447b3f7 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 9 Jan 2018 17:43:40 +0800 Subject: [PATCH 048/276] add missing exit() function --- easytrader/remoteclient.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/easytrader/remoteclient.py b/easytrader/remoteclient.py index d1d4b6c7..7a2da3e3 100644 --- a/easytrader/remoteclient.py +++ b/easytrader/remoteclient.py @@ -60,6 +60,9 @@ def today_trades(self): def cancel_entrusts(self): return self.common_get('cancel_entrusts') + def exit(self): + return self.common_get('exit') + def common_get(self, endpoint): response = self._s.get(self._api + '/' + endpoint) if response.status_code >= 300: From 1c4fa96d82af50b57987f2cb057d1c051faee509 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 9 Jan 2018 17:48:44 +0800 Subject: [PATCH 049/276] remove invalid files --- convert_jq.sh | 25 ------------------------- gf.json | 4 ---- xczq.json | 4 ---- 3 files changed, 33 deletions(-) delete mode 100644 convert_jq.sh delete mode 100644 gf.json delete mode 100644 xczq.json diff --git a/convert_jq.sh b/convert_jq.sh deleted file mode 100644 index e918642e..00000000 --- a/convert_jq.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -for config in xq.json yh.json global.json -do - cp easytrader/config/${config} . -done - -for file in easytrader/webtrader.py easytrader/xqtrader.py easytrader/yhtrader.py easytrader/api.py easytrader/helpers.py easytrader/log.py -do - sed -e 's/\/config\///g; s/from \. import/import/g; s/from \./from /g ' ${file} > `basename ${file}` -done - -# delete api.py invalid lines -delete_line_flag='_follower gftrader httrader yjbtrader' -sed_cmd='' -for flag in ${delete_line_flag} -do - sed_cmd="/${flag}/d;${sed_cmd}" -done - -sed -i ${sed_cmd} api.py -mv api.py jq_easytrader.py - -sed -i '/thirdlibrary/d' helpers.py -echo generate jq files success diff --git a/gf.json b/gf.json deleted file mode 100644 index bea8a335..00000000 --- a/gf.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "username": "加密的客户号", - "password": "加密的密码" -} diff --git a/xczq.json b/xczq.json deleted file mode 100644 index 9a22b100..00000000 --- a/xczq.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "account": "客户号", - "password": "密码" -} From 87ad63da201696502f07da52a387d5e5c073307d Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 10 Jan 2018 11:38:44 +0800 Subject: [PATCH 050/276] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BD=93?= =?UTF-8?q?=E5=8F=AA=E6=9C=89=E6=B2=AA=E6=B7=B1=E8=82=A1=E5=B8=82=E4=B9=8B?= =?UTF-8?q?=E4=B8=80=E6=9C=89=E9=A2=9D=E5=BA=A6=E6=97=B6=EF=BC=8C=20auto?= =?UTF-8?q?=5Fipo=20=E8=B0=83=E7=94=A8=E5=87=BA=E9=94=99=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20fix=20#239?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/__init__.py | 2 +- easytrader/clienttrader.py | 41 +++++++++++++++++++++++++++++++++-- easytrader/config/client.py | 11 ++++++++-- easytrader/ht_clienttrader.py | 22 ------------------- easytrader/remoteclient.py | 3 +++ easytrader/server.py | 11 ++++++++++ easytrader/yh_clienttrader.py | 23 -------------------- 7 files changed, 63 insertions(+), 50 deletions(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 6a15a825..317a48d4 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.5' +__version__ = '0.12.6' __author__ = 'shidenggui' diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 5d99fd57..13d213d9 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -76,7 +76,45 @@ def sell(self, security, price, amount, **kwargs): pass def auto_ipo(self): - raise NotImplementedError + self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) + + stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + valid_list_idx = [i for i, v in enumerate(stock_list) if v['申购数量'] <= 0] + self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) + self._wait(0.1) + + for row in valid_list_idx: + self._click_grid_by_row(row) + self._wait(0.1) + + self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) + self._wait(0.1) + + return self._handle_auto_ipo_pop_dialog() + + def _click_grid_by_row(self, row): + x = self._config.COMMON_GRID_LEFT_MARGIN + y = self._config.COMMON_GRID_FIRST_ROW_HEIGHT + self._config.COMMON_GRID_ROW_HEIGHT * row + self._app.top_window().window( + control_id=self._config.COMMON_GRID_CONTROL_ID, + class_name='CVirtualGridCtrl' + ).click(coords=(x, y)) + + def _handle_auto_ipo_pop_dialog(self): + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + title = self._get_pop_dialog_title() + if '提示信息' in title or '委托确认' in title: + self._app.top_window().type_keys('%Y') + elif '提示' in title: + data = self._app.top_window().Static.window_text() + self._app.top_window()['确定'].click() + return {'message': data} + else: + data = self._app.top_window().Static.window_text() + self._app.top_window().close() + return {'message': 'unkown message: {}'.find(data)} + self._wait(0.1) + return {'message': 'success'} def _run_exe_path(self, exe_path): return os.path.join( @@ -95,4 +133,3 @@ def _close_prompt_windows(self): if w.window_text() != self._config.TITLE: w.close() self._wait(1) - diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 504a2855..ea2818ce 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -9,8 +9,13 @@ def create(broker): return GJ raise NotImplemented +class CommonConfig: + COMMON_GRID_LEFT_MARGIN = 10 + COMMON_GRID_FIRST_ROW_HEIGHT = 30 + COMMON_GRID_ROW_HEIGHT = 16 -class YH: + +class YH(CommonConfig): DEFAULT_EXE_PATH = r'C:\中国银河证券双子星3.2\Binarystar.exe' TITLE = '网上股票交易系统5.0' @@ -44,9 +49,10 @@ class YH: AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 AUTO_IPO_BUTTON_CONTROL_ID = 1006 + AUTO_IPO_MENU_PATH = ['新股申购', '一键打新'] -class HT: +class HT(CommonConfig): DEFAULT_EXE_PATH = r'C:\htzqzyb2\xiadan.exe' TITLE = '网上股票交易系统5.0' @@ -88,6 +94,7 @@ class HT: AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 AUTO_IPO_BUTTON_CONTROL_ID = 1006 + AUTO_IPO_MENU_PATH = ['新股申购', '批量新股申购'] class GJ: DEFAULT_EXE_PATH = 'C:\\全能行证券交易终端\\xiadan.exe' diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index b0c56ce5..51ce724b 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -133,28 +133,6 @@ def cancel_entrust(self, entrust_no): else: return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} - def auto_ipo(self): - self._switch_left_menus(['新股申购', '批量新股申购']) - - self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) - self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) - - return self._handle_auto_ipo_pop_dialog() - - def _handle_auto_ipo_pop_dialog(self): - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): - title = self._get_pop_dialog_title() - if '提示信息' in title: - self._app.top_window().type_keys('%Y') - elif '提示' in title: - data = self._app.top_window().Static.window_text() - self._app.top_window()['确定'].click() - return {'message': data} - else: - data = self._app.top_window().Static.window_text() - self._app.top_window().close() - return {'message': 'unkown message: {}'.find(data)} - self._wait(0.1) def _click(self, control_id): self._app.top_window().window( diff --git a/easytrader/remoteclient.py b/easytrader/remoteclient.py index 7a2da3e3..cf8513ab 100644 --- a/easytrader/remoteclient.py +++ b/easytrader/remoteclient.py @@ -60,6 +60,9 @@ def today_trades(self): def cancel_entrusts(self): return self.common_get('cancel_entrusts') + def auto_ipo(self): + return self.common_get('auto_ipo') + def exit(self): return self.common_get('exit') diff --git a/easytrader/server.py b/easytrader/server.py index e4463e9b..fe159657 100644 --- a/easytrader/server.py +++ b/easytrader/server.py @@ -43,6 +43,17 @@ def get_position(): return jsonify(position), 200 +@app.route('/auto_ipo', methods=['GET']) +def get_auto_ipo(): + try: + user = global_store['user'] + res = user.auto_ipo() + except Exception as e: + return jsonify({'error': str(e)}), 400 + + return jsonify(res), 200 + + @app.route('/today_entrusts', methods=['GET']) def get_today_entrusts(): try: diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 104e25e7..5785201c 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -152,29 +152,6 @@ def cancel_entrust(self, entrust_no): else: return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} - def auto_ipo(self): - self._switch_left_menus(['新股申购', '一键打新']) - - self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) - self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) - - return self._handle_auto_ipo_pop_dialog() - - def _handle_auto_ipo_pop_dialog(self): - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): - title = self._get_pop_dialog_title() - if '提示信息' in title: - self._app.top_window().type_keys('%Y') - elif '提示' in title: - data = self._app.top_window().Static.window_text() - self._app.top_window()['确定'].click() - return {'message': data} - else: - data = self._app.top_window().Static.window_text() - self._app.top_window().close() - return {'message': 'unkown message: {}'.find(data)} - self._wait(0.1) - def _click(self, control_id): self._app.top_window().window( control_id=control_id, From e5d84262f40316912e209b6fd2674b57ff9b2527 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 10 Jan 2018 11:55:15 +0800 Subject: [PATCH 051/276] update documents --- docs/install.md | 13 +++++++++---- docs/usage.md | 47 ++++++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/docs/install.md b/docs/install.md index b0fc6a8e..6b788697 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,14 +1,19 @@ ### requirements -银河可以直接自动登录, 其他券商如果登陆需要识别验证码的话需要安装 tesseract: - -* `tesseract` : 非 `pytesseract`, 需要单独安装, [地址](https://github.com/tesseract-ocr/tesseract/wiki),保证在命令行下 `tesseract` 可用 +### 客户端设置 -##### 客户端设置 +需要对客户端按以下设置,不然会导致下单时价格出错以及客户端超时锁定 * 系统设置 > 界面设置: 界面不操作超时时间设为 0 * 系统设置 > 交易设置: 默认买入价格/买入数量/卖出价格/卖出数量 都设置为 空 +### 登陆时的验证码识别 + +银河可以直接自动登录, 其他券商如果登陆需要识别验证码的话需要安装 tesseract: + +* `tesseract` : 非 `pytesseract`, 需要单独安装, [地址](https://github.com/tesseract-ocr/tesseract/wiki),保证在命令行下 `tesseract` 可用 + +或者你也可以手动登陆后在通过 `easytrader` 调用,此时 `easytrader` 在登陆过程中会直接识别到已登陆的窗口。 ### 安装 diff --git a/docs/usage.md b/docs/usage.md index 6eb99d07..623e4d27 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -32,7 +32,7 @@ user = easytrader.use('ht_client') # 华泰客户端支持 ['ht_client', '华泰 登陆账号有两种方式,`使用参数` 和 `使用配置文件` -** 参数登录(推荐)** +**参数登录(推荐)** ``` user.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码,其他券商不用') @@ -40,7 +40,7 @@ user.prepare(user='用户名', password='雪球、银河客户端为明文密码 **注:**雪球额外有个 account 参数,见上文介绍 -** 使用配置文件** +**使用配置文件** ```python user.prepare('/path/to/your/yh_client.json') // 配置文件路径 @@ -48,7 +48,7 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 **注**: 使用配置文件模式, 配置文件需要自己用编辑器编辑生成, 请勿使用记事本, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) -*格式如下* +**格式如下** 银河客户端 @@ -242,6 +242,27 @@ print(ipo_data) user.exit() ``` +### 远端服务器模式 + +#### 在服务器上启动服务 + +```python +from easytrader import server + +server.run(port=1430) # 默认端口为 1430 +``` + +#### 远程客户端调用 + +```python +from easytrader import remoteclient + +user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client 等', host='服务器ip', port='服务器端口,默认为1430') + +其他用法同上 +``` + + #### 雪球组合调仓 ```python @@ -339,26 +360,6 @@ follower.follow(***, entrust_prop='market') follower.follow(***, send_interval=30) # 设置下单间隔为 30 s ``` -### 远端服务器模式 - -#### 在服务器上启动服务 - -```python -from easytrader import server - -server.run(port=1430) # 默认端口为 1430 -``` - -#### 远程客户端调用 - -```python -from easytrader import remoteclient - -user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client 等', host='服务器ip', port='服务器端口,默认为1430') - -其他用法同上 -``` - ### 命令行模式 #### 登录 From 5c5c15d1ba9c251701b504bd1af7b5844a0bb2d9 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 10 Jan 2018 11:56:15 +0800 Subject: [PATCH 052/276] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=A2=9E=E5=8A=A0=E4=BA=A4=E6=98=93=E5=90=8E=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E6=B6=88=E6=81=AF=E7=AA=97=E5=8F=A3=E5=BC=B9=E5=87=BA?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E9=97=B4=EF=BC=8C=E4=BB=8E=200.2s=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=200.3s,=20=E5=87=8F=E5=B0=91=E5=87=BA?= =?UTF-8?q?=E9=94=99=E7=9A=84=E6=A6=82=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 27 +++++++++++++++++++++++++++ easytrader/ht_clienttrader.py | 28 ---------------------------- easytrader/yh_clienttrader.py | 27 --------------------------- 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 13d213d9..fce24e4b 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -4,6 +4,7 @@ import time from abc import abstractmethod +from . import exceptions from . import helpers from .config import client @@ -133,3 +134,29 @@ def _close_prompt_windows(self): if w.window_text() != self._config.TITLE: w.close() self._wait(1) + + def trade(self, security, price, amount): + self._set_trade_params(security, price, amount) + + self._submit_trade() + + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + pop_title = self._get_pop_dialog_title() + if pop_title == '委托确认': + self._app.top_window().type_keys('%Y') + elif pop_title == '提示信息': + if '超出涨跌停' in self._app.top_window().Static.window_text(): + self._app.top_window().type_keys('%Y') + elif pop_title == '提示': + content = self._app.top_window().Static.window_text() + if '成功' in content: + entrust_no = self._extract_entrust_id(content) + self._app.top_window()['确定'].click() + return {'entrust_no': entrust_no} + else: + self._app.top_window()['确定'].click() + self._wait(0.05) + raise exceptions.TradeError(content) + else: + self._app.top_window().close() + self._wait(0.3) # wait next dialog display diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 51ce724b..0941a999 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -12,7 +12,6 @@ import pywinauto import pywinauto.clipboard -from . import exceptions from .clienttrader import ClientTrader from .log import log @@ -133,39 +132,12 @@ def cancel_entrust(self, entrust_no): else: return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} - def _click(self, control_id): self._app.top_window().window( control_id=control_id, class_name='Button' ).click() - def trade(self, security, price, amount): - self._set_trade_params(security, price, amount) - - self._submit_trade() - - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): - pop_title = self._get_pop_dialog_title() - if pop_title == '委托确认': - self._app.top_window().type_keys('%Y') - elif pop_title == '提示信息': - if '超出涨跌停' in self._app.top_window().Static.window_text(): - self._app.top_window().type_keys('%Y') - elif pop_title == '提示': - content = self._app.top_window().Static.window_text() - if '成功' in content: - entrust_no = self._extract_entrust_id(content) - self._app.top_window()['确定'].click() - return {'entrust_no': entrust_no} - else: - self._app.top_window()['确定'].click() - self._wait(0.05) - raise exceptions.TradeError(content) - else: - self._app.top_window().close() - self._wait(0.2) # wait next dialog display - def _extract_entrust_id(self, content): return re.search(r'\d+', content).group() diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 5785201c..8950cdeb 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -13,7 +13,6 @@ import pywinauto import pywinauto.clipboard -from . import exceptions from . import helpers from .clienttrader import ClientTrader from .log import log @@ -158,32 +157,6 @@ def _click(self, control_id): class_name='Button' ).click() - def trade(self, security, price, amount): - self._set_trade_params(security, price, amount) - - self._submit_trade() - - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): - pop_title = self._get_pop_dialog_title() - if pop_title == '委托确认': - self._app.top_window().type_keys('%Y') - elif pop_title == '提示信息': - if '超出涨跌停' in self._app.top_window().Static.window_text(): - self._app.top_window().type_keys('%Y') - elif pop_title == '提示': - content = self._app.top_window().Static.window_text() - if '成功' in content: - entrust_no = self._extract_entrust_id(content) - self._app.top_window()['确定'].click() - return {'entrust_no': entrust_no} - else: - self._app.top_window()['确定'].click() - self._wait(0.05) - raise exceptions.TradeError(content) - else: - self._app.top_window().close() - self._wait(0.2) # wait next dialog display - def _extract_entrust_id(self, content): return re.search(r'\d+', content).group() From be2eb9c032f7050624812faaa0628dfa9a470d11 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 10 Jan 2018 22:02:55 +0800 Subject: [PATCH 053/276] =?UTF-8?q?feature:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=9B=BD=E9=87=91=E8=AF=81=E5=88=B8=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + docs/index.md | 3 +- docs/usage.md | 19 ++-- easytrader/__init__.py | 2 +- easytrader/clienttrader.py | 186 ++++++++++++++++++++++++++++++---- easytrader/config/client.py | 22 +++- easytrader/gj_clienttrader.py | 35 ++----- easytrader/helpers.py | 7 +- easytrader/ht_clienttrader.py | 182 +-------------------------------- easytrader/yh_clienttrader.py | 181 +-------------------------------- 10 files changed, 216 insertions(+), 423 deletions(-) diff --git a/README.md b/README.md index 916870f8..c6414f7f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ * 银河客户端, 须在 `windows` 平台下载 `银河双子星` 客户端 * 华泰客户端(同花顺版本) +* 国金客户端(全能行证券交易终端PC版) + ### 实盘易 diff --git a/docs/index.md b/docs/index.md index b71c6d36..cce0dbe7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ * 支持跟踪 `joinquant`, `ricequant` 的模拟交易 * 支持跟踪 雪球组合 调仓, 实盘雪球组合 * 支持命令行调用,方便其他语言适配 -* 支持通过 webserver 远程操作客户端 +* 支持远程操作客户端 * 支持 Python3 , Linux / Win / Mac * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 * 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) 或者 银河开户可以加群找我 @@ -21,6 +21,7 @@ * 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 * 华泰客户端(同花顺版本) +* 国金客户端(全能行证券交易终端PC版) ### 实盘易 diff --git a/docs/usage.md b/docs/usage.md index 623e4d27..5341fa41 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,20 +9,25 @@ import easytrader ** 银河客户端** ```python -user = easytrader.use('yh_client') # 银河客户端支持 ['yh_client', '银河客户端'] +user = easytrader.use('yh_client') ``` ** 华泰客户端** ```python -user = easytrader.use('ht_client') # 华泰客户端支持 ['ht_client', '华泰客户端'] +user = easytrader.use('ht_client') +``` + +** 国金客户端** +```python +user = easytrader.use('gj_client') ``` **雪球** 雪球配置中 `username` 为邮箱, `account` 为手机, 填两者之一即可,另一项改为 `""`, 密码直接填写登录的明文密码即可,不需要抓取 `POST` 的密码 -**银河客户端** +**银河/国金客户端** -银河客户端直接使用明文的账号和密码即可 +客户端直接使用明文的账号和密码即可 **华泰客户端** @@ -50,12 +55,12 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 **格式如下** -银河客户端 +银河/国金客户端 ``` { - "user": "银河用户名", - "password": "银河明文密码" + "user": "用户名", + "password": "明文密码" } ``` diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 317a48d4..3ceed2df 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.6' +__version__ = '0.12.7' __author__ = 'shidenggui' diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index fce24e4b..65974697 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -1,17 +1,27 @@ # coding:utf-8 +import easyutils +import functools +import io import os +import pandas as pd +import pywinauto +import pywinauto.clipboard +import re import time from abc import abstractmethod from . import exceptions from . import helpers from .config import client +from .log import log class ClientTrader: def __init__(self): self._config = client.create(self.broker_type) + self._app = None + self._main = None def prepare(self, config_path=None, user=None, password=None, exe_path=None, comm_password=None, **kwargs): @@ -40,41 +50,65 @@ def broker_type(self): pass @property - @abstractmethod def balance(self): - pass + self._switch_left_menus(['查询[F4]', '资金股票']) + + return self._get_balance_from_statics() + + def _get_balance_from_statics(self): + result = {} + for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): + result[key] = float( + self._main.window( + control_id=control_id, + class_name='Static', + ).window_text() + ) + return result @property - @abstractmethod def position(self): - pass + self._switch_left_menus(['查询[F4]', '资金股票']) - @property - @abstractmethod - def cancel_entrusts(self): - pass + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @property - @abstractmethod def today_entrusts(self): - pass + self._switch_left_menus(['查询[F4]', '当日委托']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @property - @abstractmethod def today_trades(self): - pass + self._switch_left_menus(['查询[F4]', '当日成交']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + + @property + def cancel_entrusts(self): + self._refresh() + self._switch_left_menus(['撤单[F3]']) + + return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - @abstractmethod def cancel_entrust(self, entrust_no): - pass + self._refresh() + for i, entrust in enumerate(self.cancel_entrusts): + if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: + self._cancel_entrust_by_double_click(i) + return self._handle_cancel_entrust_pop_dialog() + else: + return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} - @abstractmethod def buy(self, security, price, amount, **kwargs): - pass + self._switch_left_menus(['买入[F1]']) + + return self.trade(security, price, amount) - @abstractmethod def sell(self, security, price, amount, **kwargs): - pass + self._switch_left_menus(['卖出[F2]']) + + return self.trade(security, price, amount) def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) @@ -160,3 +194,119 @@ def trade(self, security, price, amount): else: self._app.top_window().close() self._wait(0.3) # wait next dialog display + + def _click(self, control_id): + self._app.top_window().window( + control_id=control_id, + class_name='Button' + ).click() + + def _extract_entrust_id(self, content): + return re.search(r'\d+', content).group() + + def _submit_trade(self): + time.sleep(0.05) + self._app.top_window().window( + control_id=self._config.TRADE_SUBMIT_CONTROL_ID, + class_name='Button' + ).click() + + def _get_pop_dialog_title(self): + return self._app.top_window().window( + control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID + ).window_text() + + def _set_trade_params(self, security, price, amount): + code = security[-6:] + + self._type_keys( + self._config.TRADE_SECURITY_CONTROL_ID, + code + ) + self._type_keys( + self._config.TRADE_PRICE_CONTROL_ID, + easyutils.round_price_by_code(price, code) + ) + self._type_keys( + self._config.TRADE_AMOUNT_CONTROL_ID, + str(int(amount)) + ) + + def _get_grid_data(self, control_id): + grid = self._main.window( + control_id=control_id, + class_name='CVirtualGridCtrl' + ) + grid.type_keys('^A^C') + return self._format_grid_data( + self._get_clipboard_data() + ) + + def _type_keys(self, control_id, text): + self._main.window( + control_id=control_id, + class_name='Edit' + ).type_keys(text) + + def _get_clipboard_data(self): + while True: + try: + return pywinauto.clipboard.GetData() + except Exception as e: + log.warning('{}, retry ......'.format(e)) + + def _switch_left_menus(self, path, sleep=0.2): + self._get_left_menus_handle().get_item(path).click() + self._wait(sleep) + + def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): + self._app.top_window().type_keys(shortcut) + self._wait(sleep) + + @functools.lru_cache() + def _get_left_menus_handle(self): + while True: + try: + handle = self._main.window( + control_id=129, + class_name='SysTreeView32' + ) + # sometime can't find handle ready, must retry + handle.wait('ready', 2) + return handle + except: + pass + + def _format_grid_data(self, data): + df = pd.read_csv(io.StringIO(data), + delimiter='\t', + dtype=self._config.GRID_DTYPE, + na_filter=False, + ) + return df.to_dict('records') + + def _handle_cancel_entrust_pop_dialog(self): + while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + title = self._get_pop_dialog_title() + if '提示信息' in title: + self._app.top_window().type_keys('%Y') + elif '提示' in title: + data = self._app.top_window().Static.window_text() + self._app.top_window()['确定'].click() + return {'message': data} + else: + data = self._app.top_window().Static.window_text() + self._app.top_window().close() + return {'message': 'unkown message: {}'.find(data)} + self._wait(0.2) + + def _cancel_entrust_by_double_click(self, row): + x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN + y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row + self._app.top_window().window( + control_id=self._config.COMMON_GRID_CONTROL_ID, + class_name='CVirtualGridCtrl' + ).double_click(coords=(x, y)) + + def _refresh(self): + self._switch_left_menus(['买入[F1]'], sleep=0.05) diff --git a/easytrader/config/client.py b/easytrader/config/client.py index ea2818ce..9f7ba1b7 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -6,14 +6,28 @@ def create(broker): elif broker == 'ht': return HT elif broker == 'gj': - return GJ + return GJ raise NotImplemented + class CommonConfig: COMMON_GRID_LEFT_MARGIN = 10 COMMON_GRID_FIRST_ROW_HEIGHT = 30 COMMON_GRID_ROW_HEIGHT = 16 + BALANCE_MENU_PATH = ['查询[F4]', '资金股票'] + POSITION_MENU_PATH = ['查询[F4]', '资金股票'] + TODAY_ENTRUSTS_MENU_PATH = ['查询[F4]', '当日委托'] + TODAY_TRADES_MENU_PATH = ['查询[F4]', '当日成交'] + + BALANCE_CONTROL_ID_GROUP = { + '资金余额': 1012, + '可用金额': 1016, + '可取金额': 1017, + '股票市值': 1014, + '总资产': 1015 + } + class YH(CommonConfig): DEFAULT_EXE_PATH = r'C:\中国银河证券双子星3.2\Binarystar.exe' @@ -96,7 +110,8 @@ class HT(CommonConfig): AUTO_IPO_BUTTON_CONTROL_ID = 1006 AUTO_IPO_MENU_PATH = ['新股申购', '批量新股申购'] -class GJ: + +class GJ(CommonConfig): DEFAULT_EXE_PATH = 'C:\\全能行证券交易终端\\xiadan.exe' TITLE = '网上股票交易系统5.0' @@ -133,4 +148,5 @@ class GJ: ENABLE_BALANCE_TEXT_ID = 0x3f8 TOTAL_BALANCE_TEXT_ID = 0x3f7 - \ No newline at end of file + + AUTO_IPO_MENU_PATH = ['新股申购', '新股批量申购'] diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index 4ab32e11..0567e63e 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -1,25 +1,18 @@ # coding:utf8 from __future__ import division -import functools -import io -import os +# import pandas as pd +import pywinauto +import pywinauto.clipboard import re import tempfile import time -import sys -import easyutils -# import pandas as pd -import pywinauto -import pywinauto.clipboard -from . import exceptions from . import helpers -from .yh_clienttrader import YHClientTrader -from .log import log +from .clienttrader import ClientTrader -class GJClientTrader(YHClientTrader): +class GJClientTrader(ClientTrader): @property def broker_type(self): return 'gj' @@ -52,7 +45,6 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): while True: try: code = self._handle_verify_code() - print('verify code=',code) edit3.type_keys( code ) @@ -66,30 +58,17 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window()['确定'].click() pass except Exception as e: - print("Exception,",e) pass - print('connect start') + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) - print('connect end') self._main = self._app.window(title='网上股票交易系统5.0') def _handle_verify_code(self): control = self._app.top_window().window(control_id=0x5db) control.click() time.sleep(0.2) - file_path = tempfile.mktemp()+'.jpg' + file_path = tempfile.mktemp() + '.jpg' control.capture_as_image().save(file_path) time.sleep(0.2) vcode = helpers.recognize_verify_code(file_path, 'gj_client') return ''.join(re.findall('[a-zA-Z0-9]+', vcode)) - - @property - def balance(self): - self._switch_left_menus(['查询[F4]', '资金股票']) - retv={} - retv['enable_balance'] = self._app.top_window().window(control_id=0x3f8).window_text() - retv['total_balance'] = self._app.top_window().window(control_id=0x3f7).window_text() - return [retv] - - - \ No newline at end of file diff --git a/easytrader/helpers.py b/easytrader/helpers.py index f28430e2..a5f4291c 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -4,11 +4,10 @@ import datetime import json import re -import ssl -import uuid - import requests import six +import ssl +import uuid from requests.adapters import HTTPAdapter from requests.packages.urllib3.poolmanager import PoolManager from six.moves import input @@ -72,7 +71,7 @@ def recognize_verify_code(image_path, broker='ht'): if broker == 'gf': return detect_gf_result(image_path) - elif broker == 'yh_client': + elif broker in ['yh_client', 'gj_client']: return detect_yh_client_result(image_path) # 调用 tesseract 识别 return default_verify_code_detect(image_path) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 0941a999..e69e9ad4 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,19 +1,9 @@ # coding:utf8 -from __future__ import division -import functools -import io -import os -import re -import time - -import easyutils -import pandas as pd import pywinauto import pywinauto.clipboard from .clienttrader import ClientTrader -from .log import log class HTClientTrader(ClientTrader): @@ -60,20 +50,9 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._close_prompt_windows() self._main = self._app.window(title='网上股票交易系统5.0') - def _run_exe_path(self, exe_path): - return os.path.join( - os.path.dirname(exe_path), 'xiadan.exe' - ) - - def _wait(self, seconds): - time.sleep(seconds) - - def exit(self): - self._app.kill() - @property def balance(self): - self._switch_left_menus(['查询[F4]', '资金股票']) + self._switch_left_menus(self._config.BALANCE_MENU_PATH) return self._get_balance_from_statics() @@ -87,162 +66,3 @@ def _get_balance_from_statics(self): ).window_text() ) return result - - @property - def position(self): - self._switch_left_menus(['查询[F4]', '资金股票']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - @property - def today_entrusts(self): - self._switch_left_menus(['查询[F4]', '当日委托']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - @property - def today_trades(self): - self._switch_left_menus(['查询[F4]', '当日成交']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - def buy(self, security, price, amount, **kwargs): - self._switch_left_menus(['买入[F1]']) - - return self.trade(security, price, amount) - - def sell(self, security, price, amount, **kwargs): - self._switch_left_menus(['卖出[F2]']) - - return self.trade(security, price, amount) - - @property - def cancel_entrusts(self): - self._refresh() - self._switch_left_menus(['撤单[F3]']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - def cancel_entrust(self, entrust_no): - self._refresh() - for i, entrust in enumerate(self.cancel_entrusts): - if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: - self._cancel_entrust_by_double_click(i) - return self._handle_cancel_entrust_pop_dialog() - else: - return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} - - def _click(self, control_id): - self._app.top_window().window( - control_id=control_id, - class_name='Button' - ).click() - - def _extract_entrust_id(self, content): - return re.search(r'\d+', content).group() - - def _submit_trade(self): - self._main.window( - control_id=self._config.TRADE_SUBMIT_CONTROL_ID, - class_name='Button' - ).click() - - def _get_pop_dialog_title(self): - return self._app.top_window().window( - control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID - ).window_text() - - def _set_trade_params(self, security, price, amount): - code = security[-6:] - - self._type_keys( - self._config.TRADE_SECURITY_CONTROL_ID, - code - ) - self._type_keys( - self._config.TRADE_PRICE_CONTROL_ID, - easyutils.round_price_by_code(price, code) - ) - self._type_keys( - self._config.TRADE_AMOUNT_CONTROL_ID, - str(int(amount)) - ) - - def _get_grid_data(self, control_id): - grid = self._app.top_window().window( - control_id=control_id, - class_name='CVirtualGridCtrl' - ) - grid.type_keys('^A^C') - return self._format_grid_data( - self._get_clipboard_data() - ) - - def _type_keys(self, control_id, text): - self._app.top_window().window( - control_id=control_id, - class_name='Edit' - ).type_keys(text) - - def _get_clipboard_data(self): - while True: - try: - return pywinauto.clipboard.GetData() - except Exception as e: - log.warning('{}, retry ......'.format(e)) - - def _switch_left_menus(self, path, sleep=0.2): - self._get_left_menus_handle().get_item(path).click() - self._wait(sleep) - - def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): - self._app.top_window().type_keys(shortcut) - self._wait(sleep) - - @functools.lru_cache() - def _get_left_menus_handle(self): - while True: - try: - handle = self._app.top_window().window( - control_id=129, - class_name='SysTreeView32' - ) - # sometime can't find handle ready, must retry - handle.wait('ready', 2) - return handle - except: - pass - - def _format_grid_data(self, data): - df = pd.read_csv(io.StringIO(data), - delimiter='\t', - dtype=self._config.GRID_DTYPE, - na_filter=False, - ) - return df.to_dict('records') - - def _handle_cancel_entrust_pop_dialog(self): - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): - title = self._get_pop_dialog_title() - if '提示信息' in title: - self._app.top_window().type_keys('%Y') - elif '提示' in title: - data = self._app.top_window().Static.window_text() - self._app.top_window()['确定'].click() - return {'message': data} - else: - data = self._app.top_window().Static.window_text() - self._app.top_window().close() - return {'message': 'unkown message: {}'.find(data)} - self._wait(0.2) - - def _cancel_entrust_by_double_click(self, row): - x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN - y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row - self._app.top_window().window( - control_id=self._config.COMMON_GRID_CONTROL_ID, - class_name='CVirtualGridCtrl' - ).double_click(coords=(x, y)) - - def _refresh(self): - self._switch_left_menus(['买入[F1]'], sleep=0.05) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 8950cdeb..7030f875 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -1,21 +1,13 @@ # coding:utf8 -from __future__ import division -import functools -import io -import os import re import tempfile -import time -import easyutils -import pandas as pd import pywinauto import pywinauto.clipboard from . import helpers from .clienttrader import ClientTrader -from .log import log class YHClientTrader(ClientTrader): @@ -80,17 +72,6 @@ def _switch_window_to_normal_mode(self): class_name='Button' ).click() - def _run_exe_path(self, exe_path): - return os.path.join( - os.path.dirname(exe_path), 'xiadan.exe' - ) - - def _wait(self, seconds): - time.sleep(seconds) - - def exit(self): - self._app.kill() - def _handle_verify_code(self): control = self._app.top_window().window(control_id=22202) control.click() @@ -103,166 +84,6 @@ def _handle_verify_code(self): @property def balance(self): - self._switch_left_menus(['查询[F4]', '资金股票']) + self._switch_left_menus(self._config.BALANCE_MENU_PATH) return self._get_grid_data(self._config.BALANCE_GRID_CONTROL_ID) - - @property - def position(self): - self._switch_left_menus(['查询[F4]', '资金股票']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - @property - def today_entrusts(self): - self._switch_left_menus(['查询[F4]', '当日委托']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - @property - def today_trades(self): - self._switch_left_menus(['查询[F4]', '当日成交']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - def buy(self, security, price, amount, **kwargs): - self._switch_left_menus(['买入[F1]']) - - return self.trade(security, price, amount) - - def sell(self, security, price, amount, **kwargs): - self._switch_left_menus(['卖出[F2]']) - - return self.trade(security, price, amount) - - @property - def cancel_entrusts(self): - self._refresh() - self._switch_left_menus(['撤单[F3]']) - - return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - - def cancel_entrust(self, entrust_no): - self._refresh() - for i, entrust in enumerate(self.cancel_entrusts): - if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: - self._cancel_entrust_by_double_click(i) - return self._handle_cancel_entrust_pop_dialog() - else: - return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} - - def _click(self, control_id): - self._app.top_window().window( - control_id=control_id, - class_name='Button' - ).click() - - def _extract_entrust_id(self, content): - return re.search(r'\d+', content).group() - - def _submit_trade(self): - time.sleep(0.05) - self._app.top_window().window( - control_id=self._config.TRADE_SUBMIT_CONTROL_ID, - class_name='Button' - ).click() - - def _get_pop_dialog_title(self): - return self._app.top_window().window( - control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID - ).window_text() - - def _set_trade_params(self, security, price, amount): - code = security[-6:] - - self._type_keys( - self._config.TRADE_SECURITY_CONTROL_ID, - code - ) - self._type_keys( - self._config.TRADE_PRICE_CONTROL_ID, - easyutils.round_price_by_code(price, code) - ) - self._type_keys( - self._config.TRADE_AMOUNT_CONTROL_ID, - str(int(amount)) - ) - - def _get_grid_data(self, control_id): - grid = self._main.window( - control_id=control_id, - class_name='CVirtualGridCtrl' - ) - grid.type_keys('^A^C') - return self._format_grid_data( - self._get_clipboard_data() - ) - - def _type_keys(self, control_id, text): - self._main.window( - control_id=control_id, - class_name='Edit' - ).type_keys(text) - - def _get_clipboard_data(self): - while True: - try: - return pywinauto.clipboard.GetData() - except Exception as e: - log.warning('{}, retry ......'.format(e)) - - def _switch_left_menus(self, path, sleep=0.2): - self._get_left_menus_handle().get_item(path).click() - self._wait(sleep) - - def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): - self._app.top_window().type_keys(shortcut) - self._wait(sleep) - - @functools.lru_cache() - def _get_left_menus_handle(self): - while True: - try: - handle = self._main.window( - control_id=129, - class_name='SysTreeView32' - ) - # sometime can't find handle ready, must retry - handle.wait('ready', 2) - return handle - except: - pass - - def _format_grid_data(self, data): - df = pd.read_csv(io.StringIO(data), - delimiter='\t', - dtype=self._config.GRID_DTYPE, - na_filter=False, - ) - return df.to_dict('records') - - def _handle_cancel_entrust_pop_dialog(self): - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): - title = self._get_pop_dialog_title() - if '提示信息' in title: - self._app.top_window().type_keys('%Y') - elif '提示' in title: - data = self._app.top_window().Static.window_text() - self._app.top_window()['确定'].click() - return {'message': data} - else: - data = self._app.top_window().Static.window_text() - self._app.top_window().close() - return {'message': 'unkown message: {}'.find(data)} - self._wait(0.2) - - def _cancel_entrust_by_double_click(self, row): - x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN - y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row - self._app.top_window().window( - control_id=self._config.COMMON_GRID_CONTROL_ID, - class_name='CVirtualGridCtrl' - ).double_click(coords=(x, y)) - - def _refresh(self): - self._switch_left_menus(['买入[F1]'], sleep=0.05) From 227d1ae3f80b4630a654d0e9dcc3cd9c4c81c0e6 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 11 Jan 2018 13:37:28 +0800 Subject: [PATCH 054/276] update README.md --- README.md | 2 +- docs/index.md | 2 +- docs/install.md | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c6414f7f..d438ff23 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ ### 支持券商 * 银河客户端, 须在 `windows` 平台下载 `银河双子星` 客户端 -* 华泰客户端(同花顺版本) +* 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) diff --git a/docs/index.md b/docs/index.md index cce0dbe7..69164471 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ ### 支持券商 * 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 -* 华泰客户端(同花顺版本) +* 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) diff --git a/docs/install.md b/docs/install.md index 6b788697..767395bd 100644 --- a/docs/install.md +++ b/docs/install.md @@ -7,6 +7,8 @@ * 系统设置 > 界面设置: 界面不操作超时时间设为 0 * 系统设置 > 交易设置: 默认买入价格/买入数量/卖出价格/卖出数量 都设置为 空 +同时客户端不能最小化也不能处于精简模式 + ### 登陆时的验证码识别 银河可以直接自动登录, 其他券商如果登陆需要识别验证码的话需要安装 tesseract: From 954041813960269f583f0988b50d99e1978c6c0e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 11 Jan 2018 22:27:44 +0800 Subject: [PATCH 055/276] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E5=90=8C=E8=8A=B1=E9=A1=BA=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- docs/index.md | 1 + docs/usage.md | 24 ++++++++-- easytrader/__init__.py | 2 +- easytrader/api.py | 3 ++ easytrader/clienttrader.py | 25 ++++++++--- easytrader/config/client.py | 88 ++++++++++++++----------------------- 7 files changed, 79 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index d438ff23..5b282439 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # easytrader * 进行自动的程序化股票交易 -* 实现自动登录 * 支持跟踪 `joinquant`, `ricequant` 的模拟交易 * 支持跟踪 雪球组合 调仓 +* 支持通用的同花顺客户端模拟操作 +* 实现自动登录 * 支持通过 webserver 远程操作客户端 * 支持命令行调用,方便其他语言适配 * 支持 Python3, Linux / Win, 推荐使用 `Python3` diff --git a/docs/index.md b/docs/index.md index 69164471..d9d5a63a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,6 +3,7 @@ * 进行自动的程序化股票交易 * 支持跟踪 `joinquant`, `ricequant` 的模拟交易 * 支持跟踪 雪球组合 调仓, 实盘雪球组合 +* 支持通用的同花顺客户端模拟操作 * 支持命令行调用,方便其他语言适配 * 支持远程操作客户端 * 支持 Python3 , Linux / Win / Mac diff --git a/docs/usage.md b/docs/usage.md index 5341fa41..cd57c3de 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,21 +6,30 @@ import easytrader **设置账户**: -** 银河客户端** +**银河客户端** ```python user = easytrader.use('yh_client') ``` -** 华泰客户端** +**华泰客户端** + ```python user = easytrader.use('ht_client') ``` -** 国金客户端** +**国金客户端** + ```python user = easytrader.use('gj_client') ``` +**通用同花顺客户端** + +```python +user = easytrader.use('ths') +``` + + **雪球** 雪球配置中 `username` 为邮箱, `account` 为手机, 填两者之一即可,另一项改为 `""`, 密码直接填写登录的明文密码即可,不需要抓取 `POST` 的密码 @@ -76,6 +85,15 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 ``` +# 直接连接通用同花顺客户端 + +需要先手动登陆客户端,然后运用下面的代码连接客户端 + +```python +user.connect('客户端exe路径') +``` + + ### 交易相关 以下用法以银河为例 diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 3ceed2df..ac73e1b0 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.7' +__version__ = '0.12.8' __author__ = 'shidenggui' diff --git a/easytrader/api.py b/easytrader/api.py index 67b6f76e..1632edea 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -34,6 +34,9 @@ def use(broker, debug=True, **kwargs): elif broker.lower() in ['gj_client', '国金客户端']: from .gj_clienttrader import GJClientTrader return GJClientTrader() + elif broker.lower() in ['ths', '同花顺客户端']: + from .clienttrader import ClientTrader + return ClientTrader() def follower(platform, **kwargs): diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 65974697..796c3b3d 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -1,16 +1,17 @@ # coding:utf-8 -import easyutils import functools import io import os -import pandas as pd -import pywinauto -import pywinauto.clipboard import re import time from abc import abstractmethod +import easyutils +import pandas as pd +import pywinauto +import pywinauto.clipboard + from . import exceptions from . import helpers from .config import client @@ -44,10 +45,20 @@ def prepare(self, config_path=None, user=None, password=None, exe_path=None, com def login(self, user, password, exe_path, comm_password=None, **kwargs): pass + def connect(self, exe_path=None, **kwargs): + """ + 直接连接登陆后的客户端 + :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe' + :return: + """ + self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path or self._config.DEFAULT_EXE_PATH), + timeout=10) + self._close_prompt_windows() + self._main = self._app.window(title=self._config.TITLE) + @property - @abstractmethod def broker_type(self): - pass + return 'ths' @property def balance(self): @@ -138,7 +149,7 @@ def _click_grid_by_row(self, row): def _handle_auto_ipo_pop_dialog(self): while self._main.wrapper_object() != self._app.top_window().wrapper_object(): title = self._get_pop_dialog_title() - if '提示信息' in title or '委托确认' in title: + if '提示信息' in title or '委托确认' in title or '网上交易用户协议' in title: self._app.top_window().type_keys('%Y') elif '提示' in title: data = self._app.top_window().Static.window_text() diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 9f7ba1b7..ac760e1e 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -1,5 +1,6 @@ # coding:utf8 + def create(broker): if broker == 'yh': return YH @@ -7,10 +8,22 @@ def create(broker): return HT elif broker == 'gj': return GJ + elif broker == 'ths': + return CommonConfig raise NotImplemented class CommonConfig: + TITLE = '网上股票交易系统5.0' + + TRADE_SECURITY_CONTROL_ID = 1032 + TRADE_PRICE_CONTROL_ID = 1033 + TRADE_AMOUNT_CONTROL_ID = 1034 + + TRADE_SUBMIT_CONTROL_ID = 1006 + + COMMON_GRID_CONTROL_ID = 1047 + COMMON_GRID_LEFT_MARGIN = 10 COMMON_GRID_FIRST_ROW_HEIGHT = 30 COMMON_GRID_ROW_HEIGHT = 16 @@ -28,20 +41,6 @@ class CommonConfig: '总资产': 1015 } - -class YH(CommonConfig): - DEFAULT_EXE_PATH = r'C:\中国银河证券双子星3.2\Binarystar.exe' - TITLE = '网上股票交易系统5.0' - - TRADE_SECURITY_CONTROL_ID = 1032 - TRADE_PRICE_CONTROL_ID = 1033 - TRADE_AMOUNT_CONTROL_ID = 1034 - - TRADE_SUBMIT_CONTROL_ID = 1006 - - COMMON_GRID_CONTROL_ID = 1047 - BALANCE_GRID_CONTROL_ID = 1308 - POP_DIALOD_TITLE_CONTROL_ID = 1365 GRID_DTYPE = { @@ -63,20 +62,31 @@ class YH(CommonConfig): AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 AUTO_IPO_BUTTON_CONTROL_ID = 1006 - AUTO_IPO_MENU_PATH = ['新股申购', '一键打新'] + AUTO_IPO_MENU_PATH = ['新股申购', '批量新股申购'] -class HT(CommonConfig): - DEFAULT_EXE_PATH = r'C:\htzqzyb2\xiadan.exe' - TITLE = '网上股票交易系统5.0' +class YH(CommonConfig): + DEFAULT_EXE_PATH = r'C:\中国银河证券双子星3.2\Binarystar.exe' - TRADE_SECURITY_CONTROL_ID = 1032 - TRADE_PRICE_CONTROL_ID = 1033 - TRADE_AMOUNT_CONTROL_ID = 1034 + BALANCE_GRID_CONTROL_ID = 1308 - TRADE_SUBMIT_CONTROL_ID = 1006 + GRID_DTYPE = { + '操作日期': str, + '委托编号': str, + '申请编号': str, + '合同编号': str, + '证券代码': str, + '股东代码': str, + '资金帐号': str, + '资金帐户': str, + '发生日期': str + } + + AUTO_IPO_MENU_PATH = ['新股申购', '一键打新'] - COMMON_GRID_CONTROL_ID = 1047 + +class HT(CommonConfig): + DEFAULT_EXE_PATH = r'C:\htzqzyb2\xiadan.exe' BALANCE_CONTROL_ID_GROUP = { '资金余额': 1012, @@ -87,8 +97,6 @@ class HT(CommonConfig): '总资产': 1015 } - POP_DIALOD_TITLE_CONTROL_ID = 1365 - GRID_DTYPE = { '操作日期': str, '委托编号': str, @@ -101,30 +109,11 @@ class HT(CommonConfig): '发生日期': str } - CANCEL_ENTRUST_ENTRUST_FIELD = '合同编号' - CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50 - CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30 - CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16 - - AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 - AUTO_IPO_BUTTON_CONTROL_ID = 1006 AUTO_IPO_MENU_PATH = ['新股申购', '批量新股申购'] class GJ(CommonConfig): DEFAULT_EXE_PATH = 'C:\\全能行证券交易终端\\xiadan.exe' - TITLE = '网上股票交易系统5.0' - - TRADE_SECURITY_CONTROL_ID = 1032 - TRADE_PRICE_CONTROL_ID = 1033 - TRADE_AMOUNT_CONTROL_ID = 1034 - - TRADE_SUBMIT_CONTROL_ID = 1006 - - COMMON_GRID_CONTROL_ID = 1047 - BALANCE_GRID_CONTROL_ID = 1047 - - POP_DIALOD_TITLE_CONTROL_ID = 1365 GRID_DTYPE = { '操作日期': str, @@ -138,15 +127,4 @@ class GJ(CommonConfig): '发生日期': str } - CANCEL_ENTRUST_ENTRUST_FIELD = '合同编号' - CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50 - CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30 - CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16 - - AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 - AUTO_IPO_BUTTON_CONTROL_ID = 1006 - - ENABLE_BALANCE_TEXT_ID = 0x3f8 - TOTAL_BALANCE_TEXT_ID = 0x3f7 - AUTO_IPO_MENU_PATH = ['新股申购', '新股批量申购'] From 11aa299f7a0326a9cb9eeb0e4064e67bcabcdb19 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 12 Jan 2018 10:34:37 +0800 Subject: [PATCH 056/276] =?UTF-8?q?feature:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=B8=82=E4=BB=B7=E4=BA=A4=E6=98=93=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 79 +++++++++++++++++++++++++++++++++++++ easytrader/config/client.py | 2 + 2 files changed, 81 insertions(+) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 796c3b3d..ec56969b 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -121,6 +121,70 @@ def sell(self, security, price, amount, **kwargs): return self.trade(security, price, amount) + def market_buy(self, security, amount, ttype=None, **kwargs): + """ + 市价买入 + :param security: 六位证券代码 + :param amount: 交易数量 + :param ttype: 市价委托类型,默认客户端默认选择, + 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] + 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] + + :return: {'entrust_no': '委托单号'} + """ + self._switch_left_menus(['市价委托', '买入']) + + return self.market_trade(security, amount, ttype) + + def market_sell(self, security, amount, ttype=None, **kwargs): + """ + 市价卖出 + :param security: 六位证券代码 + :param amount: 交易数量 + :param ttype: 市价委托类型,默认客户端默认选择, + 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] + 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] + + :return: {'entrust_no': '委托单号'} + """ + self._switch_left_menus(['市价委托', '卖出']) + + return self.market_trade(security, amount, ttype) + + def market_trade(self, security, amount, ttype=None, **kwargs): + """ + 市价交易 + :param security: 六位证券代码 + :param amount: 交易数量 + :param ttype: 市价委托类型,默认客户端默认选择, + 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] + 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] + + :return: {'entrust_no': '委托单号'} + """ + self._set_market_trade_params(security, amount) + if ttype is not None: + self._set_market_trade_type(ttype) + self._submit_trade() + + return self._handle_trade_pop_dialog() + + def _set_market_trade_type(self, ttype): + """根据选择的市价交易类型选择对应的下拉选项""" + selects = self._main( + control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, + class_name='ComboBox' + ) + for i, text in selects.texts(): + # skip 0 index, because 0 index is current select index + if i == 0: + continue + if ttype in text: + selects.select(i - 1) + break + else: + raise TypeError('不支持对应的市价类型: {}'.format(ttype)) + def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) @@ -185,6 +249,9 @@ def trade(self, security, price, amount): self._submit_trade() + return self._handle_trade_pop_dialog() + + def _handle_trade_pop_dialog(self): while self._main.wrapper_object() != self._app.top_window().wrapper_object(): pop_title = self._get_pop_dialog_title() if pop_title == '委托确认': @@ -243,6 +310,18 @@ def _set_trade_params(self, security, price, amount): str(int(amount)) ) + def _set_market_trade_params(self, security, amount): + code = security[-6:] + + self._type_keys( + self._config.TRADE_SECURITY_CONTROL_ID, + code + ) + self._type_keys( + self._config.TRADE_AMOUNT_CONTROL_ID, + str(int(amount)) + ) + def _get_grid_data(self, control_id): grid = self._main.window( control_id=control_id, diff --git a/easytrader/config/client.py b/easytrader/config/client.py index ac760e1e..74766c8d 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -22,6 +22,8 @@ class CommonConfig: TRADE_SUBMIT_CONTROL_ID = 1006 + TRADE_MARKET_TYPE_CONTROL_ID = 1541 + COMMON_GRID_CONTROL_ID = 1047 COMMON_GRID_LEFT_MARGIN = 10 From 85cc6c7d68e6d49ddf0d2397dd33ac336360e772 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 12 Jan 2018 10:36:37 +0800 Subject: [PATCH 057/276] feature: increase wait time between operations for system stability --- easytrader/clienttrader.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index ec56969b..7740e671 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -252,6 +252,7 @@ def trade(self, security, price, amount): return self._handle_trade_pop_dialog() def _handle_trade_pop_dialog(self): + self._wait(0.2) # wait dialog display while self._main.wrapper_object() != self._app.top_window().wrapper_object(): pop_title = self._get_pop_dialog_title() if pop_title == '委托确认': @@ -301,6 +302,10 @@ def _set_trade_params(self, security, price, amount): self._config.TRADE_SECURITY_CONTROL_ID, code ) + + # wait security input finish + self._wait(0.1) + self._type_keys( self._config.TRADE_PRICE_CONTROL_ID, easyutils.round_price_by_code(price, code) @@ -317,6 +322,10 @@ def _set_market_trade_params(self, security, amount): self._config.TRADE_SECURITY_CONTROL_ID, code ) + + # wait security input finish + self._wait(0.1) + self._type_keys( self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount)) From 5fc6b4550097ea3c367f366d7054a6167cad1cdf Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 12 Jan 2018 10:37:21 +0800 Subject: [PATCH 058/276] update version from 0.12.8 to 0.13.0 --- easytrader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index ac73e1b0..bdca2844 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.12.8' +__version__ = '0.13.0' __author__ = 'shidenggui' From cd75e9810ae88c9317548481a73d47a8241e6eb3 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 12 Jan 2018 10:41:27 +0800 Subject: [PATCH 059/276] update docs --- docs/install.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/install.md b/docs/install.md index 767395bd..230bf9da 100644 --- a/docs/install.md +++ b/docs/install.md @@ -9,6 +9,10 @@ 同时客户端不能最小化也不能处于精简模式 +### 云端部署建议 + +在云服务上部署时,使用自带的远程桌面会有问题,推荐使用 TightVNC + ### 登陆时的验证码识别 银河可以直接自动登录, 其他券商如果登陆需要识别验证码的话需要安装 tesseract: From 3ce7c84198cde765e9bff8b4f9f091eb6467ef61 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 12 Jan 2018 15:53:33 +0800 Subject: [PATCH 060/276] update docs --- .github/ISSUE_TEMPLATE.md | 2 +- docs/index.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 173c9c67..6857d15e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ ## env -OS: win / mac / linux +OS: win7/ win10 / mac / linux PYTHON_VERSION: 3.x EASYTRADER_VERSION: 0.xx.xx diff --git a/docs/index.md b/docs/index.md index d9d5a63a..0ade6da2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,6 +23,7 @@ * 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) +* 其他券商通用同花顺客户端(需要手动登陆) ### 实盘易 From 2f67a7fd69209dfc730662aa7fc4bb74ba5f3135 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 15 Jan 2018 10:45:26 +0800 Subject: [PATCH 061/276] * add more detail error description for common ths client * fix connect common trade window error when trade window title isn't default ths titile * update docs * update version --- docs/usage.md | 27 +++++++++------------------ easytrader/__init__.py | 2 +- easytrader/clienttrader.py | 15 +++++++++++---- easytrader/config/client.py | 2 ++ test_easytrader.py | 17 +++++++++++++++-- 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index cd57c3de..13543481 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -4,7 +4,7 @@ import easytrader ``` -**设置账户**: +# 设置券商类型 **银河客户端** @@ -25,35 +25,26 @@ user = easytrader.use('gj_client') **通用同花顺客户端** + ```python -user = easytrader.use('ths') +user = easytrader.use('ths') ``` +注: 通用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本), 海王星(通达信版本) -**雪球** - - 雪球配置中 `username` 为邮箱, `account` 为手机, 填两者之一即可,另一项改为 `""`, 密码直接填写登录的明文密码即可,不需要抓取 `POST` 的密码 - -**银河/国金客户端** -客户端直接使用明文的账号和密码即可 - -**华泰客户端** - -华泰客户端直接使用明文账号、交易密码及通讯密码 - -# 登录帐号 +# 设置账户信息 登陆账号有两种方式,`使用参数` 和 `使用配置文件` +使用通用同花顺客户端不支持自动登陆,所以无需设置,参看下文`直接连接通用同花顺客户端` + **参数登录(推荐)** ``` user.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码,其他券商不用') ``` -**注:**雪球额外有个 account 参数,见上文介绍 - **使用配置文件** ```python @@ -87,10 +78,10 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 # 直接连接通用同花顺客户端 -需要先手动登陆客户端,然后运用下面的代码连接客户端 +需要先手动登陆客户端到交易窗口,然后运用下面的代码连接交易窗口 ```python -user.connect('客户端exe路径') +user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' ``` diff --git a/easytrader/__init__.py b/easytrader/__init__.py index bdca2844..29961de4 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.0' +__version__ = '0.13.1' __author__ = 'shidenggui' diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 7740e671..c7778a65 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -4,19 +4,22 @@ import io import os import re +import sys import time from abc import abstractmethod import easyutils import pandas as pd -import pywinauto -import pywinauto.clipboard from . import exceptions from . import helpers from .config import client from .log import log +if not sys.platform.startswith('darwin'): + import pywinauto + import pywinauto.clipboard + class ClientTrader: def __init__(self): @@ -51,10 +54,14 @@ def connect(self, exe_path=None, **kwargs): :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe' :return: """ - self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path or self._config.DEFAULT_EXE_PATH), + connect_path = exe_path or self._config.DEFAULT_EXE_PATH + if connect_path is None: + raise ValueError('参数 exe_path 未设置,请设置客户端对应的 exe 地址,类似 C:\\客户端安装目录\\xiadan.exe') + + self._app = pywinauto.Application().connect(path=connect_path, timeout=10) self._close_prompt_windows() - self._main = self._app.window(title=self._config.TITLE) + self._main = self._app.top_window() @property def broker_type(self): diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 74766c8d..e6710737 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -14,8 +14,10 @@ def create(broker): class CommonConfig: + DEFAULT_EXE_PATH = None TITLE = '网上股票交易系统5.0' + TRADE_SECURITY_CONTROL_ID = 1032 TRADE_PRICE_CONTROL_ID = 1033 TRADE_AMOUNT_CONTROL_ID = 1034 diff --git a/test_easytrader.py b/test_easytrader.py index a1834bc3..0ce8cfba 100644 --- a/test_easytrader.py +++ b/test_easytrader.py @@ -7,8 +7,6 @@ sys.path.append('.') -import easytrader - TEST_CLIENTS = os.environ.get('EZ_TEST_CLIENTS', 'yh') @@ -16,6 +14,7 @@ class TestYhClientTrader(unittest.TestCase): @classmethod def setUpClass(cls): + import easytrader if 'yh' not in TEST_CLIENTS: return @@ -43,10 +42,12 @@ def test_cancel_entrust(self): result = self._user.cancel_entrust('123456789') def test_invalid_buy(self): + import easytrader with self.assertRaises(easytrader.exceptions.TradeError): result = self._user.buy('511990', 1, 1e10) def test_invalid_sell(self): + import easytrader with self.assertRaises(easytrader.exceptions.TradeError): result = self._user.sell('162411', 200, 1e10) @@ -58,6 +59,7 @@ def test_auto_ipo(self): class TestHTClientTrader(unittest.TestCase): @classmethod def setUpClass(cls): + import easytrader if 'ht' not in TEST_CLIENTS: return @@ -86,10 +88,12 @@ def test_cancel_entrust(self): result = self._user.cancel_entrust('123456789') def test_invalid_buy(self): + import easytrader with self.assertRaises(easytrader.exceptions.TradeError): result = self._user.buy('511990', 1, 1e10) def test_invalid_sell(self): + import easytrader with self.assertRaises(easytrader.exceptions.TradeError): result = self._user.sell('162411', 200, 1e10) @@ -97,5 +101,14 @@ def test_auto_ipo(self): self._user.auto_ipo() +class TestClientTrader(unittest.TestCase): + def test_connect(self): + from easytrader.clienttrader import ClientTrader + c = ClientTrader() + + with self.assertRaises(ValueError): + c.connect() + + if __name__ == '__main__': unittest.main() From f7c666a460a8914c6ee10656f97600f53901be4f Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 18 Jan 2018 17:09:45 +0800 Subject: [PATCH 062/276] fix dependency --- requirements.txt | 2 +- setup.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 82cbeda1..b04cbb85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ pywinauto bs4 requests -demjson dill click six @@ -11,3 +10,4 @@ pytesseract pandas pyperclip rqopen-client +easyutils diff --git a/setup.py b/setup.py index 4c410a7e..acf46747 100644 --- a/setup.py +++ b/setup.py @@ -88,10 +88,14 @@ url='https://github.com/shidenggui/easytrader', keywords='China stock trade', install_requires=[ - 'demjson', - 'requests', - 'six', - 'rqopen-client', + 'requests', + 'six', + 'rqopen-client', + 'easyutils', + 'flask', + 'pywinauto', + 'pillow', + 'pandas' ], classifiers=['Development Status :: 4 - Beta', 'Programming Language :: Python :: 2.6', From 50600987efad32b3b1512b54c032ea6c717d1206 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 18 Jan 2018 17:10:33 +0800 Subject: [PATCH 063/276] delete unused file --- httpserver.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 httpserver.py diff --git a/httpserver.py b/httpserver.py deleted file mode 100644 index ef091596..00000000 --- a/httpserver.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import print_function - -import json - -from flask import Flask, request, jsonify - -import easytrader - -app = Flask(__name__) -user = None - - -@app.route('/login') -def login(): - global user - args = request.args - user = easytrader.use(args['use']) - user.prepare(args['prepare']) - return jsonify({'message': 'login ok'}) - - -@app.route('/call') -def do(): - global user - target = request.args.get('func') - params_str = request.args.get('params', None) - - if params_str: - params = params_str.split(',') - if target in ['buy', 'sell']: - params[-1] = int(params[-1]) - params[-2] = float(params[-2]) - result = getattr(user, target)(*params) - else: - result = getattr(user, target) - return json.dumps({'return': result}, ensure_ascii=False) - - -if __name__ == '__main__': - app.run(debug=True) From 897f9588317c04c8978f50ff8cdd36844b33e48b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 20 Jan 2018 04:52:41 +0800 Subject: [PATCH 064/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=95=E6=97=A5?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E6=96=B0=E8=82=A1=E6=88=96=E8=80=85=E6=9C=89?= =?UTF-8?q?=E6=96=B0=E8=82=A1=E4=BD=86=E6=98=AF=E9=83=BD=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E9=A2=9D=E5=BA=A6=E6=97=B6=20auto=5Fipo()=20=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=87=BA=E9=94=99=E7=9A=84=E6=83=85=E5=86=B5=20fix=20#259?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 11 +++++++++-- test_easytrader.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index c7778a65..52af696b 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -196,11 +196,18 @@ def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - valid_list_idx = [i for i, v in enumerate(stock_list) if v['申购数量'] <= 0] + + if len(stock_list) == 0: + return {'message': '今日无新股'} + invalid_list_idx = [i for i, v in enumerate(stock_list) if v['申购数量'] <= 0] + + if len(stock_list) == len(invalid_list_idx): + return {'message': '没有发现可以申购的新股'} + self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) self._wait(0.1) - for row in valid_list_idx: + for row in invalid_list_idx: self._click_grid_by_row(row) self._wait(0.1) diff --git a/test_easytrader.py b/test_easytrader.py index 0ce8cfba..e5bf6762 100644 --- a/test_easytrader.py +++ b/test_easytrader.py @@ -4,6 +4,7 @@ import sys import time import unittest +from unittest import mock sys.path.append('.') @@ -109,6 +110,18 @@ def test_connect(self): with self.assertRaises(ValueError): c.connect() + def test_auto_ipo_with_failed_situation(self): + from easytrader.clienttrader import ClientTrader + c = ClientTrader() + with mock.patch.object(c, '_switch_left_menus'): + for case, res in [ + ([], {'message': '今日无新股'}), + ([{'申购数量': 0}], {'message': '没有发现可以申购的新股'}) + ]: + with mock.patch.object(c, '_get_grid_data') as ipo_list_mock: + ipo_list_mock.return_value = case + self.assertDictEqual(c.auto_ipo(), res) + if __name__ == '__main__': unittest.main() From fe686a410bb4226ee8f6e2e46d45a53f270b9ed1 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 20 Jan 2018 04:55:15 +0800 Subject: [PATCH 065/276] update version --- easytrader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 29961de4..2d9a72fc 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.1' +__version__ = '0.13.2' __author__ = 'shidenggui' From 321b1042dc42f58e24e97e112ff17a40773467b8 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 20 Jan 2018 04:57:36 +0800 Subject: [PATCH 066/276] update README.md --- README.md | 8 +------- docs/index.md | 7 +------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5b282439..7e06b2dd 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,7 @@ * 支持命令行调用,方便其他语言适配 * 支持 Python3, Linux / Win, 推荐使用 `Python3` * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 -* 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) 或者 银河开户可以加群找我 - -## 公众号 - -请扫码关注“易量化”的微信公众号,不定时更新`easytrader`的最新动态及量化方面的相关文章 - -![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) +* 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) **开发环境** : `Ubuntu 16.04` / `Python 3.5` diff --git a/docs/index.md b/docs/index.md index 0ade6da2..0a9f00f0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,13 +8,8 @@ * 支持远程操作客户端 * 支持 Python3 , Linux / Win / Mac * 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 -* 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) 或者 银河开户可以加群找我 +* 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) -## 公众号 - -请扫码关注“易量化”的微信公众号,不定时更新`easytrader`的最新动态及量化方面的相关文章 - -![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) **开发环境** : `OSX 10.12.3` / `Python 3.5` From 9a95fb0fa0f92e91fb0ef0f2c6ccd9b4b4009ace Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 20 Jan 2018 08:23:29 +0800 Subject: [PATCH 067/276] update README.md --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7e06b2dd..1904844b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ * 支持通过 webserver 远程操作客户端 * 支持命令行调用,方便其他语言适配 * 支持 Python3, Linux / Win, 推荐使用 `Python3` -* 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 +* 有兴趣的可以加群 `556050652` 一起讨论 * 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) diff --git a/docs/index.md b/docs/index.md index 0a9f00f0..fe412074 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ * 支持命令行调用,方便其他语言适配 * 支持远程操作客户端 * 支持 Python3 , Linux / Win / Mac -* 有兴趣的可以加群 `556050652` 、`549879767`(已满) 、`429011814`(已满) 一起讨论 +* 有兴趣的可以加群 `556050652` 一起讨论 * 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) From 6566b4d051be49e1b43a70ce079a73a9c742519e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 20 Jan 2018 22:37:47 +0800 Subject: [PATCH 068/276] refactor: extract duplicate pop dialog detect code to method --- easytrader/clienttrader.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 52af696b..b2932a92 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -224,8 +224,12 @@ def _click_grid_by_row(self, row): class_name='CVirtualGridCtrl' ).click(coords=(x, y)) + def _is_exist_pop_dialog(self): + return self._main.wrapper_object() != self._app.top_window().wrapper_object() + + def _handle_auto_ipo_pop_dialog(self): - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + while self._is_exist_pop_dialog(): title = self._get_pop_dialog_title() if '提示信息' in title or '委托确认' in title or '网上交易用户协议' in title: self._app.top_window().type_keys('%Y') @@ -267,7 +271,7 @@ def trade(self, security, price, amount): def _handle_trade_pop_dialog(self): self._wait(0.2) # wait dialog display - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + while self._is_exist_pop_dialog(): pop_title = self._get_pop_dialog_title() if pop_title == '委托确认': self._app.top_window().type_keys('%Y') @@ -399,7 +403,7 @@ def _format_grid_data(self, data): return df.to_dict('records') def _handle_cancel_entrust_pop_dialog(self): - while self._main.wrapper_object() != self._app.top_window().wrapper_object(): + while self._is_exist_pop_dialog(): title = self._get_pop_dialog_title() if '提示信息' in title: self._app.top_window().type_keys('%Y') From e0b5db442c64d51f2721e4a198556bd60fef189e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 20 Jan 2018 23:26:48 +0800 Subject: [PATCH 069/276] refactor: extract handle pop dialog code to PopDialog class, for DRY --- easytrader/clienttrader.py | 136 +++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 60 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index b2932a92..fe41e805 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -21,6 +21,62 @@ import pywinauto.clipboard +class PopDialog: + def __init__(self, app): + self._app = app + + def handle_common(self, title): + if any(s in title for s in + ['提示信息', '委托确认', '网上交易用户协议']): + self._submit_by_shortcut() + + elif '提示' in title: + content = self._extract_content() + self._submit_by_click() + return {'message': content} + + else: + content = self._extract_content() + self._close() + return {'message': 'unknown message: {}'.find(content)} + + def handle_trade(self, title): + if title == '委托确认': + self._submit_by_shortcut() + + elif title == '提示信息': + if '超出涨跌停' in self._extract_content(): + self._submit_by_shortcut() + + elif title == '提示': + content = self._extract_content() + if '成功' in content: + entrust_no = self._extract_entrust_id(content) + self._submit_by_click() + return {'entrust_no': entrust_no} + else: + self._submit_by_click() + time.sleep(0.05) + raise exceptions.TradeError(content) + else: + self._close() + + def _extract_content(self): + return self._app.top_window().Static.window_text() + + def _extract_entrust_id(self, content): + return re.search(r'\d+', content).group() + + def _submit_by_click(self): + self._app.top_window()['确定'].click() + + def _submit_by_shortcut(self): + self._app.top_window().type_keys('%Y') + + def _close(self): + self._app.top_window().close() + + class ClientTrader: def __init__(self): self._config = client.create(self.broker_type) @@ -114,7 +170,7 @@ def cancel_entrust(self, entrust_no): for i, entrust in enumerate(self.cancel_entrusts): if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: self._cancel_entrust_by_double_click(i) - return self._handle_cancel_entrust_pop_dialog() + return self._handle_common_pop_dialog() else: return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} @@ -214,7 +270,7 @@ def auto_ipo(self): self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) self._wait(0.1) - return self._handle_auto_ipo_pop_dialog() + return self._handle_common_pop_dialog() def _click_grid_by_row(self, row): x = self._config.COMMON_GRID_LEFT_MARGIN @@ -225,25 +281,9 @@ def _click_grid_by_row(self, row): ).click(coords=(x, y)) def _is_exist_pop_dialog(self): + self._wait(0.2) # wait dialog display return self._main.wrapper_object() != self._app.top_window().wrapper_object() - - def _handle_auto_ipo_pop_dialog(self): - while self._is_exist_pop_dialog(): - title = self._get_pop_dialog_title() - if '提示信息' in title or '委托确认' in title or '网上交易用户协议' in title: - self._app.top_window().type_keys('%Y') - elif '提示' in title: - data = self._app.top_window().Static.window_text() - self._app.top_window()['确定'].click() - return {'message': data} - else: - data = self._app.top_window().Static.window_text() - self._app.top_window().close() - return {'message': 'unkown message: {}'.find(data)} - self._wait(0.1) - return {'message': 'success'} - def _run_exe_path(self, exe_path): return os.path.join( os.path.dirname(exe_path), 'xiadan.exe' @@ -269,38 +309,12 @@ def trade(self, security, price, amount): return self._handle_trade_pop_dialog() - def _handle_trade_pop_dialog(self): - self._wait(0.2) # wait dialog display - while self._is_exist_pop_dialog(): - pop_title = self._get_pop_dialog_title() - if pop_title == '委托确认': - self._app.top_window().type_keys('%Y') - elif pop_title == '提示信息': - if '超出涨跌停' in self._app.top_window().Static.window_text(): - self._app.top_window().type_keys('%Y') - elif pop_title == '提示': - content = self._app.top_window().Static.window_text() - if '成功' in content: - entrust_no = self._extract_entrust_id(content) - self._app.top_window()['确定'].click() - return {'entrust_no': entrust_no} - else: - self._app.top_window()['确定'].click() - self._wait(0.05) - raise exceptions.TradeError(content) - else: - self._app.top_window().close() - self._wait(0.3) # wait next dialog display - def _click(self, control_id): self._app.top_window().window( control_id=control_id, class_name='Button' ).click() - def _extract_entrust_id(self, content): - return re.search(r'\d+', content).group() - def _submit_trade(self): time.sleep(0.05) self._app.top_window().window( @@ -402,21 +416,6 @@ def _format_grid_data(self, data): ) return df.to_dict('records') - def _handle_cancel_entrust_pop_dialog(self): - while self._is_exist_pop_dialog(): - title = self._get_pop_dialog_title() - if '提示信息' in title: - self._app.top_window().type_keys('%Y') - elif '提示' in title: - data = self._app.top_window().Static.window_text() - self._app.top_window()['确定'].click() - return {'message': data} - else: - data = self._app.top_window().Static.window_text() - self._app.top_window().close() - return {'message': 'unkown message: {}'.find(data)} - self._wait(0.2) - def _cancel_entrust_by_double_click(self, row): x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row @@ -427,3 +426,20 @@ def _cancel_entrust_by_double_click(self, row): def _refresh(self): self._switch_left_menus(['买入[F1]'], sleep=0.05) + + def _handle_trade_pop_dialog(self): + while self._is_exist_pop_dialog(): + title = self._get_pop_dialog_title() + + result = PopDialog(self._app).handle_trade(title) + if result: + return result + + def _handle_common_pop_dialog(self): + while self._is_exist_pop_dialog(): + title = self._get_pop_dialog_title() + + result = PopDialog(self._app).handle_common(title) + if result: + return result + return {'message': 'success'} From 663a1673e84356f1837f328472902fddc77b0196 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 21 Jan 2018 10:28:09 +0800 Subject: [PATCH 070/276] fix: fix ht buy/sell button cant click error fix #257 --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index fe41e805..f0f170f2 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -317,7 +317,7 @@ def _click(self, control_id): def _submit_trade(self): time.sleep(0.05) - self._app.top_window().window( + self._main.window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name='Button' ).click() From d02268bff5f4c2780712279445dedc74c2102c09 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 21 Jan 2018 10:28:41 +0800 Subject: [PATCH 071/276] fix: handle pop dialog function add missing condition --- easytrader/clienttrader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index f0f170f2..7972ae88 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -45,7 +45,10 @@ def handle_trade(self, title): self._submit_by_shortcut() elif title == '提示信息': - if '超出涨跌停' in self._extract_content(): + content = self._extract_content() + if '超出涨跌停' in content: + self._submit_by_shortcut() + elif '委托价格的小数价格应为' in content: self._submit_by_shortcut() elif title == '提示': From 1124e801fba0214b9a72cb62322d939e40af3f12 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 21 Jan 2018 10:29:09 +0800 Subject: [PATCH 072/276] update version to 0.13.3 --- easytrader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 2d9a72fc..d99140c2 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.2' +__version__ = '0.13.3' __author__ = 'shidenggui' From c6555b92bf579eb5c2eff06a1dbf67bc6d5011ae Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 21 Jan 2018 14:41:17 +0800 Subject: [PATCH 073/276] refactor: merge duplicated code --- easytrader/clienttrader.py | 58 ++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 7972ae88..6c56f382 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -21,11 +21,11 @@ import pywinauto.clipboard -class PopDialog: +class PopDialogHandler: def __init__(self, app): self._app = app - def handle_common(self, title): + def handle(self, title): if any(s in title for s in ['提示信息', '委托确认', '网上交易用户协议']): self._submit_by_shortcut() @@ -40,7 +40,24 @@ def handle_common(self, title): self._close() return {'message': 'unknown message: {}'.find(content)} - def handle_trade(self, title): + def _extract_content(self): + return self._app.top_window().Static.window_text() + + def _extract_entrust_id(self, content): + return re.search(r'\d+', content).group() + + def _submit_by_click(self): + self._app.top_window()['确定'].click() + + def _submit_by_shortcut(self): + self._app.top_window().type_keys('%Y') + + def _close(self): + self._app.top_window().close() + + +class TradePopDialogHandler(PopDialogHandler): + def handle(self, title): if title == '委托确认': self._submit_by_shortcut() @@ -64,21 +81,6 @@ def handle_trade(self, title): else: self._close() - def _extract_content(self): - return self._app.top_window().Static.window_text() - - def _extract_entrust_id(self, content): - return re.search(r'\d+', content).group() - - def _submit_by_click(self): - self._app.top_window()['确定'].click() - - def _submit_by_shortcut(self): - self._app.top_window().type_keys('%Y') - - def _close(self): - self._app.top_window().close() - class ClientTrader: def __init__(self): @@ -173,7 +175,7 @@ def cancel_entrust(self, entrust_no): for i, entrust in enumerate(self.cancel_entrusts): if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: self._cancel_entrust_by_double_click(i) - return self._handle_common_pop_dialog() + return self._handle_pop_dialogs() else: return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} @@ -233,7 +235,7 @@ def market_trade(self, security, amount, ttype=None, **kwargs): self._set_market_trade_type(ttype) self._submit_trade() - return self._handle_trade_pop_dialog() + return self._handle_pop_dialogs(handler_class=TradePopDialogHandler) def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" @@ -273,7 +275,7 @@ def auto_ipo(self): self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) self._wait(0.1) - return self._handle_common_pop_dialog() + return self._handle_pop_dialogs() def _click_grid_by_row(self, row): x = self._config.COMMON_GRID_LEFT_MARGIN @@ -310,7 +312,7 @@ def trade(self, security, price, amount): self._submit_trade() - return self._handle_trade_pop_dialog() + return self._handle_pop_dialogs(handler_class=TradePopDialogHandler) def _click(self, control_id): self._app.top_window().window( @@ -430,19 +432,13 @@ def _cancel_entrust_by_double_click(self, row): def _refresh(self): self._switch_left_menus(['买入[F1]'], sleep=0.05) - def _handle_trade_pop_dialog(self): - while self._is_exist_pop_dialog(): - title = self._get_pop_dialog_title() - - result = PopDialog(self._app).handle_trade(title) - if result: - return result + def _handle_pop_dialogs(self, handler_class=PopDialogHandler): + handler = handler_class(self._app) - def _handle_common_pop_dialog(self): while self._is_exist_pop_dialog(): title = self._get_pop_dialog_title() - result = PopDialog(self._app).handle_common(title) + result = handler.handle(title) if result: return result return {'message': 'success'} From 2310359f7e7d4cea860b9a294011048c30700f23 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 21 Jan 2018 14:44:23 +0800 Subject: [PATCH 074/276] fix: fix typo --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 6c56f382..1fff07dd 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -38,7 +38,7 @@ def handle(self, title): else: content = self._extract_content() self._close() - return {'message': 'unknown message: {}'.find(content)} + return {'message': 'unknown message: {}'.format(content)} def _extract_content(self): return self._app.top_window().Static.window_text() From 426596263a83e113490bb2ec0979674294bf3210 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 22 Jan 2018 17:07:34 +0800 Subject: [PATCH 075/276] refactor: use set replace list --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 1fff07dd..2d9d1c4a 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -27,7 +27,7 @@ def __init__(self, app): def handle(self, title): if any(s in title for s in - ['提示信息', '委托确认', '网上交易用户协议']): + {'提示信息', '委托确认', '网上交易用户协议'}): self._submit_by_shortcut() elif '提示' in title: From f15ebc7f15c41578c3e0cab044f8f0e55f6663ce Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 22 Jan 2018 23:13:15 +0800 Subject: [PATCH 076/276] refactor: server use decorater replace duplicated code --- easytrader/server.py | 106 +++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/easytrader/server.py b/easytrader/server.py index fe159657..d5579a5e 100644 --- a/easytrader/server.py +++ b/easytrader/server.py @@ -1,135 +1,131 @@ +import functools + from flask import Flask, request, jsonify from . import api +from .log import log app = Flask(__name__) global_store = {} +def error_handle(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + log.exception('server error') + message = '{}: {}'.format(e.__class__, e) + return jsonify({'error': message}), 400 + + return wrapper + + @app.route('/prepare', methods=['POST']) +@error_handle def post_prepare(): json_data = request.get_json(force=True) - try: - user = api.use(json_data.pop('broker')) - user.prepare(**json_data) - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = api.use(json_data.pop('broker')) + user.prepare(**json_data) global_store['user'] = user return jsonify({'msg': 'login success'}), 201 @app.route('/balance', methods=['GET']) +@error_handle def get_balance(): - try: - user = global_store['user'] - balance = user.balance - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + balance = user.balance return jsonify(balance), 200 @app.route('/position', methods=['GET']) +@error_handle def get_position(): - try: - user = global_store['user'] - position = user.position - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + position = user.position return jsonify(position), 200 @app.route('/auto_ipo', methods=['GET']) +@error_handle def get_auto_ipo(): - try: - user = global_store['user'] - res = user.auto_ipo() - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + res = user.auto_ipo() return jsonify(res), 200 @app.route('/today_entrusts', methods=['GET']) +@error_handle def get_today_entrusts(): - try: - user = global_store['user'] - today_entrusts = user.today_entrusts - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + today_entrusts = user.today_entrusts return jsonify(today_entrusts), 200 @app.route('/today_trades', methods=['GET']) +@error_handle def get_today_trades(): - try: - user = global_store['user'] - today_trades = user.today_trades - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + today_trades = user.today_trades return jsonify(today_trades), 200 @app.route('/cancel_entrusts', methods=['GET']) +@error_handle def get_cancel_entrusts(): - try: - user = global_store['user'] - cancel_entrusts = user.cancel_entrusts - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + cancel_entrusts = user.cancel_entrusts return jsonify(cancel_entrusts), 200 @app.route('/buy', methods=['POST']) +@error_handle def post_buy(): json_data = request.get_json(force=True) - try: - user = global_store['user'] - res = user.buy(**json_data) - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + res = user.buy(**json_data) return jsonify(res), 201 @app.route('/sell', methods=['POST']) +@error_handle def post_sell(): json_data = request.get_json(force=True) - try: - user = global_store['user'] - res = user.sell(**json_data) - except Exception as e: - return jsonify({'error': str(e)}), 400 + + user = global_store['user'] + res = user.sell(**json_data) return jsonify(res), 201 @app.route('/cancel_entrust', methods=['POST']) +@error_handle def post_cancel_entrust(): json_data = request.get_json(force=True) - try: - user = global_store['user'] - res = user.cancel_entrust(**json_data) - except Exception as e: - return jsonify({'error': str(e)}), 400 + + user = global_store['user'] + res = user.cancel_entrust(**json_data) return jsonify(res), 201 @app.route('/exit', methods=['GET']) +@error_handle def get_exit(): - try: - user = global_store['user'] - user.exit() - except Exception as e: - return jsonify({'error': str(e)}), 400 + user = global_store['user'] + user.exit() return jsonify({'msg': 'exit success'}), 200 From d39b4582ff04f4eaae93dad60c55e10d241e3606 Mon Sep 17 00:00:00 2001 From: ahui132 <1781999+ahui132@users.noreply.github.com> Date: Sat, 27 Jan 2018 09:49:37 +0800 Subject: [PATCH 077/276] =?UTF-8?q?clienttrader=E4=B8=AD=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=94=AF=E6=8C=81exe=5Fpath=E5=92=8Ccomm=5Fp?= =?UTF-8?q?assword=20(#262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ cli.py | 2 +- easytrader/clienttrader.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ec2d826e..0688ba4b 100755 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ gft.json test.py ht_account.json .idea +.vscode .ipynb_checkpoints Untitled.ipynb untitled.txt @@ -11,6 +12,7 @@ untitled.txt __pycache__/ *.py[cod] account.json +account.session # C extensions *.so diff --git a/cli.py b/cli.py index 10fd4583..1bcb62b5 100644 --- a/cli.py +++ b/cli.py @@ -20,7 +20,7 @@ def main(prepare, use, do, get, params, debug): if get is not None: do = get - if prepare is not None and use in ['ht', 'yjb', 'yh', 'gf', 'xq']: + if prepare is not None and use in ['ht_client', 'yjb', 'yh_client','yh','ht', 'gf', 'xq']: user = easytrader.use(use, debug) user.prepare(prepare) with open(ACCOUNT_OBJECT_FILE, 'wb') as f: diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 2d9d1c4a..a38e9121 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -103,6 +103,8 @@ def prepare(self, config_path=None, user=None, password=None, exe_path=None, com account = helpers.file2dict(config_path) user = account['user'] password = account['password'] + comm_password = account.get('comm_password') + exe_path = account.get('exe_path') self.login(user, password, exe_path or self._config.DEFAULT_EXE_PATH, comm_password, **kwargs) @abstractmethod From 8c3a691591c7ea37452e443c94d39f127c15088e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 28 Jan 2018 21:48:59 +0800 Subject: [PATCH 078/276] update version from 0.13.3 to 0.13.4 --- easytrader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index d99140c2..3ac9daba 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.3' +__version__ = '0.13.4' __author__ = 'shidenggui' From 1b33f0a9223a60a6c8b112aac428c73f62285bc2 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 5 Feb 2018 13:49:31 +0800 Subject: [PATCH 079/276] fix typo close #265 --- easytrader/remoteclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/remoteclient.py b/easytrader/remoteclient.py index cf8513ab..0c06af7b 100644 --- a/easytrader/remoteclient.py +++ b/easytrader/remoteclient.py @@ -31,7 +31,7 @@ def prepare(self, config_path=None, user=None, password=None, exe_path=None, com if config_path is not None: account = helpers.file2dict(config_path) params['user'] = account['user'] - password['password'] = account['password'] + params['password'] = account['password'] params['broker'] = self._broker From 9db0bea56577796e6cbbf367b58e2a2fc48025c5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 6 Feb 2018 11:00:47 +0800 Subject: [PATCH 080/276] fix: rename stock_code => security --- easytrader/follower.py | 2 +- easytrader/xqtrader.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/easytrader/follower.py b/easytrader/follower.py index c2d41652..fdb74097 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -216,7 +216,7 @@ def trade_worker(self, users, expire_seconds=120, entrust_prop='limit', send_int break args = { - 'stock_code': trade_cmd['stock_code'], + 'security': trade_cmd['stock_code'], 'price': trade_cmd['price'], 'amount': trade_cmd['amount'], 'entrust_prop': entrust_prop diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index 19d41f36..f7bcc12d 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -282,7 +282,7 @@ def cancel_entrust(self, entrust_no): raise TradeError(u"移除的股票操作无法撤销,建议重新买入") balance = self.get_balance()[0] volume = abs(entrust['target_weight'] - entrust['weight']) * balance['asset_balance'] / 100 - r = self.__trade(stock_code=entrust['stock_symbol'], volume=volume, entrust_bs=bs) + r = self.__trade(security=entrust['stock_symbol'], volume=volume, entrust_bs=bs) if len(r) > 0 and 'error_info' in r[0]: raise TradeError(u"撤销失败!%s" % ('error_info' in r[0])) if not is_have: @@ -362,17 +362,17 @@ def adjust_weight(self, stock_code, weight): else: log.debug('调仓成功 %s: 持仓比例%d' % (stock['name'], weight)) - def __trade(self, stock_code, price=0, amount=0, volume=0, entrust_bs='buy'): + def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): """ 调仓 - :param stock_code: + :param security: :param price: :param amount: :param volume: :param entrust_bs: :return: """ - stock = self.__search_stock_info(stock_code) + stock = self.__search_stock_info(security) balance = self.get_balance()[0] if stock is None: raise TradeError(u"没有查询要操作的股票信息") @@ -469,27 +469,27 @@ def __trade(self, stock_code, price=0, amount=0, volume=0, entrust_bs='buy'): 'entrust_time': self.__time_strftime(rebalance_status['updated_at']), 'entrust_price': price, 'entrust_amount': amount, - 'stock_code': stock_code, + 'stock_code': security, 'entrust_bs': '买入', 'entrust_type': '雪球虚拟委托', 'entrust_status': '-'}] - def buy(self, stock_code, price=0, amount=0, volume=0, entrust_prop=0): + def buy(self, security, price=0, amount=0, volume=0, entrust_prop=0): """买入卖出股票 - :param stock_code: 股票代码 + :param security: 股票代码 :param price: 买入价格 :param amount: 买入股数 :param volume: 买入总金额 由 volume / price 取整, 若指定 price 则此参数无效 :param entrust_prop: """ - return self.__trade(stock_code, price, amount, volume, 'buy') + return self.__trade(security, price, amount, volume, 'buy') - def sell(self, stock_code, price=0, amount=0, volume=0, entrust_prop=0): + def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0): """卖出股票 - :param stock_code: 股票代码 + :param security: 股票代码 :param price: 卖出价格 :param amount: 卖出股数 :param volume: 卖出总金额 由 volume / price 取整, 若指定 price 则此参数无效 :param entrust_prop: """ - return self.__trade(stock_code, price, amount, volume, 'sell') + return self.__trade(security, price, amount, volume, 'sell') From 4450bdc70158d940e2563d5375a39f07dd2c3b6d Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 6 Feb 2018 11:01:16 +0800 Subject: [PATCH 081/276] update version 0.13.4 to 0.13.5 --- easytrader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 3ac9daba..77bf2a70 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.4' +__version__ = '0.13.5' __author__ = 'shidenggui' From 25c4d8d69b5b6b5bda60eae74598804bf83d6518 Mon Sep 17 00:00:00 2001 From: Kim Date: Tue, 6 Feb 2018 13:39:23 +0800 Subject: [PATCH 082/276] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8D=8E=E6=B3=B0?= =?UTF-8?q?=E6=96=B0=E7=89=88=E6=9C=AC=E7=9A=84=E4=B8=80=E4=BA=9B=E5=B0=8F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/ht_clienttrader.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index e69e9ad4..5adf6fa9 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -32,6 +32,9 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): while True: try: self._app.top_window().Edit1.wait('ready') + #dlg = self._app.window(title='华泰证券网上证券交易分析系统') + #dlg.wrapper_object() + #dlg.button0.click() break except RuntimeError: pass @@ -41,7 +44,9 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().Edit3.type_keys(comm_password) - self._app.top_window().type_keys('%Y') + # dlg = self._app.window(title='用户登陆') + # dlg.wrapper_object() + self._app.top_window().button0.click() # detect login is success or not self._app.top_window().wait_not('exists', 10) From 4cacda57c026d0f50762a5959555d818688a5985 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Apr 2018 11:37:34 +0800 Subject: [PATCH 083/276] update docs --- docs/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.md b/docs/index.md index fe412074..2dedac9e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,8 @@ * 国金客户端(全能行证券交易终端PC版) * 其他券商通用同花顺客户端(需要手动登陆) +注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,下面在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供几个老版本同花顺的下载地址。如有大家有补充的也可以再下面回复 + ### 实盘易 From 826a5480b36ca86ca855bd30fbdd1214cb7c52c4 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Apr 2018 11:39:36 +0800 Subject: [PATCH 084/276] update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1904844b..cbcd9ea7 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) +注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,下面在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供几个老版本同花顺的下载地址。如有大家有补充的也可以再下面回复 + ### 实盘易 From 45db710928b114942968b1f0859324a86a087182 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Apr 2018 11:41:08 +0800 Subject: [PATCH 085/276] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbcd9ea7..be678ec4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) -注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,下面在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供几个老版本同花顺的下载地址。如有大家有补充的也可以再下面回复 +注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,下面在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供几个老版本的下载地址。 ### 实盘易 From e5d2609f9155b040dfa621ea92a6544c1514e81e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Apr 2018 11:41:36 +0800 Subject: [PATCH 086/276] refactor: remove outdated test code --- test_easytrader.py | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/test_easytrader.py b/test_easytrader.py index e5bf6762..5cdd127a 100644 --- a/test_easytrader.py +++ b/test_easytrader.py @@ -21,7 +21,8 @@ def setUpClass(cls): # input your test account and password cls._ACCOUNT = os.environ.get('EZ_TEST_YH_ACCOUNT') or 'your account' - cls._PASSWORD = os.environ.get('EZ_TEST_YH_password') or 'your password' + cls._PASSWORD = os.environ.get( + 'EZ_TEST_YH_password') or 'your password' cls._user = easytrader.use('yh_client') cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD) @@ -66,11 +67,16 @@ def setUpClass(cls): # input your test account and password cls._ACCOUNT = os.environ.get('EZ_TEST_HT_ACCOUNT') or 'your account' - cls._PASSWORD = os.environ.get('EZ_TEST_HT_password') or 'your password' - cls._COMM_PASSWORD = os.environ.get('EZ_TEST_HT_comm_password') or 'your comm password' + cls._PASSWORD = os.environ.get( + 'EZ_TEST_HT_password') or 'your password' + cls._COMM_PASSWORD = os.environ.get( + 'EZ_TEST_HT_comm_password') or 'your comm password' cls._user = easytrader.use('ht_client') - cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD) + cls._user.prepare( + user=cls._ACCOUNT, + password=cls._PASSWORD, + comm_password=cls._COMM_PASSWORD) def test_balance(self): time.sleep(3) @@ -102,26 +108,5 @@ def test_auto_ipo(self): self._user.auto_ipo() -class TestClientTrader(unittest.TestCase): - def test_connect(self): - from easytrader.clienttrader import ClientTrader - c = ClientTrader() - - with self.assertRaises(ValueError): - c.connect() - - def test_auto_ipo_with_failed_situation(self): - from easytrader.clienttrader import ClientTrader - c = ClientTrader() - with mock.patch.object(c, '_switch_left_menus'): - for case, res in [ - ([], {'message': '今日无新股'}), - ([{'申购数量': 0}], {'message': '没有发现可以申购的新股'}) - ]: - with mock.patch.object(c, '_get_grid_data') as ipo_list_mock: - ipo_list_mock.return_value = case - self.assertDictEqual(c.auto_ipo(), res) - - if __name__ == '__main__': unittest.main() From ce3cc5385184bcc0cb8d29bffbc4d829e0ff1e5e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Apr 2018 11:42:55 +0800 Subject: [PATCH 087/276] refactor: remove outdated test code --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index be678ec4..9863727f 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,9 @@ * 银河客户端, 须在 `windows` 平台下载 `银河双子星` 客户端 * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) +* 其他券商通用同花顺客户端(需要手动登陆) -注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,下面在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供几个老版本的下载地址。 +注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,我在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供了几个券商老版本的下载地址。 ### 实盘易 From 9fd751cdd26a4691691e29b46103f0baa2f5e226 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 5 Apr 2018 16:54:35 +0800 Subject: [PATCH 088/276] :star: use bumpversion --- .bumpversion.cfg | 6 +++ setup.py | 38 +++++++++---------- .../test_easytrader.py | 6 ++- 3 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 .bumpversion.cfg rename test_easytrader.py => tests/test_easytrader.py (93%) diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 00000000..1dbea736 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,6 @@ +[bumpversion] +current_version = 0.13.5 +commit = True +files = easytrader/__init__.py setup.py +tag = True +tag_name = {new_version} diff --git a/setup.py b/setup.py index acf46747..12e2ed95 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,6 @@ # coding:utf8 from setuptools import setup -import easytrader - long_desc = """ easytrader =============== @@ -79,7 +77,7 @@ setup( name='easytrader', - version=easytrader.__version__, + version='0.13.5', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', @@ -88,23 +86,23 @@ url='https://github.com/shidenggui/easytrader', keywords='China stock trade', install_requires=[ - 'requests', - 'six', - 'rqopen-client', - 'easyutils', - 'flask', - 'pywinauto', - 'pillow', - 'pandas' + 'requests', 'six', 'rqopen-client', 'easyutils', 'flask', 'pywinauto', + 'pillow', 'pandas' + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'License :: OSI Approved :: BSD License' ], - classifiers=['Development Status :: 4 - Beta', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'License :: OSI Approved :: BSD License'], packages=['easytrader', 'easytrader.config'], - package_data={'': ['*.jar', '*.json'], 'config': ['config/*.json'], 'thirdlibrary': ['thirdlibrary/*.jar']}, + package_data={ + '': ['*.jar', '*.json'], + 'config': ['config/*.json'], + 'thirdlibrary': ['thirdlibrary/*.jar'] + }, ) diff --git a/test_easytrader.py b/tests/test_easytrader.py similarity index 93% rename from test_easytrader.py rename to tests/test_easytrader.py index 5cdd127a..22fe2357 100644 --- a/test_easytrader.py +++ b/tests/test_easytrader.py @@ -10,8 +10,10 @@ TEST_CLIENTS = os.environ.get('EZ_TEST_CLIENTS', 'yh') +IS_WIN_PLATFORM = sys.platform != 'darwin' -@unittest.skipUnless('yh' in TEST_CLIENTS, 'skip yh test') + +@unittest.skipUnless('yh' in TEST_CLIENTS and IS_WIN_PLATFORM, 'skip yh test') class TestYhClientTrader(unittest.TestCase): @classmethod def setUpClass(cls): @@ -57,7 +59,7 @@ def test_auto_ipo(self): self._user.auto_ipo() -@unittest.skipUnless('ht' in TEST_CLIENTS, 'skip ht test') +@unittest.skipUnless('ht' in TEST_CLIENTS and IS_WIN_PLATFORM, 'skip ht test') class TestHTClientTrader(unittest.TestCase): @classmethod def setUpClass(cls): From b848c7ebf2e0489012e03207f4db19861a666692 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 5 Apr 2018 16:55:11 +0800 Subject: [PATCH 089/276] =?UTF-8?q?Bump=20version:=200.13.5=20=E2=86=92=20?= =?UTF-8?q?0.13.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 3 ++- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1dbea736..9ed701ab 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,6 +1,7 @@ [bumpversion] -current_version = 0.13.5 +current_version = 0.13.6 commit = True files = easytrader/__init__.py setup.py tag = True tag_name = {new_version} + diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 77bf2a70..5695c234 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.5' +__version__ = '0.13.6' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index 12e2ed95..9e6c23cb 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.5', + version='0.13.6', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 330fb3ef295666e74dd061c940b7b261556fa565 Mon Sep 17 00:00:00 2001 From: shidenggui Date: Sat, 14 Apr 2018 21:41:25 +0800 Subject: [PATCH 090/276] Update README.md --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9863727f..d7c5fecc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,16 @@ * 支持命令行调用,方便其他语言适配 * 支持 Python3, Linux / Win, 推荐使用 `Python3` * 有兴趣的可以加群 `556050652` 一起讨论 -* 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) +* 捐助: + +![微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png?imageView2/1/w/300/h/300) ![支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png?imageView2/1/w/300/h/300) + + +## 公众号 + +扫码关注“易量化”的微信公众号,不定时更新一些个人文章及与大家交流 + +![](http://7xqo8v.com1.z0.glb.clouddn.com/easy_quant_qrcode.jpg?imageView2/1/w/300/h/300) **开发环境** : `Ubuntu 16.04` / `Python 3.5` From 85d533c16010753932ed3b8d8d048929d17e7fd1 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 15 Apr 2018 18:29:15 +0800 Subject: [PATCH 091/276] :hammer: remove comment code --- easytrader/ht_clienttrader.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 5adf6fa9..5fc7606e 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -24,7 +24,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): raise ValueError('华泰必须设置通讯密码') try: - self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1) except Exception: self._app = pywinauto.Application().start(exe_path) @@ -32,9 +33,6 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): while True: try: self._app.top_window().Edit1.wait('ready') - #dlg = self._app.window(title='华泰证券网上证券交易分析系统') - #dlg.wrapper_object() - #dlg.button0.click() break except RuntimeError: pass @@ -44,14 +42,13 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().Edit3.type_keys(comm_password) - # dlg = self._app.window(title='用户登陆') - # dlg.wrapper_object() self._app.top_window().button0.click() # detect login is success or not self._app.top_window().wait_not('exists', 10) - self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10) self._close_prompt_windows() self._main = self._app.window(title='网上股票交易系统5.0') @@ -68,6 +65,5 @@ def _get_balance_from_statics(self): self._main.window( control_id=control_id, class_name='Static', - ).window_text() - ) + ).window_text()) return result From f6ccbd4ac259057490cb78d97b8de806051197ad Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 15 Apr 2018 18:36:29 +0800 Subject: [PATCH 092/276] :bug: fix #264 --- easytrader/yh_clienttrader.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 7030f875..dc01dde7 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -25,7 +25,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): :return: """ try: - self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1) except Exception: self._app = pywinauto.Application().start(exe_path) @@ -42,35 +43,31 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): while True: self._app.top_window().Edit3.type_keys( - self._handle_verify_code() - ) + self._handle_verify_code()) self._app.top_window()['登录'].click() # detect login is success or not try: - self._app.top_window().wait_not('exists', 10) + self._app.top_window().wait_not('exists visible', 10) break except: pass - self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10) self._close_prompt_windows() self._main = self._app.window(title='网上股票交易系统5.0') try: self._main.window( - control_id=129, - class_name='SysTreeView32' - ).wait('ready', 2) + control_id=129, class_name='SysTreeView32').wait('ready', 2) except: self._wait(2) self._switch_window_to_normal_mode() def _switch_window_to_normal_mode(self): self._app.top_window().window( - control_id=32812, - class_name='Button' - ).click() + control_id=32812, class_name='Button').click() def _handle_verify_code(self): control = self._app.top_window().window(control_id=22202) From 354813f783c515919c11ea3f782b435444a0aca5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 15 Apr 2018 18:39:23 +0800 Subject: [PATCH 093/276] :hammer: update README.md --- docs/index.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 2dedac9e..c1d169bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,16 @@ * 支持远程操作客户端 * 支持 Python3 , Linux / Win / Mac * 有兴趣的可以加群 `556050652` 一起讨论 -* 捐助: [支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png) [微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png) +* 捐助: + +![微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png?imageView2/1/w/300/h/300) ![支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png?imageView2/1/w/300/h/300) + + +## 公众号 + +扫码关注“易量化”的微信公众号,不定时更新一些个人文章及与大家交流 + +![](http://7xqo8v.com1.z0.glb.clouddn.com/easy_quant_qrcode.jpg?imageView2/1/w/300/h/300) **开发环境** : `OSX 10.12.3` / `Python 3.5` From 838ae966d8efdea6caf534d870b5569044e13ea6 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 15 Apr 2018 18:39:31 +0800 Subject: [PATCH 094/276] =?UTF-8?q?Bump=20version:=200.13.6=20=E2=86=92=20?= =?UTF-8?q?0.13.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9ed701ab..3fe2b7ec 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.6 +current_version = 0.13.7 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 5695c234..38a80f11 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.6' +__version__ = '0.13.7' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index 9e6c23cb..ae8cf8ce 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.6', + version='0.13.7', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 80aa80d5d10ba1ba42125f32acb04ebd01e30bd9 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 18 Apr 2018 22:33:22 +0800 Subject: [PATCH 095/276] :star: update README.md, close #275 --- docs/usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index 13543481..90597460 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -142,6 +142,8 @@ user.buy('162411', price=0.55, amount=100) {'entrust_no': 'xxxxxxxx'} ``` +注: 系统可以配置是否返回成交回报。如果没配的话默认返回 `{"message": "success"}` + #### 卖出: ```python From 09357dfb20d37a68815e299081f614337175f506 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 23 Apr 2018 23:06:46 +0800 Subject: [PATCH 096/276] :fix: alter easytrader yh verify code server address --- easytrader/helpers.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/easytrader/helpers.py b/easytrader/helpers.py index a5f4291c..9b5bbebe 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -18,10 +18,11 @@ class Ssl3HttpAdapter(HTTPAdapter): def init_poolmanager(self, connections, maxsize, block=False): - self.poolmanager = PoolManager(num_pools=connections, - maxsize=maxsize, - block=block, - ssl_version=ssl.PROTOCOL_TLSv1) + self.poolmanager = PoolManager( + num_pools=connections, + maxsize=maxsize, + block=block, + ssl_version=ssl.PROTOCOL_TLSv1) def file2dict(path): @@ -40,9 +41,11 @@ def get_stock_type(stock_code): stock_code = str(stock_code) if stock_code.startswith(('sh', 'sz')): return stock_code[:2] - if stock_code.startswith(('50', '51', '60', '73', '90', '110', '113', '132', '204', '78')): + if stock_code.startswith(('50', '51', '60', '73', '90', '110', '113', + '132', '204', '78')): return 'sh' - if stock_code.startswith(('00', '13', '18', '15', '16', '18', '20', '30', '39', '115', '1318')): + if stock_code.startswith(('00', '13', '18', '15', '16', '18', '20', '30', + '39', '115', '1318')): return 'sz' if stock_code.startswith(('5', '6', '9')): return 'sh' @@ -79,11 +82,9 @@ def recognize_verify_code(image_path, broker='ht'): def detect_yh_client_result(image_path): """封装了tesseract的识别,部署在阿里云上,服务端源码地址为: https://github.com/shidenggui/yh_verify_code_docker""" - api = 'http://123.56.157.162:5000/yh_client' + api = 'http://yh.ez.shidenggui.com:5000/yh_client' with open(image_path, 'rb') as f: - rep = requests.post(api, files={ - 'image': f - }) + rep = requests.post(api, files={'image': f}) if rep.status_code != 201: error = rep.json()['message'] raise Exception('request {} error: {]'.format(api, error)) @@ -94,7 +95,8 @@ def input_verify_code_manual(image_path): from PIL import Image image = Image.open(image_path) image.show() - code = input('image path: {}, input verify code answer:'.format(image_path)) + code = input( + 'image path: {}, input verify code answer:'.format(image_path)) return code @@ -129,15 +131,19 @@ def invoke_tesseract_to_recognize(img): try: res = pytesseract.image_to_string(img) except FileNotFoundError: - raise Exception('tesseract 未安装,请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程') + raise Exception( + 'tesseract 未安装,请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程' + ) valid_chars = re.findall('[0-9a-z]', res, re.IGNORECASE) return ''.join(valid_chars) def get_mac(): # 获取mac地址 link: http://stackoverflow.com/questions/28927958/python-get-mac-address - return ("".join(c + "-" if i % 2 else c for i, c in enumerate(hex( - uuid.getnode())[2:].zfill(12)))[:-1]).upper() + return ("".join( + c + "-" if i % 2 else c + for i, c in enumerate(hex(uuid.getnode())[2:].zfill(12)))[:-1] + ).upper() def grep_comma(num_str): From 71bbec8c6ffc65e026d5a6e04b7795baf098d263 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 23 Apr 2018 23:06:50 +0800 Subject: [PATCH 097/276] =?UTF-8?q?Bump=20version:=200.13.7=20=E2=86=92=20?= =?UTF-8?q?0.13.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3fe2b7ec..fdfdf33c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.7 +current_version = 0.13.8 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 38a80f11..39b4d9ca 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.7' +__version__ = '0.13.8' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index ae8cf8ce..17e25104 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.7', + version='0.13.8', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 37e346d8045d97fb3fa54eca6b9ab59a0b77b41f Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 24 Apr 2018 22:44:33 +0800 Subject: [PATCH 098/276] :bug: fix typo --- easytrader/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easytrader/helpers.py b/easytrader/helpers.py index 9b5bbebe..bf8fdb85 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -11,6 +11,7 @@ from requests.adapters import HTTPAdapter from requests.packages.urllib3.poolmanager import PoolManager from six.moves import input +from . import exceptions if six.PY2: from io import open @@ -87,7 +88,7 @@ def detect_yh_client_result(image_path): rep = requests.post(api, files={'image': f}) if rep.status_code != 201: error = rep.json()['message'] - raise Exception('request {} error: {]'.format(api, error)) + raise exceptions.TradeError('request {} error: {}'.format(api, error)) return rep.json()['result'] From 050647ae0d086af11bb39399a34cc9f408356ed2 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 24 Apr 2018 22:44:46 +0800 Subject: [PATCH 099/276] =?UTF-8?q?Bump=20version:=200.13.8=20=E2=86=92=20?= =?UTF-8?q?0.13.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index fdfdf33c..162b6c72 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.8 +current_version = 0.13.9 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 39b4d9ca..c53edfa5 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.8' +__version__ = '0.13.9' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index 17e25104..a51cd2f0 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.8', + version='0.13.9', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 79d7c56efa801f43ff11bd709264d80ac9951231 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Apr 2018 22:28:27 +0800 Subject: [PATCH 100/276] :star: ISSUE_TEMPLATE add BROKER_TYPE --- .github/ISSUE_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6857d15e..65d340cf 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,6 +3,7 @@ OS: win7/ win10 / mac / linux PYTHON_VERSION: 3.x EASYTRADER_VERSION: 0.xx.xx +BROKER_TYPE: gj / ht / xq / xxx ## problem From 29db34439c6b7ee32ab085708d0473a27f89d003 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Apr 2018 22:31:59 +0800 Subject: [PATCH 101/276] :star: add not support py3 prompt --- easytrader/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/easytrader/api.py b/easytrader/api.py index 1632edea..6e4deffe 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -1,12 +1,17 @@ # coding=utf-8 import logging +import six + from .joinquant_follower import JoinQuantFollower from .log import log from .ricequant_follower import RiceQuantFollower from .xq_follower import XueQiuFollower from .xqtrader import XueQiuTrader +if six.PY2: + raise TypeError('不支持 Python2,请升级 Python3 ') + def use(broker, debug=True, **kwargs): """用于生成特定的券商对象 From 741cfea266006fa5d631fd0e4173d0686d420491 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Apr 2018 22:32:03 +0800 Subject: [PATCH 102/276] =?UTF-8?q?Bump=20version:=200.13.9=20=E2=86=92=20?= =?UTF-8?q?0.13.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 162b6c72..a109fd61 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.9 +current_version = 0.13.10 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index c53edfa5..399774ea 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.9' +__version__ = '0.13.10' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index a51cd2f0..986ae13f 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.9', + version='0.13.10', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 6b00ab57e44a34a94a3b08960869c9aada6a411b Mon Sep 17 00:00:00 2001 From: Ethan Lynn Date: Thu, 26 Apr 2018 12:20:30 +0800 Subject: [PATCH 103/276] update _type_keys (#279) Use set_edit_text in case some field was auto filled, e.g. price. --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index a38e9121..e8725566 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -384,7 +384,7 @@ def _type_keys(self, control_id, text): self._main.window( control_id=control_id, class_name='Edit' - ).type_keys(text) + ).set_edit_text(text) def _get_clipboard_data(self): while True: From d9552bdb34f08edfc7afc84ee430feed7b76435b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 14:52:07 +0800 Subject: [PATCH 104/276] :hammer: format code style --- easytrader/api.py | 7 +- easytrader/clienttrader.py | 115 ++++++-------- easytrader/exceptions.py | 7 + easytrader/joinquant_follower.py | 40 +++-- easytrader/webtrader.py | 41 ++--- easytrader/xq_follower.py | 78 +++++---- easytrader/xqtrader.py | 265 +++++++++++++++++++------------ easytrader/yh_clienttrader.py | 13 +- 8 files changed, 307 insertions(+), 259 deletions(-) diff --git a/easytrader/api.py b/easytrader/api.py index 6e4deffe..667c0bc4 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -1,6 +1,5 @@ # coding=utf-8 import logging - import six from .joinquant_follower import JoinQuantFollower @@ -47,8 +46,10 @@ def use(broker, debug=True, **kwargs): def follower(platform, **kwargs): """用于生成特定的券商对象 :param platform:平台支持 ['jq', 'joinquant', '聚宽’] - :param initial_assets: [雪球参数] 控制雪球初始资金,默认为一万, 总资金由 initial_assets * 组合当前净值 得出 - :param total_assets: [雪球参数] 控制雪球总资金,无默认值, 若设置则覆盖 initial_assets + :param initial_assets: [雪球参数] 控制雪球初始资金,默认为一万, + 总资金由 initial_assets * 组合当前净值 得出 + :param total_assets: [雪球参数] 控制雪球总资金,无默认值, + 若设置则覆盖 initial_assets :return the class of follower Usage:: diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index e8725566..5dc67174 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -1,16 +1,14 @@ # coding:utf-8 - +import easyutils import functools import io import os +import pandas as pd import re import sys import time from abc import abstractmethod -import easyutils -import pandas as pd - from . import exceptions from . import helpers from .config import client @@ -26,8 +24,7 @@ def __init__(self, app): self._app = app def handle(self, title): - if any(s in title for s in - {'提示信息', '委托确认', '网上交易用户协议'}): + if any(s in title for s in {'提示信息', '委托确认', '网上交易用户协议'}): self._submit_by_shortcut() elif '提示' in title: @@ -88,7 +85,12 @@ def __init__(self): self._app = None self._main = None - def prepare(self, config_path=None, user=None, password=None, exe_path=None, comm_password=None, + def prepare(self, + config_path=None, + user=None, + password=None, + exe_path=None, + comm_password=None, **kwargs): """ 登陆客户端 @@ -105,7 +107,8 @@ def prepare(self, config_path=None, user=None, password=None, exe_path=None, com password = account['password'] comm_password = account.get('comm_password') exe_path = account.get('exe_path') - self.login(user, password, exe_path or self._config.DEFAULT_EXE_PATH, comm_password, **kwargs) + self.login(user, password, exe_path or self._config.DEFAULT_EXE_PATH, + comm_password, **kwargs) @abstractmethod def login(self, user, password, exe_path, comm_password=None, **kwargs): @@ -119,10 +122,11 @@ def connect(self, exe_path=None, **kwargs): """ connect_path = exe_path or self._config.DEFAULT_EXE_PATH if connect_path is None: - raise ValueError('参数 exe_path 未设置,请设置客户端对应的 exe 地址,类似 C:\\客户端安装目录\\xiadan.exe') + raise ValueError( + '参数 exe_path 未设置,请设置客户端对应的 exe 地址,类似 C:\\客户端安装目录\\xiadan.exe') - self._app = pywinauto.Application().connect(path=connect_path, - timeout=10) + self._app = pywinauto.Application().connect( + path=connect_path, timeout=10) self._close_prompt_windows() self._main = self._app.top_window() @@ -143,8 +147,7 @@ def _get_balance_from_statics(self): self._main.window( control_id=control_id, class_name='Static', - ).window_text() - ) + ).window_text()) return result @property @@ -175,7 +178,8 @@ def cancel_entrusts(self): def cancel_entrust(self, entrust_no): self._refresh() for i, entrust in enumerate(self.cancel_entrusts): - if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: + if entrust[ + self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: self._cancel_entrust_by_double_click(i) return self._handle_pop_dialogs() else: @@ -243,8 +247,7 @@ def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" selects = self._main( control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, - class_name='ComboBox' - ) + class_name='ComboBox') for i, text in selects.texts(): # skip 0 index, because 0 index is current select index if i == 0: @@ -262,7 +265,9 @@ def auto_ipo(self): if len(stock_list) == 0: return {'message': '今日无新股'} - invalid_list_idx = [i for i, v in enumerate(stock_list) if v['申购数量'] <= 0] + invalid_list_idx = [ + i for i, v in enumerate(stock_list) if v['申购数量'] <= 0 + ] if len(stock_list) == len(invalid_list_idx): return {'message': '没有发现可以申购的新股'} @@ -284,17 +289,15 @@ def _click_grid_by_row(self, row): y = self._config.COMMON_GRID_FIRST_ROW_HEIGHT + self._config.COMMON_GRID_ROW_HEIGHT * row self._app.top_window().window( control_id=self._config.COMMON_GRID_CONTROL_ID, - class_name='CVirtualGridCtrl' - ).click(coords=(x, y)) + class_name='CVirtualGridCtrl').click(coords=(x, y)) def _is_exist_pop_dialog(self): self._wait(0.2) # wait dialog display - return self._main.wrapper_object() != self._app.top_window().wrapper_object() + return self._main.wrapper_object() != self._app.top_window( + ).wrapper_object() def _run_exe_path(self, exe_path): - return os.path.join( - os.path.dirname(exe_path), 'xiadan.exe' - ) + return os.path.join(os.path.dirname(exe_path), 'xiadan.exe') def _wait(self, seconds): time.sleep(seconds) @@ -318,73 +321,49 @@ def trade(self, security, price, amount): def _click(self, control_id): self._app.top_window().window( - control_id=control_id, - class_name='Button' - ).click() + control_id=control_id, class_name='Button').click() def _submit_trade(self): time.sleep(0.05) self._main.window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, - class_name='Button' - ).click() + class_name='Button').click() def _get_pop_dialog_title(self): return self._app.top_window().window( - control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID - ).window_text() + control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID).window_text() def _set_trade_params(self, security, price, amount): code = security[-6:] - self._type_keys( - self._config.TRADE_SECURITY_CONTROL_ID, - code - ) + self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish self._wait(0.1) - self._type_keys( - self._config.TRADE_PRICE_CONTROL_ID, - easyutils.round_price_by_code(price, code) - ) - self._type_keys( - self._config.TRADE_AMOUNT_CONTROL_ID, - str(int(amount)) - ) + self._type_keys(self._config.TRADE_PRICE_CONTROL_ID, + easyutils.round_price_by_code(price, code)) + self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _set_market_trade_params(self, security, amount): code = security[-6:] - self._type_keys( - self._config.TRADE_SECURITY_CONTROL_ID, - code - ) + self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish self._wait(0.1) - self._type_keys( - self._config.TRADE_AMOUNT_CONTROL_ID, - str(int(amount)) - ) + self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _get_grid_data(self, control_id): grid = self._main.window( - control_id=control_id, - class_name='CVirtualGridCtrl' - ) + control_id=control_id, class_name='CVirtualGridCtrl') grid.type_keys('^A^C') - return self._format_grid_data( - self._get_clipboard_data() - ) + return self._format_grid_data(self._get_clipboard_data()) def _type_keys(self, control_id, text): self._main.window( - control_id=control_id, - class_name='Edit' - ).set_edit_text(text) + control_id=control_id, class_name='Edit').set_edit_text(text) def _get_clipboard_data(self): while True: @@ -406,9 +385,7 @@ def _get_left_menus_handle(self): while True: try: handle = self._main.window( - control_id=129, - class_name='SysTreeView32' - ) + control_id=129, class_name='SysTreeView32') # sometime can't find handle ready, must retry handle.wait('ready', 2) return handle @@ -416,11 +393,12 @@ def _get_left_menus_handle(self): pass def _format_grid_data(self, data): - df = pd.read_csv(io.StringIO(data), - delimiter='\t', - dtype=self._config.GRID_DTYPE, - na_filter=False, - ) + df = pd.read_csv( + io.StringIO(data), + delimiter='\t', + dtype=self._config.GRID_DTYPE, + na_filter=False, + ) return df.to_dict('records') def _cancel_entrust_by_double_click(self, row): @@ -428,8 +406,7 @@ def _cancel_entrust_by_double_click(self, row): y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row self._app.top_window().window( control_id=self._config.COMMON_GRID_CONTROL_ID, - class_name='CVirtualGridCtrl' - ).double_click(coords=(x, y)) + class_name='CVirtualGridCtrl').double_click(coords=(x, y)) def _refresh(self): self._switch_left_menus(['买入[F1]'], sleep=0.05) diff --git a/easytrader/exceptions.py b/easytrader/exceptions.py index be29946c..45d67882 100644 --- a/easytrader/exceptions.py +++ b/easytrader/exceptions.py @@ -1,4 +1,11 @@ # coding:utf8 + class TradeError(IOError): pass + + +class NotLoginError(Exception): + def __init__(self, result=None): + super(NotLoginError, self).__init__() + self.result = result diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index 5c98e323..bce3fa9a 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -7,7 +7,7 @@ from .follower import BaseFollower from .log import log -from .webtrader import NotLoginError +from easytrader.exceptions import NotLoginError class JoinQuantFollower(BaseFollower): @@ -29,12 +29,16 @@ def check_login_success(self, rep): set_cookie = rep.headers['set-cookie'] if len(set_cookie) < 100: raise NotLoginError('登录失败,请检查用户名和密码') - self.s.headers.update({ - 'cookie': set_cookie - }) - - def follow(self, users, strategies, track_interval=1, trade_cmd_expire_seconds=120, cmd_cache=True, - entrust_prop='limit', send_interval=0): + self.s.headers.update({'cookie': set_cookie}) + + 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 的模拟交易地址,支持使用 [] 指定多个模拟交易, @@ -51,7 +55,8 @@ def follow(self, users, strategies, track_interval=1, trade_cmd_expire_seconds=1 if cmd_cache: self.load_expired_cmd_cache() - self.start_trader_thread(users, trade_cmd_expire_seconds, entrust_prop, send_interval) + self.start_trader_thread(users, trade_cmd_expire_seconds, entrust_prop, + send_interval) workers = [] for strategy_url in strategies: @@ -61,8 +66,10 @@ def follow(self, users, strategies, track_interval=1, trade_cmd_expire_seconds=1 except: log.error('抽取交易id和策略名失败, 无效的模拟交易url: {}'.format(strategy_url)) raise - strategy_worker = Thread(target=self.track_strategy_worker, args=[strategy_id, strategy_name], - kwargs={'interval': track_interval}) + strategy_worker = Thread( + target=self.track_strategy_worker, + args=[strategy_id, strategy_name], + kwargs={'interval': track_interval}) strategy_worker.start() workers.append(strategy_worker) log.info('开始跟踪策略: {}'.format(strategy_name)) @@ -75,15 +82,12 @@ def extract_strategy_id(strategy_url): def extract_strategy_name(self, strategy_url): rep = self.s.get(strategy_url) - return self.re_find(r'(?<=title="点击修改策略名称"\>).*(?=\).*(?=\ replace_none(entrust['prev_weight']) else u"卖出", - 'report_time': self.__time_strftime(entrust['updated_at']), - 'entrust_status': status, - 'stock_code': entrust['stock_symbol'], - 'stock_name': entrust['stock_name'], - 'business_amount': 100, - 'business_price': price, - 'entrust_amount': 100, - 'entrust_price': price, + 'entrust_no': + entrust['id'], + 'entrust_bs': + u"买入" if entrust['target_weight'] > replace_none( + entrust['prev_weight']) else u"卖出", + 'report_time': + self.__time_strftime(entrust['updated_at']), + 'entrust_status': + status, + 'stock_code': + entrust['stock_symbol'], + 'stock_name': + entrust['stock_name'], + 'business_amount': + 100, + 'business_price': + price, + 'entrust_amount': + 100, + 'entrust_price': + price, }) return entrust_list @@ -279,14 +308,19 @@ def cancel_entrust(self, entrust_no): is_have = True bs = 'buy' if entrust['target_weight'] < entrust['weight'] else 'sell' if entrust['target_weight'] == 0 and entrust['weight'] == 0: - raise TradeError(u"移除的股票操作无法撤销,建议重新买入") + raise exceptions.TradeError(u"移除的股票操作无法撤销,建议重新买入") balance = self.get_balance()[0] - volume = abs(entrust['target_weight'] - entrust['weight']) * balance['asset_balance'] / 100 - r = self.__trade(security=entrust['stock_symbol'], volume=volume, entrust_bs=bs) + volume = abs(entrust['target_weight'] - entrust['weight'] + ) * balance['asset_balance'] / 100 + r = self.__trade( + security=entrust['stock_symbol'], + volume=volume, + entrust_bs=bs) if len(r) > 0 and 'error_info' in r[0]: - raise TradeError(u"撤销失败!%s" % ('error_info' in r[0])) + raise exceptions.TradeError(u"撤销失败!%s" % + ('error_info' in r[0])) if not is_have: - raise TradeError(u"撤销对象已失效") + raise exceptions.TradeError(u"撤销对象已失效") return True def adjust_weight(self, stock_code, weight): @@ -298,9 +332,9 @@ def adjust_weight(self, stock_code, weight): stock = self.__search_stock_info(stock_code) if stock is None: - raise TradeError(u"没有查询要操作的股票信息") + raise exceptions.TradeError(u"没有查询要操作的股票信息") if stock['flag'] != 1: - raise TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") + raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") # 仓位比例向下取两位数 weight = round(weight, 2) @@ -313,7 +347,9 @@ def adjust_weight(self, stock_code, weight): position['proactive'] = True position['weight'] = weight - if weight != 0 and stock['stock_id'] not in [k['stock_id'] for k in position_list]: + 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'], @@ -348,17 +384,19 @@ def adjust_weight(self, stock_code, weight): } try: - rebalance_res = self.session.post(self.config['rebalance_url'], data=data) + resp = self.session.post(self.config['rebalance_url'], data=data) except Exception as e: log.warn('调仓失败: %s ' % e) return else: log.debug('调仓 %s: 持仓比例%d' % (stock['name'], weight)) - rebalance_status = json.loads(rebalance_res.text) - if 'error_description' in rebalance_status.keys() and rebalance_res.status_code != 200: - log.error('调仓错误: %s' % (rebalance_status['error_description'])) - return [{'error_no': rebalance_status['error_code'], - 'error_info': rebalance_status['error_description']}] + resp_json = json.loads(resp.text) + if 'error_description' in resp_json and resp.status_code != 200: + log.error('调仓错误: %s' % (resp_json['error_description'])) + return [{ + 'error_no': resp_json['error_code'], + 'error_info': resp_json['error_description'] + }] else: log.debug('调仓成功 %s: 持仓比例%d' % (stock['name'], weight)) @@ -375,15 +413,15 @@ def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): stock = self.__search_stock_info(security) balance = self.get_balance()[0] if stock is None: - raise TradeError(u"没有查询要操作的股票信息") + raise exceptions.TradeError(u"没有查询要操作的股票信息") if not volume: volume = int(float(price) * amount) # 可能要取整数 if balance['current_balance'] < volume and entrust_bs == 'buy': - raise TradeError(u"没有足够的现金进行操作") + raise exceptions.TradeError(u"没有足够的现金进行操作") if stock['flag'] != 1: - raise TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") + raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") if volume == 0: - raise TradeError(u"操作金额不能为零") + raise exceptions.TradeError(u"操作金额不能为零") # 计算调仓调仓份额 weight = volume / balance['asset_balance'] * 100 @@ -403,7 +441,7 @@ def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): position['weight'] = weight + old_weight else: if weight > old_weight: - raise TradeError(u"操作数量大于实际可卖出数量") + raise exceptions.TradeError(u"操作数量大于实际可卖出数量") else: position['weight'] = old_weight - weight position['weight'] = round(position['weight'], 2) @@ -431,12 +469,14 @@ def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): "price": str(stock['current']) }) else: - raise TradeError(u"没有持有要卖出的股票") + raise exceptions.TradeError(u"没有持有要卖出的股票") if entrust_bs == 'buy': - cash = (balance['current_balance'] - volume) / balance['asset_balance'] * 100 + cash = (balance['current_balance'] - volume + ) / balance['asset_balance'] * 100 else: - cash = (balance['current_balance'] + volume) / balance['asset_balance'] * 100 + cash = (balance['current_balance'] + volume + ) / balance['asset_balance'] * 100 cash = round(cash, 2) log.debug("weight:%f, cash:%f" % (weight, cash)) @@ -449,30 +489,47 @@ def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): } try: - rebalance_res = self.session.post(self.config['rebalance_url'], data=data) + resp = self.session.post(self.config['rebalance_url'], data=data) except Exception as e: log.warn('调仓失败: %s ' % e) return else: - log.debug('调仓 %s%s: %d' % (entrust_bs, stock['name'], rebalance_res.status_code)) - rebalance_status = json.loads(rebalance_res.text) - if 'error_description' in rebalance_status and rebalance_res.status_code != 200: - log.error('调仓错误: %s' % (rebalance_status['error_description'])) - return [{'error_no': rebalance_status['error_code'], - 'error_info': rebalance_status['error_description']}] + log.debug('调仓 %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: + log.error('调仓错误: %s' % (resp_json['error_description'])) + return [{ + 'error_no': resp_json['error_code'], + 'error_info': resp_json['error_description'] + }] else: - return [{'entrust_no': rebalance_status['id'], - 'init_date': self.__time_strftime(rebalance_status['created_at']), - 'batch_no': '委托批号', - 'report_no': '申报号', - 'seat_no': '席位编号', - 'entrust_time': self.__time_strftime(rebalance_status['updated_at']), - 'entrust_price': price, - 'entrust_amount': amount, - 'stock_code': security, - 'entrust_bs': '买入', - 'entrust_type': '雪球虚拟委托', - 'entrust_status': '-'}] + 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 buy(self, security, price=0, amount=0, volume=0, entrust_prop=0): """买入卖出股票 diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index dc01dde7..d1cbff83 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -1,16 +1,14 @@ # coding:utf8 - -import re -import tempfile - import pywinauto import pywinauto.clipboard +import re +import tempfile +from . import clienttrader from . import helpers -from .clienttrader import ClientTrader -class YHClientTrader(ClientTrader): +class YHClientTrader(clienttrader.ClientTrader): @property def broker_type(self): return 'yh' @@ -20,7 +18,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): 登陆客户端 :param user: 账号 :param password: 明文密码 - :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' + :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', + 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' :param comm_password: 通讯密码, 华泰需要,可不设 :return: """ From 4b38bc992ec88c3ff8122eb93d7116e98c65240f Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 15:51:26 +0800 Subject: [PATCH 105/276] =?UTF-8?q?:bug:=20=E9=9B=AA=E7=90=83=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=BD=BF=E7=94=A8=20cookies=20=E7=99=BB=E9=99=86=20fi?= =?UTF-8?q?x=20#269=20fix=20#274=20fix=20#267?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/usage.md | 13 +++++ easytrader/xqtrader.py | 114 ++++++++++++++++++----------------------- xq.json | 4 +- 3 files changed, 63 insertions(+), 68 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 90597460..7aece8e3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -45,6 +45,8 @@ user = easytrader.use('ths') user.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码,其他券商不用') ``` +注: 雪球比较特殊,见下列配置文件格式 + **使用配置文件** ```python @@ -76,6 +78,17 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 ``` +雪球 + +``` +{ + "cookies": "雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/", + "portfolio_code": "组合代码(例:ZH818559)", + "portfolio_market": "交易市场(例:us 或者 cn 或者 hk)" +} + +``` + # 直接连接通用同花顺客户端 需要先手动登陆客户端到交易窗口,然后运用下面的代码连接交易窗口 diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index a74b4cbf..599cc86d 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -16,8 +16,9 @@ class XueQiuTrader(webtrader.WebTrader): _HEADERS = { 'User-Agent': - 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:32.0) ' - 'Gecko/20100101 Firefox/32.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/64.0.3282.167 Safari/537.36', 'Host': 'xueqiu.com', 'Pragma': @@ -27,15 +28,15 @@ class XueQiuTrader(webtrader.WebTrader): 'Accept': '*/*', 'Accept-Encoding': - 'gzip,deflate,sdch', + 'gzip, deflate, br', + 'Accept-Language': + 'zh-CN,zh;q=0.9,en;q=0.8', 'Cache-Control': 'no-cache', 'Referer': - 'http://xueqiu.com/P/ZH003694', + 'https://xueqiu.com/P/ZH004612', 'X-Requested-With': 'XMLHttpRequest', - 'Accept-Language': - 'zh-CN,zh;q=0.8' } def __init__(self, **kwargs): @@ -55,30 +56,28 @@ def __init__(self, **kwargs): def autologin(self, **kwargs): """ - 重写自动登录方法 - 避免重试导致的帐号封停 + 使用 cookies 之后不需要自动登陆 :return: """ - self.login() + self._set_cookies(self.account_config['cookies']) - def login(self, throw=False): + def _set_cookies(self, cookies): + """设置雪球 cookies,代码来自于 + https://github.com/shidenggui/easytrader/issues/269 + :param cookies: 雪球 cookies + :type cookies: str """ - 登录 - :param throw: - :return: - """ - login_status, result = self.post_login_data() - if login_status is False and throw: - raise exceptions.NotLoginError(result) - log.debug('login status: %s' % result) - return login_status + cookie_dict = {} + for record in cookies.split(";"): + key, value = record.strip().split("=", 1) + cookie_dict[key] = value + self.session.cookies[key] = value def _prepare_account(self, user='', password='', **kwargs): """ 转换参数到登录所需的字典格式 - :param user: 雪球邮箱(邮箱手机二选一) - :param password: 雪球密码 - :param account: 雪球手机号(邮箱手机二选一) + :param cookies: 雪球登陆需要设置 cookies, 具体见 + https://smalltool.github.io/2016/08/02/cookie/ :param portfolio_code: 组合代码 :param portfolio_market: 交易市场, 可选['cn', 'us', 'hk'] 默认 'cn' :return: @@ -87,30 +86,15 @@ def _prepare_account(self, user='', password='', **kwargs): raise TypeError('雪球登录需要设置 portfolio_code(组合代码) 参数') if 'portfolio_market' not in kwargs: kwargs['portfolio_market'] = 'cn' - if 'account' not in kwargs: - kwargs['account'] = '' + if 'cookies' not in kwargs: + raise TypeError('雪球登陆需要设置 cookies, 具体见' + 'https://smalltool.github.io/2016/08/02/cookie/') self.account_config = { - 'username': user, - 'account': kwargs['account'], - 'password': password, 'portfolio_code': kwargs['portfolio_code'], 'portfolio_market': kwargs['portfolio_market'] } - def post_login_data(self): - login_post_data = { - 'username': self.account_config.get('username', ''), - 'areacode': '86', - 'password': self.account_config['password'] - } - login_response = self.session.post( - self.config['login_api'], data=login_post_data) - login_status = login_response.json() - if 'error_description' in login_status: - return False, login_status['error_description'] - return True, "SUCCESS" - - def __virtual_to_balance(self, virtual): + def _virtual_to_balance(self, virtual): """ 虚拟净值转化为资金 :param virtual: 雪球组合净值 @@ -118,10 +102,10 @@ def __virtual_to_balance(self, virtual): """ return virtual * self.multiple - def __get_html(self, url): + def _get_html(self, url): return self.session.get(url).text - def __search_stock_info(self, code): + def _search_stock_info(self, code): """ 通过雪球的接口获取股票详细信息 :param code: 股票代码 000001 @@ -146,13 +130,13 @@ def __search_stock_info(self, code): stock = stocks[0] return stock - def __get_portfolio_info(self, portfolio_code): + def _get_portfolio_info(self, portfolio_code): """ 获取组合信息 :return: 字典 """ url = self.config['portfolio_url'] + portfolio_code - html = self.__get_html(url) + html = self._get_html(url) match_info = re.search(r'(?<=SNB.cubeInfo = ).*(?=;\n)', html) if match_info is None: raise Exception( @@ -169,8 +153,8 @@ def get_balance(self): :return: """ portfolio_code = self.account_config.get('portfolio_code', 'ch') - portfolio_info = self.__get_portfolio_info(portfolio_code) - asset_balance = self.__virtual_to_balance( + portfolio_info = self._get_portfolio_info(portfolio_code) + asset_balance = self._virtual_to_balance( float(portfolio_info['net_value'])) # 总资产 position = portfolio_info['view_rebalancing'] # 仓位结构 cash = asset_balance * float(position['cash']) / 100 @@ -184,13 +168,13 @@ def get_balance(self): 'pre_interest': 0.25 }] - def __get_position(self): + def _get_position(self): """ 获取雪球持仓 :return: """ portfolio_code = self.account_config['portfolio_code'] - portfolio_info = self.__get_portfolio_info(portfolio_code) + portfolio_info = self._get_portfolio_info(portfolio_code) position = portfolio_info['view_rebalancing'] # 仓位结构 stocks = position['holdings'] # 持仓股票 return stocks @@ -208,7 +192,7 @@ def get_position(self): 获取持仓 :return: """ - xq_positions = self.__get_position() + xq_positions = self._get_position() balance = self.get_balance()[0] position_list = [] for pos in xq_positions: @@ -227,7 +211,7 @@ def get_position(self): }) return position_list - def __get_xq_history(self): + def _get_xq_history(self): """ 获取雪球调仓历史 :param instance: @@ -239,13 +223,13 @@ def __get_xq_history(self): 'count': 20, 'page': 1 } - r = self.session.get(self.config['history_url'], params=data) - r = json.loads(r.text) - return r['list'] + resp = self.session.get(self.config['history_url'], params=data) + res = json.loads(resp.text) + return res['list'] @property def history(self): - return self.__get_xq_history() + return self._get_xq_history() def get_entrust(self): """ @@ -253,7 +237,7 @@ def get_entrust(self): 操作数量都按1手模拟换算的 :return: """ - xq_entrust_list = self.__get_xq_history() + xq_entrust_list = self._get_xq_history() entrust_list = [] replace_none = lambda s: s or 0 for xq_entrusts in xq_entrust_list: @@ -299,7 +283,7 @@ def cancel_entrust(self, entrust_no): :param entrust_no: :return: """ - xq_entrust_list = self.__get_xq_history() + xq_entrust_list = self._get_xq_history() is_have = False for xq_entrusts in xq_entrust_list: status = xq_entrusts['status'] # 调仓状态 @@ -312,7 +296,7 @@ def cancel_entrust(self, entrust_no): balance = self.get_balance()[0] volume = abs(entrust['target_weight'] - entrust['weight'] ) * balance['asset_balance'] / 100 - r = self.__trade( + r = self._trade( security=entrust['stock_symbol'], volume=volume, entrust_bs=bs) @@ -330,7 +314,7 @@ def adjust_weight(self, stock_code, weight): :param weight: float 调整之后的持仓百分比, 0 - 100 之间的浮点数 """ - stock = self.__search_stock_info(stock_code) + stock = self._search_stock_info(stock_code) if stock is None: raise exceptions.TradeError(u"没有查询要操作的股票信息") if stock['flag'] != 1: @@ -339,7 +323,7 @@ def adjust_weight(self, stock_code, weight): # 仓位比例向下取两位数 weight = round(weight, 2) # 获取原有仓位信息 - position_list = self.__get_position() + position_list = self._get_position() # 调整后的持仓 for position in position_list: @@ -400,7 +384,7 @@ def adjust_weight(self, stock_code, weight): else: log.debug('调仓成功 %s: 持仓比例%d' % (stock['name'], weight)) - def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): + def _trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): """ 调仓 :param security: @@ -410,7 +394,7 @@ def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): :param entrust_bs: :return: """ - stock = self.__search_stock_info(security) + stock = self._search_stock_info(security) balance = self.get_balance()[0] if stock is None: raise exceptions.TradeError(u"没有查询要操作的股票信息") @@ -428,7 +412,7 @@ def __trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): weight = round(weight, 2) # 获取原有仓位信息 - position_list = self.__get_position() + position_list = self._get_position() # 调整后的持仓 is_have = False @@ -539,7 +523,7 @@ def buy(self, security, price=0, amount=0, volume=0, entrust_prop=0): :param volume: 买入总金额 由 volume / price 取整, 若指定 price 则此参数无效 :param entrust_prop: """ - return self.__trade(security, price, amount, volume, 'buy') + return self._trade(security, price, amount, volume, 'buy') def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0): """卖出股票 @@ -549,4 +533,4 @@ def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0): :param volume: 卖出总金额 由 volume / price 取整, 若指定 price 则此参数无效 :param entrust_prop: """ - return self.__trade(security, price, amount, volume, 'sell') + return self._trade(security, price, amount, volume, 'sell') diff --git a/xq.json b/xq.json index a40f936e..9690b683 100644 --- a/xq.json +++ b/xq.json @@ -1,7 +1,5 @@ { - "username": "邮箱", - "account": "手机号", - "password": "密码", + "cookies": "雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/", "portfolio_code": "组合代码(例:ZH818559)", "portfolio_market": "交易市场(例:us 或者 cn 或者 hk)" } From d0a4230d2ace7b400c577777ea957a25bfeee0f4 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 15:51:33 +0800 Subject: [PATCH 106/276] =?UTF-8?q?Bump=20version:=200.13.10=20=E2=86=92?= =?UTF-8?q?=200.13.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a109fd61..d4c80d69 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.10 +current_version = 0.13.11 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 399774ea..d6f95f08 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.10' +__version__ = '0.13.11' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index 986ae13f..b897ba42 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.10', + version='0.13.11', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From fb8004c07e0c876e8e4d11114a17935e08b0e518 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 16:31:35 +0800 Subject: [PATCH 107/276] :bug: fix forget set cookies in account_config --- easytrader/xqtrader.py | 1 + tests/__init__.py | 1 + tests/test_easytrader.py | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index 599cc86d..83a99e54 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -90,6 +90,7 @@ def _prepare_account(self, user='', password='', **kwargs): raise TypeError('雪球登陆需要设置 cookies, 具体见' 'https://smalltool.github.io/2016/08/02/cookie/') self.account_config = { + 'cookies': kwargs['cookies'], 'portfolio_code': kwargs['portfolio_code'], 'portfolio_market': kwargs['portfolio_market'] } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..8288f42b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# coding:utf8 diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index 22fe2357..252b1798 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -1,5 +1,4 @@ # coding: utf-8 - import os import sys import time From 834c4f719085f83213525ec6bfa4e147128d911c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 16:31:45 +0800 Subject: [PATCH 108/276] =?UTF-8?q?Bump=20version:=200.13.11=20=E2=86=92?= =?UTF-8?q?=200.13.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d4c80d69..2457aaf1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.11 +current_version = 0.13.12 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index d6f95f08..78d65914 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.11' +__version__ = '0.13.12' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index b897ba42..b4ed8b50 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.11', + version='0.13.12', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 1d8f1e90b839f14d94fbe84e5a78b2dfe95a3bd7 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 21:46:53 +0800 Subject: [PATCH 109/276] =?UTF-8?q?:bug:=20xq=5Ffollower=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20cookies=20=E7=99=BB=E9=99=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/usage.md | 4 +- easytrader/follower.py | 134 +++++++++++++++++++++++++------------- easytrader/helpers.py | 24 ++++++- easytrader/xq_follower.py | 35 ++++++---- easytrader/xqtrader.py | 19 +++--- 5 files changed, 142 insertions(+), 74 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 7aece8e3..1ee3eeb3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -94,7 +94,7 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 需要先手动登陆客户端到交易窗口,然后运用下面的代码连接交易窗口 ```python -user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' +user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' ``` @@ -351,7 +351,7 @@ enjoy it ``` xq_follower = easytrader.follower('xq') -xq_follower.login(user='xq用户名', password='xq密码') +xq_follower.login(cookies='雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/') ``` #### 连接 follower 和 trader diff --git a/easytrader/follower.py b/easytrader/follower.py index fdb74097..f2eaa8ec 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -29,18 +29,15 @@ def __init__(self): self.s = requests.Session() - def login(self, user, password, **kwargs): - # mock headers - headers = { - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'en-US,en;q=0.8', - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.100 Safari/537.36', - 'Referer': self.WEB_REFERER, - 'X-Requested-With': 'XMLHttpRequest', - 'Origin': self.WEB_ORIGIN, - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - } + 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 @@ -53,6 +50,27 @@ def login(self, user, password, **kwargs): self.check_login_success(rep) log.info('登录成功') + def _generate_headers(self): + headers = { + 'Accept': + 'application/json, text/javascript, */*; q=0.01', + 'Accept-Encoding': + 'gzip, deflate, br', + 'Accept-Language': + 'en-US,en;q=0.8', + 'User-Agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.100 Safari/537.36', + 'Referer': + self.WEB_REFERER, + 'X-Requested-With': + 'XMLHttpRequest', + 'Origin': + self.WEB_ORIGIN, + 'Content-Type': + 'application/x-www-form-urlencoded; charset=UTF-8', + } + return headers + def check_login_success(self, rep): """检查登录状态是否成功 :param rep: post login 接口返回的 response 对象 @@ -67,8 +85,13 @@ def create_login_params(self, user, password, **kwargs): """ pass - def follow(self, users, strategies, track_interval=1, - trade_cmd_expire_seconds=120, cmd_cache=True, **kwargs): + def follow(self, + users, + strategies, + track_interval=1, + trade_cmd_expire_seconds=120, + cmd_cache=True, + **kwargs): """跟踪平台对应的模拟交易,支持多用户多策略 :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户 :param strategies: 雪球组合名, 类似 ZH123450 @@ -88,10 +111,19 @@ def load_expired_cmd_cache(self): with open(self.CMD_CACHE_FILE, 'rb') as f: self.expired_cmds = pickle.load(f) - def start_trader_thread(self, users, trade_cmd_expire_seconds, entrust_prop='limit', send_interval=0): - trader = Thread(target=self.trade_worker, args=[users], kwargs={'expire_seconds': trade_cmd_expire_seconds, - 'entrust_prop': entrust_prop, - 'send_interval': send_interval}) + def start_trader_thread(self, + users, + trade_cmd_expire_seconds, + entrust_prop='limit', + send_interval=0): + trader = Thread( + target=self.trade_worker, + args=[users], + kwargs={ + 'expire_seconds': trade_cmd_expire_seconds, + 'entrust_prop': entrust_prop, + 'send_interval': send_interval + }) trader.setDaemon(True) trader.start() @@ -125,7 +157,8 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): :param interval: 轮询策略的时间间隔,单位为秒""" while True: try: - transactions = self.query_strategy_transaction(strategy, **kwargs) + transactions = self.query_strategy_transaction( + strategy, **kwargs) except Exception as e: log.warning('无法获取策略 {} 调仓信息, 错误: {}, 跳过此次调仓查询'.format(name, e)) continue @@ -141,10 +174,11 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): } if self.is_cmd_expired(trade_cmd): continue - log.info('策略 [{}] 发送指令到交易队列, 股票: {} 动作: {} 数量: {} 价格: {} 信号产生时间: {}'.format( - name, trade_cmd['stock_code'], trade_cmd['action'], trade_cmd['amount'], trade_cmd['price'], - trade_cmd['datetime'] - )) + log.info( + '策略 [{}] 发送指令到交易队列, 股票: {} 动作: {} 数量: {} 价格: {} 信号产生时间: {}'. + format(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: @@ -157,7 +191,8 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): @staticmethod def generate_expired_cmd_key(cmd): return '{}_{}_{}_{}_{}_{}'.format( - cmd['strategy_name'], cmd['stock_code'], cmd['action'], cmd['amount'], cmd['price'], cmd['datetime']) + cmd['strategy_name'], cmd['stock_code'], cmd['action'], + cmd['amount'], cmd['price'], cmd['datetime']) def is_cmd_expired(self, cmd): key = self.generate_expired_cmd_key(cmd) @@ -178,7 +213,11 @@ def _is_number(s): except ValueError: return False - def trade_worker(self, users, expire_seconds=120, entrust_prop='limit', send_interval=0): + def trade_worker(self, + users, + expire_seconds=120, + entrust_prop='limit', + send_interval=0): """ :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时买出单没有及时成交导致的买入金额不足 """ @@ -190,29 +229,32 @@ def trade_worker(self, users, expire_seconds=120, entrust_prop='limit', send_int expire = (now - trade_cmd['datetime']).total_seconds() if expire > expire_seconds: log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃'.format( - trade_cmd['strategy_name'], trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], now, expire_seconds)) + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃'. + format(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: log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃'.format( - trade_cmd['strategy_name'], trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], now)) + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃'. + format(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: log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃'.format( - trade_cmd['strategy_name'], trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], now)) + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃'. + format(trade_cmd['strategy_name'], + trade_cmd['stock_code'], trade_cmd['action'], + trade_cmd['amount'], trade_cmd['price'], + trade_cmd['datetime'], now)) break args = { @@ -227,16 +269,18 @@ def trade_worker(self, users, expire_seconds=120, entrust_prop='limit', send_int trader_name = type(user).__name__ err_msg = '{}: {}'.format(type(e).__name__, e.message) log.error( - '{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}'.format( - trader_name, trade_cmd['strategy_name'], trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], err_msg)) + '{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}'. + format(trader_name, trade_cmd['strategy_name'], + trade_cmd['stock_code'], trade_cmd['action'], + trade_cmd['amount'], trade_cmd['price'], + trade_cmd['datetime'], err_msg)) continue log.info( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}'.format( - trade_cmd['strategy_name'], trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], response)) + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}'. + format(trade_cmd['strategy_name'], trade_cmd['stock_code'], + trade_cmd['action'], trade_cmd['amount'], + trade_cmd['price'], trade_cmd['datetime'], + response)) time.sleep(send_interval) def query_strategy_transaction(self, strategy, **kwargs): diff --git a/easytrader/helpers.py b/easytrader/helpers.py index bf8fdb85..9e9a17cd 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -4,13 +4,15 @@ import datetime import json import re -import requests -import six import ssl import uuid + +import requests +import six from requests.adapters import HTTPAdapter from requests.packages.urllib3.poolmanager import PoolManager from six.moves import input + from . import exceptions if six.PY2: @@ -26,6 +28,21 @@ def init_poolmanager(self, connections, maxsize, block=False): ssl_version=ssl.PROTOCOL_TLSv1) +def parse_cookies_str(cookies): + """ + parse cookies str to dict + :param cookies: cookies str + :type cookies: str + :return: cookie dict + :rtype: dict + """ + cookie_dict = {} + for record in cookies.split(";"): + key, value = record.strip().split("=", 1) + cookie_dict[key] = value + return cookie_dict + + def file2dict(path): with open(path, encoding='utf-8') as f: return json.load(f) @@ -196,7 +213,8 @@ def get_today_ipo_data(): home_page_url = 'https://xueqiu.com' ipo_data_url = "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl" \ "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_" \ - "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" % (str(sj)) + "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" % ( + str(sj)) session = requests.session() session.get(home_page_url, headers=send_headers) # 产生cookies diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index c29ed8b2..4d1bfefa 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -7,7 +7,7 @@ from numbers import Number from threading import Thread -from easytrader import exceptions +from . import helpers from .follower import BaseFollower from .log import log @@ -22,19 +22,26 @@ class XueQiuFollower(BaseFollower): def __init__(self): super(XueQiuFollower, self).__init__() - def check_login_success(self, login_status): - if 'error_description' in login_status: - raise exceptions.NotLoginError(login_status['error_description']) - - def create_login_params(self, user, password, **kwargs): - params = { - 'username': user, - 'areacode': '86', - 'telephone': kwargs.get('account', ''), - 'remember_me': '0', - 'password': password - } - return params + 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) + + cookie_dict = helpers.parse_cookies_str(cookies) + self.s.cookies.update(cookie_dict) + + log.info('登录成功') def follow(self, users, diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index 83a99e54..7dd5a32d 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -3,10 +3,12 @@ import numbers import os import re -import requests import time +import requests + from . import exceptions +from . import helpers from . import webtrader from .log import log @@ -67,11 +69,8 @@ def _set_cookies(self, cookies): :param cookies: 雪球 cookies :type cookies: str """ - cookie_dict = {} - for record in cookies.split(";"): - key, value = record.strip().split("=", 1) - cookie_dict[key] = value - self.session.cookies[key] = value + cookie_dict = helpers.parse_cookies_str(cookies) + self.session.cookies.update(cookie_dict) def _prepare_account(self, user='', password='', **kwargs): """ @@ -181,7 +180,7 @@ def _get_position(self): return stocks @staticmethod - def __time_strftime(time_stamp): + def _time_strftime(time_stamp): try: local_time = time.localtime(time_stamp / 1000) return time.strftime("%Y-%m-%d %H:%M:%S", local_time) @@ -260,7 +259,7 @@ def get_entrust(self): u"买入" if entrust['target_weight'] > replace_none( entrust['prev_weight']) else u"卖出", 'report_time': - self.__time_strftime(entrust['updated_at']), + self._time_strftime(entrust['updated_at']), 'entrust_status': status, 'stock_code': @@ -493,7 +492,7 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): 'entrust_no': resp_json['id'], 'init_date': - self.__time_strftime(resp_json['created_at']), + self._time_strftime(resp_json['created_at']), 'batch_no': '委托批号', 'report_no': @@ -501,7 +500,7 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): 'seat_no': '席位编号', 'entrust_time': - self.__time_strftime(resp_json['updated_at']), + self._time_strftime(resp_json['updated_at']), 'entrust_price': price, 'entrust_amount': From 46c8e55627a65d35edfec52087cd3e49256a94ee Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 21:48:16 +0800 Subject: [PATCH 110/276] :hammer: refactor code --- easytrader/webtrader.py | 4 ++-- easytrader/xqtrader.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index 79a2d7b2..f9d4a0b3 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -2,11 +2,11 @@ import logging import os import re -import requests -import six import time from threading import Thread +import requests + from . import exceptions from . import helpers from .log import log diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index 7dd5a32d..bca1ea73 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -52,8 +52,8 @@ def __init__(self, **kwargs): if self.multiple < 1e3: raise ValueError('雪球初始资产不能小于1000元,当前预设值 {}'.format(self.multiple)) - self.session = requests.Session() - self.session.headers.update(self._HEADERS) + self.s = requests.Session() + self.s.headers.update(self._HEADERS) self.account_config = None def autologin(self, **kwargs): @@ -70,7 +70,7 @@ def _set_cookies(self, cookies): :type cookies: str """ cookie_dict = helpers.parse_cookies_str(cookies) - self.session.cookies.update(cookie_dict) + self.s.cookies.update(cookie_dict) def _prepare_account(self, user='', password='', **kwargs): """ @@ -103,7 +103,7 @@ def _virtual_to_balance(self, virtual): return virtual * self.multiple def _get_html(self, url): - return self.session.get(url).text + return self.s.get(url).text def _search_stock_info(self, code): """ @@ -122,7 +122,7 @@ def _search_stock_info(self, code): 'key': '47bce5c74f', 'market': self.account_config['portfolio_market'], } - r = self.session.get(self.config['search_stock_url'], params=data) + r = self.s.get(self.config['search_stock_url'], params=data) stocks = json.loads(r.text) stocks = stocks['stocks'] stock = None @@ -223,7 +223,7 @@ def _get_xq_history(self): 'count': 20, 'page': 1 } - resp = self.session.get(self.config['history_url'], params=data) + resp = self.s.get(self.config['history_url'], params=data) res = json.loads(resp.text) return res['list'] @@ -368,7 +368,7 @@ def adjust_weight(self, stock_code, weight): } try: - resp = self.session.post(self.config['rebalance_url'], data=data) + resp = self.s.post(self.config['rebalance_url'], data=data) except Exception as e: log.warn('调仓失败: %s ' % e) return @@ -473,7 +473,7 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): } try: - resp = self.session.post(self.config['rebalance_url'], data=data) + resp = self.s.post(self.config['rebalance_url'], data=data) except Exception as e: log.warn('调仓失败: %s ' % e) return From 47a241f9119c18787ee78046406a93883a6903bf Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 29 Apr 2018 21:48:55 +0800 Subject: [PATCH 111/276] =?UTF-8?q?Bump=20version:=200.13.12=20=E2=86=92?= =?UTF-8?q?=200.13.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2457aaf1..dc4e9605 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.12 +current_version = 0.13.13 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 78d65914..45cfa80f 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.12' +__version__ = '0.13.13' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index b4ed8b50..0945ed8c 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.12', + version='0.13.13', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 8a98544a906ff344d90b3b4e390ab2334a974edb Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 5 May 2018 21:34:57 +0800 Subject: [PATCH 112/276] =?UTF-8?q?:star:=20xq=5Ffollower=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20adjust=5Fsell=20=E5=8F=82=E6=95=B0=E4=BB=A5?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E6=A0=B9=E6=8D=AE=E7=99=BE=E5=88=86=E6=AF=94?= =?UTF-8?q?=E8=B0=83=E4=BB=93=E6=97=B6=E5=8D=96=E5=87=BA=E8=82=A1=E4=BB=BD?= =?UTF-8?q?=E5=81=8F=E5=B7=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/usage.md | 2 ++ easytrader/xq_follower.py | 47 ++++++++++++++++++++++++++++++++++- tests/test_xq_follower.py | 52 +++++++++++++++++++++++++++++++++++++++ tests/test_xqtrader.py | 17 +++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 tests/test_xq_follower.py create mode 100644 tests/test_xqtrader.py diff --git a/docs/usage.md b/docs/usage.md index 1ee3eeb3..0d878126 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -366,6 +366,8 @@ xq_follower.follow(xq_user, 'xq组合ID,类似ZH123456', total_assets=100000) * 这里可以设置 total_assets, 为当前组合的净值对应的总资金额度, 具体可以参考参数说明 * 或者设置 initial_assets, 这时候总资金额度为 initial_assets * 组合净值 +* 雪球额外支持 adjust_sell 参数,决定是否根据用户的实际持仓数调整卖出股票数量,解决雪球根据百分比调仓时计算出的股数有偏差的问题。当卖出股票数大于实际持仓数时,调整为实际持仓数。目前仅在银河客户端测试通过。 当 users 为多个时,根据第一个 user 的持仓数决定 + #### 多用户跟踪多策略 diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index 4d1bfefa..2df017cd 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -48,6 +48,7 @@ def follow(self, strategies, total_assets=10000, initial_assets=None, + adjust_sell=False, track_interval=10, trade_cmd_expire_seconds=120, cmd_cache=True): @@ -59,6 +60,10 @@ def follow(self, 设置 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 会覆盖此参数 @@ -66,7 +71,11 @@ def follow(self, :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒 :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令 """ + self._adjust_sell = adjust_sell + users = self.warp_list(users) + self._users = users + strategies = self.warp_list(strategies) total_assets = self.warp_list(total_assets) initial_assets = self.warp_list(initial_assets) @@ -148,7 +157,6 @@ def project_transactions(self, transactions, assets): t['prev_weight']) initial_amount = abs(weight_diff) / 100 * assets / t['price'] - t['amount'] = int(round(initial_amount, -2)) t['datetime'] = datetime.fromtimestamp(t['created_at'] // 1000) @@ -156,6 +164,43 @@ def project_transactions(self, transactions, assets): t['action'] = 'buy' if weight_diff > 0 else 'sell' + t['amount'] = int(round(initial_amount, -2)) + if self._adjust_sell: + t['amount'] = self._adjust_sell_amount(t['stock_code'], + t['amount']) + + 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 + """ + user = self._users[0] + position = user.position + try: + stock = next(s for s in position if s['证券代码'] == stock_code) + except StopIteration: + log.info('根据持仓调整 {} 卖出额,发现未持有股票 {}, 不做任何调整'.format( + stock_code, stock_code)) + return amount + + available_amount = stock['可用余额'] + if available_amount >= amount: + return amount + + adjust_amount = available_amount // 100 * 100 + log.info('股票 {} 实际可用余额 {}, 指令卖出股数为 {}, 调整为 {}'.format( + stock_code, available_amount, amount, adjust_amount)) + return adjust_amount + def _get_portfolio_info(self, portfolio_code): """ 获取组合信息 diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py new file mode 100644 index 00000000..d78fe086 --- /dev/null +++ b/tests/test_xq_follower.py @@ -0,0 +1,52 @@ +# coding:utf-8 +import unittest +from unittest import mock + +from easytrader import XueQiuFollower + + +class TestXueQiuTrader(unittest.TestCase): + def test_adjust_sell_amount_without_enable(self): + follower = XueQiuFollower() + + mock_user = mock.MagicMock() + follower._users = [mock_user] + + follower._adjust_sell = False + amount = follower._adjust_sell_amount('169101', 1000) + self.assertEqual(amount, amount) + + def test_adjust_sell_amount(self): + follower = XueQiuFollower() + + mock_user = mock.MagicMock() + follower._users = [mock_user] + mock_user.position = TEST_POSITION + + follower._adjust_sell = True + test_cases = [ + ('169101', 600, 600), + ('169101', 700, 600), + ('000000', 100, 100), + ] + for stock_code, sell_amount, excepted_amount in test_cases: + amount = follower._adjust_sell_amount(stock_code, sell_amount) + self.assertEqual(amount, excepted_amount) + + +TEST_POSITION = [{ + 'Unnamed: 14': '', + '买入冻结': 0, + '交易市场': '深A', + '卖出冻结': 0, + '参考市价': 1.464, + '参考市值': 919.39, + '参考成本价': 1.534, + '参考盈亏': -43.77, + '可用余额': 628, + '当前持仓': 628, + '盈亏比例(%)': -4.544, + '股东代码': '0000000000', + '股份余额': 628, + '证券代码': '169101' +}] diff --git a/tests/test_xqtrader.py b/tests/test_xqtrader.py new file mode 100644 index 00000000..d5632f8c --- /dev/null +++ b/tests/test_xqtrader.py @@ -0,0 +1,17 @@ +# coding: utf-8 +import unittest + +from easytrader import XueQiuTrader + + +class TestXueQiuTrader(unittest.TestCase): + def test_prepare_account(self): + user = XueQiuTrader() + params_without_cookies = dict( + portfolio_code='ZH123456', portfolio_market='cn') + with self.assertRaises(TypeError): + user._prepare_account(**params_without_cookies) + + params_without_cookies.update(cookies='123') + user._prepare_account(**params_without_cookies) + self.assertEqual(params_without_cookies, user.account_config) From f3a6d5af6c6f43a0ebf31594b9de9c01eabf3a76 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 5 May 2018 21:35:43 +0800 Subject: [PATCH 113/276] =?UTF-8?q?Bump=20version:=200.13.13=20=E2=86=92?= =?UTF-8?q?=200.14.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index dc4e9605..4dc88e92 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.13 +current_version = 0.14.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 45cfa80f..4faf1beb 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.13.13' +__version__ = '0.14.0' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index 0945ed8c..0f53d840 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.13.13', + version='0.14.0', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 6ade291d142ab083f43482f57cf28a0abb2d4bdb Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 6 May 2018 22:03:00 +0800 Subject: [PATCH 114/276] :star: add .travis.yml --- .travis.yml | 15 +++++++++++++++ test-requirements.txt | 5 +++++ 2 files changed, 20 insertions(+) create mode 100644 .travis.yml create mode 100644 test-requirements.txt diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..04787538 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: python + +python: + - "3.4" + - "3.5" + - "3.6" + +env: + - PROJ=${PWD##*/} + +install: + - pip install -r test-requirements.txt + +script: + - pytest --cov=$PROJ -v tests diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..edfca411 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,5 @@ +-r requirements.txt + +pytest +pytest-cov + From a430968c3486e6c1299e36c5e8f6334f2c5187fe Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 6 May 2018 22:03:57 +0800 Subject: [PATCH 115/276] :hammer: reformat code --- easytrader/clienttrader.py | 5 +++-- easytrader/gj_clienttrader.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 5dc67174..f776c06b 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -1,14 +1,15 @@ # coding:utf-8 -import easyutils import functools import io import os -import pandas as pd import re import sys import time from abc import abstractmethod +import easyutils +import pandas as pd + from . import exceptions from . import helpers from .config import client diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index 0567e63e..04760d28 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -1,13 +1,13 @@ # coding:utf8 from __future__ import division -# import pandas as pd -import pywinauto -import pywinauto.clipboard import re import tempfile import time +import pywinauto +import pywinauto.clipboard + from . import helpers from .clienttrader import ClientTrader @@ -27,7 +27,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): :return: """ try: - self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=1) + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1) except Exception: self._app = pywinauto.Application().start(exe_path) @@ -45,9 +46,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): while True: try: code = self._handle_verify_code() - edit3.type_keys( - code - ) + edit3.type_keys(code) time.sleep(1) self._app.top_window()['确定(Y)'].click() # detect login is success or not @@ -60,7 +59,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): except Exception as e: pass - self._app = pywinauto.Application().connect(path=self._run_exe_path(exe_path), timeout=10) + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10) self._main = self._app.window(title='网上股票交易系统5.0') def _handle_verify_code(self): From 2fb027ec442d6ff6459c1271c429708da778f750 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 6 May 2018 22:05:51 +0800 Subject: [PATCH 116/276] :hammer: update README.md, add icons --- LICENSE | 21 +++++++++++++++++++++ README.md | 4 ++++ 2 files changed, 25 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..6d2dafda --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 shidenggui + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d7c5fecc..aa148361 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # easytrader +[![Package](https://img.shields.io/pypi/v/easytrader.svg)](https://pypi.python.org/pypi/easytrader) +[![Travis](https://img.shields.io/travis/shidenggui/easytrader.svg)](https://travis-ci.org/shidenggui/easytrader) +[![License](https://img.shields.io/github/license/shidenggui/easytrader.svg)](https://github.com/shidenggui/easytrader/blob/master/LICENSE) + * 进行自动的程序化股票交易 * 支持跟踪 `joinquant`, `ricequant` 的模拟交易 * 支持跟踪 雪球组合 调仓 From 6f62cc7578afab360a77a19205bd3fd94e5ddc1c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 13 May 2018 11:41:13 +0800 Subject: [PATCH 117/276] :bug: fix xq_follower adjust_sell_price should handle project stock code --- easytrader/follower.py | 134 +++++++++++++++++++++----------------- easytrader/xq_follower.py | 1 + tests/test_xq_follower.py | 1 + 3 files changed, 76 insertions(+), 60 deletions(-) diff --git a/easytrader/follower.py b/easytrader/follower.py index f2eaa8ec..0c066c7e 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -12,6 +12,7 @@ # noinspection PyUnresolvedReferences from six.moves.queue import Queue +from . import exceptions from .log import log @@ -59,7 +60,9 @@ def _generate_headers(self): 'Accept-Language': 'en-US,en;q=0.8', 'User-Agent': - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.100 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/54.0.2840.100 Safari/537.36', 'Referer': self.WEB_REFERER, 'X-Requested-With': @@ -213,6 +216,73 @@ def _is_number(s): except ValueError: return False + 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.now() + expire = (now - trade_cmd['datetime']).total_seconds() + if expire > expire_seconds: + log.warning( + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃'. + format(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: + log.warning( + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃'. + format(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: + log.warning( + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃'. + format(trade_cmd['strategy_name'], trade_cmd['stock_code'], + trade_cmd['action'], trade_cmd['amount'], + trade_cmd['price'], trade_cmd['datetime'], now)) + break + + args = { + 'security': trade_cmd['stock_code'], + 'price': trade_cmd['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) + log.error( + '{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}'. + format(trader_name, trade_cmd['strategy_name'], + trade_cmd['stock_code'], trade_cmd['action'], + trade_cmd['amount'], trade_cmd['price'], + trade_cmd['datetime'], err_msg)) + else: + log.info( + '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}'. + format(trade_cmd['strategy_name'], trade_cmd['stock_code'], + trade_cmd['action'], trade_cmd['amount'], + trade_cmd['price'], trade_cmd['datetime'], + response)) + def trade_worker(self, users, expire_seconds=120, @@ -223,65 +293,9 @@ def trade_worker(self, """ while True: trade_cmd = self.trade_queue.get() - for user in users: - # check expire - now = datetime.now() - expire = (now - trade_cmd['datetime']).total_seconds() - if expire > expire_seconds: - log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃'. - format(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: - log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃'. - format(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: - log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃'. - format(trade_cmd['strategy_name'], - trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], trade_cmd['price'], - trade_cmd['datetime'], now)) - break - - args = { - 'security': trade_cmd['stock_code'], - 'price': trade_cmd['price'], - 'amount': trade_cmd['amount'], - 'entrust_prop': entrust_prop - } - try: - response = getattr(user, trade_cmd['action'])(**args) - except Exception as e: - trader_name = type(user).__name__ - err_msg = '{}: {}'.format(type(e).__name__, e.message) - log.error( - '{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}'. - format(trader_name, trade_cmd['strategy_name'], - trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], trade_cmd['price'], - trade_cmd['datetime'], err_msg)) - continue - log.info( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}'. - format(trade_cmd['strategy_name'], trade_cmd['stock_code'], - trade_cmd['action'], trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], - response)) - time.sleep(send_interval) + self._execute_trade_cmd(trade_cmd, users, expire_seconds, + entrust_prop, send_interval) + time.sleep(send_interval) def query_strategy_transaction(self, strategy, **kwargs): params = self.create_query_transaction_params(strategy) diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index 2df017cd..39a8d685 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -183,6 +183,7 @@ def _adjust_sell_amount(self, stock_code, amount): :return: 考虑实际持仓之后的卖出股份数 :rtype: int """ + stock_code = stock_code[-6:] user = self._users[0] position = user.position try: diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py index d78fe086..49238f40 100644 --- a/tests/test_xq_follower.py +++ b/tests/test_xq_follower.py @@ -28,6 +28,7 @@ def test_adjust_sell_amount(self): ('169101', 600, 600), ('169101', 700, 600), ('000000', 100, 100), + ('sh169101', 700, 600), ] for stock_code, sell_amount, excepted_amount in test_cases: amount = follower._adjust_sell_amount(stock_code, sell_amount) From 6028b16e5ef0d1d6e2d3a636e1250a64db401d2b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 13 May 2018 11:41:19 +0800 Subject: [PATCH 118/276] =?UTF-8?q?Bump=20version:=200.14.0=20=E2=86=92=20?= =?UTF-8?q?0.14.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4dc88e92..c2c2a256 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.0 +current_version = 0.14.1 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 4faf1beb..0f04eb3e 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.14.0' +__version__ = '0.14.1' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index 0f53d840..eb867d74 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.14.0', + version='0.14.1', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 554ed518ed8ad9e03cf20b012b4b705eabf5eb17 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 24 May 2018 22:10:43 +0800 Subject: [PATCH 119/276] :bug: add rqopen-client version limit, support pip 10 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b04cbb85..6316789a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,5 @@ Pillow pytesseract pandas pyperclip -rqopen-client +rqopen-client>=0.0.5 easyutils From 9b19135e2637adf0f29b593ba739397a1e49cefe Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 24 May 2018 22:10:50 +0800 Subject: [PATCH 120/276] =?UTF-8?q?Bump=20version:=200.14.1=20=E2=86=92=20?= =?UTF-8?q?0.14.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c2c2a256..4adce60d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.1 +current_version = 0.14.2 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 0f04eb3e..9e9c0ff2 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.14.1' +__version__ = '0.14.2' __author__ = 'shidenggui' diff --git a/setup.py b/setup.py index eb867d74..c1fd5119 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name='easytrader', - version='0.14.1', + version='0.14.2', description='A utility for China Stock Trade', long_description=long_desc, author='shidenggui', From 02f09197d6c470def70b9f6cc8654fd5c2056964 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 28 Jun 2018 00:07:09 +0800 Subject: [PATCH 121/276] :hammer: use black to format code --- cli.py | 34 +- easytrader/__init__.py | 4 +- easytrader/api.py | 22 +- easytrader/clienttrader.py | 182 ++++++----- easytrader/config/client.py | 129 ++++---- easytrader/follower.py | 244 ++++++++------- easytrader/gj_clienttrader.py | 24 +- easytrader/helpers.py | 128 ++++---- easytrader/ht_clienttrader.py | 22 +- easytrader/joinquant_follower.py | 97 +++--- easytrader/log.py | 6 +- easytrader/remoteclient.py | 61 ++-- easytrader/ricequant_follower.py | 62 +++- easytrader/server.py | 58 ++-- easytrader/webtrader.py | 39 +-- easytrader/xqtrader.py | 512 ++++++++++++++++--------------- easytrader/yh_clienttrader.py | 33 +- setup.py | 50 +-- tests/test_easytrader.py | 62 ++-- tests/test_xq_follower.py | 44 +-- tests/test_xqtrader.py | 5 +- 21 files changed, 1005 insertions(+), 813 deletions(-) diff --git a/cli.py b/cli.py index 1bcb62b5..43abf030 100644 --- a/cli.py +++ b/cli.py @@ -7,26 +7,34 @@ import easytrader -ACCOUNT_OBJECT_FILE = 'account.session' +ACCOUNT_OBJECT_FILE = "account.session" @click.command() -@click.option('--use', help='指定券商 [ht, yjb, yh]') -@click.option('--prepare', type=click.Path(exists=True), help='指定登录账户文件路径') -@click.option('--get', help='调用 easytrader 中对应的变量') -@click.option('--do', help='调用 easytrader 中对应的函数名') -@click.option('--debug', default=False, help='是否输出 easytrader 的 debug 日志') -@click.argument('params', nargs=-1) +@click.option("--use", help="指定券商 [ht, yjb, yh]") +@click.option("--prepare", type=click.Path(exists=True), help="指定登录账户文件路径") +@click.option("--get", help="调用 easytrader 中对应的变量") +@click.option("--do", help="调用 easytrader 中对应的函数名") +@click.option("--debug", default=False, help="是否输出 easytrader 的 debug 日志") +@click.argument("params", nargs=-1) def main(prepare, use, do, get, params, debug): if get is not None: do = get - if prepare is not None and use in ['ht_client', 'yjb', 'yh_client','yh','ht', 'gf', 'xq']: + if prepare is not None and use in [ + "ht_client", + "yjb", + "yh_client", + "yh", + "ht", + "gf", + "xq", + ]: user = easytrader.use(use, debug) user.prepare(prepare) - with open(ACCOUNT_OBJECT_FILE, 'wb') as f: + with open(ACCOUNT_OBJECT_FILE, "wb") as f: dill.dump(user, f) if do is not None: - with open(ACCOUNT_OBJECT_FILE, 'rb') as f: + with open(ACCOUNT_OBJECT_FILE, "rb") as f: user = dill.load(f) if get is not None: @@ -34,9 +42,11 @@ def main(prepare, use, do, get, params, debug): else: result = getattr(user, do)(*params) - json_result = json.dumps(result, indent=4, ensure_ascii=False, sort_keys=True) + json_result = json.dumps( + result, indent=4, ensure_ascii=False, sort_keys=True + ) click.echo(json_result) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 9e9c0ff2..c44aa982 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = '0.14.2' -__author__ = 'shidenggui' +__version__ = "0.14.2" +__author__ = "shidenggui" diff --git a/easytrader/api.py b/easytrader/api.py index 667c0bc4..09a32cc0 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -9,7 +9,7 @@ from .xqtrader import XueQiuTrader if six.PY2: - raise TypeError('不支持 Python2,请升级 Python3 ') + raise TypeError("不支持 Python2,请升级 Python3 ") def use(broker, debug=True, **kwargs): @@ -27,19 +27,23 @@ def use(broker, debug=True, **kwargs): """ if not debug: log.setLevel(logging.INFO) - elif broker.lower() in ['xq', '雪球']: + elif broker.lower() in ["xq", "雪球"]: return XueQiuTrader(**kwargs) - elif broker.lower() in ['yh_client', '银河客户端']: + elif broker.lower() in ["yh_client", "银河客户端"]: from .yh_clienttrader import YHClientTrader + return YHClientTrader() - elif broker.lower() in ['ht_client', '华泰客户端']: + elif broker.lower() in ["ht_client", "华泰客户端"]: from .ht_clienttrader import HTClientTrader + return HTClientTrader() - elif broker.lower() in ['gj_client', '国金客户端']: + elif broker.lower() in ["gj_client", "国金客户端"]: from .gj_clienttrader import GJClientTrader + return GJClientTrader() - elif broker.lower() in ['ths', '同花顺客户端']: + elif broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader + return ClientTrader() @@ -61,9 +65,9 @@ def follower(platform, **kwargs): >>> jq.login(user='username', password='password') >>> jq.follow(users=user, strategies=['strategies_link']) """ - if platform.lower() in ['rq', 'ricequant', '米筐']: + if platform.lower() in ["rq", "ricequant", "米筐"]: return RiceQuantFollower() - if platform.lower() in ['jq', 'joinquant', '聚宽']: + if platform.lower() in ["jq", "joinquant", "聚宽"]: return JoinQuantFollower() - if platform.lower() in ['xq', 'xueqiu', '雪球']: + if platform.lower() in ["xq", "xueqiu", "雪球"]: return XueQiuFollower(**kwargs) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index f776c06b..b0423154 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -15,7 +15,7 @@ from .config import client from .log import log -if not sys.platform.startswith('darwin'): +if not sys.platform.startswith("darwin"): import pywinauto import pywinauto.clipboard @@ -25,30 +25,30 @@ def __init__(self, app): self._app = app def handle(self, title): - if any(s in title for s in {'提示信息', '委托确认', '网上交易用户协议'}): + if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): self._submit_by_shortcut() - elif '提示' in title: + elif "提示" in title: content = self._extract_content() self._submit_by_click() - return {'message': content} + return {"message": content} else: content = self._extract_content() self._close() - return {'message': 'unknown message: {}'.format(content)} + return {"message": "unknown message: {}".format(content)} def _extract_content(self): return self._app.top_window().Static.window_text() def _extract_entrust_id(self, content): - return re.search(r'\d+', content).group() + return re.search(r"\d+", content).group() def _submit_by_click(self): - self._app.top_window()['确定'].click() + self._app.top_window()["确定"].click() def _submit_by_shortcut(self): - self._app.top_window().type_keys('%Y') + self._app.top_window().type_keys("%Y") def _close(self): self._app.top_window().close() @@ -56,22 +56,22 @@ def _close(self): class TradePopDialogHandler(PopDialogHandler): def handle(self, title): - if title == '委托确认': + if title == "委托确认": self._submit_by_shortcut() - elif title == '提示信息': + elif title == "提示信息": content = self._extract_content() - if '超出涨跌停' in content: + if "超出涨跌停" in content: self._submit_by_shortcut() - elif '委托价格的小数价格应为' in content: + elif "委托价格的小数价格应为" in content: self._submit_by_shortcut() - elif title == '提示': + elif title == "提示": content = self._extract_content() - if '成功' in content: + if "成功" in content: entrust_no = self._extract_entrust_id(content) self._submit_by_click() - return {'entrust_no': entrust_no} + return {"entrust_no": entrust_no} else: self._submit_by_click() time.sleep(0.05) @@ -86,13 +86,15 @@ def __init__(self): self._app = None self._main = None - def prepare(self, - config_path=None, - user=None, - password=None, - exe_path=None, - comm_password=None, - **kwargs): + def prepare( + self, + config_path=None, + user=None, + password=None, + exe_path=None, + comm_password=None, + **kwargs + ): """ 登陆客户端 :param config_path: 登陆配置文件,跟参数登陆方式二选一 @@ -104,12 +106,17 @@ def prepare(self, """ if config_path is not None: account = helpers.file2dict(config_path) - user = account['user'] - password = account['password'] - comm_password = account.get('comm_password') - exe_path = account.get('exe_path') - self.login(user, password, exe_path or self._config.DEFAULT_EXE_PATH, - comm_password, **kwargs) + user = account["user"] + password = account["password"] + comm_password = account.get("comm_password") + exe_path = account.get("exe_path") + self.login( + user, + password, + exe_path or self._config.DEFAULT_EXE_PATH, + comm_password, + **kwargs + ) @abstractmethod def login(self, user, password, exe_path, comm_password=None, **kwargs): @@ -124,20 +131,22 @@ def connect(self, exe_path=None, **kwargs): connect_path = exe_path or self._config.DEFAULT_EXE_PATH if connect_path is None: raise ValueError( - '参数 exe_path 未设置,请设置客户端对应的 exe 地址,类似 C:\\客户端安装目录\\xiadan.exe') + "参数 exe_path 未设置,请设置客户端对应的 exe 地址,类似 C:\\客户端安装目录\\xiadan.exe" + ) self._app = pywinauto.Application().connect( - path=connect_path, timeout=10) + path=connect_path, timeout=10 + ) self._close_prompt_windows() self._main = self._app.top_window() @property def broker_type(self): - return 'ths' + return "ths" @property def balance(self): - self._switch_left_menus(['查询[F4]', '资金股票']) + self._switch_left_menus(["查询[F4]", "资金股票"]) return self._get_balance_from_statics() @@ -146,53 +155,55 @@ def _get_balance_from_statics(self): for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): result[key] = float( self._main.window( - control_id=control_id, - class_name='Static', - ).window_text()) + control_id=control_id, class_name="Static" + ).window_text() + ) return result @property def position(self): - self._switch_left_menus(['查询[F4]', '资金股票']) + self._switch_left_menus(["查询[F4]", "资金股票"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @property def today_entrusts(self): - self._switch_left_menus(['查询[F4]', '当日委托']) + self._switch_left_menus(["查询[F4]", "当日委托"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @property def today_trades(self): - self._switch_left_menus(['查询[F4]', '当日成交']) + self._switch_left_menus(["查询[F4]", "当日成交"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @property def cancel_entrusts(self): self._refresh() - self._switch_left_menus(['撤单[F3]']) + self._switch_left_menus(["撤单[F3]"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) def cancel_entrust(self, entrust_no): self._refresh() for i, entrust in enumerate(self.cancel_entrusts): - if entrust[ - self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: + if ( + entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] + == entrust_no + ): self._cancel_entrust_by_double_click(i) return self._handle_pop_dialogs() else: - return {'message': '委托单状态错误不能撤单, 该委托单可能已经成交或者已撤'} + return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} def buy(self, security, price, amount, **kwargs): - self._switch_left_menus(['买入[F1]']) + self._switch_left_menus(["买入[F1]"]) return self.trade(security, price, amount) def sell(self, security, price, amount, **kwargs): - self._switch_left_menus(['卖出[F2]']) + self._switch_left_menus(["卖出[F2]"]) return self.trade(security, price, amount) @@ -207,7 +218,7 @@ def market_buy(self, security, amount, ttype=None, **kwargs): :return: {'entrust_no': '委托单号'} """ - self._switch_left_menus(['市价委托', '买入']) + self._switch_left_menus(["市价委托", "买入"]) return self.market_trade(security, amount, ttype) @@ -222,7 +233,7 @@ def market_sell(self, security, amount, ttype=None, **kwargs): :return: {'entrust_no': '委托单号'} """ - self._switch_left_menus(['市价委托', '卖出']) + self._switch_left_menus(["市价委托", "卖出"]) return self.market_trade(security, amount, ttype) @@ -248,7 +259,8 @@ def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" selects = self._main( control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, - class_name='ComboBox') + class_name="ComboBox", + ) for i, text in selects.texts(): # skip 0 index, because 0 index is current select index if i == 0: @@ -257,7 +269,7 @@ def _set_market_trade_type(self, ttype): selects.select(i - 1) break else: - raise TypeError('不支持对应的市价类型: {}'.format(ttype)) + raise TypeError("不支持对应的市价类型: {}".format(ttype)) def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) @@ -265,13 +277,13 @@ def auto_ipo(self): stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) if len(stock_list) == 0: - return {'message': '今日无新股'} + return {"message": "今日无新股"} invalid_list_idx = [ - i for i, v in enumerate(stock_list) if v['申购数量'] <= 0 + i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 ] if len(stock_list) == len(invalid_list_idx): - return {'message': '没有发现可以申购的新股'} + return {"message": "没有发现可以申购的新股"} self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) self._wait(0.1) @@ -287,18 +299,24 @@ def auto_ipo(self): def _click_grid_by_row(self, row): x = self._config.COMMON_GRID_LEFT_MARGIN - y = self._config.COMMON_GRID_FIRST_ROW_HEIGHT + self._config.COMMON_GRID_ROW_HEIGHT * row + y = ( + self._config.COMMON_GRID_FIRST_ROW_HEIGHT + + self._config.COMMON_GRID_ROW_HEIGHT * row + ) self._app.top_window().window( control_id=self._config.COMMON_GRID_CONTROL_ID, - class_name='CVirtualGridCtrl').click(coords=(x, y)) + class_name="CVirtualGridCtrl", + ).click(coords=(x, y)) def _is_exist_pop_dialog(self): self._wait(0.2) # wait dialog display - return self._main.wrapper_object() != self._app.top_window( - ).wrapper_object() + return ( + self._main.wrapper_object() + != self._app.top_window().wrapper_object() + ) def _run_exe_path(self, exe_path): - return os.path.join(os.path.dirname(exe_path), 'xiadan.exe') + return os.path.join(os.path.dirname(exe_path), "xiadan.exe") def _wait(self, seconds): time.sleep(seconds) @@ -308,7 +326,7 @@ def exit(self): def _close_prompt_windows(self): self._wait(1) - for w in self._app.windows(class_name='#32770'): + for w in self._app.windows(class_name="#32770"): if w.window_text() != self._config.TITLE: w.close() self._wait(1) @@ -322,17 +340,22 @@ def trade(self, security, price, amount): def _click(self, control_id): self._app.top_window().window( - control_id=control_id, class_name='Button').click() + control_id=control_id, class_name="Button" + ).click() def _submit_trade(self): time.sleep(0.05) self._main.window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, - class_name='Button').click() + class_name="Button", + ).click() def _get_pop_dialog_title(self): - return self._app.top_window().window( - control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID).window_text() + return ( + self._app.top_window() + .window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + .window_text() + ) def _set_trade_params(self, security, price, amount): code = security[-6:] @@ -342,8 +365,10 @@ def _set_trade_params(self, security, price, amount): # wait security input finish self._wait(0.1) - self._type_keys(self._config.TRADE_PRICE_CONTROL_ID, - easyutils.round_price_by_code(price, code)) + self._type_keys( + self._config.TRADE_PRICE_CONTROL_ID, + easyutils.round_price_by_code(price, code), + ) self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _set_market_trade_params(self, security, amount): @@ -358,20 +383,22 @@ def _set_market_trade_params(self, security, amount): def _get_grid_data(self, control_id): grid = self._main.window( - control_id=control_id, class_name='CVirtualGridCtrl') - grid.type_keys('^A^C') + control_id=control_id, class_name="CVirtualGridCtrl" + ) + grid.type_keys("^A^C") return self._format_grid_data(self._get_clipboard_data()) def _type_keys(self, control_id, text): self._main.window( - control_id=control_id, class_name='Edit').set_edit_text(text) + control_id=control_id, class_name="Edit" + ).set_edit_text(text) def _get_clipboard_data(self): while True: try: return pywinauto.clipboard.GetData() except Exception as e: - log.warning('{}, retry ......'.format(e)) + log.warning("{}, retry ......".format(e)) def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() @@ -386,9 +413,10 @@ def _get_left_menus_handle(self): while True: try: handle = self._main.window( - control_id=129, class_name='SysTreeView32') + control_id=129, class_name="SysTreeView32" + ) # sometime can't find handle ready, must retry - handle.wait('ready', 2) + handle.wait("ready", 2) return handle except: pass @@ -396,21 +424,25 @@ def _get_left_menus_handle(self): def _format_grid_data(self, data): df = pd.read_csv( io.StringIO(data), - delimiter='\t', + delimiter="\t", dtype=self._config.GRID_DTYPE, na_filter=False, ) - return df.to_dict('records') + return df.to_dict("records") def _cancel_entrust_by_double_click(self, row): x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN - y = self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row + y = ( + self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row + ) self._app.top_window().window( control_id=self._config.COMMON_GRID_CONTROL_ID, - class_name='CVirtualGridCtrl').double_click(coords=(x, y)) + class_name="CVirtualGridCtrl", + ).double_click(coords=(x, y)) def _refresh(self): - self._switch_left_menus(['买入[F1]'], sleep=0.05) + self._switch_left_menus(["买入[F1]"], sleep=0.05) def _handle_pop_dialogs(self, handler_class=PopDialogHandler): handler = handler_class(self._app) @@ -421,4 +453,4 @@ def _handle_pop_dialogs(self, handler_class=PopDialogHandler): result = handler.handle(title) if result: return result - return {'message': 'success'} + return {"message": "success"} diff --git a/easytrader/config/client.py b/easytrader/config/client.py index e6710737..6b13a544 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -2,21 +2,20 @@ def create(broker): - if broker == 'yh': + if broker == "yh": return YH - elif broker == 'ht': + elif broker == "ht": return HT - elif broker == 'gj': + elif broker == "gj": return GJ - elif broker == 'ths': + elif broker == "ths": return CommonConfig raise NotImplemented class CommonConfig: DEFAULT_EXE_PATH = None - TITLE = '网上股票交易系统5.0' - + TITLE = "网上股票交易系统5.0" TRADE_SECURITY_CONTROL_ID = 1032 TRADE_PRICE_CONTROL_ID = 1033 @@ -32,103 +31,103 @@ class CommonConfig: COMMON_GRID_FIRST_ROW_HEIGHT = 30 COMMON_GRID_ROW_HEIGHT = 16 - BALANCE_MENU_PATH = ['查询[F4]', '资金股票'] - POSITION_MENU_PATH = ['查询[F4]', '资金股票'] - TODAY_ENTRUSTS_MENU_PATH = ['查询[F4]', '当日委托'] - TODAY_TRADES_MENU_PATH = ['查询[F4]', '当日成交'] + BALANCE_MENU_PATH = ["查询[F4]", "资金股票"] + POSITION_MENU_PATH = ["查询[F4]", "资金股票"] + TODAY_ENTRUSTS_MENU_PATH = ["查询[F4]", "当日委托"] + TODAY_TRADES_MENU_PATH = ["查询[F4]", "当日成交"] BALANCE_CONTROL_ID_GROUP = { - '资金余额': 1012, - '可用金额': 1016, - '可取金额': 1017, - '股票市值': 1014, - '总资产': 1015 + "资金余额": 1012, + "可用金额": 1016, + "可取金额": 1017, + "股票市值": 1014, + "总资产": 1015, } POP_DIALOD_TITLE_CONTROL_ID = 1365 GRID_DTYPE = { - '操作日期': str, - '委托编号': str, - '申请编号': str, - '合同编号': str, - '证券代码': str, - '股东代码': str, - '资金帐号': str, - '资金帐户': str, - '发生日期': str + "操作日期": str, + "委托编号": str, + "申请编号": str, + "合同编号": str, + "证券代码": str, + "股东代码": str, + "资金帐号": str, + "资金帐户": str, + "发生日期": str, } - CANCEL_ENTRUST_ENTRUST_FIELD = '合同编号' + CANCEL_ENTRUST_ENTRUST_FIELD = "合同编号" CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50 CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30 CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16 AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 AUTO_IPO_BUTTON_CONTROL_ID = 1006 - AUTO_IPO_MENU_PATH = ['新股申购', '批量新股申购'] + AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"] class YH(CommonConfig): - DEFAULT_EXE_PATH = r'C:\中国银河证券双子星3.2\Binarystar.exe' + DEFAULT_EXE_PATH = r"C:\中国银河证券双子星3.2\Binarystar.exe" BALANCE_GRID_CONTROL_ID = 1308 GRID_DTYPE = { - '操作日期': str, - '委托编号': str, - '申请编号': str, - '合同编号': str, - '证券代码': str, - '股东代码': str, - '资金帐号': str, - '资金帐户': str, - '发生日期': str + "操作日期": str, + "委托编号": str, + "申请编号": str, + "合同编号": str, + "证券代码": str, + "股东代码": str, + "资金帐号": str, + "资金帐户": str, + "发生日期": str, } - AUTO_IPO_MENU_PATH = ['新股申购', '一键打新'] + AUTO_IPO_MENU_PATH = ["新股申购", "一键打新"] class HT(CommonConfig): - DEFAULT_EXE_PATH = r'C:\htzqzyb2\xiadan.exe' + DEFAULT_EXE_PATH = r"C:\htzqzyb2\xiadan.exe" BALANCE_CONTROL_ID_GROUP = { - '资金余额': 1012, - '冻结资金': 1013, - '可用金额': 1016, - '可取金额': 1017, - '股票市值': 1014, - '总资产': 1015 + "资金余额": 1012, + "冻结资金": 1013, + "可用金额": 1016, + "可取金额": 1017, + "股票市值": 1014, + "总资产": 1015, } GRID_DTYPE = { - '操作日期': str, - '委托编号': str, - '申请编号': str, - '合同编号': str, - '证券代码': str, - '股东代码': str, - '资金帐号': str, - '资金帐户': str, - '发生日期': str + "操作日期": str, + "委托编号": str, + "申请编号": str, + "合同编号": str, + "证券代码": str, + "股东代码": str, + "资金帐号": str, + "资金帐户": str, + "发生日期": str, } - AUTO_IPO_MENU_PATH = ['新股申购', '批量新股申购'] + AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"] class GJ(CommonConfig): - DEFAULT_EXE_PATH = 'C:\\全能行证券交易终端\\xiadan.exe' + DEFAULT_EXE_PATH = "C:\\全能行证券交易终端\\xiadan.exe" GRID_DTYPE = { - '操作日期': str, - '委托编号': str, - '申请编号': str, - '合同编号': str, - '证券代码': str, - '股东代码': str, - '资金帐号': str, - '资金帐户': str, - '发生日期': str + "操作日期": str, + "委托编号": str, + "申请编号": str, + "合同编号": str, + "证券代码": str, + "股东代码": str, + "资金帐号": str, + "资金帐户": str, + "发生日期": str, } - AUTO_IPO_MENU_PATH = ['新股申购', '新股批量申购'] + AUTO_IPO_MENU_PATH = ["新股申购", "新股批量申购"] diff --git a/easytrader/follower.py b/easytrader/follower.py index 0c066c7e..749cc660 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -9,6 +9,7 @@ from threading import Thread import requests + # noinspection PyUnresolvedReferences from six.moves.queue import Queue @@ -17,12 +18,12 @@ class BaseFollower(object): - LOGIN_PAGE = '' - LOGIN_API = '' - TRANSACTION_API = '' - CMD_CACHE_FILE = 'cmd_cache.pk' - WEB_REFERER = '' - WEB_ORIGIN = '' + LOGIN_PAGE = "" + LOGIN_API = "" + TRANSACTION_API = "" + CMD_CACHE_FILE = "cmd_cache.pk" + WEB_REFERER = "" + WEB_ORIGIN = "" def __init__(self): self.trade_queue = Queue() @@ -49,28 +50,20 @@ def login(self, user=None, password=None, **kwargs): rep = self.s.post(self.LOGIN_API, data=params) self.check_login_success(rep) - log.info('登录成功') + log.info("登录成功") def _generate_headers(self): headers = { - 'Accept': - 'application/json, text/javascript, */*; q=0.01', - 'Accept-Encoding': - 'gzip, deflate, br', - 'Accept-Language': - 'en-US,en;q=0.8', - 'User-Agent': - 'Mozilla/5.0 (X11; Linux x86_64) ' - 'AppleWebKit/537.36 (KHTML, like Gecko) ' - 'Chrome/54.0.2840.100 Safari/537.36', - 'Referer': - self.WEB_REFERER, - 'X-Requested-With': - 'XMLHttpRequest', - 'Origin': - self.WEB_ORIGIN, - 'Content-Type': - 'application/x-www-form-urlencoded; charset=UTF-8', + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.8", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/54.0.2840.100 Safari/537.36", + "Referer": self.WEB_REFERER, + "X-Requested-With": "XMLHttpRequest", + "Origin": self.WEB_ORIGIN, + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", } return headers @@ -88,13 +81,15 @@ def create_login_params(self, user, password, **kwargs): """ pass - def follow(self, - users, - strategies, - track_interval=1, - trade_cmd_expire_seconds=120, - cmd_cache=True, - **kwargs): + def follow( + self, + users, + strategies, + track_interval=1, + trade_cmd_expire_seconds=120, + cmd_cache=True, + **kwargs + ): """跟踪平台对应的模拟交易,支持多用户多策略 :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户 :param strategies: 雪球组合名, 类似 ZH123450 @@ -111,22 +106,25 @@ def follow(self, def load_expired_cmd_cache(self): if os.path.exists(self.CMD_CACHE_FILE): - with open(self.CMD_CACHE_FILE, 'rb') as f: + with open(self.CMD_CACHE_FILE, "rb") as f: self.expired_cmds = pickle.load(f) - def start_trader_thread(self, - users, - trade_cmd_expire_seconds, - entrust_prop='limit', - send_interval=0): + def start_trader_thread( + self, + users, + trade_cmd_expire_seconds, + entrust_prop="limit", + send_interval=0, + ): trader = Thread( target=self.trade_worker, args=[users], kwargs={ - 'expire_seconds': trade_cmd_expire_seconds, - 'entrust_prop': entrust_prop, - 'send_interval': send_interval - }) + "expire_seconds": trade_cmd_expire_seconds, + "entrust_prop": entrust_prop, + "send_interval": send_interval, + }, + ) trader.setDaemon(True) trader.start() @@ -161,41 +159,52 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): while True: try: transactions = self.query_strategy_transaction( - strategy, **kwargs) + strategy, **kwargs + ) except Exception as e: - log.warning('无法获取策略 {} 调仓信息, 错误: {}, 跳过此次调仓查询'.format(name, e)) + log.warning("无法获取策略 {} 调仓信息, 错误: {}, 跳过此次调仓查询".format(name, e)) continue for t in transactions: trade_cmd = { - 'strategy': strategy, - 'strategy_name': name, - 'action': t['action'], - 'stock_code': t['stock_code'], - 'amount': t['amount'], - 'price': t['price'], - 'datetime': t['datetime'] + "strategy": strategy, + "strategy_name": name, + "action": t["action"], + "stock_code": t["stock_code"], + "amount": t["amount"], + "price": t["price"], + "datetime": t["datetime"], } if self.is_cmd_expired(trade_cmd): continue log.info( - '策略 [{}] 发送指令到交易队列, 股票: {} 动作: {} 数量: {} 价格: {} 信号产生时间: {}'. - format(name, trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], trade_cmd['price'], - trade_cmd['datetime'])) + "策略 [{}] 发送指令到交易队列, 股票: {} 动作: {} 数量: {} 价格: {} 信号产生时间: {}".format( + 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: - log.info('程序退出') + log.info("程序退出") break @staticmethod def generate_expired_cmd_key(cmd): - return '{}_{}_{}_{}_{}_{}'.format( - cmd['strategy_name'], cmd['stock_code'], cmd['action'], - cmd['amount'], cmd['price'], cmd['datetime']) + return "{}_{}_{}_{}_{}_{}".format( + cmd["strategy_name"], + cmd["stock_code"], + cmd["action"], + cmd["amount"], + cmd["price"], + cmd["datetime"], + ) def is_cmd_expired(self, cmd): key = self.generate_expired_cmd_key(cmd) @@ -205,7 +214,7 @@ def add_cmd_to_expired_cmds(self, cmd): key = self.generate_expired_cmd_key(cmd) self.expired_cmds.add(key) - with open(self.CMD_CACHE_FILE, 'wb') as f: + with open(self.CMD_CACHE_FILE, "wb") as f: pickle.dump(self.expired_cmds, f) @staticmethod @@ -216,8 +225,9 @@ def _is_number(s): except ValueError: return False - def _execute_trade_cmd(self, trade_cmd, users, expire_seconds, - entrust_prop, send_interval): + def _execute_trade_cmd( + self, trade_cmd, users, expire_seconds, entrust_prop, send_interval + ): """分发交易指令到对应的 user 并执行 :param trade_cmd: :param users: @@ -229,72 +239,100 @@ def _execute_trade_cmd(self, trade_cmd, users, expire_seconds, for user in users: # check expire now = datetime.now() - expire = (now - trade_cmd['datetime']).total_seconds() + expire = (now - trade_cmd["datetime"]).total_seconds() if expire > expire_seconds: log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃'. - format(trade_cmd['strategy_name'], trade_cmd['stock_code'], - trade_cmd['action'], trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], now, - expire_seconds)) + "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃".format( + 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'] + price = trade_cmd["price"] if not self._is_number(price) or price <= 0: log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃'. - format(trade_cmd['strategy_name'], trade_cmd['stock_code'], - trade_cmd['action'], trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], now)) + "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃".format( + 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: + if trade_cmd["amount"] <= 0: log.warning( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃'. - format(trade_cmd['strategy_name'], trade_cmd['stock_code'], - trade_cmd['action'], trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], now)) + "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃".format( + trade_cmd["strategy_name"], + trade_cmd["stock_code"], + trade_cmd["action"], + trade_cmd["amount"], + trade_cmd["price"], + trade_cmd["datetime"], + now, + ) + ) break args = { - 'security': trade_cmd['stock_code'], - 'price': trade_cmd['price'], - 'amount': trade_cmd['amount'], - 'entrust_prop': entrust_prop + "security": trade_cmd["stock_code"], + "price": trade_cmd["price"], + "amount": trade_cmd["amount"], + "entrust_prop": entrust_prop, } try: - response = getattr(user, trade_cmd['action'])(**args) + 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) + err_msg = "{}: {}".format(type(e).__name__, e.args) log.error( - '{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}'. - format(trader_name, trade_cmd['strategy_name'], - trade_cmd['stock_code'], trade_cmd['action'], - trade_cmd['amount'], trade_cmd['price'], - trade_cmd['datetime'], err_msg)) + "{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}".format( + trader_name, + trade_cmd["strategy_name"], + trade_cmd["stock_code"], + trade_cmd["action"], + trade_cmd["amount"], + trade_cmd["price"], + trade_cmd["datetime"], + err_msg, + ) + ) else: log.info( - '策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}'. - format(trade_cmd['strategy_name'], trade_cmd['stock_code'], - trade_cmd['action'], trade_cmd['amount'], - trade_cmd['price'], trade_cmd['datetime'], - response)) - - def trade_worker(self, - users, - expire_seconds=120, - entrust_prop='limit', - send_interval=0): + "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}".format( + trade_cmd["strategy_name"], + trade_cmd["stock_code"], + trade_cmd["action"], + trade_cmd["amount"], + trade_cmd["price"], + trade_cmd["datetime"], + response, + ) + ) + + def trade_worker( + self, users, expire_seconds=120, entrust_prop="limit", send_interval=0 + ): """ :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时买出单没有及时成交导致的买入金额不足 """ while True: trade_cmd = self.trade_queue.get() - self._execute_trade_cmd(trade_cmd, users, expire_seconds, - entrust_prop, send_interval) + self._execute_trade_cmd( + trade_cmd, users, expire_seconds, entrust_prop, send_interval + ) time.sleep(send_interval) def query_strategy_transaction(self, strategy, **kwargs): @@ -339,7 +377,7 @@ def order_transactions_sell_first(self, transactions): # 调整调仓记录的顺序为先卖再买 sell_first_transactions = [] for t in transactions: - if t['action'] == 'sell': + if t["action"] == "sell": sell_first_transactions.insert(0, t) else: sell_first_transactions.append(t) diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index 04760d28..2d1311a4 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -15,7 +15,7 @@ class GJClientTrader(ClientTrader): @property def broker_type(self): - return 'gj' + return "gj" def login(self, user, password, exe_path, comm_password=None, **kwargs): """ @@ -28,14 +28,15 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): """ try: self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=1) + path=self._run_exe_path(exe_path), timeout=1 + ) except Exception: self._app = pywinauto.Application().start(exe_path) # wait login window ready while True: try: - self._app.top_window().Edit1.wait('ready') + self._app.top_window().Edit1.wait("ready") break except RuntimeError: pass @@ -48,27 +49,28 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): code = self._handle_verify_code() edit3.type_keys(code) time.sleep(1) - self._app.top_window()['确定(Y)'].click() + self._app.top_window()["确定(Y)"].click() # detect login is success or not try: - self._app.top_window().wait_not('exists', 5) + self._app.top_window().wait_not("exists", 5) break except: - self._app.top_window()['确定'].click() + self._app.top_window()["确定"].click() pass except Exception as e: pass self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=10) - self._main = self._app.window(title='网上股票交易系统5.0') + path=self._run_exe_path(exe_path), timeout=10 + ) + self._main = self._app.window(title="网上股票交易系统5.0") def _handle_verify_code(self): control = self._app.top_window().window(control_id=0x5db) control.click() time.sleep(0.2) - file_path = tempfile.mktemp() + '.jpg' + file_path = tempfile.mktemp() + ".jpg" control.capture_as_image().save(file_path) time.sleep(0.2) - vcode = helpers.recognize_verify_code(file_path, 'gj_client') - return ''.join(re.findall('[a-zA-Z0-9]+', vcode)) + vcode = helpers.recognize_verify_code(file_path, "gj_client") + return "".join(re.findall("[a-zA-Z0-9]+", vcode)) diff --git a/easytrader/helpers.py b/easytrader/helpers.py index 9e9a17cd..f9d84e52 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -25,7 +25,8 @@ def init_poolmanager(self, connections, maxsize, block=False): num_pools=connections, maxsize=maxsize, block=block, - ssl_version=ssl.PROTOCOL_TLSv1) + ssl_version=ssl.PROTOCOL_TLSv1, + ) def parse_cookies_str(cookies): @@ -44,7 +45,7 @@ def parse_cookies_str(cookies): def file2dict(path): - with open(path, encoding='utf-8') as f: + with open(path, encoding="utf-8") as f: return json.load(f) @@ -57,17 +58,19 @@ def get_stock_type(stock_code): :param stock_code:股票ID, 若以 'sz', 'sh' 开头直接返回对应类型,否则使用内置规则判断 :return 'sh' or 'sz'""" stock_code = str(stock_code) - if stock_code.startswith(('sh', 'sz')): + if stock_code.startswith(("sh", "sz")): return stock_code[:2] - if stock_code.startswith(('50', '51', '60', '73', '90', '110', '113', - '132', '204', '78')): - return 'sh' - if stock_code.startswith(('00', '13', '18', '15', '16', '18', '20', '30', - '39', '115', '1318')): - return 'sz' - if stock_code.startswith(('5', '6', '9')): - return 'sh' - return 'sz' + if stock_code.startswith( + ("50", "51", "60", "73", "90", "110", "113", "132", "204", "78") + ): + return "sh" + if stock_code.startswith( + ("00", "13", "18", "15", "16", "18", "20", "30", "39", "115", "1318") + ): + return "sz" + if stock_code.startswith(("5", "6", "9")): + return "sh" + return "sz" def ht_verify_code_new(image_path): @@ -79,20 +82,20 @@ def ht_verify_code_new(image_path): img.show() # 关闭图片后输入答案 - s = input('input the pics answer :') + s = input("input the pics answer :") return s -def recognize_verify_code(image_path, broker='ht'): +def recognize_verify_code(image_path, broker="ht"): """识别验证码,返回识别后的字符串,使用 tesseract 实现 :param image_path: 图片路径 :param broker: 券商 ['ht', 'yjb', 'gf', 'yh'] :return recognized: verify code string""" - if broker == 'gf': + if broker == "gf": return detect_gf_result(image_path) - elif broker in ['yh_client', 'gj_client']: + elif broker in ["yh_client", "gj_client"]: return detect_yh_client_result(image_path) # 调用 tesseract 识别 return default_verify_code_detect(image_path) @@ -100,32 +103,36 @@ def recognize_verify_code(image_path, broker='ht'): def detect_yh_client_result(image_path): """封装了tesseract的识别,部署在阿里云上,服务端源码地址为: https://github.com/shidenggui/yh_verify_code_docker""" - api = 'http://yh.ez.shidenggui.com:5000/yh_client' - with open(image_path, 'rb') as f: - rep = requests.post(api, files={'image': f}) + api = "http://yh.ez.shidenggui.com:5000/yh_client" + with open(image_path, "rb") as f: + rep = requests.post(api, files={"image": f}) if rep.status_code != 201: - error = rep.json()['message'] - raise exceptions.TradeError('request {} error: {}'.format(api, error)) - return rep.json()['result'] + error = rep.json()["message"] + raise exceptions.TradeError("request {} error: {}".format(api, error)) + return rep.json()["result"] def input_verify_code_manual(image_path): from PIL import Image + image = Image.open(image_path) image.show() code = input( - 'image path: {}, input verify code answer:'.format(image_path)) + "image path: {}, input verify code answer:".format(image_path) + ) return code def default_verify_code_detect(image_path): from PIL import Image + img = Image.open(image_path) return invoke_tesseract_to_recognize(img) def detect_gf_result(image_path): from PIL import ImageFilter, Image + img = Image.open(image_path) if hasattr(img, "width"): width, height = img.width, img.height @@ -135,7 +142,7 @@ def detect_gf_result(image_path): for y in range(height): if img.getpixel((x, y)) < (100, 100, 100): img.putpixel((x, y), (256, 256, 256)) - gray = img.convert('L') + gray = img.convert("L") two = gray.point(lambda p: 0 if 68 < p < 90 else 256) min_res = two.filter(ImageFilter.MinFilter) med_res = min_res.filter(ImageFilter.MedianFilter) @@ -146,31 +153,34 @@ def detect_gf_result(image_path): def invoke_tesseract_to_recognize(img): import pytesseract + try: res = pytesseract.image_to_string(img) except FileNotFoundError: raise Exception( - 'tesseract 未安装,请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程' + "tesseract 未安装,请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程" ) - valid_chars = re.findall('[0-9a-z]', res, re.IGNORECASE) - return ''.join(valid_chars) + valid_chars = re.findall("[0-9a-z]", res, re.IGNORECASE) + return "".join(valid_chars) def get_mac(): # 获取mac地址 link: http://stackoverflow.com/questions/28927958/python-get-mac-address - return ("".join( - c + "-" if i % 2 else c - for i, c in enumerate(hex(uuid.getnode())[2:].zfill(12)))[:-1] - ).upper() + return ( + "".join( + c + "-" if i % 2 else c + for i, c in enumerate(hex(uuid.getnode())[2:].zfill(12)) + )[:-1] + ).upper() def grep_comma(num_str): - return num_str.replace(',', '') + return num_str.replace(",", "") -def str2num(num_str, convert_type='float'): +def str2num(num_str, convert_type="float"): num = float(grep_comma(num_str)) - return num if convert_type == 'float' else int(num) + return num if convert_type == "float" else int(num) def get_30_date(): @@ -196,25 +206,27 @@ def get_today_ipo_data(): import datetime import requests - agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0' + agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0" send_headers = { - 'Host': 'xueqiu.com', - 'User-Agent': agent, - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', - 'Accept-Encoding': 'deflate', - 'Cache-Control': 'no-cache', - 'X-Requested-With': 'XMLHttpRequest', - 'Referer': 'https://xueqiu.com/hq', - 'Connection': 'keep-alive' + "Host": "xueqiu.com", + "User-Agent": agent, + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3", + "Accept-Encoding": "deflate", + "Cache-Control": "no-cache", + "X-Requested-With": "XMLHttpRequest", + "Referer": "https://xueqiu.com/hq", + "Connection": "keep-alive", } sj = random.randint(1000000000000, 9999999999999) - home_page_url = 'https://xueqiu.com' - ipo_data_url = "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl" \ - "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_" \ - "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" % ( - str(sj)) + home_page_url = "https://xueqiu.com" + ipo_data_url = ( + "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl" + "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_" + "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" + % (str(sj)) + ) session = requests.session() session.get(home_page_url, headers=send_headers) # 产生cookies @@ -223,14 +235,16 @@ def get_today_ipo_data(): json_data = json.loads(ipo_response.text) today_ipo = [] - for line in json_data['data']: + for line in json_data["data"]: # if datetime.datetime(2016, 9, 14).ctime()[:10] == line[3][:10]: - if datetime.datetime.now().strftime('%a %b %d') == line[3][:10]: - today_ipo.append({ - 'stock_code': line[0], - 'stock_name': line[1], - 'apply_code': line[2], - 'price': line[7] - }) + if datetime.datetime.now().strftime("%a %b %d") == line[3][:10]: + today_ipo.append( + { + "stock_code": line[0], + "stock_name": line[1], + "apply_code": line[2], + "price": line[7], + } + ) return today_ipo diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 5fc7606e..7ea93cc2 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -9,7 +9,7 @@ class HTClientTrader(ClientTrader): @property def broker_type(self): - return 'ht' + return "ht" def login(self, user, password, exe_path, comm_password=None, **kwargs): """ @@ -21,18 +21,19 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): :return: """ if comm_password is None: - raise ValueError('华泰必须设置通讯密码') + raise ValueError("华泰必须设置通讯密码") try: self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=1) + path=self._run_exe_path(exe_path), timeout=1 + ) except Exception: self._app = pywinauto.Application().start(exe_path) # wait login window ready while True: try: - self._app.top_window().Edit1.wait('ready') + self._app.top_window().Edit1.wait("ready") break except RuntimeError: pass @@ -45,12 +46,13 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().button0.click() # detect login is success or not - self._app.top_window().wait_not('exists', 10) + self._app.top_window().wait_not("exists", 10) self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=10) + path=self._run_exe_path(exe_path), timeout=10 + ) self._close_prompt_windows() - self._main = self._app.window(title='网上股票交易系统5.0') + self._main = self._app.window(title="网上股票交易系统5.0") @property def balance(self): @@ -63,7 +65,7 @@ def _get_balance_from_statics(self): for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): result[key] = float( self._main.window( - control_id=control_id, - class_name='Static', - ).window_text()) + control_id=control_id, class_name="Static" + ).window_text() + ) return result diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index bce3fa9a..1a4d4ce5 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -11,34 +11,38 @@ class JoinQuantFollower(BaseFollower): - LOGIN_PAGE = 'https://www.joinquant.com' - LOGIN_API = 'https://www.joinquant.com/user/login/doLogin?ajax=1' - TRANSACTION_API = 'https://www.joinquant.com/algorithm/live/transactionDetail' - WEB_REFERER = 'https://www.joinquant.com/user/login/index' - WEB_ORIGIN = 'https://www.joinquant.com' + LOGIN_PAGE = "https://www.joinquant.com" + LOGIN_API = "https://www.joinquant.com/user/login/doLogin?ajax=1" + TRANSACTION_API = ( + "https://www.joinquant.com/algorithm/live/transactionDetail" + ) + WEB_REFERER = "https://www.joinquant.com/user/login/index" + WEB_ORIGIN = "https://www.joinquant.com" def create_login_params(self, user, password, **kwargs): params = { - 'CyLoginForm[username]': user, - 'CyLoginForm[pwd]': password, - 'ajax': 1 + "CyLoginForm[username]": user, + "CyLoginForm[pwd]": password, + "ajax": 1, } return params def check_login_success(self, rep): - set_cookie = rep.headers['set-cookie'] + set_cookie = rep.headers["set-cookie"] if len(set_cookie) < 100: - raise NotLoginError('登录失败,请检查用户名和密码') - self.s.headers.update({'cookie': set_cookie}) - - def follow(self, - users, - strategies, - track_interval=1, - trade_cmd_expire_seconds=120, - cmd_cache=True, - entrust_prop='limit', - send_interval=0): + raise NotLoginError("登录失败,请检查用户名和密码") + self.s.headers.update({"cookie": set_cookie}) + + 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 的模拟交易地址,支持使用 [] 指定多个模拟交易, @@ -55,8 +59,9 @@ def follow(self, if cmd_cache: self.load_expired_cmd_cache() - self.start_trader_thread(users, trade_cmd_expire_seconds, entrust_prop, - send_interval) + self.start_trader_thread( + users, trade_cmd_expire_seconds, entrust_prop, send_interval + ) workers = [] for strategy_url in strategies: @@ -64,56 +69,58 @@ def follow(self, strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: - log.error('抽取交易id和策略名失败, 无效的模拟交易url: {}'.format(strategy_url)) + log.error("抽取交易id和策略名失败, 无效的模拟交易url: {}".format(strategy_url)) raise strategy_worker = Thread( target=self.track_strategy_worker, args=[strategy_id, strategy_name], - kwargs={'interval': track_interval}) + kwargs={"interval": track_interval}, + ) strategy_worker.start() workers.append(strategy_worker) - log.info('开始跟踪策略: {}'.format(strategy_name)) + log.info("开始跟踪策略: {}".format(strategy_name)) for worker in workers: worker.join() @staticmethod def extract_strategy_id(strategy_url): - return re.search(r'(?<=backtestId=)\w+', strategy_url).group() + return re.search(r"(?<=backtestId=)\w+", strategy_url).group() def extract_strategy_name(self, strategy_url): rep = self.s.get(strategy_url) - return self.re_find(r'(?<=title="点击修改策略名称"\>).*(?=\).*(?=\= 300: - raise Exception(response.json()['error']) + raise Exception(response.json()["error"]) return response.json() @property def balance(self): - return self.common_get('balance') + return self.common_get("balance") @property def position(self): - return self.common_get('position') + return self.common_get("position") @property def today_entrusts(self): - return self.common_get('today_entrusts') + return self.common_get("today_entrusts") @property def today_trades(self): - return self.common_get('today_trades') + return self.common_get("today_trades") @property def cancel_entrusts(self): - return self.common_get('cancel_entrusts') + return self.common_get("cancel_entrusts") def auto_ipo(self): - return self.common_get('auto_ipo') + return self.common_get("auto_ipo") def exit(self): - return self.common_get('exit') + return self.common_get("exit") def common_get(self, endpoint): - response = self._s.get(self._api + '/' + endpoint) + response = self._s.get(self._api + "/" + endpoint) if response.status_code >= 300: - raise Exception(response.json()['error']) + raise Exception(response.json()["error"]) return response.json() def buy(self, security, price, amount, **kwargs): params = locals().copy() - params.pop('self') + params.pop("self") - response = self._s.post(self._api + '/buy', json=params) + response = self._s.post(self._api + "/buy", json=params) if response.status_code >= 300: - raise Exception(response.json()['error']) + raise Exception(response.json()["error"]) return response.json() def sell(self, security, price, amount, **kwargs): params = locals().copy() - params.pop('self') + params.pop("self") - response = self._s.post(self._api + '/sell', json=params) + response = self._s.post(self._api + "/sell", json=params) if response.status_code >= 300: - raise Exception(response.json()['error']) + raise Exception(response.json()["error"]) return response.json() def cancel_entrust(self, entrust_no): params = locals().copy() - params.pop('self') + params.pop("self") - response = self._s.post(self._api + '/cancel_entrust', json=params) + response = self._s.post(self._api + "/cancel_entrust", json=params) if response.status_code >= 300: - raise Exception(response.json()['error']) + raise Exception(response.json()["error"]) return response.json() diff --git a/easytrader/ricequant_follower.py b/easytrader/ricequant_follower.py index 73764903..4fff6bb1 100644 --- a/easytrader/ricequant_follower.py +++ b/easytrader/ricequant_follower.py @@ -11,10 +11,19 @@ class RiceQuantFollower(BaseFollower): def login(self, user, password, **kwargs): from rqopen_client import RQOpenClient + self.client = RQOpenClient(user, password, logger=log) - def follow(self, users, run_id, track_interval=1, - trade_cmd_expire_seconds=120, cmd_cache=True, entrust_prop='limit', send_interval=0): + 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,支持使用 [] 指定多个模拟交易 @@ -30,30 +39,43 @@ def follow(self, users, run_id, track_interval=1, if cmd_cache: self.load_expired_cmd_cache() - self.start_trader_thread(users, trade_cmd_expire_seconds, entrust_prop, send_interval) + self.start_trader_thread( + users, trade_cmd_expire_seconds, entrust_prop, send_interval + ) workers = [] for run_id in run_id_list: strategy_name = self.extract_strategy_name(run_id) - strategy_worker = Thread(target=self.track_strategy_worker, args=[run_id, strategy_name], - kwargs={'interval': track_interval}) + strategy_worker = Thread( + target=self.track_strategy_worker, + args=[run_id, strategy_name], + kwargs={"interval": track_interval}, + ) strategy_worker.start() workers.append(strategy_worker) - log.info('开始跟踪策略: {}'.format(strategy_name)) + log.info("开始跟踪策略: {}".format(strategy_name)) for worker in workers: worker.join() def extract_strategy_name(self, run_id): ret_json = self.client.get_positions(run_id) if ret_json["code"] != 200: - log.error("fetch data from run_id {} fail, msg {}".format(run_id, ret_json["msg"])) + log.error( + "fetch data from run_id {} fail, msg {}".format( + run_id, ret_json["msg"] + ) + ) raise RuntimeError(ret_json["msg"]) return ret_json["resp"]["name"] def extract_day_trades(self, run_id): ret_json = self.client.get_day_trades(run_id) if ret_json["code"] != 200: - log.error("fetch day trades from run_id {} fail, msg {}".format(run_id, ret_json["msg"])) + log.error( + "fetch day trades from run_id {} fail, msg {}".format( + run_id, ret_json["msg"] + ) + ) raise RuntimeError(ret_json["msg"]) return ret_json["resp"]["trades"] @@ -64,13 +86,15 @@ def query_strategy_transaction(self, strategy, **kwargs): @staticmethod def stock_shuffle_to_prefix(stock): - assert len(stock) == 11, 'stock {} must like 123456.XSHG or 123456.XSHE'.format(stock) + assert ( + len(stock) == 11 + ), "stock {} must like 123456.XSHG or 123456.XSHE".format(stock) code = stock[:6] - if stock.find('XSHG') != -1: - return 'sh' + code - elif stock.find('XSHE') != -1: - return 'sz' + code - raise TypeError('not valid stock code: {}'.format(code)) + if stock.find("XSHG") != -1: + return "sh" + code + elif stock.find("XSHE") != -1: + return "sz" + code + raise TypeError("not valid stock code: {}".format(code)) def project_transactions(self, transactions, **kwargs): new_transactions = [] @@ -78,9 +102,13 @@ def project_transactions(self, transactions, **kwargs): trans = {} trans["price"] = t["price"] trans["amount"] = int(abs(t["quantity"])) - trans["datetime"] = datetime.strptime(t["time"], '%Y-%m-%d %H:%M:%S') - trans["stock_code"] = self.stock_shuffle_to_prefix(t["order_book_id"]) - trans["action"] = 'buy' if t["quantity"] > 0 else 'sell' + trans["datetime"] = datetime.strptime( + t["time"], "%Y-%m-%d %H:%M:%S" + ) + trans["stock_code"] = self.stock_shuffle_to_prefix( + t["order_book_id"] + ) + trans["action"] = "buy" if t["quantity"] > 0 else "sell" new_transactions.append(trans) return new_transactions diff --git a/easytrader/server.py b/easytrader/server.py index d5579a5e..1b935388 100644 --- a/easytrader/server.py +++ b/easytrader/server.py @@ -16,119 +16,119 @@ def wrapper(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: - log.exception('server error') - message = '{}: {}'.format(e.__class__, e) - return jsonify({'error': message}), 400 + log.exception("server error") + message = "{}: {}".format(e.__class__, e) + return jsonify({"error": message}), 400 return wrapper -@app.route('/prepare', methods=['POST']) +@app.route("/prepare", methods=["POST"]) @error_handle def post_prepare(): json_data = request.get_json(force=True) - user = api.use(json_data.pop('broker')) + user = api.use(json_data.pop("broker")) user.prepare(**json_data) - global_store['user'] = user - return jsonify({'msg': 'login success'}), 201 + global_store["user"] = user + return jsonify({"msg": "login success"}), 201 -@app.route('/balance', methods=['GET']) +@app.route("/balance", methods=["GET"]) @error_handle def get_balance(): - user = global_store['user'] + user = global_store["user"] balance = user.balance return jsonify(balance), 200 -@app.route('/position', methods=['GET']) +@app.route("/position", methods=["GET"]) @error_handle def get_position(): - user = global_store['user'] + user = global_store["user"] position = user.position return jsonify(position), 200 -@app.route('/auto_ipo', methods=['GET']) +@app.route("/auto_ipo", methods=["GET"]) @error_handle def get_auto_ipo(): - user = global_store['user'] + user = global_store["user"] res = user.auto_ipo() return jsonify(res), 200 -@app.route('/today_entrusts', methods=['GET']) +@app.route("/today_entrusts", methods=["GET"]) @error_handle def get_today_entrusts(): - user = global_store['user'] + user = global_store["user"] today_entrusts = user.today_entrusts return jsonify(today_entrusts), 200 -@app.route('/today_trades', methods=['GET']) +@app.route("/today_trades", methods=["GET"]) @error_handle def get_today_trades(): - user = global_store['user'] + user = global_store["user"] today_trades = user.today_trades return jsonify(today_trades), 200 -@app.route('/cancel_entrusts', methods=['GET']) +@app.route("/cancel_entrusts", methods=["GET"]) @error_handle def get_cancel_entrusts(): - user = global_store['user'] + user = global_store["user"] cancel_entrusts = user.cancel_entrusts return jsonify(cancel_entrusts), 200 -@app.route('/buy', methods=['POST']) +@app.route("/buy", methods=["POST"]) @error_handle def post_buy(): json_data = request.get_json(force=True) - user = global_store['user'] + user = global_store["user"] res = user.buy(**json_data) return jsonify(res), 201 -@app.route('/sell', methods=['POST']) +@app.route("/sell", methods=["POST"]) @error_handle def post_sell(): json_data = request.get_json(force=True) - user = global_store['user'] + user = global_store["user"] res = user.sell(**json_data) return jsonify(res), 201 -@app.route('/cancel_entrust', methods=['POST']) +@app.route("/cancel_entrust", methods=["POST"]) @error_handle def post_cancel_entrust(): json_data = request.get_json(force=True) - user = global_store['user'] + user = global_store["user"] res = user.cancel_entrust(**json_data) return jsonify(res), 201 -@app.route('/exit', methods=['GET']) +@app.route("/exit", methods=["GET"]) @error_handle def get_exit(): - user = global_store['user'] + user = global_store["user"] user.exit() - return jsonify({'msg': 'exit success'}), 200 + return jsonify({"msg": "exit success"}), 200 def run(port=1430): - app.run(host='0.0.0.0', port=port) + app.run(host="0.0.0.0", port=port) diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index f9d4a0b3..554e220d 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -14,13 +14,13 @@ # noinspection PyIncorrectDocstring class WebTrader(object): - global_config_path = os.path.dirname(__file__) + '/config/global.json' - config_path = '' + global_config_path = os.path.dirname(__file__) + "/config/global.json" + config_path = "" def __init__(self, debug=True): self.__read_config() - self.trade_prefix = self.config['prefix'] - self.account_config = '' + self.trade_prefix = self.config["prefix"] + self.account_config = "" self.heart_active = True self.heart_thread = Thread(target=self.send_heartbeat) self.heart_thread.setDaemon(True) @@ -31,10 +31,10 @@ def read_config(self, path): try: self.account_config = helpers.file2dict(path) except ValueError: - log.error('配置文件格式有误,请勿使用记事本编辑,推荐 sublime text') + log.error("配置文件格式有误,请勿使用记事本编辑,推荐 sublime text") for v in self.account_config: if type(v) is int: - log.warn('配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题') + log.warn("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题") def prepare(self, config_file=None, user=None, password=None, **kwargs): """登录的统一接口 @@ -54,7 +54,7 @@ def prepare(self, config_file=None, user=None, password=None, **kwargs): def _prepare_account(self, user, password, **kwargs): """映射用户名密码到对应的字段""" - raise Exception('支持参数登录需要实现此方法') + raise Exception("支持参数登录需要实现此方法") def autologin(self, limit=10): """实现自动登录 @@ -65,7 +65,8 @@ def autologin(self, limit=10): break else: raise exceptions.NotLoginError( - '登录失败次数过多, 请检查密码是否正确 / 券商服务器是否处于维护中 / 网络连接是否正常') + "登录失败次数过多, 请检查密码是否正确 / 券商服务器是否处于维护中 / 网络连接是否正常" + ) self.keepalive() def login(self): @@ -95,7 +96,7 @@ def check_login(self, sleepy=30): pass except Exception as e: log.setLevel(self.log_level) - log.error('心跳线程发现账户出现错误: {} {}, 尝试重新登陆'.format(e.__class__, e)) + log.error("心跳线程发现账户出现错误: {} {}, 尝试重新登陆".format(e.__class__, e)) self.autologin() finally: log.setLevel(self.log_level) @@ -123,7 +124,7 @@ def balance(self): def get_balance(self): """获取账户资金状况""" - return self.do(self.config['balance']) + return self.do(self.config["balance"]) @property def position(self): @@ -131,7 +132,7 @@ def position(self): def get_position(self): """获取持仓""" - return self.do(self.config['position']) + return self.do(self.config["position"]) @property def entrust(self): @@ -139,7 +140,7 @@ def entrust(self): def get_entrust(self): """获取当日委托列表""" - return self.do(self.config['entrust']) + return self.do(self.config["entrust"]) @property def current_deal(self): @@ -148,7 +149,7 @@ def current_deal(self): def get_current_deal(self): """获取当日委托列表""" # return self.do(self.config['current_deal']) - log.warning('目前仅在 佣金宝/银河子类 中实现, 其余券商需要补充') + log.warning("目前仅在 佣金宝/银河子类 中实现, 其余券商需要补充") @property def exchangebill(self): @@ -167,7 +168,7 @@ def get_exchangebill(self, start_date, end_date): :param end_date: 20160211 :return: """ - log.warning('目前仅在 华泰子类 中实现, 其余券商需要补充') + log.warning("目前仅在 华泰子类 中实现, 其余券商需要补充") def get_ipo_limit(self, stock_code): """ @@ -175,7 +176,7 @@ def get_ipo_limit(self, stock_code): :param stock_code: 申购代码 ID :return: """ - log.warning('目前仅在 佣金宝子类 中实现, 其余券商需要补充') + log.warning("目前仅在 佣金宝子类 中实现, 其余券商需要补充") def do(self, params): """发起对 api 的请求并过滤返回结果 @@ -221,15 +222,15 @@ def format_response_data_type(self, response_data): if type(response_data) is not list: return response_data - int_match_str = '|'.join(self.config['response_format']['int']) - float_match_str = '|'.join(self.config['response_format']['float']) + int_match_str = "|".join(self.config["response_format"]["int"]) + float_match_str = "|".join(self.config["response_format"]["float"]) for item in response_data: for key in item: try: if re.search(int_match_str, key) is not None: - item[key] = helpers.str2num(item[key], 'int') + item[key] = helpers.str2num(item[key], "int") elif re.search(float_match_str, key) is not None: - item[key] = helpers.str2num(item[key], 'float') + item[key] = helpers.str2num(item[key], "float") except ValueError: continue return response_data diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index bca1ea73..e732e836 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -14,43 +14,34 @@ class XueQiuTrader(webtrader.WebTrader): - config_path = os.path.dirname(__file__) + '/config/xq.json' + config_path = os.path.dirname(__file__) + "/config/xq.json" _HEADERS = { - 'User-Agent': - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) ' - 'AppleWebKit/537.36 (KHTML, like Gecko) ' - 'Chrome/64.0.3282.167 Safari/537.36', - 'Host': - 'xueqiu.com', - 'Pragma': - 'no-cache', - 'Connection': - 'keep-alive', - 'Accept': - '*/*', - 'Accept-Encoding': - 'gzip, deflate, br', - 'Accept-Language': - 'zh-CN,zh;q=0.9,en;q=0.8', - 'Cache-Control': - 'no-cache', - 'Referer': - 'https://xueqiu.com/P/ZH004612', - 'X-Requested-With': - 'XMLHttpRequest', + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/64.0.3282.167 Safari/537.36", + "Host": "xueqiu.com", + "Pragma": "no-cache", + "Connection": "keep-alive", + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Cache-Control": "no-cache", + "Referer": "https://xueqiu.com/P/ZH004612", + "X-Requested-With": "XMLHttpRequest", } def __init__(self, **kwargs): super(XueQiuTrader, self).__init__() # 资金换算倍数 - self.multiple = kwargs[ - 'initial_assets'] if 'initial_assets' in kwargs else 1000000 + self.multiple = ( + kwargs["initial_assets"] if "initial_assets" in kwargs else 1000000 + ) if not isinstance(self.multiple, numbers.Number): - raise TypeError('initial assets must be number(int, float)') + raise TypeError("initial assets must be number(int, float)") if self.multiple < 1e3: - raise ValueError('雪球初始资产不能小于1000元,当前预设值 {}'.format(self.multiple)) + raise ValueError("雪球初始资产不能小于1000元,当前预设值 {}".format(self.multiple)) self.s = requests.Session() self.s.headers.update(self._HEADERS) @@ -61,7 +52,7 @@ def autologin(self, **kwargs): 使用 cookies 之后不需要自动登陆 :return: """ - self._set_cookies(self.account_config['cookies']) + self._set_cookies(self.account_config["cookies"]) def _set_cookies(self, cookies): """设置雪球 cookies,代码来自于 @@ -72,7 +63,7 @@ def _set_cookies(self, cookies): cookie_dict = helpers.parse_cookies_str(cookies) self.s.cookies.update(cookie_dict) - def _prepare_account(self, user='', password='', **kwargs): + def _prepare_account(self, user="", password="", **kwargs): """ 转换参数到登录所需的字典格式 :param cookies: 雪球登陆需要设置 cookies, 具体见 @@ -81,17 +72,19 @@ def _prepare_account(self, user='', password='', **kwargs): :param portfolio_market: 交易市场, 可选['cn', 'us', 'hk'] 默认 'cn' :return: """ - if 'portfolio_code' not in kwargs: - raise TypeError('雪球登录需要设置 portfolio_code(组合代码) 参数') - if 'portfolio_market' not in kwargs: - kwargs['portfolio_market'] = 'cn' - if 'cookies' not in kwargs: - raise TypeError('雪球登陆需要设置 cookies, 具体见' - 'https://smalltool.github.io/2016/08/02/cookie/') + if "portfolio_code" not in kwargs: + raise TypeError("雪球登录需要设置 portfolio_code(组合代码) 参数") + if "portfolio_market" not in kwargs: + kwargs["portfolio_market"] = "cn" + if "cookies" not in kwargs: + raise TypeError( + "雪球登陆需要设置 cookies, 具体见" + "https://smalltool.github.io/2016/08/02/cookie/" + ) self.account_config = { - 'cookies': kwargs['cookies'], - 'portfolio_code': kwargs['portfolio_code'], - 'portfolio_market': kwargs['portfolio_market'] + "cookies": kwargs["cookies"], + "portfolio_code": kwargs["portfolio_code"], + "portfolio_market": kwargs["portfolio_market"], } def _virtual_to_balance(self, virtual): @@ -117,14 +110,14 @@ def _search_stock_info(self, code): ** flag : 未上市(0)、正常(1)、停牌(2)、涨跌停(3)、退市(4) """ data = { - 'code': str(code), - 'size': '300', - 'key': '47bce5c74f', - 'market': self.account_config['portfolio_market'], + "code": str(code), + "size": "300", + "key": "47bce5c74f", + "market": self.account_config["portfolio_market"], } - r = self.s.get(self.config['search_stock_url'], params=data) + r = self.s.get(self.config["search_stock_url"], params=data) stocks = json.loads(r.text) - stocks = stocks['stocks'] + stocks = stocks["stocks"] stock = None if len(stocks) > 0: stock = stocks[0] @@ -135,16 +128,17 @@ def _get_portfolio_info(self, portfolio_code): 获取组合信息 :return: 字典 """ - url = self.config['portfolio_url'] + portfolio_code + url = self.config["portfolio_url"] + portfolio_code html = self._get_html(url) - match_info = re.search(r'(?<=SNB.cubeInfo = ).*(?=;\n)', html) + match_info = re.search(r"(?<=SNB.cubeInfo = ).*(?=;\n)", html) if match_info is None: raise Exception( - 'cant get portfolio info, portfolio html : {}'.format(html)) + "cant get portfolio info, portfolio html : {}".format(html) + ) try: portfolio_info = json.loads(match_info.group()) except Exception as e: - raise Exception('get portfolio info error: {}'.format(e)) + raise Exception("get portfolio info error: {}".format(e)) return portfolio_info def get_balance(self): @@ -152,31 +146,34 @@ def get_balance(self): 获取账户资金状况 :return: """ - portfolio_code = self.account_config.get('portfolio_code', 'ch') + portfolio_code = self.account_config.get("portfolio_code", "ch") portfolio_info = self._get_portfolio_info(portfolio_code) asset_balance = self._virtual_to_balance( - float(portfolio_info['net_value'])) # 总资产 - position = portfolio_info['view_rebalancing'] # 仓位结构 - cash = asset_balance * float(position['cash']) / 100 + float(portfolio_info["net_value"]) + ) # 总资产 + position = portfolio_info["view_rebalancing"] # 仓位结构 + cash = asset_balance * float(position["cash"]) / 100 market = asset_balance - cash - return [{ - 'asset_balance': asset_balance, - 'current_balance': cash, - 'enable_balance': cash, - 'market_value': market, - 'money_type': u'人民币', - 'pre_interest': 0.25 - }] + return [ + { + "asset_balance": asset_balance, + "current_balance": cash, + "enable_balance": cash, + "market_value": market, + "money_type": u"人民币", + "pre_interest": 0.25, + } + ] def _get_position(self): """ 获取雪球持仓 :return: """ - portfolio_code = self.account_config['portfolio_code'] + portfolio_code = self.account_config["portfolio_code"] portfolio_info = self._get_portfolio_info(portfolio_code) - position = portfolio_info['view_rebalancing'] # 仓位结构 - stocks = position['holdings'] # 持仓股票 + position = portfolio_info["view_rebalancing"] # 仓位结构 + stocks = position["holdings"] # 持仓股票 return stocks @staticmethod @@ -196,19 +193,21 @@ def get_position(self): balance = self.get_balance()[0] position_list = [] for pos in xq_positions: - volume = pos['weight'] * balance['asset_balance'] / 100 - position_list.append({ - 'cost_price': volume / 100, - 'current_amount': 100, - 'enable_amount': 100, - 'income_balance': 0, - 'keep_cost_price': volume / 100, - 'last_price': volume / 100, - 'market_value': volume, - 'position_str': 'random', - 'stock_code': pos['stock_symbol'], - 'stock_name': pos['stock_name'] - }) + volume = pos["weight"] * balance["asset_balance"] / 100 + position_list.append( + { + "cost_price": volume / 100, + "current_amount": 100, + "enable_amount": 100, + "income_balance": 0, + "keep_cost_price": volume / 100, + "last_price": volume / 100, + "market_value": volume, + "position_str": "random", + "stock_code": pos["stock_symbol"], + "stock_name": pos["stock_name"], + } + ) return position_list def _get_xq_history(self): @@ -219,13 +218,13 @@ def _get_xq_history(self): :return: """ data = { - "cube_symbol": str(self.account_config['portfolio_code']), - 'count': 20, - 'page': 1 + "cube_symbol": str(self.account_config["portfolio_code"]), + "count": 20, + "page": 1, } - resp = self.s.get(self.config['history_url'], params=data) + resp = self.s.get(self.config["history_url"], params=data) res = json.loads(resp.text) - return res['list'] + return res["list"] @property def history(self): @@ -241,40 +240,42 @@ def get_entrust(self): entrust_list = [] replace_none = lambda s: s or 0 for xq_entrusts in xq_entrust_list: - status = xq_entrusts['status'] # 调仓状态 - if status == 'pending': + status = xq_entrusts["status"] # 调仓状态 + if status == "pending": status = "已报" - elif status in ['canceled', 'failed']: + elif status in ["canceled", "failed"]: status = "废单" else: status = "已成" - for entrust in xq_entrusts['rebalancing_histories']: - volume = abs(entrust['target_weight'] - replace_none( - entrust['prev_weight'])) * self.multiple / 10000 - price = entrust['price'] - entrust_list.append({ - 'entrust_no': - entrust['id'], - 'entrust_bs': - u"买入" if entrust['target_weight'] > replace_none( - entrust['prev_weight']) else u"卖出", - 'report_time': - self._time_strftime(entrust['updated_at']), - 'entrust_status': - status, - 'stock_code': - entrust['stock_symbol'], - 'stock_name': - entrust['stock_name'], - 'business_amount': - 100, - 'business_price': - price, - 'entrust_amount': - 100, - 'entrust_price': - price, - }) + for entrust in xq_entrusts["rebalancing_histories"]: + volume = ( + abs( + entrust["target_weight"] + - replace_none(entrust["prev_weight"]) + ) + * self.multiple + / 10000 + ) + price = entrust["price"] + entrust_list.append( + { + "entrust_no": entrust["id"], + "entrust_bs": u"买入" + if entrust["target_weight"] + > replace_none(entrust["prev_weight"]) + else u"卖出", + "report_time": self._time_strftime( + entrust["updated_at"] + ), + "entrust_status": status, + "stock_code": entrust["stock_symbol"], + "stock_name": entrust["stock_name"], + "business_amount": 100, + "business_price": price, + "entrust_amount": 100, + "entrust_price": price, + } + ) return entrust_list def cancel_entrust(self, entrust_no): @@ -286,23 +287,35 @@ def cancel_entrust(self, entrust_no): xq_entrust_list = self._get_xq_history() is_have = False for xq_entrusts in xq_entrust_list: - status = xq_entrusts['status'] # 调仓状态 - for entrust in xq_entrusts['rebalancing_histories']: - if entrust['id'] == entrust_no and status == 'pending': + status = xq_entrusts["status"] # 调仓状态 + for entrust in xq_entrusts["rebalancing_histories"]: + if entrust["id"] == entrust_no and status == "pending": is_have = True - bs = 'buy' if entrust['target_weight'] < entrust['weight'] else 'sell' - if entrust['target_weight'] == 0 and entrust['weight'] == 0: + bs = ( + "buy" + if entrust["target_weight"] < entrust["weight"] + else "sell" + ) + if ( + entrust["target_weight"] == 0 + and entrust["weight"] == 0 + ): raise exceptions.TradeError(u"移除的股票操作无法撤销,建议重新买入") balance = self.get_balance()[0] - volume = abs(entrust['target_weight'] - entrust['weight'] - ) * balance['asset_balance'] / 100 + volume = ( + abs(entrust["target_weight"] - entrust["weight"]) + * balance["asset_balance"] + / 100 + ) r = self._trade( - security=entrust['stock_symbol'], + security=entrust["stock_symbol"], volume=volume, - entrust_bs=bs) - if len(r) > 0 and 'error_info' in r[0]: - raise exceptions.TradeError(u"撤销失败!%s" % - ('error_info' in r[0])) + entrust_bs=bs, + ) + if len(r) > 0 and "error_info" in r[0]: + raise exceptions.TradeError( + u"撤销失败!%s" % ("error_info" in r[0]) + ) if not is_have: raise exceptions.TradeError(u"撤销对象已失效") return True @@ -317,7 +330,7 @@ def adjust_weight(self, stock_code, weight): stock = self._search_stock_info(stock_code) if stock is None: raise exceptions.TradeError(u"没有查询要操作的股票信息") - if stock['flag'] != 1: + if stock["flag"] != 1: raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") # 仓位比例向下取两位数 @@ -327,64 +340,68 @@ def adjust_weight(self, stock_code, weight): # 调整后的持仓 for position in position_list: - if position['stock_id'] == stock['stock_id']: - position['proactive'] = True - position['weight'] = weight + 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 + 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) + 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) log.debug("调仓比例:%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': "" + "cube_symbol": str(self.account_config["portfolio_code"]), + "segment": "true", + "comment": "", } try: - resp = self.s.post(self.config['rebalance_url'], data=data) + resp = self.s.post(self.config["rebalance_url"], data=data) except Exception as e: - log.warn('调仓失败: %s ' % e) + log.warn("调仓失败: %s " % e) return else: - log.debug('调仓 %s: 持仓比例%d' % (stock['name'], weight)) + log.debug("调仓 %s: 持仓比例%d" % (stock["name"], weight)) resp_json = json.loads(resp.text) - if 'error_description' in resp_json and resp.status_code != 200: - log.error('调仓错误: %s' % (resp_json['error_description'])) - return [{ - 'error_no': resp_json['error_code'], - 'error_info': resp_json['error_description'] - }] + if "error_description" in resp_json and resp.status_code != 200: + log.error("调仓错误: %s" % (resp_json["error_description"])) + return [ + { + "error_no": resp_json["error_code"], + "error_info": resp_json["error_description"], + } + ] else: - log.debug('调仓成功 %s: 持仓比例%d' % (stock['name'], weight)) + log.debug("调仓成功 %s: 持仓比例%d" % (stock["name"], weight)) - def _trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): + def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): """ 调仓 :param security: @@ -400,15 +417,15 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): raise exceptions.TradeError(u"没有查询要操作的股票信息") if not volume: volume = int(float(price) * amount) # 可能要取整数 - if balance['current_balance'] < volume and entrust_bs == 'buy': + if balance["current_balance"] < volume and entrust_bs == "buy": raise exceptions.TradeError(u"没有足够的现金进行操作") - if stock['flag'] != 1: + if stock["flag"] != 1: raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。") if volume == 0: raise exceptions.TradeError(u"操作金额不能为零") # 计算调仓调仓份额 - weight = volume / balance['asset_balance'] * 100 + weight = volume / balance["asset_balance"] * 100 weight = round(weight, 2) # 获取原有仓位信息 @@ -417,103 +434,108 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs='buy'): # 调整后的持仓 is_have = False for position in position_list: - if position['stock_id'] == stock['stock_id']: + 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 + 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) + 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']) - }) + 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 + if entrust_bs == "buy": + cash = ( + (balance["current_balance"] - volume) + / balance["asset_balance"] + * 100 + ) else: - cash = (balance['current_balance'] + volume - ) / balance['asset_balance'] * 100 + cash = ( + (balance["current_balance"] + volume) + / balance["asset_balance"] + * 100 + ) cash = round(cash, 2) log.debug("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': "" + "cube_symbol": str(self.account_config["portfolio_code"]), + "segment": 1, + "comment": "", } try: - resp = self.s.post(self.config['rebalance_url'], data=data) + resp = self.s.post(self.config["rebalance_url"], data=data) except Exception as e: - log.warn('调仓失败: %s ' % e) + log.warn("调仓失败: %s " % e) return else: - log.debug('调仓 %s%s: %d' % (entrust_bs, stock['name'], - resp.status_code)) + log.debug( + "调仓 %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: - log.error('调仓错误: %s' % (resp_json['error_description'])) - return [{ - 'error_no': resp_json['error_code'], - 'error_info': resp_json['error_description'] - }] + if "error_description" in resp_json and resp.status_code != 200: + log.error("调仓错误: %s" % (resp_json["error_description"])) + return [ + { + "error_no": resp_json["error_code"], + "error_info": resp_json["error_description"], + } + ] else: - 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': - '-' - }] + 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 buy(self, security, price=0, amount=0, volume=0, entrust_prop=0): """买入卖出股票 @@ -523,7 +545,7 @@ def buy(self, security, price=0, amount=0, volume=0, entrust_prop=0): :param volume: 买入总金额 由 volume / price 取整, 若指定 price 则此参数无效 :param entrust_prop: """ - return self._trade(security, price, amount, volume, 'buy') + return self._trade(security, price, amount, volume, "buy") def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0): """卖出股票 @@ -533,4 +555,4 @@ def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0): :param volume: 卖出总金额 由 volume / price 取整, 若指定 price 则此参数无效 :param entrust_prop: """ - return self._trade(security, price, amount, volume, 'sell') + return self._trade(security, price, amount, volume, "sell") diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index d1cbff83..559223e5 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -11,7 +11,7 @@ class YHClientTrader(clienttrader.ClientTrader): @property def broker_type(self): - return 'yh' + return "yh" def login(self, user, password, exe_path, comm_password=None, **kwargs): """ @@ -25,14 +25,15 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): """ try: self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=1) + path=self._run_exe_path(exe_path), timeout=1 + ) except Exception: self._app = pywinauto.Application().start(exe_path) # wait login window ready while True: try: - self._app.top_window().Edit1.wait('ready') + self._app.top_window().Edit1.wait("ready") break except RuntimeError: pass @@ -42,31 +43,35 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): while True: self._app.top_window().Edit3.type_keys( - self._handle_verify_code()) + self._handle_verify_code() + ) - self._app.top_window()['登录'].click() + self._app.top_window()["登录"].click() # detect login is success or not try: - self._app.top_window().wait_not('exists visible', 10) + self._app.top_window().wait_not("exists visible", 10) break except: pass self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=10) + path=self._run_exe_path(exe_path), timeout=10 + ) self._close_prompt_windows() - self._main = self._app.window(title='网上股票交易系统5.0') + self._main = self._app.window(title="网上股票交易系统5.0") try: - self._main.window( - control_id=129, class_name='SysTreeView32').wait('ready', 2) + self._main.window(control_id=129, class_name="SysTreeView32").wait( + "ready", 2 + ) except: self._wait(2) self._switch_window_to_normal_mode() def _switch_window_to_normal_mode(self): self._app.top_window().window( - control_id=32812, class_name='Button').click() + control_id=32812, class_name="Button" + ).click() def _handle_verify_code(self): control = self._app.top_window().window(control_id=22202) @@ -74,9 +79,9 @@ def _handle_verify_code(self): control.draw_outline() file_path = tempfile.mktemp() - control.capture_as_image().save(file_path, 'jpeg') - vcode = helpers.recognize_verify_code(file_path, 'yh_client') - return ''.join(re.findall('\d+', vcode)) + control.capture_as_image().save(file_path, "jpeg") + vcode = helpers.recognize_verify_code(file_path, "yh_client") + return "".join(re.findall("\d+", vcode)) @property def balance(self): diff --git a/setup.py b/setup.py index c1fd5119..417961ad 100644 --- a/setup.py +++ b/setup.py @@ -76,33 +76,39 @@ """ setup( - name='easytrader', - version='0.14.2', - description='A utility for China Stock Trade', + name="easytrader", + version="0.14.2", + description="A utility for China Stock Trade", long_description=long_desc, - author='shidenggui', - author_email='longlyshidenggui@gmail.com', - license='BSD', - url='https://github.com/shidenggui/easytrader', - keywords='China stock trade', + author="shidenggui", + author_email="longlyshidenggui@gmail.com", + license="BSD", + url="https://github.com/shidenggui/easytrader", + keywords="China stock trade", install_requires=[ - 'requests', 'six', 'rqopen-client', 'easyutils', 'flask', 'pywinauto', - 'pillow', 'pandas' + "requests", + "six", + "rqopen-client", + "easyutils", + "flask", + "pywinauto", + "pillow", + "pandas", ], classifiers=[ - 'Development Status :: 4 - Beta', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'License :: OSI Approved :: BSD License' + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "License :: OSI Approved :: BSD License", ], - packages=['easytrader', 'easytrader.config'], + packages=["easytrader", "easytrader.config"], package_data={ - '': ['*.jar', '*.json'], - 'config': ['config/*.json'], - 'thirdlibrary': ['thirdlibrary/*.jar'] + "": ["*.jar", "*.json"], + "config": ["config/*.json"], + "thirdlibrary": ["thirdlibrary/*.jar"], }, ) diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index 252b1798..a9270420 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -5,27 +5,29 @@ import unittest from unittest import mock -sys.path.append('.') +sys.path.append(".") -TEST_CLIENTS = os.environ.get('EZ_TEST_CLIENTS', 'yh') +TEST_CLIENTS = os.environ.get("EZ_TEST_CLIENTS", "yh") -IS_WIN_PLATFORM = sys.platform != 'darwin' +IS_WIN_PLATFORM = sys.platform != "darwin" -@unittest.skipUnless('yh' in TEST_CLIENTS and IS_WIN_PLATFORM, 'skip yh test') +@unittest.skipUnless("yh" in TEST_CLIENTS and IS_WIN_PLATFORM, "skip yh test") class TestYhClientTrader(unittest.TestCase): @classmethod def setUpClass(cls): import easytrader - if 'yh' not in TEST_CLIENTS: + + if "yh" not in TEST_CLIENTS: return # input your test account and password - cls._ACCOUNT = os.environ.get('EZ_TEST_YH_ACCOUNT') or 'your account' - cls._PASSWORD = os.environ.get( - 'EZ_TEST_YH_password') or 'your password' + cls._ACCOUNT = os.environ.get("EZ_TEST_YH_ACCOUNT") or "your account" + cls._PASSWORD = ( + os.environ.get("EZ_TEST_YH_password") or "your password" + ) - cls._user = easytrader.use('yh_client') + cls._user = easytrader.use("yh_client") cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD) def test_balance(self): @@ -42,42 +44,48 @@ def test_cancel_entrusts(self): result = self._user.cancel_entrusts def test_cancel_entrust(self): - result = self._user.cancel_entrust('123456789') + result = self._user.cancel_entrust("123456789") def test_invalid_buy(self): import easytrader + with self.assertRaises(easytrader.exceptions.TradeError): - result = self._user.buy('511990', 1, 1e10) + result = self._user.buy("511990", 1, 1e10) def test_invalid_sell(self): import easytrader + with self.assertRaises(easytrader.exceptions.TradeError): - result = self._user.sell('162411', 200, 1e10) + result = self._user.sell("162411", 200, 1e10) def test_auto_ipo(self): self._user.auto_ipo() -@unittest.skipUnless('ht' in TEST_CLIENTS and IS_WIN_PLATFORM, 'skip ht test') +@unittest.skipUnless("ht" in TEST_CLIENTS and IS_WIN_PLATFORM, "skip ht test") class TestHTClientTrader(unittest.TestCase): @classmethod def setUpClass(cls): import easytrader - if 'ht' not in TEST_CLIENTS: + + if "ht" not in TEST_CLIENTS: return # input your test account and password - cls._ACCOUNT = os.environ.get('EZ_TEST_HT_ACCOUNT') or 'your account' - cls._PASSWORD = os.environ.get( - 'EZ_TEST_HT_password') or 'your password' - cls._COMM_PASSWORD = os.environ.get( - 'EZ_TEST_HT_comm_password') or 'your comm password' - - cls._user = easytrader.use('ht_client') + cls._ACCOUNT = os.environ.get("EZ_TEST_HT_ACCOUNT") or "your account" + cls._PASSWORD = ( + os.environ.get("EZ_TEST_HT_password") or "your password" + ) + cls._COMM_PASSWORD = ( + os.environ.get("EZ_TEST_HT_comm_password") or "your comm password" + ) + + cls._user = easytrader.use("ht_client") cls._user.prepare( user=cls._ACCOUNT, password=cls._PASSWORD, - comm_password=cls._COMM_PASSWORD) + comm_password=cls._COMM_PASSWORD, + ) def test_balance(self): time.sleep(3) @@ -93,21 +101,23 @@ def test_cancel_entrusts(self): result = self._user.cancel_entrusts def test_cancel_entrust(self): - result = self._user.cancel_entrust('123456789') + result = self._user.cancel_entrust("123456789") def test_invalid_buy(self): import easytrader + with self.assertRaises(easytrader.exceptions.TradeError): - result = self._user.buy('511990', 1, 1e10) + result = self._user.buy("511990", 1, 1e10) def test_invalid_sell(self): import easytrader + with self.assertRaises(easytrader.exceptions.TradeError): - result = self._user.sell('162411', 200, 1e10) + result = self._user.sell("162411", 200, 1e10) def test_auto_ipo(self): self._user.auto_ipo() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py index 49238f40..529fe032 100644 --- a/tests/test_xq_follower.py +++ b/tests/test_xq_follower.py @@ -13,7 +13,7 @@ def test_adjust_sell_amount_without_enable(self): follower._users = [mock_user] follower._adjust_sell = False - amount = follower._adjust_sell_amount('169101', 1000) + amount = follower._adjust_sell_amount("169101", 1000) self.assertEqual(amount, amount) def test_adjust_sell_amount(self): @@ -25,29 +25,31 @@ def test_adjust_sell_amount(self): follower._adjust_sell = True test_cases = [ - ('169101', 600, 600), - ('169101', 700, 600), - ('000000', 100, 100), - ('sh169101', 700, 600), + ("169101", 600, 600), + ("169101", 700, 600), + ("000000", 100, 100), + ("sh169101", 700, 600), ] for stock_code, sell_amount, excepted_amount in test_cases: amount = follower._adjust_sell_amount(stock_code, sell_amount) self.assertEqual(amount, excepted_amount) -TEST_POSITION = [{ - 'Unnamed: 14': '', - '买入冻结': 0, - '交易市场': '深A', - '卖出冻结': 0, - '参考市价': 1.464, - '参考市值': 919.39, - '参考成本价': 1.534, - '参考盈亏': -43.77, - '可用余额': 628, - '当前持仓': 628, - '盈亏比例(%)': -4.544, - '股东代码': '0000000000', - '股份余额': 628, - '证券代码': '169101' -}] +TEST_POSITION = [ + { + "Unnamed: 14": "", + "买入冻结": 0, + "交易市场": "深A", + "卖出冻结": 0, + "参考市价": 1.464, + "参考市值": 919.39, + "参考成本价": 1.534, + "参考盈亏": -43.77, + "可用余额": 628, + "当前持仓": 628, + "盈亏比例(%)": -4.544, + "股东代码": "0000000000", + "股份余额": 628, + "证券代码": "169101", + } +] diff --git a/tests/test_xqtrader.py b/tests/test_xqtrader.py index d5632f8c..ac322bd8 100644 --- a/tests/test_xqtrader.py +++ b/tests/test_xqtrader.py @@ -8,10 +8,11 @@ class TestXueQiuTrader(unittest.TestCase): def test_prepare_account(self): user = XueQiuTrader() params_without_cookies = dict( - portfolio_code='ZH123456', portfolio_market='cn') + portfolio_code="ZH123456", portfolio_market="cn" + ) with self.assertRaises(TypeError): user._prepare_account(**params_without_cookies) - params_without_cookies.update(cookies='123') + params_without_cookies.update(cookies="123") user._prepare_account(**params_without_cookies) self.assertEqual(params_without_cookies, user.account_config) From e273e3fe07a2932d6ac830e621b30b7ccff69968 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Jul 2018 14:43:07 +0800 Subject: [PATCH 122/276] :star: support get grid data by xls strategy --- docs/usage.md | 10 ++ easytrader/clienttrader.py | 260 +++++++++++++-------------- easytrader/config/client.py | 2 +- easytrader/config/gf.json | 198 -------------------- easytrader/gj_clienttrader.py | 5 +- easytrader/grid_data_get_strategy.py | 99 ++++++++++ easytrader/ht_clienttrader.py | 4 +- easytrader/pop_dialog_handler.py | 65 +++++++ easytrader/yh_clienttrader.py | 24 ++- tests/test_easytrader.py | 7 +- 10 files changed, 329 insertions(+), 345 deletions(-) delete mode 100644 easytrader/config/gf.json create mode 100644 easytrader/grid_data_get_strategy.py create mode 100644 easytrader/pop_dialog_handler.py diff --git a/docs/usage.md b/docs/usage.md index 0d878126..73dfc795 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -97,6 +97,16 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' ``` +## 某些同花顺客户端不允许拷贝 `Grid` 数据导致无法获取持仓等问题的解决办法 + +现在默认获取 `Grid` 数据的策略是通过剪切板拷贝,有些券商不允许这种方式,所以额外实现了一种通过将 `Grid` 数据存为文件再读取的策略, +使用方式如下: + +```python +from easytrader import grid_data_get_strategy + +user.grid_data_get_strategy = grid_data_get_strategy.XlsStrategy +``` ### 交易相关 diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index b0423154..d5429a2b 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -1,126 +1,99 @@ # coding:utf-8 +import abc import functools -import io import os -import re import sys import time -from abc import abstractmethod import easyutils -import pandas as pd -from . import exceptions +from . import grid_data_get_strategy from . import helpers from .config import client -from .log import log +from . import pop_dialog_handler if not sys.platform.startswith("darwin"): import pywinauto import pywinauto.clipboard -class PopDialogHandler: - def __init__(self, app): - self._app = app +class IClientTrader(abc.ABC): + @property + @abc.abstractmethod + def app(self): + """Return current app instance""" + pass - def handle(self, title): - if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): - self._submit_by_shortcut() + @property + @abc.abstractmethod + def main(self): + """Return current main window instance""" + pass - elif "提示" in title: - content = self._extract_content() - self._submit_by_click() - return {"message": content} + @property + @abc.abstractmethod + def config(self): + """Return current config instance""" + pass - else: - content = self._extract_content() - self._close() - return {"message": "unknown message: {}".format(content)} - - def _extract_content(self): - return self._app.top_window().Static.window_text() - - def _extract_entrust_id(self, content): - return re.search(r"\d+", content).group() - - def _submit_by_click(self): - self._app.top_window()["确定"].click() - - def _submit_by_shortcut(self): - self._app.top_window().type_keys("%Y") - - def _close(self): - self._app.top_window().close() - - -class TradePopDialogHandler(PopDialogHandler): - def handle(self, title): - if title == "委托确认": - self._submit_by_shortcut() - - elif title == "提示信息": - content = self._extract_content() - if "超出涨跌停" in content: - self._submit_by_shortcut() - elif "委托价格的小数价格应为" in content: - self._submit_by_shortcut() - - elif title == "提示": - content = self._extract_content() - if "成功" in content: - entrust_no = self._extract_entrust_id(content) - self._submit_by_click() - return {"entrust_no": entrust_no} - else: - self._submit_by_click() - time.sleep(0.05) - raise exceptions.TradeError(content) - else: - self._close() + @abc.abstractmethod + def wait(self, seconds): + """Wait for operation return""" + pass + + @property + @abc.abstractmethod + def grid_data_get_strategy(self): + """ + :return: Implement class of IGridDataGetStrategy + :rtype: grid_data.get_strategy.IGridDataGetStrategy + """ + pass + @grid_data_get_strategy.setter + @abc.abstractmethod + def grid_data_get_strategy(self, strategy_cls): + """ + :param strategy_cls: Grid data get strategy + :type strategy_cls: grid_data.get_strategy.IGridDataGetStrategy + :return: formatted grid data + :rtype: list[dict] + """ + pass -class ClientTrader: + +class ClientTrader(IClientTrader): def __init__(self): self._config = client.create(self.broker_type) self._app = None self._main = None + self.grid_data_get_strategy = grid_data_get_strategy.CopyStrategy - def prepare( - self, - config_path=None, - user=None, - password=None, - exe_path=None, - comm_password=None, - **kwargs - ): - """ - 登陆客户端 - :param config_path: 登陆配置文件,跟参数登陆方式二选一 - :param user: 账号 - :param password: 明文密码 - :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe' - :param comm_password: 通讯密码 - :return: - """ - if config_path is not None: - account = helpers.file2dict(config_path) - user = account["user"] - password = account["password"] - comm_password = account.get("comm_password") - exe_path = account.get("exe_path") - self.login( - user, - password, - exe_path or self._config.DEFAULT_EXE_PATH, - comm_password, - **kwargs - ) + @property + def app(self): + return self._app - @abstractmethod - def login(self, user, password, exe_path, comm_password=None, **kwargs): - pass + @property + def main(self): + return self._main + + @property + def config(self): + return self._config + + @property + def grid_data_get_strategy(self): + return self._grid_data_get_strategy + + @grid_data_get_strategy.setter + def grid_data_get_strategy(self, strategy_cls): + if not issubclass( + strategy_cls, grid_data_get_strategy.IGridDataGetStrategy + ): + raise TypeError( + "Strategy must be implement class of IGridDataGetStrategy" + ) + self._grid_data_get_strategy = strategy_cls(self) def connect(self, exe_path=None, **kwargs): """ @@ -253,7 +226,9 @@ def market_trade(self, security, amount, ttype=None, **kwargs): self._set_market_trade_type(ttype) self._submit_trade() - return self._handle_pop_dialogs(handler_class=TradePopDialogHandler) + return self._handle_pop_dialogs( + handler_class=pop_dialog_handler.TradePopDialogHandler + ) def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" @@ -286,14 +261,14 @@ def auto_ipo(self): return {"message": "没有发现可以申购的新股"} self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID) - self._wait(0.1) + self.wait(0.1) for row in invalid_list_idx: self._click_grid_by_row(row) - self._wait(0.1) + self.wait(0.1) self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) - self._wait(0.1) + self.wait(0.1) return self._handle_pop_dialogs() @@ -309,7 +284,7 @@ def _click_grid_by_row(self, row): ).click(coords=(x, y)) def _is_exist_pop_dialog(self): - self._wait(0.2) # wait dialog display + self.wait(0.2) # wait dialog display return ( self._main.wrapper_object() != self._app.top_window().wrapper_object() @@ -318,25 +293,27 @@ def _is_exist_pop_dialog(self): def _run_exe_path(self, exe_path): return os.path.join(os.path.dirname(exe_path), "xiadan.exe") - def _wait(self, seconds): + def wait(self, seconds): time.sleep(seconds) def exit(self): self._app.kill() def _close_prompt_windows(self): - self._wait(1) + self.wait(1) for w in self._app.windows(class_name="#32770"): if w.window_text() != self._config.TITLE: w.close() - self._wait(1) + self.wait(1) def trade(self, security, price, amount): self._set_trade_params(security, price, amount) self._submit_trade() - return self._handle_pop_dialogs(handler_class=TradePopDialogHandler) + return self._handle_pop_dialogs( + handler_class=pop_dialog_handler.TradePopDialogHandler + ) def _click(self, control_id): self._app.top_window().window( @@ -363,7 +340,7 @@ def _set_trade_params(self, security, price, amount): self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish - self._wait(0.1) + self.wait(0.1) self._type_keys( self._config.TRADE_PRICE_CONTROL_ID, @@ -377,36 +354,25 @@ def _set_market_trade_params(self, security, amount): self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish - self._wait(0.1) + self.wait(0.1) self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _get_grid_data(self, control_id): - grid = self._main.window( - control_id=control_id, class_name="CVirtualGridCtrl" - ) - grid.type_keys("^A^C") - return self._format_grid_data(self._get_clipboard_data()) + return self._grid_data_get_strategy.get(control_id) def _type_keys(self, control_id, text): self._main.window( control_id=control_id, class_name="Edit" ).set_edit_text(text) - def _get_clipboard_data(self): - while True: - try: - return pywinauto.clipboard.GetData() - except Exception as e: - log.warning("{}, retry ......".format(e)) - def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() - self._wait(sleep) + self.wait(sleep) def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): self._app.top_window().type_keys(shortcut) - self._wait(sleep) + self.wait(sleep) @functools.lru_cache() def _get_left_menus_handle(self): @@ -421,15 +387,6 @@ def _get_left_menus_handle(self): except: pass - def _format_grid_data(self, data): - df = pd.read_csv( - io.StringIO(data), - delimiter="\t", - dtype=self._config.GRID_DTYPE, - na_filter=False, - ) - return df.to_dict("records") - def _cancel_entrust_by_double_click(self, row): x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN y = ( @@ -444,7 +401,9 @@ def _cancel_entrust_by_double_click(self, row): def _refresh(self): self._switch_left_menus(["买入[F1]"], sleep=0.05) - def _handle_pop_dialogs(self, handler_class=PopDialogHandler): + def _handle_pop_dialogs( + self, handler_class=pop_dialog_handler.PopDialogHandler + ): handler = handler_class(self._app) while self._is_exist_pop_dialog(): @@ -454,3 +413,42 @@ def _handle_pop_dialogs(self, handler_class=PopDialogHandler): if result: return result return {"message": "success"} + + +class BaseLoginClientTrader(ClientTrader): + @abc.abstractmethod + def login(self, user, password, exe_path, comm_password=None, **kwargs): + """Login Client Trader""" + pass + + def prepare( + self, + config_path=None, + user=None, + password=None, + exe_path=None, + comm_password=None, + **kwargs + ): + """ + 登陆客户端 + :param config_path: 登陆配置文件,跟参数登陆方式二选一 + :param user: 账号 + :param password: 明文密码 + :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe' + :param comm_password: 通讯密码 + :return: + """ + if config_path is not None: + account = helpers.file2dict(config_path) + user = account["user"] + password = account["password"] + comm_password = account.get("comm_password") + exe_path = account.get("exe_path") + self.login( + user, + password, + exe_path or self._config.DEFAULT_EXE_PATH, + comm_password, + **kwargs + ) diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 6b13a544..0c723ffc 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -69,7 +69,7 @@ class CommonConfig: class YH(CommonConfig): - DEFAULT_EXE_PATH = r"C:\中国银河证券双子星3.2\Binarystar.exe" + DEFAULT_EXE_PATH = r"C:\双子星-中国银河证券\Binarystar.exe" BALANCE_GRID_CONTROL_ID = 1308 diff --git a/easytrader/config/gf.json b/easytrader/config/gf.json deleted file mode 100644 index 1dc2ba39..00000000 --- a/easytrader/config/gf.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "login_api": "https://trade.gf.com.cn/login", - "login_page": "https://trade.gf.com.cn/", - "verify_code_api": "https://trade.gf.com.cn/yzm.jpgx", - "prefix": "https://trade.gf.com.cn/entry", - "logout_api": "https://trade.gf.com.cn/entry", - "login": { - "authtype": 2, - "disknum": "1SVEYNFA915146", - "loginType": 2, - "origin": "web" - }, - "balance":{ - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryAssert" - }, - "position":{ - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryCC", - "request_num": 500, - "start": 0, - "limit": 10 - }, - "entrust":{ - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryDRWT", - "action_in": 0, - "request_num": 100, - "query_direction": 0, - "start": 0, - "limit": 10 - }, - "entrust_pos":{ - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryDRWT", - "request_num": 100, - "query_direction": 0, - "query_mode": 0 - }, - "cancel_entrust": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "cancel", - "exchange_type": 1, - "batch_flag": 0 - }, - "accountinfo": { - "classname": "com.gf.etrade.control.FrameWorkControl", - "method": "getMainJS" - }, - "buy": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "entrust", - "entrust_bs": 1 - }, - "sell": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "entrust", - "entrust_bs": 2 - }, - "cnjj_apply": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "CNJJSS", - "entrust_bs": 1 - }, - "cnjj_redeem": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "CNJJSS", - "entrust_bs": 2 - }, - "fundsubscribe": { - "classname": "com.gf.etrade.control.SHLOFFundControl", - "method": "assetSecuprtTrade", - "entrust_bs": 1 - }, - "fundpurchase": { - "classname": "com.gf.etrade.control.SHLOFFundControl", - "method": "assetSecuprtTrade", - "entrust_bs": 1 - }, - "fundredemption": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "doDZJYEntrust", - "entrust_bs": 2 - }, - "fundmerge": { - "classname": "com.gf.etrade.control.SHLOFFundControl", - "method": "assetSecuprtTrade", - "entrust_bs": "" - }, - "fundsplit": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "doDZJYEntrust", - "entrust_bs": "" - }, - "nxbQueryPrice": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "nxbQueryPrice" - }, - "nxbentrust": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "nxbentrust" - }, - "nxbQueryDeliver": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "nxbQueryDeliver" - }, - "nxbQueryHisDeliver": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "nxbQueryHisDeliver" - }, - "nxbQueryEntrust": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "nxbQueryEntrust" - }, - "queryOfStkCodes": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "queryOfStkCodes" - }, - "queryNXBOfStock": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "queryNXBOfStock" - }, - "nxbentrustcancel": { - "classname": "com.gf.etrade.control.NXBUF2Control", - "method": "nxbentrustcancel" - }, - "exchangebill": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryDeliver", - "request_num": 50, - "query_direction": 0, - "start_date": "", - "end_date": "", - "deliver_type": 1 - }, - "queryStockInfo": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "getStockHQ" - }, - "today_ipo_list": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryNewStkcode", - "request_num": 50, - "query_direction": 1 - }, - "entrust_his": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryLSWT", - "request_num": 50, - "query_direction": 0, - "start_date": "", - "end_date": "", - "deliver_type": 1, - "query_mode": 0 - }, - "today_ipo_limit": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "querySecuSubequity", - "limit": 50 - }, - "rzrq": { - "classname": "com.gf.etrade.control.RZRQUF2Control", - "method": "ValidataLogin" - }, - "rzrq_exchangebill": { - "classname": "com.gf.etrade.control.RZRQUF2Control", - "method": "queryDeliver", - "request_num": 50, - "query_direction": 1, - "start_date": "", - "end_date": "", - "deliver_type": 1 - }, - "rzrq_entrust_his": { - "classname": "com.gf.etrade.control.RZRQUF2Control", - "method": "queryLSWT", - "request_num": 50, - "query_direction": 1, - "start_date": "", - "end_date": "", - "deliver_type": 1, - "query_mode": 0 - }, - "capitalflow": { - "classname": "com.gf.etrade.control.StockUF2Control", - "method": "queryFundjour", - "limit": 50, - "query_direction": 0, - "start_date": "", - "end_date": "", - "deliver_type": 1, - "query_mode": 0 - }, - "exit": { - "classname": "com.gf.etrade.control.AuthenticateControl", - "method": "logout" - } -} diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index 2d1311a4..ada48f60 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -1,5 +1,4 @@ # coding:utf8 -from __future__ import division import re import tempfile @@ -8,11 +7,11 @@ import pywinauto import pywinauto.clipboard +from . import clienttrader from . import helpers -from .clienttrader import ClientTrader -class GJClientTrader(ClientTrader): +class GJClientTrader(clienttrader.BaseLoginClientTrader): @property def broker_type(self): return "gj" diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py new file mode 100644 index 00000000..39614959 --- /dev/null +++ b/easytrader/grid_data_get_strategy.py @@ -0,0 +1,99 @@ +# coding:utf-8 +import abc +import io +import tempfile + +import pandas as pd +import pywinauto.clipboard + +from .log import log + + +class IGridDataGetStrategy(abc.ABC): + @abc.abstractmethod + def get(self, control_id: int): + """ + :param control_id: grid 的 control id + :return: grid 数据 + :rtype: List[Dict] + """ + pass + + +class BaseStrategy(IGridDataGetStrategy): + def __init__(self, trader): + self._trader = trader + + @abc.abstractmethod + def get(self, control_id: int): + """ + :param control_id: grid 的 control id + :return: grid 数据 + :rtype: list[dict] + """ + pass + + def _get_grid(self, control_id): + grid = self._trader.main.window( + control_id=control_id, class_name="CVirtualGridCtrl" + ) + return grid + + +class CopyStrategy(BaseStrategy): + """ + 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 + """ + + def get(self, control_id: int): + grid = self._get_grid(control_id) + grid.type_keys("^A^C") + content = self._get_clipboard_data() + return self._format_grid_data(content) + + def _format_grid_data(self, data): + df = pd.read_csv( + io.StringIO(data), + delimiter="\t", + dtype=self._trader.config.GRID_DTYPE, + na_filter=False, + ) + return df.to_dict("records") + + def _get_clipboard_data(self): + while True: + try: + return pywinauto.clipboard.GetData() + except Exception as e: + log.warning("{}, retry ......".format(e)) + + +class XlsStrategy(BaseStrategy): + """ + 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, + 用于绕过一些客户端不允许复制的限制 + """ + + def get(self, control_id: int): + grid = self._get_grid(control_id) + + # ctrl+s 保存 grid 内容为 xls 文件 + grid.type_keys("^s") + self._trader.wait(1) + + temp_path = tempfile.mktemp(suffix=".csv", prefix="easytrader_") + self._trader.app.top_window().type_keys(temp_path) + + # alt+s保存,alt+y替换已存在的文件 + self._trader.app.top_window().type_keys("%{s}%{y}") + return self._format_grid_data(temp_path) + + def _format_grid_data(self, data): + df = pd.read_csv( + data, + encoding="gbk", + delimiter="\t", + dtype=self._trader.config.GRID_DTYPE, + na_filter=False, + ) + return df.to_dict("records") diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 7ea93cc2..c43a088c 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -3,10 +3,10 @@ import pywinauto import pywinauto.clipboard -from .clienttrader import ClientTrader +from . import clienttrader -class HTClientTrader(ClientTrader): +class HTClientTrader(clienttrader.BaseLoginClientTrader): @property def broker_type(self): return "ht" diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py new file mode 100644 index 00000000..b274c2dc --- /dev/null +++ b/easytrader/pop_dialog_handler.py @@ -0,0 +1,65 @@ +# coding:utf-8 +import re +import time + +from easytrader import exceptions + + +class PopDialogHandler: + def __init__(self, app): + self._app = app + + def handle(self, title): + if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): + self._submit_by_shortcut() + + elif "提示" in title: + content = self._extract_content() + self._submit_by_click() + return {"message": content} + + else: + content = self._extract_content() + self._close() + return {"message": "unknown message: {}".format(content)} + + def _extract_content(self): + return self._app.top_window().Static.window_text() + + def _extract_entrust_id(self, content): + return re.search(r"\d+", content).group() + + def _submit_by_click(self): + self._app.top_window()["确定"].click() + + def _submit_by_shortcut(self): + self._app.top_window().type_keys("%Y") + + def _close(self): + self._app.top_window().close() + + +class TradePopDialogHandler(PopDialogHandler): + def handle(self, title): + if title == "委托确认": + self._submit_by_shortcut() + + elif title == "提示信息": + content = self._extract_content() + if "超出涨跌停" in content: + self._submit_by_shortcut() + elif "委托价格的小数价格应为" in content: + self._submit_by_shortcut() + + elif title == "提示": + content = self._extract_content() + if "成功" in content: + entrust_no = self._extract_entrust_id(content) + self._submit_by_click() + return {"entrust_no": entrust_no} + else: + self._submit_by_click() + time.sleep(0.05) + raise exceptions.TradeError(content) + else: + self._close() diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 559223e5..749b05e1 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -1,14 +1,26 @@ # coding:utf8 -import pywinauto -import pywinauto.clipboard import re import tempfile +import pywinauto + from . import clienttrader +from . import grid_data_get_strategy from . import helpers -class YHClientTrader(clienttrader.ClientTrader): +class YHClientTrader(clienttrader.BaseLoginClientTrader): + def __init__(self): + """ + Changelog: + + 2018.07.01: + 银河客户端 2018.5.11 更新后不再支持通过剪切板复制获取 Grid 内容, + 改为使用保存为 Xls 再读取的方式获取 + """ + super().__init__() + self.grid_data_get_strategy = grid_data_get_strategy.XlsStrategy + @property def broker_type(self): return "yh" @@ -65,7 +77,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): "ready", 2 ) except: - self._wait(2) + self.wait(2) self._switch_window_to_normal_mode() def _switch_window_to_normal_mode(self): @@ -80,8 +92,8 @@ def _handle_verify_code(self): file_path = tempfile.mktemp() control.capture_as_image().save(file_path, "jpeg") - vcode = helpers.recognize_verify_code(file_path, "yh_client") - return "".join(re.findall("\d+", vcode)) + verify_code = helpers.recognize_verify_code(file_path, "yh_client") + return "".join(re.findall("\d+", verify_code)) @property def balance(self): diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index a9270420..37ac3923 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -3,7 +3,6 @@ import sys import time import unittest -from unittest import mock sys.path.append(".") @@ -24,7 +23,7 @@ def setUpClass(cls): # input your test account and password cls._ACCOUNT = os.environ.get("EZ_TEST_YH_ACCOUNT") or "your account" cls._PASSWORD = ( - os.environ.get("EZ_TEST_YH_password") or "your password" + os.environ.get("EZ_TEST_YH_PASSWORD") or "your password" ) cls._user = easytrader.use("yh_client") @@ -74,10 +73,10 @@ def setUpClass(cls): # input your test account and password cls._ACCOUNT = os.environ.get("EZ_TEST_HT_ACCOUNT") or "your account" cls._PASSWORD = ( - os.environ.get("EZ_TEST_HT_password") or "your password" + os.environ.get("EZ_TEST_HT_PASSWORD") or "your password" ) cls._COMM_PASSWORD = ( - os.environ.get("EZ_TEST_HT_comm_password") or "your comm password" + os.environ.get("EZ_TEST_HT_COMM_PASSWORD") or "your comm password" ) cls._user = easytrader.use("ht_client") From 3090fbcb514ee17833844dfa2c8983520a4504c4 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Jul 2018 14:43:56 +0800 Subject: [PATCH 123/276] =?UTF-8?q?Bump=20version:=200.14.2=20=E2=86=92=20?= =?UTF-8?q?0.15.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4adce60d..1a30046b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.2 +current_version = 0.15.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index c44aa982..4b9ab673 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = "0.14.2" +__version__ = "0.15.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 417961ad..95d37cfc 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.14.2", + version="0.15.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 062cb90cafdf75c735cfc97548222235005cd4b6 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Jul 2018 15:14:32 +0800 Subject: [PATCH 124/276] :hammer: refactor code style --- easytrader/__init__.py | 2 +- easytrader/api.py | 3 ++- easytrader/clienttrader.py | 4 ++-- easytrader/config/client.py | 4 +--- easytrader/exceptions.py | 4 +--- easytrader/follower.py | 12 +++++------- easytrader/gj_clienttrader.py | 3 +-- easytrader/grid_data_get_strategy.py | 2 +- easytrader/helpers.py | 5 +---- easytrader/ht_clienttrader.py | 3 +-- easytrader/joinquant_follower.py | 8 +++----- easytrader/log.py | 2 +- easytrader/pop_dialog_handler.py | 2 +- easytrader/remoteclient.py | 2 +- easytrader/ricequant_follower.py | 2 +- easytrader/webtrader.py | 2 +- easytrader/xq_follower.py | 2 +- easytrader/yh_clienttrader.py | 2 +- 18 files changed, 26 insertions(+), 38 deletions(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 4b9ab673..82c08b72 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,4 +1,4 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- from .api import * from .webtrader import WebTrader from .joinquant_follower import JoinQuantFollower diff --git a/easytrader/api.py b/easytrader/api.py index 09a32cc0..f1fc2b83 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -1,5 +1,6 @@ -# coding=utf-8 +# -*- coding: utf-8 -*- import logging + import six from .joinquant_follower import JoinQuantFollower diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index d5429a2b..2f54e7be 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -1,4 +1,4 @@ -# coding:utf-8 +# -*- coding: utf-8 -*- import abc import functools import os @@ -9,8 +9,8 @@ from . import grid_data_get_strategy from . import helpers -from .config import client from . import pop_dialog_handler +from .config import client if not sys.platform.startswith("darwin"): import pywinauto diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 0c723ffc..2d0e2e2e 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -1,6 +1,4 @@ -# coding:utf8 - - +# -*- coding: utf-8 -*- def create(broker): if broker == "yh": return YH diff --git a/easytrader/exceptions.py b/easytrader/exceptions.py index 45d67882..dacd5f15 100644 --- a/easytrader/exceptions.py +++ b/easytrader/exceptions.py @@ -1,6 +1,4 @@ -# coding:utf8 - - +# -*- coding: utf-8 -*- class TradeError(IOError): pass diff --git a/easytrader/follower.py b/easytrader/follower.py index 749cc660..ff494d95 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -1,12 +1,10 @@ -# coding:utf8 -from __future__ import unicode_literals, print_function, division - +# -*- coding: utf-8 -*- +import datetime import os import pickle import re +import threading import time -from datetime import datetime -from threading import Thread import requests @@ -116,7 +114,7 @@ def start_trader_thread( entrust_prop="limit", send_interval=0, ): - trader = Thread( + trader = threading.Thread( target=self.trade_worker, args=[users], kwargs={ @@ -238,7 +236,7 @@ def _execute_trade_cmd( """ for user in users: # check expire - now = datetime.now() + now = datetime.datetime.now() expire = (now - trade_cmd["datetime"]).total_seconds() if expire > expire_seconds: log.warning( diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index ada48f60..2576cf02 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -1,5 +1,4 @@ -# coding:utf8 - +# -*- coding: utf-8 -*- import re import tempfile import time diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py index 39614959..1fd503de 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_data_get_strategy.py @@ -1,4 +1,4 @@ -# coding:utf-8 +# -*- coding: utf-8 -*- import abc import io import tempfile diff --git a/easytrader/helpers.py b/easytrader/helpers.py index f9d84e52..bc80c5c6 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -1,6 +1,4 @@ -# coding: utf-8 -from __future__ import division - +# -*- coding: utf-8 -*- import datetime import json import re @@ -236,7 +234,6 @@ def get_today_ipo_data(): today_ipo = [] for line in json_data["data"]: - # if datetime.datetime(2016, 9, 14).ctime()[:10] == line[3][:10]: if datetime.datetime.now().strftime("%a %b %d") == line[3][:10]: today_ipo.append( { diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index c43a088c..a3991364 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,5 +1,4 @@ -# coding:utf8 - +# -*- coding: utf-8 -*- import pywinauto import pywinauto.clipboard diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index 1a4d4ce5..2221f3d8 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -1,13 +1,11 @@ -# coding:utf8 -from __future__ import unicode_literals - +# -*- coding: utf-8 -*- import re from datetime import datetime from threading import Thread +from . import exceptions from .follower import BaseFollower from .log import log -from easytrader.exceptions import NotLoginError class JoinQuantFollower(BaseFollower): @@ -30,7 +28,7 @@ def create_login_params(self, user, password, **kwargs): def check_login_success(self, rep): set_cookie = rep.headers["set-cookie"] if len(set_cookie) < 100: - raise NotLoginError("登录失败,请检查用户名和密码") + raise exceptions.NotLoginError("登录失败,请检查用户名和密码") self.s.headers.update({"cookie": set_cookie}) def follow( diff --git a/easytrader/log.py b/easytrader/log.py index 6a704c13..08510a9d 100644 --- a/easytrader/log.py +++ b/easytrader/log.py @@ -1,4 +1,4 @@ -# coding:utf8 +# -*- coding: utf-8 -*- import logging log = logging.getLogger("easytrader") diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index b274c2dc..d845879f 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -2,7 +2,7 @@ import re import time -from easytrader import exceptions +from . import exceptions class PopDialogHandler: diff --git a/easytrader/remoteclient.py b/easytrader/remoteclient.py index b9160118..cdf68593 100644 --- a/easytrader/remoteclient.py +++ b/easytrader/remoteclient.py @@ -1,4 +1,4 @@ -# coding:utf8 +# -*- coding: utf-8 -*- import requests from . import helpers diff --git a/easytrader/ricequant_follower.py b/easytrader/ricequant_follower.py index 4fff6bb1..08bba729 100644 --- a/easytrader/ricequant_follower.py +++ b/easytrader/ricequant_follower.py @@ -1,4 +1,4 @@ -# coding:utf8 +# -*- coding: utf-8 -*- from __future__ import unicode_literals from datetime import datetime diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index 554e220d..dcbe4de6 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -1,4 +1,4 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- import logging import os import re diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index 39a8d685..d4dd34e9 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -1,4 +1,4 @@ -# coding:utf8 +# -*- coding: utf-8 -*- from __future__ import unicode_literals, print_function, division import json diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 749b05e1..81ab10ef 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -1,4 +1,4 @@ -# coding:utf8 +# -*- coding: utf-8 -*- import re import tempfile From 75f165cab6713c6649183e6407038e601f42cb7c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 1 Jul 2018 15:28:13 +0800 Subject: [PATCH 125/276] :hammer: disable client test by default, for travis ci --- tests/test_easytrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index 37ac3923..b1edbec7 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -6,7 +6,7 @@ sys.path.append(".") -TEST_CLIENTS = os.environ.get("EZ_TEST_CLIENTS", "yh") +TEST_CLIENTS = os.environ.get("EZ_TEST_CLIENTS", "") IS_WIN_PLATFORM = sys.platform != "darwin" From 2db155e73bd672891dff3c5107cc1997375f5024 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 2 Jul 2018 22:38:18 +0800 Subject: [PATCH 126/276] :bug: fix somtime save file failed because window full path length limit --- easytrader/grid_data_get_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py index 1fd503de..a3aab10f 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_data_get_strategy.py @@ -81,7 +81,7 @@ def get(self, control_id: int): grid.type_keys("^s") self._trader.wait(1) - temp_path = tempfile.mktemp(suffix=".csv", prefix="easytrader_") + temp_path = tempfile.mktemp(suffix=".csv") self._trader.app.top_window().type_keys(temp_path) # alt+s保存,alt+y替换已存在的文件 From 39bcdf343c47235b158970a3bc1ad22f9ecff859 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 2 Jul 2018 22:38:28 +0800 Subject: [PATCH 127/276] =?UTF-8?q?Bump=20version:=200.15.0=20=E2=86=92=20?= =?UTF-8?q?0.15.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1a30046b..d3429f00 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.15.0 +current_version = 0.15.1 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 82c08b72..d7538c09 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = "0.15.0" +__version__ = "0.15.1" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 95d37cfc..620b0ed1 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.15.0", + version="0.15.1", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From ddd29d39fbacc01dc18cc6ced1839f95ad09548f Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 4 Jul 2018 21:04:59 +0800 Subject: [PATCH 128/276] :bug: grid data get copy strategy should wait until file save complete --- easytrader/clienttrader.py | 2 +- easytrader/grid_data_get_strategy.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 2f54e7be..0eeadb69 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -37,7 +37,7 @@ def config(self): pass @abc.abstractmethod - def wait(self, seconds): + def wait(self, seconds: int): """Wait for operation return""" pass diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py index a3aab10f..d6670c2a 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_data_get_strategy.py @@ -51,7 +51,7 @@ def get(self, control_id: int): content = self._get_clipboard_data() return self._format_grid_data(content) - def _format_grid_data(self, data): + def _format_grid_data(self, data: str) -> dict: df = pd.read_csv( io.StringIO(data), delimiter="\t", @@ -60,7 +60,7 @@ def _format_grid_data(self, data): ) return df.to_dict("records") - def _get_clipboard_data(self): + def _get_clipboard_data(self) -> str: while True: try: return pywinauto.clipboard.GetData() @@ -84,11 +84,14 @@ def get(self, control_id: int): temp_path = tempfile.mktemp(suffix=".csv") self._trader.app.top_window().type_keys(temp_path) + # Wait until file save complete + self._trader.wait(0.5) + # alt+s保存,alt+y替换已存在的文件 self._trader.app.top_window().type_keys("%{s}%{y}") return self._format_grid_data(temp_path) - def _format_grid_data(self, data): + def _format_grid_data(self, data: str) -> dict: df = pd.read_csv( data, encoding="gbk", From 76b3d73314e454a1f57560367fd21d104ea2f4cb Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 4 Jul 2018 21:05:04 +0800 Subject: [PATCH 129/276] =?UTF-8?q?Bump=20version:=200.15.1=20=E2=86=92=20?= =?UTF-8?q?0.15.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d3429f00..30c02fb3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.15.1 +current_version = 0.15.2 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index d7538c09..d14a93cb 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -5,5 +5,5 @@ from .ricequant_follower import RiceQuantFollower from . import exceptions -__version__ = "0.15.1" +__version__ = "0.15.2" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 620b0ed1..28496bdb 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.15.1", + version="0.15.2", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 9504619d5cf40d6c103feda843f3a100f3847771 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 8 Aug 2018 09:12:31 +0800 Subject: [PATCH 130/276] :star: add basic hooks --- .coveragerc | 7 + .gitignore | 4 + .pre-commit-config.yaml | 36 ++ .pylintrc | 571 ++++++++++++++++++++ Pipfile | 41 ++ Pipfile.lock | 747 +++++++++++++++++++++++++++ cli.py | 52 -- easytrader/__init__.py | 5 +- easytrader/api.py | 13 +- easytrader/clienttrader.py | 22 +- easytrader/config/client.py | 10 +- easytrader/exceptions.py | 5 + easytrader/follower.py | 149 +++--- easytrader/gj_clienttrader.py | 17 +- easytrader/grid_data_get_strategy.py | 3 +- easytrader/helpers.py | 55 +- easytrader/ht_clienttrader.py | 1 + easytrader/joinquant_follower.py | 27 +- easytrader/pop_dialog_handler.py | 37 +- easytrader/ricequant_follower.py | 53 +- easytrader/server.py | 9 +- easytrader/webtrader.py | 34 +- easytrader/xq_follower.py | 50 +- easytrader/xqtrader.py | 101 ++-- easytrader/yh_clienttrader.py | 17 +- mypy.ini | 2 + requirements.txt | 44 +- tests/test_xq_follower.py | 2 +- tests/test_xqtrader.py | 2 +- 29 files changed, 1733 insertions(+), 383 deletions(-) create mode 100644 .coveragerc create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 Pipfile create mode 100644 Pipfile.lock delete mode 100644 cli.py create mode 100644 mypy.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..873efb65 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +include = src/* +omit = tests/* + +[report] +fail_under = -1 diff --git a/.gitignore b/.gitignore index 0688ba4b..273558e6 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +bak +.mypy_cache +.pyre +.pytest_cache yjb_account.json htt.json gft.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..99e6af23 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +fail_fast: true +repos: +- repo: local + hooks: + - id: python_sort_imports + name: python_sort_imports + entry: pipenv run sort_imports + language: system + types: [python] + - id: python_format + name: python_format + entry: pipenv run format + language: system + types: [python] + - id: python_lint + name: python_lint + entry: pipenv run lint + language: system + types: [python] + - id: python_type_check + name: python_type_check + entry: pipenv run type_check + language: system + types: [python] + - id: python_test + name: python_test + entry: pipenv run test + language: system + types: [python] + verbose: true + - id: django_test + name: django_test + entry: pipenv run test + language: system + types: [python] + verbose: true diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..e5c5b89b --- /dev/null +++ b/.pylintrc @@ -0,0 +1,571 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns=\d{4}.+\.py, + test, + apps.py, + __init__.py, + urls.py, + manage.py + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=0 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=too-many-public-methods, + len-as-condition, + unused-argument, + too-many-arguments, + arguments-differ, + line-too-long, + fixme, + missing-docstring, + invalid-envvar-default, + ungrouped-imports, + bad-continuation, + too-many-ancestors, + too-few-public-methods, + no-self-use, + #print-statement, + #parameter-unpacking, + #unpacking-in-except, + #old-raise-syntax, + #backtick, + #long-suffix, + #old-ne-operator, + #old-octal-literal, + #import-star-module-level, + #non-ascii-bytes-literal, + #raw-checker-failed, + #bad-inline-option, + #locally-disabled, + #locally-enabled, + #file-ignored, + #suppressed-message, + #useless-suppression, + #deprecated-pragma, + #apply-builtin, + #basestring-builtin, + #buffer-builtin, + #cmp-builtin, + #coerce-builtin, + #execfile-builtin, + #file-builtin, + #long-builtin, + #raw_input-builtin, + #reduce-builtin, + #standarderror-builtin, + #unicode-builtin, + #xrange-builtin, + #coerce-method, + #delslice-method, + #getslice-method, + #setslice-method, + #no-absolute-import, + #old-division, + #dict-iter-method, + #dict-view-method, + #next-method-called, + #metaclass-assignment, + #indexing-exception, + #raising-string, + #reload-builtin, + #oct-method, + #hex-method, + #nonzero-method, + #cmp-method, + #input-builtin, + #round-builtin, + #intern-builtin, + #unichr-builtin, + #map-builtin-not-iterating, + #zip-builtin-not-iterating, + #range-builtin-not-iterating, + #filter-builtin-not-iterating, + #using-cmp-argument, + #eq-without-hash, + #div-method, + #idiv-method, + #rdiv-method, + #exception-message-attribute, + #invalid-str-codec, + #sys-max-int, + #bad-python3-import, + #deprecated-string-function, + #deprecated-str-translate-call, + #deprecated-itertools-function, + #deprecated-types-field, + #next-method-defined, + #dict-items-not-iterating, + #dict-keys-not-iterating, + #dict-values-not-iterating + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=optparse.Values,sys.exit + + +[BASIC] + +# Naming style matching correct argument names +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style +#argument-rgx= + +# Naming style matching correct attribute names +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style +#class-attribute-rgx= + +# Naming style matching correct class names +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming-style +#class-rgx= + +# Naming style matching correct constant names +const-naming-style=any + +# Regular expression matching correct constant names. Overrides const-naming- +# style +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=5 + +# Naming style matching correct function names +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma +good-names=i, + do, + f, + df, + s, + j, + k, + ex, + Run, + _, + db, + r, + x, + y, + e + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct inline iteration names +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style +#inlinevar-rgx= + +# Naming style matching correct method names +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style +#method-rgx= + +# Naming style matching correct module names +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=79 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=20 + +# Maximum number of locals for function / method body +max-locals=20 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception + diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..bf458f95 --- /dev/null +++ b/Pipfile @@ -0,0 +1,41 @@ +[[source]] +url = "http://mirrors.aliyun.com/pypi/simple/" +verify_ssl = false +name = "pypi" + +[packages] +pywinauto = "*" +"bs4" = "*" +requests = "*" +dill = "*" +click = "*" +six = "*" +flask = "*" +pillow = "*" +pytesseract = "*" +pandas = "*" +pyperclip = "*" +rqopen-client = ">=0.0.5" +easyutils = "*" + +[dev-packages] +pytest-cov = "*" +pre-commit = "*" +pytest = "*" +pylint = "*" +mypy = "*" +isort = "*" +black = "==18.6b4" +ipython = "*" +better-exceptions = "*" + +[requires] +python_version = "3.6" + +[scripts] +sort_imports = "bash -c 'isort \"$@\"; git add -u' --" +format = "bash -c 'black -l 79 \"$@\"; git add -u' --" +lint = "pylint" +type_check = "mypy" +test = "bash -c 'pytest -vx --cov=easytrader tests'" +lock = "bash -c 'pipenv lock -r > requirements.txt'" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..c793e662 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,747 @@ +{ + "_meta": { + "hash": { + "sha256": "e2a2ba761a3628e4851f250cc8882bca58d22c9ebfa11a6923549503a00d577a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "http://mirrors.aliyun.com/pypi/simple/", + "verify_ssl": false + } + ] + }, + "default": { + "beautifulsoup4": { + "hashes": [ + "sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", + "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", + "sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89" + ], + "version": "==4.6.0" + }, + "bs4": { + "hashes": [ + "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" + ], + "index": "pypi", + "version": "==0.0.1" + }, + "certifi": { + "hashes": [ + "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", + "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + ], + "version": "==2018.4.16" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "index": "pypi", + "version": "==6.7" + }, + "cssselect": { + "hashes": [ + "sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204", + "sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206" + ], + "markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*'", + "version": "==1.0.3" + }, + "dill": { + "hashes": [ + "sha256:624dc244b94371bb2d6e7f40084228a2edfff02373fe20e018bef1ee92fdd5b3" + ], + "index": "pypi", + "version": "==0.2.8.2" + }, + "easyutils": { + "hashes": [ + "sha256:45b46748e20dd3c0e840fa9c1fa7d7f3dc295e58a81796d10329957c20b7f20a" + ], + "index": "pypi", + "version": "==0.1.7" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "itsdangerous": { + "hashes": [ + "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" + ], + "version": "==0.24" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "lxml": { + "hashes": [ + "sha256:0941f4313208c07734410414d8308812b044fd3fb98573454e3d3a0d2e201f3d", + "sha256:0b18890aa5730f9d847bc5469e8820f782d72af9985a15a7552109a86b01c113", + "sha256:21f427945f612ac75576632b1bb8c21233393c961f2da890d7be3927a4b6085f", + "sha256:24cf6f622a4d49851afcf63ac4f0f3419754d4e98a7a548ab48dd03c635d9bd3", + "sha256:2dc6705486b8abee1af9e2a3761e30a3cb19e8276f20ca7e137ee6611b93707c", + "sha256:2e43b2e5b7d2b9abe6e0301eef2c2c122ab45152b968910eae68bdee2c4cfae0", + "sha256:329a6d8b6d36f7d6f8b6c6a1db3b2c40f7e30a19d3caf62023c9d6a677c1b5e1", + "sha256:423cde55430a348bda6f1021faad7235c2a95a6bdb749e34824e5758f755817a", + "sha256:4651ea05939374cfb5fe87aab5271ed38c31ea47997e17ec3834b75b94bd9f15", + "sha256:4be3bbfb2968d7da6e5c2cd4104fc5ec1caf9c0794f6cae724da5a53b4d9f5a3", + "sha256:622f7e40faef13d232fb52003661f2764ce6cdef3edb0a59af7c1559e4cc36d1", + "sha256:664dfd4384d886b239ef0d7ee5cff2b463831079d250528b10e394a322f141f9", + "sha256:697c0f58ac637b11991a1bc92e07c34da4a72e2eda34d317d2c1c47e2f24c1b3", + "sha256:6ec908b4c8a4faa7fe1a0080768e2ce733f268b287dfefb723273fb34141475f", + "sha256:7ec3fe795582b75bb49bb1685ffc462dbe38d74312dac07ce386671a28b5316b", + "sha256:8c39babd923c431dcf1e5874c0f778d3a5c745a62c3a9b6bd755efd489ee8a1d", + "sha256:949ca5bc56d6cb73d956f4862ba06ad3c5d2808eac76304284f53ae0c8b2334a", + "sha256:9f0daddeefb0791a600e6195441910bdf01eac470be596b9467e6122b51239a6", + "sha256:a359893b01c30e949eae0e8a85671a593364c9f0b8162afe0cb97317af0953bf", + "sha256:ad5d5d8efed59e6b1d4c50c1eac59fb6ecec91b2073676af1e15fc4d43e9b6c5", + "sha256:bc1a36f95a6b3667c09b34995fc3a46a82e4cf0dc3e7ab281e4c77b15bd7af05", + "sha256:be37b3f55b6d7d923f43bf74c356fc1878eb36e28505f38e198cb432c19c7b1a", + "sha256:c45bca5e544eb75f7500ffd730df72922eb878a2f0213b0dc5a5f357ded3a85d", + "sha256:ccee7ebbb4735ebc341d347fca9ee09f2fa6c0580528c1414bc4e1d31372835c", + "sha256:dc62c0840b2fc7753550b40405532a3e125c0d3761f34af948873393aa688160", + "sha256:f7d9d5aa1c7e54167f1a3cba36b5c52c7c540f30952c9bd7d9302a1eda318424" + ], + "version": "==4.2.3" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "numpy": { + "hashes": [ + "sha256:14fb76bde161c87dcec52d91c78f65aa8a23aa2e1530a71f412dabe03927d917", + "sha256:21041014b7529237994a6b578701c585703fbb3b1bea356cdb12a5ea7804241c", + "sha256:24f3bb9a5f6c3936a8ccd4ddfc1210d9511f4aeb879a12efd2e80bec647b8695", + "sha256:34033b581bc01b1135ca2e3e93a94daea7c739f21a97a75cca93e29d9f0c8e71", + "sha256:3fbccb399fe9095b1c1d7b41e7c7867db8aa0d2347fc44c87a7a180cedda112b", + "sha256:50718eea8e77a1bedcc85befd22c8dbf5a24c9d2c0c1e36bbb8d7a38da847eb3", + "sha256:55daf757e5f69aa75b4477cf4511bf1f96325c730e4ad32d954ccb593acd2585", + "sha256:61efc65f325770bbe787f34e00607bc124f08e6c25fdf04723848585e81560dc", + "sha256:62cb836506f40ce2529bfba9d09edc4b2687dd18c56cf4457e51c3e7145402fd", + "sha256:64c6acf5175745fd1b7b7e17c74fdbfb7191af3b378bc54f44560279f41238d3", + "sha256:674ea7917f0657ddb6976bd102ac341bc493d072c32a59b98e5b8c6eaa2d5ec0", + "sha256:73a816e441dace289302e04a7a34ec4772ed234ab6885c968e3ca2fc2d06fe2d", + "sha256:78c35dc7ad184aebf3714dbf43f054714c6e430e14b9c06c49a864fb9e262030", + "sha256:7f17efe9605444fcbfd990ba9b03371552d65a3c259fc2d258c24fb95afdd728", + "sha256:816645178f2180be257a576b735d3ae245b1982280b97ae819550ce8bcdf2b6b", + "sha256:924f37e66db78464b4b85ed4b6d2e5cda0c0416e657cac7ccbef14b9fa2c40b5", + "sha256:a17a8fd5df4fec5b56b4d11c9ba8b9ebfb883c90ec361628d07be00aaa4f009a", + "sha256:aaa519335a71f87217ca8a680c3b66b61960e148407bdf5c209c42f50fe30f49", + "sha256:ae3864816287d0e86ead580b69921daec568fe680857f07ee2a87bf7fd77ce24", + "sha256:b5f8c15cb9173f6cdf0f994955e58d1265331029ae26296232379461a297e5f2", + "sha256:c3ac359ace241707e5a48fe2922e566ac666aacacf4f8031f2994ac429c31344", + "sha256:c7c660cc0209fdf29a4e50146ca9ac9d8664acaded6b6ae2f5d0ae2e91a0f0cd", + "sha256:d690a2ff49f6c3bc35336693c9924fe5916be3cc0503fe1ea6c7e2bf951409ee", + "sha256:e2317cf091c2e7f0dacdc2e72c693cc34403ca1f8e3807622d0bb653dc978616", + "sha256:f28e73cf18d37a413f7d5de35d024e6b98f14566a10d82100f9dc491a7d449f9", + "sha256:f2a778dd9bb3e4590dbe3bbac28e7c7134280c4ec97e3bf8678170ee58c67b21", + "sha256:f5a758252502b466b9c2b201ea397dae5a914336c987f3a76c3741a82d43c96e", + "sha256:fb4c33a404d9eff49a0cdc8ead0af6453f62f19e071b60d283f9dc05581e4134" + ], + "markers": "python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7'", + "version": "==1.15.0" + }, + "pandas": { + "hashes": [ + "sha256:05ac350f8a35abe6a02054f8cf54e0c048f13423b2acb87d018845afd736f0b4", + "sha256:174543cd68eaee60620146b38faaed950071f5665e0a4fa4adfdcfc23d7f7936", + "sha256:1a62a237fb7223c11d09daaeaf7d15f234bb836bfaf3d4f85746cdf9b2582f99", + "sha256:2c1ed1de5308918a7c6833df6db75a19c416c122921824e306c64a0626b3606c", + "sha256:33825ad26ce411d6526f903b3d02c0edf627223af59cf4b5876aa925578eec74", + "sha256:4c5f76fce8a4851f65374ea1d95ca24e9439540550e41e556c0879379517a6f5", + "sha256:67504a96f72fb4d7f051cfe77b9a7bb0d094c4e2e5a6efb2769eb80f36e6b309", + "sha256:683e0cc8c7faececbbc06aa4735709a07abad106099f165730c1015da916adec", + "sha256:77cd1b485c6a860b950ab3a85be7b5683eaacbc51cadf096db967886607d2231", + "sha256:814f8785f1ab412a7e9b9a8abb81dfe8727ebdeef850ecfaa262c04b1664000f", + "sha256:894216edaf7dd0a92623cdad423bbec2a23fc06eb9c85483e21876d1ef8f47e9", + "sha256:9331e20a07360b81d8c7b4b50223da387d264151d533a5a5853325800e6631a4", + "sha256:9cd3614b4e31a0889388ff1bd19ae857ad52658b33f776065793c293a29cf612", + "sha256:9d79e958adcd037eba3debbb66222804171197c0f5cd462315d1356aa72a5a30", + "sha256:b90e5d5460f23607310cbd1688a7517c96ce7b284095a48340d249dfc429172e", + "sha256:bc80c13ffddc7e269b706ed58002cc4c98cc135c36d827c99fb5ca54ced0eb7a", + "sha256:cbb074efb2a5e4956b261a670bfc2126b0ccfbf5b96b6ed021bc8c8cb56cf4a8", + "sha256:e8c62ab16feeda84d4732c42b7b67d7a89ad89df7e99efed80ea017bdc472f26", + "sha256:ff5ef271805fe877fe0d1337b6b1861113c44c75b9badb595c713a72cd337371" + ], + "index": "pypi", + "version": "==0.23.3" + }, + "pillow": { + "hashes": [ + "sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a", + "sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72", + "sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574", + "sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd", + "sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2", + "sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c", + "sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273", + "sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554", + "sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612", + "sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934", + "sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786", + "sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27", + "sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b", + "sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62", + "sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9", + "sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710", + "sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3", + "sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4", + "sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b", + "sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f", + "sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e", + "sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b", + "sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a", + "sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f", + "sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea", + "sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7", + "sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4", + "sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333", + "sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109", + "sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab" + ], + "index": "pypi", + "version": "==5.2.0" + }, + "pyperclip": { + "hashes": [ + "sha256:f70e83d27c445795b6bf98c2bc826bbf2d0d63d4c7f83091c8064439042ba0dc" + ], + "index": "pypi", + "version": "==1.6.4" + }, + "pyquery": { + "hashes": [ + "sha256:07987c2ed2aed5cba29ff18af95e56e9eb04a2249f42ce47bddfb37f487229a3", + "sha256:4771db76bd14352eba006463656aef990a0147a0eeaf094725097acfa90442bf" + ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*'", + "version": "==1.4.0" + }, + "pytesseract": { + "hashes": [ + "sha256:9a9fae6331084f588c0cf2ad9ed50fca47e20429407e63389eb42d4e64460013" + ], + "index": "pypi", + "version": "==0.2.4" + }, + "python-dateutil": { + "hashes": [ + "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", + "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" + ], + "version": "==2.7.3" + }, + "python-xlib": { + "hashes": [ + "sha256:2ffa01fa51bdf53842fa4e3f9e2501f8147d4abf546a83e9c2b091982da2e1a8", + "sha256:c3deb8329038620d07b21be05673fa5a495dd8b04a2d9f4dca37a3811d192ae4" + ], + "version": "==0.23" + }, + "pytz": { + "hashes": [ + "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", + "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + ], + "version": "==2018.5" + }, + "pywinauto": { + "hashes": [ + "sha256:75fdfdea3f018c0efc9196cb184ecd14df8b35734889df9624610b8e74812807" + ], + "index": "pypi", + "version": "==0.6.4" + }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "index": "pypi", + "version": "==2.19.1" + }, + "rqopen-client": { + "hashes": [ + "sha256:9bda6a1ceac7453ff66ba0ee61ac56e1dd88bfcbd5eb27c98f49c460bf6d5ff7" + ], + "index": "pypi", + "version": "==0.0.5" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "markers": "python_version != '3.2.*' and python_version < '4' and python_version != '3.3.*' and python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==1.23" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "appnope": { + "hashes": [ + "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", + "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.0" + }, + "aspy.yaml": { + "hashes": [ + "sha256:04d26279513618f1024e1aba46471db870b3b33aef204c2d09bcf93bea9ba13f", + "sha256:0a77e23fafe7b242068ffc0252cee130d3e509040908fc678d9d1060e7494baa" + ], + "version": "==1.1.1" + }, + "astroid": { + "hashes": [ + "sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", + "sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" + ], + "version": "==2.0.1" + }, + "atomicwrites": { + "hashes": [ + "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", + "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" + ], + "version": "==1.1.5" + }, + "attrs": { + "hashes": [ + "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", + "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" + ], + "version": "==18.1.0" + }, + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, + "better-exceptions": { + "hashes": [ + "sha256:0a73efef96b48f867ea980227ac3b00d36a92754e6d316ad2ee472f136014580" + ], + "index": "pypi", + "version": "==0.2.1" + }, + "black": { + "hashes": [ + "sha256:22158b89c1a6b4eb333a1e65e791a3f8b998cf3b11ae094adb2570f31f769a44", + "sha256:4b475bbd528acce094c503a3d2dbc2d05a4075f6d0ef7d9e7514518e14cc5191" + ], + "index": "pypi", + "version": "==18.6b4" + }, + "cached-property": { + "hashes": [ + "sha256:630fdbf0f4ac7d371aa866016eba1c3ac43e9032246748d4994e67cb05f99bc4", + "sha256:f1f9028757dc40b4cb0fd2234bd7b61a302d7b84c683cb8c2c529238a24b8938" + ], + "version": "==1.4.3" + }, + "cfgv": { + "hashes": [ + "sha256:73f48a752bd7aab103c4b882d6596c6360b7aa63b34073dd2c35c7b4b8f93010", + "sha256:d1791caa9ff5c0c7bce80e7ecc1921752a2eb7c2463a08ed9b6c96b85a2f75aa" + ], + "version": "==1.1.0" + }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "index": "pypi", + "version": "==6.7" + }, + "coverage": { + "hashes": [ + "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", + "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", + "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", + "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", + "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", + "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", + "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", + "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", + "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", + "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", + "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", + "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", + "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", + "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", + "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", + "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", + "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", + "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", + "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", + "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", + "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", + "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", + "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", + "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", + "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", + "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", + "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", + "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", + "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", + "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", + "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" + ], + "markers": "python_version < '4' and python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==4.5.1" + }, + "decorator": { + "hashes": [ + "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", + "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + ], + "version": "==4.3.0" + }, + "identify": { + "hashes": [ + "sha256:49845e70fc6b1ec3694ab930a2c558912d7de24548eebcd448f65567dc757c43", + "sha256:68daab16a3db364fa204591f97dc40bfffd1a7739f27788a4895b4d8fd3516e5" + ], + "version": "==1.1.4" + }, + "ipython": { + "hashes": [ + "sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", + "sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c" + ], + "index": "pypi", + "version": "==6.4.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "index": "pypi", + "version": "==4.3.4" + }, + "jedi": { + "hashes": [ + "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", + "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f" + ], + "version": "==0.12.1" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", + "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", + "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" + ], + "version": "==4.2.0" + }, + "mypy": { + "hashes": [ + "sha256:673ea75fb750289b7d1da1331c125dc62fc1c3a8db9129bb372ae7b7d5bf300a", + "sha256:c770605a579fdd4a014e9f0a34b6c7a36ce69b08100ff728e96e27445cef3b3c" + ], + "index": "pypi", + "version": "==0.620" + }, + "nodeenv": { + "hashes": [ + "sha256:aa040ab5189bae17d272175609010be6c5b589ec4b8dbd832cc50c9e9cb7496f" + ], + "version": "==1.3.2" + }, + "parso": { + "hashes": [ + "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", + "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + ], + "version": "==0.3.1" + }, + "pexpect": { + "hashes": [ + "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", + "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.6.0" + }, + "pickleshare": { + "hashes": [ + "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", + "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5" + ], + "version": "==0.7.4" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", + "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", + "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==0.6.0" + }, + "pre-commit": { + "hashes": [ + "sha256:99cb6313a8ea7d88871aa2875a12d3c3a7636edf8ce4634b056328966682c8ce", + "sha256:c71e6cf84e812226f8dadbe346b5e6d6728fa65a364bbfe7624b219a18950540" + ], + "index": "pypi", + "version": "==1.10.4" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", + "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", + "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" + ], + "version": "==1.0.15" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", + "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" + ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==1.5.4" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "pylint": { + "hashes": [ + "sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", + "sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "pytest": { + "hashes": [ + "sha256:341ec10361b64a24accaec3c7ba5f7d5ee1ca4cebea30f76fad3dd12db9f0541", + "sha256:952c0389db115437f966c4c2079ae9d54714b9455190e56acebe14e8c38a7efa" + ], + "index": "pypi", + "version": "==3.6.4" + }, + "pytest-cov": { + "hashes": [ + "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", + "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" + ], + "index": "pypi", + "version": "==2.5.1" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, + "simplegeneric": { + "hashes": [ + "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" + ], + "version": "==0.8.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "toml": { + "hashes": [ + "sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d" + ], + "version": "==0.9.4" + }, + "traitlets": { + "hashes": [ + "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", + "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + ], + "version": "==4.3.2" + }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", + "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", + "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", + "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", + "version": "==1.1.0" + }, + "virtualenv": { + "hashes": [ + "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", + "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" + ], + "markers": "python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*'", + "version": "==16.0.0" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} diff --git a/cli.py b/cli.py deleted file mode 100644 index 43abf030..00000000 --- a/cli.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -import better_exceptions -import click -import dill - -import easytrader - -ACCOUNT_OBJECT_FILE = "account.session" - - -@click.command() -@click.option("--use", help="指定券商 [ht, yjb, yh]") -@click.option("--prepare", type=click.Path(exists=True), help="指定登录账户文件路径") -@click.option("--get", help="调用 easytrader 中对应的变量") -@click.option("--do", help="调用 easytrader 中对应的函数名") -@click.option("--debug", default=False, help="是否输出 easytrader 的 debug 日志") -@click.argument("params", nargs=-1) -def main(prepare, use, do, get, params, debug): - if get is not None: - do = get - if prepare is not None and use in [ - "ht_client", - "yjb", - "yh_client", - "yh", - "ht", - "gf", - "xq", - ]: - user = easytrader.use(use, debug) - user.prepare(prepare) - with open(ACCOUNT_OBJECT_FILE, "wb") as f: - dill.dump(user, f) - if do is not None: - with open(ACCOUNT_OBJECT_FILE, "rb") as f: - user = dill.load(f) - - if get is not None: - result = getattr(user, do) - else: - result = getattr(user, do)(*params) - - json_result = json.dumps( - result, indent=4, ensure_ascii=False, sort_keys=True - ) - click.echo(json_result) - - -if __name__ == "__main__": - main() diff --git a/easytrader/__init__.py b/easytrader/__init__.py index d14a93cb..07572ad6 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -from .api import * -from .webtrader import WebTrader -from .joinquant_follower import JoinQuantFollower -from .ricequant_follower import RiceQuantFollower +from .api import use, follower from . import exceptions __version__ = "0.15.2" diff --git a/easytrader/api.py b/easytrader/api.py index f1fc2b83..64811bc6 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -28,25 +28,27 @@ def use(broker, debug=True, **kwargs): """ if not debug: log.setLevel(logging.INFO) - elif broker.lower() in ["xq", "雪球"]: + if broker.lower() in ["xq", "雪球"]: return XueQiuTrader(**kwargs) - elif broker.lower() in ["yh_client", "银河客户端"]: + if broker.lower() in ["yh_client", "银河客户端"]: from .yh_clienttrader import YHClientTrader return YHClientTrader() - elif broker.lower() in ["ht_client", "华泰客户端"]: + if broker.lower() in ["ht_client", "华泰客户端"]: from .ht_clienttrader import HTClientTrader return HTClientTrader() - elif broker.lower() in ["gj_client", "国金客户端"]: + if broker.lower() in ["gj_client", "国金客户端"]: from .gj_clienttrader import GJClientTrader return GJClientTrader() - elif broker.lower() in ["ths", "同花顺客户端"]: + if broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader return ClientTrader() + raise NotImplementedError + def follower(platform, **kwargs): """用于生成特定的券商对象 @@ -72,3 +74,4 @@ def follower(platform, **kwargs): return JoinQuantFollower() if platform.lower() in ["xq", "xueqiu", "雪球"]: return XueQiuFollower(**kwargs) + raise NotImplementedError diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 0eeadb69..d736d5c6 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -7,9 +7,7 @@ import easyutils -from . import grid_data_get_strategy -from . import helpers -from . import pop_dialog_handler +from . import grid_data_get_strategy, helpers, pop_dialog_handler from .config import client if not sys.platform.startswith("darwin"): @@ -41,7 +39,7 @@ def wait(self, seconds: int): """Wait for operation return""" pass - @property + @property # type: ignore @abc.abstractmethod def grid_data_get_strategy(self): """ @@ -50,7 +48,7 @@ def grid_data_get_strategy(self): """ pass - @grid_data_get_strategy.setter + @grid_data_get_strategy.setter # type: ignore @abc.abstractmethod def grid_data_get_strategy(self, strategy_cls): """ @@ -67,7 +65,7 @@ def __init__(self): self._config = client.create(self.broker_type) self._app = None self._main = None - self.grid_data_get_strategy = grid_data_get_strategy.CopyStrategy + self._grid_data_get_strategy = grid_data_get_strategy.CopyStrategy @property def app(self): @@ -167,8 +165,7 @@ def cancel_entrust(self, entrust_no): ): self._cancel_entrust_by_double_click(i) return self._handle_pop_dialogs() - else: - return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} + return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} def buy(self, security, price, amount, **kwargs): self._switch_left_menus(["买入[F1]"]) @@ -301,9 +298,9 @@ def exit(self): def _close_prompt_windows(self): self.wait(1) - for w in self._app.windows(class_name="#32770"): - if w.window_text() != self._config.TITLE: - w.close() + for window in self._app.windows(class_name="#32770"): + if window.window_text() != self._config.TITLE: + window.close() self.wait(1) def trade(self, security, price, amount): @@ -384,7 +381,8 @@ def _get_left_menus_handle(self): # sometime can't find handle ready, must retry handle.wait("ready", 2) return handle - except: + # pylint: disable=broad-except + except Exception: pass def _cancel_entrust_by_double_click(self, row): diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 2d0e2e2e..69207789 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -2,17 +2,17 @@ def create(broker): if broker == "yh": return YH - elif broker == "ht": + if broker == "ht": return HT - elif broker == "gj": + if broker == "gj": return GJ - elif broker == "ths": + if broker == "ths": return CommonConfig - raise NotImplemented + raise NotImplementedError class CommonConfig: - DEFAULT_EXE_PATH = None + DEFAULT_EXE_PATH: str = "" TITLE = "网上股票交易系统5.0" TRADE_SECURITY_CONTROL_ID = 1032 diff --git a/easytrader/exceptions.py b/easytrader/exceptions.py index dacd5f15..8a838ef3 100644 --- a/easytrader/exceptions.py +++ b/easytrader/exceptions.py @@ -1,4 +1,9 @@ # -*- coding: utf-8 -*- +""" +Exceptions +""" + + class TradeError(IOError): pass diff --git a/easytrader/follower.py b/easytrader/follower.py index ff494d95..c887be13 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- +import abc import datetime import os import pickle +import queue import re import threading import time +from typing import List import requests -# noinspection PyUnresolvedReferences -from six.moves.queue import Queue - from . import exceptions from .log import log -class BaseFollower(object): +class BaseFollower(metaclass=abc.ABCMeta): LOGIN_PAGE = "" LOGIN_API = "" TRANSACTION_API = "" @@ -24,7 +24,7 @@ class BaseFollower(object): WEB_ORIGIN = "" def __init__(self): - self.trade_queue = Queue() + self.trade_queue = queue.Queue() self.expired_cmds = set() self.s = requests.Session() @@ -71,13 +71,13 @@ def check_login_success(self, rep): :raise 如果登录失败应该抛出 NotLoginError """ pass - def create_login_params(self, user, password, **kwargs): + def create_login_params(self, user, password, **kwargs) -> dict: """生成 post 登录接口的参数 :param user: 用户名 :param password: 密码 :return dict 登录参数的字典 """ - pass + return {} def follow( self, @@ -159,30 +159,30 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): transactions = self.query_strategy_transaction( strategy, **kwargs ) + # pylint: disable=broad-except except Exception as e: - log.warning("无法获取策略 {} 调仓信息, 错误: {}, 跳过此次调仓查询".format(name, e)) + log.warning("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e) continue - for t in transactions: + for transaction in transactions: trade_cmd = { "strategy": strategy, "strategy_name": name, - "action": t["action"], - "stock_code": t["stock_code"], - "amount": t["amount"], - "price": t["price"], - "datetime": t["datetime"], + "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 log.info( - "策略 [{}] 发送指令到交易队列, 股票: {} 动作: {} 数量: {} 价格: {} 信号产生时间: {}".format( - name, - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - ) + "策略 [%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) @@ -240,16 +240,15 @@ def _execute_trade_cmd( expire = (now - trade_cmd["datetime"]).total_seconds() if expire > expire_seconds: log.warning( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - now, - expire_seconds, - ) + "策略 [%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 @@ -257,30 +256,28 @@ def _execute_trade_cmd( price = trade_cmd["price"] if not self._is_number(price) or price <= 0: log.warning( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - now, - ) + "策略 [%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: log.warning( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - now, - ) + "策略 [%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 @@ -296,28 +293,26 @@ def _execute_trade_cmd( trader_name = type(user).__name__ err_msg = "{}: {}".format(type(e).__name__, e.args) log.error( - "{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}".format( - trader_name, - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - err_msg, - ) + "%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s 指令产生时间: %s) 失败, 错误信息: %s", + trader_name, + trade_cmd["strategy_name"], + trade_cmd["stock_code"], + trade_cmd["action"], + trade_cmd["amount"], + trade_cmd["price"], + trade_cmd["datetime"], + err_msg, ) else: log.info( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - response, - ) + "策略 [%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"], + response, ) def trade_worker( @@ -343,21 +338,21 @@ def query_strategy_transaction(self, strategy, **kwargs): self.project_transactions(transactions, **kwargs) return self.order_transactions_sell_first(transactions) - def extract_transactions(self, history): + def extract_transactions(self, history) -> List[str]: """ 抽取接口返回中的调仓记录列表 :param history: 调仓接口返回信息的字典对象 :return: [] 调参历史记录的列表 """ - pass + return [] - def create_query_transaction_params(self, strategy): + def create_query_transaction_params(self, strategy) -> dict: """ 生成用于查询调参记录的参数 :param strategy: 策略 id :return: dict 调参记录参数 """ - pass + return {} @staticmethod def re_find(pattern, string, dtype=str): @@ -374,9 +369,9 @@ def project_transactions(self, transactions, **kwargs): def order_transactions_sell_first(self, transactions): # 调整调仓记录的顺序为先卖再买 sell_first_transactions = [] - for t in transactions: - if t["action"] == "sell": - sell_first_transactions.insert(0, t) + for transaction in transactions: + if transaction["action"] == "sell": + sell_first_transactions.insert(0, transaction) else: - sell_first_transactions.append(t) + sell_first_transactions.append(transaction) return sell_first_transactions diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index 2576cf02..7fb4012c 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -6,8 +6,7 @@ import pywinauto import pywinauto.clipboard -from . import clienttrader -from . import helpers +from . import clienttrader, helpers class GJClientTrader(clienttrader.BaseLoginClientTrader): @@ -18,9 +17,11 @@ def broker_type(self): def login(self, user, password, exe_path, comm_password=None, **kwargs): """ 登陆客户端 + :param user: 账号 :param password: 明文密码 - :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' + :param exe_path: 客户端路径类似 'C:\\中国银河证券双子星3.2\\Binarystar.exe', + 默认 'C:\\中国银河证券双子星3.2\\Binarystar.exe' :param comm_password: 通讯密码, 华泰需要,可不设 :return: """ @@ -28,6 +29,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=1 ) + # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) @@ -52,10 +54,13 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): try: self._app.top_window().wait_not("exists", 5) break - except: + + # pylint: disable=broad-except + except Exception: self._app.top_window()["确定"].click() - pass - except Exception as e: + + # pylint: disable=broad-except + except Exception: pass self._app = pywinauto.Application().connect( diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py index d6670c2a..641cef84 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_data_get_strategy.py @@ -64,8 +64,9 @@ def _get_clipboard_data(self) -> str: while True: try: return pywinauto.clipboard.GetData() + # pylint: disable=broad-except except Exception as e: - log.warning("{}, retry ......".format(e)) + log.warning("%s, retry ......", e) class XlsStrategy(BaseStrategy): diff --git a/easytrader/helpers.py b/easytrader/helpers.py index bc80c5c6..dd5d560c 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -1,31 +1,13 @@ # -*- coding: utf-8 -*- import datetime import json +import random import re -import ssl -import uuid import requests -import six -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.poolmanager import PoolManager -from six.moves import input from . import exceptions -if six.PY2: - from io import open - - -class Ssl3HttpAdapter(HTTPAdapter): - def init_poolmanager(self, connections, maxsize, block=False): - self.poolmanager = PoolManager( - num_pools=connections, - maxsize=maxsize, - block=block, - ssl_version=ssl.PROTOCOL_TLSv1, - ) - def parse_cookies_str(cookies): """ @@ -71,20 +53,6 @@ def get_stock_type(stock_code): return "sz" -def ht_verify_code_new(image_path): - """显示图片,人肉读取,手工输入""" - - from PIL import Image - - img = Image.open(image_path) - img.show() - - # 关闭图片后输入答案 - s = input("input the pics answer :") - - return s - - def recognize_verify_code(image_path, broker="ht"): """识别验证码,返回识别后的字符串,使用 tesseract 实现 :param image_path: 图片路径 @@ -93,7 +61,7 @@ def recognize_verify_code(image_path, broker="ht"): if broker == "gf": return detect_gf_result(image_path) - elif broker in ["yh_client", "gj_client"]: + if broker in ["yh_client", "gj_client"]: return detect_yh_client_result(image_path) # 调用 tesseract 识别 return default_verify_code_detect(image_path) @@ -162,16 +130,6 @@ def invoke_tesseract_to_recognize(img): return "".join(valid_chars) -def get_mac(): - # 获取mac地址 link: http://stackoverflow.com/questions/28927958/python-get-mac-address - return ( - "".join( - c + "-" if i % 2 else c - for i, c in enumerate(hex(uuid.getnode())[2:].zfill(12)) - )[:-1] - ).upper() - - def grep_comma(num_str): return num_str.replace(",", "") @@ -199,11 +157,6 @@ def get_today_ipo_data(): :return: 今日可申购新股列表 apply_code申购代码 price发行价格 """ - import random - import json - import datetime - import requests - agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0" send_headers = { "Host": "xueqiu.com", @@ -217,13 +170,13 @@ def get_today_ipo_data(): "Connection": "keep-alive", } - sj = random.randint(1000000000000, 9999999999999) + timestamp = random.randint(1000000000000, 9999999999999) home_page_url = "https://xueqiu.com" ipo_data_url = ( "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl" "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_" "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" - % (str(sj)) + % (str(timestamp)) ) session = requests.session() diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index a3991364..87a9c2db 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -26,6 +26,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=1 ) + # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index 2221f3d8..1df5fcc8 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -67,7 +67,7 @@ def follow( strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: - log.error("抽取交易id和策略名失败, 无效的模拟交易url: {}".format(strategy_url)) + log.error("抽取交易id和策略名失败, 无效的模拟交易url: %s", strategy_url) raise strategy_worker = Thread( target=self.track_strategy_worker, @@ -76,7 +76,7 @@ def follow( ) strategy_worker.start() workers.append(strategy_worker) - log.info("开始跟踪策略: {}".format(strategy_name)) + log.info("开始跟踪策略: %s", strategy_name) for worker in workers: worker.join() @@ -107,18 +107,25 @@ def stock_shuffle_to_prefix(stock): code = stock[:6] if stock.find("XSHG") != -1: return "sh" + code - elif stock.find("XSHE") != -1: + + if stock.find("XSHE") != -1: return "sz" + code raise TypeError("not valid stock code: {}".format(code)) def project_transactions(self, transactions, **kwargs): - for t in transactions: - t["amount"] = self.re_find("\d+", t["amount"], dtype=int) + for transaction in transactions: + transaction["amount"] = self.re_find( + r"\d+", transaction["amount"], dtype=int + ) - time_str = "{} {}".format(t["date"], t["time"]) - t["datetime"] = datetime.strptime(time_str, "%Y-%m-%d %H:%M") + time_str = "{} {}".format(transaction["date"], transaction["time"]) + transaction["datetime"] = datetime.strptime( + time_str, "%Y-%m-%d %H:%M" + ) - stock = self.re_find(r"\d{6}\.\w{4}", t["stock"]) - t["stock_code"] = self.stock_shuffle_to_prefix(stock) + stock = self.re_find(r"\d{6}\.\w{4}", transaction["stock"]) + transaction["stock_code"] = self.stock_shuffle_to_prefix(stock) - t["action"] = "buy" if t["transaction"] == "买" else "sell" + transaction["action"] = ( + "buy" if transaction["transaction"] == "买" else "sell" + ) diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index d845879f..e264d5db 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -1,6 +1,7 @@ # coding:utf-8 import re import time +from typing import Optional from . import exceptions @@ -12,16 +13,16 @@ def __init__(self, app): def handle(self, title): if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): self._submit_by_shortcut() + return None - elif "提示" in title: + if "提示" in title: content = self._extract_content() self._submit_by_click() return {"message": content} - else: - content = self._extract_content() - self._close() - return {"message": "unknown message: {}".format(content)} + content = self._extract_content() + self._close() + return {"message": "unknown message: {}".format(content)} def _extract_content(self): return self._app.top_window().Static.window_text() @@ -40,26 +41,32 @@ def _close(self): class TradePopDialogHandler(PopDialogHandler): - def handle(self, title): + def handle(self, title) -> Optional[dict]: if title == "委托确认": self._submit_by_shortcut() + return None - elif title == "提示信息": + if title == "提示信息": content = self._extract_content() if "超出涨跌停" in content: self._submit_by_shortcut() - elif "委托价格的小数价格应为" in content: + return None + + if "委托价格的小数价格应为" in content: self._submit_by_shortcut() + return None + + return None - elif title == "提示": + if title == "提示": content = self._extract_content() if "成功" in content: entrust_no = self._extract_entrust_id(content) self._submit_by_click() return {"entrust_no": entrust_no} - else: - self._submit_by_click() - time.sleep(0.05) - raise exceptions.TradeError(content) - else: - self._close() + + self._submit_by_click() + time.sleep(0.05) + raise exceptions.TradeError(content) + self._close() + return None diff --git a/easytrader/ricequant_follower.py b/easytrader/ricequant_follower.py index 08bba729..dd1c2191 100644 --- a/easytrader/ricequant_follower.py +++ b/easytrader/ricequant_follower.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from datetime import datetime from threading import Thread @@ -9,7 +8,11 @@ class RiceQuantFollower(BaseFollower): - def login(self, user, password, **kwargs): + def __init__(self): + super().__init__() + self.client = None + + def login(self, user=None, password=None, **kwargs): from rqopen_client import RQOpenClient self.client = RQOpenClient(user, password, logger=log) @@ -34,7 +37,7 @@ def follow( :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足 """ users = self.warp_list(users) - run_id_list = self.warp_list(run_id) + run_ids = self.warp_list(run_id) if cmd_cache: self.load_expired_cmd_cache() @@ -44,16 +47,16 @@ def follow( ) workers = [] - for run_id in run_id_list: - strategy_name = self.extract_strategy_name(run_id) + for id_ in run_ids: + strategy_name = self.extract_strategy_name(id_) strategy_worker = Thread( target=self.track_strategy_worker, - args=[run_id, strategy_name], + args=[id_, strategy_name], kwargs={"interval": track_interval}, ) strategy_worker.start() workers.append(strategy_worker) - log.info("开始跟踪策略: {}".format(strategy_name)) + log.info("开始跟踪策略: %s", strategy_name) for worker in workers: worker.join() @@ -61,9 +64,9 @@ def extract_strategy_name(self, run_id): ret_json = self.client.get_positions(run_id) if ret_json["code"] != 200: log.error( - "fetch data from run_id {} fail, msg {}".format( - run_id, ret_json["msg"] - ) + "fetch data from run_id %s fail, msg %s", + run_id, + ret_json["msg"], ) raise RuntimeError(ret_json["msg"]) return ret_json["resp"]["name"] @@ -72,9 +75,9 @@ def extract_day_trades(self, run_id): ret_json = self.client.get_day_trades(run_id) if ret_json["code"] != 200: log.error( - "fetch day trades from run_id {} fail, msg {}".format( - run_id, ret_json["msg"] - ) + "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"] @@ -92,23 +95,25 @@ def stock_shuffle_to_prefix(stock): code = stock[:6] if stock.find("XSHG") != -1: return "sh" + code - elif stock.find("XSHE") != -1: + if stock.find("XSHE") != -1: return "sz" + code raise TypeError("not valid stock code: {}".format(code)) def project_transactions(self, transactions, **kwargs): new_transactions = [] - for t in transactions: - trans = {} - trans["price"] = t["price"] - trans["amount"] = int(abs(t["quantity"])) - trans["datetime"] = datetime.strptime( - t["time"], "%Y-%m-%d %H:%M:%S" + for transaction in transactions: + new_transaction = {} + new_transaction["price"] = transaction["price"] + new_transaction["amount"] = int(abs(transaction["quantity"])) + new_transaction["datetime"] = datetime.strptime( + transaction["time"], "%Y-%m-%d %H:%M:%S" + ) + new_transaction["stock_code"] = self.stock_shuffle_to_prefix( + transaction["order_book_id"] ) - trans["stock_code"] = self.stock_shuffle_to_prefix( - t["order_book_id"] + new_transaction["action"] = ( + "buy" if transaction["quantity"] > 0 else "sell" ) - trans["action"] = "buy" if t["quantity"] > 0 else "sell" - new_transactions.append(trans) + new_transactions.append(new_transaction) return new_transactions diff --git a/easytrader/server.py b/easytrader/server.py index 1b935388..7a8fe5c0 100644 --- a/easytrader/server.py +++ b/easytrader/server.py @@ -1,6 +1,6 @@ import functools -from flask import Flask, request, jsonify +from flask import Flask, jsonify, request from . import api from .log import log @@ -10,11 +10,12 @@ global_store = {} -def error_handle(f): - @functools.wraps(f) +def error_handle(func): + @functools.wraps(func) def wrapper(*args, **kwargs): try: - return f(*args, **kwargs) + return func(*args, **kwargs) + # pylint: disable=broad-except except Exception as e: log.exception("server error") message = "{}: {}".format(e.__class__, e) diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index dcbe4de6..d4a71670 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import abc import logging import os import re @@ -6,14 +7,14 @@ from threading import Thread import requests +import requests.exceptions -from . import exceptions -from . import helpers +from . import exceptions, helpers from .log import log # noinspection PyIncorrectDocstring -class WebTrader(object): +class WebTrader(metaclass=abc.ABCMeta): global_config_path = os.path.dirname(__file__) + "/config/global.json" config_path = "" @@ -32,9 +33,9 @@ def read_config(self, path): self.account_config = helpers.file2dict(path) except ValueError: log.error("配置文件格式有误,请勿使用记事本编辑,推荐 sublime text") - for v in self.account_config: - if type(v) is int: - log.warn("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题") + for value in self.account_config: + if isinstance(value, int): + log.warning("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题") def prepare(self, config_file=None, user=None, password=None, **kwargs): """登录的统一接口 @@ -94,9 +95,9 @@ def check_login(self, sleepy=30): self.check_account_live(response) except requests.exceptions.ConnectionError: pass - except Exception as e: + except requests.exceptions.RequestException as e: log.setLevel(self.log_level) - log.error("心跳线程发现账户出现错误: {} {}, 尝试重新登陆".format(e.__class__, e)) + log.error("心跳线程发现账户出现错误: %s %s, 尝试重新登陆", e.__class__, e) self.autologin() finally: log.setLevel(self.log_level) @@ -186,7 +187,8 @@ def do(self, params): response_data = self.request(request_params) try: format_json_data = self.format_response_data(response_data) - except: + # pylint: disable=broad-except + except Exception: # Caused by server force logged out return None return_data = self.fix_error_data(format_json_data) @@ -196,19 +198,19 @@ def do(self, params): self.autologin() return return_data - def create_basic_params(self): + def create_basic_params(self) -> dict: """生成基本的参数""" - pass + return {} - def request(self, params): + def request(self, params) -> dict: """请求并获取 JSON 数据 :param params: Get 参数""" - pass + return {} def format_response_data(self, data): """格式化返回的 json 数据 :param data: 请求返回的数据 """ - pass + return data def fix_error_data(self, data): """若是返回错误移除外层的列表 @@ -219,7 +221,9 @@ def format_response_data_type(self, response_data): """格式化返回的值为正确的类型 :param response_data: 返回的数据 """ - if type(response_data) is not list: + if isinstance(response_data, list) and not isinstance( + response_data, str + ): return response_data int_match_str = "|".join(self.config["response_format"]["int"]) diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index d4dd34e9..e504b971 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals, print_function, division +from __future__ import division, print_function, unicode_literals import json import re @@ -20,7 +20,9 @@ class XueQiuFollower(BaseFollower): WEB_REFERER = 'https://www.xueqiu.com' def __init__(self): - super(XueQiuFollower, self).__init__() + super().__init__() + self._adjust_sell = None + self._users = None def login(self, user=None, password=None, **kwargs): """ @@ -73,8 +75,7 @@ def follow(self, """ self._adjust_sell = adjust_sell - users = self.warp_list(users) - self._users = users + self._users = self.warp_list(users) strategies = self.warp_list(strategies) total_assets = self.warp_list(total_assets) @@ -83,7 +84,7 @@ def follow(self, if cmd_cache: self.load_expired_cmd_cache() - self.start_trader_thread(users, trade_cmd_expire_seconds) + 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): @@ -93,7 +94,7 @@ def follow(self, strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: - log.error('抽取交易id和策略名失败, 无效模拟交易url: {}'.format(strategy_url)) + log.error('抽取交易id和策略名失败, 无效模拟交易url: %s', strategy_url) raise strategy_worker = Thread( target=self.track_strategy_worker, @@ -103,7 +104,7 @@ def follow(self, 'assets': assets }) strategy_worker.start() - log.info('开始跟踪策略: {}'.format(strategy_name)) + log.info('开始跟踪策略: %s', strategy_name) def calculate_assets(self, strategy_url, @@ -147,27 +148,30 @@ def create_query_transaction_params(self, strategy): def none_to_zero(self, data): if data is None: return 0 - else: - return data + return data # noinspection PyMethodOverriding def project_transactions(self, transactions, assets): - for t in transactions: - weight_diff = self.none_to_zero(t['weight']) - self.none_to_zero( - t['prev_weight']) + for transaction in transactions: + weight_diff = self.none_to_zero( + transaction['weight']) - self.none_to_zero( + transaction['prev_weight']) - initial_amount = abs(weight_diff) / 100 * assets / t['price'] + initial_amount = abs(weight_diff) / 100 * assets / transaction[ + 'price'] - t['datetime'] = datetime.fromtimestamp(t['created_at'] // 1000) + transaction['datetime'] = datetime.fromtimestamp( + transaction['created_at'] // 1000) - t['stock_code'] = t['stock_symbol'].lower() + transaction['stock_code'] = transaction['stock_symbol'].lower() - t['action'] = 'buy' if weight_diff > 0 else 'sell' + transaction['action'] = 'buy' if weight_diff > 0 else 'sell' - t['amount'] = int(round(initial_amount, -2)) + transaction['amount'] = int(round(initial_amount, -2)) if self._adjust_sell: - t['amount'] = self._adjust_sell_amount(t['stock_code'], - t['amount']) + transaction['amount'] = self._adjust_sell_amount( + transaction['stock_code'], + transaction['amount']) def _adjust_sell_amount(self, stock_code, amount): """ @@ -189,8 +193,8 @@ def _adjust_sell_amount(self, stock_code, amount): try: stock = next(s for s in position if s['证券代码'] == stock_code) except StopIteration: - log.info('根据持仓调整 {} 卖出额,发现未持有股票 {}, 不做任何调整'.format( - stock_code, stock_code)) + log.info('根据持仓调整 %s 卖出额,发现未持有股票 %s, 不做任何调整', + stock_code, stock_code) return amount available_amount = stock['可用余额'] @@ -198,8 +202,8 @@ def _adjust_sell_amount(self, stock_code, amount): return amount adjust_amount = available_amount // 100 * 100 - log.info('股票 {} 实际可用余额 {}, 指令卖出股数为 {}, 调整为 {}'.format( - stock_code, available_amount, amount, adjust_amount)) + log.info('股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s', + stock_code, available_amount, amount, adjust_amount) return adjust_amount def _get_portfolio_info(self, portfolio_code): diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index e732e836..6da1362f 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -7,9 +7,7 @@ import requests -from . import exceptions -from . import helpers -from . import webtrader +from . import exceptions, helpers, webtrader from .log import log @@ -181,7 +179,8 @@ def _time_strftime(time_stamp): try: local_time = time.localtime(time_stamp / 1000) return time.strftime("%Y-%m-%d %H:%M:%S", local_time) - except: + # pylint: disable=broad-except + except Exception: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) def get_position(self): @@ -248,14 +247,6 @@ def get_entrust(self): else: status = "已成" for entrust in xq_entrusts["rebalancing_histories"]: - volume = ( - abs( - entrust["target_weight"] - - replace_none(entrust["prev_weight"]) - ) - * self.multiple - / 10000 - ) price = entrust["price"] entrust_list.append( { @@ -291,7 +282,7 @@ def cancel_entrust(self, entrust_no): for entrust in xq_entrusts["rebalancing_histories"]: if entrust["id"] == entrust_no and status == "pending": is_have = True - bs = ( + buy_or_sell = ( "buy" if entrust["target_weight"] < entrust["weight"] else "sell" @@ -310,7 +301,7 @@ def cancel_entrust(self, entrust_no): r = self._trade( security=entrust["stock_symbol"], volume=volume, - entrust_bs=bs, + entrust_bs=buy_or_sell, ) if len(r) > 0 and "error_info" in r[0]: raise exceptions.TradeError( @@ -373,7 +364,7 @@ def adjust_weight(self, stock_code, weight): remain_weight = 100 - sum(i.get("weight") for i in position_list) cash = round(remain_weight, 2) - log.debug("调仓比例:%f, 剩余持仓 :%f" % (weight, remain_weight)) + log.debug("调仓比例:%f, 剩余持仓 :%f", weight, remain_weight) data = { "cash": cash, "holdings": str(json.dumps(position_list)), @@ -384,22 +375,22 @@ def adjust_weight(self, stock_code, weight): try: resp = self.s.post(self.config["rebalance_url"], data=data) + # pylint: disable=broad-except except Exception as e: - log.warn("调仓失败: %s " % e) - return - else: - log.debug("调仓 %s: 持仓比例%d" % (stock["name"], weight)) - resp_json = json.loads(resp.text) - if "error_description" in resp_json and resp.status_code != 200: - log.error("调仓错误: %s" % (resp_json["error_description"])) - return [ - { - "error_no": resp_json["error_code"], - "error_info": resp_json["error_description"], - } - ] - else: - log.debug("调仓成功 %s: 持仓比例%d" % (stock["name"], weight)) + log.warning("调仓失败: %s ", e) + return None + log.debug("调仓 %s: 持仓比例%d", stock["name"], weight) + resp_json = json.loads(resp.text) + if "error_description" in resp_json and resp.status_code != 200: + log.error("调仓错误: %s", resp_json["error_description"]) + return [ + { + "error_no": resp_json["error_code"], + "error_info": resp_json["error_description"], + } + ] + log.debug("调仓成功 %s: 持仓比例%d", stock["name"], weight) + return None def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): """ @@ -487,7 +478,7 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): * 100 ) cash = round(cash, 2) - log.debug("weight:%f, cash:%f" % (weight, cash)) + log.debug("weight:%f, cash:%f", weight, cash) data = { "cash": cash, @@ -499,43 +490,41 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): try: resp = self.s.post(self.config["rebalance_url"], data=data) + # pylint: disable=broad-except except Exception as e: - log.warn("调仓失败: %s " % e) - return + log.warning("调仓失败: %s ", e) + return None else: log.debug( - "调仓 %s%s: %d" % (entrust_bs, stock["name"], resp.status_code) + "调仓 %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: - log.error("调仓错误: %s" % (resp_json["error_description"])) + log.error("调仓错误: %s", resp_json["error_description"]) return [ { "error_no": resp_json["error_code"], "error_info": resp_json["error_description"], } ] - else: - 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": "-", - } - ] + 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 buy(self, security, price=0, amount=0, volume=0, entrust_prop=0): """买入卖出股票 diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 81ab10ef..c3330ffe 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -4,9 +4,7 @@ import pywinauto -from . import clienttrader -from . import grid_data_get_strategy -from . import helpers +from . import clienttrader, grid_data_get_strategy, helpers class YHClientTrader(clienttrader.BaseLoginClientTrader): @@ -30,8 +28,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): 登陆客户端 :param user: 账号 :param password: 明文密码 - :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', - 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' + :param exe_path: 客户端路径类似 'C:\\中国银河证券双子星3.2\\Binarystar.exe', + 默认 'C:\\中国银河证券双子星3.2\\Binarystar.exe' :param comm_password: 通讯密码, 华泰需要,可不设 :return: """ @@ -39,6 +37,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=1 ) + # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) @@ -64,7 +63,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): try: self._app.top_window().wait_not("exists visible", 10) break - except: + # pylint: disable=broad-except + except Exception: pass self._app = pywinauto.Application().connect( @@ -76,7 +76,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._main.window(control_id=129, class_name="SysTreeView32").wait( "ready", 2 ) - except: + # pylint: disable=broad-except + except Exception: self.wait(2) self._switch_window_to_normal_mode() @@ -93,7 +94,7 @@ def _handle_verify_code(self): file_path = tempfile.mktemp() control.capture_as_image().save(file_path, "jpeg") verify_code = helpers.recognize_verify_code(file_path, "yh_client") - return "".join(re.findall("\d+", verify_code)) + return "".join(re.findall(r"\d+", verify_code)) @property def balance(self): diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..976ba029 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/requirements.txt b/requirements.txt index 6316789a..0e9f430d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,31 @@ -pywinauto -bs4 -requests -dill -click -six -flask -Pillow -pytesseract -pandas -pyperclip -rqopen-client>=0.0.5 -easyutils +-i http://mirrors.aliyun.com/pypi/simple/ +--trusted-host mirrors.aliyun.com +beautifulsoup4==4.6.0 +bs4==0.0.1 +certifi==2018.4.16 +chardet==3.0.4 +click==6.7 +cssselect==1.0.3; python_version != '3.3.*' +dill==0.2.8.2 +easyutils==0.1.7 +flask==1.0.2 +idna==2.7 +itsdangerous==0.24 +jinja2==2.10 +lxml==4.2.3 +markupsafe==1.0 +numpy==1.15.0; python_version >= '2.7' +pandas==0.23.3 +pillow==5.2.0 +pyperclip==1.6.4 +pyquery==1.4.0; python_version != '3.0.*' +pytesseract==0.2.4 +python-dateutil==2.7.3 +python-xlib==0.23 +pytz==2018.5 +pywinauto==0.6.4 +requests==2.19.1 +rqopen-client==0.0.5 +six==1.11.0 +urllib3==1.23; python_version != '3.1.*' +werkzeug==0.14.1 diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py index 529fe032..db336fdb 100644 --- a/tests/test_xq_follower.py +++ b/tests/test_xq_follower.py @@ -2,7 +2,7 @@ import unittest from unittest import mock -from easytrader import XueQiuFollower +from easytrader.xq_follower import XueQiuFollower class TestXueQiuTrader(unittest.TestCase): diff --git a/tests/test_xqtrader.py b/tests/test_xqtrader.py index ac322bd8..22abf274 100644 --- a/tests/test_xqtrader.py +++ b/tests/test_xqtrader.py @@ -1,7 +1,7 @@ # coding: utf-8 import unittest -from easytrader import XueQiuTrader +from easytrader.xqtrader import XueQiuTrader class TestXueQiuTrader(unittest.TestCase): From 3d4fd4d80d4a54b2a453c8366f08b3800dd7210b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 8 Aug 2018 09:12:31 +0800 Subject: [PATCH 131/276] :star: add basic hooks --- .coveragerc | 7 + .gitignore | 4 + .pre-commit-config.yaml | 36 ++ .pylintrc | 571 ++++++++++++++++++++ Pipfile | 41 ++ Pipfile.lock | 747 +++++++++++++++++++++++++++ cli.py | 52 -- easytrader/__init__.py | 5 +- easytrader/api.py | 13 +- easytrader/clienttrader.py | 22 +- easytrader/config/client.py | 10 +- easytrader/exceptions.py | 5 + easytrader/follower.py | 149 +++--- easytrader/gj_clienttrader.py | 17 +- easytrader/grid_data_get_strategy.py | 3 +- easytrader/helpers.py | 55 +- easytrader/ht_clienttrader.py | 1 + easytrader/joinquant_follower.py | 27 +- easytrader/pop_dialog_handler.py | 37 +- easytrader/ricequant_follower.py | 53 +- easytrader/server.py | 9 +- easytrader/webtrader.py | 34 +- easytrader/xq_follower.py | 50 +- easytrader/xqtrader.py | 101 ++-- easytrader/yh_clienttrader.py | 17 +- mypy.ini | 2 + requirements.txt | 44 +- tests/test_xq_follower.py | 2 +- tests/test_xqtrader.py | 2 +- 29 files changed, 1733 insertions(+), 383 deletions(-) create mode 100644 .coveragerc create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 Pipfile create mode 100644 Pipfile.lock delete mode 100644 cli.py create mode 100644 mypy.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..65e90598 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +include = easytrader/* +omit = tests/* + +[report] +fail_under = -1 diff --git a/.gitignore b/.gitignore index 0688ba4b..273558e6 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +bak +.mypy_cache +.pyre +.pytest_cache yjb_account.json htt.json gft.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..99e6af23 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +fail_fast: true +repos: +- repo: local + hooks: + - id: python_sort_imports + name: python_sort_imports + entry: pipenv run sort_imports + language: system + types: [python] + - id: python_format + name: python_format + entry: pipenv run format + language: system + types: [python] + - id: python_lint + name: python_lint + entry: pipenv run lint + language: system + types: [python] + - id: python_type_check + name: python_type_check + entry: pipenv run type_check + language: system + types: [python] + - id: python_test + name: python_test + entry: pipenv run test + language: system + types: [python] + verbose: true + - id: django_test + name: django_test + entry: pipenv run test + language: system + types: [python] + verbose: true diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..e5c5b89b --- /dev/null +++ b/.pylintrc @@ -0,0 +1,571 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns=\d{4}.+\.py, + test, + apps.py, + __init__.py, + urls.py, + manage.py + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=0 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=too-many-public-methods, + len-as-condition, + unused-argument, + too-many-arguments, + arguments-differ, + line-too-long, + fixme, + missing-docstring, + invalid-envvar-default, + ungrouped-imports, + bad-continuation, + too-many-ancestors, + too-few-public-methods, + no-self-use, + #print-statement, + #parameter-unpacking, + #unpacking-in-except, + #old-raise-syntax, + #backtick, + #long-suffix, + #old-ne-operator, + #old-octal-literal, + #import-star-module-level, + #non-ascii-bytes-literal, + #raw-checker-failed, + #bad-inline-option, + #locally-disabled, + #locally-enabled, + #file-ignored, + #suppressed-message, + #useless-suppression, + #deprecated-pragma, + #apply-builtin, + #basestring-builtin, + #buffer-builtin, + #cmp-builtin, + #coerce-builtin, + #execfile-builtin, + #file-builtin, + #long-builtin, + #raw_input-builtin, + #reduce-builtin, + #standarderror-builtin, + #unicode-builtin, + #xrange-builtin, + #coerce-method, + #delslice-method, + #getslice-method, + #setslice-method, + #no-absolute-import, + #old-division, + #dict-iter-method, + #dict-view-method, + #next-method-called, + #metaclass-assignment, + #indexing-exception, + #raising-string, + #reload-builtin, + #oct-method, + #hex-method, + #nonzero-method, + #cmp-method, + #input-builtin, + #round-builtin, + #intern-builtin, + #unichr-builtin, + #map-builtin-not-iterating, + #zip-builtin-not-iterating, + #range-builtin-not-iterating, + #filter-builtin-not-iterating, + #using-cmp-argument, + #eq-without-hash, + #div-method, + #idiv-method, + #rdiv-method, + #exception-message-attribute, + #invalid-str-codec, + #sys-max-int, + #bad-python3-import, + #deprecated-string-function, + #deprecated-str-translate-call, + #deprecated-itertools-function, + #deprecated-types-field, + #next-method-defined, + #dict-items-not-iterating, + #dict-keys-not-iterating, + #dict-values-not-iterating + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=optparse.Values,sys.exit + + +[BASIC] + +# Naming style matching correct argument names +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style +#argument-rgx= + +# Naming style matching correct attribute names +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style +#class-attribute-rgx= + +# Naming style matching correct class names +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming-style +#class-rgx= + +# Naming style matching correct constant names +const-naming-style=any + +# Regular expression matching correct constant names. Overrides const-naming- +# style +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=5 + +# Naming style matching correct function names +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma +good-names=i, + do, + f, + df, + s, + j, + k, + ex, + Run, + _, + db, + r, + x, + y, + e + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct inline iteration names +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style +#inlinevar-rgx= + +# Naming style matching correct method names +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style +#method-rgx= + +# Naming style matching correct module names +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=79 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=20 + +# Maximum number of locals for function / method body +max-locals=20 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception + diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..bf458f95 --- /dev/null +++ b/Pipfile @@ -0,0 +1,41 @@ +[[source]] +url = "http://mirrors.aliyun.com/pypi/simple/" +verify_ssl = false +name = "pypi" + +[packages] +pywinauto = "*" +"bs4" = "*" +requests = "*" +dill = "*" +click = "*" +six = "*" +flask = "*" +pillow = "*" +pytesseract = "*" +pandas = "*" +pyperclip = "*" +rqopen-client = ">=0.0.5" +easyutils = "*" + +[dev-packages] +pytest-cov = "*" +pre-commit = "*" +pytest = "*" +pylint = "*" +mypy = "*" +isort = "*" +black = "==18.6b4" +ipython = "*" +better-exceptions = "*" + +[requires] +python_version = "3.6" + +[scripts] +sort_imports = "bash -c 'isort \"$@\"; git add -u' --" +format = "bash -c 'black -l 79 \"$@\"; git add -u' --" +lint = "pylint" +type_check = "mypy" +test = "bash -c 'pytest -vx --cov=easytrader tests'" +lock = "bash -c 'pipenv lock -r > requirements.txt'" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..c793e662 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,747 @@ +{ + "_meta": { + "hash": { + "sha256": "e2a2ba761a3628e4851f250cc8882bca58d22c9ebfa11a6923549503a00d577a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "http://mirrors.aliyun.com/pypi/simple/", + "verify_ssl": false + } + ] + }, + "default": { + "beautifulsoup4": { + "hashes": [ + "sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", + "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", + "sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89" + ], + "version": "==4.6.0" + }, + "bs4": { + "hashes": [ + "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" + ], + "index": "pypi", + "version": "==0.0.1" + }, + "certifi": { + "hashes": [ + "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", + "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + ], + "version": "==2018.4.16" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "index": "pypi", + "version": "==6.7" + }, + "cssselect": { + "hashes": [ + "sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204", + "sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206" + ], + "markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*'", + "version": "==1.0.3" + }, + "dill": { + "hashes": [ + "sha256:624dc244b94371bb2d6e7f40084228a2edfff02373fe20e018bef1ee92fdd5b3" + ], + "index": "pypi", + "version": "==0.2.8.2" + }, + "easyutils": { + "hashes": [ + "sha256:45b46748e20dd3c0e840fa9c1fa7d7f3dc295e58a81796d10329957c20b7f20a" + ], + "index": "pypi", + "version": "==0.1.7" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "itsdangerous": { + "hashes": [ + "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" + ], + "version": "==0.24" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "lxml": { + "hashes": [ + "sha256:0941f4313208c07734410414d8308812b044fd3fb98573454e3d3a0d2e201f3d", + "sha256:0b18890aa5730f9d847bc5469e8820f782d72af9985a15a7552109a86b01c113", + "sha256:21f427945f612ac75576632b1bb8c21233393c961f2da890d7be3927a4b6085f", + "sha256:24cf6f622a4d49851afcf63ac4f0f3419754d4e98a7a548ab48dd03c635d9bd3", + "sha256:2dc6705486b8abee1af9e2a3761e30a3cb19e8276f20ca7e137ee6611b93707c", + "sha256:2e43b2e5b7d2b9abe6e0301eef2c2c122ab45152b968910eae68bdee2c4cfae0", + "sha256:329a6d8b6d36f7d6f8b6c6a1db3b2c40f7e30a19d3caf62023c9d6a677c1b5e1", + "sha256:423cde55430a348bda6f1021faad7235c2a95a6bdb749e34824e5758f755817a", + "sha256:4651ea05939374cfb5fe87aab5271ed38c31ea47997e17ec3834b75b94bd9f15", + "sha256:4be3bbfb2968d7da6e5c2cd4104fc5ec1caf9c0794f6cae724da5a53b4d9f5a3", + "sha256:622f7e40faef13d232fb52003661f2764ce6cdef3edb0a59af7c1559e4cc36d1", + "sha256:664dfd4384d886b239ef0d7ee5cff2b463831079d250528b10e394a322f141f9", + "sha256:697c0f58ac637b11991a1bc92e07c34da4a72e2eda34d317d2c1c47e2f24c1b3", + "sha256:6ec908b4c8a4faa7fe1a0080768e2ce733f268b287dfefb723273fb34141475f", + "sha256:7ec3fe795582b75bb49bb1685ffc462dbe38d74312dac07ce386671a28b5316b", + "sha256:8c39babd923c431dcf1e5874c0f778d3a5c745a62c3a9b6bd755efd489ee8a1d", + "sha256:949ca5bc56d6cb73d956f4862ba06ad3c5d2808eac76304284f53ae0c8b2334a", + "sha256:9f0daddeefb0791a600e6195441910bdf01eac470be596b9467e6122b51239a6", + "sha256:a359893b01c30e949eae0e8a85671a593364c9f0b8162afe0cb97317af0953bf", + "sha256:ad5d5d8efed59e6b1d4c50c1eac59fb6ecec91b2073676af1e15fc4d43e9b6c5", + "sha256:bc1a36f95a6b3667c09b34995fc3a46a82e4cf0dc3e7ab281e4c77b15bd7af05", + "sha256:be37b3f55b6d7d923f43bf74c356fc1878eb36e28505f38e198cb432c19c7b1a", + "sha256:c45bca5e544eb75f7500ffd730df72922eb878a2f0213b0dc5a5f357ded3a85d", + "sha256:ccee7ebbb4735ebc341d347fca9ee09f2fa6c0580528c1414bc4e1d31372835c", + "sha256:dc62c0840b2fc7753550b40405532a3e125c0d3761f34af948873393aa688160", + "sha256:f7d9d5aa1c7e54167f1a3cba36b5c52c7c540f30952c9bd7d9302a1eda318424" + ], + "version": "==4.2.3" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "numpy": { + "hashes": [ + "sha256:14fb76bde161c87dcec52d91c78f65aa8a23aa2e1530a71f412dabe03927d917", + "sha256:21041014b7529237994a6b578701c585703fbb3b1bea356cdb12a5ea7804241c", + "sha256:24f3bb9a5f6c3936a8ccd4ddfc1210d9511f4aeb879a12efd2e80bec647b8695", + "sha256:34033b581bc01b1135ca2e3e93a94daea7c739f21a97a75cca93e29d9f0c8e71", + "sha256:3fbccb399fe9095b1c1d7b41e7c7867db8aa0d2347fc44c87a7a180cedda112b", + "sha256:50718eea8e77a1bedcc85befd22c8dbf5a24c9d2c0c1e36bbb8d7a38da847eb3", + "sha256:55daf757e5f69aa75b4477cf4511bf1f96325c730e4ad32d954ccb593acd2585", + "sha256:61efc65f325770bbe787f34e00607bc124f08e6c25fdf04723848585e81560dc", + "sha256:62cb836506f40ce2529bfba9d09edc4b2687dd18c56cf4457e51c3e7145402fd", + "sha256:64c6acf5175745fd1b7b7e17c74fdbfb7191af3b378bc54f44560279f41238d3", + "sha256:674ea7917f0657ddb6976bd102ac341bc493d072c32a59b98e5b8c6eaa2d5ec0", + "sha256:73a816e441dace289302e04a7a34ec4772ed234ab6885c968e3ca2fc2d06fe2d", + "sha256:78c35dc7ad184aebf3714dbf43f054714c6e430e14b9c06c49a864fb9e262030", + "sha256:7f17efe9605444fcbfd990ba9b03371552d65a3c259fc2d258c24fb95afdd728", + "sha256:816645178f2180be257a576b735d3ae245b1982280b97ae819550ce8bcdf2b6b", + "sha256:924f37e66db78464b4b85ed4b6d2e5cda0c0416e657cac7ccbef14b9fa2c40b5", + "sha256:a17a8fd5df4fec5b56b4d11c9ba8b9ebfb883c90ec361628d07be00aaa4f009a", + "sha256:aaa519335a71f87217ca8a680c3b66b61960e148407bdf5c209c42f50fe30f49", + "sha256:ae3864816287d0e86ead580b69921daec568fe680857f07ee2a87bf7fd77ce24", + "sha256:b5f8c15cb9173f6cdf0f994955e58d1265331029ae26296232379461a297e5f2", + "sha256:c3ac359ace241707e5a48fe2922e566ac666aacacf4f8031f2994ac429c31344", + "sha256:c7c660cc0209fdf29a4e50146ca9ac9d8664acaded6b6ae2f5d0ae2e91a0f0cd", + "sha256:d690a2ff49f6c3bc35336693c9924fe5916be3cc0503fe1ea6c7e2bf951409ee", + "sha256:e2317cf091c2e7f0dacdc2e72c693cc34403ca1f8e3807622d0bb653dc978616", + "sha256:f28e73cf18d37a413f7d5de35d024e6b98f14566a10d82100f9dc491a7d449f9", + "sha256:f2a778dd9bb3e4590dbe3bbac28e7c7134280c4ec97e3bf8678170ee58c67b21", + "sha256:f5a758252502b466b9c2b201ea397dae5a914336c987f3a76c3741a82d43c96e", + "sha256:fb4c33a404d9eff49a0cdc8ead0af6453f62f19e071b60d283f9dc05581e4134" + ], + "markers": "python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7'", + "version": "==1.15.0" + }, + "pandas": { + "hashes": [ + "sha256:05ac350f8a35abe6a02054f8cf54e0c048f13423b2acb87d018845afd736f0b4", + "sha256:174543cd68eaee60620146b38faaed950071f5665e0a4fa4adfdcfc23d7f7936", + "sha256:1a62a237fb7223c11d09daaeaf7d15f234bb836bfaf3d4f85746cdf9b2582f99", + "sha256:2c1ed1de5308918a7c6833df6db75a19c416c122921824e306c64a0626b3606c", + "sha256:33825ad26ce411d6526f903b3d02c0edf627223af59cf4b5876aa925578eec74", + "sha256:4c5f76fce8a4851f65374ea1d95ca24e9439540550e41e556c0879379517a6f5", + "sha256:67504a96f72fb4d7f051cfe77b9a7bb0d094c4e2e5a6efb2769eb80f36e6b309", + "sha256:683e0cc8c7faececbbc06aa4735709a07abad106099f165730c1015da916adec", + "sha256:77cd1b485c6a860b950ab3a85be7b5683eaacbc51cadf096db967886607d2231", + "sha256:814f8785f1ab412a7e9b9a8abb81dfe8727ebdeef850ecfaa262c04b1664000f", + "sha256:894216edaf7dd0a92623cdad423bbec2a23fc06eb9c85483e21876d1ef8f47e9", + "sha256:9331e20a07360b81d8c7b4b50223da387d264151d533a5a5853325800e6631a4", + "sha256:9cd3614b4e31a0889388ff1bd19ae857ad52658b33f776065793c293a29cf612", + "sha256:9d79e958adcd037eba3debbb66222804171197c0f5cd462315d1356aa72a5a30", + "sha256:b90e5d5460f23607310cbd1688a7517c96ce7b284095a48340d249dfc429172e", + "sha256:bc80c13ffddc7e269b706ed58002cc4c98cc135c36d827c99fb5ca54ced0eb7a", + "sha256:cbb074efb2a5e4956b261a670bfc2126b0ccfbf5b96b6ed021bc8c8cb56cf4a8", + "sha256:e8c62ab16feeda84d4732c42b7b67d7a89ad89df7e99efed80ea017bdc472f26", + "sha256:ff5ef271805fe877fe0d1337b6b1861113c44c75b9badb595c713a72cd337371" + ], + "index": "pypi", + "version": "==0.23.3" + }, + "pillow": { + "hashes": [ + "sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a", + "sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72", + "sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574", + "sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd", + "sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2", + "sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c", + "sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273", + "sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554", + "sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612", + "sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934", + "sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786", + "sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27", + "sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b", + "sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62", + "sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9", + "sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710", + "sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3", + "sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4", + "sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b", + "sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f", + "sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e", + "sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b", + "sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a", + "sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f", + "sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea", + "sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7", + "sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4", + "sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333", + "sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109", + "sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab" + ], + "index": "pypi", + "version": "==5.2.0" + }, + "pyperclip": { + "hashes": [ + "sha256:f70e83d27c445795b6bf98c2bc826bbf2d0d63d4c7f83091c8064439042ba0dc" + ], + "index": "pypi", + "version": "==1.6.4" + }, + "pyquery": { + "hashes": [ + "sha256:07987c2ed2aed5cba29ff18af95e56e9eb04a2249f42ce47bddfb37f487229a3", + "sha256:4771db76bd14352eba006463656aef990a0147a0eeaf094725097acfa90442bf" + ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*'", + "version": "==1.4.0" + }, + "pytesseract": { + "hashes": [ + "sha256:9a9fae6331084f588c0cf2ad9ed50fca47e20429407e63389eb42d4e64460013" + ], + "index": "pypi", + "version": "==0.2.4" + }, + "python-dateutil": { + "hashes": [ + "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", + "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" + ], + "version": "==2.7.3" + }, + "python-xlib": { + "hashes": [ + "sha256:2ffa01fa51bdf53842fa4e3f9e2501f8147d4abf546a83e9c2b091982da2e1a8", + "sha256:c3deb8329038620d07b21be05673fa5a495dd8b04a2d9f4dca37a3811d192ae4" + ], + "version": "==0.23" + }, + "pytz": { + "hashes": [ + "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", + "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + ], + "version": "==2018.5" + }, + "pywinauto": { + "hashes": [ + "sha256:75fdfdea3f018c0efc9196cb184ecd14df8b35734889df9624610b8e74812807" + ], + "index": "pypi", + "version": "==0.6.4" + }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "index": "pypi", + "version": "==2.19.1" + }, + "rqopen-client": { + "hashes": [ + "sha256:9bda6a1ceac7453ff66ba0ee61ac56e1dd88bfcbd5eb27c98f49c460bf6d5ff7" + ], + "index": "pypi", + "version": "==0.0.5" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "markers": "python_version != '3.2.*' and python_version < '4' and python_version != '3.3.*' and python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==1.23" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "appnope": { + "hashes": [ + "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", + "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.0" + }, + "aspy.yaml": { + "hashes": [ + "sha256:04d26279513618f1024e1aba46471db870b3b33aef204c2d09bcf93bea9ba13f", + "sha256:0a77e23fafe7b242068ffc0252cee130d3e509040908fc678d9d1060e7494baa" + ], + "version": "==1.1.1" + }, + "astroid": { + "hashes": [ + "sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", + "sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" + ], + "version": "==2.0.1" + }, + "atomicwrites": { + "hashes": [ + "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", + "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" + ], + "version": "==1.1.5" + }, + "attrs": { + "hashes": [ + "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", + "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" + ], + "version": "==18.1.0" + }, + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, + "better-exceptions": { + "hashes": [ + "sha256:0a73efef96b48f867ea980227ac3b00d36a92754e6d316ad2ee472f136014580" + ], + "index": "pypi", + "version": "==0.2.1" + }, + "black": { + "hashes": [ + "sha256:22158b89c1a6b4eb333a1e65e791a3f8b998cf3b11ae094adb2570f31f769a44", + "sha256:4b475bbd528acce094c503a3d2dbc2d05a4075f6d0ef7d9e7514518e14cc5191" + ], + "index": "pypi", + "version": "==18.6b4" + }, + "cached-property": { + "hashes": [ + "sha256:630fdbf0f4ac7d371aa866016eba1c3ac43e9032246748d4994e67cb05f99bc4", + "sha256:f1f9028757dc40b4cb0fd2234bd7b61a302d7b84c683cb8c2c529238a24b8938" + ], + "version": "==1.4.3" + }, + "cfgv": { + "hashes": [ + "sha256:73f48a752bd7aab103c4b882d6596c6360b7aa63b34073dd2c35c7b4b8f93010", + "sha256:d1791caa9ff5c0c7bce80e7ecc1921752a2eb7c2463a08ed9b6c96b85a2f75aa" + ], + "version": "==1.1.0" + }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "index": "pypi", + "version": "==6.7" + }, + "coverage": { + "hashes": [ + "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", + "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", + "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", + "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", + "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", + "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", + "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", + "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", + "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", + "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", + "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", + "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", + "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", + "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", + "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", + "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", + "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", + "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", + "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", + "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", + "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", + "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", + "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", + "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", + "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", + "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", + "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", + "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", + "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", + "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", + "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" + ], + "markers": "python_version < '4' and python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==4.5.1" + }, + "decorator": { + "hashes": [ + "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", + "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + ], + "version": "==4.3.0" + }, + "identify": { + "hashes": [ + "sha256:49845e70fc6b1ec3694ab930a2c558912d7de24548eebcd448f65567dc757c43", + "sha256:68daab16a3db364fa204591f97dc40bfffd1a7739f27788a4895b4d8fd3516e5" + ], + "version": "==1.1.4" + }, + "ipython": { + "hashes": [ + "sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", + "sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c" + ], + "index": "pypi", + "version": "==6.4.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "index": "pypi", + "version": "==4.3.4" + }, + "jedi": { + "hashes": [ + "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", + "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f" + ], + "version": "==0.12.1" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", + "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", + "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" + ], + "version": "==4.2.0" + }, + "mypy": { + "hashes": [ + "sha256:673ea75fb750289b7d1da1331c125dc62fc1c3a8db9129bb372ae7b7d5bf300a", + "sha256:c770605a579fdd4a014e9f0a34b6c7a36ce69b08100ff728e96e27445cef3b3c" + ], + "index": "pypi", + "version": "==0.620" + }, + "nodeenv": { + "hashes": [ + "sha256:aa040ab5189bae17d272175609010be6c5b589ec4b8dbd832cc50c9e9cb7496f" + ], + "version": "==1.3.2" + }, + "parso": { + "hashes": [ + "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", + "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + ], + "version": "==0.3.1" + }, + "pexpect": { + "hashes": [ + "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", + "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.6.0" + }, + "pickleshare": { + "hashes": [ + "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", + "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5" + ], + "version": "==0.7.4" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", + "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", + "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==0.6.0" + }, + "pre-commit": { + "hashes": [ + "sha256:99cb6313a8ea7d88871aa2875a12d3c3a7636edf8ce4634b056328966682c8ce", + "sha256:c71e6cf84e812226f8dadbe346b5e6d6728fa65a364bbfe7624b219a18950540" + ], + "index": "pypi", + "version": "==1.10.4" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", + "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", + "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" + ], + "version": "==1.0.15" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", + "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" + ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==1.5.4" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "pylint": { + "hashes": [ + "sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", + "sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "pytest": { + "hashes": [ + "sha256:341ec10361b64a24accaec3c7ba5f7d5ee1ca4cebea30f76fad3dd12db9f0541", + "sha256:952c0389db115437f966c4c2079ae9d54714b9455190e56acebe14e8c38a7efa" + ], + "index": "pypi", + "version": "==3.6.4" + }, + "pytest-cov": { + "hashes": [ + "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", + "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" + ], + "index": "pypi", + "version": "==2.5.1" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, + "simplegeneric": { + "hashes": [ + "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" + ], + "version": "==0.8.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "toml": { + "hashes": [ + "sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d" + ], + "version": "==0.9.4" + }, + "traitlets": { + "hashes": [ + "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", + "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + ], + "version": "==4.3.2" + }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", + "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", + "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", + "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", + "version": "==1.1.0" + }, + "virtualenv": { + "hashes": [ + "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", + "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" + ], + "markers": "python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*'", + "version": "==16.0.0" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} diff --git a/cli.py b/cli.py deleted file mode 100644 index 43abf030..00000000 --- a/cli.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -import better_exceptions -import click -import dill - -import easytrader - -ACCOUNT_OBJECT_FILE = "account.session" - - -@click.command() -@click.option("--use", help="指定券商 [ht, yjb, yh]") -@click.option("--prepare", type=click.Path(exists=True), help="指定登录账户文件路径") -@click.option("--get", help="调用 easytrader 中对应的变量") -@click.option("--do", help="调用 easytrader 中对应的函数名") -@click.option("--debug", default=False, help="是否输出 easytrader 的 debug 日志") -@click.argument("params", nargs=-1) -def main(prepare, use, do, get, params, debug): - if get is not None: - do = get - if prepare is not None and use in [ - "ht_client", - "yjb", - "yh_client", - "yh", - "ht", - "gf", - "xq", - ]: - user = easytrader.use(use, debug) - user.prepare(prepare) - with open(ACCOUNT_OBJECT_FILE, "wb") as f: - dill.dump(user, f) - if do is not None: - with open(ACCOUNT_OBJECT_FILE, "rb") as f: - user = dill.load(f) - - if get is not None: - result = getattr(user, do) - else: - result = getattr(user, do)(*params) - - json_result = json.dumps( - result, indent=4, ensure_ascii=False, sort_keys=True - ) - click.echo(json_result) - - -if __name__ == "__main__": - main() diff --git a/easytrader/__init__.py b/easytrader/__init__.py index d14a93cb..07572ad6 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -from .api import * -from .webtrader import WebTrader -from .joinquant_follower import JoinQuantFollower -from .ricequant_follower import RiceQuantFollower +from .api import use, follower from . import exceptions __version__ = "0.15.2" diff --git a/easytrader/api.py b/easytrader/api.py index f1fc2b83..64811bc6 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -28,25 +28,27 @@ def use(broker, debug=True, **kwargs): """ if not debug: log.setLevel(logging.INFO) - elif broker.lower() in ["xq", "雪球"]: + if broker.lower() in ["xq", "雪球"]: return XueQiuTrader(**kwargs) - elif broker.lower() in ["yh_client", "银河客户端"]: + if broker.lower() in ["yh_client", "银河客户端"]: from .yh_clienttrader import YHClientTrader return YHClientTrader() - elif broker.lower() in ["ht_client", "华泰客户端"]: + if broker.lower() in ["ht_client", "华泰客户端"]: from .ht_clienttrader import HTClientTrader return HTClientTrader() - elif broker.lower() in ["gj_client", "国金客户端"]: + if broker.lower() in ["gj_client", "国金客户端"]: from .gj_clienttrader import GJClientTrader return GJClientTrader() - elif broker.lower() in ["ths", "同花顺客户端"]: + if broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader return ClientTrader() + raise NotImplementedError + def follower(platform, **kwargs): """用于生成特定的券商对象 @@ -72,3 +74,4 @@ def follower(platform, **kwargs): return JoinQuantFollower() if platform.lower() in ["xq", "xueqiu", "雪球"]: return XueQiuFollower(**kwargs) + raise NotImplementedError diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 0eeadb69..d736d5c6 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -7,9 +7,7 @@ import easyutils -from . import grid_data_get_strategy -from . import helpers -from . import pop_dialog_handler +from . import grid_data_get_strategy, helpers, pop_dialog_handler from .config import client if not sys.platform.startswith("darwin"): @@ -41,7 +39,7 @@ def wait(self, seconds: int): """Wait for operation return""" pass - @property + @property # type: ignore @abc.abstractmethod def grid_data_get_strategy(self): """ @@ -50,7 +48,7 @@ def grid_data_get_strategy(self): """ pass - @grid_data_get_strategy.setter + @grid_data_get_strategy.setter # type: ignore @abc.abstractmethod def grid_data_get_strategy(self, strategy_cls): """ @@ -67,7 +65,7 @@ def __init__(self): self._config = client.create(self.broker_type) self._app = None self._main = None - self.grid_data_get_strategy = grid_data_get_strategy.CopyStrategy + self._grid_data_get_strategy = grid_data_get_strategy.CopyStrategy @property def app(self): @@ -167,8 +165,7 @@ def cancel_entrust(self, entrust_no): ): self._cancel_entrust_by_double_click(i) return self._handle_pop_dialogs() - else: - return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} + return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} def buy(self, security, price, amount, **kwargs): self._switch_left_menus(["买入[F1]"]) @@ -301,9 +298,9 @@ def exit(self): def _close_prompt_windows(self): self.wait(1) - for w in self._app.windows(class_name="#32770"): - if w.window_text() != self._config.TITLE: - w.close() + for window in self._app.windows(class_name="#32770"): + if window.window_text() != self._config.TITLE: + window.close() self.wait(1) def trade(self, security, price, amount): @@ -384,7 +381,8 @@ def _get_left_menus_handle(self): # sometime can't find handle ready, must retry handle.wait("ready", 2) return handle - except: + # pylint: disable=broad-except + except Exception: pass def _cancel_entrust_by_double_click(self, row): diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 2d0e2e2e..69207789 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -2,17 +2,17 @@ def create(broker): if broker == "yh": return YH - elif broker == "ht": + if broker == "ht": return HT - elif broker == "gj": + if broker == "gj": return GJ - elif broker == "ths": + if broker == "ths": return CommonConfig - raise NotImplemented + raise NotImplementedError class CommonConfig: - DEFAULT_EXE_PATH = None + DEFAULT_EXE_PATH: str = "" TITLE = "网上股票交易系统5.0" TRADE_SECURITY_CONTROL_ID = 1032 diff --git a/easytrader/exceptions.py b/easytrader/exceptions.py index dacd5f15..8a838ef3 100644 --- a/easytrader/exceptions.py +++ b/easytrader/exceptions.py @@ -1,4 +1,9 @@ # -*- coding: utf-8 -*- +""" +Exceptions +""" + + class TradeError(IOError): pass diff --git a/easytrader/follower.py b/easytrader/follower.py index ff494d95..c887be13 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- +import abc import datetime import os import pickle +import queue import re import threading import time +from typing import List import requests -# noinspection PyUnresolvedReferences -from six.moves.queue import Queue - from . import exceptions from .log import log -class BaseFollower(object): +class BaseFollower(metaclass=abc.ABCMeta): LOGIN_PAGE = "" LOGIN_API = "" TRANSACTION_API = "" @@ -24,7 +24,7 @@ class BaseFollower(object): WEB_ORIGIN = "" def __init__(self): - self.trade_queue = Queue() + self.trade_queue = queue.Queue() self.expired_cmds = set() self.s = requests.Session() @@ -71,13 +71,13 @@ def check_login_success(self, rep): :raise 如果登录失败应该抛出 NotLoginError """ pass - def create_login_params(self, user, password, **kwargs): + def create_login_params(self, user, password, **kwargs) -> dict: """生成 post 登录接口的参数 :param user: 用户名 :param password: 密码 :return dict 登录参数的字典 """ - pass + return {} def follow( self, @@ -159,30 +159,30 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): transactions = self.query_strategy_transaction( strategy, **kwargs ) + # pylint: disable=broad-except except Exception as e: - log.warning("无法获取策略 {} 调仓信息, 错误: {}, 跳过此次调仓查询".format(name, e)) + log.warning("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e) continue - for t in transactions: + for transaction in transactions: trade_cmd = { "strategy": strategy, "strategy_name": name, - "action": t["action"], - "stock_code": t["stock_code"], - "amount": t["amount"], - "price": t["price"], - "datetime": t["datetime"], + "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 log.info( - "策略 [{}] 发送指令到交易队列, 股票: {} 动作: {} 数量: {} 价格: {} 信号产生时间: {}".format( - name, - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - ) + "策略 [%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) @@ -240,16 +240,15 @@ def _execute_trade_cmd( expire = (now - trade_cmd["datetime"]).total_seconds() if expire > expire_seconds: log.warning( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 超过设置的最大过期时间 {} 秒, 被丢弃".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - now, - expire_seconds, - ) + "策略 [%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 @@ -257,30 +256,28 @@ def _execute_trade_cmd( price = trade_cmd["price"] if not self._is_number(price) or price <= 0: log.warning( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 价格无效 , 被丢弃".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - now, - ) + "策略 [%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: log.warning( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {})超时,指令产生时间: {} 当前时间: {}, 买入股数无效 , 被丢弃".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - now, - ) + "策略 [%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 @@ -296,28 +293,26 @@ def _execute_trade_cmd( trader_name = type(user).__name__ err_msg = "{}: {}".format(type(e).__name__, e.args) log.error( - "{} 执行 策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 失败, 错误信息: {}".format( - trader_name, - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - err_msg, - ) + "%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s 指令产生时间: %s) 失败, 错误信息: %s", + trader_name, + trade_cmd["strategy_name"], + trade_cmd["stock_code"], + trade_cmd["action"], + trade_cmd["amount"], + trade_cmd["price"], + trade_cmd["datetime"], + err_msg, ) else: log.info( - "策略 [{}] 指令(股票: {} 动作: {} 数量: {} 价格: {} 指令产生时间: {}) 执行成功, 返回: {}".format( - trade_cmd["strategy_name"], - trade_cmd["stock_code"], - trade_cmd["action"], - trade_cmd["amount"], - trade_cmd["price"], - trade_cmd["datetime"], - response, - ) + "策略 [%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"], + response, ) def trade_worker( @@ -343,21 +338,21 @@ def query_strategy_transaction(self, strategy, **kwargs): self.project_transactions(transactions, **kwargs) return self.order_transactions_sell_first(transactions) - def extract_transactions(self, history): + def extract_transactions(self, history) -> List[str]: """ 抽取接口返回中的调仓记录列表 :param history: 调仓接口返回信息的字典对象 :return: [] 调参历史记录的列表 """ - pass + return [] - def create_query_transaction_params(self, strategy): + def create_query_transaction_params(self, strategy) -> dict: """ 生成用于查询调参记录的参数 :param strategy: 策略 id :return: dict 调参记录参数 """ - pass + return {} @staticmethod def re_find(pattern, string, dtype=str): @@ -374,9 +369,9 @@ def project_transactions(self, transactions, **kwargs): def order_transactions_sell_first(self, transactions): # 调整调仓记录的顺序为先卖再买 sell_first_transactions = [] - for t in transactions: - if t["action"] == "sell": - sell_first_transactions.insert(0, t) + for transaction in transactions: + if transaction["action"] == "sell": + sell_first_transactions.insert(0, transaction) else: - sell_first_transactions.append(t) + sell_first_transactions.append(transaction) return sell_first_transactions diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index 2576cf02..7fb4012c 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -6,8 +6,7 @@ import pywinauto import pywinauto.clipboard -from . import clienttrader -from . import helpers +from . import clienttrader, helpers class GJClientTrader(clienttrader.BaseLoginClientTrader): @@ -18,9 +17,11 @@ def broker_type(self): def login(self, user, password, exe_path, comm_password=None, **kwargs): """ 登陆客户端 + :param user: 账号 :param password: 明文密码 - :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' + :param exe_path: 客户端路径类似 'C:\\中国银河证券双子星3.2\\Binarystar.exe', + 默认 'C:\\中国银河证券双子星3.2\\Binarystar.exe' :param comm_password: 通讯密码, 华泰需要,可不设 :return: """ @@ -28,6 +29,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=1 ) + # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) @@ -52,10 +54,13 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): try: self._app.top_window().wait_not("exists", 5) break - except: + + # pylint: disable=broad-except + except Exception: self._app.top_window()["确定"].click() - pass - except Exception as e: + + # pylint: disable=broad-except + except Exception: pass self._app = pywinauto.Application().connect( diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py index d6670c2a..641cef84 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_data_get_strategy.py @@ -64,8 +64,9 @@ def _get_clipboard_data(self) -> str: while True: try: return pywinauto.clipboard.GetData() + # pylint: disable=broad-except except Exception as e: - log.warning("{}, retry ......".format(e)) + log.warning("%s, retry ......", e) class XlsStrategy(BaseStrategy): diff --git a/easytrader/helpers.py b/easytrader/helpers.py index bc80c5c6..dd5d560c 100644 --- a/easytrader/helpers.py +++ b/easytrader/helpers.py @@ -1,31 +1,13 @@ # -*- coding: utf-8 -*- import datetime import json +import random import re -import ssl -import uuid import requests -import six -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.poolmanager import PoolManager -from six.moves import input from . import exceptions -if six.PY2: - from io import open - - -class Ssl3HttpAdapter(HTTPAdapter): - def init_poolmanager(self, connections, maxsize, block=False): - self.poolmanager = PoolManager( - num_pools=connections, - maxsize=maxsize, - block=block, - ssl_version=ssl.PROTOCOL_TLSv1, - ) - def parse_cookies_str(cookies): """ @@ -71,20 +53,6 @@ def get_stock_type(stock_code): return "sz" -def ht_verify_code_new(image_path): - """显示图片,人肉读取,手工输入""" - - from PIL import Image - - img = Image.open(image_path) - img.show() - - # 关闭图片后输入答案 - s = input("input the pics answer :") - - return s - - def recognize_verify_code(image_path, broker="ht"): """识别验证码,返回识别后的字符串,使用 tesseract 实现 :param image_path: 图片路径 @@ -93,7 +61,7 @@ def recognize_verify_code(image_path, broker="ht"): if broker == "gf": return detect_gf_result(image_path) - elif broker in ["yh_client", "gj_client"]: + if broker in ["yh_client", "gj_client"]: return detect_yh_client_result(image_path) # 调用 tesseract 识别 return default_verify_code_detect(image_path) @@ -162,16 +130,6 @@ def invoke_tesseract_to_recognize(img): return "".join(valid_chars) -def get_mac(): - # 获取mac地址 link: http://stackoverflow.com/questions/28927958/python-get-mac-address - return ( - "".join( - c + "-" if i % 2 else c - for i, c in enumerate(hex(uuid.getnode())[2:].zfill(12)) - )[:-1] - ).upper() - - def grep_comma(num_str): return num_str.replace(",", "") @@ -199,11 +157,6 @@ def get_today_ipo_data(): :return: 今日可申购新股列表 apply_code申购代码 price发行价格 """ - import random - import json - import datetime - import requests - agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0" send_headers = { "Host": "xueqiu.com", @@ -217,13 +170,13 @@ def get_today_ipo_data(): "Connection": "keep-alive", } - sj = random.randint(1000000000000, 9999999999999) + timestamp = random.randint(1000000000000, 9999999999999) home_page_url = "https://xueqiu.com" ipo_data_url = ( "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl" "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_" "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" - % (str(sj)) + % (str(timestamp)) ) session = requests.session() diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index a3991364..87a9c2db 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -26,6 +26,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=1 ) + # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index 2221f3d8..1df5fcc8 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -67,7 +67,7 @@ def follow( strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: - log.error("抽取交易id和策略名失败, 无效的模拟交易url: {}".format(strategy_url)) + log.error("抽取交易id和策略名失败, 无效的模拟交易url: %s", strategy_url) raise strategy_worker = Thread( target=self.track_strategy_worker, @@ -76,7 +76,7 @@ def follow( ) strategy_worker.start() workers.append(strategy_worker) - log.info("开始跟踪策略: {}".format(strategy_name)) + log.info("开始跟踪策略: %s", strategy_name) for worker in workers: worker.join() @@ -107,18 +107,25 @@ def stock_shuffle_to_prefix(stock): code = stock[:6] if stock.find("XSHG") != -1: return "sh" + code - elif stock.find("XSHE") != -1: + + if stock.find("XSHE") != -1: return "sz" + code raise TypeError("not valid stock code: {}".format(code)) def project_transactions(self, transactions, **kwargs): - for t in transactions: - t["amount"] = self.re_find("\d+", t["amount"], dtype=int) + for transaction in transactions: + transaction["amount"] = self.re_find( + r"\d+", transaction["amount"], dtype=int + ) - time_str = "{} {}".format(t["date"], t["time"]) - t["datetime"] = datetime.strptime(time_str, "%Y-%m-%d %H:%M") + time_str = "{} {}".format(transaction["date"], transaction["time"]) + transaction["datetime"] = datetime.strptime( + time_str, "%Y-%m-%d %H:%M" + ) - stock = self.re_find(r"\d{6}\.\w{4}", t["stock"]) - t["stock_code"] = self.stock_shuffle_to_prefix(stock) + stock = self.re_find(r"\d{6}\.\w{4}", transaction["stock"]) + transaction["stock_code"] = self.stock_shuffle_to_prefix(stock) - t["action"] = "buy" if t["transaction"] == "买" else "sell" + transaction["action"] = ( + "buy" if transaction["transaction"] == "买" else "sell" + ) diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index d845879f..e264d5db 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -1,6 +1,7 @@ # coding:utf-8 import re import time +from typing import Optional from . import exceptions @@ -12,16 +13,16 @@ def __init__(self, app): def handle(self, title): if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): self._submit_by_shortcut() + return None - elif "提示" in title: + if "提示" in title: content = self._extract_content() self._submit_by_click() return {"message": content} - else: - content = self._extract_content() - self._close() - return {"message": "unknown message: {}".format(content)} + content = self._extract_content() + self._close() + return {"message": "unknown message: {}".format(content)} def _extract_content(self): return self._app.top_window().Static.window_text() @@ -40,26 +41,32 @@ def _close(self): class TradePopDialogHandler(PopDialogHandler): - def handle(self, title): + def handle(self, title) -> Optional[dict]: if title == "委托确认": self._submit_by_shortcut() + return None - elif title == "提示信息": + if title == "提示信息": content = self._extract_content() if "超出涨跌停" in content: self._submit_by_shortcut() - elif "委托价格的小数价格应为" in content: + return None + + if "委托价格的小数价格应为" in content: self._submit_by_shortcut() + return None + + return None - elif title == "提示": + if title == "提示": content = self._extract_content() if "成功" in content: entrust_no = self._extract_entrust_id(content) self._submit_by_click() return {"entrust_no": entrust_no} - else: - self._submit_by_click() - time.sleep(0.05) - raise exceptions.TradeError(content) - else: - self._close() + + self._submit_by_click() + time.sleep(0.05) + raise exceptions.TradeError(content) + self._close() + return None diff --git a/easytrader/ricequant_follower.py b/easytrader/ricequant_follower.py index 08bba729..dd1c2191 100644 --- a/easytrader/ricequant_follower.py +++ b/easytrader/ricequant_follower.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from datetime import datetime from threading import Thread @@ -9,7 +8,11 @@ class RiceQuantFollower(BaseFollower): - def login(self, user, password, **kwargs): + def __init__(self): + super().__init__() + self.client = None + + def login(self, user=None, password=None, **kwargs): from rqopen_client import RQOpenClient self.client = RQOpenClient(user, password, logger=log) @@ -34,7 +37,7 @@ def follow( :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足 """ users = self.warp_list(users) - run_id_list = self.warp_list(run_id) + run_ids = self.warp_list(run_id) if cmd_cache: self.load_expired_cmd_cache() @@ -44,16 +47,16 @@ def follow( ) workers = [] - for run_id in run_id_list: - strategy_name = self.extract_strategy_name(run_id) + for id_ in run_ids: + strategy_name = self.extract_strategy_name(id_) strategy_worker = Thread( target=self.track_strategy_worker, - args=[run_id, strategy_name], + args=[id_, strategy_name], kwargs={"interval": track_interval}, ) strategy_worker.start() workers.append(strategy_worker) - log.info("开始跟踪策略: {}".format(strategy_name)) + log.info("开始跟踪策略: %s", strategy_name) for worker in workers: worker.join() @@ -61,9 +64,9 @@ def extract_strategy_name(self, run_id): ret_json = self.client.get_positions(run_id) if ret_json["code"] != 200: log.error( - "fetch data from run_id {} fail, msg {}".format( - run_id, ret_json["msg"] - ) + "fetch data from run_id %s fail, msg %s", + run_id, + ret_json["msg"], ) raise RuntimeError(ret_json["msg"]) return ret_json["resp"]["name"] @@ -72,9 +75,9 @@ def extract_day_trades(self, run_id): ret_json = self.client.get_day_trades(run_id) if ret_json["code"] != 200: log.error( - "fetch day trades from run_id {} fail, msg {}".format( - run_id, ret_json["msg"] - ) + "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"] @@ -92,23 +95,25 @@ def stock_shuffle_to_prefix(stock): code = stock[:6] if stock.find("XSHG") != -1: return "sh" + code - elif stock.find("XSHE") != -1: + if stock.find("XSHE") != -1: return "sz" + code raise TypeError("not valid stock code: {}".format(code)) def project_transactions(self, transactions, **kwargs): new_transactions = [] - for t in transactions: - trans = {} - trans["price"] = t["price"] - trans["amount"] = int(abs(t["quantity"])) - trans["datetime"] = datetime.strptime( - t["time"], "%Y-%m-%d %H:%M:%S" + for transaction in transactions: + new_transaction = {} + new_transaction["price"] = transaction["price"] + new_transaction["amount"] = int(abs(transaction["quantity"])) + new_transaction["datetime"] = datetime.strptime( + transaction["time"], "%Y-%m-%d %H:%M:%S" + ) + new_transaction["stock_code"] = self.stock_shuffle_to_prefix( + transaction["order_book_id"] ) - trans["stock_code"] = self.stock_shuffle_to_prefix( - t["order_book_id"] + new_transaction["action"] = ( + "buy" if transaction["quantity"] > 0 else "sell" ) - trans["action"] = "buy" if t["quantity"] > 0 else "sell" - new_transactions.append(trans) + new_transactions.append(new_transaction) return new_transactions diff --git a/easytrader/server.py b/easytrader/server.py index 1b935388..7a8fe5c0 100644 --- a/easytrader/server.py +++ b/easytrader/server.py @@ -1,6 +1,6 @@ import functools -from flask import Flask, request, jsonify +from flask import Flask, jsonify, request from . import api from .log import log @@ -10,11 +10,12 @@ global_store = {} -def error_handle(f): - @functools.wraps(f) +def error_handle(func): + @functools.wraps(func) def wrapper(*args, **kwargs): try: - return f(*args, **kwargs) + return func(*args, **kwargs) + # pylint: disable=broad-except except Exception as e: log.exception("server error") message = "{}: {}".format(e.__class__, e) diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index dcbe4de6..d4a71670 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import abc import logging import os import re @@ -6,14 +7,14 @@ from threading import Thread import requests +import requests.exceptions -from . import exceptions -from . import helpers +from . import exceptions, helpers from .log import log # noinspection PyIncorrectDocstring -class WebTrader(object): +class WebTrader(metaclass=abc.ABCMeta): global_config_path = os.path.dirname(__file__) + "/config/global.json" config_path = "" @@ -32,9 +33,9 @@ def read_config(self, path): self.account_config = helpers.file2dict(path) except ValueError: log.error("配置文件格式有误,请勿使用记事本编辑,推荐 sublime text") - for v in self.account_config: - if type(v) is int: - log.warn("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题") + for value in self.account_config: + if isinstance(value, int): + log.warning("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题") def prepare(self, config_file=None, user=None, password=None, **kwargs): """登录的统一接口 @@ -94,9 +95,9 @@ def check_login(self, sleepy=30): self.check_account_live(response) except requests.exceptions.ConnectionError: pass - except Exception as e: + except requests.exceptions.RequestException as e: log.setLevel(self.log_level) - log.error("心跳线程发现账户出现错误: {} {}, 尝试重新登陆".format(e.__class__, e)) + log.error("心跳线程发现账户出现错误: %s %s, 尝试重新登陆", e.__class__, e) self.autologin() finally: log.setLevel(self.log_level) @@ -186,7 +187,8 @@ def do(self, params): response_data = self.request(request_params) try: format_json_data = self.format_response_data(response_data) - except: + # pylint: disable=broad-except + except Exception: # Caused by server force logged out return None return_data = self.fix_error_data(format_json_data) @@ -196,19 +198,19 @@ def do(self, params): self.autologin() return return_data - def create_basic_params(self): + def create_basic_params(self) -> dict: """生成基本的参数""" - pass + return {} - def request(self, params): + def request(self, params) -> dict: """请求并获取 JSON 数据 :param params: Get 参数""" - pass + return {} def format_response_data(self, data): """格式化返回的 json 数据 :param data: 请求返回的数据 """ - pass + return data def fix_error_data(self, data): """若是返回错误移除外层的列表 @@ -219,7 +221,9 @@ def format_response_data_type(self, response_data): """格式化返回的值为正确的类型 :param response_data: 返回的数据 """ - if type(response_data) is not list: + if isinstance(response_data, list) and not isinstance( + response_data, str + ): return response_data int_match_str = "|".join(self.config["response_format"]["int"]) diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index d4dd34e9..e504b971 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals, print_function, division +from __future__ import division, print_function, unicode_literals import json import re @@ -20,7 +20,9 @@ class XueQiuFollower(BaseFollower): WEB_REFERER = 'https://www.xueqiu.com' def __init__(self): - super(XueQiuFollower, self).__init__() + super().__init__() + self._adjust_sell = None + self._users = None def login(self, user=None, password=None, **kwargs): """ @@ -73,8 +75,7 @@ def follow(self, """ self._adjust_sell = adjust_sell - users = self.warp_list(users) - self._users = users + self._users = self.warp_list(users) strategies = self.warp_list(strategies) total_assets = self.warp_list(total_assets) @@ -83,7 +84,7 @@ def follow(self, if cmd_cache: self.load_expired_cmd_cache() - self.start_trader_thread(users, trade_cmd_expire_seconds) + 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): @@ -93,7 +94,7 @@ def follow(self, strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: - log.error('抽取交易id和策略名失败, 无效模拟交易url: {}'.format(strategy_url)) + log.error('抽取交易id和策略名失败, 无效模拟交易url: %s', strategy_url) raise strategy_worker = Thread( target=self.track_strategy_worker, @@ -103,7 +104,7 @@ def follow(self, 'assets': assets }) strategy_worker.start() - log.info('开始跟踪策略: {}'.format(strategy_name)) + log.info('开始跟踪策略: %s', strategy_name) def calculate_assets(self, strategy_url, @@ -147,27 +148,30 @@ def create_query_transaction_params(self, strategy): def none_to_zero(self, data): if data is None: return 0 - else: - return data + return data # noinspection PyMethodOverriding def project_transactions(self, transactions, assets): - for t in transactions: - weight_diff = self.none_to_zero(t['weight']) - self.none_to_zero( - t['prev_weight']) + for transaction in transactions: + weight_diff = self.none_to_zero( + transaction['weight']) - self.none_to_zero( + transaction['prev_weight']) - initial_amount = abs(weight_diff) / 100 * assets / t['price'] + initial_amount = abs(weight_diff) / 100 * assets / transaction[ + 'price'] - t['datetime'] = datetime.fromtimestamp(t['created_at'] // 1000) + transaction['datetime'] = datetime.fromtimestamp( + transaction['created_at'] // 1000) - t['stock_code'] = t['stock_symbol'].lower() + transaction['stock_code'] = transaction['stock_symbol'].lower() - t['action'] = 'buy' if weight_diff > 0 else 'sell' + transaction['action'] = 'buy' if weight_diff > 0 else 'sell' - t['amount'] = int(round(initial_amount, -2)) + transaction['amount'] = int(round(initial_amount, -2)) if self._adjust_sell: - t['amount'] = self._adjust_sell_amount(t['stock_code'], - t['amount']) + transaction['amount'] = self._adjust_sell_amount( + transaction['stock_code'], + transaction['amount']) def _adjust_sell_amount(self, stock_code, amount): """ @@ -189,8 +193,8 @@ def _adjust_sell_amount(self, stock_code, amount): try: stock = next(s for s in position if s['证券代码'] == stock_code) except StopIteration: - log.info('根据持仓调整 {} 卖出额,发现未持有股票 {}, 不做任何调整'.format( - stock_code, stock_code)) + log.info('根据持仓调整 %s 卖出额,发现未持有股票 %s, 不做任何调整', + stock_code, stock_code) return amount available_amount = stock['可用余额'] @@ -198,8 +202,8 @@ def _adjust_sell_amount(self, stock_code, amount): return amount adjust_amount = available_amount // 100 * 100 - log.info('股票 {} 实际可用余额 {}, 指令卖出股数为 {}, 调整为 {}'.format( - stock_code, available_amount, amount, adjust_amount)) + log.info('股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s', + stock_code, available_amount, amount, adjust_amount) return adjust_amount def _get_portfolio_info(self, portfolio_code): diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index e732e836..6da1362f 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -7,9 +7,7 @@ import requests -from . import exceptions -from . import helpers -from . import webtrader +from . import exceptions, helpers, webtrader from .log import log @@ -181,7 +179,8 @@ def _time_strftime(time_stamp): try: local_time = time.localtime(time_stamp / 1000) return time.strftime("%Y-%m-%d %H:%M:%S", local_time) - except: + # pylint: disable=broad-except + except Exception: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) def get_position(self): @@ -248,14 +247,6 @@ def get_entrust(self): else: status = "已成" for entrust in xq_entrusts["rebalancing_histories"]: - volume = ( - abs( - entrust["target_weight"] - - replace_none(entrust["prev_weight"]) - ) - * self.multiple - / 10000 - ) price = entrust["price"] entrust_list.append( { @@ -291,7 +282,7 @@ def cancel_entrust(self, entrust_no): for entrust in xq_entrusts["rebalancing_histories"]: if entrust["id"] == entrust_no and status == "pending": is_have = True - bs = ( + buy_or_sell = ( "buy" if entrust["target_weight"] < entrust["weight"] else "sell" @@ -310,7 +301,7 @@ def cancel_entrust(self, entrust_no): r = self._trade( security=entrust["stock_symbol"], volume=volume, - entrust_bs=bs, + entrust_bs=buy_or_sell, ) if len(r) > 0 and "error_info" in r[0]: raise exceptions.TradeError( @@ -373,7 +364,7 @@ def adjust_weight(self, stock_code, weight): remain_weight = 100 - sum(i.get("weight") for i in position_list) cash = round(remain_weight, 2) - log.debug("调仓比例:%f, 剩余持仓 :%f" % (weight, remain_weight)) + log.debug("调仓比例:%f, 剩余持仓 :%f", weight, remain_weight) data = { "cash": cash, "holdings": str(json.dumps(position_list)), @@ -384,22 +375,22 @@ def adjust_weight(self, stock_code, weight): try: resp = self.s.post(self.config["rebalance_url"], data=data) + # pylint: disable=broad-except except Exception as e: - log.warn("调仓失败: %s " % e) - return - else: - log.debug("调仓 %s: 持仓比例%d" % (stock["name"], weight)) - resp_json = json.loads(resp.text) - if "error_description" in resp_json and resp.status_code != 200: - log.error("调仓错误: %s" % (resp_json["error_description"])) - return [ - { - "error_no": resp_json["error_code"], - "error_info": resp_json["error_description"], - } - ] - else: - log.debug("调仓成功 %s: 持仓比例%d" % (stock["name"], weight)) + log.warning("调仓失败: %s ", e) + return None + log.debug("调仓 %s: 持仓比例%d", stock["name"], weight) + resp_json = json.loads(resp.text) + if "error_description" in resp_json and resp.status_code != 200: + log.error("调仓错误: %s", resp_json["error_description"]) + return [ + { + "error_no": resp_json["error_code"], + "error_info": resp_json["error_description"], + } + ] + log.debug("调仓成功 %s: 持仓比例%d", stock["name"], weight) + return None def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): """ @@ -487,7 +478,7 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): * 100 ) cash = round(cash, 2) - log.debug("weight:%f, cash:%f" % (weight, cash)) + log.debug("weight:%f, cash:%f", weight, cash) data = { "cash": cash, @@ -499,43 +490,41 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): try: resp = self.s.post(self.config["rebalance_url"], data=data) + # pylint: disable=broad-except except Exception as e: - log.warn("调仓失败: %s " % e) - return + log.warning("调仓失败: %s ", e) + return None else: log.debug( - "调仓 %s%s: %d" % (entrust_bs, stock["name"], resp.status_code) + "调仓 %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: - log.error("调仓错误: %s" % (resp_json["error_description"])) + log.error("调仓错误: %s", resp_json["error_description"]) return [ { "error_no": resp_json["error_code"], "error_info": resp_json["error_description"], } ] - else: - 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": "-", - } - ] + 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 buy(self, security, price=0, amount=0, volume=0, entrust_prop=0): """买入卖出股票 diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 81ab10ef..c3330ffe 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -4,9 +4,7 @@ import pywinauto -from . import clienttrader -from . import grid_data_get_strategy -from . import helpers +from . import clienttrader, grid_data_get_strategy, helpers class YHClientTrader(clienttrader.BaseLoginClientTrader): @@ -30,8 +28,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): 登陆客户端 :param user: 账号 :param password: 明文密码 - :param exe_path: 客户端路径类似 r'C:\中国银河证券双子星3.2\Binarystar.exe', - 默认 r'C:\中国银河证券双子星3.2\Binarystar.exe' + :param exe_path: 客户端路径类似 'C:\\中国银河证券双子星3.2\\Binarystar.exe', + 默认 'C:\\中国银河证券双子星3.2\\Binarystar.exe' :param comm_password: 通讯密码, 华泰需要,可不设 :return: """ @@ -39,6 +37,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=1 ) + # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) @@ -64,7 +63,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): try: self._app.top_window().wait_not("exists visible", 10) break - except: + # pylint: disable=broad-except + except Exception: pass self._app = pywinauto.Application().connect( @@ -76,7 +76,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._main.window(control_id=129, class_name="SysTreeView32").wait( "ready", 2 ) - except: + # pylint: disable=broad-except + except Exception: self.wait(2) self._switch_window_to_normal_mode() @@ -93,7 +94,7 @@ def _handle_verify_code(self): file_path = tempfile.mktemp() control.capture_as_image().save(file_path, "jpeg") verify_code = helpers.recognize_verify_code(file_path, "yh_client") - return "".join(re.findall("\d+", verify_code)) + return "".join(re.findall(r"\d+", verify_code)) @property def balance(self): diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..976ba029 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/requirements.txt b/requirements.txt index 6316789a..0e9f430d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,31 @@ -pywinauto -bs4 -requests -dill -click -six -flask -Pillow -pytesseract -pandas -pyperclip -rqopen-client>=0.0.5 -easyutils +-i http://mirrors.aliyun.com/pypi/simple/ +--trusted-host mirrors.aliyun.com +beautifulsoup4==4.6.0 +bs4==0.0.1 +certifi==2018.4.16 +chardet==3.0.4 +click==6.7 +cssselect==1.0.3; python_version != '3.3.*' +dill==0.2.8.2 +easyutils==0.1.7 +flask==1.0.2 +idna==2.7 +itsdangerous==0.24 +jinja2==2.10 +lxml==4.2.3 +markupsafe==1.0 +numpy==1.15.0; python_version >= '2.7' +pandas==0.23.3 +pillow==5.2.0 +pyperclip==1.6.4 +pyquery==1.4.0; python_version != '3.0.*' +pytesseract==0.2.4 +python-dateutil==2.7.3 +python-xlib==0.23 +pytz==2018.5 +pywinauto==0.6.4 +requests==2.19.1 +rqopen-client==0.0.5 +six==1.11.0 +urllib3==1.23; python_version != '3.1.*' +werkzeug==0.14.1 diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py index 529fe032..db336fdb 100644 --- a/tests/test_xq_follower.py +++ b/tests/test_xq_follower.py @@ -2,7 +2,7 @@ import unittest from unittest import mock -from easytrader import XueQiuFollower +from easytrader.xq_follower import XueQiuFollower class TestXueQiuTrader(unittest.TestCase): diff --git a/tests/test_xqtrader.py b/tests/test_xqtrader.py index ac322bd8..22abf274 100644 --- a/tests/test_xqtrader.py +++ b/tests/test_xqtrader.py @@ -1,7 +1,7 @@ # coding: utf-8 import unittest -from easytrader import XueQiuTrader +from easytrader.xqtrader import XueQiuTrader class TestXueQiuTrader(unittest.TestCase): From 5efa6c13bc34d36fe4d94971779b0aa35e7e58fd Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 9 Aug 2018 06:57:29 +0800 Subject: [PATCH 132/276] :star: remove support for python 3.4 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 04787538..1aada1a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - "3.4" - "3.5" - "3.6" From ec7af51a9379266efb0dedb9a2dd793ad9da5f0a Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 9 Aug 2018 07:44:40 +0800 Subject: [PATCH 133/276] :hammer: travis use pipenv --- .travis.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1aada1a0..b85e7215 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,11 @@ language: python python: - - "3.5" - "3.6" -env: - - PROJ=${PWD##*/} - install: - - pip install -r test-requirements.txt + - pip install pipenv + - pipenv install --dev --system script: - - pytest --cov=$PROJ -v tests + - pipenv run test From e60df4f4d5f370986e399d702c96bf2572649c4c Mon Sep 17 00:00:00 2001 From: zhoubeiqing Date: Fri, 10 Aug 2018 10:28:22 +0800 Subject: [PATCH 134/276] add sleep wait file save success (#299) * add sleep wait file save success * Update grid_data_get_strategy.py --- easytrader/grid_data_get_strategy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py index 641cef84..cc1972f4 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_data_get_strategy.py @@ -90,6 +90,8 @@ def get(self, control_id: int): # alt+s保存,alt+y替换已存在的文件 self._trader.app.top_window().type_keys("%{s}%{y}") + # Wait until file save complete otherwise pandas can not find file + self._trader.wait(0.3) return self._format_grid_data(temp_path) def _format_grid_data(self, data: str) -> dict: From af39125eb01bf346b09d6f72f1aa40f017e3dfc9 Mon Sep 17 00:00:00 2001 From: zhoubeiqing Date: Tue, 14 Aug 2018 10:35:51 +0800 Subject: [PATCH 135/276] =?UTF-8?q?=E9=93=B6=E6=B2=B3=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E4=BF=AE=E6=94=B9=E7=99=BB=E9=99=86=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E4=B8=BA=E7=A1=AE=E5=AE=9A,=E6=9B=B4=E6=96=B0=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E6=8E=A7=E4=BB=B6id,=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E6=88=AA=E5=9B=BE=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E8=BF=87=E5=B0=8F=E5=AF=BC=E8=87=B4=E5=8F=AA=E6=9C=89=E4=B8=89?= =?UTF-8?q?=E4=B8=AA=E6=95=B0=E5=AD=97=E8=A2=AB=E6=88=AA=E5=8F=96=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#300)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 银河客户端修改登陆按钮为确定,更新验证码控件id,修复验证码截图范围过小导致只有三个数字被截取问题 * 银河客户端修改登陆按钮为确定,更新验证码控件id,修复验证码截图范围过小导致只有三个数字被截取问题2 * 银河客户端修改登陆按钮为确定,更新验证码控件id,修复验证码截图范围过小导致只有三个数字被截取问题3 --- easytrader/grid_data_get_strategy.py | 4 ++-- easytrader/yh_clienttrader.py | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_data_get_strategy.py index cc1972f4..b82315c6 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_data_get_strategy.py @@ -86,12 +86,12 @@ def get(self, control_id: int): self._trader.app.top_window().type_keys(temp_path) # Wait until file save complete - self._trader.wait(0.5) + self._trader.wait(0.3) # alt+s保存,alt+y替换已存在的文件 self._trader.app.top_window().type_keys("%{s}%{y}") # Wait until file save complete otherwise pandas can not find file - self._trader.wait(0.3) + self._trader.wait(0.2) return self._format_grid_data(temp_path) def _format_grid_data(self, data: str) -> dict: diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index c3330ffe..694ab3ee 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -40,7 +40,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) - + is_xiadan=True if 'xiadan.exe' in exe_path else False # wait login window ready while True: try: @@ -51,13 +51,11 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) - while True: self._app.top_window().Edit3.type_keys( - self._handle_verify_code() + self._handle_verify_code(is_xiadan) ) - - self._app.top_window()["登录"].click() + self._app.top_window()["确定" if is_xiadan else "登陆"].click() # detect login is success or not try: @@ -65,7 +63,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break # pylint: disable=broad-except except Exception: - pass + is_xiadan and self._app.top_window()["确定"].click() self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=10 @@ -86,13 +84,18 @@ def _switch_window_to_normal_mode(self): control_id=32812, class_name="Button" ).click() - def _handle_verify_code(self): - control = self._app.top_window().window(control_id=22202) + def _handle_verify_code(self,is_xiadan): + control = self._app.top_window().window(control_id=1499 if is_xiadan else 22202) control.click() control.draw_outline() file_path = tempfile.mktemp() - control.capture_as_image().save(file_path, "jpeg") + if is_xiadan: + rect=control.element_info.rectangle + rect.right=round(rect.right+(rect.right-rect.left)*0.3)#扩展验证码控件截图范围为4个字符 + control.capture_as_image(rect).save(file_path, "jpeg") + else: + control.capture_as_image().save(file_path, "jpeg") verify_code = helpers.recognize_verify_code(file_path, "yh_client") return "".join(re.findall(r"\d+", verify_code)) From ec53abf5b9bdbe0aaec03e627d4997e1a27eba7d Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 15 Aug 2018 21:30:36 +0800 Subject: [PATCH 136/276] :star: add clienttrader add refresh api. close 297 --- docs/usage.md | 6 ++++++ easytrader/clienttrader.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 73dfc795..35bd6c9d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -281,6 +281,12 @@ print(ipo_data) user.exit() ``` +#### 刷新数据 + +``` +user.refresh() +``` + ### 远端服务器模式 #### 在服务器上启动服务 diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index d736d5c6..2d7371ae 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -39,6 +39,11 @@ def wait(self, seconds: int): """Wait for operation return""" pass + @abc.abstractmethod + def refresh(self): + """Refresh data""" + pass + @property # type: ignore @abc.abstractmethod def grid_data_get_strategy(self): @@ -151,13 +156,13 @@ def today_trades(self): @property def cancel_entrusts(self): - self._refresh() + self.refresh() self._switch_left_menus(["撤单[F3]"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) def cancel_entrust(self, entrust_no): - self._refresh() + self.refresh() for i, entrust in enumerate(self.cancel_entrusts): if ( entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] @@ -396,7 +401,7 @@ def _cancel_entrust_by_double_click(self, row): class_name="CVirtualGridCtrl", ).double_click(coords=(x, y)) - def _refresh(self): + def refresh(self): self._switch_left_menus(["买入[F1]"], sleep=0.05) def _handle_pop_dialogs( From b43605dc67f665cfacaf5b1e99c81802aad61710 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 15 Aug 2018 21:33:46 +0800 Subject: [PATCH 137/276] :star: yh_clienttrader support login by xiadan.exe --- easytrader/yh_clienttrader.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 694ab3ee..2ce579df 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -40,7 +40,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): # pylint: disable=broad-except except Exception: self._app = pywinauto.Application().start(exe_path) - is_xiadan=True if 'xiadan.exe' in exe_path else False + is_xiadan = True if "xiadan.exe" in exe_path else False # wait login window ready while True: try: @@ -55,7 +55,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().Edit3.type_keys( self._handle_verify_code(is_xiadan) ) - self._app.top_window()["确定" if is_xiadan else "登陆"].click() + self._app.top_window()["确定" if is_xiadan else "登录"].click() # detect login is success or not try: @@ -63,7 +63,8 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break # pylint: disable=broad-except except Exception: - is_xiadan and self._app.top_window()["确定"].click() + if is_xiadan: + self._app.top_window()["确定"].click() self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=10 @@ -84,15 +85,19 @@ def _switch_window_to_normal_mode(self): control_id=32812, class_name="Button" ).click() - def _handle_verify_code(self,is_xiadan): - control = self._app.top_window().window(control_id=1499 if is_xiadan else 22202) + def _handle_verify_code(self, is_xiadan): + control = self._app.top_window().window( + control_id=1499 if is_xiadan else 22202 + ) control.click() control.draw_outline() file_path = tempfile.mktemp() if is_xiadan: - rect=control.element_info.rectangle - rect.right=round(rect.right+(rect.right-rect.left)*0.3)#扩展验证码控件截图范围为4个字符 + rect = control.element_info.rectangle + rect.right = round( + rect.right + (rect.right - rect.left) * 0.3 + ) # 扩展验证码控件截图范围为4个字符 control.capture_as_image(rect).save(file_path, "jpeg") else: control.capture_as_image().save(file_path, "jpeg") From afe63042d30b99b773f0e7dfa0576371c2bfd6cc Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 15 Aug 2018 21:34:29 +0800 Subject: [PATCH 138/276] =?UTF-8?q?Bump=20version:=200.15.2=20=E2=86=92=20?= =?UTF-8?q?0.16.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 30c02fb3..b2a7e8b8 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.15.2 +current_version = 0.16.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 07572ad6..41666af5 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -2,5 +2,5 @@ from .api import use, follower from . import exceptions -__version__ = "0.15.2" +__version__ = "0.16.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 28496bdb..91462476 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.15.2", + version="0.16.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 90919981e0e034ee088b4565b9f63ed7edbca3a4 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 17 Aug 2018 22:23:05 +0800 Subject: [PATCH 139/276] :hammer: refactor gird_strategy interface --- docs/usage.md | 4 +- easytrader/api.py | 2 +- easytrader/clienttrader.py | 45 +++---------------- ...ata_get_strategy.py => grid_strategies.py} | 33 ++++++++------ easytrader/yh_clienttrader.py | 19 ++++---- 5 files changed, 38 insertions(+), 65 deletions(-) rename easytrader/{grid_data_get_strategy.py => grid_strategies.py} (76%) diff --git a/docs/usage.md b/docs/usage.md index 35bd6c9d..d89f4c68 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -103,9 +103,9 @@ user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' 使用方式如下: ```python -from easytrader import grid_data_get_strategy +from easytrader import grid_strategies -user.grid_data_get_strategy = grid_data_get_strategy.XlsStrategy +user.grid_strategy = grid_strategies.Xls ``` ### 交易相关 diff --git a/easytrader/api.py b/easytrader/api.py index 64811bc6..c53e8e22 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -10,7 +10,7 @@ from .xqtrader import XueQiuTrader if six.PY2: - raise TypeError("不支持 Python2,请升级 Python3 ") + raise TypeError("不支持 Python2,请升级 Python3") def use(broker, debug=True, **kwargs): diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 2d7371ae..b8ecbaa8 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -4,10 +4,11 @@ import os import sys import time +from typing import Type import easyutils -from . import grid_data_get_strategy, helpers, pop_dialog_handler +from . import grid_strategies, helpers, pop_dialog_handler from .config import client if not sys.platform.startswith("darwin"): @@ -35,7 +36,7 @@ def config(self): pass @abc.abstractmethod - def wait(self, seconds: int): + def wait(self, seconds: float): """Wait for operation return""" pass @@ -44,33 +45,15 @@ def refresh(self): """Refresh data""" pass - @property # type: ignore - @abc.abstractmethod - def grid_data_get_strategy(self): - """ - :return: Implement class of IGridDataGetStrategy - :rtype: grid_data.get_strategy.IGridDataGetStrategy - """ - pass - - @grid_data_get_strategy.setter # type: ignore - @abc.abstractmethod - def grid_data_get_strategy(self, strategy_cls): - """ - :param strategy_cls: Grid data get strategy - :type strategy_cls: grid_data.get_strategy.IGridDataGetStrategy - :return: formatted grid data - :rtype: list[dict] - """ - pass - class ClientTrader(IClientTrader): + # The strategy to use for getting grid data + grid_strategy: Type[grid_strategies.IGridStrategy] = grid_strategies.Copy + def __init__(self): self._config = client.create(self.broker_type) self._app = None self._main = None - self._grid_data_get_strategy = grid_data_get_strategy.CopyStrategy @property def app(self): @@ -84,20 +67,6 @@ def main(self): def config(self): return self._config - @property - def grid_data_get_strategy(self): - return self._grid_data_get_strategy - - @grid_data_get_strategy.setter - def grid_data_get_strategy(self, strategy_cls): - if not issubclass( - strategy_cls, grid_data_get_strategy.IGridDataGetStrategy - ): - raise TypeError( - "Strategy must be implement class of IGridDataGetStrategy" - ) - self._grid_data_get_strategy = strategy_cls(self) - def connect(self, exe_path=None, **kwargs): """ 直接连接登陆后的客户端 @@ -361,7 +330,7 @@ def _set_market_trade_params(self, security, amount): self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _get_grid_data(self, control_id): - return self._grid_data_get_strategy.get(control_id) + return self.grid_strategy(self).get(control_id) def _type_keys(self, control_id, text): self._main.window( diff --git a/easytrader/grid_data_get_strategy.py b/easytrader/grid_strategies.py similarity index 76% rename from easytrader/grid_data_get_strategy.py rename to easytrader/grid_strategies.py index b82315c6..e209b5cf 100644 --- a/easytrader/grid_data_get_strategy.py +++ b/easytrader/grid_strategies.py @@ -2,56 +2,61 @@ import abc import io import tempfile +from typing import TYPE_CHECKING, Dict, List import pandas as pd import pywinauto.clipboard from .log import log +if TYPE_CHECKING: + # pylint: disable=unused-import + from . import clienttrader -class IGridDataGetStrategy(abc.ABC): + +class IGridStrategy(abc.ABC): @abc.abstractmethod - def get(self, control_id: int): + def get(self, control_id: int) -> List[Dict]: """ + 获取 gird 数据并格式化返回 + :param control_id: grid 的 control id :return: grid 数据 - :rtype: List[Dict] """ pass -class BaseStrategy(IGridDataGetStrategy): - def __init__(self, trader): +class BaseStrategy(IGridStrategy): + def __init__(self, trader: "clienttrader.IClientTrader") -> None: self._trader = trader @abc.abstractmethod - def get(self, control_id: int): + def get(self, control_id: int) -> List[Dict]: """ :param control_id: grid 的 control id :return: grid 数据 - :rtype: list[dict] """ pass - def _get_grid(self, control_id): + def _get_grid(self, control_id: int): grid = self._trader.main.window( control_id=control_id, class_name="CVirtualGridCtrl" ) return grid -class CopyStrategy(BaseStrategy): +class Copy(BaseStrategy): """ 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 """ - def get(self, control_id: int): + def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) grid.type_keys("^A^C") content = self._get_clipboard_data() return self._format_grid_data(content) - def _format_grid_data(self, data: str) -> dict: + def _format_grid_data(self, data: str) -> List[Dict]: df = pd.read_csv( io.StringIO(data), delimiter="\t", @@ -69,13 +74,13 @@ def _get_clipboard_data(self) -> str: log.warning("%s, retry ......", e) -class XlsStrategy(BaseStrategy): +class Xls(BaseStrategy): """ 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, 用于绕过一些客户端不允许复制的限制 """ - def get(self, control_id: int): + def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) # ctrl+s 保存 grid 内容为 xls 文件 @@ -94,7 +99,7 @@ def get(self, control_id: int): self._trader.wait(0.2) return self._format_grid_data(temp_path) - def _format_grid_data(self, data: str) -> dict: + def _format_grid_data(self, data: str) -> List[Dict]: df = pd.read_csv( data, encoding="gbk", diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 2ce579df..d79e3a52 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -4,20 +4,19 @@ import pywinauto -from . import clienttrader, grid_data_get_strategy, helpers +from . import clienttrader, grid_strategies, helpers class YHClientTrader(clienttrader.BaseLoginClientTrader): - def __init__(self): - """ - Changelog: + """ + Changelog: - 2018.07.01: - 银河客户端 2018.5.11 更新后不再支持通过剪切板复制获取 Grid 内容, - 改为使用保存为 Xls 再读取的方式获取 - """ - super().__init__() - self.grid_data_get_strategy = grid_data_get_strategy.XlsStrategy + 2018.07.01: + 银河客户端 2018.5.11 更新后不再支持通过剪切板复制获取 Grid 内容, + 改为使用保存为 Xls 再读取的方式获取 + """ + + grid_strategy = grid_strategies.Xls @property def broker_type(self): From 7641a11c0eca72aec854d4c2969ae874504ad52e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 17 Aug 2018 22:26:59 +0800 Subject: [PATCH 140/276] =?UTF-8?q?Bump=20version:=200.16.0=20=E2=86=92=20?= =?UTF-8?q?0.17.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b2a7e8b8..6676efcf 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.16.0 +current_version = 0.17.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 41666af5..a9d14562 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -2,5 +2,5 @@ from .api import use, follower from . import exceptions -__version__ = "0.16.0" +__version__ = "0.17.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 91462476..0000f02c 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.16.0", + version="0.17.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From d46971e70f3a657575af9188c043d843847add7a Mon Sep 17 00:00:00 2001 From: lihz Date: Fri, 31 Aug 2018 17:06:13 +0800 Subject: [PATCH 141/276] bugfix: 1) sendkeys escape ~ in temp dir --- easytrader/grid_strategies.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index e209b5cf..67d59895 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -88,7 +88,7 @@ def get(self, control_id: int) -> List[Dict]: self._trader.wait(1) temp_path = tempfile.mktemp(suffix=".csv") - self._trader.app.top_window().type_keys(temp_path) + self._trader.app.top_window().type_keys(self.normalize_path(temp_path)) # Wait until file save complete self._trader.wait(0.3) @@ -99,6 +99,13 @@ def get(self, control_id: int) -> List[Dict]: self._trader.wait(0.2) return self._format_grid_data(temp_path) + def normalize_path(self, temp_path): + """ + :param temp_path :str + :return: str + """ + return temp_path.replace('~', '{~}') + def _format_grid_data(self, data: str) -> List[Dict]: df = pd.read_csv( data, From 9b1f3f74fedd3a1a6f619d3c3cc7297438a83718 Mon Sep 17 00:00:00 2001 From: zhangyichent Date: Fri, 31 Aug 2018 17:13:19 +0800 Subject: [PATCH 142/276] missing window in _set_market_trade_type (#303) Signed-off-by: zhangyichent --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index b8ecbaa8..5b1fef09 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -203,7 +203,7 @@ def market_trade(self, security, amount, ttype=None, **kwargs): def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" - selects = self._main( + selects = self._main.window( control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, class_name="ComboBox", ) From 10c17562e362236bffc94abc1487e68bd724457c Mon Sep 17 00:00:00 2001 From: shidenggui Date: Fri, 31 Aug 2018 17:17:02 +0800 Subject: [PATCH 143/276] :star: use type annotation --- easytrader/grid_strategies.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 67d59895..37c7810c 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -99,11 +99,7 @@ def get(self, control_id: int) -> List[Dict]: self._trader.wait(0.2) return self._format_grid_data(temp_path) - def normalize_path(self, temp_path): - """ - :param temp_path :str - :return: str - """ + def normalize_path(self, temp_path: str) -> str: return temp_path.replace('~', '{~}') def _format_grid_data(self, data: str) -> List[Dict]: From fdd4068a08118734374f0202ef9fb720d4ee529e Mon Sep 17 00:00:00 2001 From: lhztop Date: Fri, 31 Aug 2018 17:17:25 +0800 Subject: [PATCH 144/276] bugfix for sendkeys (#304) * bugfix: 1) sendkeys escape ~ in temp dir * :star: use type annotation --- easytrader/grid_strategies.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index e209b5cf..37c7810c 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -88,7 +88,7 @@ def get(self, control_id: int) -> List[Dict]: self._trader.wait(1) temp_path = tempfile.mktemp(suffix=".csv") - self._trader.app.top_window().type_keys(temp_path) + self._trader.app.top_window().type_keys(self.normalize_path(temp_path)) # Wait until file save complete self._trader.wait(0.3) @@ -99,6 +99,9 @@ def get(self, control_id: int) -> List[Dict]: self._trader.wait(0.2) return self._format_grid_data(temp_path) + def normalize_path(self, temp_path: str) -> str: + return temp_path.replace('~', '{~}') + def _format_grid_data(self, data: str) -> List[Dict]: df = pd.read_csv( data, From dcdb48cfd781dd406ff1b66af64155a1699728c1 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 7 Sep 2018 21:33:56 +0800 Subject: [PATCH 145/276] :star: follower support slippage --- easytrader/follower.py | 29 +++++++++++++++-- easytrader/xq_follower.py | 30 ++++++++++++------ tests/test_xq_follower.py | 67 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/easytrader/follower.py b/easytrader/follower.py index c887be13..e077044c 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -16,6 +16,10 @@ class BaseFollower(metaclass=abc.ABCMeta): + """ + slippage: 滑点,取值范围为 [0, 1] + """ + LOGIN_PAGE = "" LOGIN_API = "" TRANSACTION_API = "" @@ -29,6 +33,8 @@ def __init__(self): self.s = requests.Session() + self.slippage: float = 0.0 + def login(self, user=None, password=None, **kwargs): """ 登陆接口 @@ -86,9 +92,11 @@ def follow( track_interval=1, trade_cmd_expire_seconds=120, cmd_cache=True, + slippage: float = 0.0, **kwargs ): """跟踪平台对应的模拟交易,支持多用户多策略 + :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户 :param strategies: 雪球组合名, 类似 ZH123450 :param total_assets: 雪球组合对应的总资产, 格式 [ 组合1对应资金, 组合2对应资金 ] @@ -99,8 +107,22 @@ def follow( :param track_interval: 轮询模拟交易时间,单位为秒 :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒 :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令 + :param slippage: 滑点,0.0 表示无滑点, 0.05 表示滑点为 5% """ - raise NotImplementedError + self.slippage = slippage + + def _calculate_price_by_slippage(self, action: str, price: float) -> float: + """ + 计算考虑滑点之后的价格 + :param action: 交易动作, 支持 ['buy', 'sell'] + :param price: 原始交易价格 + :return: 考虑滑点后的交易价格 + """ + if action == "buy": + return price * (1 + self.slippage) + if action == "sell": + return price * (1 - self.slippage) + return price def load_expired_cmd_cache(self): if os.path.exists(self.CMD_CACHE_FILE): @@ -281,9 +303,12 @@ def _execute_trade_cmd( ) break + actual_price = self._calculate_price_by_slippage( + trade_cmd["action"], trade_cmd["price"] + ) args = { "security": trade_cmd["stock_code"], - "price": trade_cmd["price"], + "price": actual_price, "amount": trade_cmd["amount"], "entrust_prop": entrust_prop, } diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index e504b971..9a52166f 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -45,15 +45,17 @@ def login(self, user=None, password=None, **kwargs): log.info('登录成功') - def follow(self, - users, - strategies, - total_assets=10000, - initial_assets=None, - adjust_sell=False, - track_interval=10, - trade_cmd_expire_seconds=120, - cmd_cache=True): + def follow( # type: ignore + self, + users, + strategies, + total_assets=10000, + initial_assets=None, + adjust_sell=False, + track_interval=10, + trade_cmd_expire_seconds=120, + cmd_cache=True, + slippage: float = 0.0): """跟踪 joinquant 对应的模拟交易,支持多用户多策略 :param users: 支持 easytrader 的用户对象,支持使用 [] 指定多个用户 :param strategies: 雪球组合名, 类似 ZH123450 @@ -72,7 +74,15 @@ def follow(self, :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._users = self.warp_list(users) @@ -194,7 +204,7 @@ def _adjust_sell_amount(self, stock_code, amount): stock = next(s for s in position if s['证券代码'] == stock_code) except StopIteration: log.info('根据持仓调整 %s 卖出额,发现未持有股票 %s, 不做任何调整', - stock_code, stock_code) + stock_code, stock_code) return amount available_amount = stock['可用余额'] diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py index db336fdb..010218a8 100644 --- a/tests/test_xq_follower.py +++ b/tests/test_xq_follower.py @@ -1,4 +1,5 @@ # coding:utf-8 +import datetime import unittest from unittest import mock @@ -34,6 +35,72 @@ def test_adjust_sell_amount(self): amount = follower._adjust_sell_amount(stock_code, sell_amount) self.assertEqual(amount, excepted_amount) + def test_slippage_with_default(self): + follower = XueQiuFollower() + mock_user = mock.MagicMock() + + # test default no slippage + test_price = 1.0 + test_trade_cmd = { + "strategy": "test_strategy", + "strategy_name": "test_strategy", + "action": "buy", + "stock_code": "162411", + "amount": 100, + "price": 1.0, + "datetime": datetime.datetime.now(), + } + follower._execute_trade_cmd( + trade_cmd=test_trade_cmd, + users=[mock_user], + expire_seconds=10, + entrust_prop="limit", + send_interval=10, + ) + _, kwargs = getattr(mock_user, test_trade_cmd["action"]).call_args + self.assertAlmostEqual(kwargs["price"], test_price) + + def test_slippage(self): + follower = XueQiuFollower() + mock_user = mock.MagicMock() + + test_price = 1.0 + follower.slippage = 0.05 + + # test buy + test_trade_cmd = { + "strategy": "test_strategy", + "strategy_name": "test_strategy", + "action": "buy", + "stock_code": "162411", + "amount": 100, + "price": 1.0, + "datetime": datetime.datetime.now(), + } + follower._execute_trade_cmd( + trade_cmd=test_trade_cmd, + users=[mock_user], + expire_seconds=10, + entrust_prop="limit", + send_interval=10, + ) + excepted_price = test_price * (1 + follower.slippage) + _, kwargs = getattr(mock_user, test_trade_cmd["action"]).call_args + self.assertAlmostEqual(kwargs["price"], excepted_price) + + # test sell + test_trade_cmd["action"] = "sell" + follower._execute_trade_cmd( + trade_cmd=test_trade_cmd, + users=[mock_user], + expire_seconds=10, + entrust_prop="limit", + send_interval=10, + ) + excepted_price = test_price * (1 - follower.slippage) + _, kwargs = getattr(mock_user, test_trade_cmd["action"]).call_args + self.assertAlmostEqual(kwargs["price"], excepted_price) + TEST_POSITION = [ { From 5940c935091dea198efd96a0d32e17463133e1f7 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 7 Sep 2018 21:36:25 +0800 Subject: [PATCH 146/276] :star: add Makefile --- Makefile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..bde70097 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +test: + pytest -vx --cov=easytrader tests From 7a9358119b35f64f1cc96b40e43a850c21e1e096 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 7 Sep 2018 21:38:52 +0800 Subject: [PATCH 147/276] =?UTF-8?q?Bump=20version:=200.17.0=20=E2=86=92=20?= =?UTF-8?q?0.18.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6676efcf..1bbf69b9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.17.0 +current_version = 0.18.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index a9d14562..0a81f2ca 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -2,5 +2,5 @@ from .api import use, follower from . import exceptions -__version__ = "0.17.0" +__version__ = "0.18.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 0000f02c..24f56aa5 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.17.0", + version="0.18.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From af4b0b2256923137d4b200472c8a74263a5db4d5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 7 Sep 2018 21:41:57 +0800 Subject: [PATCH 148/276] :memo: update docs about follower slippage --- docs/usage.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index d89f4c68..11e416f9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -406,6 +406,11 @@ follower.follow(***, entrust_prop='market') ``` follower.follow(***, send_interval=30) # 设置下单间隔为 30 s ``` +#### 设置买卖时的滑点 + +``` +follower.follow(***, slippage=0.05) # 设置滑点为 5% +``` ### 命令行模式 From 737c78d8168de8bec1180c0d2b234e11c6b2a0da Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 8 Sep 2018 21:13:19 +0800 Subject: [PATCH 149/276] :bug: follower should print price include slippage --- easytrader/follower.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/easytrader/follower.py b/easytrader/follower.py index e077044c..8b6fca6b 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -318,24 +318,24 @@ def _execute_trade_cmd( trader_name = type(user).__name__ err_msg = "{}: {}".format(type(e).__name__, e.args) log.error( - "%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s 指令产生时间: %s) 失败, 错误信息: %s", + "%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 失败, 错误信息: %s", trader_name, trade_cmd["strategy_name"], trade_cmd["stock_code"], trade_cmd["action"], trade_cmd["amount"], - trade_cmd["price"], + actual_price, trade_cmd["datetime"], err_msg, ) else: log.info( - "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s 指令产生时间: %s) 执行成功, 返回: %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"], + actual_price, trade_cmd["datetime"], response, ) From f7148fafc86ba82fa9bc5c8b0ed8c91cb7a55a62 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 8 Sep 2018 21:13:56 +0800 Subject: [PATCH 150/276] =?UTF-8?q?Bump=20version:=200.18.0=20=E2=86=92=20?= =?UTF-8?q?0.18.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1bbf69b9..e48927fa 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.0 +current_version = 0.18.1 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 0a81f2ca..cd934942 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -2,5 +2,5 @@ from .api import use, follower from . import exceptions -__version__ = "0.18.0" +__version__ = "0.18.1" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 24f56aa5..c82f44ff 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.0", + version="0.18.1", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 619aa066103c9bc97fd2ee0099b2f26439f0a805 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 14 Sep 2018 20:54:25 +0800 Subject: [PATCH 151/276] :bug: follower should not adjust sell amount when buy stock --- easytrader/xq_follower.py | 2 +- tests/test_xq_follower.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index 9a52166f..4f0fd462 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -178,7 +178,7 @@ def project_transactions(self, transactions, assets): transaction['action'] = 'buy' if weight_diff > 0 else 'sell' transaction['amount'] = int(round(initial_amount, -2)) - if self._adjust_sell: + if transaction['action'] == 'sell' and self._adjust_sell: transaction['amount'] = self._adjust_sell_amount( transaction['stock_code'], transaction['amount']) diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py index 010218a8..079b8f8b 100644 --- a/tests/test_xq_follower.py +++ b/tests/test_xq_follower.py @@ -1,5 +1,6 @@ # coding:utf-8 import datetime +import time import unittest from unittest import mock @@ -17,6 +18,33 @@ def test_adjust_sell_amount_without_enable(self): amount = follower._adjust_sell_amount("169101", 1000) self.assertEqual(amount, amount) + def test_adjust_sell_should_only_work_when_sell(self): + follower = XueQiuFollower() + follower._adjust_sell = True + test_transaction = { + "weight": 10, + "prev_weight": 0, + "price": 10, + "stock_symbol": "162411", + "created_at": int(time.time() * 1000), + } + test_assets = 1000 + + mock_adjust_sell_amount = mock.MagicMock() + follower._adjust_sell_amount = mock_adjust_sell_amount + + follower.project_transactions( + transactions=[test_transaction], assets=test_assets + ) + mock_adjust_sell_amount.assert_not_called() + + mock_adjust_sell_amount.reset_mock() + test_transaction["prev_weight"] = test_transaction["weight"] + 1 + follower.project_transactions( + transactions=[test_transaction], assets=test_assets + ) + mock_adjust_sell_amount.assert_called() + def test_adjust_sell_amount(self): follower = XueQiuFollower() From e4b43042caf457962bd5068136c2e609f761d8fc Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 14 Sep 2018 20:56:06 +0800 Subject: [PATCH 152/276] =?UTF-8?q?Bump=20version:=200.18.1=20=E2=86=92=20?= =?UTF-8?q?0.18.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e48927fa..000057f9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.1 +current_version = 0.18.2 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index cd934942..8ba4996a 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -2,5 +2,5 @@ from .api import use, follower from . import exceptions -__version__ = "0.18.1" +__version__ = "0.18.2" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index c82f44ff..e158b6d3 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.1", + version="0.18.2", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 5b617501ab2e83ca6eb9120dca8e38fe2f96d9b4 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 26 Sep 2018 10:39:03 +0800 Subject: [PATCH 153/276] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa148361..67ff431f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ * 实现自动登录 * 支持通过 webserver 远程操作客户端 * 支持命令行调用,方便其他语言适配 -* 支持 Python3, Linux / Win, 推荐使用 `Python3` +* 支持 Python3, Win, 推荐使用 `Python3`。注: Linux 仅支持雪球 * 有兴趣的可以加群 `556050652` 一起讨论 * 捐助: From d75d949d4b3ab1710d2aecc2a1165d3a3a44b054 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 26 Sep 2018 10:40:26 +0800 Subject: [PATCH 154/276] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67ff431f..23f1b9a0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ * 实现自动登录 * 支持通过 webserver 远程操作客户端 * 支持命令行调用,方便其他语言适配 -* 支持 Python3, Win, 推荐使用 `Python3`。注: Linux 仅支持雪球 +* 基于 Python3, Win。注: Linux 仅支持雪球 * 有兴趣的可以加群 `556050652` 一起讨论 * 捐助: From a9248768b3ec84a13495d39267a6bf5390992d30 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 5 Nov 2018 08:28:11 +0800 Subject: [PATCH 155/276] :star: update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index aa148361..17b26bb5 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,6 @@ 注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,我在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供了几个券商老版本的下载地址。 -### 实盘易 - -如果有对其他券商或者通达信版本的需求,可以查看 [实盘易](http://www.iguuu.com/e?x=19828) - ### 模拟交易 * 雪球组合 by @[haogefeifei](https://github.com/haogefeifei)([说明](doc/xueqiu.md)) From 7523735fa632baf9829d2f1fec28c949c8dd80cd Mon Sep 17 00:00:00 2001 From: lihz Date: Tue, 13 Nov 2018 19:33:57 +0800 Subject: [PATCH 156/276] error handler --- easytrader/clienttrader.py | 17 +++++++++++++++-- easytrader/pop_dialog_handler.py | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index b8ecbaa8..80ef7cff 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -337,6 +337,11 @@ def _type_keys(self, control_id, text): control_id=control_id, class_name="Edit" ).set_edit_text(text) + def _collapse_left_menus(self): + items = self._get_left_menus_handle().roots() + for item in items: + item.collapse() + def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() self.wait(sleep) @@ -347,17 +352,22 @@ def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): @functools.lru_cache() def _get_left_menus_handle(self): + count = 10 while True: try: handle = self._main.window( control_id=129, class_name="SysTreeView32" ) + if count <= 0: + return handle # sometime can't find handle ready, must retry handle.wait("ready", 2) return handle # pylint: disable=broad-except - except Exception: + except Exception as ex: + print(ex) pass + count = count - 1 def _cancel_entrust_by_double_click(self, row): x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN @@ -379,7 +389,10 @@ def _handle_pop_dialogs( handler = handler_class(self._app) while self._is_exist_pop_dialog(): - title = self._get_pop_dialog_title() + try: + title = self._get_pop_dialog_title() + except pywinauto.findwindows.ElementNotFoundError: + return {"message": "success"} result = handler.handle(title) if result: diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index e264d5db..66e2b7cb 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -31,7 +31,10 @@ def _extract_entrust_id(self, content): return re.search(r"\d+", content).group() def _submit_by_click(self): - self._app.top_window()["确定"].click() + try: + self._app.top_window()["确定"].click() + except Exception as ex: + self._app.Window_(best_match='Dialog', top_level_only=True).ChildWindow(best_match='确定').click() def _submit_by_shortcut(self): self._app.top_window().type_keys("%Y") From f637309455ea6dd73b436e023de79e0eb8df5b60 Mon Sep 17 00:00:00 2001 From: lihz Date: Wed, 14 Nov 2018 14:32:27 +0800 Subject: [PATCH 157/276] modify to 2 --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 80ef7cff..cfc05181 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -352,7 +352,7 @@ def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): @functools.lru_cache() def _get_left_menus_handle(self): - count = 10 + count = 2 while True: try: handle = self._main.window( From ea16a093c8e5bc8bdc789af360684f473e2fcb54 Mon Sep 17 00:00:00 2001 From: lihz Date: Thu, 15 Nov 2018 16:02:12 +0800 Subject: [PATCH 158/276] add perf clock --- easytrader/__init__.py | 39 ++++++++++++++++++++++++++++++++ easytrader/clienttrader.py | 20 ++++++++++++++-- easytrader/pop_dialog_handler.py | 6 ++++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/easytrader/__init__.py b/easytrader/__init__.py index a9d14562..bff1efb7 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,6 +1,45 @@ # -*- coding: utf-8 -*- from .api import use, follower from . import exceptions +import jsonpickle __version__ = "0.17.0" __author__ = "shidenggui" + + +try: + from time import process_time +except: + from time import clock as process_time +import timeit +#from decorator import decorator +import logging + +internal_logger = logging.getLogger("PERF") + +# Decorator +def perf_clock(logger=None): + if logger is None: + logger = internal_logger + + def perf_decorator(method): + + #@decorator + def timed(*args, **kw): + ts = timeit.default_timer() + cs = process_time() + ex = None + result = None + try: + result = method(*args, **kw) + except Exception as ex1: + ex = ex1 + + te = timeit.default_timer() + ce = process_time() + logger.info('%r consume %2.4f sec, cpu %2.4f sec. args %s, extra args %s' % (method.__name__, te-ts, ce-cs, jsonpickle.dumps(args[1:]), jsonpickle.dumps(kw))) + if ex is not None: + raise ex + return result + return timed + return perf_decorator \ No newline at end of file diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index cfc05181..dfd7ccc7 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -5,6 +5,7 @@ import sys import time from typing import Type +from . import perf_clock import easyutils @@ -151,6 +152,7 @@ def sell(self, security, price, amount, **kwargs): return self.trade(security, price, amount) + @perf_clock() def market_buy(self, security, amount, ttype=None, **kwargs): """ 市价买入 @@ -254,6 +256,7 @@ def _click_grid_by_row(self, row): class_name="CVirtualGridCtrl", ).click(coords=(x, y)) + @perf_clock() def _is_exist_pop_dialog(self): self.wait(0.2) # wait dialog display return ( @@ -291,6 +294,7 @@ def _click(self, control_id): control_id=control_id, class_name="Button" ).click() + @perf_clock() def _submit_trade(self): time.sleep(0.05) self._main.window( @@ -298,12 +302,22 @@ def _submit_trade(self): class_name="Button", ).click() + @perf_clock() + def __get_top_window_pop_dialog(self): + return self._app.top_window().window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + + @perf_clock() def _get_pop_dialog_title(self): return ( - self._app.top_window() - .window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + self.__get_top_window_pop_dialog() .window_text() ) + # def _get_pop_dialog_title(self): + # return ( + # self._app.top_window() + # .window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + # .window_text() + # ) def _set_trade_params(self, security, price, amount): code = security[-6:] @@ -342,6 +356,7 @@ def _collapse_left_menus(self): for item in items: item.collapse() + @perf_clock() def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() self.wait(sleep) @@ -383,6 +398,7 @@ def _cancel_entrust_by_double_click(self, row): def refresh(self): self._switch_left_menus(["买入[F1]"], sleep=0.05) + @perf_clock() def _handle_pop_dialogs( self, handler_class=pop_dialog_handler.PopDialogHandler ): diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index 66e2b7cb..c4627ad6 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -3,13 +3,14 @@ import time from typing import Optional -from . import exceptions +from . import exceptions, perf_clock class PopDialogHandler: def __init__(self, app): self._app = app + @perf_clock() def handle(self, title): if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): self._submit_by_shortcut() @@ -27,6 +28,7 @@ def handle(self, title): def _extract_content(self): return self._app.top_window().Static.window_text() + @perf_clock() def _extract_entrust_id(self, content): return re.search(r"\d+", content).group() @@ -44,6 +46,8 @@ def _close(self): class TradePopDialogHandler(PopDialogHandler): + + @perf_clock() def handle(self, title) -> Optional[dict]: if title == "委托确认": self._submit_by_shortcut() From c19185c01b8902fb5f57d09ca4c25de9b5ba01d4 Mon Sep 17 00:00:00 2001 From: lihz Date: Fri, 16 Nov 2018 12:27:30 +0800 Subject: [PATCH 159/276] market buy sell --- easytrader/clienttrader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index dfd7ccc7..bcb07043 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -205,11 +205,11 @@ def market_trade(self, security, amount, ttype=None, **kwargs): def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" - selects = self._main( + selects = self._app.top_window().window( control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, class_name="ComboBox", ) - for i, text in selects.texts(): + for i, text in enumerate(selects.texts()): # skip 0 index, because 0 index is current select index if i == 0: continue From 9f2fbc8cd998bdf390e91233fc76f49cf2f0d92b Mon Sep 17 00:00:00 2001 From: lihz Date: Fri, 16 Nov 2018 19:44:24 +0800 Subject: [PATCH 160/276] add log --- easytrader/clienttrader.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index bcb07043..588015e2 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -106,18 +106,21 @@ def _get_balance_from_statics(self): ) return result + @perf_clock() @property def position(self): self._switch_left_menus(["查询[F4]", "资金股票"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + @perf_clock() @property def today_entrusts(self): self._switch_left_menus(["查询[F4]", "当日委托"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + @perf_clock() @property def today_trades(self): self._switch_left_menus(["查询[F4]", "当日成交"]) @@ -131,6 +134,7 @@ def cancel_entrusts(self): return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + @perf_clock() def cancel_entrust(self, entrust_no): self.refresh() for i, entrust in enumerate(self.cancel_entrusts): @@ -142,11 +146,13 @@ def cancel_entrust(self, entrust_no): return self._handle_pop_dialogs() return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} + @perf_clock() def buy(self, security, price, amount, **kwargs): self._switch_left_menus(["买入[F1]"]) return self.trade(security, price, amount) + @perf_clock() def sell(self, security, price, amount, **kwargs): self._switch_left_menus(["卖出[F2]"]) @@ -168,6 +174,7 @@ def market_buy(self, security, amount, ttype=None, **kwargs): return self.market_trade(security, amount, ttype) + @perf_clock() def market_sell(self, security, amount, ttype=None, **kwargs): """ 市价卖出 From b35a3c578ff67e6b5021fc6809c198e290848001 Mon Sep 17 00:00:00 2001 From: lihz Date: Mon, 19 Nov 2018 09:52:02 +0800 Subject: [PATCH 161/276] perf on property --- easytrader/clienttrader.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 588015e2..fbcc9c97 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -106,21 +106,18 @@ def _get_balance_from_statics(self): ) return result - @perf_clock() @property def position(self): self._switch_left_menus(["查询[F4]", "资金股票"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - @perf_clock() @property def today_entrusts(self): self._switch_left_menus(["查询[F4]", "当日委托"]) return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - @perf_clock() @property def today_trades(self): self._switch_left_menus(["查询[F4]", "当日成交"]) From 74f426a038c4f6951aa04064fe7bcdd3360bab7c Mon Sep 17 00:00:00 2001 From: lihz Date: Mon, 19 Nov 2018 10:02:24 +0800 Subject: [PATCH 162/276] bugfix: set params on market sell/buy when ttype --- easytrader/clienttrader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index fbcc9c97..a2c8fc93 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -216,7 +216,10 @@ def _set_market_trade_type(self, ttype): for i, text in enumerate(selects.texts()): # skip 0 index, because 0 index is current select index if i == 0: - continue + if ttype in text: # 当前已经选中 + break + else: + continue if ttype in text: selects.select(i - 1) break From 839322e1f10b6eacb0815e8852d0c80610f73033 Mon Sep 17 00:00:00 2001 From: lihz Date: Fri, 23 Nov 2018 10:54:55 +0800 Subject: [PATCH 163/276] bugfix: setforegroundwindow instead of setfocus --- easytrader/grid_strategies.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 37c7810c..5a0217c1 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -6,6 +6,8 @@ import pandas as pd import pywinauto.clipboard +from pywinauto.win32functions import SetForegroundWindow, ShowWindow +import pywinauto from .log import log @@ -79,12 +81,18 @@ class Xls(BaseStrategy): 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, 用于绕过一些客户端不允许复制的限制 """ + def _set_foreground(self): + if self._trader.main.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(self._trader.main.wrapper_object(), 9) # restore window state + else: + SetForegroundWindow(self._trader.main.wrapper_object()) # bring to front def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) # ctrl+s 保存 grid 内容为 xls 文件 - grid.type_keys("^s") + self._set_foreground() # setFocus buggy, instead of SetForegroundWindow + grid.type_keys("^s", set_foreground=False) self._trader.wait(1) temp_path = tempfile.mktemp(suffix=".csv") From 5219dcb7f6e419724cf5a40b694aad9ca7dad470 Mon Sep 17 00:00:00 2001 From: lhz Date: Fri, 23 Nov 2018 11:53:55 +0800 Subject: [PATCH 164/276] bugfix2 --- easytrader/grid_strategies.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 5a0217c1..e9ac1ec5 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -81,17 +81,19 @@ class Xls(BaseStrategy): 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, 用于绕过一些客户端不允许复制的限制 """ - def _set_foreground(self): - if self._trader.main.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized - ShowWindow(self._trader.main.wrapper_object(), 9) # restore window state + def _set_foreground(self, grid=None): + if grid is None: + grid = self._trader.main + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(grid.wrapper_object(), 9) # restore window state else: - SetForegroundWindow(self._trader.main.wrapper_object()) # bring to front + SetForegroundWindow(grid.wrapper_object()) # bring to front def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) # ctrl+s 保存 grid 内容为 xls 文件 - self._set_foreground() # setFocus buggy, instead of SetForegroundWindow + self._set_foreground(grid) # setFocus buggy, instead of SetForegroundWindow grid.type_keys("^s", set_foreground=False) self._trader.wait(1) From 690ed46fb4efe3ecd61d3147e55541feb827ebb1 Mon Sep 17 00:00:00 2001 From: lihz Date: Tue, 25 Dec 2018 11:31:44 +0800 Subject: [PATCH 165/276] modify type keys --- easytrader/clienttrader.py | 29 +++++++++++++++++++++-------- easytrader/grid_strategies.py | 31 ++++++++++++++++--------------- easytrader/pop_dialog_handler.py | 14 ++++++++++++-- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index a2c8fc93..2e9bddf2 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -11,6 +11,7 @@ from . import grid_strategies, helpers, pop_dialog_handler from .config import client +from pywinauto.win32functions import SetForegroundWindow, ShowWindow if not sys.platform.startswith("darwin"): import pywinauto @@ -56,6 +57,14 @@ def __init__(self): self._app = None self._main = None + def _set_foreground(self, grid=None): + if grid is None: + grid = self._trader.main + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(grid.wrapper_object(), 9) # restore window state + else: + SetForegroundWindow(grid.wrapper_object()) # bring to front + @property def app(self): return self._app @@ -265,7 +274,7 @@ def _click_grid_by_row(self, row): @perf_clock() def _is_exist_pop_dialog(self): - self.wait(0.2) # wait dialog display + self.wait(0.5) # wait dialog display return ( self._main.wrapper_object() != self._app.top_window().wrapper_object() @@ -303,7 +312,7 @@ def _click(self, control_id): @perf_clock() def _submit_trade(self): - time.sleep(0.05) + time.sleep(0.2) self._main.window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name="Button", @@ -329,31 +338,35 @@ def _get_pop_dialog_title(self): def _set_trade_params(self, security, price, amount): code = security[-6:] - self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) + self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish self.wait(0.1) - self._type_keys( + self._type_edit_control_keys( self._config.TRADE_PRICE_CONTROL_ID, easyutils.round_price_by_code(price, code), ) - self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) + self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _set_market_trade_params(self, security, amount): code = security[-6:] - self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) + self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish self.wait(0.1) - self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) + self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _get_grid_data(self, control_id): return self.grid_strategy(self).get(control_id) - def _type_keys(self, control_id, text): + def _type_common_control_keys(self, control, text): + self._set_foreground(control) + control.type_keys(text, set_foreground=False) + + def _type_edit_control_keys(self, control_id, text): self._main.window( control_id=control_id, class_name="Edit" ).set_edit_text(text) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index e9ac1ec5..af2f1b90 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -46,6 +46,14 @@ def _get_grid(self, control_id: int): ) return grid + def _set_foreground(self, grid=None): + if grid is None: + grid = self._trader.main + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(grid.wrapper_object(), 9) # restore window state + else: + SetForegroundWindow(grid.wrapper_object()) # bring to front + class Copy(BaseStrategy): """ @@ -54,7 +62,8 @@ class Copy(BaseStrategy): def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) - grid.type_keys("^A^C") + self._set_foreground(grid) + grid.type_keys("^A^C", set_foreground=False) content = self._get_clipboard_data() return self._format_grid_data(content) @@ -73,7 +82,7 @@ def _get_clipboard_data(self) -> str: return pywinauto.clipboard.GetData() # pylint: disable=broad-except except Exception as e: - log.warning("%s, retry ......", e) + log.exception("%s, retry ......", e) class Xls(BaseStrategy): @@ -81,13 +90,6 @@ class Xls(BaseStrategy): 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, 用于绕过一些客户端不允许复制的限制 """ - def _set_foreground(self, grid=None): - if grid is None: - grid = self._trader.main - if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized - ShowWindow(grid.wrapper_object(), 9) # restore window state - else: - SetForegroundWindow(grid.wrapper_object()) # bring to front def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) @@ -95,16 +97,15 @@ def get(self, control_id: int) -> List[Dict]: # ctrl+s 保存 grid 内容为 xls 文件 self._set_foreground(grid) # setFocus buggy, instead of SetForegroundWindow grid.type_keys("^s", set_foreground=False) - self._trader.wait(1) + self._trader.wait(0.5) temp_path = tempfile.mktemp(suffix=".csv") - self._trader.app.top_window().type_keys(self.normalize_path(temp_path)) - - # Wait until file save complete - self._trader.wait(0.3) + self._set_foreground(self._trader.app.top_window()) + self._trader.app.top_window().type_keys(self.normalize_path(temp_path), set_foreground=False) # alt+s保存,alt+y替换已存在的文件 - self._trader.app.top_window().type_keys("%{s}%{y}") + self._set_foreground(self._trader.app.top_window()) + 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) return self._format_grid_data(temp_path) diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index c4627ad6..e045841b 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -4,12 +4,21 @@ from typing import Optional from . import exceptions, perf_clock - +import pywinauto +from pywinauto.win32functions import SetForegroundWindow, ShowWindow class PopDialogHandler: def __init__(self, app): self._app = app + def _set_foreground(self, grid=None): + if grid is None: + grid = self._trader.main + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(grid.wrapper_object(), 9) # restore window state + else: + SetForegroundWindow(grid.wrapper_object()) # bring to front + @perf_clock() def handle(self, title): if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): @@ -39,7 +48,8 @@ def _submit_by_click(self): self._app.Window_(best_match='Dialog', top_level_only=True).ChildWindow(best_match='确定').click() def _submit_by_shortcut(self): - self._app.top_window().type_keys("%Y") + self._set_foreground(self._app.top_window()) + self._app.top_window().type_keys("%Y", set_foreground=False) def _close(self): self._app.top_window().close() From 1235fc92a823ec41d7151d6e7a53239a799bce62 Mon Sep 17 00:00:00 2001 From: lihz Date: Wed, 26 Dec 2018 14:34:57 +0800 Subject: [PATCH 166/276] add set foreground --- easytrader/ht_clienttrader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 87a9c2db..5483beb1 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import pywinauto import pywinauto.clipboard +from pywinauto.win32functions import SetForegroundWindow from . import clienttrader @@ -37,7 +38,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break except RuntimeError: pass - + SetForegroundWindow(self._app.top_window()) self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) From e2bc0df2e5d9d153e0c5367858ffbe2350f4de1a Mon Sep 17 00:00:00 2001 From: lhz Date: Wed, 26 Dec 2018 15:39:16 +0800 Subject: [PATCH 167/276] wrap --- easytrader/ht_clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 5483beb1..7bfb7b97 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -38,7 +38,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break except RuntimeError: pass - SetForegroundWindow(self._app.top_window()) + SetForegroundWindow(self._app.top_window().Edit1.wrapper_object()) self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) From aa10483877f9e75ad005229cd1199fcf4625944b Mon Sep 17 00:00:00 2001 From: lihz Date: Fri, 28 Dec 2018 10:38:37 +0800 Subject: [PATCH 168/276] guozhai --- easytrader/pop_dialog_handler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index e045841b..5b195dbb 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -73,6 +73,10 @@ def handle(self, title) -> Optional[dict]: self._submit_by_shortcut() return None + if "逆回购" in content: + self._submit_by_shortcut() + return None + return None if title == "提示": From c17c1f055bffafe60c783cabee4e1f3c913bcd1c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 16 Jan 2019 23:32:09 +0800 Subject: [PATCH 169/276] :pencli2: fix doc json format close #315 --- docs/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index 11e416f9..52537df3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -72,7 +72,7 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 ``` {  "user": "华泰用户名", -  "password": "华泰明文密码" +  "password": "华泰明文密码",  "comm_password": "华泰通讯密码" } From 289fdde29a894fb7a840f9327679a051b142f19b Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 21 Mar 2019 12:07:53 +0800 Subject: [PATCH 170/276] delete else --- easytrader/clienttrader.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 2e9bddf2..cd32460c 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -226,14 +226,13 @@ def _set_market_trade_type(self, ttype): # skip 0 index, because 0 index is current select index if i == 0: if ttype in text: # 当前已经选中 - break + return else: continue if ttype in text: selects.select(i - 1) - break - else: - raise TypeError("不支持对应的市价类型: {}".format(ttype)) + return + raise TypeError("不支持对应的市价类型: {}".format(ttype)) def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) From a719638c151239066989ecec1a726766868f21c1 Mon Sep 17 00:00:00 2001 From: lhz Date: Fri, 29 Mar 2019 08:24:29 +0800 Subject: [PATCH 171/276] modify --- easytrader/clienttrader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index cd32460c..734332b1 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -8,6 +8,7 @@ from . import perf_clock import easyutils +import re from . import grid_strategies, helpers, pop_dialog_handler from .config import client @@ -225,11 +226,11 @@ def _set_market_trade_type(self, ttype): for i, text in enumerate(selects.texts()): # skip 0 index, because 0 index is current select index if i == 0: - if ttype in text: # 当前已经选中 + if re.search(ttype, text): # 当前已经选中 return else: continue - if ttype in text: + if re.search(ttype, text): selects.select(i - 1) return raise TypeError("不支持对应的市价类型: {}".format(ttype)) From 7c908a159b59c177e374f18cc49a714014ffb8f5 Mon Sep 17 00:00:00 2001 From: lhz Date: Fri, 29 Mar 2019 08:26:41 +0800 Subject: [PATCH 172/276] test --- easytrader/ht_clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 7bfb7b97..1a2b7967 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -47,7 +47,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().button0.click() # detect login is success or not - self._app.top_window().wait_not("exists", 10) + self._app.top_window().wait_not("exists", 100) self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=10 From eea23d8c46854bdc56581ef0c1b880ce94d19a03 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 4 Apr 2019 09:42:03 +0800 Subject: [PATCH 173/276] :bug: fix xq follower cant get history --- .gitignore | 1 + easytrader/__init__.py | 6 ++- easytrader/follower.py | 4 +- easytrader/xq_follower.py | 10 ++++- easytrader/xqtrader.py | 1 + tests/test_xq_follower.py | 90 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 273558e6..9c30b1f3 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +cmd_cache.pk bak .mypy_cache .pyre diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 8ba4996a..01076ea9 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- -from .api import use, follower +import urllib3 + from . import exceptions +from .api import use, follower + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) __version__ = "0.18.2" __author__ = "shidenggui" diff --git a/easytrader/follower.py b/easytrader/follower.py index 8b6fca6b..ef1b517a 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -32,6 +32,7 @@ def __init__(self): self.expired_cmds = set() self.s = requests.Session() + self.s.verify = False self.slippage: float = 0.0 @@ -183,7 +184,8 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): ) # pylint: disable=broad-except except Exception as e: - log.warning("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e) + log.exception("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e) + time.sleep(3) continue for transaction in transactions: trade_cmd = { diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index 4f0fd462..260ffe9c 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -142,12 +142,18 @@ def extract_strategy_name(self, strategy_url): return rep.json()[info_index]['name'] def extract_transactions(self, history): - print(history) if history['count'] <= 0: return [] rebalancing_index = 0 - transactions = history['list'][rebalancing_index][ + raw_transactions = history['list'][rebalancing_index][ 'rebalancing_histories'] + transactions = [] + for transaction in raw_transactions: + if transaction['price'] is None: + log.info('该笔交易无法获取价格,疑似未成交,跳过。交易详情: %s', transaction) + continue + transactions.append(transaction) + return transactions def create_query_transaction_params(self, strategy): diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index 6da1362f..5f8f1c61 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -42,6 +42,7 @@ def __init__(self, **kwargs): raise ValueError("雪球初始资产不能小于1000元,当前预设值 {}".format(self.multiple)) self.s = requests.Session() + self.s.verify = False self.s.headers.update(self._HEADERS) self.account_config = None diff --git a/tests/test_xq_follower.py b/tests/test_xq_follower.py index 079b8f8b..dec9a690 100644 --- a/tests/test_xq_follower.py +++ b/tests/test_xq_follower.py @@ -1,5 +1,6 @@ # coding:utf-8 import datetime +import os import time import unittest from unittest import mock @@ -130,6 +131,19 @@ def test_slippage(self): self.assertAlmostEqual(kwargs["price"], excepted_price) +class TestXqFollower(unittest.TestCase): + def setUp(self): + self.follower = XueQiuFollower() + cookies = os.getenv("EZ_TEST_XQ_COOKIES") + if not cookies: + return + self.follower.login(cookies=cookies) + + def test_extract_transactions(self): + result = self.follower.extract_transactions(TEST_XQ_PORTOFOLIO_HISTORY) + self.assertTrue(len(result) == 1) + + TEST_POSITION = [ { "Unnamed: 14": "", @@ -148,3 +162,79 @@ def test_slippage(self): "证券代码": "169101", } ] + +TEST_XQ_PORTOFOLIO_HISTORY = { + "count": 1, + "page": 1, + "totalCount": 17, + "list": [ + { + "id": 1, + "status": "pending", + "cube_id": 1, + "prev_bebalancing_id": 1, + "category": "user_rebalancing", + "exe_strategy": "intraday_all", + "created_at": 1, + "updated_at": 1, + "cash_value": 0.1, + "cash": 100.0, + "error_code": "1", + "error_message": None, + "error_status": None, + "holdings": None, + "rebalancing_histories": [ + { + "id": 1, + "rebalancing_id": 1, + "stock_id": 1023662, + "stock_name": "华宝油气", + "stock_symbol": "SZ162411", + "volume": 0.0, + "price": None, + "net_value": 0.0, + "weight": 0.0, + "target_weight": 0.1, + "prev_weight": None, + "prev_target_weight": None, + "prev_weight_adjusted": None, + "prev_volume": None, + "prev_price": None, + "prev_net_value": None, + "proactive": True, + "created_at": 1554339333333, + "updated_at": 1554339233333, + "target_volume": 0.00068325, + "prev_target_volume": None, + }, + { + "id": 2, + "rebalancing_id": 1, + "stock_id": 1023662, + "stock_name": "华宝油气", + "stock_symbol": "SZ162411", + "volume": 0.0, + "price": 0.55, + "net_value": 0.0, + "weight": 0.0, + "target_weight": 0.1, + "prev_weight": None, + "prev_target_weight": None, + "prev_weight_adjusted": None, + "prev_volume": None, + "prev_price": None, + "prev_net_value": None, + "proactive": True, + "created_at": 1554339333333, + "updated_at": 1554339233333, + "target_volume": 0.00068325, + "prev_target_volume": None, + }, + ], + "comment": "", + "diff": 0.0, + "new_buy_count": 0, + } + ], + "maxPage": 17, +} From e6a90087eba655f9f17b08f4c6adce3eca665048 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 4 Apr 2019 09:42:29 +0800 Subject: [PATCH 174/276] =?UTF-8?q?Bump=20version:=200.18.2=20=E2=86=92=20?= =?UTF-8?q?0.18.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 000057f9..c3de12c0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.2 +current_version = 0.18.3 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 01076ea9..9550c620 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.18.2" +__version__ = "0.18.3" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index e158b6d3..146b58ad 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.2", + version="0.18.3", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 76b432910f5cab0999f2d20a01862f8701582ced Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 4 Apr 2019 10:19:59 +0800 Subject: [PATCH 175/276] close prompt --- easytrader/clienttrader.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 734332b1..eb4e43a3 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -210,7 +210,16 @@ def market_trade(self, security, amount, ttype=None, **kwargs): """ self._set_market_trade_params(security, amount) if ttype is not None: - self._set_market_trade_type(ttype) + retry = 0 + retry_max = 10 + while retry < retry_max: + try: + self._set_market_trade_type(ttype) + break + except: + retry += 1 + self.wait(0.1) + self._submit_trade() return self._handle_pop_dialogs( @@ -296,6 +305,12 @@ def _close_prompt_windows(self): window.close() self.wait(1) + def close_pormpt_window_no_wait(self): + for window in self._app.windows(class_name="#32770"): + if window.window_text() != self._config.TITLE: + window.close() + + def trade(self, security, price, amount): self._set_trade_params(security, price, amount) From ec6bafff0aca777993ce073f0799a80b5394bd1b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 5 Apr 2019 21:31:48 +0800 Subject: [PATCH 176/276] :star: upgrade pywinauto to 0.6.6 && refactor DeprecationWarning --- easytrader/clienttrader.py | 18 +++++++++--------- easytrader/grid_strategies.py | 4 ++-- easytrader/ht_clienttrader.py | 2 +- easytrader/yh_clienttrader.py | 10 +++++----- requirements.txt | 2 +- tests/test_easytrader.py | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 5b1fef09..30888807 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -99,7 +99,7 @@ def _get_balance_from_statics(self): result = {} for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): result[key] = float( - self._main.window( + self._main.child_window( control_id=control_id, class_name="Static" ).window_text() ) @@ -203,7 +203,7 @@ def market_trade(self, security, amount, ttype=None, **kwargs): def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" - selects = self._main.window( + selects = self._main.child_window( control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, class_name="ComboBox", ) @@ -249,7 +249,7 @@ def _click_grid_by_row(self, row): self._config.COMMON_GRID_FIRST_ROW_HEIGHT + self._config.COMMON_GRID_ROW_HEIGHT * row ) - self._app.top_window().window( + self._app.top_window().child_window( control_id=self._config.COMMON_GRID_CONTROL_ID, class_name="CVirtualGridCtrl", ).click(coords=(x, y)) @@ -287,13 +287,13 @@ def trade(self, security, price, amount): ) def _click(self, control_id): - self._app.top_window().window( + self._app.top_window().child_window( control_id=control_id, class_name="Button" ).click() def _submit_trade(self): time.sleep(0.05) - self._main.window( + self._main.child_window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name="Button", ).click() @@ -301,7 +301,7 @@ def _submit_trade(self): def _get_pop_dialog_title(self): return ( self._app.top_window() - .window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) .window_text() ) @@ -333,7 +333,7 @@ def _get_grid_data(self, control_id): return self.grid_strategy(self).get(control_id) def _type_keys(self, control_id, text): - self._main.window( + self._main.child_window( control_id=control_id, class_name="Edit" ).set_edit_text(text) @@ -349,7 +349,7 @@ def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): def _get_left_menus_handle(self): while True: try: - handle = self._main.window( + handle = self._main.child_window( control_id=129, class_name="SysTreeView32" ) # sometime can't find handle ready, must retry @@ -365,7 +365,7 @@ def _cancel_entrust_by_double_click(self, row): self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row ) - self._app.top_window().window( + self._app.top_window().child_window( control_id=self._config.COMMON_GRID_CONTROL_ID, class_name="CVirtualGridCtrl", ).double_click(coords=(x, y)) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 37c7810c..9949fd2c 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -39,7 +39,7 @@ def get(self, control_id: int) -> List[Dict]: pass def _get_grid(self, control_id: int): - grid = self._trader.main.window( + grid = self._trader.main.child_window( control_id=control_id, class_name="CVirtualGridCtrl" ) return grid @@ -100,7 +100,7 @@ def get(self, control_id: int) -> List[Dict]: return self._format_grid_data(temp_path) def normalize_path(self, temp_path: str) -> str: - return temp_path.replace('~', '{~}') + return temp_path.replace("~", "{~}") def _format_grid_data(self, data: str) -> List[Dict]: df = pd.read_csv( diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 87a9c2db..e39f856c 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -64,7 +64,7 @@ def _get_balance_from_statics(self): result = {} for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): result[key] = float( - self._main.window( + self._main.child_window( control_id=control_id, class_name="Static" ).window_text() ) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index d79e3a52..eebfb91b 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -71,21 +71,21 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._close_prompt_windows() self._main = self._app.window(title="网上股票交易系统5.0") try: - self._main.window(control_id=129, class_name="SysTreeView32").wait( - "ready", 2 - ) + self._main.child_window( + control_id=129, class_name="SysTreeView32" + ).wait("ready", 2) # pylint: disable=broad-except except Exception: self.wait(2) self._switch_window_to_normal_mode() def _switch_window_to_normal_mode(self): - self._app.top_window().window( + self._app.top_window().child_window( control_id=32812, class_name="Button" ).click() def _handle_verify_code(self, is_xiadan): - control = self._app.top_window().window( + control = self._app.top_window().child_window( control_id=1499 if is_xiadan else 22202 ) control.click() diff --git a/requirements.txt b/requirements.txt index 0e9f430d..5e6d3e4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ pytesseract==0.2.4 python-dateutil==2.7.3 python-xlib==0.23 pytz==2018.5 -pywinauto==0.6.4 +pywinauto==0.6.6 requests==2.19.1 rqopen-client==0.0.5 six==1.11.0 diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index b1edbec7..731ba660 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -119,4 +119,4 @@ def test_auto_ipo(self): if __name__ == "__main__": - unittest.main() + unittest.main(verbosity=2) From e5ae4daeda4ea125763a95b280dd694c7f68257d Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 5 Apr 2019 21:32:19 +0800 Subject: [PATCH 177/276] =?UTF-8?q?Bump=20version:=200.18.3=20=E2=86=92=20?= =?UTF-8?q?0.18.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c3de12c0..b3a39ac0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.3 +current_version = 0.18.4 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 9550c620..dea68363 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -6,5 +6,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.18.3" +__version__ = "0.18.4" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 146b58ad..f83c7d2e 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.3", + version="0.18.4", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 674340170c80d702d1b21f15bf070f0ce4b14146 Mon Sep 17 00:00:00 2001 From: zhoubeiqing Date: Mon, 20 May 2019 14:03:50 +0800 Subject: [PATCH 178/276] =?UTF-8?q?=E9=93=B6=E6=B2=B3=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E9=AA=8C=E8=AF=81=E7=A0=81=E5=8E=BB=E6=8E=89=E7=BB=98?= =?UTF-8?q?=E5=88=B6=E8=BE=B9=E6=A1=86=20(#332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close #331 --- easytrader/yh_clienttrader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index eebfb91b..856ce098 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -89,7 +89,6 @@ def _handle_verify_code(self, is_xiadan): control_id=1499 if is_xiadan else 22202 ) control.click() - control.draw_outline() file_path = tempfile.mktemp() if is_xiadan: From a61681fe81e0a107ce9bc0f728554da2bf88fddb Mon Sep 17 00:00:00 2001 From: zhoubeiqing Date: Sun, 26 May 2019 09:59:20 +0800 Subject: [PATCH 179/276] =?UTF-8?q?=20=E9=93=B6=E6=B2=B3=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E5=9C=A8=E6=93=8D=E4=BD=9C=E5=89=8D=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=88=B7=E6=96=B0,=E5=90=A6=E5=88=99=E5=BE=97=E5=88=B0?= =?UTF-8?q?=E7=9A=84=E5=80=BC=E6=B0=B8=E8=BF=9C=E6=98=AF=E4=B8=80=E6=A0=B7?= =?UTF-8?q?=E7=9A=84=20(#334)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 银河客户端验证码去掉绘制边框 * 银行客户端在操作前增加刷新,否则得到的值永远是一样的 --- easytrader/clienttrader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 30888807..755ceb90 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -339,6 +339,7 @@ def _type_keys(self, control_id, text): def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() + self._app.top_window().type_keys('{F5}') self.wait(sleep) def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): From 0330dfa7276365f19c89abf1897d5ca209cffdd8 Mon Sep 17 00:00:00 2001 From: lhz Date: Fri, 12 Jul 2019 13:51:32 +0800 Subject: [PATCH 180/276] ipo no kechuangban --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index eb4e43a3..7b66d6cc 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -252,7 +252,7 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} invalid_list_idx = [ - i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 + i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 or v["证券代码"][:2] == "78" ] if len(stock_list) == len(invalid_list_idx): From fcb85412d30e4de7ff55ff111f0ce5a6a2274d95 Mon Sep 17 00:00:00 2001 From: lhz Date: Wed, 31 Jul 2019 09:55:33 +0800 Subject: [PATCH 181/276] kcb --- easytrader/clienttrader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 7b66d6cc..00356201 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -252,7 +252,8 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} invalid_list_idx = [ - i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 or v["证券代码"][:2] == "78" + # i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 or v["证券代码"][:2] == "78" + i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 ] if len(stock_list) == len(invalid_list_idx): From 4e31e221d66098b2b10c5384561b517435b75936 Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 6 Aug 2019 16:33:36 +0800 Subject: [PATCH 182/276] auto test --- easytrader/ht_clienttrader.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 1a2b7967..d98432e9 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -7,6 +7,9 @@ class HTClientTrader(clienttrader.BaseLoginClientTrader): + + login_test_host: bool = True + @property def broker_type(self): return "ht" @@ -38,6 +41,17 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break except RuntimeError: pass + + if self.login_test_host: + self._app.top_window().type_keys("%t") + self.wait(0.5) + self._app.top_window().Button2.wait('enabled',timeout=20, retry_interval=1) + self._app.top_window().Button5.check() # enable 自动选择 + self.wait(0.5) + self._app.top_window().Button3.click() + self.wait(0.3) + + SetForegroundWindow(self._app.top_window().Edit1.wrapper_object()) self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) From ba6615ef67faa0a8c4ac5b12076e489aa480e56f Mon Sep 17 00:00:00 2001 From: zhoubeiqing Date: Wed, 14 Aug 2019 17:54:15 +0800 Subject: [PATCH 183/276] =?UTF-8?q?=E9=93=B6=E6=B2=B3=E5=8F=8C=E5=AD=90?= =?UTF-8?q?=E6=98=9F=E5=AE=A2=E6=88=B7=E7=AB=AF=E4=B8=80=E9=94=AE=E6=89=93?= =?UTF-8?q?=E6=96=B0=E6=97=A0=E9=9C=80=E7=82=B9=E5=87=BB=E5=85=A8=E9=80=89?= =?UTF-8?q?=20(#337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 银河双子星客户端一键打新无需点击全选 * quik commit without comment * quik commit without comment * quik commit without comment * quik commit without comment --- easytrader/clienttrader.py | 1 + easytrader/yh_clienttrader.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 755ceb90..f66e92e6 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -339,6 +339,7 @@ def _type_keys(self, control_id, text): def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() + self._app.top_window().type_keys('{ESC}') self._app.top_window().type_keys('{F5}') self.wait(sleep) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 856ce098..0a32ee73 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -105,5 +105,20 @@ def _handle_verify_code(self, is_xiadan): @property def balance(self): self._switch_left_menus(self._config.BALANCE_MENU_PATH) - return self._get_grid_data(self._config.BALANCE_GRID_CONTROL_ID) + def auto_ipo(self): + self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) + stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + if len(stock_list) == 0: + return {"message": "今日无新股"} + invalid_list_idx = [ + i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 + ] + if len(stock_list) == len(invalid_list_idx): + return {"message": "没有发现可以申购的新股"} + self.wait(0.1) + # for row in invalid_list_idx: + # self._click_grid_by_row(row) + self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) + self.wait(0.1) + return self._handle_pop_dialogs() From 2706cd07ba6713ca0e90b08f6a4ee2740f7b30d2 Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 15 Aug 2019 09:40:05 +0800 Subject: [PATCH 184/276] timeout --- easytrader/ht_clienttrader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index d98432e9..2d546136 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -45,7 +45,10 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): if self.login_test_host: self._app.top_window().type_keys("%t") self.wait(0.5) - self._app.top_window().Button2.wait('enabled',timeout=20, retry_interval=1) + try: + self._app.top_window().Button2.wait('enabled',timeout=30, retry_interval=1) + except: + pass self._app.top_window().Button5.check() # enable 自动选择 self.wait(0.5) self._app.top_window().Button3.click() From 775a9d919adfd72dc6a12d7f22016ead2dfd353a Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 8 Apr 2019 12:17:31 +0800 Subject: [PATCH 185/276] :memo: update docs for xq --- easytrader/webtrader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index d4a71670..a069699b 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -40,9 +40,9 @@ def read_config(self, path): def prepare(self, config_file=None, user=None, password=None, **kwargs): """登录的统一接口 :param config_file 登录数据文件,若无则选择参数登录模式 - :param user: 各家券商的账号或者雪球的用户名 - :param password: 密码, 券商为加密后的密码,雪球为明文密码 - :param account: [雪球登录需要]雪球手机号(邮箱手机二选一) + :param user: 各家券商的账号 + :param password: 密码, 券商为加密后的密码 + :param cookies: [雪球登录需要]雪球登录需要设置对应的 cookies :param portfolio_code: [雪球登录需要]组合代码 :param portfolio_market: [雪球登录需要]交易市场, 可选['cn', 'us', 'hk'] 默认 'cn' From eb4dc69f4ccdbd24e96597c3d48578a3efae36f9 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 17 Sep 2019 13:32:52 +0800 Subject: [PATCH 186/276] :star: update docs --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9481ad3d..79ddcc8b 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,16 @@ * 支持命令行调用,方便其他语言适配 * 基于 Python3, Win。注: Linux 仅支持雪球 * 有兴趣的可以加群 `556050652` 一起讨论 -* 捐助: - -![微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png?imageView2/1/w/300/h/300) ![支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png?imageView2/1/w/300/h/300) - ## 公众号 扫码关注“易量化”的微信公众号,不定时更新一些个人文章及与大家交流 -![](http://7xqo8v.com1.z0.glb.clouddn.com/easy_quant_qrcode.jpg?imageView2/1/w/300/h/300) +![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) +## 赞助: -**开发环境** : `Ubuntu 16.04` / `Python 3.5` +![微信](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/wechat_pay_qr.png) ![支付宝](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/alipay_qr.jpeg) ### 相关 @@ -53,4 +50,7 @@ ### 其他 -[软件实现原理](http://www.jisilu.cn/question/42707) +最近在开发一个[大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com),同时也将相关的功能集成到了对应的公众号,刚刚起步,如果大家对网文感兴趣的话欢迎关注交流 + +![推书君](https://raw.githubusercontent.com/shidenggui/assets/master/tuishujun/qr.jpeg) + From 4e137ec54c68baa410951956534d9076da490762 Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 19 Sep 2019 16:51:55 +0800 Subject: [PATCH 187/276] timeout --- easytrader/ht_clienttrader.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 2d546136..c7961afd 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -4,6 +4,7 @@ from pywinauto.win32functions import SetForegroundWindow from . import clienttrader +import logging class HTClientTrader(clienttrader.BaseLoginClientTrader): @@ -47,8 +48,9 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self.wait(0.5) try: self._app.top_window().Button2.wait('enabled',timeout=30, retry_interval=1) - except: - pass + except Exception as ex: + logging.exception(ex) + self._app.top_window().wrapper_object().close() self._app.top_window().Button5.check() # enable 自动选择 self.wait(0.5) self._app.top_window().Button3.click() From 94e885dd283c046a7f5a6c31797d3566436045d1 Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 19 Sep 2019 17:10:07 +0800 Subject: [PATCH 188/276] dd --- easytrader/ht_clienttrader.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 2d546136..73469e5e 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -4,7 +4,7 @@ from pywinauto.win32functions import SetForegroundWindow from . import clienttrader - +import logging class HTClientTrader(clienttrader.BaseLoginClientTrader): @@ -46,13 +46,15 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().type_keys("%t") self.wait(0.5) try: - self._app.top_window().Button2.wait('enabled',timeout=30, retry_interval=1) - except: - pass - self._app.top_window().Button5.check() # enable 自动选择 - self.wait(0.5) - self._app.top_window().Button3.click() - self.wait(0.3) + self._app.top_window().Button2.wait('enabled',timeout=30, retry_interval=5) + self._app.top_window().Button5.check() # enable 自动选择 + self.wait(0.5) + self._app.top_window().Button3.click() + self.wait(0.3) + except Exception as ex: + logging.exception("test speed error", ex) + self._app.top_window().wrapper_object().Close() + self.wait(0.3) SetForegroundWindow(self._app.top_window().Edit1.wrapper_object()) From e3be7aae601aa85257786b663cd7a3d38530bfb0 Mon Sep 17 00:00:00 2001 From: lhz Date: Mon, 23 Sep 2019 10:49:02 +0800 Subject: [PATCH 189/276] add kcb limit price when market trade --- easytrader/clienttrader.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 00356201..f91dc565 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -166,7 +166,7 @@ def sell(self, security, price, amount, **kwargs): return self.trade(security, price, amount) @perf_clock() - def market_buy(self, security, amount, ttype=None, **kwargs): + def market_buy(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价买入 :param security: 六位证券代码 @@ -174,15 +174,16 @@ def market_buy(self, security, amount, ttype=None, **kwargs): :param ttype: 市价委托类型,默认客户端默认选择, 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] + :param limit_price: 科创板 限价 :return: {'entrust_no': '委托单号'} """ self._switch_left_menus(["市价委托", "买入"]) - return self.market_trade(security, amount, ttype) + return self.market_trade(security, amount, ttype, limit_price=limit_price) @perf_clock() - def market_sell(self, security, amount, ttype=None, **kwargs): + def market_sell(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价卖出 :param security: 六位证券代码 @@ -190,14 +191,14 @@ def market_sell(self, security, amount, ttype=None, **kwargs): :param ttype: 市价委托类型,默认客户端默认选择, 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] - + :param limit_price: 科创板 限价 :return: {'entrust_no': '委托单号'} """ self._switch_left_menus(["市价委托", "卖出"]) - return self.market_trade(security, amount, ttype) + return self.market_trade(security, amount, ttype, limit_price=limit_price) - def market_trade(self, security, amount, ttype=None, **kwargs): + def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价交易 :param security: 六位证券代码 @@ -365,7 +366,7 @@ def _set_trade_params(self, security, price, amount): ) self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) - def _set_market_trade_params(self, security, amount): + def _set_market_trade_params(self, security, amount, limit_price=None): code = security[-6:] self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) @@ -374,6 +375,13 @@ def _set_market_trade_params(self, security, amount): self.wait(0.1) self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) + self.wait(0.1) + price_control = self._main.window( + control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit" + ) + if price_control is not None: + price_control.set_edit_text(limit_price) + def _get_grid_data(self, control_id): return self.grid_strategy(self).get(control_id) From 8c72ce413798611da531a40e4c6ad5845a9b545e Mon Sep 17 00:00:00 2001 From: lhz Date: Mon, 23 Sep 2019 14:39:04 +0800 Subject: [PATCH 190/276] client trader --- easytrader/clienttrader.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index f91dc565..1ca646ce 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -5,6 +5,9 @@ import sys import time from typing import Type + +from pywinauto import findwindows + from . import perf_clock import easyutils @@ -13,6 +16,7 @@ from . import grid_strategies, helpers, pop_dialog_handler from .config import client from pywinauto.win32functions import SetForegroundWindow, ShowWindow +import logging if not sys.platform.startswith("darwin"): import pywinauto @@ -286,10 +290,14 @@ def _click_grid_by_row(self, row): @perf_clock() def _is_exist_pop_dialog(self): self.wait(0.5) # wait dialog display - return ( - self._main.wrapper_object() - != self._app.top_window().wrapper_object() - ) + try: + return ( + self._main.wrapper_object() + != self._app.top_window().wrapper_object() + ) + except findwindows.ElementNotFoundError|pywinauto.timings.TimeoutError as ex: + logging.exception(ex) + return False def _run_exe_path(self, exe_path): return os.path.join(os.path.dirname(exe_path), "xiadan.exe") From 84d5da06e2df7a2b87aa13ba838a8c04985921ba Mon Sep 17 00:00:00 2001 From: lhz Date: Mon, 23 Sep 2019 14:46:20 +0800 Subject: [PATCH 191/276] kcb limit price --- easytrader/clienttrader.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 1ca646ce..83ffe491 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -384,9 +384,14 @@ def _set_market_trade_params(self, security, amount, limit_price=None): self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) self.wait(0.1) - price_control = self._main.window( - control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit" - ) + price_control = None + if str(security).startswith("68"): # 科创板存在限价 + try: + price_control = self._main.window( + control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit" + ) + except: + pass if price_control is not None: price_control.set_edit_text(limit_price) From 58e87f746ce100eb068722e783a41e0c67accb11 Mon Sep 17 00:00:00 2001 From: lhz Date: Mon, 23 Sep 2019 15:02:34 +0800 Subject: [PATCH 192/276] kcb --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 83ffe491..739219a7 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -213,7 +213,7 @@ def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs) :return: {'entrust_no': '委托单号'} """ - self._set_market_trade_params(security, amount) + self._set_market_trade_params(security, amount, limit_price=limit_price) if ttype is not None: retry = 0 retry_max = 10 From a2644282b5d078c6cf28c14dba3ba382a6217deb Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 15 Oct 2019 09:12:47 +0800 Subject: [PATCH 193/276] trader --- easytrader/clienttrader.py | 16 +++++--- easytrader/grid_strategies.py | 65 +++++++++++++++++++++++++++----- easytrader/ht_clienttrader.py | 7 ++-- easytrader/pop_dialog_handler.py | 2 +- easytrader/utils/__init__.py | 9 +++++ easytrader/utils/captcha.py | 20 ++++++++++ requirements.txt | 1 + 7 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 easytrader/utils/__init__.py create mode 100644 easytrader/utils/captcha.py diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 739219a7..92368801 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -15,7 +15,7 @@ from . import grid_strategies, helpers, pop_dialog_handler from .config import client -from pywinauto.win32functions import SetForegroundWindow, ShowWindow +from win32gui import SetForegroundWindow, ShowWindow import logging if not sys.platform.startswith("darwin"): @@ -54,6 +54,7 @@ def refresh(self): class ClientTrader(IClientTrader): + _editor_need_type_keys = True # The strategy to use for getting grid data grid_strategy: Type[grid_strategies.IGridStrategy] = grid_strategies.Copy @@ -295,7 +296,7 @@ def _is_exist_pop_dialog(self): self._main.wrapper_object() != self._app.top_window().wrapper_object() ) - except findwindows.ElementNotFoundError|pywinauto.timings.TimeoutError as ex: + except findwindows.ElementNotFoundError|pywinauto.timings.TimeoutError|RuntimeError as ex: logging.exception(ex) return False @@ -404,9 +405,14 @@ def _type_common_control_keys(self, control, text): control.type_keys(text, set_foreground=False) def _type_edit_control_keys(self, control_id, text): - self._main.window( - control_id=control_id, class_name="Edit" - ).set_edit_text(text) + if not self._editor_need_type_keys: + self._main.window( + control_id=control_id, class_name="Edit" + ).set_edit_text(text) + else: + self._main.window( + control_id=control_id, class_name="Edit" + ).type_keys(text) def _collapse_left_menus(self): items = self._get_left_menus_handle().roots() diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index af2f1b90..afee7103 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -2,14 +2,19 @@ import abc import io import tempfile + +from pywinauto import win32defines from typing import TYPE_CHECKING, Dict, List import pandas as pd import pywinauto.clipboard -from pywinauto.win32functions import SetForegroundWindow, ShowWindow +from .utils import SetForegroundWindow, ShowWindow import pywinauto +import logging from .log import log +from .utils.captcha import captcha_recognize + if TYPE_CHECKING: # pylint: disable=unused-import @@ -59,6 +64,7 @@ class Copy(BaseStrategy): """ 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 """ + _need_captcha_reg = True def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) @@ -68,23 +74,64 @@ def get(self, control_id: int) -> List[Dict]: return self._format_grid_data(content) def _format_grid_data(self, data: str) -> List[Dict]: - df = pd.read_csv( - io.StringIO(data), - delimiter="\t", - dtype=self._trader.config.GRID_DTYPE, - na_filter=False, - ) - return df.to_dict("records") + try: + df = pd.read_csv( + io.StringIO(data), + delimiter="\t", + dtype=self._trader.config.GRID_DTYPE, + na_filter=False, + ) + return df.to_dict("records") + except: + Copy._need_captcha_reg = True def _get_clipboard_data(self) -> str: - while True: + 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 + while count > 0: + self._trader.app.top_window().window(control_id=0x965, class_name='Static').\ + CaptureAsImage().save(file_path) # 保存验证码 + + captcha_num = captcha_recognize(file_path) # 识别验证码 + log.info("captcha result-->", captcha_num) + 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: + log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static', timeout=0.5).window_text()) + except: + break + count -= 1 + self._trader.wait(0.1) + 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 log.exception("%s, retry ......", e) +class WMCopy(Copy): + """ + 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 + """ + + def get(self, control_id: int) -> List[Dict]: + grid = self._get_grid(control_id) + grid.post_message(win32defines.WM_COMMAND, 0xe122, 0) + self._trader.wait(0.1) + content = self._get_clipboard_data() + return self._format_grid_data(content) + + + class Xls(BaseStrategy): """ 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index a775f4d4..0e5623f5 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import pywinauto import pywinauto.clipboard -from pywinauto.win32functions import SetForegroundWindow +from win32gui import SetForegroundWindow from . import clienttrader import logging @@ -23,6 +23,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): :param kwargs: :return: """ + self._editor_need_type_keys = False if comm_password is None: raise ValueError("华泰必须设置通讯密码") @@ -41,7 +42,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break except RuntimeError: pass - + self.login_test_host = False if self.login_test_host: self._app.top_window().type_keys("%t") self.wait(0.5) @@ -56,7 +57,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().wrapper_object().close() self.wait(0.3) - SetForegroundWindow(self._app.top_window().Edit1.wrapper_object()) + self._app.top_window().Edit1.set_focus() self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index 5b195dbb..5aa2b7f3 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -5,7 +5,7 @@ from . import exceptions, perf_clock import pywinauto -from pywinauto.win32functions import SetForegroundWindow, ShowWindow +from .utils import SetForegroundWindow, ShowWindow class PopDialogHandler: def __init__(self, app): diff --git a/easytrader/utils/__init__.py b/easytrader/utils/__init__.py new file mode 100644 index 00000000..ff52b97d --- /dev/null +++ b/easytrader/utils/__init__.py @@ -0,0 +1,9 @@ +from .captcha import captcha_recognize + +import win32gui + +def SetForegroundWindow(hwd): + win32gui.SetForegroundWindow(hwd._as_parameter_) + +def ShowWindow(hwd, window_status): + win32gui.ShowWindow(hwd._as_parameter_, window_status) \ No newline at end of file diff --git a/easytrader/utils/captcha.py b/easytrader/utils/captcha.py new file mode 100644 index 00000000..e3f81f24 --- /dev/null +++ b/easytrader/utils/captcha.py @@ -0,0 +1,20 @@ +import pytesseract +from PIL import Image + + +def captcha_recognize(img_path): + im = Image.open(img_path).convert("L") + # 1. threshold the image + threshold = 200 + table = [] + for i in range(256): + if i < threshold: + table.append(0) + else: + table.append(1) + + out = im.point(table, '1') + # out.show() + # 2. recognize with tesseract + num = pytesseract.image_to_string(out) + return num \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0e9f430d..6044b7d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +win32gui -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com beautifulsoup4==4.6.0 From dddffa49b1afc0c0baec876d46b5bdfe4a639ebb Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 15 Oct 2019 10:21:12 +0800 Subject: [PATCH 194/276] log info --- easytrader/grid_strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index afee7103..be39783f 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -95,7 +95,7 @@ def _get_clipboard_data(self) -> str: CaptureAsImage().save(file_path) # 保存验证码 captcha_num = captcha_recognize(file_path) # 识别验证码 - log.info("captcha result-->", captcha_num) + log.info("captcha result-->" + captcha_num) self._trader.app.top_window().window(control_id=0x964, class_name='Edit').set_text(captcha_num) # 模拟输入验证码 self._trader.app.top_window().set_focus() From 9a84a780f55bdfcb6331c176d525b8fb1b8bcc18 Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 15 Oct 2019 10:35:38 +0800 Subject: [PATCH 195/276] log --- easytrader/utils/captcha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/utils/captcha.py b/easytrader/utils/captcha.py index e3f81f24..6ec3b712 100644 --- a/easytrader/utils/captcha.py +++ b/easytrader/utils/captcha.py @@ -17,4 +17,4 @@ def captcha_recognize(img_path): # out.show() # 2. recognize with tesseract num = pytesseract.image_to_string(out) - return num \ No newline at end of file + return num From f5d33feae481569b1cc68031f5b8f80961b91ff9 Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 15 Oct 2019 13:05:57 +0800 Subject: [PATCH 196/276] click cancel --- easytrader/grid_strategies.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index be39783f..915c3ecd 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -90,9 +90,10 @@ def _get_clipboard_data(self) -> str: 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').\ - CaptureAsImage().save(file_path) # 保存验证码 + capture_as_image().save(file_path) # 保存验证码 captcha_num = captcha_recognize(file_path) # 识别验证码 log.info("captcha result-->" + captcha_num) @@ -103,9 +104,12 @@ def _get_clipboard_data(self) -> str: try: log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static', timeout=0.5).window_text()) except: + found = True break count -= 1 self._trader.wait(0.1) + if not found: + self._trader.app.top_window().Button2.click() # 点击取消 else: Copy._need_captcha_reg = False count = 5 From 7d3d234433b140654218afe3c3e3f70e5ebc1bae Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 15 Oct 2019 17:23:22 +0800 Subject: [PATCH 197/276] easy diff --- easytrader/grid_strategies.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 915c3ecd..453c04bb 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -97,17 +97,19 @@ def _get_clipboard_data(self) -> str: captcha_num = captcha_recognize(file_path) # 识别验证码 log.info("captcha result-->" + captcha_num) - 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: - log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static', timeout=0.5).window_text()) - except: - found = True - break + 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: + log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static', timeout=0.5).window_text()) + except: # 窗体消失 + 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: From 89166a2d92b14e6f52174525b6682289c92e4e3d Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 17 Oct 2019 11:34:10 +0800 Subject: [PATCH 198/276] log --- easytrader/grid_strategies.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 453c04bb..a48eead6 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -104,7 +104,8 @@ def _get_clipboard_data(self) -> str: pywinauto.keyboard.SendKeys("{ENTER}") # 模拟发送enter,点击确定 try: log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static', timeout=0.5).window_text()) - except: # 窗体消失 + except Exception as ex: # 窗体消失 + log.exception(ex) found = True break count -= 1 From 42ad1287d7dc2f45e5a741e87da77bbf0f917708 Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 17 Oct 2019 14:56:31 +0800 Subject: [PATCH 199/276] t --- easytrader/grid_strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index a48eead6..e5f66428 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -103,7 +103,7 @@ def _get_clipboard_data(self) -> str: self._trader.app.top_window().set_focus() pywinauto.keyboard.SendKeys("{ENTER}") # 模拟发送enter,点击确定 try: - log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static', timeout=0.5).window_text()) + log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static').window_text()) except Exception as ex: # 窗体消失 log.exception(ex) found = True From 89a53184a7014d5a39deb374bb8bba19f88a8bd3 Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 22 Oct 2019 15:38:15 +0800 Subject: [PATCH 200/276] gbk error --- easytrader/grid_strategies.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index e5f66428..096d42ca 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -11,6 +11,10 @@ from .utils import SetForegroundWindow, ShowWindow import pywinauto import logging +try: + import StringIO +except: + from io import StringIO from .log import log from .utils.captcha import captcha_recognize @@ -168,9 +172,12 @@ def normalize_path(self, temp_path: str) -> str: return temp_path.replace('~', '{~}') def _format_grid_data(self, data: str) -> List[Dict]: + f = open(data, encoding="gbk", errors='replace') + cont = f.read() + f.close() + df = pd.read_csv( - data, - encoding="gbk", + StringIO(cont), delimiter="\t", dtype=self._trader.config.GRID_DTYPE, na_filter=False, From d9a217abfab90b6c0b800be42f67209a6531b8cc Mon Sep 17 00:00:00 2001 From: samdeng2 <56903855+samdeng2@users.noreply.github.com> Date: Wed, 23 Oct 2019 12:09:00 +0800 Subject: [PATCH 201/276] Update joinquant_follower.py (#349) --- easytrader/joinquant_follower.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index 1df5fcc8..eb217329 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -27,7 +27,7 @@ def create_login_params(self, user, password, **kwargs): def check_login_success(self, rep): set_cookie = rep.headers["set-cookie"] - if len(set_cookie) < 100: + if len(set_cookie) < 50: raise exceptions.NotLoginError("登录失败,请检查用户名和密码") self.s.headers.update({"cookie": set_cookie}) From fbfe74c03054ed0fb7eae295c70da04d4d0d4d99 Mon Sep 17 00:00:00 2001 From: lhz Date: Wed, 30 Oct 2019 11:41:01 +0800 Subject: [PATCH 202/276] modify --- easytrader/grid_strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 096d42ca..adcd2e52 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -162,7 +162,7 @@ def get(self, control_id: int) -> List[Dict]: self._trader.app.top_window().type_keys(self.normalize_path(temp_path), set_foreground=False) # alt+s保存,alt+y替换已存在的文件 - self._set_foreground(self._trader.app.top_window()) + # self._set_foreground(self._trader.app.top_window()) 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) From 951c31be870b342b7d0e75d59eebfc44dc236878 Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 7 Nov 2019 18:34:55 +0800 Subject: [PATCH 203/276] commit --- easytrader/clienttrader.py | 7 ++++--- easytrader/grid_strategies.py | 15 +++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 92368801..fac6fff7 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -410,9 +410,10 @@ def _type_edit_control_keys(self, control_id, text): control_id=control_id, class_name="Edit" ).set_edit_text(text) else: - self._main.window( - control_id=control_id, class_name="Edit" - ).type_keys(text) + editor = self._main.window( + control_id=control_id, class_name="Edit") + editor.select() + editor.type_keys(text) def _collapse_left_menus(self): items = self._get_left_menus_handle().roots() diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 096d42ca..6b03a440 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -142,7 +142,6 @@ def get(self, control_id: int) -> List[Dict]: return self._format_grid_data(content) - class Xls(BaseStrategy): """ 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, @@ -159,13 +158,21 @@ def get(self, control_id: int) -> List[Dict]: temp_path = tempfile.mktemp(suffix=".csv") self._set_foreground(self._trader.app.top_window()) - self._trader.app.top_window().type_keys(self.normalize_path(temp_path), set_foreground=False) + # self._trader.app.top_window().type_keys(self.normalize_path(temp_path), set_foreground=False) + # alt+s保存,alt+y替换已存在的文件 - self._set_foreground(self._trader.app.top_window()) + # # self._set_foreground(self._trader.app.top_window()) + # self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False) + self._trader.app.top_window().Edit1.set_edit_text(self.normalize_path(temp_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) + # Wait until file save complete otherwise pandas can not find file + return self._format_grid_data(temp_path) def normalize_path(self, temp_path: str) -> str: From ce9b766e5bce353d7d29cfc25bc69c266a77ec8c Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 19 Nov 2019 17:56:12 +0800 Subject: [PATCH 204/276] trade --- easytrader/clienttrader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index fac6fff7..735b4592 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -314,6 +314,8 @@ def _close_prompt_windows(self): for window in self._app.windows(class_name="#32770"): if window.window_text() != self._config.TITLE: window.close() + logging.info("close " + window.window_text()) + self.wait(0.2) self.wait(1) def close_pormpt_window_no_wait(self): From 7695c9d5c3ba0dec24c1b0e222ddb9228f0d5c21 Mon Sep 17 00:00:00 2001 From: lhz Date: Tue, 19 Nov 2019 17:57:58 +0800 Subject: [PATCH 205/276] trade --- easytrader/clienttrader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 735b4592..f7724ed9 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -313,8 +313,9 @@ def _close_prompt_windows(self): self.wait(1) for window in self._app.windows(class_name="#32770"): if window.window_text() != self._config.TITLE: - window.close() logging.info("close " + window.window_text()) + window.close() + self.wait(0.2) self.wait(1) From 8d262f8610eae969922d43d027cac395cd6a8e2f Mon Sep 17 00:00:00 2001 From: lhz Date: Wed, 20 Nov 2019 16:36:32 +0800 Subject: [PATCH 206/276] log close --- easytrader/clienttrader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index f7724ed9..7e6e7559 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -312,10 +312,10 @@ def exit(self): def _close_prompt_windows(self): self.wait(1) for window in self._app.windows(class_name="#32770"): - if window.window_text() != self._config.TITLE: - logging.info("close " + window.window_text()) + title = window.window_text() + if title != self._config.TITLE: + logging.info("close " + title) window.close() - self.wait(0.2) self.wait(1) From 80c3d032fe17ed3fea1feed5a136f5099914b242 Mon Sep 17 00:00:00 2001 From: lhz Date: Wed, 20 Nov 2019 17:04:21 +0800 Subject: [PATCH 207/276] close_pormpt_window_no_wait --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 7e6e7559..88d32d30 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -311,7 +311,7 @@ def exit(self): def _close_prompt_windows(self): self.wait(1) - for window in self._app.windows(class_name="#32770"): + for window in self._app.windows(class_name="#32770", visible_only=True): title = window.window_text() if title != self._config.TITLE: logging.info("close " + title) From 18f2e3c2c56154d7a75d29c3ded7d65da3698669 Mon Sep 17 00:00:00 2001 From: lhz Date: Wed, 11 Dec 2019 08:40:48 +0800 Subject: [PATCH 208/276] error --- easytrader/clienttrader.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 9d44faa9..45ab9c23 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -404,8 +404,6 @@ def _set_market_trade_params(self, security, amount, limit_price=None): def _get_grid_data(self, control_id): return self.grid_strategy(self).get(control_id) - ).set_edit_text(text) - def _type_keys(self, control_id, text): self._main.child_window( control_id=control_id, class_name="Edit" @@ -414,7 +412,6 @@ def _type_keys(self, control_id, text): def _type_common_control_keys(self, control, text): self._set_foreground(control) control.type_keys(text, set_foreground=False) - ).set_edit_text(text) def _type_edit_control_keys(self, control_id, text): if not self._editor_need_type_keys: From 99f205c468cebb8d0bf8d48887ee9811816cb3d7 Mon Sep 17 00:00:00 2001 From: lhz Date: Wed, 11 Dec 2019 08:43:03 +0800 Subject: [PATCH 209/276] client --- easytrader/clienttrader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 45ab9c23..f48c2dde 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -405,9 +405,7 @@ def _get_grid_data(self, control_id): return self.grid_strategy(self).get(control_id) def _type_keys(self, control_id, text): - self._main.child_window( - control_id=control_id, class_name="Edit" - ).set_edit_text(text) + self._main.child_window(control_id=control_id, class_name="Edit").set_edit_text(text) def _type_common_control_keys(self, control, text): self._set_foreground(control) From fd34783df37e1fa5da7044f908a4591224dd30a6 Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 12 Dec 2019 13:15:16 +0800 Subject: [PATCH 210/276] delete shortcuts --- easytrader/clienttrader.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index f48c2dde..50ec87dc 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -54,9 +54,16 @@ def refresh(self): class ClientTrader(IClientTrader): - _editor_need_type_keys = True + _editor_need_type_keys = False # The strategy to use for getting grid data grid_strategy: Type[grid_strategies.IGridStrategy] = grid_strategies.Copy + _grid_strategy_instance: grid_strategies.IGridStrategy = None + + @property + def grid_strategy_instance(self): + if self._grid_strategy_instance is None: + self._grid_strategy_instance = self.grid_strategy(self) + return self._grid_strategy_instance def __init__(self): self._config = client.create(self.broker_type) @@ -392,7 +399,7 @@ def _set_market_trade_params(self, security, amount, limit_price=None): price_control = None if str(security).startswith("68"): # 科创板存在限价 try: - price_control = self._main.window( + price_control = self._main.child_window( control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit" ) except: @@ -402,7 +409,7 @@ def _set_market_trade_params(self, security, amount, limit_price=None): def _get_grid_data(self, control_id): - return self.grid_strategy(self).get(control_id) + return self.grid_strategy_instance.get(control_id) def _type_keys(self, control_id, text): self._main.child_window(control_id=control_id, class_name="Edit").set_edit_text(text) @@ -413,11 +420,11 @@ def _type_common_control_keys(self, control, text): def _type_edit_control_keys(self, control_id, text): if not self._editor_need_type_keys: - self._main.window( + self._main.child_window( control_id=control_id, class_name="Edit" ).set_edit_text(text) else: - editor = self._main.window( + editor = self._main.child_window( control_id=control_id, class_name="Edit") editor.select() editor.type_keys(text) @@ -430,8 +437,8 @@ def _collapse_left_menus(self): @perf_clock() def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() - self._app.top_window().type_keys('{ESC}') - self._app.top_window().type_keys('{F5}') + # self._app.top_window().type_keys('{ESC}') + # self._app.top_window().type_keys('{F5}') self.wait(sleep) def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): From 316993d2bb4957f5e0f147b371212079962c632f Mon Sep 17 00:00:00 2001 From: lhz Date: Thu, 26 Dec 2019 18:04:48 +0800 Subject: [PATCH 211/276] wk client --- easytrader/api.py | 12 ++++--- easytrader/clienttrader.py | 12 ++----- easytrader/config/client.py | 6 ++++ easytrader/ht_clienttrader.py | 67 +++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 13 deletions(-) diff --git a/easytrader/api.py b/easytrader/api.py index c53e8e22..5beb2142 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -30,21 +30,25 @@ def use(broker, debug=True, **kwargs): log.setLevel(logging.INFO) if broker.lower() in ["xq", "雪球"]: return XueQiuTrader(**kwargs) + if broker.lower() in ["yh_client", "银河客户端"]: from .yh_clienttrader import YHClientTrader - return YHClientTrader() + if broker.lower() in ["ht_client", "华泰客户端"]: from .ht_clienttrader import HTClientTrader - return HTClientTrader() + + if broker.lower() in ["wk_client", "五矿客户端"]: + from .ht_clienttrader import WKClientTrader + return WKClientTrader() + if broker.lower() in ["gj_client", "国金客户端"]: from .gj_clienttrader import GJClientTrader - return GJClientTrader() + if broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader - return ClientTrader() raise NotImplementedError diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 50ec87dc..e8361f91 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -221,7 +221,8 @@ def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs) :return: {'entrust_no': '委托单号'} """ - self._set_market_trade_params(security, amount, limit_price=limit_price) + code = security[-6:] + self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) if ttype is not None: retry = 0 retry_max = 10 @@ -232,7 +233,7 @@ def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs) except: retry += 1 self.wait(0.1) - + self._set_market_trade_params(security, amount, limit_price=limit_price) self._submit_trade() return self._handle_pop_dialogs( @@ -387,13 +388,6 @@ def _set_trade_params(self, security, price, amount): self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _set_market_trade_params(self, security, amount, limit_price=None): - code = security[-6:] - - self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) - - # wait security input finish - self.wait(0.1) - self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) self.wait(0.1) price_control = None diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 69207789..a2fb4cc8 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -8,6 +8,8 @@ def create(broker): return GJ if broker == "ths": return CommonConfig + if broker == "wk": + return WK raise NotImplementedError @@ -129,3 +131,7 @@ class GJ(CommonConfig): } AUTO_IPO_MENU_PATH = ["新股申购", "新股批量申购"] + + +class WK(HT): + pass \ No newline at end of file diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 88c3bb51..98b916b8 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -89,3 +89,70 @@ def _get_balance_from_statics(self): ).window_text() ) return result + + +class WKClientTrader(HTClientTrader): + + @property + def broker_type(self): + return "wk" + + def login(self, user, password, exe_path, comm_password=None, **kwargs): + """ + :param user: 用户名 + :param password: 密码 + :param exe_path: 客户端路径, 类似 + :param comm_password: + :param kwargs: + :return: + """ + self._editor_need_type_keys = False + if comm_password is None: + raise ValueError("五矿必须设置通讯密码") + + try: + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1 + ) + # pylint: disable=broad-except + except Exception: + self._app = pywinauto.Application().start(exe_path) + + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait("ready") + break + except RuntimeError: + pass + # self.login_test_host = False + # if self.login_test_host: + # self._app.top_window().type_keys("%t") + # self.wait(0.5) + # try: + # self._app.top_window().Button2.wait('enabled', timeout=30, retry_interval=5) + # self._app.top_window().Button5.check() # enable 自动选择 + # self.wait(0.5) + # self._app.top_window().Button3.click() + # self.wait(0.3) + # except Exception as ex: + # logging.exception("test speed error", ex) + # self._app.top_window().wrapper_object().close() + # self.wait(0.3) + + self._app.top_window().Edit1.set_focus() + self._app.top_window().Edit1.set_edit_text(user) + self._app.top_window().Edit2.set_edit_text(password) + + self._app.top_window().Edit3.set_edit_text(comm_password) + + self._app.top_window().Button1.click() + + # detect login is success or not + self._app.top_window().wait_not("exists", 100) + + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10 + ) + self._close_prompt_windows() + self._main = self._app.window(title="网上股票交易系统5.0") \ No newline at end of file From 0a15007610cd9b7e138b1c92105ddeb1c83e286a Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 20:02:48 +0800 Subject: [PATCH 212/276] :star: refacotr utils functions and use absolute import --- .pre-commit-config.yaml | 36 ------ docs/usage.md | 4 +- easytrader/__init__.py | 40 +------ easytrader/api.py | 27 +++-- easytrader/clienttrader.py | 106 ++++++++++------ easytrader/exceptions.py | 3 - easytrader/follower.py | 22 ++-- easytrader/gj_clienttrader.py | 5 +- easytrader/grid_strategies.py | 85 ++++++++----- easytrader/helpers.py | 200 ------------------------------- easytrader/ht_clienttrader.py | 12 +- easytrader/joinquant_follower.py | 10 +- easytrader/log.py | 8 +- easytrader/pop_dialog_handler.py | 22 ++-- easytrader/remoteclient.py | 7 +- easytrader/ricequant_follower.py | 12 +- easytrader/server.py | 4 +- easytrader/utils/__init__.py | 9 -- easytrader/utils/captcha.py | 88 +++++++++++++- easytrader/utils/misc.py | 31 +++++ easytrader/utils/perf.py | 47 ++++++++ easytrader/utils/stock.py | 91 ++++++++++++++ easytrader/utils/win_gui.py | 10 ++ easytrader/webtrader.py | 40 ++++--- easytrader/xq_follower.py | 158 ++++++++++++------------ easytrader/xqtrader.py | 25 ++-- easytrader/yh_clienttrader.py | 8 +- requirements.txt | 1 + 28 files changed, 582 insertions(+), 529 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 easytrader/helpers.py create mode 100644 easytrader/utils/misc.py create mode 100644 easytrader/utils/perf.py create mode 100644 easytrader/utils/stock.py create mode 100644 easytrader/utils/win_gui.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 99e6af23..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,36 +0,0 @@ -fail_fast: true -repos: -- repo: local - hooks: - - id: python_sort_imports - name: python_sort_imports - entry: pipenv run sort_imports - language: system - types: [python] - - id: python_format - name: python_format - entry: pipenv run format - language: system - types: [python] - - id: python_lint - name: python_lint - entry: pipenv run lint - language: system - types: [python] - - id: python_type_check - name: python_type_check - entry: pipenv run type_check - language: system - types: [python] - - id: python_test - name: python_test - entry: pipenv run test - language: system - types: [python] - verbose: true - - id: django_test - name: django_test - entry: pipenv run test - language: system - types: [python] - verbose: true diff --git a/docs/usage.md b/docs/usage.md index 52537df3..31469bbb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -261,8 +261,8 @@ user.today_entrusts #### 查询今天可以申购的新股信息 ```python -from easytrader import helpers -ipo_data = helpers.get_today_ipo_data() +from easytrader.utils.stock import get_today_ipo_data +ipo_data = get_today_ipo_data() print(ipo_data) ``` diff --git a/easytrader/__init__.py b/easytrader/__init__.py index fdd14f90..87ff2b0a 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -1,49 +1,11 @@ # -*- coding: utf-8 -*- import urllib3 +from .log import logger from . import exceptions from .api import use, follower -import jsonpickle urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) __version__ = "0.18.4" __author__ = "shidenggui" - - -try: - from time import process_time -except: - from time import clock as process_time -import timeit -#from decorator import decorator -import logging - -internal_logger = logging.getLogger("PERF") - -# Decorator -def perf_clock(logger=None): - if logger is None: - logger = internal_logger - - def perf_decorator(method): - - #@decorator - def timed(*args, **kw): - ts = timeit.default_timer() - cs = process_time() - ex = None - result = None - try: - result = method(*args, **kw) - except Exception as ex1: - ex = ex1 - - te = timeit.default_timer() - ce = process_time() - logger.info('%r consume %2.4f sec, cpu %2.4f sec. args %s, extra args %s' % (method.__name__, te-ts, ce-cs, jsonpickle.dumps(args[1:]), jsonpickle.dumps(kw))) - if ex is not None: - raise ex - return result - return timed - return perf_decorator \ No newline at end of file diff --git a/easytrader/api.py b/easytrader/api.py index 5beb2142..736a1a77 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- import logging +import sys import six -from .joinquant_follower import JoinQuantFollower -from .log import log -from .ricequant_follower import RiceQuantFollower -from .xq_follower import XueQiuFollower -from .xqtrader import XueQiuTrader +from easytrader.joinquant_follower import JoinQuantFollower +from easytrader.log import logger +from easytrader.ricequant_follower import RiceQuantFollower +from easytrader.xq_follower import XueQiuFollower +from easytrader.xqtrader import XueQiuTrader -if six.PY2: - raise TypeError("不支持 Python2,请升级 Python3") +if sys.version_info <= (3, 5): + raise TypeError("不支持 Python3.5 及以下版本,请升级") -def use(broker, debug=True, **kwargs): +def use(broker, debug=False, **kwargs): """用于生成特定的券商对象 :param broker:券商名支持 ['yh_client', '银河客户端'] ['ht_client', '华泰客户端'] :param debug: 控制 debug 日志的显示, 默认为 True @@ -26,29 +27,35 @@ def use(broker, debug=True, **kwargs): >>> user = easytrader.use('xq') >>> user.prepare('xq.json') """ - if not debug: - log.setLevel(logging.INFO) + if debug: + logger.setLevel(logging.DEBUG) + if broker.lower() in ["xq", "雪球"]: return XueQiuTrader(**kwargs) if broker.lower() in ["yh_client", "银河客户端"]: from .yh_clienttrader import YHClientTrader + return YHClientTrader() if broker.lower() in ["ht_client", "华泰客户端"]: from .ht_clienttrader import HTClientTrader + return HTClientTrader() if broker.lower() in ["wk_client", "五矿客户端"]: from .ht_clienttrader import WKClientTrader + return WKClientTrader() if broker.lower() in ["gj_client", "国金客户端"]: from .gj_clienttrader import GJClientTrader + return GJClientTrader() if broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader + return ClientTrader() raise NotImplementedError diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index e8361f91..8731e271 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -1,22 +1,21 @@ # -*- coding: utf-8 -*- import abc import functools +import logging import os +import re import sys import time from typing import Type -from pywinauto import findwindows - -from . import perf_clock - import easyutils -import re +from pywinauto import findwindows -from . import grid_strategies, helpers, pop_dialog_handler -from .config import client +from easytrader import grid_strategies, pop_dialog_handler +from easytrader.config import client +from easytrader.utils.misc import file2dict +from easytrader.utils.perf import perf_clock from win32gui import SetForegroundWindow, ShowWindow -import logging if not sys.platform.startswith("darwin"): import pywinauto @@ -153,7 +152,7 @@ def cancel_entrusts(self): return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) - @perf_clock() + @perf_clock def cancel_entrust(self, entrust_no): self.refresh() for i, entrust in enumerate(self.cancel_entrusts): @@ -165,20 +164,22 @@ def cancel_entrust(self, entrust_no): return self._handle_pop_dialogs() return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} - @perf_clock() + @perf_clock def buy(self, security, price, amount, **kwargs): self._switch_left_menus(["买入[F1]"]) return self.trade(security, price, amount) - @perf_clock() + @perf_clock def sell(self, security, price, amount, **kwargs): self._switch_left_menus(["卖出[F2]"]) return self.trade(security, price, amount) - @perf_clock() - def market_buy(self, security, amount, ttype=None, limit_price=None, **kwargs): + @perf_clock + def market_buy( + self, security, amount, ttype=None, limit_price=None, **kwargs + ): """ 市价买入 :param security: 六位证券代码 @@ -192,10 +193,14 @@ def market_buy(self, security, amount, ttype=None, limit_price=None, **kwargs): """ self._switch_left_menus(["市价委托", "买入"]) - return self.market_trade(security, amount, ttype, limit_price=limit_price) + return self.market_trade( + security, amount, ttype, limit_price=limit_price + ) - @perf_clock() - def market_sell(self, security, amount, ttype=None, limit_price=None, **kwargs): + @perf_clock + def market_sell( + self, security, amount, ttype=None, limit_price=None, **kwargs + ): """ 市价卖出 :param security: 六位证券代码 @@ -208,9 +213,13 @@ def market_sell(self, security, amount, ttype=None, limit_price=None, **kwargs): """ self._switch_left_menus(["市价委托", "卖出"]) - return self.market_trade(security, amount, ttype, limit_price=limit_price) + return self.market_trade( + security, amount, ttype, limit_price=limit_price + ) - def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs): + def market_trade( + self, security, amount, ttype=None, limit_price=None, **kwargs + ): """ 市价交易 :param security: 六位证券代码 @@ -222,7 +231,9 @@ def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs) :return: {'entrust_no': '委托单号'} """ code = security[-6:] - self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) + self._type_edit_control_keys( + self._config.TRADE_SECURITY_CONTROL_ID, code + ) if ttype is not None: retry = 0 retry_max = 10 @@ -233,7 +244,9 @@ def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs) except: retry += 1 self.wait(0.1) - self._set_market_trade_params(security, amount, limit_price=limit_price) + self._set_market_trade_params( + security, amount, limit_price=limit_price + ) self._submit_trade() return self._handle_pop_dialogs( @@ -267,7 +280,9 @@ def auto_ipo(self): return {"message": "今日无新股"} invalid_list_idx = [ # i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 or v["证券代码"][:2] == "78" - i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 + i + for i, v in enumerate(stock_list) + if v["申购数量"] <= 0 ] if len(stock_list) == len(invalid_list_idx): @@ -296,7 +311,7 @@ def _click_grid_by_row(self, row): class_name="CVirtualGridCtrl", ).click(coords=(x, y)) - @perf_clock() + @perf_clock def _is_exist_pop_dialog(self): self.wait(0.5) # wait dialog display try: @@ -304,7 +319,7 @@ def _is_exist_pop_dialog(self): self._main.wrapper_object() != self._app.top_window().wrapper_object() ) - except findwindows.ElementNotFoundError|pywinauto.timings.TimeoutError|RuntimeError as ex: + except findwindows.ElementNotFoundError | pywinauto.timings.TimeoutError | RuntimeError as ex: logging.exception(ex) return False @@ -319,7 +334,9 @@ def exit(self): def _close_prompt_windows(self): self.wait(1) - for window in self._app.windows(class_name="#32770", visible_only=True): + for window in self._app.windows( + class_name="#32770", visible_only=True + ): title = window.window_text() if title != self._config.TITLE: logging.info("close " + title) @@ -332,7 +349,6 @@ def close_pormpt_window_no_wait(self): if window.window_text() != self._config.TITLE: window.close() - def trade(self, security, price, amount): self._set_trade_params(security, price, amount) @@ -347,7 +363,7 @@ def _click(self, control_id): control_id=control_id, class_name="Button" ).click() - @perf_clock() + @perf_clock def _submit_trade(self): time.sleep(0.2) self._main.child_window( @@ -355,17 +371,20 @@ def _submit_trade(self): class_name="Button", ).click() - @perf_clock() + @perf_clock def __get_top_window_pop_dialog(self): - return self._app.top_window().window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + return self._app.top_window().window( + control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID + ) - @perf_clock() + @perf_clock def _get_pop_dialog_title(self): return ( self._app.top_window() .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) .window_text() ) + # def _get_pop_dialog_title(self): # return ( # self._app.top_window() @@ -376,7 +395,9 @@ def _get_pop_dialog_title(self): def _set_trade_params(self, security, price, amount): code = security[-6:] - self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) + self._type_edit_control_keys( + self._config.TRADE_SECURITY_CONTROL_ID, code + ) # wait security input finish self.wait(0.1) @@ -385,28 +406,34 @@ def _set_trade_params(self, security, price, amount): self._config.TRADE_PRICE_CONTROL_ID, easyutils.round_price_by_code(price, code), ) - self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) + self._type_edit_control_keys( + self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount)) + ) def _set_market_trade_params(self, security, amount, limit_price=None): - self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) + self._type_edit_control_keys( + self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount)) + ) self.wait(0.1) price_control = None if str(security).startswith("68"): # 科创板存在限价 try: price_control = self._main.child_window( - control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit" + control_id=self._config.TRADE_PRICE_CONTROL_ID, + class_name="Edit", ) except: pass if price_control is not None: price_control.set_edit_text(limit_price) - def _get_grid_data(self, control_id): return self.grid_strategy_instance.get(control_id) def _type_keys(self, control_id, text): - self._main.child_window(control_id=control_id, class_name="Edit").set_edit_text(text) + self._main.child_window( + control_id=control_id, class_name="Edit" + ).set_edit_text(text) def _type_common_control_keys(self, control, text): self._set_foreground(control) @@ -419,7 +446,8 @@ def _type_edit_control_keys(self, control_id, text): ).set_edit_text(text) else: editor = self._main.child_window( - control_id=control_id, class_name="Edit") + control_id=control_id, class_name="Edit" + ) editor.select() editor.type_keys(text) @@ -428,7 +456,7 @@ def _collapse_left_menus(self): for item in items: item.collapse() - @perf_clock() + @perf_clock def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() # self._app.top_window().type_keys('{ESC}') @@ -472,7 +500,7 @@ def _cancel_entrust_by_double_click(self, row): def refresh(self): self._switch_left_menus(["买入[F1]"], sleep=0.05) - @perf_clock() + @perf_clock def _handle_pop_dialogs( self, handler_class=pop_dialog_handler.PopDialogHandler ): @@ -515,7 +543,7 @@ def prepare( :return: """ if config_path is not None: - account = helpers.file2dict(config_path) + account = file2dict(config_path) user = account["user"] password = account["password"] comm_password = account.get("comm_password") diff --git a/easytrader/exceptions.py b/easytrader/exceptions.py index 8a838ef3..adbaa82a 100644 --- a/easytrader/exceptions.py +++ b/easytrader/exceptions.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -""" -Exceptions -""" class TradeError(IOError): diff --git a/easytrader/follower.py b/easytrader/follower.py index ef1b517a..431b4a96 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -11,8 +11,8 @@ import requests -from . import exceptions -from .log import log +from easytrader import exceptions +from easytrader.log import logger class BaseFollower(metaclass=abc.ABCMeta): @@ -55,7 +55,7 @@ def login(self, user=None, password=None, **kwargs): rep = self.s.post(self.LOGIN_API, data=params) self.check_login_success(rep) - log.info("登录成功") + logger.info("登录成功") def _generate_headers(self): headers = { @@ -184,7 +184,7 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): ) # pylint: disable=broad-except except Exception as e: - log.exception("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e) + logger.exception("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e) time.sleep(3) continue for transaction in transactions: @@ -199,7 +199,7 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): } if self.is_cmd_expired(trade_cmd): continue - log.info( + logger.info( "策略 [%s] 发送指令到交易队列, 股票: %s 动作: %s 数量: %s 价格: %s 信号产生时间: %s", name, trade_cmd["stock_code"], @@ -214,7 +214,7 @@ def track_strategy_worker(self, strategy, name, interval=10, **kwargs): for _ in range(interval): time.sleep(1) except KeyboardInterrupt: - log.info("程序退出") + logger.info("程序退出") break @staticmethod @@ -263,7 +263,7 @@ def _execute_trade_cmd( now = datetime.datetime.now() expire = (now - trade_cmd["datetime"]).total_seconds() if expire > expire_seconds: - log.warning( + logger.warning( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 超过设置的最大过期时间 %s 秒, 被丢弃", trade_cmd["strategy_name"], trade_cmd["stock_code"], @@ -279,7 +279,7 @@ def _execute_trade_cmd( # check price price = trade_cmd["price"] if not self._is_number(price) or price <= 0: - log.warning( + logger.warning( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 价格无效 , 被丢弃", trade_cmd["strategy_name"], trade_cmd["stock_code"], @@ -293,7 +293,7 @@ def _execute_trade_cmd( # check amount if trade_cmd["amount"] <= 0: - log.warning( + logger.warning( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 买入股数无效 , 被丢弃", trade_cmd["strategy_name"], trade_cmd["stock_code"], @@ -319,7 +319,7 @@ def _execute_trade_cmd( except exceptions.TradeError as e: trader_name = type(user).__name__ err_msg = "{}: {}".format(type(e).__name__, e.args) - log.error( + logger.error( "%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 失败, 错误信息: %s", trader_name, trade_cmd["strategy_name"], @@ -331,7 +331,7 @@ def _execute_trade_cmd( err_msg, ) else: - log.info( + logger.info( "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 执行成功, 返回: %s", trade_cmd["strategy_name"], trade_cmd["stock_code"], diff --git a/easytrader/gj_clienttrader.py b/easytrader/gj_clienttrader.py index 7fb4012c..00d865d9 100644 --- a/easytrader/gj_clienttrader.py +++ b/easytrader/gj_clienttrader.py @@ -6,7 +6,8 @@ import pywinauto import pywinauto.clipboard -from . import clienttrader, helpers +from easytrader import clienttrader +from easytrader.utils.captcha import recognize_verify_code class GJClientTrader(clienttrader.BaseLoginClientTrader): @@ -75,5 +76,5 @@ def _handle_verify_code(self): file_path = tempfile.mktemp() + ".jpg" control.capture_as_image().save(file_path) time.sleep(0.2) - vcode = helpers.recognize_verify_code(file_path, "gj_client") + vcode = recognize_verify_code(file_path, "gj_client") return "".join(re.findall("[a-zA-Z0-9]+", vcode)) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 38614a62..e944a7ec 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -2,27 +2,21 @@ import abc import io import tempfile - -from pywinauto import win32defines +from io import StringIO from typing import TYPE_CHECKING, Dict, List import pandas as pd -import pywinauto.clipboard -from .utils import SetForegroundWindow, ShowWindow import pywinauto -import logging -try: - import StringIO -except: - from io import StringIO - -from .log import log -from .utils.captcha import captcha_recognize +import pywinauto.clipboard +from pywinauto import win32defines +from easytrader.log import logger +from easytrader.utils.captcha import captcha_recognize +from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow if TYPE_CHECKING: # pylint: disable=unused-import - from . import clienttrader + from easytrader import clienttrader class IGridStrategy(abc.ABC): @@ -59,7 +53,9 @@ def _set_foreground(self, grid=None): try: if grid is None: grid = self._trader.main - if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + if grid.has_style( + pywinauto.win32defines.WS_MINIMIZE + ): # if minimized ShowWindow(grid.wrapper_object(), 9) # restore window state else: SetForegroundWindow(grid.wrapper_object()) # bring to front @@ -71,6 +67,7 @@ class Copy(BaseStrategy): """ 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 """ + _need_captcha_reg = True def get(self, control_id: int) -> List[Dict]: @@ -94,30 +91,49 @@ def _format_grid_data(self, data: str) -> List[Dict]: 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): + 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) # 保存验证码 + self._trader.app.top_window().window( + control_id=0x965, class_name="Static" + ).capture_as_image().save( + file_path + ) # 保存验证码 captcha_num = captcha_recognize(file_path) # 识别验证码 - log.info("captcha result-->" + captcha_num) + 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().window( + control_id=0x964, class_name="Edit" + ).set_text( + captcha_num + ) # 模拟输入验证码 self._trader.app.top_window().set_focus() - pywinauto.keyboard.SendKeys("{ENTER}") # 模拟发送enter,点击确定 + pywinauto.keyboard.SendKeys( + "{ENTER}" + ) # 模拟发送enter,点击确定 try: - log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static').window_text()) - except Exception as ex: # 窗体消失 - log.exception(ex) + 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() + self._trader.app.top_window().window( + control_id=0x965, class_name="Static" + ).click() if not found: self._trader.app.top_window().Button2.click() # 点击取消 else: @@ -129,7 +145,7 @@ def _get_clipboard_data(self) -> str: # pylint: disable=broad-except except Exception as e: count -= 1 - log.exception("%s, retry ......", e) + logger.exception("%s, retry ......", e) class WMCopy(Copy): @@ -139,7 +155,7 @@ class WMCopy(Copy): def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) - grid.post_message(win32defines.WM_COMMAND, 0xe122, 0) + grid.post_message(win32defines.WM_COMMAND, 0xE122, 0) self._trader.wait(0.1) content = self._get_clipboard_data() return self._format_grid_data(content) @@ -155,7 +171,9 @@ 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 + self._set_foreground( + grid + ) # setFocus buggy, instead of SetForegroundWindow grid.type_keys("^s", set_foreground=False) count = 10 while count > 0: @@ -168,13 +186,16 @@ def get(self, control_id: int) -> List[Dict]: self._set_foreground(self._trader.app.top_window()) # self._trader.app.top_window().type_keys(self.normalize_path(temp_path), set_foreground=False) - # alt+s保存,alt+y替换已存在的文件 # # self._set_foreground(self._trader.app.top_window()) # self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False) - self._trader.app.top_window().Edit1.set_edit_text(self.normalize_path(temp_path)) + self._trader.app.top_window().Edit1.set_edit_text( + self.normalize_path(temp_path) + ) self._trader.wait(0.1) - self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False) + self._trader.app.top_window().type_keys( + "%{s}%{y}", set_foreground=False + ) self._trader.wait(0.2) if self._trader._is_exist_pop_dialog(): self._trader.app.top_window().Button2.click() @@ -184,10 +205,10 @@ def get(self, control_id: int) -> List[Dict]: return self._format_grid_data(temp_path) def normalize_path(self, temp_path: str) -> str: - return temp_path.replace('~', '{~}') + return temp_path.replace("~", "{~}") def _format_grid_data(self, data: str) -> List[Dict]: - f = open(data, encoding="gbk", errors='replace') + f = open(data, encoding="gbk", errors="replace") cont = f.read() f.close() diff --git a/easytrader/helpers.py b/easytrader/helpers.py deleted file mode 100644 index dd5d560c..00000000 --- a/easytrader/helpers.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -import json -import random -import re - -import requests - -from . import exceptions - - -def parse_cookies_str(cookies): - """ - parse cookies str to dict - :param cookies: cookies str - :type cookies: str - :return: cookie dict - :rtype: dict - """ - cookie_dict = {} - for record in cookies.split(";"): - key, value = record.strip().split("=", 1) - cookie_dict[key] = value - return cookie_dict - - -def file2dict(path): - with open(path, encoding="utf-8") as f: - return json.load(f) - - -def get_stock_type(stock_code): - """判断股票ID对应的证券市场 - 匹配规则 - ['50', '51', '60', '90', '110'] 为 sh - ['00', '13', '18', '15', '16', '18', '20', '30', '39', '115'] 为 sz - ['5', '6', '9'] 开头的为 sh, 其余为 sz - :param stock_code:股票ID, 若以 'sz', 'sh' 开头直接返回对应类型,否则使用内置规则判断 - :return 'sh' or 'sz'""" - stock_code = str(stock_code) - if stock_code.startswith(("sh", "sz")): - return stock_code[:2] - if stock_code.startswith( - ("50", "51", "60", "73", "90", "110", "113", "132", "204", "78") - ): - return "sh" - if stock_code.startswith( - ("00", "13", "18", "15", "16", "18", "20", "30", "39", "115", "1318") - ): - return "sz" - if stock_code.startswith(("5", "6", "9")): - return "sh" - return "sz" - - -def recognize_verify_code(image_path, broker="ht"): - """识别验证码,返回识别后的字符串,使用 tesseract 实现 - :param image_path: 图片路径 - :param broker: 券商 ['ht', 'yjb', 'gf', 'yh'] - :return recognized: verify code string""" - - if broker == "gf": - return detect_gf_result(image_path) - if broker in ["yh_client", "gj_client"]: - return detect_yh_client_result(image_path) - # 调用 tesseract 识别 - return default_verify_code_detect(image_path) - - -def detect_yh_client_result(image_path): - """封装了tesseract的识别,部署在阿里云上,服务端源码地址为: https://github.com/shidenggui/yh_verify_code_docker""" - api = "http://yh.ez.shidenggui.com:5000/yh_client" - with open(image_path, "rb") as f: - rep = requests.post(api, files={"image": f}) - if rep.status_code != 201: - error = rep.json()["message"] - raise exceptions.TradeError("request {} error: {}".format(api, error)) - return rep.json()["result"] - - -def input_verify_code_manual(image_path): - from PIL import Image - - image = Image.open(image_path) - image.show() - code = input( - "image path: {}, input verify code answer:".format(image_path) - ) - return code - - -def default_verify_code_detect(image_path): - from PIL import Image - - img = Image.open(image_path) - return invoke_tesseract_to_recognize(img) - - -def detect_gf_result(image_path): - from PIL import ImageFilter, Image - - img = Image.open(image_path) - 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 p: 0 if 68 < p < 90 else 256) - min_res = two.filter(ImageFilter.MinFilter) - med_res = min_res.filter(ImageFilter.MedianFilter) - for _ in range(2): - med_res = med_res.filter(ImageFilter.MedianFilter) - return invoke_tesseract_to_recognize(med_res) - - -def invoke_tesseract_to_recognize(img): - import pytesseract - - try: - res = pytesseract.image_to_string(img) - except FileNotFoundError: - raise Exception( - "tesseract 未安装,请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程" - ) - valid_chars = re.findall("[0-9a-z]", res, re.IGNORECASE) - return "".join(valid_chars) - - -def grep_comma(num_str): - return num_str.replace(",", "") - - -def str2num(num_str, convert_type="float"): - num = float(grep_comma(num_str)) - return num if convert_type == "float" else int(num) - - -def get_30_date(): - """ - 获得用于查询的默认日期, 今天的日期, 以及30天前的日期 - 用于查询的日期格式通常为 20160211 - :return: - """ - now = datetime.datetime.now() - end_date = now.date() - start_date = end_date - datetime.timedelta(days=30) - return start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d") - - -def get_today_ipo_data(): - """ - 查询今天可以申购的新股信息 - :return: 今日可申购新股列表 apply_code申购代码 price发行价格 - """ - - agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0" - send_headers = { - "Host": "xueqiu.com", - "User-Agent": agent, - "Accept": "application/json, text/javascript, */*; q=0.01", - "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3", - "Accept-Encoding": "deflate", - "Cache-Control": "no-cache", - "X-Requested-With": "XMLHttpRequest", - "Referer": "https://xueqiu.com/hq", - "Connection": "keep-alive", - } - - timestamp = random.randint(1000000000000, 9999999999999) - home_page_url = "https://xueqiu.com" - ipo_data_url = ( - "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl" - "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_" - "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" - % (str(timestamp)) - ) - - session = requests.session() - session.get(home_page_url, headers=send_headers) # 产生cookies - ipo_response = session.post(ipo_data_url, headers=send_headers) - - json_data = json.loads(ipo_response.text) - today_ipo = [] - - for line in json_data["data"]: - if datetime.datetime.now().strftime("%a %b %d") == line[3][:10]: - today_ipo.append( - { - "stock_code": line[0], - "stock_name": line[1], - "apply_code": line[2], - "price": line[7], - } - ) - - return today_ipo diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index 98b916b8..d16f167e 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- +import logging + import pywinauto import pywinauto.clipboard -from win32gui import SetForegroundWindow from . import clienttrader -import logging + class HTClientTrader(clienttrader.BaseLoginClientTrader): @@ -47,7 +48,9 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().type_keys("%t") self.wait(0.5) try: - self._app.top_window().Button2.wait('enabled',timeout=30, retry_interval=5) + self._app.top_window().Button2.wait( + "enabled", timeout=30, retry_interval=5 + ) self._app.top_window().Button5.check() # enable 自动选择 self.wait(0.5) self._app.top_window().Button3.click() @@ -92,7 +95,6 @@ def _get_balance_from_statics(self): class WKClientTrader(HTClientTrader): - @property def broker_type(self): return "wk" @@ -155,4 +157,4 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): path=self._run_exe_path(exe_path), timeout=10 ) self._close_prompt_windows() - self._main = self._app.window(title="网上股票交易系统5.0") \ No newline at end of file + self._main = self._app.window(title="网上股票交易系统5.0") diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index eb217329..71a6bb11 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -3,9 +3,9 @@ from datetime import datetime from threading import Thread -from . import exceptions -from .follower import BaseFollower -from .log import log +from easytrader import exceptions +from easytrader.follower import BaseFollower +from easytrader.log import logger class JoinQuantFollower(BaseFollower): @@ -67,7 +67,7 @@ def follow( strategy_id = self.extract_strategy_id(strategy_url) strategy_name = self.extract_strategy_name(strategy_url) except: - log.error("抽取交易id和策略名失败, 无效的模拟交易url: %s", strategy_url) + logger.error("抽取交易id和策略名失败, 无效的模拟交易url: %s", strategy_url) raise strategy_worker = Thread( target=self.track_strategy_worker, @@ -76,7 +76,7 @@ def follow( ) strategy_worker.start() workers.append(strategy_worker) - log.info("开始跟踪策略: %s", strategy_name) + logger.info("开始跟踪策略: %s", strategy_name) for worker in workers: worker.join() diff --git a/easytrader/log.py b/easytrader/log.py index 08510a9d..93be16b8 100644 --- a/easytrader/log.py +++ b/easytrader/log.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import logging -log = logging.getLogger("easytrader") -log.setLevel(logging.DEBUG) -log.propagate = False +logger = logging.getLogger("easytrader") +logger.setLevel(logging.DEBUG) +logger.propagate = False fmt = logging.Formatter( "%(asctime)s [%(levelname)s] %(filename)s %(lineno)s: %(message)s" @@ -11,4 +11,4 @@ ch = logging.StreamHandler() ch.setFormatter(fmt) -log.handlers.append(ch) +logger.handlers.append(ch) diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index 5aa2b7f3..5142745c 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -3,9 +3,12 @@ import time from typing import Optional -from . import exceptions, perf_clock -import pywinauto -from .utils import SetForegroundWindow, ShowWindow +import pywinauto + +from easytrader import exceptions +from easytrader.utils.perf import perf_clock +from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow + class PopDialogHandler: def __init__(self, app): @@ -19,7 +22,7 @@ def _set_foreground(self, grid=None): else: SetForegroundWindow(grid.wrapper_object()) # bring to front - @perf_clock() + @perf_clock def handle(self, title): if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): self._submit_by_shortcut() @@ -37,7 +40,7 @@ def handle(self, title): def _extract_content(self): return self._app.top_window().Static.window_text() - @perf_clock() + @perf_clock def _extract_entrust_id(self, content): return re.search(r"\d+", content).group() @@ -45,7 +48,9 @@ def _submit_by_click(self): try: self._app.top_window()["确定"].click() except Exception as ex: - self._app.Window_(best_match='Dialog', top_level_only=True).ChildWindow(best_match='确定').click() + self._app.Window_( + best_match="Dialog", top_level_only=True + ).ChildWindow(best_match="确定").click() def _submit_by_shortcut(self): self._set_foreground(self._app.top_window()) @@ -56,8 +61,7 @@ def _close(self): class TradePopDialogHandler(PopDialogHandler): - - @perf_clock() + @perf_clock def handle(self, title) -> Optional[dict]: if title == "委托确认": self._submit_by_shortcut() @@ -73,7 +77,7 @@ def handle(self, title) -> Optional[dict]: self._submit_by_shortcut() return None - if "逆回购" in content: + if "逆回购" in content: self._submit_by_shortcut() return None diff --git a/easytrader/remoteclient.py b/easytrader/remoteclient.py index cdf68593..cfddab94 100644 --- a/easytrader/remoteclient.py +++ b/easytrader/remoteclient.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import requests -from . import helpers +from easytrader.utils.misc import file2dict def use(broker, host, port=1430, **kwargs): @@ -28,7 +28,8 @@ def prepare( :param config_path: 登陆配置文件,跟参数登陆方式二选一 :param user: 账号 :param password: 明文密码 - :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe' + :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', + 默认 r'C:\\htzqzyb2\\xiadan.exe' :param comm_password: 通讯密码 :return: """ @@ -36,7 +37,7 @@ def prepare( params.pop("self") if config_path is not None: - account = helpers.file2dict(config_path) + account = file2dict(config_path) params["user"] = account["user"] params["password"] = account["password"] diff --git a/easytrader/ricequant_follower.py b/easytrader/ricequant_follower.py index dd1c2191..4c93e6f5 100644 --- a/easytrader/ricequant_follower.py +++ b/easytrader/ricequant_follower.py @@ -3,8 +3,8 @@ from datetime import datetime from threading import Thread -from .follower import BaseFollower -from .log import log +from easytrader.follower import BaseFollower +from easytrader.log import logger class RiceQuantFollower(BaseFollower): @@ -15,7 +15,7 @@ def __init__(self): def login(self, user=None, password=None, **kwargs): from rqopen_client import RQOpenClient - self.client = RQOpenClient(user, password, logger=log) + self.client = RQOpenClient(user, password, logger=logger) def follow( self, @@ -56,14 +56,14 @@ def follow( ) strategy_worker.start() workers.append(strategy_worker) - log.info("开始跟踪策略: %s", strategy_name) + logger.info("开始跟踪策略: %s", strategy_name) for worker in workers: worker.join() def extract_strategy_name(self, run_id): ret_json = self.client.get_positions(run_id) if ret_json["code"] != 200: - log.error( + logger.error( "fetch data from run_id %s fail, msg %s", run_id, ret_json["msg"], @@ -74,7 +74,7 @@ def extract_strategy_name(self, run_id): def extract_day_trades(self, run_id): ret_json = self.client.get_day_trades(run_id) if ret_json["code"] != 200: - log.error( + logger.error( "fetch day trades from run_id %s fail, msg %s", run_id, ret_json["msg"], diff --git a/easytrader/server.py b/easytrader/server.py index 7a8fe5c0..02ce98d6 100644 --- a/easytrader/server.py +++ b/easytrader/server.py @@ -3,7 +3,7 @@ from flask import Flask, jsonify, request from . import api -from .log import log +from .log import logger app = Flask(__name__) @@ -17,7 +17,7 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) # pylint: disable=broad-except except Exception as e: - log.exception("server error") + logger.exception("server error") message = "{}: {}".format(e.__class__, e) return jsonify({"error": message}), 400 diff --git a/easytrader/utils/__init__.py b/easytrader/utils/__init__.py index ff52b97d..e69de29b 100644 --- a/easytrader/utils/__init__.py +++ b/easytrader/utils/__init__.py @@ -1,9 +0,0 @@ -from .captcha import captcha_recognize - -import win32gui - -def SetForegroundWindow(hwd): - win32gui.SetForegroundWindow(hwd._as_parameter_) - -def ShowWindow(hwd, window_status): - win32gui.ShowWindow(hwd._as_parameter_, window_status) \ No newline at end of file diff --git a/easytrader/utils/captcha.py b/easytrader/utils/captcha.py index 6ec3b712..818ec66f 100644 --- a/easytrader/utils/captcha.py +++ b/easytrader/utils/captcha.py @@ -1,8 +1,14 @@ -import pytesseract +import re + +import requests from PIL import Image +from easytrader import exceptions + def captcha_recognize(img_path): + import pytesseract + im = Image.open(img_path).convert("L") # 1. threshold the image threshold = 200 @@ -13,8 +19,86 @@ def captcha_recognize(img_path): else: table.append(1) - out = im.point(table, '1') + out = im.point(table, "1") # out.show() # 2. recognize with tesseract num = pytesseract.image_to_string(out) return num + + +def recognize_verify_code(image_path, broker="ht"): + """识别验证码,返回识别后的字符串,使用 tesseract 实现 + :param image_path: 图片路径 + :param broker: 券商 ['ht', 'yjb', 'gf', 'yh'] + :return recognized: verify code string""" + + if broker == "gf": + return detect_gf_result(image_path) + if broker in ["yh_client", "gj_client"]: + return detect_yh_client_result(image_path) + # 调用 tesseract 识别 + return default_verify_code_detect(image_path) + + +def detect_yh_client_result(image_path): + """封装了tesseract的识别,部署在阿里云上, + 服务端源码地址为: https://github.com/shidenggui/yh_verify_code_docker""" + api = "http://yh.ez.shidenggui.com:5000/yh_client" + with open(image_path, "rb") as f: + rep = requests.post(api, files={"image": f}) + if rep.status_code != 201: + error = rep.json()["message"] + raise exceptions.TradeError("request {} error: {}".format(api, error)) + return rep.json()["result"] + + +def input_verify_code_manual(image_path): + from PIL import Image + + image = Image.open(image_path) + image.show() + code = input( + "image path: {}, input verify code answer:".format(image_path) + ) + return code + + +def default_verify_code_detect(image_path): + from PIL import Image + + img = Image.open(image_path) + return invoke_tesseract_to_recognize(img) + + +def detect_gf_result(image_path): + from PIL import ImageFilter, Image + + img = Image.open(image_path) + 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 p: 0 if 68 < p < 90 else 256) + min_res = two.filter(ImageFilter.MinFilter) + med_res = min_res.filter(ImageFilter.MedianFilter) + for _ in range(2): + med_res = med_res.filter(ImageFilter.MedianFilter) + return invoke_tesseract_to_recognize(med_res) + + +def invoke_tesseract_to_recognize(img): + import pytesseract + + try: + res = pytesseract.image_to_string(img) + except FileNotFoundError: + raise Exception( + "tesseract 未安装,请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程" + ) + valid_chars = re.findall("[0-9a-z]", res, re.IGNORECASE) + return "".join(valid_chars) diff --git a/easytrader/utils/misc.py b/easytrader/utils/misc.py new file mode 100644 index 00000000..a47592b5 --- /dev/null +++ b/easytrader/utils/misc.py @@ -0,0 +1,31 @@ +# coding:utf-8 +import json + + +def parse_cookies_str(cookies): + """ + parse cookies str to dict + :param cookies: cookies str + :type cookies: str + :return: cookie dict + :rtype: dict + """ + cookie_dict = {} + for record in cookies.split(";"): + key, value = record.strip().split("=", 1) + cookie_dict[key] = value + return cookie_dict + + +def file2dict(path): + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def grep_comma(num_str): + return num_str.replace(",", "") + + +def str2num(num_str, convert_type="float"): + num = float(grep_comma(num_str)) + return num if convert_type == "float" else int(num) diff --git a/easytrader/utils/perf.py b/easytrader/utils/perf.py new file mode 100644 index 00000000..2d6a48a2 --- /dev/null +++ b/easytrader/utils/perf.py @@ -0,0 +1,47 @@ +# coding:utf-8 +import functools +import logging +import timeit + +import jsonpickle +from easytrader import logger + +try: + from time import process_time +except: + from time import clock as process_time + + +def perf_clock(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + if not logger.isEnabledFor(logging.DEBUG): + return f(*args, **kwargs) + + ts = timeit.default_timer() + cs = process_time() + ex = None + result = None + + try: + result = f(*args, **kwargs) + except Exception as ex1: + ex = ex1 + + te = timeit.default_timer() + ce = process_time() + logger.debug( + "%r consume %2.4f sec, cpu %2.4f sec. args %s, extra args %s" + % ( + f.__name__, + te - ts, + ce - cs, + jsonpickle.dumps(args[1:]), + jsonpickle.dumps(kwargs), + ) + ) + if ex is not None: + raise ex + return result + + return wrapper diff --git a/easytrader/utils/stock.py b/easytrader/utils/stock.py new file mode 100644 index 00000000..21acea23 --- /dev/null +++ b/easytrader/utils/stock.py @@ -0,0 +1,91 @@ +# coding:utf-8 +import datetime +import json +import random + +import requests + + +def get_stock_type(stock_code): + """判断股票ID对应的证券市场 + 匹配规则 + ['50', '51', '60', '90', '110'] 为 sh + ['00', '13', '18', '15', '16', '18', '20', '30', '39', '115'] 为 sz + ['5', '6', '9'] 开头的为 sh, 其余为 sz + :param stock_code:股票ID, 若以 'sz', 'sh' 开头直接返回对应类型,否则使用内置规则判断 + :return 'sh' or 'sz'""" + stock_code = str(stock_code) + if stock_code.startswith(("sh", "sz")): + return stock_code[:2] + if stock_code.startswith( + ("50", "51", "60", "73", "90", "110", "113", "132", "204", "78") + ): + return "sh" + if stock_code.startswith( + ("00", "13", "18", "15", "16", "18", "20", "30", "39", "115", "1318") + ): + return "sz" + if stock_code.startswith(("5", "6", "9")): + return "sh" + return "sz" + + +def get_30_date(): + """ + 获得用于查询的默认日期, 今天的日期, 以及30天前的日期 + 用于查询的日期格式通常为 20160211 + :return: + """ + now = datetime.datetime.now() + end_date = now.date() + start_date = end_date - datetime.timedelta(days=30) + return start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d") + + +def get_today_ipo_data(): + """ + 查询今天可以申购的新股信息 + :return: 今日可申购新股列表 apply_code申购代码 price发行价格 + """ + + agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0" + send_headers = { + "Host": "xueqiu.com", + "User-Agent": agent, + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3", + "Accept-Encoding": "deflate", + "Cache-Control": "no-cache", + "X-Requested-With": "XMLHttpRequest", + "Referer": "https://xueqiu.com/hq", + "Connection": "keep-alive", + } + + timestamp = random.randint(1000000000000, 9999999999999) + home_page_url = "https://xueqiu.com" + ipo_data_url = ( + "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl" + "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_" + "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s" + % (str(timestamp)) + ) + + session = requests.session() + session.get(home_page_url, headers=send_headers) # 产生cookies + ipo_response = session.post(ipo_data_url, headers=send_headers) + + json_data = json.loads(ipo_response.text) + today_ipo = [] + + for line in json_data["data"]: + if datetime.datetime.now().strftime("%a %b %d") == line[3][:10]: + today_ipo.append( + { + "stock_code": line[0], + "stock_name": line[1], + "apply_code": line[2], + "price": line[7], + } + ) + + return today_ipo diff --git a/easytrader/utils/win_gui.py b/easytrader/utils/win_gui.py new file mode 100644 index 00000000..75360ffa --- /dev/null +++ b/easytrader/utils/win_gui.py @@ -0,0 +1,10 @@ +# coding:utf-8 +import win32gui + + +def SetForegroundWindow(hwd): + win32gui.SetForegroundWindow(hwd._as_parameter_) + + +def ShowWindow(hwd, window_status): + win32gui.ShowWindow(hwd._as_parameter_, window_status) diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index a069699b..cafc88b2 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -9,8 +9,10 @@ import requests import requests.exceptions -from . import exceptions, helpers -from .log import log +from easytrader import exceptions +from easytrader.log import logger +from easytrader.utils.misc import file2dict +from easytrader.utils.stock import get_30_date # noinspection PyIncorrectDocstring @@ -30,12 +32,12 @@ def __init__(self, debug=True): def read_config(self, path): try: - self.account_config = helpers.file2dict(path) + self.account_config = file2dict(path) except ValueError: - log.error("配置文件格式有误,请勿使用记事本编辑,推荐 sublime text") + logger.error("配置文件格式有误,请勿使用记事本编辑,推荐 sublime text") for value in self.account_config: if isinstance(value, int): - log.warning("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题") + logger.warning("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题") def prepare(self, config_file=None, user=None, password=None, **kwargs): """登录的统一接口 @@ -89,18 +91,18 @@ def send_heartbeat(self): time.sleep(1) def check_login(self, sleepy=30): - log.setLevel(logging.ERROR) + logger.setLevel(logging.ERROR) try: response = self.heartbeat() self.check_account_live(response) except requests.exceptions.ConnectionError: pass except requests.exceptions.RequestException as e: - log.setLevel(self.log_level) - log.error("心跳线程发现账户出现错误: %s %s, 尝试重新登陆", e.__class__, e) + logger.setLevel(self.log_level) + logger.error("心跳线程发现账户出现错误: %s %s, 尝试重新登陆", e.__class__, e) self.autologin() finally: - log.setLevel(self.log_level) + logger.setLevel(self.log_level) time.sleep(sleepy) def heartbeat(self): @@ -115,8 +117,8 @@ def exit(self): def __read_config(self): """读取 config""" - self.config = helpers.file2dict(self.config_path) - self.global_config = helpers.file2dict(self.global_config_path) + self.config = file2dict(self.config_path) + self.global_config = file2dict(self.global_config_path) self.config.update(self.global_config) @property @@ -150,7 +152,7 @@ def current_deal(self): def get_current_deal(self): """获取当日委托列表""" # return self.do(self.config['current_deal']) - log.warning("目前仅在 佣金宝/银河子类 中实现, 其余券商需要补充") + logger.warning("目前仅在 佣金宝/银河子类 中实现, 其余券商需要补充") @property def exchangebill(self): @@ -159,7 +161,7 @@ def exchangebill(self): :return: """ # TODO 目前仅在 华泰子类 中实现 - start_date, end_date = helpers.get_30_date() + start_date, end_date = get_30_date() return self.get_exchangebill(start_date, end_date) def get_exchangebill(self, start_date, end_date): @@ -169,7 +171,7 @@ def get_exchangebill(self, start_date, end_date): :param end_date: 20160211 :return: """ - log.warning("目前仅在 华泰子类 中实现, 其余券商需要补充") + logger.warning("目前仅在 华泰子类 中实现, 其余券商需要补充") def get_ipo_limit(self, stock_code): """ @@ -177,7 +179,7 @@ def get_ipo_limit(self, stock_code): :param stock_code: 申购代码 ID :return: """ - log.warning("目前仅在 佣金宝子类 中实现, 其余券商需要补充") + logger.warning("目前仅在 佣金宝子类 中实现, 其余券商需要补充") def do(self, params): """发起对 api 的请求并过滤返回结果 @@ -232,9 +234,13 @@ def format_response_data_type(self, response_data): for key in item: try: if re.search(int_match_str, key) is not None: - item[key] = helpers.str2num(item[key], "int") + item[key] = easytrader.utils.misc.str2num( + item[key], "int" + ) elif re.search(float_match_str, key) is not None: - item[key] = helpers.str2num(item[key], "float") + item[key] = easytrader.utils.misc.str2num( + item[key], "float" + ) except ValueError: continue return response_data diff --git a/easytrader/xq_follower.py b/easytrader/xq_follower.py index 260ffe9c..e084d0ca 100644 --- a/easytrader/xq_follower.py +++ b/easytrader/xq_follower.py @@ -7,17 +7,17 @@ from numbers import Number from threading import Thread -from . import helpers -from .follower import BaseFollower -from .log import log +from easytrader.follower import BaseFollower +from easytrader.log import logger +from easytrader.utils.misc import parse_cookies_str class XueQiuFollower(BaseFollower): - LOGIN_PAGE = 'https://www.xueqiu.com' - LOGIN_API = 'https://xueqiu.com/snowman/login' - TRANSACTION_API = 'https://xueqiu.com/cubes/rebalancing/history.json' - PORTFOLIO_URL = 'https://xueqiu.com/p/' - WEB_REFERER = 'https://www.xueqiu.com' + LOGIN_PAGE = "https://www.xueqiu.com" + LOGIN_API = "https://xueqiu.com/snowman/login" + TRANSACTION_API = "https://xueqiu.com/cubes/rebalancing/history.json" + PORTFOLIO_URL = "https://xueqiu.com/p/" + WEB_REFERER = "https://www.xueqiu.com" def __init__(self): super().__init__() @@ -31,31 +31,33 @@ def login(self, user=None, password=None, **kwargs): https://smalltool.github.io/2016/08/02/cookie/ :return: """ - cookies = kwargs.get('cookies') + cookies = kwargs.get("cookies") if cookies is None: - raise TypeError('雪球登陆需要设置 cookies, 具体见' - 'https://smalltool.github.io/2016/08/02/cookie/') + 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) - cookie_dict = helpers.parse_cookies_str(cookies) + cookie_dict = parse_cookies_str(cookies) self.s.cookies.update(cookie_dict) - log.info('登录成功') + logger.info("登录成功") def follow( # type: ignore - self, - users, - strategies, - total_assets=10000, - initial_assets=None, - adjust_sell=False, - track_interval=10, - trade_cmd_expire_seconds=120, - cmd_cache=True, - slippage: float = 0.0): + self, + users, + strategies, + total_assets=10000, + initial_assets=None, + adjust_sell=False, + track_interval=10, + trade_cmd_expire_seconds=120, + cmd_cache=True, + slippage: float = 0.0, + ): """跟踪 joinquant 对应的模拟交易,支持多用户多策略 :param users: 支持 easytrader 的用户对象,支持使用 [] 指定多个用户 :param strategies: 雪球组合名, 类似 ZH123450 @@ -76,12 +78,14 @@ def follow( # type: ignore :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) + 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 @@ -97,37 +101,34 @@ def follow( # type: ignore 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) + 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: - log.error('抽取交易id和策略名失败, 无效模拟交易url: %s', strategy_url) + 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 - }) + kwargs={"interval": track_interval, "assets": assets}, + ) strategy_worker.start() - log.info('开始跟踪策略: %s', strategy_name) + logger.info("开始跟踪策略: %s", strategy_name) - def calculate_assets(self, - strategy_url, - total_assets=None, - initial_assets=None): + def calculate_assets(self, strategy_url, total_assets=None, initial_assets=None): # 都设置时优先选择 total_assets if total_assets is None and initial_assets is not None: net_value = self._get_portfolio_net_value(strategy_url) total_assets = initial_assets * net_value if not isinstance(total_assets, Number): - raise TypeError('input assets type must be number(int, float)') + raise TypeError("input assets type must be number(int, float)") if total_assets < 1e3: - raise ValueError('雪球总资产不能小于1000元,当前预设值 {}'.format(total_assets)) + raise ValueError("雪球总资产不能小于1000元,当前预设值 {}".format(total_assets)) return total_assets @staticmethod @@ -135,29 +136,28 @@ def extract_strategy_id(strategy_url): return strategy_url def extract_strategy_name(self, strategy_url): - base_url = 'https://xueqiu.com/cubes/nav_daily/all.json?cube_symbol={}' + base_url = "https://xueqiu.com/cubes/nav_daily/all.json?cube_symbol={}" url = base_url.format(strategy_url) rep = self.s.get(url) info_index = 0 - return rep.json()[info_index]['name'] + return rep.json()[info_index]["name"] def extract_transactions(self, history): - if history['count'] <= 0: + if history["count"] <= 0: return [] rebalancing_index = 0 - raw_transactions = history['list'][rebalancing_index][ - 'rebalancing_histories'] + raw_transactions = history["list"][rebalancing_index]["rebalancing_histories"] transactions = [] for transaction in raw_transactions: - if transaction['price'] is None: - log.info('该笔交易无法获取价格,疑似未成交,跳过。交易详情: %s', transaction) + if transaction["price"] is None: + logger.info("该笔交易无法获取价格,疑似未成交,跳过。交易详情: %s", transaction) continue transactions.append(transaction) return transactions def create_query_transaction_params(self, strategy): - params = {'cube_symbol': strategy, 'page': 1, 'count': 1} + params = {"cube_symbol": strategy, "page": 1, "count": 1} return params # noinspection PyMethodOverriding @@ -169,25 +169,25 @@ def none_to_zero(self, data): # noinspection PyMethodOverriding def project_transactions(self, transactions, assets): for transaction in transactions: - weight_diff = self.none_to_zero( - transaction['weight']) - self.none_to_zero( - transaction['prev_weight']) + weight_diff = self.none_to_zero(transaction["weight"]) - self.none_to_zero( + transaction["prev_weight"] + ) - initial_amount = abs(weight_diff) / 100 * assets / transaction[ - 'price'] + initial_amount = abs(weight_diff) / 100 * assets / transaction["price"] - transaction['datetime'] = datetime.fromtimestamp( - transaction['created_at'] // 1000) + transaction["datetime"] = datetime.fromtimestamp( + transaction["created_at"] // 1000 + ) - transaction['stock_code'] = transaction['stock_symbol'].lower() + transaction["stock_code"] = transaction["stock_symbol"].lower() - transaction['action'] = 'buy' if weight_diff > 0 else 'sell' + transaction["action"] = "buy" if weight_diff > 0 else "sell" - transaction['amount'] = int(round(initial_amount, -2)) - if transaction['action'] == 'sell' and self._adjust_sell: - transaction['amount'] = self._adjust_sell_amount( - transaction['stock_code'], - transaction['amount']) + transaction["amount"] = int(round(initial_amount, -2)) + if transaction["action"] == "sell" and self._adjust_sell: + transaction["amount"] = self._adjust_sell_amount( + transaction["stock_code"], transaction["amount"] + ) def _adjust_sell_amount(self, stock_code, amount): """ @@ -207,19 +207,23 @@ def _adjust_sell_amount(self, stock_code, amount): user = self._users[0] position = user.position try: - stock = next(s for s in position if s['证券代码'] == stock_code) + stock = next(s for s in position if s["证券代码"] == stock_code) except StopIteration: - log.info('根据持仓调整 %s 卖出额,发现未持有股票 %s, 不做任何调整', - stock_code, stock_code) + logger.info("根据持仓调整 %s 卖出额,发现未持有股票 %s, 不做任何调整", stock_code, stock_code) return amount - available_amount = stock['可用余额'] + available_amount = stock["可用余额"] if available_amount >= amount: return amount adjust_amount = available_amount // 100 * 100 - log.info('股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s', - stock_code, available_amount, amount, adjust_amount) + logger.info( + "股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s", + stock_code, + available_amount, + amount, + adjust_amount, + ) return adjust_amount def _get_portfolio_info(self, portfolio_code): @@ -228,15 +232,13 @@ def _get_portfolio_info(self, portfolio_code): """ url = self.PORTFOLIO_URL + portfolio_code portfolio_page = self.s.get(url) - match_info = re.search(r'(?<=SNB.cubeInfo = ).*(?=;\n)', - portfolio_page.text) + match_info = re.search(r"(?<=SNB.cubeInfo = ).*(?=;\n)", portfolio_page.text) if match_info is None: - raise Exception( - 'cant get portfolio info, portfolio url : {}'.format(url)) + raise Exception("cant get portfolio info, portfolio url : {}".format(url)) try: portfolio_info = json.loads(match_info.group()) except Exception as e: - raise Exception('get portfolio info error: {}'.format(e)) + raise Exception("get portfolio info error: {}".format(e)) return portfolio_info def _get_portfolio_net_value(self, portfolio_code): @@ -244,4 +246,4 @@ def _get_portfolio_net_value(self, portfolio_code): 获取组合信息 """ portfolio_info = self._get_portfolio_info(portfolio_code) - return portfolio_info['net_value'] + return portfolio_info["net_value"] diff --git a/easytrader/xqtrader.py b/easytrader/xqtrader.py index 5f8f1c61..679f5c80 100644 --- a/easytrader/xqtrader.py +++ b/easytrader/xqtrader.py @@ -7,8 +7,9 @@ import requests -from . import exceptions, helpers, webtrader -from .log import log +from easytrader import exceptions, webtrader +from easytrader.log import logger +from easytrader.utils.misc import parse_cookies_str class XueQiuTrader(webtrader.WebTrader): @@ -59,7 +60,7 @@ def _set_cookies(self, cookies): :param cookies: 雪球 cookies :type cookies: str """ - cookie_dict = helpers.parse_cookies_str(cookies) + cookie_dict = parse_cookies_str(cookies) self.s.cookies.update(cookie_dict) def _prepare_account(self, user="", password="", **kwargs): @@ -365,7 +366,7 @@ def adjust_weight(self, stock_code, weight): remain_weight = 100 - sum(i.get("weight") for i in position_list) cash = round(remain_weight, 2) - log.debug("调仓比例:%f, 剩余持仓 :%f", weight, remain_weight) + logger.info("调仓比例:%f, 剩余持仓 :%f", weight, remain_weight) data = { "cash": cash, "holdings": str(json.dumps(position_list)), @@ -378,19 +379,19 @@ def adjust_weight(self, stock_code, weight): resp = self.s.post(self.config["rebalance_url"], data=data) # pylint: disable=broad-except except Exception as e: - log.warning("调仓失败: %s ", e) + logger.warning("调仓失败: %s ", e) return None - log.debug("调仓 %s: 持仓比例%d", stock["name"], weight) + logger.info("调仓 %s: 持仓比例%d", stock["name"], weight) resp_json = json.loads(resp.text) if "error_description" in resp_json and resp.status_code != 200: - log.error("调仓错误: %s", resp_json["error_description"]) + logger.error("调仓错误: %s", resp_json["error_description"]) return [ { "error_no": resp_json["error_code"], "error_info": resp_json["error_description"], } ] - log.debug("调仓成功 %s: 持仓比例%d", stock["name"], weight) + logger.info("调仓成功 %s: 持仓比例%d", stock["name"], weight) return None def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): @@ -479,7 +480,7 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): * 100 ) cash = round(cash, 2) - log.debug("weight:%f, cash:%f", weight, cash) + logger.info("weight:%f, cash:%f", weight, cash) data = { "cash": cash, @@ -493,15 +494,15 @@ def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"): resp = self.s.post(self.config["rebalance_url"], data=data) # pylint: disable=broad-except except Exception as e: - log.warning("调仓失败: %s ", e) + logger.warning("调仓失败: %s ", e) return None else: - log.debug( + 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: - log.error("调仓错误: %s", resp_json["error_description"]) + logger.error("调仓错误: %s", resp_json["error_description"]) return [ { "error_no": resp_json["error_code"], diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index 0a32ee73..c0c90c0c 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -4,7 +4,8 @@ import pywinauto -from . import clienttrader, grid_strategies, helpers +from easytrader import clienttrader, grid_strategies +from easytrader.utils.captcha import recognize_verify_code class YHClientTrader(clienttrader.BaseLoginClientTrader): @@ -99,13 +100,14 @@ def _handle_verify_code(self, is_xiadan): control.capture_as_image(rect).save(file_path, "jpeg") else: control.capture_as_image().save(file_path, "jpeg") - verify_code = helpers.recognize_verify_code(file_path, "yh_client") + verify_code = recognize_verify_code(file_path, "yh_client") return "".join(re.findall(r"\d+", verify_code)) @property def balance(self): self._switch_left_menus(self._config.BALANCE_MENU_PATH) return self._get_grid_data(self._config.BALANCE_GRID_CONTROL_ID) + def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) @@ -118,7 +120,7 @@ def auto_ipo(self): return {"message": "没有发现可以申购的新股"} self.wait(0.1) # for row in invalid_list_idx: - # self._click_grid_by_row(row) + # self._click_grid_by_row(row) self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) self.wait(0.1) return self._handle_pop_dialogs() diff --git a/requirements.txt b/requirements.txt index cdbd20b5..05b9a2fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,4 @@ rqopen-client==0.0.5 six==1.11.0 urllib3==1.23; python_version != '3.1.*' werkzeug==0.14.1 +jsonpickle==1.2 From 428b87b6d562f23f1d94093429335738f851feb8 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 20:58:55 +0800 Subject: [PATCH 213/276] :star: ht client trader changes default grid copy strategy to xls --- easytrader/api.py | 2 +- easytrader/ht_clienttrader.py | 86 +---------------------------------- easytrader/wk_clienttrader.py | 56 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 85 deletions(-) create mode 100644 easytrader/wk_clienttrader.py diff --git a/easytrader/api.py b/easytrader/api.py index 736a1a77..a6b76686 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -44,7 +44,7 @@ def use(broker, debug=False, **kwargs): return HTClientTrader() if broker.lower() in ["wk_client", "五矿客户端"]: - from .ht_clienttrader import WKClientTrader + from easytrader.wk_clienttrader import WKClientTrader return WKClientTrader() diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index d16f167e..d7853387 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- -import logging import pywinauto import pywinauto.clipboard +from easytrader import grid_strategies from . import clienttrader class HTClientTrader(clienttrader.BaseLoginClientTrader): - - login_test_host: bool = True + grid_strategy = grid_strategies.Xls @property def broker_type(self): @@ -43,23 +42,6 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break except RuntimeError: pass - self.login_test_host = False - if self.login_test_host: - self._app.top_window().type_keys("%t") - self.wait(0.5) - try: - self._app.top_window().Button2.wait( - "enabled", timeout=30, retry_interval=5 - ) - self._app.top_window().Button5.check() # enable 自动选择 - self.wait(0.5) - self._app.top_window().Button3.click() - self.wait(0.3) - except Exception as ex: - logging.exception("test speed error", ex) - self._app.top_window().wrapper_object().close() - self.wait(0.3) - self._app.top_window().Edit1.set_focus() self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) @@ -94,67 +76,3 @@ def _get_balance_from_statics(self): return result -class WKClientTrader(HTClientTrader): - @property - def broker_type(self): - return "wk" - - def login(self, user, password, exe_path, comm_password=None, **kwargs): - """ - :param user: 用户名 - :param password: 密码 - :param exe_path: 客户端路径, 类似 - :param comm_password: - :param kwargs: - :return: - """ - self._editor_need_type_keys = False - if comm_password is None: - raise ValueError("五矿必须设置通讯密码") - - try: - self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=1 - ) - # pylint: disable=broad-except - except Exception: - self._app = pywinauto.Application().start(exe_path) - - # wait login window ready - while True: - try: - self._app.top_window().Edit1.wait("ready") - break - except RuntimeError: - pass - # self.login_test_host = False - # if self.login_test_host: - # self._app.top_window().type_keys("%t") - # self.wait(0.5) - # try: - # self._app.top_window().Button2.wait('enabled', timeout=30, retry_interval=5) - # self._app.top_window().Button5.check() # enable 自动选择 - # self.wait(0.5) - # self._app.top_window().Button3.click() - # self.wait(0.3) - # except Exception as ex: - # logging.exception("test speed error", ex) - # self._app.top_window().wrapper_object().close() - # self.wait(0.3) - - self._app.top_window().Edit1.set_focus() - self._app.top_window().Edit1.set_edit_text(user) - self._app.top_window().Edit2.set_edit_text(password) - - self._app.top_window().Edit3.set_edit_text(comm_password) - - self._app.top_window().Button1.click() - - # detect login is success or not - self._app.top_window().wait_not("exists", 100) - - self._app = pywinauto.Application().connect( - path=self._run_exe_path(exe_path), timeout=10 - ) - self._close_prompt_windows() - self._main = self._app.window(title="网上股票交易系统5.0") diff --git a/easytrader/wk_clienttrader.py b/easytrader/wk_clienttrader.py new file mode 100644 index 00000000..8906d8ce --- /dev/null +++ b/easytrader/wk_clienttrader.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +import pywinauto + +from easytrader.ht_clienttrader import HTClientTrader + + +class WKClientTrader(HTClientTrader): + @property + def broker_type(self): + return "wk" + + def login(self, user, password, exe_path, comm_password=None, **kwargs): + """ + :param user: 用户名 + :param password: 密码 + :param exe_path: 客户端路径, 类似 + :param comm_password: + :param kwargs: + :return: + """ + self._editor_need_type_keys = False + if comm_password is None: + raise ValueError("五矿必须设置通讯密码") + + try: + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1 + ) + # pylint: disable=broad-except + except Exception: + self._app = pywinauto.Application().start(exe_path) + + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait("ready") + break + except RuntimeError: + pass + + self._app.top_window().Edit1.set_focus() + self._app.top_window().Edit1.set_edit_text(user) + self._app.top_window().Edit2.set_edit_text(password) + + self._app.top_window().Edit3.set_edit_text(comm_password) + + self._app.top_window().Button1.click() + + # detect login is success or not + self._app.top_window().wait_not("exists", 100) + + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10 + ) + self._close_prompt_windows() + self._main = self._app.window(title="网上股票交易系统5.0") \ No newline at end of file From 5cd4a5fb25400783c2b1be2300117e1834ff1913 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 21:05:14 +0800 Subject: [PATCH 214/276] :star: remove jsonpickle --- easytrader/utils/perf.py | 5 ++--- requirements.txt | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/easytrader/utils/perf.py b/easytrader/utils/perf.py index 2d6a48a2..7d815ded 100644 --- a/easytrader/utils/perf.py +++ b/easytrader/utils/perf.py @@ -3,7 +3,6 @@ import logging import timeit -import jsonpickle from easytrader import logger try: @@ -36,8 +35,8 @@ def wrapper(*args, **kwargs): f.__name__, te - ts, ce - cs, - jsonpickle.dumps(args[1:]), - jsonpickle.dumps(kwargs), + args[1:], + kwargs, ) ) if ex is not None: diff --git a/requirements.txt b/requirements.txt index 05b9a2fa..cdbd20b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,3 @@ rqopen-client==0.0.5 six==1.11.0 urllib3==1.23; python_version != '3.1.*' werkzeug==0.14.1 -jsonpickle==1.2 From 8f56583c8ee5437b9c6c48d9d60db3ea24a3dab3 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 21:40:15 +0800 Subject: [PATCH 215/276] :star: refactor code --- easytrader/clienttrader.py | 28 +++++++++++----------------- easytrader/grid_strategies.py | 21 +++++++++------------ easytrader/pop_dialog_handler.py | 1 - easytrader/utils/captcha.py | 1 - requirements.txt | 2 +- 5 files changed, 21 insertions(+), 32 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 8731e271..7696cf3f 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -9,10 +9,11 @@ from typing import Type import easyutils -from pywinauto import findwindows +from pywinauto import findwindows, timings from easytrader import grid_strategies, pop_dialog_handler from easytrader.config import client +from easytrader.log import logger from easytrader.utils.misc import file2dict from easytrader.utils.perf import perf_clock from win32gui import SetForegroundWindow, ShowWindow @@ -51,6 +52,10 @@ def refresh(self): """Refresh data""" pass + @abc.abstractmethod + def is_exist_pop_dialog(self): + pass + class ClientTrader(IClientTrader): _editor_need_type_keys = False @@ -279,7 +284,6 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} invalid_list_idx = [ - # i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 or v["证券代码"][:2] == "78" i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 @@ -312,15 +316,15 @@ def _click_grid_by_row(self, row): ).click(coords=(x, y)) @perf_clock - def _is_exist_pop_dialog(self): + def is_exist_pop_dialog(self): self.wait(0.5) # wait dialog display try: return ( self._main.wrapper_object() != self._app.top_window().wrapper_object() ) - except findwindows.ElementNotFoundError | pywinauto.timings.TimeoutError | RuntimeError as ex: - logging.exception(ex) + except (findwindows.ElementNotFoundError, timings.TimeoutError, RuntimeError) as ex: + logger.exception('check pop dialog timeout') return False def _run_exe_path(self, exe_path): @@ -385,13 +389,6 @@ def _get_pop_dialog_title(self): .window_text() ) - # def _get_pop_dialog_title(self): - # return ( - # self._app.top_window() - # .window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) - # .window_text() - # ) - def _set_trade_params(self, security, price, amount): code = security[-6:] @@ -459,8 +456,6 @@ def _collapse_left_menus(self): @perf_clock def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() - # self._app.top_window().type_keys('{ESC}') - # self._app.top_window().type_keys('{F5}') self.wait(sleep) def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): @@ -482,8 +477,7 @@ def _get_left_menus_handle(self): return handle # pylint: disable=broad-except except Exception as ex: - print(ex) - pass + logger.exception('error occurred when trying to get left menus') count = count - 1 def _cancel_entrust_by_double_click(self, row): @@ -506,7 +500,7 @@ def _handle_pop_dialogs( ): handler = handler_class(self._app) - while self._is_exist_pop_dialog(): + while self.is_exist_pop_dialog(): try: title = self._get_pop_dialog_title() except pywinauto.findwindows.ElementNotFoundError: diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index e944a7ec..f483abdb 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Dict, List import pandas as pd +import pywinauto.keyboard import pywinauto import pywinauto.clipboard from pywinauto import win32defines @@ -65,7 +66,7 @@ def _set_foreground(self, grid=None): class Copy(BaseStrategy): """ - 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 + 通过复制 grid 内容到剪切板再读取来获取 grid 内容 """ _need_captcha_reg = True @@ -150,7 +151,7 @@ def _get_clipboard_data(self) -> str: class WMCopy(Copy): """ - 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 + 通过复制 grid 内容到剪切板再读取来获取 grid 内容 """ def get(self, control_id: int) -> List[Dict]: @@ -177,18 +178,15 @@ def get(self, control_id: int) -> List[Dict]: grid.type_keys("^s", set_foreground=False) count = 10 while count > 0: - if self._trader._is_exist_pop_dialog(): + if self._trader.is_exist_pop_dialog(): break self._trader.wait(0.2) count -= 1 temp_path = tempfile.mktemp(suffix=".csv") self._set_foreground(self._trader.app.top_window()) - # self._trader.app.top_window().type_keys(self.normalize_path(temp_path), set_foreground=False) # alt+s保存,alt+y替换已存在的文件 - # # self._set_foreground(self._trader.app.top_window()) - # self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False) self._trader.app.top_window().Edit1.set_edit_text( self.normalize_path(temp_path) ) @@ -196,11 +194,11 @@ def get(self, control_id: int) -> List[Dict]: 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(): + if self._trader.is_exist_pop_dialog(): self._trader.app.top_window().Button2.click() self._trader.wait(0.2) - # Wait until file save complete otherwise pandas can not find file return self._format_grid_data(temp_path) @@ -208,12 +206,11 @@ def normalize_path(self, temp_path: str) -> str: return temp_path.replace("~", "{~}") def _format_grid_data(self, data: str) -> List[Dict]: - f = open(data, encoding="gbk", errors="replace") - cont = f.read() - f.close() + with open(data, encoding="gbk", errors="replace") as f: + content = f.read() df = pd.read_csv( - StringIO(cont), + StringIO(content), delimiter="\t", dtype=self._trader.config.GRID_DTYPE, na_filter=False, diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index 5142745c..0394a187 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -40,7 +40,6 @@ def handle(self, title): def _extract_content(self): return self._app.top_window().Static.window_text() - @perf_clock def _extract_entrust_id(self, content): return re.search(r"\d+", content).group() diff --git a/easytrader/utils/captcha.py b/easytrader/utils/captcha.py index 818ec66f..cfbfc009 100644 --- a/easytrader/utils/captcha.py +++ b/easytrader/utils/captcha.py @@ -20,7 +20,6 @@ def captcha_recognize(img_path): table.append(1) out = im.point(table, "1") - # out.show() # 2. recognize with tesseract num = pytesseract.image_to_string(out) return num diff --git a/requirements.txt b/requirements.txt index cdbd20b5..0dddfec3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -win32gui -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com beautifulsoup4==4.6.0 @@ -30,3 +29,4 @@ rqopen-client==0.0.5 six==1.11.0 urllib3==1.23; python_version != '3.1.*' werkzeug==0.14.1 +win32gui From 40b1cf6424b62a3362623ab75216f08ea4c94e48 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 21:48:41 +0800 Subject: [PATCH 216/276] :star: update README.md --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 79ddcc8b..d40e2706 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,15 @@ * 实现自动登录 * 支持通过 webserver 远程操作客户端 * 支持命令行调用,方便其他语言适配 -* 基于 Python3, Win。注: Linux 仅支持雪球 +* 基于 Python3.6, Win。注: Linux 仅支持雪球 * 有兴趣的可以加群 `556050652` 一起讨论 ## 公众号 -扫码关注“易量化”的微信公众号,不定时更新一些个人文章及与大家交流 +欢迎大家扫码关注我的个人公众号"食灯鬼",一起交流 ![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) -## 赞助: - -![微信](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/wechat_pay_qr.png) ![支付宝](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/alipay_qr.jpeg) ### 相关 @@ -32,13 +29,10 @@ ### 支持券商 -* 银河客户端, 须在 `windows` 平台下载 `银河双子星` 客户端 * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) * 其他券商通用同花顺客户端(需要手动登陆) -注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,我在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供了几个券商老版本的下载地址。 - ### 模拟交易 From da70dcb2d1870d3c8fc4e5a64201e2fd3e4dcc1d Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 21:49:12 +0800 Subject: [PATCH 217/276] =?UTF-8?q?Bump=20version:=200.18.4=20=E2=86=92=20?= =?UTF-8?q?0.18.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b3a39ac0..3d00b0a2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.4 +current_version = 0.18.5 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 87ff2b0a..0958dc15 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.18.4" +__version__ = "0.18.5" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index f83c7d2e..29b47bde 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.4", + version="0.18.5", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 7e62935f3934587a19c417e3a4be2c4ba324a9d0 Mon Sep 17 00:00:00 2001 From: lhztop Date: Sun, 12 Jan 2020 21:51:22 +0800 Subject: [PATCH 218/276] modify for easytrader (#354) * bugfix: 1) sendkeys escape ~ in temp dir * :star: use type annotation * error handler * modify to 2 * add perf clock * market buy sell * add log * perf on property * bugfix: set params on market sell/buy when ttype * bugfix: setforegroundwindow instead of setfocus * bugfix2 * modify type keys * add set foreground * wrap * guozhai * delete else * modify * test * close prompt * ipo no kechuangban * kcb * auto test * timeout * timeout * dd * add kcb limit price when market trade * client trader * kcb limit price * kcb * trader * log info * log * click cancel * easy diff * log * t * gbk error * modify * commit * trade * trade * log close * close_pormpt_window_no_wait * error * client * delete shortcuts * wk client Co-authored-by: shidenggui --- easytrader/__init__.py | 39 +++++++ easytrader/api.py | 12 ++- easytrader/clienttrader.py | 179 ++++++++++++++++++++++++------- easytrader/config/client.py | 6 ++ easytrader/grid_strategies.py | 127 ++++++++++++++++++---- easytrader/ht_clienttrader.py | 91 +++++++++++++++- easytrader/pop_dialog_handler.py | 29 ++++- easytrader/utils/__init__.py | 9 ++ easytrader/utils/captcha.py | 20 ++++ requirements.txt | 1 + 10 files changed, 444 insertions(+), 69 deletions(-) create mode 100644 easytrader/utils/__init__.py create mode 100644 easytrader/utils/captcha.py diff --git a/easytrader/__init__.py b/easytrader/__init__.py index dea68363..fdd14f90 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -3,8 +3,47 @@ from . import exceptions from .api import use, follower +import jsonpickle urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) __version__ = "0.18.4" __author__ = "shidenggui" + + +try: + from time import process_time +except: + from time import clock as process_time +import timeit +#from decorator import decorator +import logging + +internal_logger = logging.getLogger("PERF") + +# Decorator +def perf_clock(logger=None): + if logger is None: + logger = internal_logger + + def perf_decorator(method): + + #@decorator + def timed(*args, **kw): + ts = timeit.default_timer() + cs = process_time() + ex = None + result = None + try: + result = method(*args, **kw) + except Exception as ex1: + ex = ex1 + + te = timeit.default_timer() + ce = process_time() + logger.info('%r consume %2.4f sec, cpu %2.4f sec. args %s, extra args %s' % (method.__name__, te-ts, ce-cs, jsonpickle.dumps(args[1:]), jsonpickle.dumps(kw))) + if ex is not None: + raise ex + return result + return timed + return perf_decorator \ No newline at end of file diff --git a/easytrader/api.py b/easytrader/api.py index c53e8e22..5beb2142 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -30,21 +30,25 @@ def use(broker, debug=True, **kwargs): log.setLevel(logging.INFO) if broker.lower() in ["xq", "雪球"]: return XueQiuTrader(**kwargs) + if broker.lower() in ["yh_client", "银河客户端"]: from .yh_clienttrader import YHClientTrader - return YHClientTrader() + if broker.lower() in ["ht_client", "华泰客户端"]: from .ht_clienttrader import HTClientTrader - return HTClientTrader() + + if broker.lower() in ["wk_client", "五矿客户端"]: + from .ht_clienttrader import WKClientTrader + return WKClientTrader() + if broker.lower() in ["gj_client", "国金客户端"]: from .gj_clienttrader import GJClientTrader - return GJClientTrader() + if broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader - return ClientTrader() raise NotImplementedError diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index f66e92e6..e8361f91 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -6,10 +6,17 @@ import time from typing import Type +from pywinauto import findwindows + +from . import perf_clock + import easyutils +import re from . import grid_strategies, helpers, pop_dialog_handler from .config import client +from win32gui import SetForegroundWindow, ShowWindow +import logging if not sys.platform.startswith("darwin"): import pywinauto @@ -47,14 +54,30 @@ def refresh(self): class ClientTrader(IClientTrader): + _editor_need_type_keys = False # The strategy to use for getting grid data grid_strategy: Type[grid_strategies.IGridStrategy] = grid_strategies.Copy + _grid_strategy_instance: grid_strategies.IGridStrategy = None + + @property + def grid_strategy_instance(self): + if self._grid_strategy_instance is None: + self._grid_strategy_instance = self.grid_strategy(self) + return self._grid_strategy_instance def __init__(self): self._config = client.create(self.broker_type) self._app = None self._main = None + def _set_foreground(self, grid=None): + if grid is None: + grid = self._trader.main + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(grid.wrapper_object(), 9) # restore window state + else: + SetForegroundWindow(grid.wrapper_object()) # bring to front + @property def app(self): return self._app @@ -130,6 +153,7 @@ def cancel_entrusts(self): return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) + @perf_clock() def cancel_entrust(self, entrust_no): self.refresh() for i, entrust in enumerate(self.cancel_entrusts): @@ -141,17 +165,20 @@ def cancel_entrust(self, entrust_no): return self._handle_pop_dialogs() return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} + @perf_clock() def buy(self, security, price, amount, **kwargs): self._switch_left_menus(["买入[F1]"]) return self.trade(security, price, amount) + @perf_clock() def sell(self, security, price, amount, **kwargs): self._switch_left_menus(["卖出[F2]"]) return self.trade(security, price, amount) - def market_buy(self, security, amount, ttype=None, **kwargs): + @perf_clock() + def market_buy(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价买入 :param security: 六位证券代码 @@ -159,14 +186,16 @@ def market_buy(self, security, amount, ttype=None, **kwargs): :param ttype: 市价委托类型,默认客户端默认选择, 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] + :param limit_price: 科创板 限价 :return: {'entrust_no': '委托单号'} """ self._switch_left_menus(["市价委托", "买入"]) - return self.market_trade(security, amount, ttype) + return self.market_trade(security, amount, ttype, limit_price=limit_price) - def market_sell(self, security, amount, ttype=None, **kwargs): + @perf_clock() + def market_sell(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价卖出 :param security: 六位证券代码 @@ -174,14 +203,14 @@ def market_sell(self, security, amount, ttype=None, **kwargs): :param ttype: 市价委托类型,默认客户端默认选择, 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] - + :param limit_price: 科创板 限价 :return: {'entrust_no': '委托单号'} """ self._switch_left_menus(["市价委托", "卖出"]) - return self.market_trade(security, amount, ttype) + return self.market_trade(security, amount, ttype, limit_price=limit_price) - def market_trade(self, security, amount, ttype=None, **kwargs): + def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价交易 :param security: 六位证券代码 @@ -192,9 +221,19 @@ def market_trade(self, security, amount, ttype=None, **kwargs): :return: {'entrust_no': '委托单号'} """ - self._set_market_trade_params(security, amount) + code = security[-6:] + self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) if ttype is not None: - self._set_market_trade_type(ttype) + retry = 0 + retry_max = 10 + while retry < retry_max: + try: + self._set_market_trade_type(ttype) + break + except: + retry += 1 + self.wait(0.1) + self._set_market_trade_params(security, amount, limit_price=limit_price) self._submit_trade() return self._handle_pop_dialogs( @@ -207,15 +246,17 @@ def _set_market_trade_type(self, ttype): control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, class_name="ComboBox", ) - for i, text in selects.texts(): + for i, text in enumerate(selects.texts()): # skip 0 index, because 0 index is current select index if i == 0: - continue - if ttype in text: + if re.search(ttype, text): # 当前已经选中 + return + else: + continue + if re.search(ttype, text): selects.select(i - 1) - break - else: - raise TypeError("不支持对应的市价类型: {}".format(ttype)) + return + raise TypeError("不支持对应的市价类型: {}".format(ttype)) def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) @@ -225,6 +266,7 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} invalid_list_idx = [ + # i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 or v["证券代码"][:2] == "78" i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 ] @@ -254,12 +296,17 @@ def _click_grid_by_row(self, row): class_name="CVirtualGridCtrl", ).click(coords=(x, y)) + @perf_clock() def _is_exist_pop_dialog(self): - self.wait(0.2) # wait dialog display - return ( - self._main.wrapper_object() - != self._app.top_window().wrapper_object() - ) + self.wait(0.5) # wait dialog display + try: + return ( + self._main.wrapper_object() + != self._app.top_window().wrapper_object() + ) + except findwindows.ElementNotFoundError|pywinauto.timings.TimeoutError|RuntimeError as ex: + logging.exception(ex) + return False def _run_exe_path(self, exe_path): return os.path.join(os.path.dirname(exe_path), "xiadan.exe") @@ -272,10 +319,19 @@ def exit(self): def _close_prompt_windows(self): self.wait(1) + for window in self._app.windows(class_name="#32770", visible_only=True): + title = window.window_text() + if title != self._config.TITLE: + logging.info("close " + title) + window.close() + self.wait(0.2) + self.wait(1) + + def close_pormpt_window_no_wait(self): for window in self._app.windows(class_name="#32770"): if window.window_text() != self._config.TITLE: window.close() - self.wait(1) + def trade(self, security, price, amount): self._set_trade_params(security, price, amount) @@ -291,56 +347,92 @@ def _click(self, control_id): control_id=control_id, class_name="Button" ).click() + @perf_clock() def _submit_trade(self): - time.sleep(0.05) + time.sleep(0.2) self._main.child_window( control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name="Button", ).click() + @perf_clock() + def __get_top_window_pop_dialog(self): + return self._app.top_window().window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + + @perf_clock() def _get_pop_dialog_title(self): return ( self._app.top_window() .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) .window_text() ) + # def _get_pop_dialog_title(self): + # return ( + # self._app.top_window() + # .window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + # .window_text() + # ) def _set_trade_params(self, security, price, amount): code = security[-6:] - self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) + self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish self.wait(0.1) - self._type_keys( + self._type_edit_control_keys( self._config.TRADE_PRICE_CONTROL_ID, easyutils.round_price_by_code(price, code), ) - self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) - - def _set_market_trade_params(self, security, amount): - code = security[-6:] + self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) - self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) - - # wait security input finish + def _set_market_trade_params(self, security, amount, limit_price=None): + self._type_edit_control_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) self.wait(0.1) + price_control = None + if str(security).startswith("68"): # 科创板存在限价 + try: + price_control = self._main.child_window( + control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit" + ) + except: + pass + if price_control is not None: + price_control.set_edit_text(limit_price) - self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) def _get_grid_data(self, control_id): - return self.grid_strategy(self).get(control_id) + return self.grid_strategy_instance.get(control_id) def _type_keys(self, control_id, text): - self._main.child_window( - control_id=control_id, class_name="Edit" - ).set_edit_text(text) + self._main.child_window(control_id=control_id, class_name="Edit").set_edit_text(text) + def _type_common_control_keys(self, control, text): + self._set_foreground(control) + control.type_keys(text, set_foreground=False) + + def _type_edit_control_keys(self, control_id, text): + if not self._editor_need_type_keys: + self._main.child_window( + control_id=control_id, class_name="Edit" + ).set_edit_text(text) + else: + editor = self._main.child_window( + control_id=control_id, class_name="Edit") + editor.select() + editor.type_keys(text) + + def _collapse_left_menus(self): + items = self._get_left_menus_handle().roots() + for item in items: + item.collapse() + + @perf_clock() def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() - self._app.top_window().type_keys('{ESC}') - self._app.top_window().type_keys('{F5}') + # self._app.top_window().type_keys('{ESC}') + # self._app.top_window().type_keys('{F5}') self.wait(sleep) def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): @@ -349,17 +441,22 @@ def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): @functools.lru_cache() def _get_left_menus_handle(self): + count = 2 while True: try: handle = self._main.child_window( control_id=129, class_name="SysTreeView32" ) + if count <= 0: + return handle # sometime can't find handle ready, must retry handle.wait("ready", 2) return handle # pylint: disable=broad-except - except Exception: + except Exception as ex: + print(ex) pass + count = count - 1 def _cancel_entrust_by_double_click(self, row): x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN @@ -375,13 +472,17 @@ def _cancel_entrust_by_double_click(self, row): def refresh(self): self._switch_left_menus(["买入[F1]"], sleep=0.05) + @perf_clock() def _handle_pop_dialogs( self, handler_class=pop_dialog_handler.PopDialogHandler ): handler = handler_class(self._app) while self._is_exist_pop_dialog(): - title = self._get_pop_dialog_title() + try: + title = self._get_pop_dialog_title() + except pywinauto.findwindows.ElementNotFoundError: + return {"message": "success"} result = handler.handle(title) if result: diff --git a/easytrader/config/client.py b/easytrader/config/client.py index 69207789..a2fb4cc8 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -8,6 +8,8 @@ def create(broker): return GJ if broker == "ths": return CommonConfig + if broker == "wk": + return WK raise NotImplementedError @@ -129,3 +131,7 @@ class GJ(CommonConfig): } AUTO_IPO_MENU_PATH = ["新股申购", "新股批量申购"] + + +class WK(HT): + pass \ No newline at end of file diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 9949fd2c..38614a62 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -2,12 +2,23 @@ import abc import io import tempfile + +from pywinauto import win32defines from typing import TYPE_CHECKING, Dict, List import pandas as pd import pywinauto.clipboard +from .utils import SetForegroundWindow, ShowWindow +import pywinauto +import logging +try: + import StringIO +except: + from io import StringIO from .log import log +from .utils.captcha import captcha_recognize + if TYPE_CHECKING: # pylint: disable=unused-import @@ -44,34 +55,94 @@ def _get_grid(self, control_id: int): ) return grid + def _set_foreground(self, grid=None): + try: + if grid is None: + grid = self._trader.main + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(grid.wrapper_object(), 9) # restore window state + else: + SetForegroundWindow(grid.wrapper_object()) # bring to front + except: + pass + class Copy(BaseStrategy): """ 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 """ + _need_captcha_reg = True def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) - grid.type_keys("^A^C") + self._set_foreground(grid) + grid.type_keys("^A^C", set_foreground=False) content = self._get_clipboard_data() return self._format_grid_data(content) def _format_grid_data(self, data: str) -> List[Dict]: - df = pd.read_csv( - io.StringIO(data), - delimiter="\t", - dtype=self._trader.config.GRID_DTYPE, - na_filter=False, - ) - return df.to_dict("records") + try: + df = pd.read_csv( + io.StringIO(data), + delimiter="\t", + dtype=self._trader.config.GRID_DTYPE, + na_filter=False, + ) + return df.to_dict("records") + except: + Copy._need_captcha_reg = True def _get_clipboard_data(self) -> str: - while True: + 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) # 识别验证码 + log.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: + log.info(self._trader.app.top_window().window(control_id=0x966, class_name='Static').window_text()) + except Exception as ex: # 窗体消失 + log.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: - log.warning("%s, retry ......", e) + count -= 1 + log.exception("%s, retry ......", e) + + +class WMCopy(Copy): + """ + 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 + """ + + def get(self, control_id: int) -> List[Dict]: + grid = self._get_grid(control_id) + grid.post_message(win32defines.WM_COMMAND, 0xe122, 0) + self._trader.wait(0.1) + content = self._get_clipboard_data() + return self._format_grid_data(content) class Xls(BaseStrategy): @@ -84,28 +155,44 @@ def get(self, control_id: int) -> List[Dict]: grid = self._get_grid(control_id) # ctrl+s 保存 grid 内容为 xls 文件 - grid.type_keys("^s") - self._trader.wait(1) + 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") - self._trader.app.top_window().type_keys(self.normalize_path(temp_path)) + self._set_foreground(self._trader.app.top_window()) + # self._trader.app.top_window().type_keys(self.normalize_path(temp_path), set_foreground=False) - # Wait until file save complete - self._trader.wait(0.3) # alt+s保存,alt+y替换已存在的文件 - self._trader.app.top_window().type_keys("%{s}%{y}") - # Wait until file save complete otherwise pandas can not find file + # # self._set_foreground(self._trader.app.top_window()) + # self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False) + self._trader.app.top_window().Edit1.set_edit_text(self.normalize_path(temp_path)) + self._trader.wait(0.1) + self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False) self._trader.wait(0.2) + if self._trader._is_exist_pop_dialog(): + self._trader.app.top_window().Button2.click() + self._trader.wait(0.2) + # Wait until file save complete otherwise pandas can not find file + return self._format_grid_data(temp_path) def normalize_path(self, temp_path: str) -> str: - return temp_path.replace("~", "{~}") + return temp_path.replace('~', '{~}') def _format_grid_data(self, data: str) -> List[Dict]: + f = open(data, encoding="gbk", errors='replace') + cont = f.read() + f.close() + df = pd.read_csv( - data, - encoding="gbk", + StringIO(cont), delimiter="\t", dtype=self._trader.config.GRID_DTYPE, na_filter=False, diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index e39f856c..98b916b8 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- import pywinauto import pywinauto.clipboard +from win32gui import SetForegroundWindow from . import clienttrader - +import logging class HTClientTrader(clienttrader.BaseLoginClientTrader): + + login_test_host: bool = True + @property def broker_type(self): return "ht" @@ -19,6 +23,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): :param kwargs: :return: """ + self._editor_need_type_keys = False if comm_password is None: raise ValueError("华泰必须设置通讯密码") @@ -37,7 +42,22 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): break except RuntimeError: pass + self.login_test_host = False + if self.login_test_host: + self._app.top_window().type_keys("%t") + self.wait(0.5) + try: + self._app.top_window().Button2.wait('enabled',timeout=30, retry_interval=5) + self._app.top_window().Button5.check() # enable 自动选择 + self.wait(0.5) + self._app.top_window().Button3.click() + self.wait(0.3) + except Exception as ex: + logging.exception("test speed error", ex) + self._app.top_window().wrapper_object().close() + self.wait(0.3) + self._app.top_window().Edit1.set_focus() self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) @@ -46,7 +66,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().button0.click() # detect login is success or not - self._app.top_window().wait_not("exists", 10) + self._app.top_window().wait_not("exists", 100) self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=10 @@ -69,3 +89,70 @@ def _get_balance_from_statics(self): ).window_text() ) return result + + +class WKClientTrader(HTClientTrader): + + @property + def broker_type(self): + return "wk" + + def login(self, user, password, exe_path, comm_password=None, **kwargs): + """ + :param user: 用户名 + :param password: 密码 + :param exe_path: 客户端路径, 类似 + :param comm_password: + :param kwargs: + :return: + """ + self._editor_need_type_keys = False + if comm_password is None: + raise ValueError("五矿必须设置通讯密码") + + try: + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1 + ) + # pylint: disable=broad-except + except Exception: + self._app = pywinauto.Application().start(exe_path) + + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait("ready") + break + except RuntimeError: + pass + # self.login_test_host = False + # if self.login_test_host: + # self._app.top_window().type_keys("%t") + # self.wait(0.5) + # try: + # self._app.top_window().Button2.wait('enabled', timeout=30, retry_interval=5) + # self._app.top_window().Button5.check() # enable 自动选择 + # self.wait(0.5) + # self._app.top_window().Button3.click() + # self.wait(0.3) + # except Exception as ex: + # logging.exception("test speed error", ex) + # self._app.top_window().wrapper_object().close() + # self.wait(0.3) + + self._app.top_window().Edit1.set_focus() + self._app.top_window().Edit1.set_edit_text(user) + self._app.top_window().Edit2.set_edit_text(password) + + self._app.top_window().Edit3.set_edit_text(comm_password) + + self._app.top_window().Button1.click() + + # detect login is success or not + self._app.top_window().wait_not("exists", 100) + + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10 + ) + self._close_prompt_windows() + self._main = self._app.window(title="网上股票交易系统5.0") \ No newline at end of file diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index e264d5db..5aa2b7f3 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -3,13 +3,23 @@ import time from typing import Optional -from . import exceptions - +from . import exceptions, perf_clock +import pywinauto +from .utils import SetForegroundWindow, ShowWindow class PopDialogHandler: def __init__(self, app): self._app = app + def _set_foreground(self, grid=None): + if grid is None: + grid = self._trader.main + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + ShowWindow(grid.wrapper_object(), 9) # restore window state + else: + SetForegroundWindow(grid.wrapper_object()) # bring to front + + @perf_clock() def handle(self, title): if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): self._submit_by_shortcut() @@ -27,20 +37,27 @@ def handle(self, title): def _extract_content(self): return self._app.top_window().Static.window_text() + @perf_clock() def _extract_entrust_id(self, content): return re.search(r"\d+", content).group() def _submit_by_click(self): - self._app.top_window()["确定"].click() + try: + self._app.top_window()["确定"].click() + except Exception as ex: + self._app.Window_(best_match='Dialog', top_level_only=True).ChildWindow(best_match='确定').click() def _submit_by_shortcut(self): - self._app.top_window().type_keys("%Y") + self._set_foreground(self._app.top_window()) + self._app.top_window().type_keys("%Y", set_foreground=False) def _close(self): self._app.top_window().close() class TradePopDialogHandler(PopDialogHandler): + + @perf_clock() def handle(self, title) -> Optional[dict]: if title == "委托确认": self._submit_by_shortcut() @@ -56,6 +73,10 @@ def handle(self, title) -> Optional[dict]: self._submit_by_shortcut() return None + if "逆回购" in content: + self._submit_by_shortcut() + return None + return None if title == "提示": diff --git a/easytrader/utils/__init__.py b/easytrader/utils/__init__.py new file mode 100644 index 00000000..ff52b97d --- /dev/null +++ b/easytrader/utils/__init__.py @@ -0,0 +1,9 @@ +from .captcha import captcha_recognize + +import win32gui + +def SetForegroundWindow(hwd): + win32gui.SetForegroundWindow(hwd._as_parameter_) + +def ShowWindow(hwd, window_status): + win32gui.ShowWindow(hwd._as_parameter_, window_status) \ No newline at end of file diff --git a/easytrader/utils/captcha.py b/easytrader/utils/captcha.py new file mode 100644 index 00000000..6ec3b712 --- /dev/null +++ b/easytrader/utils/captcha.py @@ -0,0 +1,20 @@ +import pytesseract +from PIL import Image + + +def captcha_recognize(img_path): + im = Image.open(img_path).convert("L") + # 1. threshold the image + threshold = 200 + table = [] + for i in range(256): + if i < threshold: + table.append(0) + else: + table.append(1) + + out = im.point(table, '1') + # out.show() + # 2. recognize with tesseract + num = pytesseract.image_to_string(out) + return num diff --git a/requirements.txt b/requirements.txt index 5e6d3e4e..cdbd20b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +win32gui -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com beautifulsoup4==4.6.0 From eccc408915d22a6021ad916fbafbe11d1153ef7c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 22:22:36 +0800 Subject: [PATCH 219/276] :star: update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index d40e2706..714a348c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,5 @@ ### 其他 -最近在开发一个[大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com),同时也将相关的功能集成到了对应的公众号,刚刚起步,如果大家对网文感兴趣的话欢迎关注交流 - -![推书君](https://raw.githubusercontent.com/shidenggui/assets/master/tuishujun/qr.jpeg) +最近在开发一个[大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com),喜欢小说的朋友可以一起交流 From 7946000231b23406afe3449fb0fa2e046699d496 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 22:30:59 +0800 Subject: [PATCH 220/276] :star: update docs --- docs/index.md | 17 +++-------------- docs/usage.md | 5 ----- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/docs/index.md b/docs/index.md index c1d169bc..b9729525 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,38 +10,27 @@ * 有兴趣的可以加群 `556050652` 一起讨论 * 捐助: -![微信](http://7xqo8v.com1.z0.glb.clouddn.com/wx.png?imageView2/1/w/300/h/300) ![支付宝](http://7xqo8v.com1.z0.glb.clouddn.com/zhifubao2.png?imageView2/1/w/300/h/300) - - ## 公众号 -扫码关注“易量化”的微信公众号,不定时更新一些个人文章及与大家交流 - -![](http://7xqo8v.com1.z0.glb.clouddn.com/easy_quant_qrcode.jpg?imageView2/1/w/300/h/300) +欢迎大家扫码关注我的个人公众号"食灯鬼",一起交流 +![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) **开发环境** : `OSX 10.12.3` / `Python 3.5` + ### 支持券商 -* 银河客户端(支持自动登陆), 须在 `windows` 平台下载 `银河双子星` 客户端 * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) * 其他券商通用同花顺客户端(需要手动登陆) -注: 现在有些新的同花顺客户端对拷贝剪贴板数据做了限制,下面在 [issue](https://github.com/shidenggui/easytrader/issues/272) 里提供几个老版本同花顺的下载地址。如有大家有补充的也可以再下面回复 - - -### 实盘易 - -如果有对其他券商或者通达信版本的需求,可以查看 [实盘易](http://6du.in/0s15Iru) ### 模拟交易 * 雪球组合 by @[haogefeifei](https://github.com/haogefeifei)([说明](other/xueqiu.md)) - ### 相关 [获取新浪免费实时行情的类库: easyquotation](https://github.com/shidenggui/easyquotation) diff --git a/docs/usage.md b/docs/usage.md index 31469bbb..28b949b3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,11 +6,6 @@ import easytrader # 设置券商类型 -**银河客户端** - -```python -user = easytrader.use('yh_client') -``` **华泰客户端** ```python From 775743d733472d4f6b3201e7990e7189e0c40546 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 22:40:56 +0800 Subject: [PATCH 221/276] :star: add empty requirements for readthedocs --- readthedocs-requirements.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 readthedocs-requirements.txt diff --git a/readthedocs-requirements.txt b/readthedocs-requirements.txt new file mode 100644 index 00000000..e69de29b From df5527de0c254b6592ac8fb23b48ae259d142a6f Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 22:48:31 +0800 Subject: [PATCH 222/276] :bug: fix docs --- docs/install.md | 2 +- mkdocs.yml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/install.md b/docs/install.md index 230bf9da..5d71030a 100644 --- a/docs/install.md +++ b/docs/install.md @@ -15,7 +15,7 @@ ### 登陆时的验证码识别 -银河可以直接自动登录, 其他券商如果登陆需要识别验证码的话需要安装 tesseract: +券商如果登陆需要识别验证码的话需要安装 tesseract: * `tesseract` : 非 `pytesseract`, 需要单独安装, [地址](https://github.com/tesseract-ocr/tesseract/wiki),保证在命令行下 `tesseract` 可用 diff --git a/mkdocs.yml b/mkdocs.yml index ce98f4a8..7a94bcd0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: easytrader -pages: -- [index.md, Home] -- [install.md, 安装] -- [usage.md, 使用] -- [help.md, 常见问题] -- [other/xueqiu.md, 其他, '雪球模拟组合说明'] +nav: + - Home: index.md + - 安装: install.md + - 使用: usage.md + - 常见问题: help.md + - 其他: other/xueqiu.md theme: readthedocs From f286352c2dbc9c995b011d6ee72d55f01b56b176 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 22:55:01 +0800 Subject: [PATCH 223/276] :bug: fix mkdocs.yml --- mkdocs.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 7a94bcd0..2cc15a48 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: easytrader -nav: - - Home: index.md - - 安装: install.md - - 使用: usage.md - - 常见问题: help.md - - 其他: other/xueqiu.md +pages: +- [index.md, Home] +- [install.md, 安装] +- [usage.md, 使用] +- [help.md, 常见问题] +- [other/xueqiu.md, 其他, '雪球模拟组合说明']d theme: readthedocs From 9d73e8760f5ffac5c1b2a61a66908e0f14ea0598 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 22:59:00 +0800 Subject: [PATCH 224/276] :bug: fix mkdocs.yml --- mkdocs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 2cc15a48..d1810457 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: easytrader pages: -- [index.md, Home] -- [install.md, 安装] -- [usage.md, 使用] -- [help.md, 常见问题] -- [other/xueqiu.md, 其他, '雪球模拟组合说明']d + - Home: index.md + - 安装: install.md + - 使用: usage.md + - 常见问题: help.md + - 其他: 'other/xueqiu.md' theme: readthedocs From 3a0a533c260cfbb9f580fcb663af220d96a4560e Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sun, 12 Jan 2020 23:01:45 +0800 Subject: [PATCH 225/276] :bug: fix mkdocs.yml --- docs/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index b9729525..b4ef1ffd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,8 +16,6 @@ ![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) -**开发环境** : `OSX 10.12.3` / `Python 3.5` - ### 支持券商 From 4b6065544dadde6688462f0a8ad3584ab6698cde Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 14 Jan 2020 06:14:19 +0800 Subject: [PATCH 226/276] :bug: fix missing package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 29b47bde..b2d94f89 100644 --- a/setup.py +++ b/setup.py @@ -105,7 +105,7 @@ "Programming Language :: Python :: 3.5", "License :: OSI Approved :: BSD License", ], - packages=["easytrader", "easytrader.config"], + packages=["easytrader", "easytrader.config", "easytrader.utils"], package_data={ "": ["*.jar", "*.json"], "config": ["config/*.json"], From fc2d6a85fa30f7f17c73a9110c78080712f55df5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 14 Jan 2020 06:14:22 +0800 Subject: [PATCH 227/276] =?UTF-8?q?Bump=20version:=200.18.5=20=E2=86=92=20?= =?UTF-8?q?0.18.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3d00b0a2..aa0445db 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.5 +current_version = 0.18.6 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 75937046..9f06130f 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.18.5" +__version__ = "0.18.6" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index b2d94f89..08dbf501 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.5", + version="0.18.6", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 4ae7ee2de381e91c55d10febd34b1c00793c2f62 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 12 Mar 2020 22:26:37 +0800 Subject: [PATCH 228/276] :star: update README.md --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 714a348c..db1f085a 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,17 @@ * 支持通过 webserver 远程操作客户端 * 支持命令行调用,方便其他语言适配 * 基于 Python3.6, Win。注: Linux 仅支持雪球 -* 有兴趣的可以加群 `556050652` 一起讨论 -## 公众号 -欢迎大家扫码关注我的个人公众号"食灯鬼",一起交流 +### 加微信群以及公众号 -![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) +欢迎大家扫码关注公众号"食灯鬼",通过菜单加我好友,备注量化进群 +![JDRUhz](https://gitee.com/shidenggui/assets/raw/master/uPic/JDRUhz.jpg) + +### 作者其他作品 +* [大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com) +* [中文独立个人博客导航 - bloghub.fun](https://bloghub.fun) ### 相关 @@ -27,6 +30,7 @@ [简单的股票量化交易框架 使用 easytrader 和 easyquotation](https://github.com/shidenggui/easyquant) + ### 支持券商 * 华泰客户端(网上交易系统(专业版Ⅱ)) From 6699363106ea4394299b9d47f6cf56cd8825f40b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 12 Mar 2020 22:27:45 +0800 Subject: [PATCH 229/276] :star: update docs --- docs/index.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index b4ef1ffd..3b04f03c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,14 +7,12 @@ * 支持命令行调用,方便其他语言适配 * 支持远程操作客户端 * 支持 Python3 , Linux / Win / Mac -* 有兴趣的可以加群 `556050652` 一起讨论 -* 捐助: -## 公众号 +### 加微信群以及公众号 -欢迎大家扫码关注我的个人公众号"食灯鬼",一起交流 +欢迎大家扫码关注公众号"食灯鬼",通过菜单加我好友,备注量化进群 -![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/easy_quant_qrcode.jpg) +![JDRUhz](https://gitee.com/shidenggui/assets/raw/master/uPic/JDRUhz.jpg) ### 支持券商 @@ -35,3 +33,9 @@ [简单的股票量化交易框架 使用 easytrader 和 easyquotation](https://github.com/shidenggui/easyquant) + +### 作者其他作品 + +* [大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com) +* [中文独立个人博客导航 - bloghub.fun](https://bloghub.fun) + From f99570dc470954d809b2a2c7e25b4f7f1800f4d9 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 12 Mar 2020 22:34:01 +0800 Subject: [PATCH 230/276] :star:(trade) allow use type_keys when set_edit_text not working --- docs/help.md | 7 +++++++ easytrader/clienttrader.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/docs/help.md b/docs/help.md index aabf432f..f363bffc 100644 --- a/docs/help.md +++ b/docs/help.md @@ -1,3 +1,10 @@ +# 某些券商客户端无法输入文本 + +有些客户端无法通过 set_edit_text 方法输入内容,可以通过使用 type_keys 方法绕过,开启方式 + +``` +user.enable_type_keys_for_editor() +``` # 如何关闭 debug 日志的输出 diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 7696cf3f..b186a0c7 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -63,6 +63,12 @@ class ClientTrader(IClientTrader): grid_strategy: Type[grid_strategies.IGridStrategy] = grid_strategies.Copy _grid_strategy_instance: grid_strategies.IGridStrategy = None + def enable_type_keys_for_editor(self): + """ + 有些客户端无法通过 set_edit_text 方法输入内容,可以通过使用 type_keys 方法绕过 + """ + self._editor_need_type_keys = True + @property def grid_strategy_instance(self): if self._grid_strategy_instance is None: From df6fd07713872944ed41ab861fcba36066bbdf25 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Thu, 12 Mar 2020 22:34:10 +0800 Subject: [PATCH 231/276] =?UTF-8?q?Bump=20version:=200.18.6=20=E2=86=92=20?= =?UTF-8?q?0.18.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index aa0445db..9065d03b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.6 +current_version = 0.18.7 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 9f06130f..4da595d3 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.18.6" +__version__ = "0.18.7" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 08dbf501..3dfffc23 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.6", + version="0.18.7", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 7e90c8a42f17c84661290eafaa6265d0b5bcd421 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 13 Mar 2020 11:32:28 +0800 Subject: [PATCH 232/276] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db1f085a..c358e546 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ ### 加微信群以及公众号 -欢迎大家扫码关注公众号"食灯鬼",通过菜单加我好友,备注量化进群 +欢迎大家扫码关注公众号「食灯鬼」,一起交流。进群可通过菜单加我好友,备注量化。 -![JDRUhz](https://gitee.com/shidenggui/assets/raw/master/uPic/JDRUhz.jpg) +![公众号二维码](https://gitee.com/shidenggui/assets/raw/master/uPic/mp-qr.png) ### 作者其他作品 * [大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com) From 486370220621af33b2e0cf9b055f8f8f7ebd2d9c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 14 Mar 2020 09:08:04 +0800 Subject: [PATCH 233/276] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c358e546..8403ebbd 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ ![公众号二维码](https://gitee.com/shidenggui/assets/raw/master/uPic/mp-qr.png) +若二维码因 Github 网络无法打开,请点击[公众号二维码](https://gitee.com/shidenggui/assets/raw/master/uPic/mp-qr.png)直接打开图片。 + ### 作者其他作品 * [大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com) * [中文独立个人博客导航 - bloghub.fun](https://bloghub.fun) From 7e97cf7ae1a4b6f5799c1c0ccfd6db068f7831e7 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Sat, 14 Mar 2020 09:21:05 +0800 Subject: [PATCH 234/276] :star: update README.md --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8403ebbd..a1ef9637 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ * 基于 Python3.6, Win。注: Linux 仅支持雪球 -### 加微信群以及公众号 +### 微信群以及公众号 欢迎大家扫码关注公众号「食灯鬼」,一起交流。进群可通过菜单加我好友,备注量化。 @@ -22,9 +22,11 @@ 若二维码因 Github 网络无法打开,请点击[公众号二维码](https://gitee.com/shidenggui/assets/raw/master/uPic/mp-qr.png)直接打开图片。 -### 作者其他作品 -* [大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com) -* [中文独立个人博客导航 - bloghub.fun](https://bloghub.fun) +### Author + +**easytrader** © [shidenggui](https://github.com/shidenggui), Released under the [MIT](./LICENSE) License.
+ +> Blog [@shidenggui](https://shidenggui.com) · Weibo [@食灯鬼](https://www.weibo.com/u/1651274491) · Twitter [@shidenggui](https://twitter.com/shidenggui) ### 相关 @@ -48,7 +50,7 @@ [中文文档](http://easytrader.readthedocs.io/zh/master/) -### 其他 - -最近在开发一个[大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com),喜欢小说的朋友可以一起交流 +### 作者其他作品 +* [大数据网络小说推荐系统 - 推书君](https://www.tuishujun.com) +* [中文独立个人博客导航 - bloghub.fun](https://bloghub.fun) From de2126cbe7312383945e4995f67eec57e5da3c27 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Mar 2020 08:12:55 +0800 Subject: [PATCH 235/276] =?UTF-8?q?:star:(broker)=20support=20=E6=B5=B7?= =?UTF-8?q?=E9=80=9A=E8=AF=81=E5=88=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/api.py | 5 +++ easytrader/config/client.py | 15 +++++++- easytrader/htzq_clienttrader.py | 61 +++++++++++++++++++++++++++++++++ tests/test_easytrader.py | 57 +++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 easytrader/htzq_clienttrader.py diff --git a/easytrader/api.py b/easytrader/api.py index a6b76686..01823cad 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -48,6 +48,11 @@ def use(broker, debug=False, **kwargs): return WKClientTrader() + if broker.lower() in ["htzq_client", "海通证券客户端"]: + from easytrader.htzq_clienttrader import HTZQClientTrader + + return HTZQClientTrader() + if broker.lower() in ["gj_client", "国金客户端"]: from .gj_clienttrader import GJClientTrader diff --git a/easytrader/config/client.py b/easytrader/config/client.py index a2fb4cc8..a9166529 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -10,6 +10,8 @@ def create(broker): return CommonConfig if broker == "wk": return WK + if broker == "htzq": + return HTZQ raise NotImplementedError @@ -134,4 +136,15 @@ class GJ(CommonConfig): class WK(HT): - pass \ No newline at end of file + pass + + +class HTZQ(CommonConfig): + DEFAULT_EXE_PATH = r"c:\\海通证券委托\\xiadan.exe" + + BALANCE_CONTROL_ID_GROUP = { + "资金余额": 1012, + "可用金额": 1016, + "可取金额": 1017, + "总资产": 1015, + } diff --git a/easytrader/htzq_clienttrader.py b/easytrader/htzq_clienttrader.py new file mode 100644 index 00000000..4ffa4538 --- /dev/null +++ b/easytrader/htzq_clienttrader.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +import pywinauto +import pywinauto.clipboard + +from easytrader import grid_strategies +from . import clienttrader + + +class HTZQClientTrader(clienttrader.BaseLoginClientTrader): + grid_strategy = grid_strategies.Xls + + @property + def broker_type(self): + return "htzq" + + def login(self, user, password, exe_path, comm_password=None, **kwargs): + """ + :param user: 用户名 + :param password: 密码 + :param exe_path: 客户端路径, 类似 + :param comm_password: + :param kwargs: + :return: + """ + self._editor_need_type_keys = False + if comm_password is None: + raise ValueError("必须设置通讯密码") + + try: + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1 + ) + # pylint: disable=broad-except + except Exception: + self._app = pywinauto.Application().start(exe_path) + + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait("ready") + break + except RuntimeError: + pass + self._app.top_window().Edit1.set_focus() + self._app.top_window().Edit1.type_keys(user) + self._app.top_window().Edit2.type_keys(password) + + self._app.top_window().Edit3.type_keys(comm_password) + + self._app.top_window().button0.click() + + # detect login is success or not + self._app.top_window().wait_not("exists", 100) + + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10 + ) + self._close_prompt_windows() + self._main = self._app.window(title="网上股票交易系统5.0") + diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index 731ba660..32917b20 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -6,7 +6,7 @@ sys.path.append(".") -TEST_CLIENTS = os.environ.get("EZ_TEST_CLIENTS", "") +TEST_CLIENTS = set(os.environ.get("EZ_TEST_CLIENTS", "").split(',')) IS_WIN_PLATFORM = sys.platform != "darwin" @@ -118,5 +118,60 @@ def test_auto_ipo(self): self._user.auto_ipo() +@unittest.skipUnless("htzq" in TEST_CLIENTS and IS_WIN_PLATFORM, "skip htzq test") +class TestHTZQClientTrader(unittest.TestCase): + @classmethod + def setUpClass(cls): + import easytrader + + if "htzq" not in TEST_CLIENTS: + return + + # input your test account and password + cls._ACCOUNT = os.environ.get("EZ_TEST_HTZQ_ACCOUNT") or "your account" + cls._PASSWORD = ( + os.environ.get("EZ_TEST_HTZQ_PASSWORD") or "your password" + ) + cls._COMM_PASSWORD = ( + os.environ.get("EZ_TEST_HTZQ_COMMON_PASSWORD") or "your comm password" + ) + + cls._user = easytrader.use("htzq_client") + + cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD) + + def test_balance(self): + time.sleep(3) + result = self._user.balance + self.assertEqual(result, '') + + def test_today_entrusts(self): + result = self._user.today_entrusts + + def test_today_trades(self): + result = self._user.today_trades + + def test_cancel_entrusts(self): + result = self._user.cancel_entrusts + + def test_cancel_entrust(self): + result = self._user.cancel_entrust("123456789") + + def test_invalid_buy(self): + import easytrader + + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.buy("511990", 1, 1e10) + + def test_invalid_sell(self): + import easytrader + + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.sell("162411", 200, 1e10) + + def test_auto_ipo(self): + self._user.auto_ipo() + if __name__ == "__main__": unittest.main(verbosity=2) + From 06560a2afea8510233d34a44003440d5a173ab61 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Mar 2020 08:13:38 +0800 Subject: [PATCH 236/276] =?UTF-8?q?Bump=20version:=200.18.7=20=E2=86=92=20?= =?UTF-8?q?0.19.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9065d03b..e44edc30 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.18.7 +current_version = 0.19.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 4da595d3..925c00bb 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.18.7" +__version__ = "0.19.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 3dfffc23..a20c7ad4 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.18.7", + version="0.19.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From f17c31633bcb9a3b753462834d0598ee1bc520a2 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Mar 2020 14:02:18 +0800 Subject: [PATCH 237/276] :star:(grid) suport change tmp folder for xls strategy --- easytrader/clienttrader.py | 101 +++++++++++++--------------------- easytrader/config/client.py | 3 + easytrader/grid_strategies.py | 35 ++++++------ tests/test_easytrader.py | 29 ++++------ 4 files changed, 70 insertions(+), 98 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index b186a0c7..4dd079da 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -6,13 +6,14 @@ import re import sys import time -from typing import Type +from typing import Type, Union import easyutils from pywinauto import findwindows, timings from easytrader import grid_strategies, pop_dialog_handler from easytrader.config import client +from easytrader.grid_strategies import IGridStrategy from easytrader.log import logger from easytrader.utils.misc import file2dict from easytrader.utils.perf import perf_clock @@ -60,8 +61,8 @@ def is_exist_pop_dialog(self): class ClientTrader(IClientTrader): _editor_need_type_keys = False # The strategy to use for getting grid data - grid_strategy: Type[grid_strategies.IGridStrategy] = grid_strategies.Copy - _grid_strategy_instance: grid_strategies.IGridStrategy = None + grid_strategy: Union[IGridStrategy, Type[IGridStrategy]] = grid_strategies.Copy + _grid_strategy_instance: IGridStrategy = None def enable_type_keys_for_editor(self): """ @@ -72,7 +73,12 @@ def enable_type_keys_for_editor(self): @property def grid_strategy_instance(self): if self._grid_strategy_instance is None: - self._grid_strategy_instance = self.grid_strategy(self) + self._grid_strategy_instance = ( + self.grid_strategy + if isinstance(self.grid_strategy, IGridStrategy) + else self.grid_strategy() + ) + self._grid_strategy_instance.set_trader(self) return self._grid_strategy_instance def __init__(self): @@ -112,9 +118,7 @@ def connect(self, exe_path=None, **kwargs): "参数 exe_path 未设置,请设置客户端对应的 exe 地址,类似 C:\\客户端安装目录\\xiadan.exe" ) - self._app = pywinauto.Application().connect( - path=connect_path, timeout=10 - ) + self._app = pywinauto.Application().connect(path=connect_path, timeout=10) self._close_prompt_windows() self._main = self._app.top_window() @@ -167,10 +171,7 @@ def cancel_entrusts(self): def cancel_entrust(self, entrust_no): self.refresh() for i, entrust in enumerate(self.cancel_entrusts): - if ( - entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] - == entrust_no - ): + if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no: self._cancel_entrust_by_double_click(i) return self._handle_pop_dialogs() return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} @@ -188,9 +189,7 @@ def sell(self, security, price, amount, **kwargs): return self.trade(security, price, amount) @perf_clock - def market_buy( - self, security, amount, ttype=None, limit_price=None, **kwargs - ): + def market_buy(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价买入 :param security: 六位证券代码 @@ -204,14 +203,10 @@ def market_buy( """ self._switch_left_menus(["市价委托", "买入"]) - return self.market_trade( - security, amount, ttype, limit_price=limit_price - ) + return self.market_trade(security, amount, ttype, limit_price=limit_price) @perf_clock - def market_sell( - self, security, amount, ttype=None, limit_price=None, **kwargs - ): + def market_sell(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价卖出 :param security: 六位证券代码 @@ -224,13 +219,9 @@ def market_sell( """ self._switch_left_menus(["市价委托", "卖出"]) - return self.market_trade( - security, amount, ttype, limit_price=limit_price - ) + return self.market_trade(security, amount, ttype, limit_price=limit_price) - def market_trade( - self, security, amount, ttype=None, limit_price=None, **kwargs - ): + def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs): """ 市价交易 :param security: 六位证券代码 @@ -242,9 +233,7 @@ def market_trade( :return: {'entrust_no': '委托单号'} """ code = security[-6:] - self._type_edit_control_keys( - self._config.TRADE_SECURITY_CONTROL_ID, code - ) + self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) if ttype is not None: retry = 0 retry_max = 10 @@ -255,9 +244,7 @@ def market_trade( except: retry += 1 self.wait(0.1) - self._set_market_trade_params( - security, amount, limit_price=limit_price - ) + self._set_market_trade_params(security, amount, limit_price=limit_price) self._submit_trade() return self._handle_pop_dialogs( @@ -267,8 +254,7 @@ def market_trade( def _set_market_trade_type(self, ttype): """根据选择的市价交易类型选择对应的下拉选项""" selects = self._main.child_window( - control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, - class_name="ComboBox", + control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, class_name="ComboBox" ) for i, text in enumerate(selects.texts()): # skip 0 index, because 0 index is current select index @@ -289,11 +275,7 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} - invalid_list_idx = [ - i - for i, v in enumerate(stock_list) - if v["申购数量"] <= 0 - ] + invalid_list_idx = [i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0] if len(stock_list) == len(invalid_list_idx): return {"message": "没有发现可以申购的新股"} @@ -326,11 +308,14 @@ def is_exist_pop_dialog(self): self.wait(0.5) # wait dialog display try: return ( - self._main.wrapper_object() - != self._app.top_window().wrapper_object() + self._main.wrapper_object() != self._app.top_window().wrapper_object() ) - except (findwindows.ElementNotFoundError, timings.TimeoutError, RuntimeError) as ex: - logger.exception('check pop dialog timeout') + except ( + findwindows.ElementNotFoundError, + timings.TimeoutError, + RuntimeError, + ) as ex: + logger.exception("check pop dialog timeout") return False def _run_exe_path(self, exe_path): @@ -344,9 +329,7 @@ def exit(self): def _close_prompt_windows(self): self.wait(1) - for window in self._app.windows( - class_name="#32770", visible_only=True - ): + for window in self._app.windows(class_name="#32770", visible_only=True): title = window.window_text() if title != self._config.TITLE: logging.info("close " + title) @@ -377,8 +360,7 @@ def _click(self, control_id): def _submit_trade(self): time.sleep(0.2) self._main.child_window( - control_id=self._config.TRADE_SUBMIT_CONTROL_ID, - class_name="Button", + control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name="Button" ).click() @perf_clock @@ -398,9 +380,7 @@ def _get_pop_dialog_title(self): def _set_trade_params(self, security, price, amount): code = security[-6:] - self._type_edit_control_keys( - self._config.TRADE_SECURITY_CONTROL_ID, code - ) + self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) # wait security input finish self.wait(0.1) @@ -422,8 +402,7 @@ def _set_market_trade_params(self, security, amount, limit_price=None): if str(security).startswith("68"): # 科创板存在限价 try: price_control = self._main.child_window( - control_id=self._config.TRADE_PRICE_CONTROL_ID, - class_name="Edit", + control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit" ) except: pass @@ -434,9 +413,9 @@ def _get_grid_data(self, control_id): return self.grid_strategy_instance.get(control_id) def _type_keys(self, control_id, text): - self._main.child_window( - control_id=control_id, class_name="Edit" - ).set_edit_text(text) + self._main.child_window(control_id=control_id, class_name="Edit").set_edit_text( + text + ) def _type_common_control_keys(self, control, text): self._set_foreground(control) @@ -448,9 +427,7 @@ def _type_edit_control_keys(self, control_id, text): control_id=control_id, class_name="Edit" ).set_edit_text(text) else: - editor = self._main.child_window( - control_id=control_id, class_name="Edit" - ) + editor = self._main.child_window(control_id=control_id, class_name="Edit") editor.select() editor.type_keys(text) @@ -483,7 +460,7 @@ def _get_left_menus_handle(self): return handle # pylint: disable=broad-except except Exception as ex: - logger.exception('error occurred when trying to get left menus') + logger.exception("error occurred when trying to get left menus") count = count - 1 def _cancel_entrust_by_double_click(self, row): @@ -501,9 +478,7 @@ def refresh(self): self._switch_left_menus(["买入[F1]"], sleep=0.05) @perf_clock - def _handle_pop_dialogs( - self, handler_class=pop_dialog_handler.PopDialogHandler - ): + def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler): handler = handler_class(self._app) while self.is_exist_pop_dialog(): diff --git a/easytrader/config/client.py b/easytrader/config/client.py index a9166529..e591f8cb 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -68,6 +68,7 @@ class CommonConfig: AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 AUTO_IPO_BUTTON_CONTROL_ID = 1006 AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"] + AUTO_IPO_NUMBER = '申购数量' class YH(CommonConfig): @@ -148,3 +149,5 @@ class HTZQ(CommonConfig): "可取金额": 1017, "总资产": 1015, } + + AUTO_IPO_NUMBER = '可申购数量' diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index f483abdb..29e183da 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -3,7 +3,7 @@ import io import tempfile from io import StringIO -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, List, Optional import pandas as pd import pywinauto.keyboard @@ -31,9 +31,13 @@ def get(self, control_id: int) -> List[Dict]: """ pass + @abc.abstractmethod + def set_trader(self, trader: "clienttrader.IClientTrader"): + pass + class BaseStrategy(IGridStrategy): - def __init__(self, trader: "clienttrader.IClientTrader") -> None: + def set_trader(self, trader: "clienttrader.IClientTrader"): self._trader = trader @abc.abstractmethod @@ -54,9 +58,7 @@ def _set_foreground(self, grid=None): try: if grid is None: grid = self._trader.main - if grid.has_style( - pywinauto.win32defines.WS_MINIMIZE - ): # if minimized + if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized ShowWindow(grid.wrapper_object(), 9) # restore window state else: SetForegroundWindow(grid.wrapper_object()) # bring to front @@ -117,9 +119,7 @@ def _get_clipboard_data(self) -> str: ) # 模拟输入验证码 self._trader.app.top_window().set_focus() - pywinauto.keyboard.SendKeys( - "{ENTER}" - ) # 模拟发送enter,点击确定 + pywinauto.keyboard.SendKeys("{ENTER}") # 模拟发送enter,点击确定 try: logger.info( self._trader.app.top_window() @@ -164,17 +164,20 @@ def get(self, control_id: int) -> List[Dict]: class Xls(BaseStrategy): """ - 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容, - 用于绕过一些客户端不允许复制的限制 + 通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容 """ + def __init__(self, tmp_folder: Optional[str] = None): + """ + :param tmp_folder: 用于保持临时文件的文件夹 + """ + self.tmp_folder = tmp_folder + 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 + self._set_foreground(grid) # setFocus buggy, instead of SetForegroundWindow grid.type_keys("^s", set_foreground=False) count = 10 while count > 0: @@ -183,7 +186,7 @@ def get(self, control_id: int) -> List[Dict]: self._trader.wait(0.2) count -= 1 - temp_path = tempfile.mktemp(suffix=".csv") + temp_path = tempfile.mktemp(suffix=".csv", dir=self.tmp_folder) self._set_foreground(self._trader.app.top_window()) # alt+s保存,alt+y替换已存在的文件 @@ -191,9 +194,7 @@ def get(self, control_id: int) -> List[Dict]: self.normalize_path(temp_path) ) self._trader.wait(0.1) - self._trader.app.top_window().type_keys( - "%{s}%{y}", set_foreground=False - ) + 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(): diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index 32917b20..f11c8073 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -6,7 +6,7 @@ sys.path.append(".") -TEST_CLIENTS = set(os.environ.get("EZ_TEST_CLIENTS", "").split(',')) +TEST_CLIENTS = set(os.environ.get("EZ_TEST_CLIENTS", "").split(",")) IS_WIN_PLATFORM = sys.platform != "darwin" @@ -22,9 +22,7 @@ def setUpClass(cls): # input your test account and password cls._ACCOUNT = os.environ.get("EZ_TEST_YH_ACCOUNT") or "your account" - cls._PASSWORD = ( - os.environ.get("EZ_TEST_YH_PASSWORD") or "your password" - ) + cls._PASSWORD = os.environ.get("EZ_TEST_YH_PASSWORD") or "your password" cls._user = easytrader.use("yh_client") cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD) @@ -72,18 +70,14 @@ def setUpClass(cls): # input your test account and password cls._ACCOUNT = os.environ.get("EZ_TEST_HT_ACCOUNT") or "your account" - cls._PASSWORD = ( - os.environ.get("EZ_TEST_HT_PASSWORD") or "your password" - ) + cls._PASSWORD = os.environ.get("EZ_TEST_HT_PASSWORD") or "your password" cls._COMM_PASSWORD = ( os.environ.get("EZ_TEST_HT_COMM_PASSWORD") or "your comm password" ) cls._user = easytrader.use("ht_client") cls._user.prepare( - user=cls._ACCOUNT, - password=cls._PASSWORD, - comm_password=cls._COMM_PASSWORD, + user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD ) def test_balance(self): @@ -129,21 +123,20 @@ def setUpClass(cls): # input your test account and password cls._ACCOUNT = os.environ.get("EZ_TEST_HTZQ_ACCOUNT") or "your account" - cls._PASSWORD = ( - os.environ.get("EZ_TEST_HTZQ_PASSWORD") or "your password" - ) + cls._PASSWORD = os.environ.get("EZ_TEST_HTZQ_PASSWORD") or "your password" cls._COMM_PASSWORD = ( - os.environ.get("EZ_TEST_HTZQ_COMMON_PASSWORD") or "your comm password" + os.environ.get("EZ_TEST_HTZQ_COMM_PASSWORD") or "your comm password" ) cls._user = easytrader.use("htzq_client") - cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD) + cls._user.prepare( + user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD + ) def test_balance(self): time.sleep(3) result = self._user.balance - self.assertEqual(result, '') def test_today_entrusts(self): result = self._user.today_entrusts @@ -170,8 +163,8 @@ def test_invalid_sell(self): result = self._user.sell("162411", 200, 1e10) def test_auto_ipo(self): - self._user.auto_ipo() + self._user.auto_ipo() + if __name__ == "__main__": unittest.main(verbosity=2) - From ad47fe6799742618945da50e67766c4cb54d69cb Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Mar 2020 14:04:51 +0800 Subject: [PATCH 238/276] :star:(docs) update docs --- docs/help.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/help.md b/docs/help.md index f363bffc..3a9576f6 100644 --- a/docs/help.md +++ b/docs/help.md @@ -1,3 +1,11 @@ +# 无法保存对应的 xls 文件 + +有些系统默认的临时文件目录过长,使用 xls 策略时无法正常保存,可通过如下方式修改为自定义目录 + +``` +user.grid_strategy_instance.tmp_folder = 'C:\\custom_folder' +``` + # 某些券商客户端无法输入文本 有些客户端无法通过 set_edit_text 方法输入内容,可以通过使用 type_keys 方法绕过,开启方式 From fc1186466c91741f388eb37f514596e75c30a3a1 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Mar 2020 14:05:32 +0800 Subject: [PATCH 239/276] =?UTF-8?q?Bump=20version:=200.19.0=20=E2=86=92=20?= =?UTF-8?q?0.20.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e44edc30..d62bbb2b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.19.0 +current_version = 0.20.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 925c00bb..1404a4c8 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.19.0" +__version__ = "0.20.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index a20c7ad4..be5e292b 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.19.0", + version="0.20.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From efb7f3d896aa12031d5ef58e44904382ffcc50d1 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Mar 2020 17:11:09 +0800 Subject: [PATCH 240/276] :star:(broker) fix xls strategy bug --- easytrader/grid_strategies.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 29e183da..9c5f295b 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -186,13 +186,11 @@ def get(self, control_id: int) -> List[Dict]: self._trader.wait(0.2) count -= 1 - temp_path = tempfile.mktemp(suffix=".csv", dir=self.tmp_folder) + temp_path = tempfile.mktemp(suffix=".xls", dir=self.tmp_folder) self._set_foreground(self._trader.app.top_window()) # alt+s保存,alt+y替换已存在的文件 - self._trader.app.top_window().Edit1.set_edit_text( - self.normalize_path(temp_path) - ) + self._trader.app.top_window().Edit1.set_edit_text(temp_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 @@ -203,9 +201,6 @@ def get(self, control_id: int) -> List[Dict]: return self._format_grid_data(temp_path) - def normalize_path(self, temp_path: str) -> str: - return temp_path.replace("~", "{~}") - def _format_grid_data(self, data: str) -> List[Dict]: with open(data, encoding="gbk", errors="replace") as f: content = f.read() From c775a49c41be3f2ed8e679673d13ff09d10589d9 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 25 Mar 2020 17:11:23 +0800 Subject: [PATCH 241/276] =?UTF-8?q?Bump=20version:=200.20.0=20=E2=86=92=20?= =?UTF-8?q?0.20.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d62bbb2b..376e7092 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.20.0 +current_version = 0.20.1 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 1404a4c8..a8c121c6 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.20.0" +__version__ = "0.20.1" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index be5e292b..93c6e2f4 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.20.0", + version="0.20.1", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 57a58e22d453f239348b6f15c40aac47d5f918b0 Mon Sep 17 00:00:00 2001 From: 4415u Date: Wed, 22 Apr 2020 16:49:51 +0800 Subject: [PATCH 242/276] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=A7=94=E6=89=98?= =?UTF-8?q?=E7=BC=96=E5=8F=B7=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 3 ++- easytrader/pop_dialog_handler.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 4dd079da..282a0f16 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -475,7 +475,8 @@ def _cancel_entrust_by_double_click(self, row): ).double_click(coords=(x, y)) def refresh(self): - self._switch_left_menus(["买入[F1]"], sleep=0.05) + # self._switch_left_menus(["买入[F1]"], sleep=0.05) + self._switch_left_menus_by_shortcut("{F5}",sleep=0.1) @perf_clock def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler): diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index 0394a187..2f02ee97 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -24,7 +24,7 @@ def _set_foreground(self, grid=None): @perf_clock def handle(self, title): - if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议"}): + if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议", "撤单确认"}): self._submit_by_shortcut() return None @@ -41,7 +41,10 @@ def _extract_content(self): return self._app.top_window().Static.window_text() def _extract_entrust_id(self, content): - return re.search(r"\d+", content).group() + # return re.search(r"\d+", content).group() + rule = '编号:(.*?)。' + ids=re.findall(rule,content) + return ids[0] def _submit_by_click(self): try: From d98f38aad751670fcdddd8822adafa31484ce796 Mon Sep 17 00:00:00 2001 From: matrixleon18 Date: Wed, 27 May 2020 10:16:57 +0800 Subject: [PATCH 243/276] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=8D=8E?= =?UTF-8?q?=E6=B3=B0=E8=AF=81=E5=88=B8=E5=AE=A2=E6=88=B7=E7=AB=AF=E7=9A=84?= =?UTF-8?q?=E5=9B=BD=E5=80=BA=E6=AD=A3=E5=9B=9E=E8=B4=AD/=E9=80=86?= =?UTF-8?q?=E5=9B=9E=E8=B4=AD=E7=9A=84=E6=8E=A5=E5=8F=A3=E3=80=82=20(#375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 13 +++++++++++++ easytrader/pop_dialog_handler.py | 4 ++++ tests/test_easytrader.py | 12 ++++++++++++ 3 files changed, 29 insertions(+) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 4dd079da..b62eb88f 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -176,6 +176,19 @@ def cancel_entrust(self, entrust_no): return self._handle_pop_dialogs() return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} + @perf_clock + def repo(self, security, price, amount, **kwargs): + self._switch_left_menus(["债券回购", "融资回购(正回购)"]) + + return self.trade(security, price, amount) + + @perf_clock + def reverse_repo(self, security, price, amount, **kwargs): + self._switch_left_menus(["债券回购", "融劵回购(逆回购)"]) + + return self.trade(security, price, amount) + + @perf_clock def buy(self, security, price, amount, **kwargs): self._switch_left_menus(["买入[F1]"]) diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index 0394a187..7acd520b 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -80,6 +80,10 @@ def handle(self, title) -> Optional[dict]: self._submit_by_shortcut() return None + if "正回购" in content: + self._submit_by_shortcut() + return None + return None if title == "提示": diff --git a/tests/test_easytrader.py b/tests/test_easytrader.py index f11c8073..dd2f4599 100644 --- a/tests/test_easytrader.py +++ b/tests/test_easytrader.py @@ -111,6 +111,18 @@ def test_invalid_sell(self): def test_auto_ipo(self): self._user.auto_ipo() + def test_invalid_repo(self): + import easytrader + + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.repo("204001", 100, 1) + + def test_invalid_reverse_repo(self): + import easytrader + + with self.assertRaises(easytrader.exceptions.TradeError): + result = self._user.reverse_repo("204001", 1, 100) + @unittest.skipUnless("htzq" in TEST_CLIENTS and IS_WIN_PLATFORM, "skip htzq test") class TestHTZQClientTrader(unittest.TestCase): From 20b39a1982dda828579ac8f980eea90e5faf2130 Mon Sep 17 00:00:00 2001 From: jqz1225 <276627556@qq.com> Date: Sat, 30 May 2020 11:49:38 +0800 Subject: [PATCH 244/276] =?UTF-8?q?=E7=9B=AE=E7=9A=84=EF=BC=9A=E8=AE=A9joi?= =?UTF-8?q?nquant=20follow=E8=83=BD=E8=BF=90=E8=A1=8C=E8=B5=B7=E6=9D=A5?= =?UTF-8?q?=E3=80=82=20=E4=BF=AE=E6=94=B9=E5=86=85=E5=AE=B9=EF=BC=9A=201?= =?UTF-8?q?=EF=BC=89=E5=B0=86=E5=8E=9F=E6=9D=A5=E5=9C=A8usage.md=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=EF=BC=8C=E6=9C=89=E5=85=B3=E9=97=AE=E9=A2=98=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E7=9A=84=EF=BC=8C=E7=A7=BB=E5=88=B0=E4=BA=86help.md?= =?UTF-8?q?=E4=B8=AD=202=EF=BC=89=E9=87=8D=E6=96=B0=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E4=BA=86usage.md=203)=20fellower.py=E5=A2=9E=E5=8A=A0=E4=BA=86?= =?UTF-8?q?re=5Fsearch=E5=87=BD=E6=95=B0=EF=BC=8C=E4=BB=A5=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E5=8E=9F=E6=9D=A5=E7=9A=84re=5Ffind=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E2=80=9Clook-behind=20requires=20fi?= =?UTF-8?q?xed-width=20pattern=E2=80=9D=E9=94=99=E8=AF=AF=E3=80=82=204?= =?UTF-8?q?=EF=BC=89joinquant=5Ffollower.py=E4=BF=AE=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E5=87=BD=E6=95=B0=EF=BC=9Aextract=5Fstrategy=5Fid=E5=92=8C=20e?= =?UTF-8?q?xtract=5Fstrategy=5Fname,=20=E4=BB=A5=E8=83=BD=E5=8F=96?= =?UTF-8?q?=E5=88=B0=E6=AD=A3=E7=A1=AE=E7=9A=84id=E5=92=8Cname=EF=BC=9B?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86stock=5Fshuffle=5Fto=5Fprefix?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=B8=ADtransaction["datetime"]=E5=9C=A8?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E6=97=B6=E7=9A=84=E4=B8=80=E4=B8=AA=E5=B0=8F?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改效果:joinquant follow可用了。 局部测试中,已经可以顺利取到历史交易记录并送入交易队列中。(调试的时候,将joinquant_follower的create_query_transaction_params函数中,today_str = '2020-5-29'这样去强行赋予了一个历史日期)。所以,today_str恢复原状了,应该可以取到实时数据了,这有待观察。 【唉,很笨的调试方法】。 --- docs/help.md | 11 ++ docs/usage.md | 179 +++++++++++++++++-------------- easytrader/follower.py | 4 + easytrader/joinquant_follower.py | 40 ++++--- 4 files changed, 138 insertions(+), 96 deletions(-) diff --git a/docs/help.md b/docs/help.md index 3a9576f6..af2cce09 100644 --- a/docs/help.md +++ b/docs/help.md @@ -1,3 +1,14 @@ +# 某些同花顺客户端不允许拷贝 `Grid` 数据 + +现在默认获取 `Grid` 数据的策略是通过剪切板拷贝,有些券商不允许这种方式,导致无法获取持仓等数据。为解决此问题,额外实现了一种通过将 `Grid` 数据存为文件再读取的策略, +使用方式如下: + +```python +from easytrader import grid_strategies + +user.grid_strategy = grid_strategies.Xls +``` + # 无法保存对应的 xls 文件 有些系统默认的临时文件目录过长,使用 xls 策略时无法正常保存,可通过如下方式修改为自定义目录 diff --git a/docs/usage.md b/docs/usage.md index 28b949b3..d652f4d7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,10 +1,10 @@ -# 引入 +## 一、引入 ```python import easytrader ``` -# 设置券商类型 +## 二、设置交易客户端类型 **华泰客户端** @@ -25,16 +25,34 @@ user = easytrader.use('gj_client') user = easytrader.use('ths') ``` -注: 通用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本), 海王星(通达信版本) +注: 通用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本),国金证券网上交易独立下单程序(核新PC版)等。 +**雪球** -# 设置账户信息 +```python +user = easytrader.use('xq') +``` + + +## 三、启动并连接客户端 -登陆账号有两种方式,`使用参数` 和 `使用配置文件` +### (一)同花顺客户端 -使用通用同花顺客户端不支持自动登陆,所以无需设置,参看下文`直接连接通用同花顺客户端` +通用同花顺客户端不支持自动登录,需要先手动登录。 -**参数登录(推荐)** +请手动打开并登录客户端后,运用connect函数连接客户端。 + +```python +user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' +``` + +### (二)非同花顺客户端 + +非同花顺的客户端,可以调用prepare函数自动登录。 + +调用prepare时所需的参数,可以通过`函数参数` 或 `配置文件` 赋予。 + +**1. 函数参数(推荐)** ``` user.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码,其他券商不用') @@ -42,15 +60,15 @@ user.prepare(user='用户名', password='雪球、银河客户端为明文密码 注: 雪球比较特殊,见下列配置文件格式 -**使用配置文件** +**2. 配置文件** ```python -user.prepare('/path/to/your/yh_client.json') // 配置文件路径 +user.prepare('/path/to/your/yh_client.json') # 配置文件路径 ``` -**注**: 使用配置文件模式, 配置文件需要自己用编辑器编辑生成, 请勿使用记事本, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) +注: 配置文件需自己用编辑器编辑生成, **请勿使用记事本**, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) 。 -**格式如下** +**配置文件格式如下:** 银河/国金客户端 @@ -81,33 +99,11 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 "portfolio_code": "组合代码(例:ZH818559)", "portfolio_market": "交易市场(例:us 或者 cn 或者 hk)" } - ``` -# 直接连接通用同花顺客户端 +## 四、交易相关 -需要先手动登陆客户端到交易窗口,然后运用下面的代码连接交易窗口 - -```python -user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' -``` - -## 某些同花顺客户端不允许拷贝 `Grid` 数据导致无法获取持仓等问题的解决办法 - -现在默认获取 `Grid` 数据的策略是通过剪切板拷贝,有些券商不允许这种方式,所以额外实现了一种通过将 `Grid` 数据存为文件再读取的策略, -使用方式如下: - -```python -from easytrader import grid_strategies - -user.grid_strategy = grid_strategies.Xls -``` - -### 交易相关 - -以下用法以银河为例 - -#### 获取资金状况: +### 1. 获取资金状况 ```python user.balance @@ -124,7 +120,7 @@ user.balance '资金帐号': 'xxx'}] ``` -#### 获取持仓: +### 2. 获取持仓 ```python user.position @@ -148,7 +144,7 @@ user.position '证券名称': '工商银行'}] ``` -#### 买入: +### 3. 买入 ```python user.buy('162411', price=0.55, amount=100) @@ -162,7 +158,7 @@ user.buy('162411', price=0.55, amount=100) 注: 系统可以配置是否返回成交回报。如果没配的话默认返回 `{"message": "success"}` -#### 卖出: +### 4. 卖出 ```python user.sell('162411', price=0.55, amount=100) @@ -174,13 +170,13 @@ user.sell('162411', price=0.55, amount=100) {'entrust_no': 'xxxxxxxx'} ``` -#### 一键打新 +### 5. 一键打新 ```python user.auto_ipo() ``` -#### 撤单 +### 6. 撤单 ```python user.cancel_entrust('buy/sell 获取的 entrust_no') @@ -193,7 +189,7 @@ user.cancel_entrust('buy/sell 获取的 entrust_no') ``` -#### 当日成交 +### 7. 查询当日成交 ```python user.today_trades @@ -215,7 +211,7 @@ user.today_trades '证券名称': '华宝油气'}] ``` -#### 当日委托 +### 8. 查询当日委托 ```python user.today_entrusts @@ -253,7 +249,7 @@ user.today_entrusts ``` -#### 查询今天可以申购的新股信息 +### 9. 查询今日可申购新股 ```python from easytrader.utils.stock import get_today_ipo_data @@ -270,21 +266,37 @@ print(ipo_data) 'apply_code': '申购代码'}] ``` -#### 退出客户端软件 +### 10. 刷新数据 ``` -user.exit() +user.refresh() ``` -#### 刷新数据 +### 11. 雪球组合比例调仓 ### +```python +user.adjust_weight('股票代码', 目标比例) ``` -user.refresh() + +例如,`user.adjust_weight('000001', 10)`是将平安银行在组合中的持仓比例调整到10%。 + +## 五、退出客户端软件 + +```python +user.exit() ``` -### 远端服务器模式 +## 六、远端服务器模式 + +远端服务器模式是交易服务端和量化策略端分离的模式。 -#### 在服务器上启动服务 +**交易服务端**通常是有固定`IP`地址的云服务器,该服务器上运行着`easytrader`交易服务。而**量化策略端**可能是`JoinQuant、RiceQuant、Vn.Py`,物理上与交易服务端不在同一台电脑上。交易服务端被动或主动获取交易信号,并驱动**交易软件**(交易软件包括运行在同一服务器上的下单软件,比如同花顺`xiadan.exe`,或者运行在另一台服务器上的雪球`xq`)。 + +远端模式下,`easytrader`交易服务通过以下两种方式获得交易信号并驱动交易软件: + +### (一) 被动接收远端量化策略发送的交易相关指令 + +#### 交易服务端——启动服务 ```python from easytrader import server @@ -292,36 +304,35 @@ from easytrader import server server.run(port=1430) # 默认端口为 1430 ``` -#### 远程客户端调用 +#### 量化策略端——调用服务 ```python from easytrader import remoteclient -user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client 等', host='服务器ip', port='服务器端口,默认为1430') +user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client, ths, xq等', host='服务器ip', port='服务器端口,默认为1430') -其他用法同上 -``` +user.buy(......) +user.sell(......) -#### 雪球组合调仓 - -```python -user.adjust_weight('000001', 10) +# 交易函数用法同上,见“四、交易相关” ``` +### (二) 主动监控远端量化策略的成交记录或仓位变化 + -### 跟踪 joinquant / ricequant 的模拟交易 +#### 1. 跟踪 `joinquant` / `ricequant` 的模拟交易 -#### 初始化跟踪的 trader +##### 1) 初始化跟踪的 trader -这里以雪球为例, 也可以使用银河之类 easytrader 支持的券商 +这里以雪球为例, 也可以使用银河之类 `easytrader` 支持的券商 ``` xq_user = easytrader.use('xq') xq_user.prepare('xq.json') ``` -#### 初始化跟踪 joinquant / ricequant 的 follower +##### 2) 初始化跟踪 `joinquant` / `ricequant` 的 follower ``` target = 'jq' # joinquant @@ -330,7 +341,7 @@ follower = easytrader.follower(target) follower.login(user='rq/jq用户名', password='rq/jq密码') ``` -#### 连接 follower 和 trader +##### 3) 连接 follower 和 trader ##### joinquant ``` @@ -339,6 +350,23 @@ follower.follow(xq_user, 'jq的模拟交易url') 注: jq的模拟交易url指的是对应模拟交易对应的可以查看持仓, 交易记录的页面, 类似 `https://www.joinquant.com/algorithm/live/index?backtestId=xxx` +正常会输出 + +![enjoy it](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/joinquant.jpg) + +注: 启动后发现跟踪策略无输出,那是因为今天模拟交易没有调仓或者接收到的调仓信号过期了,默认只处理120s内的信号,想要测试的可以用下面的命令: + +```python +jq_follower.follow(user, '模拟交易url', + trade_cmd_expire_seconds=100000000000, cmd_cache=False) +``` + +- trade_cmd_expire_seconds 默认处理多少秒内的信号 + +- cmd_cache 是否读取已经执行过的命令缓存,以防止重复执行 + +目录下产生的 cmd_cache.pk,是用来存储历史执行过的交易指令,防止在重启程序时重复执行交易过的指令,可以通过 `follower.follow(xxx, cmd_cache=False)` 来关闭。 + ##### ricequant ``` @@ -346,26 +374,21 @@ follower.follow(xq_user, run_id) ``` 注:ricequant的run_id即PT列表中的ID。 -正常会输出 -![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/joinquant.jpg) +#### 2. 跟踪雪球的组合 -enjoy it - -### 跟踪 雪球的组合 - -#### 初始化跟踪的 trader +##### 1) 初始化跟踪的 trader 同上 -#### 初始化跟踪 雪球组合 的 follower +##### 2) 初始化跟踪 雪球组合 的 follower ``` xq_follower = easytrader.follower('xq') xq_follower.login(cookies='雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/') ``` -#### 连接 follower 和 trader +##### 3) 连接 follower 和 trader ``` xq_follower.follow(xq_user, 'xq组合ID,类似ZH123456', total_assets=100000) @@ -380,34 +403,32 @@ xq_follower.follow(xq_user, 'xq组合ID,类似ZH123456', total_assets=100000) * 雪球额外支持 adjust_sell 参数,决定是否根据用户的实际持仓数调整卖出股票数量,解决雪球根据百分比调仓时计算出的股数有偏差的问题。当卖出股票数大于实际持仓数时,调整为实际持仓数。目前仅在银河客户端测试通过。 当 users 为多个时,根据第一个 user 的持仓数决定 -#### 多用户跟踪多策略 +#### 3. 多用户跟踪多策略 ``` follower.follow(users=[xq_user, yh_user], strategies=['组合1', '组合2'], total_assets=[10000, 10000]) ``` -#### 目录下产生的 cmd_cache.pk - -这是用来存储历史执行过的交易指令,防止在重启程序时重复执行交易过的指令,可以通过 `follower.follow(xxx, cmd_cache=False)` 来关闭 +#### 4. 其它与跟踪有关的问题 -#### 使用市价单跟踪模式,目前仅支持银河 +使用市价单跟踪模式,目前仅支持银河 ``` follower.follow(***, entrust_prop='market') ``` -#### 调整下单间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足 +调整下单间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足 ``` follower.follow(***, send_interval=30) # 设置下单间隔为 30 s ``` -#### 设置买卖时的滑点 +设置买卖时的滑点 ``` follower.follow(***, slippage=0.05) # 设置滑点为 5% ``` -### 命令行模式 +## 七、命令行模式 #### 登录 diff --git a/easytrader/follower.py b/easytrader/follower.py index 431b4a96..dd3b7642 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -385,6 +385,10 @@ def create_query_transaction_params(self, strategy) -> dict: def re_find(pattern, string, dtype=str): return dtype(re.search(pattern, string).group()) + @staticmethod + def re_search(pattern, string, dtype=str): + return dtype(re.search(pattern,string).group(1)) + def project_transactions(self, transactions, **kwargs): """ 修证调仓记录为内部使用的统一格式 diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index 71a6bb11..d1d55b58 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import re from datetime import datetime from threading import Thread @@ -32,14 +31,14 @@ def check_login_success(self, rep): self.s.headers.update({"cookie": set_cookie}) def follow( - self, - users, - strategies, - track_interval=1, - trade_cmd_expire_seconds=120, - cmd_cache=True, - entrust_prop="limit", - send_interval=0, + self, + users, + strategies, + track_interval=1, + trade_cmd_expire_seconds=120, + cmd_cache=True, + entrust_prop="limit", + send_interval=0, ): """跟踪joinquant对应的模拟交易,支持多用户多策略 :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户 @@ -80,15 +79,22 @@ def follow( for worker in workers: worker.join() - @staticmethod - def extract_strategy_id(strategy_url): - return re.search(r"(?<=backtestId=)\w+", strategy_url).group() + # @staticmethod + # def extract_strategy_id(strategy_url): + # return re.search(r"(?<=backtestId=)\w+", strategy_url).group() + # + # def extract_strategy_name(self, strategy_url): + # rep = self.s.get(strategy_url) + # return self.re_find( + # r'(?<=title="点击修改策略名称"\>).*(?=\', rep.content.decode("utf8")) def extract_strategy_name(self, strategy_url): rep = self.s.get(strategy_url) - return self.re_find( - r'(?<=title="点击修改策略名称"\>).*(?=\(.*?)', rep.content.decode("utf8")) def create_query_transaction_params(self, strategy): today_str = datetime.today().strftime("%Y-%m-%d") @@ -102,7 +108,7 @@ def extract_transactions(self, history): @staticmethod def stock_shuffle_to_prefix(stock): assert ( - len(stock) == 11 + len(stock) == 11 ), "stock {} must like 123456.XSHG or 123456.XSHE".format(stock) code = stock[:6] if stock.find("XSHG") != -1: @@ -120,7 +126,7 @@ def project_transactions(self, transactions, **kwargs): time_str = "{} {}".format(transaction["date"], transaction["time"]) transaction["datetime"] = datetime.strptime( - time_str, "%Y-%m-%d %H:%M" + time_str, "%Y-%m-%d %H:%M:%S" ) stock = self.re_find(r"\d{6}\.\w{4}", transaction["stock"]) From 52a71c32aba0e8a72bdf348be17d7c047311e24a Mon Sep 17 00:00:00 2001 From: qzhjiang <30388582+qzhjiang@users.noreply.github.com> Date: Sun, 31 May 2020 07:54:31 +0800 Subject: [PATCH 245/276] =?UTF-8?q?=E7=9B=AE=E7=9A=84=EF=BC=9A=E8=AE=A9joi?= =?UTF-8?q?nquant=20follow=E8=83=BD=E8=BF=90=E8=A1=8C=E8=B5=B7=E6=9D=A5?= =?UTF-8?q?=E3=80=82=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改内容: 1)将原来在usage.md中的,有关问题解决的,移到了help.md中 2)重新编排了usage.md 3) fellower.py增加了re_search函数,以解决原来的re_find函数导致的“look-behind requires fixed-width pattern”错误。 4)joinquant_follower.py修改了函数:extract_strategy_id和 extract_strategy_name, 以能取到正确的id和name;修改了stock_shuffle_to_prefix函数中transaction["datetime"]在转换时的一个小错误。 修改效果:joinquant follow可用了。 局部测试中,已经可以顺利取到历史交易记录并送入交易队列中。(调试的时候,将joinquant_follower的create_query_transaction_params函数中,today_str = '2020-5-29'这样去强行赋予了一个历史日期)。所以,today_str恢复原状了,应该可以取到实时数据了,这有待观察。 【唉,很笨的调试方法】。 --- docs/help.md | 11 ++ docs/usage.md | 179 +++++++++++++++++-------------- easytrader/follower.py | 4 + easytrader/joinquant_follower.py | 40 ++++--- 4 files changed, 138 insertions(+), 96 deletions(-) diff --git a/docs/help.md b/docs/help.md index 3a9576f6..af2cce09 100644 --- a/docs/help.md +++ b/docs/help.md @@ -1,3 +1,14 @@ +# 某些同花顺客户端不允许拷贝 `Grid` 数据 + +现在默认获取 `Grid` 数据的策略是通过剪切板拷贝,有些券商不允许这种方式,导致无法获取持仓等数据。为解决此问题,额外实现了一种通过将 `Grid` 数据存为文件再读取的策略, +使用方式如下: + +```python +from easytrader import grid_strategies + +user.grid_strategy = grid_strategies.Xls +``` + # 无法保存对应的 xls 文件 有些系统默认的临时文件目录过长,使用 xls 策略时无法正常保存,可通过如下方式修改为自定义目录 diff --git a/docs/usage.md b/docs/usage.md index 28b949b3..d652f4d7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,10 +1,10 @@ -# 引入 +## 一、引入 ```python import easytrader ``` -# 设置券商类型 +## 二、设置交易客户端类型 **华泰客户端** @@ -25,16 +25,34 @@ user = easytrader.use('gj_client') user = easytrader.use('ths') ``` -注: 通用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本), 海王星(通达信版本) +注: 通用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本),国金证券网上交易独立下单程序(核新PC版)等。 +**雪球** -# 设置账户信息 +```python +user = easytrader.use('xq') +``` + + +## 三、启动并连接客户端 -登陆账号有两种方式,`使用参数` 和 `使用配置文件` +### (一)同花顺客户端 -使用通用同花顺客户端不支持自动登陆,所以无需设置,参看下文`直接连接通用同花顺客户端` +通用同花顺客户端不支持自动登录,需要先手动登录。 -**参数登录(推荐)** +请手动打开并登录客户端后,运用connect函数连接客户端。 + +```python +user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' +``` + +### (二)非同花顺客户端 + +非同花顺的客户端,可以调用prepare函数自动登录。 + +调用prepare时所需的参数,可以通过`函数参数` 或 `配置文件` 赋予。 + +**1. 函数参数(推荐)** ``` user.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码,其他券商不用') @@ -42,15 +60,15 @@ user.prepare(user='用户名', password='雪球、银河客户端为明文密码 注: 雪球比较特殊,见下列配置文件格式 -**使用配置文件** +**2. 配置文件** ```python -user.prepare('/path/to/your/yh_client.json') // 配置文件路径 +user.prepare('/path/to/your/yh_client.json') # 配置文件路径 ``` -**注**: 使用配置文件模式, 配置文件需要自己用编辑器编辑生成, 请勿使用记事本, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) +注: 配置文件需自己用编辑器编辑生成, **请勿使用记事本**, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) 。 -**格式如下** +**配置文件格式如下:** 银河/国金客户端 @@ -81,33 +99,11 @@ user.prepare('/path/to/your/yh_client.json') // 配置文件路径 "portfolio_code": "组合代码(例:ZH818559)", "portfolio_market": "交易市场(例:us 或者 cn 或者 hk)" } - ``` -# 直接连接通用同花顺客户端 +## 四、交易相关 -需要先手动登陆客户端到交易窗口,然后运用下面的代码连接交易窗口 - -```python -user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' -``` - -## 某些同花顺客户端不允许拷贝 `Grid` 数据导致无法获取持仓等问题的解决办法 - -现在默认获取 `Grid` 数据的策略是通过剪切板拷贝,有些券商不允许这种方式,所以额外实现了一种通过将 `Grid` 数据存为文件再读取的策略, -使用方式如下: - -```python -from easytrader import grid_strategies - -user.grid_strategy = grid_strategies.Xls -``` - -### 交易相关 - -以下用法以银河为例 - -#### 获取资金状况: +### 1. 获取资金状况 ```python user.balance @@ -124,7 +120,7 @@ user.balance '资金帐号': 'xxx'}] ``` -#### 获取持仓: +### 2. 获取持仓 ```python user.position @@ -148,7 +144,7 @@ user.position '证券名称': '工商银行'}] ``` -#### 买入: +### 3. 买入 ```python user.buy('162411', price=0.55, amount=100) @@ -162,7 +158,7 @@ user.buy('162411', price=0.55, amount=100) 注: 系统可以配置是否返回成交回报。如果没配的话默认返回 `{"message": "success"}` -#### 卖出: +### 4. 卖出 ```python user.sell('162411', price=0.55, amount=100) @@ -174,13 +170,13 @@ user.sell('162411', price=0.55, amount=100) {'entrust_no': 'xxxxxxxx'} ``` -#### 一键打新 +### 5. 一键打新 ```python user.auto_ipo() ``` -#### 撤单 +### 6. 撤单 ```python user.cancel_entrust('buy/sell 获取的 entrust_no') @@ -193,7 +189,7 @@ user.cancel_entrust('buy/sell 获取的 entrust_no') ``` -#### 当日成交 +### 7. 查询当日成交 ```python user.today_trades @@ -215,7 +211,7 @@ user.today_trades '证券名称': '华宝油气'}] ``` -#### 当日委托 +### 8. 查询当日委托 ```python user.today_entrusts @@ -253,7 +249,7 @@ user.today_entrusts ``` -#### 查询今天可以申购的新股信息 +### 9. 查询今日可申购新股 ```python from easytrader.utils.stock import get_today_ipo_data @@ -270,21 +266,37 @@ print(ipo_data) 'apply_code': '申购代码'}] ``` -#### 退出客户端软件 +### 10. 刷新数据 ``` -user.exit() +user.refresh() ``` -#### 刷新数据 +### 11. 雪球组合比例调仓 ### +```python +user.adjust_weight('股票代码', 目标比例) ``` -user.refresh() + +例如,`user.adjust_weight('000001', 10)`是将平安银行在组合中的持仓比例调整到10%。 + +## 五、退出客户端软件 + +```python +user.exit() ``` -### 远端服务器模式 +## 六、远端服务器模式 + +远端服务器模式是交易服务端和量化策略端分离的模式。 -#### 在服务器上启动服务 +**交易服务端**通常是有固定`IP`地址的云服务器,该服务器上运行着`easytrader`交易服务。而**量化策略端**可能是`JoinQuant、RiceQuant、Vn.Py`,物理上与交易服务端不在同一台电脑上。交易服务端被动或主动获取交易信号,并驱动**交易软件**(交易软件包括运行在同一服务器上的下单软件,比如同花顺`xiadan.exe`,或者运行在另一台服务器上的雪球`xq`)。 + +远端模式下,`easytrader`交易服务通过以下两种方式获得交易信号并驱动交易软件: + +### (一) 被动接收远端量化策略发送的交易相关指令 + +#### 交易服务端——启动服务 ```python from easytrader import server @@ -292,36 +304,35 @@ from easytrader import server server.run(port=1430) # 默认端口为 1430 ``` -#### 远程客户端调用 +#### 量化策略端——调用服务 ```python from easytrader import remoteclient -user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client 等', host='服务器ip', port='服务器端口,默认为1430') +user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client, ths, xq等', host='服务器ip', port='服务器端口,默认为1430') -其他用法同上 -``` +user.buy(......) +user.sell(......) -#### 雪球组合调仓 - -```python -user.adjust_weight('000001', 10) +# 交易函数用法同上,见“四、交易相关” ``` +### (二) 主动监控远端量化策略的成交记录或仓位变化 + -### 跟踪 joinquant / ricequant 的模拟交易 +#### 1. 跟踪 `joinquant` / `ricequant` 的模拟交易 -#### 初始化跟踪的 trader +##### 1) 初始化跟踪的 trader -这里以雪球为例, 也可以使用银河之类 easytrader 支持的券商 +这里以雪球为例, 也可以使用银河之类 `easytrader` 支持的券商 ``` xq_user = easytrader.use('xq') xq_user.prepare('xq.json') ``` -#### 初始化跟踪 joinquant / ricequant 的 follower +##### 2) 初始化跟踪 `joinquant` / `ricequant` 的 follower ``` target = 'jq' # joinquant @@ -330,7 +341,7 @@ follower = easytrader.follower(target) follower.login(user='rq/jq用户名', password='rq/jq密码') ``` -#### 连接 follower 和 trader +##### 3) 连接 follower 和 trader ##### joinquant ``` @@ -339,6 +350,23 @@ follower.follow(xq_user, 'jq的模拟交易url') 注: jq的模拟交易url指的是对应模拟交易对应的可以查看持仓, 交易记录的页面, 类似 `https://www.joinquant.com/algorithm/live/index?backtestId=xxx` +正常会输出 + +![enjoy it](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/joinquant.jpg) + +注: 启动后发现跟踪策略无输出,那是因为今天模拟交易没有调仓或者接收到的调仓信号过期了,默认只处理120s内的信号,想要测试的可以用下面的命令: + +```python +jq_follower.follow(user, '模拟交易url', + trade_cmd_expire_seconds=100000000000, cmd_cache=False) +``` + +- trade_cmd_expire_seconds 默认处理多少秒内的信号 + +- cmd_cache 是否读取已经执行过的命令缓存,以防止重复执行 + +目录下产生的 cmd_cache.pk,是用来存储历史执行过的交易指令,防止在重启程序时重复执行交易过的指令,可以通过 `follower.follow(xxx, cmd_cache=False)` 来关闭。 + ##### ricequant ``` @@ -346,26 +374,21 @@ follower.follow(xq_user, run_id) ``` 注:ricequant的run_id即PT列表中的ID。 -正常会输出 -![](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/joinquant.jpg) +#### 2. 跟踪雪球的组合 -enjoy it - -### 跟踪 雪球的组合 - -#### 初始化跟踪的 trader +##### 1) 初始化跟踪的 trader 同上 -#### 初始化跟踪 雪球组合 的 follower +##### 2) 初始化跟踪 雪球组合 的 follower ``` xq_follower = easytrader.follower('xq') xq_follower.login(cookies='雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/') ``` -#### 连接 follower 和 trader +##### 3) 连接 follower 和 trader ``` xq_follower.follow(xq_user, 'xq组合ID,类似ZH123456', total_assets=100000) @@ -380,34 +403,32 @@ xq_follower.follow(xq_user, 'xq组合ID,类似ZH123456', total_assets=100000) * 雪球额外支持 adjust_sell 参数,决定是否根据用户的实际持仓数调整卖出股票数量,解决雪球根据百分比调仓时计算出的股数有偏差的问题。当卖出股票数大于实际持仓数时,调整为实际持仓数。目前仅在银河客户端测试通过。 当 users 为多个时,根据第一个 user 的持仓数决定 -#### 多用户跟踪多策略 +#### 3. 多用户跟踪多策略 ``` follower.follow(users=[xq_user, yh_user], strategies=['组合1', '组合2'], total_assets=[10000, 10000]) ``` -#### 目录下产生的 cmd_cache.pk - -这是用来存储历史执行过的交易指令,防止在重启程序时重复执行交易过的指令,可以通过 `follower.follow(xxx, cmd_cache=False)` 来关闭 +#### 4. 其它与跟踪有关的问题 -#### 使用市价单跟踪模式,目前仅支持银河 +使用市价单跟踪模式,目前仅支持银河 ``` follower.follow(***, entrust_prop='market') ``` -#### 调整下单间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足 +调整下单间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足 ``` follower.follow(***, send_interval=30) # 设置下单间隔为 30 s ``` -#### 设置买卖时的滑点 +设置买卖时的滑点 ``` follower.follow(***, slippage=0.05) # 设置滑点为 5% ``` -### 命令行模式 +## 七、命令行模式 #### 登录 diff --git a/easytrader/follower.py b/easytrader/follower.py index 431b4a96..dd3b7642 100644 --- a/easytrader/follower.py +++ b/easytrader/follower.py @@ -385,6 +385,10 @@ def create_query_transaction_params(self, strategy) -> dict: def re_find(pattern, string, dtype=str): return dtype(re.search(pattern, string).group()) + @staticmethod + def re_search(pattern, string, dtype=str): + return dtype(re.search(pattern,string).group(1)) + def project_transactions(self, transactions, **kwargs): """ 修证调仓记录为内部使用的统一格式 diff --git a/easytrader/joinquant_follower.py b/easytrader/joinquant_follower.py index 71a6bb11..d1d55b58 100644 --- a/easytrader/joinquant_follower.py +++ b/easytrader/joinquant_follower.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import re from datetime import datetime from threading import Thread @@ -32,14 +31,14 @@ def check_login_success(self, rep): self.s.headers.update({"cookie": set_cookie}) def follow( - self, - users, - strategies, - track_interval=1, - trade_cmd_expire_seconds=120, - cmd_cache=True, - entrust_prop="limit", - send_interval=0, + self, + users, + strategies, + track_interval=1, + trade_cmd_expire_seconds=120, + cmd_cache=True, + entrust_prop="limit", + send_interval=0, ): """跟踪joinquant对应的模拟交易,支持多用户多策略 :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户 @@ -80,15 +79,22 @@ def follow( for worker in workers: worker.join() - @staticmethod - def extract_strategy_id(strategy_url): - return re.search(r"(?<=backtestId=)\w+", strategy_url).group() + # @staticmethod + # def extract_strategy_id(strategy_url): + # return re.search(r"(?<=backtestId=)\w+", strategy_url).group() + # + # def extract_strategy_name(self, strategy_url): + # rep = self.s.get(strategy_url) + # return self.re_find( + # r'(?<=title="点击修改策略名称"\>).*(?=\', rep.content.decode("utf8")) def extract_strategy_name(self, strategy_url): rep = self.s.get(strategy_url) - return self.re_find( - r'(?<=title="点击修改策略名称"\>).*(?=\(.*?)', rep.content.decode("utf8")) def create_query_transaction_params(self, strategy): today_str = datetime.today().strftime("%Y-%m-%d") @@ -102,7 +108,7 @@ def extract_transactions(self, history): @staticmethod def stock_shuffle_to_prefix(stock): assert ( - len(stock) == 11 + len(stock) == 11 ), "stock {} must like 123456.XSHG or 123456.XSHE".format(stock) code = stock[:6] if stock.find("XSHG") != -1: @@ -120,7 +126,7 @@ def project_transactions(self, transactions, **kwargs): time_str = "{} {}".format(transaction["date"], transaction["time"]) transaction["datetime"] = datetime.strptime( - time_str, "%Y-%m-%d %H:%M" + time_str, "%Y-%m-%d %H:%M:%S" ) stock = self.re_find(r"\d{6}\.\w{4}", transaction["stock"]) From 8dcddcf3b499f5b6caaa36e80aa0c741de4a4039 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 1 Jun 2020 21:59:03 +0800 Subject: [PATCH 246/276] :star: update README.md fix #373 --- README.md | 1 + docs/index.md | 2 ++ docs/usage.md | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/README.md b/README.md index a1ef9637..915ba4ff 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ ### 支持券商 +* 海通客户端(海通网上交易系统独立委托): 推荐,[万一开户](https://gitee.com/shidenggui/assets/raw/master/uPic/2020-06-01_1leybG.png) * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) * 其他券商通用同花顺客户端(需要手动登陆) diff --git a/docs/index.md b/docs/index.md index 3b04f03c..9b393e81 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,8 @@ ### 支持券商 + +* 海通客户端(海通网上交易系统独立委托): 推荐,[万一开户](https://gitee.com/shidenggui/assets/raw/master/uPic/2020-06-01_1leybG.png) * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) * 其他券商通用同花顺客户端(需要手动登陆) diff --git a/docs/usage.md b/docs/usage.md index d652f4d7..75e352b6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,6 +6,12 @@ import easytrader ## 二、设置交易客户端类型 +**海通客户端** + +```python +user = easytrader.use('htzq_client') +``` + **华泰客户端** ```python From 7833946c9f81b1a0273c2162061498445196e719 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 1 Jun 2020 22:05:03 +0800 Subject: [PATCH 247/276] :star:(bug) default logger level should be info fix #361 --- easytrader/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/log.py b/easytrader/log.py index 93be16b8..27f8dc8b 100644 --- a/easytrader/log.py +++ b/easytrader/log.py @@ -2,7 +2,7 @@ import logging logger = logging.getLogger("easytrader") -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.INFO) logger.propagate = False fmt = logging.Formatter( From 0f824b9b80339c79775e198baadb27fa3b4e3b12 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 1 Jun 2020 22:13:33 +0800 Subject: [PATCH 248/276] =?UTF-8?q?:star:=20=E6=94=AF=E6=8C=81=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E5=AD=97=E6=AF=8D=E7=9A=84=E5=A7=94=E6=89=98=E5=8D=95?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 8 ++++---- easytrader/pop_dialog_handler.py | 11 ++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index e92603b4..b1e25e9e 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -188,7 +188,6 @@ def reverse_repo(self, security, price, amount, **kwargs): return self.trade(security, price, amount) - @perf_clock def buy(self, security, price, amount, **kwargs): self._switch_left_menus(["买入[F1]"]) @@ -288,7 +287,9 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} - invalid_list_idx = [i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0] + invalid_list_idx = [ + i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0 + ] if len(stock_list) == len(invalid_list_idx): return {"message": "没有发现可以申购的新股"} @@ -488,8 +489,7 @@ def _cancel_entrust_by_double_click(self, row): ).double_click(coords=(x, y)) def refresh(self): - # self._switch_left_menus(["买入[F1]"], sleep=0.05) - self._switch_left_menus_by_shortcut("{F5}",sleep=0.1) + self._switch_left_menus_by_shortcut("{F5}", sleep=0.1) @perf_clock def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler): diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index a03efc7c..f4ab3bfd 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -41,18 +41,15 @@ def _extract_content(self): return self._app.top_window().Static.window_text() def _extract_entrust_id(self, content): - # return re.search(r"\d+", content).group() - rule = '编号:(.*?)。' - ids=re.findall(rule,content) - return ids[0] + return re.search(r"[\da-zA-Z]+", content).group() def _submit_by_click(self): try: self._app.top_window()["确定"].click() except Exception as ex: - self._app.Window_( - best_match="Dialog", top_level_only=True - ).ChildWindow(best_match="确定").click() + self._app.Window_(best_match="Dialog", top_level_only=True).ChildWindow( + best_match="确定" + ).click() def _submit_by_shortcut(self): self._set_foreground(self._app.top_window()) From acaa0b47ca754e579076e31f4dd8c8f6aa44b807 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 1 Jun 2020 22:17:11 +0800 Subject: [PATCH 249/276] =?UTF-8?q?Bump=20version:=200.20.1=20=E2=86=92=20?= =?UTF-8?q?0.20.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 376e7092..37eabe0b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.20.1 +current_version = 0.20.2 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index a8c121c6..b4102231 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.20.1" +__version__ = "0.20.2" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 93c6e2f4..c199284e 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.20.1", + version="0.20.2", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 8a6c749502f775544c1c61a51d4875d301ed21ec Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 1 Jun 2020 22:18:52 +0800 Subject: [PATCH 250/276] :star: upgrade rqopen-client --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0dddfec3..81833a1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ python-xlib==0.23 pytz==2018.5 pywinauto==0.6.6 requests==2.19.1 -rqopen-client==0.0.5 +rqopen-client==0.0.6 six==1.11.0 urllib3==1.23; python_version != '3.1.*' werkzeug==0.14.1 From 8ff177da02691857baade953a33feee3973406bb Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 1 Jun 2020 22:19:08 +0800 Subject: [PATCH 251/276] =?UTF-8?q?Bump=20version:=200.20.2=20=E2=86=92=20?= =?UTF-8?q?0.20.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 37eabe0b..f80f9d2d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.20.2 +current_version = 0.20.3 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index b4102231..95ce903c 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.20.2" +__version__ = "0.20.3" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index c199284e..36735a7c 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.20.2", + version="0.20.3", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 883be7acc0d617a64245b9c2ea0f4fb5881c6eee Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 2 Jun 2020 20:46:41 +0800 Subject: [PATCH 252/276] :star: remove rqopenclient --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 81833a1d..9a43978a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,6 @@ python-xlib==0.23 pytz==2018.5 pywinauto==0.6.6 requests==2.19.1 -rqopen-client==0.0.6 six==1.11.0 urllib3==1.23; python_version != '3.1.*' werkzeug==0.14.1 From a86bd220e41b4244c7bed862f58d15d911b862b0 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Tue, 2 Jun 2020 20:46:46 +0800 Subject: [PATCH 253/276] =?UTF-8?q?Bump=20version:=200.20.3=20=E2=86=92=20?= =?UTF-8?q?0.20.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f80f9d2d..6f2c9152 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.20.3 +current_version = 0.20.4 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 95ce903c..3618b252 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.20.3" +__version__ = "0.20.4" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 36735a7c..f8249307 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.20.3", + version="0.20.4", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 38a83c3ae6bacfaf6ed7650725611e690e09633f Mon Sep 17 00:00:00 2001 From: jqz1225 <276627556@qq.com> Date: Thu, 4 Jun 2020 19:14:02 +0800 Subject: [PATCH 254/276] =?UTF-8?q?=E5=90=8C=E8=8A=B1=E9=A1=BA=E7=BD=91?= =?UTF-8?q?=E4=B8=8A=E4=BA=A4=E6=98=935.0=20=E5=88=B7=E6=96=B0=EF=BC=88ref?= =?UTF-8?q?resh=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 44 ++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index b1e25e9e..7b4cf13d 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -85,6 +85,7 @@ def __init__(self): self._config = client.create(self.broker_type) self._app = None self._main = None + self._toolbar = None def _set_foreground(self, grid=None): if grid is None: @@ -121,6 +122,7 @@ def connect(self, exe_path=None, **kwargs): self._app = pywinauto.Application().connect(path=connect_path, timeout=10) self._close_prompt_windows() self._main = self._app.top_window() + self._toolbar = self._main.child_window(class_name="ToolbarWindow32") @property def broker_type(self): @@ -241,6 +243,7 @@ def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs) :param ttype: 市价委托类型,默认客户端默认选择, 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] + :param limit_price: :return: {'entrust_no': '委托单号'} """ @@ -287,9 +290,7 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} - invalid_list_idx = [ - i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0 - ] + invalid_list_idx = [i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0] if len(stock_list) == len(invalid_list_idx): return {"message": "没有发现可以申购的新股"} @@ -309,8 +310,8 @@ def auto_ipo(self): def _click_grid_by_row(self, row): x = self._config.COMMON_GRID_LEFT_MARGIN y = ( - self._config.COMMON_GRID_FIRST_ROW_HEIGHT - + self._config.COMMON_GRID_ROW_HEIGHT * row + self._config.COMMON_GRID_FIRST_ROW_HEIGHT + + self._config.COMMON_GRID_ROW_HEIGHT * row ) self._app.top_window().child_window( control_id=self._config.COMMON_GRID_CONTROL_ID, @@ -322,12 +323,12 @@ def is_exist_pop_dialog(self): self.wait(0.5) # wait dialog display try: return ( - self._main.wrapper_object() != self._app.top_window().wrapper_object() + self._main.wrapper_object() != self._app.top_window().wrapper_object() ) except ( - findwindows.ElementNotFoundError, - timings.TimeoutError, - RuntimeError, + findwindows.ElementNotFoundError, + timings.TimeoutError, + RuntimeError, ) as ex: logger.exception("check pop dialog timeout") return False @@ -387,8 +388,8 @@ def __get_top_window_pop_dialog(self): def _get_pop_dialog_title(self): return ( self._app.top_window() - .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) - .window_text() + .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + .window_text() ) def _set_trade_params(self, security, price, amount): @@ -480,8 +481,8 @@ def _get_left_menus_handle(self): def _cancel_entrust_by_double_click(self, row): x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN y = ( - self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT - + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row + self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row ) self._app.top_window().child_window( control_id=self._config.COMMON_GRID_CONTROL_ID, @@ -489,7 +490,8 @@ def _cancel_entrust_by_double_click(self, row): ).double_click(coords=(x, y)) def refresh(self): - self._switch_left_menus_by_shortcut("{F5}", sleep=0.1) + # self._switch_left_menus(["买入[F1]"], sleep=0.05) + self._toolbar.button(3).click() # 我的交易客户端工具栏中“刷新”是排在第4个的,所以其索引值是3 @perf_clock def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler): @@ -514,13 +516,13 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): pass def prepare( - self, - config_path=None, - user=None, - password=None, - exe_path=None, - comm_password=None, - **kwargs + self, + config_path=None, + user=None, + password=None, + exe_path=None, + comm_password=None, + **kwargs ): """ 登陆客户端 From ae97c309b972fb3d8089391e706cf439807d1603 Mon Sep 17 00:00:00 2001 From: jqz1225 <276627556@qq.com> Date: Sat, 6 Jun 2020 17:25:36 +0800 Subject: [PATCH 255/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=8C=E8=8A=B1?= =?UTF-8?q?=E9=A1=BA=E2=80=9C=E5=88=B7=E6=96=B0=E2=80=9D=EF=BC=9B=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=E5=AF=B9win32gui=E7=9A=84=E5=BC=95=E7=94=A8=20?= =?UTF-8?q?=E4=B8=80=E3=80=81=E4=BF=AE=E5=A4=8D=E5=90=8C=E8=8A=B1=E9=A1=BA?= =?UTF-8?q?=E2=80=9C=E5=88=B7=E6=96=B0=E2=80=9D=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=A8=A1=E6=8B=9F=E7=82=B9=E5=87=BB=E5=B7=A5=E5=85=B7=E6=A0=8F?= =?UTF-8?q?=E2=80=9C=E5=88=B7=E6=96=B0=E2=80=9D=E6=8C=89=E9=92=AE=E3=80=82?= =?UTF-8?q?=20=E4=BA=8C=E3=80=81=E5=8E=BB=E9=99=A4=E5=AF=B9win32gui?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=201=EF=BC=89=E5=B0=86=E6=89=80?= =?UTF-8?q?=E6=9C=89=E5=AF=B9SetForegroundWindow,=20ShowWindow,=20win32def?= =?UTF-8?q?ines=E7=9A=84=E5=BC=95=E7=94=A8=E5=85=A8=E9=83=A8=E9=9B=86?= =?UTF-8?q?=E4=B8=AD=E5=88=B0easytrader.utils.win=5Fgui=202=EF=BC=89win=5F?= =?UTF-8?q?gui.py=E4=B8=AD=EF=BC=8C=E5=88=A0=E9=99=A4=E5=AF=B9win32gui?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=EF=BC=8C=E6=94=B9=E4=B8=BA=E5=AF=B9?= =?UTF-8?q?pywinauto=E7=9A=84=E5=BC=95=E7=94=A8=E3=80=82=EF=BC=88=E8=8B=A5?= =?UTF-8?q?=E5=BC=95=E7=94=A8pywinauto=E6=9C=89=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=88=99=E5=8F=AF=E4=BB=A5=E6=8A=8A=E6=B3=A8=E9=87=8A=E6=8E=89?= =?UTF-8?q?=E7=9A=84=E5=AF=B9win32gui=E7=9A=84=E5=BC=95=E7=94=A8=E7=9E=AC?= =?UTF-8?q?=E9=97=B4=E6=81=A2=E5=A4=8D=E8=B5=B7=E6=9D=A5=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 56 +++++++++++++------------------- easytrader/grid_strategies.py | 17 +++++----- easytrader/pop_dialog_handler.py | 18 +++++----- easytrader/utils/win_gui.py | 21 +++++++----- requirements.txt | 2 +- 5 files changed, 52 insertions(+), 62 deletions(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 7b4cf13d..c0ed785a 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -17,7 +17,6 @@ from easytrader.log import logger from easytrader.utils.misc import file2dict from easytrader.utils.perf import perf_clock -from win32gui import SetForegroundWindow, ShowWindow if not sys.platform.startswith("darwin"): import pywinauto @@ -87,14 +86,6 @@ def __init__(self): self._main = None self._toolbar = None - def _set_foreground(self, grid=None): - if grid is None: - grid = self._trader.main - if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized - ShowWindow(grid.wrapper_object(), 9) # restore window state - else: - SetForegroundWindow(grid.wrapper_object()) # bring to front - @property def app(self): return self._app @@ -243,7 +234,6 @@ def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs) :param ttype: 市价委托类型,默认客户端默认选择, 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] - :param limit_price: :return: {'entrust_no': '委托单号'} """ @@ -290,7 +280,9 @@ def auto_ipo(self): if len(stock_list) == 0: return {"message": "今日无新股"} - invalid_list_idx = [i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0] + invalid_list_idx = [ + i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0 + ] if len(stock_list) == len(invalid_list_idx): return {"message": "没有发现可以申购的新股"} @@ -310,8 +302,8 @@ def auto_ipo(self): def _click_grid_by_row(self, row): x = self._config.COMMON_GRID_LEFT_MARGIN y = ( - self._config.COMMON_GRID_FIRST_ROW_HEIGHT - + self._config.COMMON_GRID_ROW_HEIGHT * row + self._config.COMMON_GRID_FIRST_ROW_HEIGHT + + self._config.COMMON_GRID_ROW_HEIGHT * row ) self._app.top_window().child_window( control_id=self._config.COMMON_GRID_CONTROL_ID, @@ -323,12 +315,12 @@ def is_exist_pop_dialog(self): self.wait(0.5) # wait dialog display try: return ( - self._main.wrapper_object() != self._app.top_window().wrapper_object() + self._main.wrapper_object() != self._app.top_window().wrapper_object() ) except ( - findwindows.ElementNotFoundError, - timings.TimeoutError, - RuntimeError, + findwindows.ElementNotFoundError, + timings.TimeoutError, + RuntimeError, ) as ex: logger.exception("check pop dialog timeout") return False @@ -388,8 +380,8 @@ def __get_top_window_pop_dialog(self): def _get_pop_dialog_title(self): return ( self._app.top_window() - .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) - .window_text() + .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) + .window_text() ) def _set_trade_params(self, security, price, amount): @@ -432,10 +424,6 @@ def _type_keys(self, control_id, text): text ) - def _type_common_control_keys(self, control, text): - self._set_foreground(control) - control.type_keys(text, set_foreground=False) - def _type_edit_control_keys(self, control_id, text): if not self._editor_need_type_keys: self._main.child_window( @@ -481,8 +469,8 @@ def _get_left_menus_handle(self): def _cancel_entrust_by_double_click(self, row): x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN y = ( - self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT - + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row + self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT + + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row ) self._app.top_window().child_window( control_id=self._config.COMMON_GRID_CONTROL_ID, @@ -490,8 +478,8 @@ def _cancel_entrust_by_double_click(self, row): ).double_click(coords=(x, y)) def refresh(self): - # self._switch_left_menus(["买入[F1]"], sleep=0.05) - self._toolbar.button(3).click() # 我的交易客户端工具栏中“刷新”是排在第4个的,所以其索引值是3 + # self._switch_left_menus_by_shortcut("{F5}", sleep=0.1) + self._toolbar.button(3).click() # 我的交易客户端工具栏中“刷新”是排在第4个的,所以其索引值是3 @perf_clock def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler): @@ -516,13 +504,13 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): pass def prepare( - self, - config_path=None, - user=None, - password=None, - exe_path=None, - comm_password=None, - **kwargs + self, + config_path=None, + user=None, + password=None, + exe_path=None, + comm_password=None, + **kwargs ): """ 登陆客户端 diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index 9c5f295b..eebed922 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -9,11 +9,10 @@ import pywinauto.keyboard import pywinauto import pywinauto.clipboard -from pywinauto import win32defines from easytrader.log import logger from easytrader.utils.captcha import captcha_recognize -from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow +from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines if TYPE_CHECKING: # pylint: disable=unused-import @@ -37,6 +36,9 @@ def set_trader(self, trader: "clienttrader.IClientTrader"): class BaseStrategy(IGridStrategy): + def __init__(self): + self._trader = None + def set_trader(self, trader: "clienttrader.IClientTrader"): self._trader = trader @@ -58,7 +60,7 @@ def _set_foreground(self, grid=None): try: if grid is None: grid = self._trader.main - if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized + if grid.has_style(win32defines.WS_MINIMIZE): # if minimized ShowWindow(grid.wrapper_object(), 9) # restore window state else: SetForegroundWindow(grid.wrapper_object()) # bring to front @@ -95,9 +97,7 @@ def _format_grid_data(self, data: str) -> List[Dict]: 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) + self._trader.app.top_window().window(class_name="Static", title_re="验证码").exists(timeout=1) ): file_path = "tmp.png" count = 5 @@ -123,8 +123,8 @@ def _get_clipboard_data(self) -> str: try: logger.info( self._trader.app.top_window() - .window(control_id=0x966, class_name="Static") - .window_text() + .window(control_id=0x966, class_name="Static") + .window_text() ) except Exception as ex: # 窗体消失 logger.exception(ex) @@ -171,6 +171,7 @@ def __init__(self, tmp_folder: Optional[str] = None): """ :param tmp_folder: 用于保持临时文件的文件夹 """ + super().__init__() self.tmp_folder = tmp_folder def get(self, control_id: int) -> List[Dict]: diff --git a/easytrader/pop_dialog_handler.py b/easytrader/pop_dialog_handler.py index f4ab3bfd..bc4ad3fd 100644 --- a/easytrader/pop_dialog_handler.py +++ b/easytrader/pop_dialog_handler.py @@ -3,24 +3,21 @@ import time from typing import Optional -import pywinauto - from easytrader import exceptions from easytrader.utils.perf import perf_clock -from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow +from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines class PopDialogHandler: def __init__(self, app): self._app = app - def _set_foreground(self, grid=None): - if grid is None: - grid = self._trader.main - if grid.has_style(pywinauto.win32defines.WS_MINIMIZE): # if minimized - ShowWindow(grid.wrapper_object(), 9) # restore window state + @staticmethod + def _set_foreground(window): + if window.has_style(win32defines.WS_MINIMIZE): # if minimized + ShowWindow(window.wrapper_object(), 9) # restore window state else: - SetForegroundWindow(grid.wrapper_object()) # bring to front + SetForegroundWindow(window.wrapper_object()) # bring to front @perf_clock def handle(self, title): @@ -40,7 +37,8 @@ def handle(self, title): def _extract_content(self): return self._app.top_window().Static.window_text() - def _extract_entrust_id(self, content): + @staticmethod + def _extract_entrust_id(content): return re.search(r"[\da-zA-Z]+", content).group() def _submit_by_click(self): diff --git a/easytrader/utils/win_gui.py b/easytrader/utils/win_gui.py index 75360ffa..6edd45dc 100644 --- a/easytrader/utils/win_gui.py +++ b/easytrader/utils/win_gui.py @@ -1,10 +1,13 @@ # coding:utf-8 -import win32gui - - -def SetForegroundWindow(hwd): - win32gui.SetForegroundWindow(hwd._as_parameter_) - - -def ShowWindow(hwd, window_status): - win32gui.ShowWindow(hwd._as_parameter_, window_status) +from pywinauto import win32defines +from pywinauto.win32functions import SetForegroundWindow, ShowWindow + +# import win32gui +# +# +# def SetForegroundWindow(hwd): +# win32gui.SetForegroundWindow(hwd._as_parameter_) +# +# +# def ShowWindow(hwd, window_status): +# win32gui.ShowWindow(hwd._as_parameter_, window_status) diff --git a/requirements.txt b/requirements.txt index 9a43978a..a101ca4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,4 +28,4 @@ requests==2.19.1 six==1.11.0 urllib3==1.23; python_version != '3.1.*' werkzeug==0.14.1 -win32gui + From b37661bee2ce79f21430eef2c424778059c7b19c Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 15 Jun 2020 10:39:50 +0800 Subject: [PATCH 256/276] :star: remove rqopenclient --- Pipfile | 1 - Pipfile.lock | 747 --------------------------------------------------- setup.py | 1 - 3 files changed, 749 deletions(-) delete mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile index bf458f95..9670212d 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,6 @@ pillow = "*" pytesseract = "*" pandas = "*" pyperclip = "*" -rqopen-client = ">=0.0.5" easyutils = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index c793e662..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,747 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "e2a2ba761a3628e4851f250cc8882bca58d22c9ebfa11a6923549503a00d577a" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.6" - }, - "sources": [ - { - "name": "pypi", - "url": "http://mirrors.aliyun.com/pypi/simple/", - "verify_ssl": false - } - ] - }, - "default": { - "beautifulsoup4": { - "hashes": [ - "sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", - "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", - "sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89" - ], - "version": "==4.6.0" - }, - "bs4": { - "hashes": [ - "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" - ], - "index": "pypi", - "version": "==0.0.1" - }, - "certifi": { - "hashes": [ - "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", - "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" - ], - "version": "==2018.4.16" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "click": { - "hashes": [ - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" - ], - "index": "pypi", - "version": "==6.7" - }, - "cssselect": { - "hashes": [ - "sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204", - "sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206" - ], - "markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*'", - "version": "==1.0.3" - }, - "dill": { - "hashes": [ - "sha256:624dc244b94371bb2d6e7f40084228a2edfff02373fe20e018bef1ee92fdd5b3" - ], - "index": "pypi", - "version": "==0.2.8.2" - }, - "easyutils": { - "hashes": [ - "sha256:45b46748e20dd3c0e840fa9c1fa7d7f3dc295e58a81796d10329957c20b7f20a" - ], - "index": "pypi", - "version": "==0.1.7" - }, - "flask": { - "hashes": [ - "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", - "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" - ], - "index": "pypi", - "version": "==1.0.2" - }, - "idna": { - "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" - ], - "version": "==2.7" - }, - "itsdangerous": { - "hashes": [ - "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" - ], - "version": "==0.24" - }, - "jinja2": { - "hashes": [ - "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", - "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" - ], - "version": "==2.10" - }, - "lxml": { - "hashes": [ - "sha256:0941f4313208c07734410414d8308812b044fd3fb98573454e3d3a0d2e201f3d", - "sha256:0b18890aa5730f9d847bc5469e8820f782d72af9985a15a7552109a86b01c113", - "sha256:21f427945f612ac75576632b1bb8c21233393c961f2da890d7be3927a4b6085f", - "sha256:24cf6f622a4d49851afcf63ac4f0f3419754d4e98a7a548ab48dd03c635d9bd3", - "sha256:2dc6705486b8abee1af9e2a3761e30a3cb19e8276f20ca7e137ee6611b93707c", - "sha256:2e43b2e5b7d2b9abe6e0301eef2c2c122ab45152b968910eae68bdee2c4cfae0", - "sha256:329a6d8b6d36f7d6f8b6c6a1db3b2c40f7e30a19d3caf62023c9d6a677c1b5e1", - "sha256:423cde55430a348bda6f1021faad7235c2a95a6bdb749e34824e5758f755817a", - "sha256:4651ea05939374cfb5fe87aab5271ed38c31ea47997e17ec3834b75b94bd9f15", - "sha256:4be3bbfb2968d7da6e5c2cd4104fc5ec1caf9c0794f6cae724da5a53b4d9f5a3", - "sha256:622f7e40faef13d232fb52003661f2764ce6cdef3edb0a59af7c1559e4cc36d1", - "sha256:664dfd4384d886b239ef0d7ee5cff2b463831079d250528b10e394a322f141f9", - "sha256:697c0f58ac637b11991a1bc92e07c34da4a72e2eda34d317d2c1c47e2f24c1b3", - "sha256:6ec908b4c8a4faa7fe1a0080768e2ce733f268b287dfefb723273fb34141475f", - "sha256:7ec3fe795582b75bb49bb1685ffc462dbe38d74312dac07ce386671a28b5316b", - "sha256:8c39babd923c431dcf1e5874c0f778d3a5c745a62c3a9b6bd755efd489ee8a1d", - "sha256:949ca5bc56d6cb73d956f4862ba06ad3c5d2808eac76304284f53ae0c8b2334a", - "sha256:9f0daddeefb0791a600e6195441910bdf01eac470be596b9467e6122b51239a6", - "sha256:a359893b01c30e949eae0e8a85671a593364c9f0b8162afe0cb97317af0953bf", - "sha256:ad5d5d8efed59e6b1d4c50c1eac59fb6ecec91b2073676af1e15fc4d43e9b6c5", - "sha256:bc1a36f95a6b3667c09b34995fc3a46a82e4cf0dc3e7ab281e4c77b15bd7af05", - "sha256:be37b3f55b6d7d923f43bf74c356fc1878eb36e28505f38e198cb432c19c7b1a", - "sha256:c45bca5e544eb75f7500ffd730df72922eb878a2f0213b0dc5a5f357ded3a85d", - "sha256:ccee7ebbb4735ebc341d347fca9ee09f2fa6c0580528c1414bc4e1d31372835c", - "sha256:dc62c0840b2fc7753550b40405532a3e125c0d3761f34af948873393aa688160", - "sha256:f7d9d5aa1c7e54167f1a3cba36b5c52c7c540f30952c9bd7d9302a1eda318424" - ], - "version": "==4.2.3" - }, - "markupsafe": { - "hashes": [ - "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" - ], - "version": "==1.0" - }, - "numpy": { - "hashes": [ - "sha256:14fb76bde161c87dcec52d91c78f65aa8a23aa2e1530a71f412dabe03927d917", - "sha256:21041014b7529237994a6b578701c585703fbb3b1bea356cdb12a5ea7804241c", - "sha256:24f3bb9a5f6c3936a8ccd4ddfc1210d9511f4aeb879a12efd2e80bec647b8695", - "sha256:34033b581bc01b1135ca2e3e93a94daea7c739f21a97a75cca93e29d9f0c8e71", - "sha256:3fbccb399fe9095b1c1d7b41e7c7867db8aa0d2347fc44c87a7a180cedda112b", - "sha256:50718eea8e77a1bedcc85befd22c8dbf5a24c9d2c0c1e36bbb8d7a38da847eb3", - "sha256:55daf757e5f69aa75b4477cf4511bf1f96325c730e4ad32d954ccb593acd2585", - "sha256:61efc65f325770bbe787f34e00607bc124f08e6c25fdf04723848585e81560dc", - "sha256:62cb836506f40ce2529bfba9d09edc4b2687dd18c56cf4457e51c3e7145402fd", - "sha256:64c6acf5175745fd1b7b7e17c74fdbfb7191af3b378bc54f44560279f41238d3", - "sha256:674ea7917f0657ddb6976bd102ac341bc493d072c32a59b98e5b8c6eaa2d5ec0", - "sha256:73a816e441dace289302e04a7a34ec4772ed234ab6885c968e3ca2fc2d06fe2d", - "sha256:78c35dc7ad184aebf3714dbf43f054714c6e430e14b9c06c49a864fb9e262030", - "sha256:7f17efe9605444fcbfd990ba9b03371552d65a3c259fc2d258c24fb95afdd728", - "sha256:816645178f2180be257a576b735d3ae245b1982280b97ae819550ce8bcdf2b6b", - "sha256:924f37e66db78464b4b85ed4b6d2e5cda0c0416e657cac7ccbef14b9fa2c40b5", - "sha256:a17a8fd5df4fec5b56b4d11c9ba8b9ebfb883c90ec361628d07be00aaa4f009a", - "sha256:aaa519335a71f87217ca8a680c3b66b61960e148407bdf5c209c42f50fe30f49", - "sha256:ae3864816287d0e86ead580b69921daec568fe680857f07ee2a87bf7fd77ce24", - "sha256:b5f8c15cb9173f6cdf0f994955e58d1265331029ae26296232379461a297e5f2", - "sha256:c3ac359ace241707e5a48fe2922e566ac666aacacf4f8031f2994ac429c31344", - "sha256:c7c660cc0209fdf29a4e50146ca9ac9d8664acaded6b6ae2f5d0ae2e91a0f0cd", - "sha256:d690a2ff49f6c3bc35336693c9924fe5916be3cc0503fe1ea6c7e2bf951409ee", - "sha256:e2317cf091c2e7f0dacdc2e72c693cc34403ca1f8e3807622d0bb653dc978616", - "sha256:f28e73cf18d37a413f7d5de35d024e6b98f14566a10d82100f9dc491a7d449f9", - "sha256:f2a778dd9bb3e4590dbe3bbac28e7c7134280c4ec97e3bf8678170ee58c67b21", - "sha256:f5a758252502b466b9c2b201ea397dae5a914336c987f3a76c3741a82d43c96e", - "sha256:fb4c33a404d9eff49a0cdc8ead0af6453f62f19e071b60d283f9dc05581e4134" - ], - "markers": "python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7'", - "version": "==1.15.0" - }, - "pandas": { - "hashes": [ - "sha256:05ac350f8a35abe6a02054f8cf54e0c048f13423b2acb87d018845afd736f0b4", - "sha256:174543cd68eaee60620146b38faaed950071f5665e0a4fa4adfdcfc23d7f7936", - "sha256:1a62a237fb7223c11d09daaeaf7d15f234bb836bfaf3d4f85746cdf9b2582f99", - "sha256:2c1ed1de5308918a7c6833df6db75a19c416c122921824e306c64a0626b3606c", - "sha256:33825ad26ce411d6526f903b3d02c0edf627223af59cf4b5876aa925578eec74", - "sha256:4c5f76fce8a4851f65374ea1d95ca24e9439540550e41e556c0879379517a6f5", - "sha256:67504a96f72fb4d7f051cfe77b9a7bb0d094c4e2e5a6efb2769eb80f36e6b309", - "sha256:683e0cc8c7faececbbc06aa4735709a07abad106099f165730c1015da916adec", - "sha256:77cd1b485c6a860b950ab3a85be7b5683eaacbc51cadf096db967886607d2231", - "sha256:814f8785f1ab412a7e9b9a8abb81dfe8727ebdeef850ecfaa262c04b1664000f", - "sha256:894216edaf7dd0a92623cdad423bbec2a23fc06eb9c85483e21876d1ef8f47e9", - "sha256:9331e20a07360b81d8c7b4b50223da387d264151d533a5a5853325800e6631a4", - "sha256:9cd3614b4e31a0889388ff1bd19ae857ad52658b33f776065793c293a29cf612", - "sha256:9d79e958adcd037eba3debbb66222804171197c0f5cd462315d1356aa72a5a30", - "sha256:b90e5d5460f23607310cbd1688a7517c96ce7b284095a48340d249dfc429172e", - "sha256:bc80c13ffddc7e269b706ed58002cc4c98cc135c36d827c99fb5ca54ced0eb7a", - "sha256:cbb074efb2a5e4956b261a670bfc2126b0ccfbf5b96b6ed021bc8c8cb56cf4a8", - "sha256:e8c62ab16feeda84d4732c42b7b67d7a89ad89df7e99efed80ea017bdc472f26", - "sha256:ff5ef271805fe877fe0d1337b6b1861113c44c75b9badb595c713a72cd337371" - ], - "index": "pypi", - "version": "==0.23.3" - }, - "pillow": { - "hashes": [ - "sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a", - "sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72", - "sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574", - "sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd", - "sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2", - "sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c", - "sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273", - "sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554", - "sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612", - "sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934", - "sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786", - "sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27", - "sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b", - "sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62", - "sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9", - "sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710", - "sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3", - "sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4", - "sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b", - "sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f", - "sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e", - "sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b", - "sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a", - "sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f", - "sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea", - "sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7", - "sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4", - "sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333", - "sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109", - "sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab" - ], - "index": "pypi", - "version": "==5.2.0" - }, - "pyperclip": { - "hashes": [ - "sha256:f70e83d27c445795b6bf98c2bc826bbf2d0d63d4c7f83091c8064439042ba0dc" - ], - "index": "pypi", - "version": "==1.6.4" - }, - "pyquery": { - "hashes": [ - "sha256:07987c2ed2aed5cba29ff18af95e56e9eb04a2249f42ce47bddfb37f487229a3", - "sha256:4771db76bd14352eba006463656aef990a0147a0eeaf094725097acfa90442bf" - ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*'", - "version": "==1.4.0" - }, - "pytesseract": { - "hashes": [ - "sha256:9a9fae6331084f588c0cf2ad9ed50fca47e20429407e63389eb42d4e64460013" - ], - "index": "pypi", - "version": "==0.2.4" - }, - "python-dateutil": { - "hashes": [ - "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", - "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" - ], - "version": "==2.7.3" - }, - "python-xlib": { - "hashes": [ - "sha256:2ffa01fa51bdf53842fa4e3f9e2501f8147d4abf546a83e9c2b091982da2e1a8", - "sha256:c3deb8329038620d07b21be05673fa5a495dd8b04a2d9f4dca37a3811d192ae4" - ], - "version": "==0.23" - }, - "pytz": { - "hashes": [ - "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", - "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" - ], - "version": "==2018.5" - }, - "pywinauto": { - "hashes": [ - "sha256:75fdfdea3f018c0efc9196cb184ecd14df8b35734889df9624610b8e74812807" - ], - "index": "pypi", - "version": "==0.6.4" - }, - "requests": { - "hashes": [ - "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", - "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" - ], - "index": "pypi", - "version": "==2.19.1" - }, - "rqopen-client": { - "hashes": [ - "sha256:9bda6a1ceac7453ff66ba0ee61ac56e1dd88bfcbd5eb27c98f49c460bf6d5ff7" - ], - "index": "pypi", - "version": "==0.0.5" - }, - "six": { - "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" - ], - "index": "pypi", - "version": "==1.11.0" - }, - "urllib3": { - "hashes": [ - "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", - "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" - ], - "markers": "python_version != '3.2.*' and python_version < '4' and python_version != '3.3.*' and python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", - "version": "==1.23" - }, - "werkzeug": { - "hashes": [ - "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", - "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" - ], - "version": "==0.14.1" - } - }, - "develop": { - "appdirs": { - "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" - ], - "version": "==1.4.3" - }, - "appnope": { - "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, - "aspy.yaml": { - "hashes": [ - "sha256:04d26279513618f1024e1aba46471db870b3b33aef204c2d09bcf93bea9ba13f", - "sha256:0a77e23fafe7b242068ffc0252cee130d3e509040908fc678d9d1060e7494baa" - ], - "version": "==1.1.1" - }, - "astroid": { - "hashes": [ - "sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", - "sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" - ], - "version": "==2.0.1" - }, - "atomicwrites": { - "hashes": [ - "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", - "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" - ], - "version": "==1.1.5" - }, - "attrs": { - "hashes": [ - "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", - "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" - ], - "version": "==18.1.0" - }, - "backcall": { - "hashes": [ - "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", - "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" - ], - "version": "==0.1.0" - }, - "better-exceptions": { - "hashes": [ - "sha256:0a73efef96b48f867ea980227ac3b00d36a92754e6d316ad2ee472f136014580" - ], - "index": "pypi", - "version": "==0.2.1" - }, - "black": { - "hashes": [ - "sha256:22158b89c1a6b4eb333a1e65e791a3f8b998cf3b11ae094adb2570f31f769a44", - "sha256:4b475bbd528acce094c503a3d2dbc2d05a4075f6d0ef7d9e7514518e14cc5191" - ], - "index": "pypi", - "version": "==18.6b4" - }, - "cached-property": { - "hashes": [ - "sha256:630fdbf0f4ac7d371aa866016eba1c3ac43e9032246748d4994e67cb05f99bc4", - "sha256:f1f9028757dc40b4cb0fd2234bd7b61a302d7b84c683cb8c2c529238a24b8938" - ], - "version": "==1.4.3" - }, - "cfgv": { - "hashes": [ - "sha256:73f48a752bd7aab103c4b882d6596c6360b7aa63b34073dd2c35c7b4b8f93010", - "sha256:d1791caa9ff5c0c7bce80e7ecc1921752a2eb7c2463a08ed9b6c96b85a2f75aa" - ], - "version": "==1.1.0" - }, - "click": { - "hashes": [ - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" - ], - "index": "pypi", - "version": "==6.7" - }, - "coverage": { - "hashes": [ - "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", - "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", - "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", - "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", - "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", - "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", - "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", - "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", - "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", - "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", - "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", - "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", - "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", - "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", - "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", - "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", - "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", - "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", - "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", - "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", - "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", - "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", - "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", - "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", - "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", - "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", - "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", - "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", - "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", - "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", - "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" - ], - "markers": "python_version < '4' and python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", - "version": "==4.5.1" - }, - "decorator": { - "hashes": [ - "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", - "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" - ], - "version": "==4.3.0" - }, - "identify": { - "hashes": [ - "sha256:49845e70fc6b1ec3694ab930a2c558912d7de24548eebcd448f65567dc757c43", - "sha256:68daab16a3db364fa204591f97dc40bfffd1a7739f27788a4895b4d8fd3516e5" - ], - "version": "==1.1.4" - }, - "ipython": { - "hashes": [ - "sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", - "sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c" - ], - "index": "pypi", - "version": "==6.4.0" - }, - "ipython-genutils": { - "hashes": [ - "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", - "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" - ], - "version": "==0.2.0" - }, - "isort": { - "hashes": [ - "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", - "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", - "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" - ], - "index": "pypi", - "version": "==4.3.4" - }, - "jedi": { - "hashes": [ - "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", - "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f" - ], - "version": "==0.12.1" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", - "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", - "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", - "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", - "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", - "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", - "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", - "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", - "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", - "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", - "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", - "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", - "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", - "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", - "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", - "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", - "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", - "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", - "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", - "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", - "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", - "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", - "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", - "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", - "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", - "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", - "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", - "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", - "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" - ], - "version": "==1.3.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "more-itertools": { - "hashes": [ - "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", - "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", - "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" - ], - "version": "==4.2.0" - }, - "mypy": { - "hashes": [ - "sha256:673ea75fb750289b7d1da1331c125dc62fc1c3a8db9129bb372ae7b7d5bf300a", - "sha256:c770605a579fdd4a014e9f0a34b6c7a36ce69b08100ff728e96e27445cef3b3c" - ], - "index": "pypi", - "version": "==0.620" - }, - "nodeenv": { - "hashes": [ - "sha256:aa040ab5189bae17d272175609010be6c5b589ec4b8dbd832cc50c9e9cb7496f" - ], - "version": "==1.3.2" - }, - "parso": { - "hashes": [ - "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", - "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" - ], - "version": "==0.3.1" - }, - "pexpect": { - "hashes": [ - "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", - "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" - ], - "markers": "sys_platform != 'win32'", - "version": "==4.6.0" - }, - "pickleshare": { - "hashes": [ - "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", - "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5" - ], - "version": "==0.7.4" - }, - "pluggy": { - "hashes": [ - "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", - "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", - "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" - ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", - "version": "==0.6.0" - }, - "pre-commit": { - "hashes": [ - "sha256:99cb6313a8ea7d88871aa2875a12d3c3a7636edf8ce4634b056328966682c8ce", - "sha256:c71e6cf84e812226f8dadbe346b5e6d6728fa65a364bbfe7624b219a18950540" - ], - "index": "pypi", - "version": "==1.10.4" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", - "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", - "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" - ], - "version": "==1.0.15" - }, - "ptyprocess": { - "hashes": [ - "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", - "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" - ], - "version": "==0.6.0" - }, - "py": { - "hashes": [ - "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", - "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" - ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", - "version": "==1.5.4" - }, - "pygments": { - "hashes": [ - "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", - "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" - ], - "version": "==2.2.0" - }, - "pylint": { - "hashes": [ - "sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", - "sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" - ], - "index": "pypi", - "version": "==2.0.1" - }, - "pytest": { - "hashes": [ - "sha256:341ec10361b64a24accaec3c7ba5f7d5ee1ca4cebea30f76fad3dd12db9f0541", - "sha256:952c0389db115437f966c4c2079ae9d54714b9455190e56acebe14e8c38a7efa" - ], - "index": "pypi", - "version": "==3.6.4" - }, - "pytest-cov": { - "hashes": [ - "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", - "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" - ], - "index": "pypi", - "version": "==2.5.1" - }, - "pyyaml": { - "hashes": [ - "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", - "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", - "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", - "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", - "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", - "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", - "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", - "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", - "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", - "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" - ], - "version": "==3.13" - }, - "simplegeneric": { - "hashes": [ - "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" - ], - "version": "==0.8.1" - }, - "six": { - "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" - ], - "index": "pypi", - "version": "==1.11.0" - }, - "toml": { - "hashes": [ - "sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d" - ], - "version": "==0.9.4" - }, - "traitlets": { - "hashes": [ - "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", - "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" - ], - "version": "==4.3.2" - }, - "typed-ast": { - "hashes": [ - "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", - "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", - "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", - "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", - "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", - "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", - "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", - "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", - "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", - "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", - "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", - "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", - "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", - "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", - "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", - "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", - "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", - "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", - "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", - "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", - "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", - "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", - "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" - ], - "markers": "python_version < '3.7' and implementation_name == 'cpython'", - "version": "==1.1.0" - }, - "virtualenv": { - "hashes": [ - "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", - "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" - ], - "markers": "python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*'", - "version": "==16.0.0" - }, - "wcwidth": { - "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" - ], - "version": "==0.1.7" - }, - "wrapt": { - "hashes": [ - "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" - ], - "version": "==1.10.11" - } - } -} diff --git a/setup.py b/setup.py index f8249307..6886e33d 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,6 @@ install_requires=[ "requests", "six", - "rqopen-client", "easyutils", "flask", "pywinauto", From 6137dc5fb30fe6df4459b74ccbf3d5d125589520 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Mon, 15 Jun 2020 10:41:58 +0800 Subject: [PATCH 257/276] =?UTF-8?q?Bump=20version:=200.20.4=20=E2=86=92=20?= =?UTF-8?q?0.20.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6f2c9152..ccc0c817 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.20.4 +current_version = 0.20.5 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 3618b252..10858924 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.20.4" +__version__ = "0.20.5" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 6886e33d..66882cf8 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.20.4", + version="0.20.5", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 3af5a20e62775ee180185db275048f6ff528a3fe Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 19 Jun 2020 18:42:35 +0800 Subject: [PATCH 258/276] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 915ba4ff..9321155d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ ### 支持券商 -* 海通客户端(海通网上交易系统独立委托): 推荐,[万一开户](https://gitee.com/shidenggui/assets/raw/master/uPic/2020-06-01_1leybG.png) +* 海通客户端(海通网上交易系统独立委托) * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) * 其他券商通用同花顺客户端(需要手动登陆) From 791bda69edfb4849dc97eed9dfb73410cfc26d3b Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 19 Jun 2020 18:43:32 +0800 Subject: [PATCH 259/276] Update index.md --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 9b393e81..7bb88f79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,7 @@ ### 支持券商 -* 海通客户端(海通网上交易系统独立委托): 推荐,[万一开户](https://gitee.com/shidenggui/assets/raw/master/uPic/2020-06-01_1leybG.png) +* 海通客户端(海通网上交易系统独立委托) * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) * 其他券商通用同花顺客户端(需要手动登陆) From 8973ad6bf66952a234420eab1beec36a7bf7e9b5 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 26 Jun 2020 11:35:28 +0800 Subject: [PATCH 260/276] :star: fix merge --- docs/usage.md | 6 ++++++ easytrader/utils/win_gui.py | 10 ---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index d652f4d7..75e352b6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,6 +6,12 @@ import easytrader ## 二、设置交易客户端类型 +**海通客户端** + +```python +user = easytrader.use('htzq_client') +``` + **华泰客户端** ```python diff --git a/easytrader/utils/win_gui.py b/easytrader/utils/win_gui.py index 6edd45dc..903c5f91 100644 --- a/easytrader/utils/win_gui.py +++ b/easytrader/utils/win_gui.py @@ -1,13 +1,3 @@ # coding:utf-8 from pywinauto import win32defines from pywinauto.win32functions import SetForegroundWindow, ShowWindow - -# import win32gui -# -# -# def SetForegroundWindow(hwd): -# win32gui.SetForegroundWindow(hwd._as_parameter_) -# -# -# def ShowWindow(hwd, window_status): -# win32gui.ShowWindow(hwd._as_parameter_, window_status) From bb1c4d10d47cfcabb12af9dda215378c9086040f Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 26 Jun 2020 11:57:48 +0800 Subject: [PATCH 261/276] =?UTF-8?q?:star:=20=E6=94=AF=E6=8C=81=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E7=82=B9=E5=87=BB=E5=B7=A5=E5=85=B7=E6=A0=8F=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=8C=89=E9=92=AE=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/help.md | 11 ++++++ easytrader/clienttrader.py | 8 +++-- easytrader/refresh_strategies.py | 59 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 easytrader/refresh_strategies.py diff --git a/docs/help.md b/docs/help.md index af2cce09..d8bbcb1a 100644 --- a/docs/help.md +++ b/docs/help.md @@ -9,6 +9,17 @@ from easytrader import grid_strategies user.grid_strategy = grid_strategies.Xls ``` +# 通过工具栏刷新按钮刷新数据 + +当前的刷新数据方式是通过切换菜单栏实现,通用但是比较缓慢,可以选择通过点击工具栏的刷新按钮来刷新 + +```python +from easytrader import refresh_strategies + +# refresh_btn_index 指的是刷新按钮在工具栏的排序,默认为第四个,根据客户端实际情况调整 +user.refresh_strategy = refresh_strategies.Toolbar(refresh_btn_index=4) +``` + # 无法保存对应的 xls 文件 有些系统默认的临时文件目录过长,使用 xls 策略时无法正常保存,可通过如下方式修改为自定义目录 diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index c0ed785a..154ebd85 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -11,10 +11,11 @@ import easyutils from pywinauto import findwindows, timings -from easytrader import grid_strategies, pop_dialog_handler +from easytrader import grid_strategies, pop_dialog_handler, refresh_strategies from easytrader.config import client from easytrader.grid_strategies import IGridStrategy from easytrader.log import logger +from easytrader.refresh_strategies import IRefreshStrategy from easytrader.utils.misc import file2dict from easytrader.utils.perf import perf_clock @@ -62,6 +63,7 @@ class ClientTrader(IClientTrader): # The strategy to use for getting grid data grid_strategy: Union[IGridStrategy, Type[IGridStrategy]] = grid_strategies.Copy _grid_strategy_instance: IGridStrategy = None + refresh_strategy: IRefreshStrategy = refresh_strategies.Switch() def enable_type_keys_for_editor(self): """ @@ -478,8 +480,8 @@ def _cancel_entrust_by_double_click(self, row): ).double_click(coords=(x, y)) def refresh(self): - # self._switch_left_menus_by_shortcut("{F5}", sleep=0.1) - self._toolbar.button(3).click() # 我的交易客户端工具栏中“刷新”是排在第4个的,所以其索引值是3 + self.refresh_strategy.set_trader(self) + self.refresh_strategy.refresh() @perf_clock def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler): diff --git a/easytrader/refresh_strategies.py b/easytrader/refresh_strategies.py new file mode 100644 index 00000000..e638c359 --- /dev/null +++ b/easytrader/refresh_strategies.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +import abc +import io +import tempfile +from io import StringIO +from typing import TYPE_CHECKING, Dict, List, Optional + +import pandas as pd +import pywinauto.keyboard +import pywinauto +import pywinauto.clipboard + +from easytrader.log import logger +from easytrader.utils.captcha import captcha_recognize +from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines + +if TYPE_CHECKING: + # pylint: disable=unused-import + from easytrader import clienttrader + + +class IRefreshStrategy(abc.ABC): + _trader: "clienttrader.ClientTrader" + + @abc.abstractmethod + def refresh(self): + """ + 刷新数据 + """ + pass + + def set_trader(self, trader: "clienttrader.ClientTrader"): + self._trader = trader + + +# noinspection PyProtectedMember +class Switch(IRefreshStrategy): + """通过切换菜单栏刷新""" + + def __init__(self, sleep: float = 0.1): + self.sleep = sleep + + def refresh(self): + self._trader._switch_left_menus_by_shortcut("{F5}", sleep=self.sleep) + + +# noinspection PyProtectedMember +class Toolbar(IRefreshStrategy): + """通过点击工具栏刷新按钮刷新""" + + def __init__(self, refresh_btn_index: int = 4): + """ + :param refresh_btn_index: + 交易客户端工具栏中“刷新”排序,默认为第4个,请根据自己实际调整 + """ + self.refresh_btn_index = refresh_btn_index + + def refresh(self): + self._trader._toolbar.button(self.refresh_btn_index - 1).click() From 2411cee24cbed5800edcec188bb08b4375a0c8e8 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 26 Jun 2020 12:16:04 +0800 Subject: [PATCH 262/276] :bug: init toolbar after login --- easytrader/clienttrader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 154ebd85..12e63207 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -115,7 +115,7 @@ def connect(self, exe_path=None, **kwargs): self._app = pywinauto.Application().connect(path=connect_path, timeout=10) self._close_prompt_windows() self._main = self._app.top_window() - self._toolbar = self._main.child_window(class_name="ToolbarWindow32") + self._init_toolbar() @property def broker_type(self): @@ -127,6 +127,9 @@ def balance(self): return self._get_balance_from_statics() + def _init_toolbar(self): + self._toolbar = self._main.child_window(class_name="ToolbarWindow32") + def _get_balance_from_statics(self): result = {} for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): @@ -536,3 +539,4 @@ def prepare( comm_password, **kwargs ) + self._init_toolbar() From e85528095e06a69023e91bf97819d4b95a66b566 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Fri, 26 Jun 2020 12:23:25 +0800 Subject: [PATCH 263/276] =?UTF-8?q?Bump=20version:=200.20.5=20=E2=86=92=20?= =?UTF-8?q?0.21.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ccc0c817..3e6f3b33 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.20.5 +current_version = 0.21.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 10858924..7df2d3ee 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.20.5" +__version__ = "0.21.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 66882cf8..524c5c70 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.20.5", + version="0.21.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From ba339249fe453d7fb0a6225c54508dbe4b09f650 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 26 Jun 2020 13:19:27 +0200 Subject: [PATCH 264/276] from easytrader.utils.misc import str2num (#384) [flake8](http://flake8.pycqa.org) testing of https://github.com/shidenggui/easytrader on Python 3.8.3 $ __flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics__ ``` ./easytrader/webtrader.py:237:37: F821 undefined name 'easytrader' item[key] = easytrader.utils.misc.str2num( ^ ./easytrader/webtrader.py:241:37: F821 undefined name 'easytrader' item[key] = easytrader.utils.misc.str2num( ^ 2 F821 undefined name 'easytrader' 2 ``` https://flake8.pycqa.org/en/latest/user/error-codes.html On the flake8 test selection, this PR does _not_ focus on "_style violations_" (the majority of flake8 error codes that [__psf/black__](https://github.com/psf/black) can autocorrect). Instead these tests are focus on runtime safety and correctness: * E9 tests are about Python syntax errors usually raised because flake8 can not build an Abstract Syntax Tree (AST). Often these issues are a sign of unused code or code that has not been ported to Python 3. These would be compile-time errors in a compiled language but in a dynamic language like Python they result in the script halting/crashing on the user. * F63 tests are usually about the confusion between identity and equality in Python. Use ==/!= to compare str, bytes, and int literals is the classic case. These are areas where __a == b__ is True but __a is b__ is False (or vice versa). Python >= 3.8 will raise SyntaxWarnings on these instances. * F7 tests logic errors and syntax errors in type hints * F82 tests are almost always _undefined names_ which are usually a sign of a typo, missing imports, or code that has not been ported to Python 3. These also would be compile-time errors in a compiled language but in Python a __NameError__ is raised which will halt/crash the script on the user. --- easytrader/webtrader.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/easytrader/webtrader.py b/easytrader/webtrader.py index cafc88b2..f591db3e 100644 --- a/easytrader/webtrader.py +++ b/easytrader/webtrader.py @@ -11,7 +11,7 @@ from easytrader import exceptions from easytrader.log import logger -from easytrader.utils.misc import file2dict +from easytrader.utils.misc import file2dict, str2num from easytrader.utils.stock import get_30_date @@ -234,13 +234,9 @@ def format_response_data_type(self, response_data): for key in item: try: if re.search(int_match_str, key) is not None: - item[key] = easytrader.utils.misc.str2num( - item[key], "int" - ) + item[key] = str2num(item[key], "int") elif re.search(float_match_str, key) is not None: - item[key] = easytrader.utils.misc.str2num( - item[key], "float" - ) + item[key] = str2num(item[key], "float") except ValueError: continue return response_data From dcb8058f93fcdad1ed48ad48305e289ef9bd0c4d Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 8 Jul 2020 11:54:02 +0800 Subject: [PATCH 265/276] :bug:(pywinauto) specify pywinauto version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 524c5c70..d55ea8c5 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ "six", "easyutils", "flask", - "pywinauto", + "pywinauto==0.6.6", "pillow", "pandas", ], From d63de43bee1e7d25ae667e1e949f5dea0655ba01 Mon Sep 17 00:00:00 2001 From: shidenggui <903618848@qq.com> Date: Wed, 8 Jul 2020 11:55:47 +0800 Subject: [PATCH 266/276] =?UTF-8?q?Bump=20version:=200.21.0=20=E2=86=92=20?= =?UTF-8?q?0.22.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3e6f3b33..4604e7ae 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.21.0 +current_version = 0.22.0 commit = True files = easytrader/__init__.py setup.py tag = True diff --git a/easytrader/__init__.py b/easytrader/__init__.py index 7df2d3ee..b828f3b4 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.21.0" +__version__ = "0.22.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index d55ea8c5..9c6be0d0 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.21.0", + version="0.22.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui", From 1724b21151750a1ab0a2f227e7d797781735804d Mon Sep 17 00:00:00 2001 From: keshunchen Date: Fri, 4 Sep 2020 18:42:21 +0800 Subject: [PATCH 267/276] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=93=B6=E6=B2=B3?= =?UTF-8?q?=E4=B8=8B=E5=8D=95=E7=A8=8B=E5=BA=8F=E7=99=BB=E9=99=86=20(#401)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/yh_clienttrader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/easytrader/yh_clienttrader.py b/easytrader/yh_clienttrader.py index c0c90c0c..29343134 100644 --- a/easytrader/yh_clienttrader.py +++ b/easytrader/yh_clienttrader.py @@ -55,7 +55,10 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().Edit3.type_keys( self._handle_verify_code(is_xiadan) ) - self._app.top_window()["确定" if is_xiadan else "登录"].click() + if is_xiadan: + self._app.top_window().child_window(control_id=1006, class_name="Button").click() + else: + self._app.top_window()["登录"].click() # detect login is success or not try: From 6de1bc3bab578dff12da560de9fb14678e1fdb1b Mon Sep 17 00:00:00 2001 From: zhoubeiqing Date: Fri, 4 Sep 2020 18:43:29 +0800 Subject: [PATCH 268/276] =?UTF-8?q?=E5=88=87=E6=8D=A2=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=97=B6=E5=A2=9E=E5=8A=A0=E5=AF=B9=E5=BC=B9=E7=AA=97=E7=9A=84?= =?UTF-8?q?=E5=8E=BB=E9=99=A4=20(#387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/clienttrader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 12e63207..1e4382a7 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -447,6 +447,8 @@ def _collapse_left_menus(self): @perf_clock def _switch_left_menus(self, path, sleep=0.2): self._get_left_menus_handle().get_item(path).click() + self._app.top_window().type_keys('{ESC}') + self._app.top_window().type_keys('{F5}') self.wait(sleep) def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): From dd7c5bc11d6e87e1f0a84a3f3661d6b160777553 Mon Sep 17 00:00:00 2001 From: wangxiaowei-cloud <71587724+wangxiaowei-cloud@users.noreply.github.com> Date: Thu, 24 Sep 2020 10:03:19 +0800 Subject: [PATCH 269/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=89=93=E6=96=B0=E9=97=AE=E9=A2=98=20(#405)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自动打新必须先点击"新股申购",然后"批量新股申购" 原来的code是找到item批量新股申购然后click,这样有时候不成功 换成select()可以解决这个问题 Signed-off-by: Xiaowei Wang --- easytrader/clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 1e4382a7..de9198ed 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -446,7 +446,7 @@ def _collapse_left_menus(self): @perf_clock def _switch_left_menus(self, path, sleep=0.2): - self._get_left_menus_handle().get_item(path).click() + self._get_left_menus_handle().get_item(path).select() self._app.top_window().type_keys('{ESC}') self._app.top_window().type_keys('{F5}') self.wait(sleep) From 4bbe9feadbc99f105dd3207556897461ba823698 Mon Sep 17 00:00:00 2001 From: xiangsf <42821374+xiangsf@users.noreply.github.com> Date: Mon, 12 Oct 2020 21:38:51 +0800 Subject: [PATCH 270/276] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=8A=9F=E8=83=BD=EF=BC=8C=20=E8=8B=A5=E6=9C=89?= =?UTF-8?q?=E5=B8=AE=E5=8A=A9=EF=BC=8C=E8=AF=B7=E5=90=88=E5=B9=B6=E5=88=B0?= =?UTF-8?q?=E4=B8=BB=E7=89=88=E6=9C=AC=E4=B8=AD=20(#407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 1,加入了针对 广发证券客户端的支持。 此客户端,为同花顺定制版本; 2,加入了 cancel_all_entrusts 函数,实现批量取消; 3,在_switch_left_menus/_switch_left_menus_by_shortcut中,加入了close_pop_windows函数,避免意外弹窗导致函数失败; 4,完善了edit空间输入字符串功能 * 1,完善readme * Revert "1,完善readme" This reverts commit ef641621 --- easytrader/api.py | 5 +++ easytrader/clienttrader.py | 76 ++++++++++++++++++++++++++++++- easytrader/config/client.py | 25 +++++++++++ easytrader/gf_clienttrader.py | 84 +++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 easytrader/gf_clienttrader.py diff --git a/easytrader/api.py b/easytrader/api.py index 01823cad..355473c5 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -58,6 +58,11 @@ def use(broker, debug=False, **kwargs): return GJClientTrader() + if broker.lower() in ["gf_client", "广发客户端"]: + from .gf_clienttrader import GFClientTrader + + return GFClientTrader() + if broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index de9198ed..96a769c0 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -8,6 +8,8 @@ import time from typing import Type, Union +import hashlib, binascii + import easyutils from pywinauto import findwindows, timings @@ -23,7 +25,6 @@ import pywinauto import pywinauto.clipboard - class IClientTrader(abc.ABC): @property @abc.abstractmethod @@ -174,6 +175,29 @@ def cancel_entrust(self, entrust_no): return self._handle_pop_dialogs() return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} + def cancel_all_entrusts(self): + self.refresh() + self._switch_left_menus(["撤单[F3]"]) + + # 点击全部撤销控件 + self._app.top_window().child_window( + control_id=self._config.TRADE_CANCEL_ALL_ENTRUST_CONTROL_ID, class_name="Button", title_re="""全撤.*""" + ).click() + self.wait(0.2) + + # 等待出现 确认兑换框 + if self.is_exist_pop_dialog(): + # 点击是 按钮 + w = self._app.top_window() + if w is not None: + btn = w["是(Y)"] + if btn is not None: + btn.click() + self.wait(0.2) + + # 如果出现了确认窗口 + self.close_pop_dialog() + @perf_clock def repo(self, security, price, amount, **kwargs): self._switch_left_menus(["债券回购", "融资回购(正回购)"]) @@ -278,6 +302,24 @@ def _set_market_trade_type(self, ttype): return raise TypeError("不支持对应的市价类型: {}".format(ttype)) + def _set_stock_exchange_type(self, ttype): + """根据选择的市价交易类型选择对应的下拉选项""" + selects = self._main.child_window( + control_id=self._config.TRADE_STOCK_EXCHANGE_CONTROL_ID, class_name="ComboBox" + ) + + for i, text in enumerate(selects.texts()): + # skip 0 index, because 0 index is current select index + if i == 0: + if ttype.strip() == text.strip(): # 当前已经选中 + return + else: + continue + if ttype.strip() == text.strip(): + selects.select(i - 1) + return + raise TypeError("不支持对应的市场类型: {}".format(ttype)) + def auto_ipo(self): self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) @@ -330,6 +372,21 @@ def is_exist_pop_dialog(self): logger.exception("check pop dialog timeout") return False + @perf_clock + def close_pop_dialog(self): + try: + if self._main.wrapper_object() != self._app.top_window().wrapper_object(): + w = self._app.top_window() + if w is not None: + w.close() + self.wait(0.2) + except ( + findwindows.ElementNotFoundError, + timings.TimeoutError, + RuntimeError, + ) as ex: + pass + def _run_exe_path(self, exe_path): return os.path.join(os.path.dirname(exe_path), "xiadan.exe") @@ -397,6 +454,14 @@ def _set_trade_params(self, security, price, amount): # wait security input finish self.wait(0.1) + # 设置交易所 + if security.lower().startswith("sz"): + self._set_stock_exchange_type("深圳A股") + if security.lower().startswith("sh"): + self._set_stock_exchange_type("上海A股") + + self.wait(0.1) + self._type_edit_control_keys( self._config.TRADE_PRICE_CONTROL_ID, easyutils.round_price_by_code(price, code), @@ -439,6 +504,13 @@ def _type_edit_control_keys(self, control_id, text): editor.select() editor.type_keys(text) + def type_edit_control_keys(self, editor, text): + if not self._editor_need_type_keys: + editor.set_edit_text(text) + else: + editor.select() + editor.type_keys(text) + def _collapse_left_menus(self): items = self._get_left_menus_handle().roots() for item in items: @@ -446,12 +518,14 @@ def _collapse_left_menus(self): @perf_clock def _switch_left_menus(self, path, sleep=0.2): + self.close_pop_dialog() self._get_left_menus_handle().get_item(path).select() self._app.top_window().type_keys('{ESC}') self._app.top_window().type_keys('{F5}') self.wait(sleep) def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5): + self.close_pop_dialog() self._app.top_window().type_keys(shortcut) self.wait(sleep) diff --git a/easytrader/config/client.py b/easytrader/config/client.py index e591f8cb..e3386237 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -6,6 +6,8 @@ def create(broker): return HT if broker == "gj": return GJ + if broker == "gf": + return GF if broker == "ths": return CommonConfig if broker == "wk": @@ -19,6 +21,12 @@ class CommonConfig: DEFAULT_EXE_PATH: str = "" TITLE = "网上股票交易系统5.0" + # 交易所类型。 深圳A股、上海A股 + TRADE_STOCK_EXCHANGE_CONTROL_ID = 1003 + + # 撤销界面上, 全部撤销按钮 + TRADE_CANCEL_ALL_ENTRUST_CONTROL_ID = 30001 + TRADE_SECURITY_CONTROL_ID = 1032 TRADE_PRICE_CONTROL_ID = 1033 TRADE_AMOUNT_CONTROL_ID = 1034 @@ -135,6 +143,23 @@ class GJ(CommonConfig): AUTO_IPO_MENU_PATH = ["新股申购", "新股批量申购"] +class GF(CommonConfig): + DEFAULT_EXE_PATH = "C:\\gfzqrzrq\\xiadan.exe" + TITLE = "核新网上交易系统" + + GRID_DTYPE = { + "操作日期": str, + "委托编号": str, + "申请编号": str, + "合同编号": str, + "证券代码": str, + "股东代码": str, + "资金帐号": str, + "资金帐户": str, + "发生日期": str, + } + + AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"] class WK(HT): pass diff --git a/easytrader/gf_clienttrader.py b/easytrader/gf_clienttrader.py new file mode 100644 index 00000000..ab5e6e8d --- /dev/null +++ b/easytrader/gf_clienttrader.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +import re +import tempfile +import time +import os + +import pywinauto +import pywinauto.clipboard + +from easytrader import clienttrader +from easytrader.utils.captcha import recognize_verify_code + + +class GFClientTrader(clienttrader.BaseLoginClientTrader): + @property + def broker_type(self): + return "gf" + + def login(self, user, password, exe_path, comm_password=None, **kwargs): + """ + 登陆客户端 + + :param user: 账号 + :param password: 明文密码 + :param exe_path: 客户端路径类似 'C:\\中国银河证券双子星3.2\\Binarystar.exe', + 默认 'C:\\中国银河证券双子星3.2\\Binarystar.exe' + :param comm_password: 通讯密码, 华泰需要,可不设 + :return: + """ + try: + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1 + ) + # pylint: disable=broad-except + except Exception: + self._app = pywinauto.Application().start(exe_path) + + # wait login window ready + while True: + try: + self._app.top_window().Edit1.wait("ready") + break + except RuntimeError: + pass + + self.type_edit_control_keys(self._app.top_window().Edit1, user) + self.type_edit_control_keys(self._app.top_window().Edit2, password) + edit3 = self._app.top_window().window(control_id=0x3eb) + while True: + try: + code = self._handle_verify_code() + self.type_edit_control_keys(edit3, code) + time.sleep(1) + self._app.top_window()["登录(Y)"].click() + # detect login is success or not + try: + self._app.top_window().wait_not("exists", 5) + break + + # pylint: disable=broad-except + except Exception: + self._app.top_window()["确定"].click() + + # pylint: disable=broad-except + except Exception: + pass + + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10 + ) + self._main = self._app.window(title_re="""{title}.*""".format(title=self._config.TITLE)) + self.close_pop_dialog() + + def _handle_verify_code(self): + control = self._app.top_window().window(control_id=0x5db) + control.click() + time.sleep(0.2) + file_path = tempfile.mktemp() + ".jpg" + control.capture_as_image().save(file_path) + time.sleep(0.2) + vcode = recognize_verify_code(file_path, "gf_client") + if os.path.exists(file_path): + os.remove(file_path) + return "".join(re.findall("[a-zA-Z0-9]+", vcode)) From 5fdbaf9dc0d65eed31e43f9b4d02af4520081583 Mon Sep 17 00:00:00 2001 From: wangxiaowei-cloud <71587724+wangxiaowei-cloud@users.noreply.github.com> Date: Fri, 23 Oct 2020 14:13:00 +0800 Subject: [PATCH 271/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=8E=E6=B3=B0?= =?UTF-8?q?=E8=AF=81=E5=88=B8=E7=99=BB=E5=85=A5=E8=BF=87=E7=A8=8B=E6=AD=BB?= =?UTF-8?q?=E9=94=81=E7=9A=84=E9=97=AE=E9=A2=98=20(#408)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 华泰证券在登入的过程中会出现死锁,这个死锁问题在单CPU的平台更容易出现 出现问题的步骤: 1,自动输入用户名和密码 2,调用self._app.top_window().wait_not("exists", 100),等待登入窗口消失,CPU进入睡眠,CPU调度去干别的事情了 3,华泰证券登入成功,登入窗口消失,主窗口出现 4,CPU调度回来,继续执行self._app.top_window().wait_not("exists", 100),问题是,现在的top_window()不再是登入窗口,而是主窗口,程序一直死等主窗口消失。死锁了 修复办法: 不是dengd登入窗口消失,而是等待主窗口变成visible Signed-off-by: Xiaowei Wang --- easytrader/ht_clienttrader.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index d7853387..feb45b0d 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -50,14 +50,12 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().button0.click() - # detect login is success or not - self._app.top_window().wait_not("exists", 100) - self._app = pywinauto.Application().connect( path=self._run_exe_path(exe_path), timeout=10 ) - self._close_prompt_windows() self._main = self._app.window(title="网上股票交易系统5.0") + self._main.wait ( "exists enabled visible ready" , timeout=100 ) + self._close_prompt_windows ( ) @property def balance(self): From e68b04d701cb3e447903c3e2ade564b98a046b8e Mon Sep 17 00:00:00 2001 From: Kang Zhao Date: Sun, 22 Nov 2020 14:35:39 +0800 Subject: [PATCH 272/276] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=8D=8E=E6=B3=B0?= =?UTF-8?q?=E9=80=9A=E8=AE=AF=E5=AF=86=E7=A0=81=E5=8C=85=E5=90=AB=E7=89=B9?= =?UTF-8?q?=E6=AE=8A=E5=AD=97=E7=AC=A6=E6=97=B6=E4=B8=8D=E8=83=BD=E8=A2=AB?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E8=BE=93=E5=85=A5=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(#412)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/ht_clienttrader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easytrader/ht_clienttrader.py b/easytrader/ht_clienttrader.py index feb45b0d..b1b0e869 100644 --- a/easytrader/ht_clienttrader.py +++ b/easytrader/ht_clienttrader.py @@ -46,7 +46,7 @@ def login(self, user, password, exe_path, comm_password=None, **kwargs): self._app.top_window().Edit1.type_keys(user) self._app.top_window().Edit2.type_keys(password) - self._app.top_window().Edit3.type_keys(comm_password) + self._app.top_window().Edit3.set_edit_text(comm_password) self._app.top_window().button0.click() From c9bb9bc670764007b8dab956c351b09ab217b215 Mon Sep 17 00:00:00 2001 From: Ckend <83493903@qq.com> Date: Wed, 6 Jan 2021 07:55:52 +0800 Subject: [PATCH 273/276] =?UTF-8?q?optimization:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E8=AF=86=E5=88=AB=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=EF=BC=8C=E5=8A=A0=E5=BF=AB=E8=8E=B7=E5=8F=96=E9=80=9F=E5=BA=A6?= =?UTF-8?q?=20(#415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easytrader/grid_strategies.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easytrader/grid_strategies.py b/easytrader/grid_strategies.py index eebed922..b294b967 100644 --- a/easytrader/grid_strategies.py +++ b/easytrader/grid_strategies.py @@ -109,7 +109,8 @@ def _get_clipboard_data(self) -> str: file_path ) # 保存验证码 - captcha_num = captcha_recognize(file_path) # 识别验证码 + captcha_num = captcha_recognize(file_path).strip() # 识别验证码 + captcha_num = "".join(captcha_num.split()) logger.info("captcha result-->" + captcha_num) if len(captcha_num) == 4: self._trader.app.top_window().window( From ca18e4e5effbac140f0a8d0b7c3c16ef7e7eed57 Mon Sep 17 00:00:00 2001 From: r52097 Date: Wed, 10 Mar 2021 10:57:14 +0800 Subject: [PATCH 274/276] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E7=89=88=E5=90=8C=E8=8A=B1=E9=A1=BA=E8=87=AA=E5=8A=A8=E6=89=93?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD=20(#424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来代码中,选中“批量新股申购”菜单后会模拟“ESC”键和“F5”键输入。但在通用版同花顺里,“ESC”键输入会导致“批量新股申购”菜单失选,故删除该语句。 用通用版同花顺登陆国联证券账户测试自动打新成功,另测试海通证券客户端无影响。 Signed-off-by: Jack Huang --- easytrader/clienttrader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/easytrader/clienttrader.py b/easytrader/clienttrader.py index 96a769c0..b0a4b648 100644 --- a/easytrader/clienttrader.py +++ b/easytrader/clienttrader.py @@ -520,7 +520,6 @@ def _collapse_left_menus(self): def _switch_left_menus(self, path, sleep=0.2): self.close_pop_dialog() self._get_left_menus_handle().get_item(path).select() - self._app.top_window().type_keys('{ESC}') self._app.top_window().type_keys('{F5}') self.wait(sleep) From 376c10a4faea123fae391faf0e3b90215468c7d3 Mon Sep 17 00:00:00 2001 From: r52097 Date: Fri, 12 Mar 2021 15:55:11 +0800 Subject: [PATCH 275/276] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=90=8C=E8=8A=B1?= =?UTF-8?q?=E9=A1=BA=E5=AE=98=E7=BD=91=E4=B8=8B=E8=BD=BD=E7=9A=84=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=EF=BC=88=E5=86=85=E5=90=AB=E5=AF=B9=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E5=88=B8=E5=95=86=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=89?= =?UTF-8?q?=20(#426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 测试国联证券、申万宏源证券通过 Signed-off-by: Jack Huang --- docs/index.md | 3 +- docs/usage.md | 23 +++++++++-- easytrader/api.py | 5 +++ easytrader/config/client.py | 15 +++++++ easytrader/universal_clienttrader.py | 60 ++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 easytrader/universal_clienttrader.py diff --git a/docs/index.md b/docs/index.md index 7bb88f79..0f1fd337 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,8 @@ * 海通客户端(海通网上交易系统独立委托) * 华泰客户端(网上交易系统(专业版Ⅱ)) * 国金客户端(全能行证券交易终端PC版) -* 其他券商通用同花顺客户端(需要手动登陆) +* 通用同花顺客户端(同花顺免费版) +* 其他券商专用同花顺客户端(需要手动登陆) ### 模拟交易 diff --git a/docs/usage.md b/docs/usage.md index 75e352b6..559b70b1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -26,12 +26,21 @@ user = easytrader.use('gj_client') **通用同花顺客户端** +```python +user = easytrader.use('universal_client') +``` + +注: 通用同花顺客户端是指同花顺官网提供的客户端软件内的下单程序,内含对多个券商的交易支持,适用于券商不直接提供同花顺客户端时的后备方案。 + +**其他券商专用同花顺客户端** ```python user = easytrader.use('ths') ``` -注: 通用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本),国金证券网上交易独立下单程序(核新PC版)等。 +注: 其他券商专用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本),国金证券网上交易独立下单程序(核新PC版)等。 + + **雪球** @@ -42,9 +51,9 @@ user = easytrader.use('xq') ## 三、启动并连接客户端 -### (一)同花顺客户端 +### (一)其他券商专用同花顺客户端 -通用同花顺客户端不支持自动登录,需要先手动登录。 +其他券商专用同花顺客户端不支持自动登录,需要先手动登录。 请手动打开并登录客户端后,运用connect函数连接客户端。 @@ -52,7 +61,13 @@ user = easytrader.use('xq') user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' ``` -### (二)非同花顺客户端 +### (二)通用同花顺客户端 + +需要先手动登录一次:添加券商,填入账户号、密码、验证码,勾选“保存密码” + +第一次登录后,上述信息被缓存,可以调用prepare函数自动登录(仅需账户号、客户端路径,密码随意输入)。 + +### (三)其它 非同花顺的客户端,可以调用prepare函数自动登录。 diff --git a/easytrader/api.py b/easytrader/api.py index 355473c5..fbdc37c3 100644 --- a/easytrader/api.py +++ b/easytrader/api.py @@ -63,6 +63,11 @@ def use(broker, debug=False, **kwargs): return GFClientTrader() + if broker.lower() in ["universal_client", "通用同花顺客户端"]: + from easytrader.universal_clienttrader import UniversalClientTrader + + return UniversalClientTrader() + if broker.lower() in ["ths", "同花顺客户端"]: from .clienttrader import ClientTrader diff --git a/easytrader/config/client.py b/easytrader/config/client.py index e3386237..a028ecef 100644 --- a/easytrader/config/client.py +++ b/easytrader/config/client.py @@ -14,6 +14,8 @@ def create(broker): return WK if broker == "htzq": return HTZQ + if broker == "universal": + return UNIVERSAL raise NotImplementedError @@ -176,3 +178,16 @@ class HTZQ(CommonConfig): } AUTO_IPO_NUMBER = '可申购数量' + + +class UNIVERSAL(CommonConfig): + DEFAULT_EXE_PATH = r"c:\\ths\\xiadan.exe" + + BALANCE_CONTROL_ID_GROUP = { + "资金余额": 1012, + "可用金额": 1016, + "可取金额": 1017, + "总资产": 1015, + } + + AUTO_IPO_NUMBER = '可申购数量' diff --git a/easytrader/universal_clienttrader.py b/easytrader/universal_clienttrader.py new file mode 100644 index 00000000..afec1eae --- /dev/null +++ b/easytrader/universal_clienttrader.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +import pywinauto +import pywinauto.clipboard + +from easytrader import grid_strategies +from . import clienttrader + + +class UniversalClientTrader(clienttrader.BaseLoginClientTrader): + grid_strategy = grid_strategies.Xls + + @property + def broker_type(self): + return "universal" + + def login(self, user, password, exe_path, comm_password=None, **kwargs): + """ + :param user: 用户名 + :param password: 密码 + :param exe_path: 客户端路径, 类似 + :param comm_password: + :param kwargs: + :return: + """ + self._editor_need_type_keys = False + + try: + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=1 + ) + # pylint: disable=broad-except + except Exception: + self._app = pywinauto.Application().start(exe_path) + + # wait login window ready + while True: + try: + login_window = pywinauto.findwindows.find_window(class_name='#32770', found_index=1) + break + except: + self.wait(1) + + self.wait(1) + self._app.window(handle=login_window).Edit1.set_focus() + self._app.window(handle=login_window).Edit1.type_keys(user) + + self._app.window(handle=login_window).button7.click() + + # detect login is success or not + # self._app.top_window().wait_not("exists", 100) + self.wait(5) + + self._app = pywinauto.Application().connect( + path=self._run_exe_path(exe_path), timeout=10 + ) + + self._close_prompt_windows() + self._main = self._app.window(title="网上股票交易系统5.0") + From dbb166564c6c73da3446588a19d2692ad52716cb Mon Sep 17 00:00:00 2001 From: shidenggui Date: Sun, 14 Mar 2021 20:54:52 +0800 Subject: [PATCH 276/276] =?UTF-8?q?Bump=20version:=200.22.0=20=E2=86=92=20?= =?UTF-8?q?0.23.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 3 +-- easytrader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4604e7ae..3ab275f2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,6 @@ [bumpversion] -current_version = 0.22.0 +current_version = 0.23.0 commit = True files = easytrader/__init__.py setup.py tag = True tag_name = {new_version} - diff --git a/easytrader/__init__.py b/easytrader/__init__.py index b828f3b4..ecff7756 100644 --- a/easytrader/__init__.py +++ b/easytrader/__init__.py @@ -7,5 +7,5 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -__version__ = "0.22.0" +__version__ = "0.23.0" __author__ = "shidenggui" diff --git a/setup.py b/setup.py index 9c6be0d0..61a540c2 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup( name="easytrader", - version="0.22.0", + version="0.23.0", description="A utility for China Stock Trade", long_description=long_desc, author="shidenggui",