├── trade_bundle ├── .DS_Store └── live_trade.py ├── trade_order ├── .DS_Store ├── src │ ├── .DS_Store │ ├── exceptions.py │ ├── ocr.py │ ├── mail.py │ ├── pop_dialog_handler.py │ ├── grid_strategy.py │ ├── client.py │ ├── index.py │ └── zt_clienttrader.py └── order_api.py ├── requirements.txt ├── index.py ├── .gitignore └── README.md /trade_bundle/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyQuant/real_trader/HEAD/trade_bundle/.DS_Store -------------------------------------------------------------------------------- /trade_order/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyQuant/real_trader/HEAD/trade_order/.DS_Store -------------------------------------------------------------------------------- /trade_order/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyQuant/real_trader/HEAD/trade_order/src/.DS_Store -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.17.4 2 | pandas==0.24.2 3 | requests==2.22.0 4 | apscheduler==3.6.3 5 | easyutils==0.1.7 6 | jqdatasdk==1.7.8 7 | Pillow==6.2.1 8 | pyautogui==0.9.48 9 | pytesseract==0.3.1 10 | pywinauto==0.6.8 11 | -------------------------------------------------------------------------------- /trade_order/src/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | class TradeError(IOError): 3 | pass 4 | 5 | class NotLoginError(Exception): 6 | def __init__(self, result=None): 7 | super(NotLoginError, self).__init__() 8 | self.result = result 9 | -------------------------------------------------------------------------------- /trade_order/src/ocr.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageGrab 2 | import pytesseract 3 | import time 4 | 5 | ''' 6 | 读取图片 7 | ''' 8 | def _get_file_content(filePath): 9 | with open(filePath, 'rb') as fp: 10 | return fp.read() 11 | 12 | def get_yzm_text(control): 13 | point = control.element_info.rectangle 14 | pic = ImageGrab.grab(bbox=(point.left, point.top, point.right, point.bottom)) 15 | pic.save('./yzm.png') 16 | 17 | ## ocr识别验证码 18 | image = Image.open('./yzm.png') 19 | result = pytesseract.image_to_string(image) # 解析图片 20 | return result -------------------------------------------------------------------------------- /trade_order/src/mail.py: -------------------------------------------------------------------------------- 1 | #导入smtplib模块 2 | from smtplib import SMTP 3 | from email.mime.text import MIMEText 4 | from email.header import Header 5 | 6 | # 封装的邮件对象 7 | class Mail: 8 | email_client = None 9 | smtp_host = 'smtp.qq.com' 10 | from_addr = None 11 | to_addrs = None 12 | password = None 13 | 14 | def __init__(self, from_addr, password, to_addrs): 15 | self.from_addr = from_addr 16 | self.to_addrs = to_addrs 17 | self.password = password 18 | 19 | def send_email(self, subject, content): 20 | self.email_client = SMTP(self.smtp_host) 21 | self.email_client.login(self.from_addr, self.password) 22 | msg = MIMEText(content,'plain','utf-8') 23 | msg['Subject'] = Header(subject, 'utf-8')#subject 24 | self.email_client.sendmail(self.from_addr, self.to_addrs, msg.as_string()) 25 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | # 简单的tick级别实时交易 支持聚宽语法 2 | # 依赖 以下类库 3 | 4 | # jqdatasdk 聚宽提供的历史行情 5 | # trade_bundle 开源的python实时tick行情 以及策略运行框架 [后续会提供历史tick数据的接口调用] 6 | # trade_order 开源的python实盘交易接口 7 | from jqdatasdk import * 8 | from trade_bundle.live_trade import * 9 | from trade_order.order_api import * 10 | 11 | def initialize(context): 12 | print('##### initialize #####') 13 | 14 | # 订阅多个标的 15 | subscribe('600519.XSHG', 'tick') 16 | subscribe('000858.XSHE', 'tick') 17 | 18 | # 测试jqdata数据 19 | print(get_price('000001.XSHE', start_date='2015-12-01 14:00:00', end_date='2015-12-02 12:00:00', frequency='1m')) 20 | 21 | def before_trading_start(context): 22 | print('##### before_trading_start #####') 23 | 24 | def handle_tick(context, tick): 25 | print('##### handle_tick #####') 26 | print(tick.current) 27 | # order('600519.XSHG', 100) 28 | 29 | def after_trading_end(context): 30 | print('##### after_trading_end #####') 31 | unsubscribe_all() 32 | 33 | # 初始化jqdatasdk 34 | auth('聚宽账号','聚宽密码') 35 | 36 | # 初始化实盘模块 37 | init_trader(g, context, '资金账号', '资金密码', r'E:\中泰证券独立下单\xiadan.exe') 38 | 39 | # 初始化实时tick行情 40 | init_current_bundle(initialize, before_trading_start, after_trading_end, handle_tick) 41 | -------------------------------------------------------------------------------- /trade_order/src/pop_dialog_handler.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import re 3 | import time 4 | from typing import Optional 5 | 6 | class PopDialogHandler: 7 | def __init__(self, app): 8 | self._app = app 9 | 10 | def handle(self, title): 11 | 12 | if any(s in title for s in {'提示信息', '委托确认', '网上交易用户协议'}): 13 | self._submit_by_shortcut() 14 | return None 15 | 16 | if '提示' in title: 17 | content = self._extract_content() 18 | self._submit_by_click() 19 | return {'message': content} 20 | 21 | content = self._extract_content() 22 | self._close() 23 | return {'message': 'unknown message: {}'.format(content)} 24 | 25 | def _extract_content(self): 26 | return self._app.top_window().Static.window_text() 27 | 28 | def _extract_entrust_id(self, content): 29 | return re.search(r"\d+", content).group() 30 | 31 | def _submit_by_click(self): 32 | self._app.top_window()['确定'].click() 33 | 34 | def _submit_by_shortcut(self): 35 | self._app.top_window().type_keys('%Y') 36 | 37 | def _close(self): 38 | self._app.top_window().close() 39 | 40 | 41 | class TradePopDialogHandler(PopDialogHandler): 42 | def handle(self, title) -> Optional[dict]: 43 | 44 | if title == '委托确认': 45 | self._submit_by_shortcut() 46 | return None 47 | 48 | if title == '提示信息': 49 | content = self._extract_content() 50 | 51 | if '超出涨跌停' in content: 52 | self._submit_by_shortcut() 53 | return None 54 | 55 | if '委托价格的小数价格应为' in content: 56 | self._submit_by_shortcut() 57 | return None 58 | 59 | if '没有行情数据或行情不正常,要继续吗' in content: 60 | self._submit_by_shortcut() 61 | self._submit_by_shortcut() 62 | return None 63 | 64 | return None 65 | 66 | if title == '提示': 67 | content = self._extract_content() 68 | if '成功' in content: 69 | entrust_no = self._extract_entrust_id(content) 70 | self._submit_by_click() 71 | return {'entrust_no': entrust_no} 72 | 73 | self._submit_by_click() 74 | time.sleep(0.1) 75 | return {'message': content} 76 | self._close() 77 | return None 78 | -------------------------------------------------------------------------------- /trade_order/src/grid_strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | import io 4 | import tempfile 5 | from typing import TYPE_CHECKING, Dict, List 6 | 7 | import pandas as pd 8 | import pywinauto.clipboard 9 | import time 10 | 11 | class IGridStrategy(abc.ABC): 12 | 13 | def __init__(self): 14 | pass 15 | 16 | @abc.abstractmethod 17 | def get(self, control_id: int) -> List[Dict]: 18 | """ 19 | 获取 gird 数据并格式化返回 20 | 21 | :param control_id: grid 的 control id 22 | :return: grid 数据 23 | """ 24 | 25 | pass 26 | 27 | class BaseStrategy(IGridStrategy): 28 | def __init__(self, trader) -> None: 29 | self._trader = trader 30 | 31 | @abc.abstractmethod 32 | def get(self, control_id: int) -> List[Dict]: 33 | """ 34 | :param control_id: grid 的 control id 35 | :return: grid 数据 36 | """ 37 | pass 38 | 39 | def _get_grid(self, control_id: int): 40 | grid = self._trader.main.child_window( 41 | control_id=control_id, class_name="CVirtualGridCtrl" 42 | ) 43 | return grid 44 | 45 | class Copy(BaseStrategy): 46 | """ 47 | 通过复制 grid 内容到剪切板z再读取来获取 grid 内容 48 | """ 49 | 50 | def get(self, control_id: int) -> List[Dict]: 51 | # print('通过复制 grid 内容到剪切板z再读取来获取 grid 内容') 52 | 53 | time.sleep(0.3) 54 | 55 | grid = self._get_grid(control_id) 56 | grid.type_keys("^A^C") 57 | 58 | def _format_grid_data(self, data: str) -> List[Dict]: 59 | df = pd.read_csv( 60 | io.StringIO(data), 61 | delimiter="\t", 62 | dtype=self._trader.config.GRID_DTYPE, 63 | na_filter=False, 64 | ) 65 | return df.to_dict("records") 66 | 67 | def _get_clipboard_data(self) -> str: 68 | while True: 69 | try: 70 | return pywinauto.clipboard.GetData() 71 | # pylint: disable=broad-except 72 | except Exception as e: 73 | pass 74 | # log.warning("%s, retry ......", e) -------------------------------------------------------------------------------- /trade_order/src/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def create(broker): 3 | if broker == "zt": 4 | return ZT 5 | 6 | if broker == "ths": 7 | return CommonConfig 8 | raise NotImplementedError 9 | 10 | 11 | class CommonConfig: 12 | DEFAULT_EXE_PATH: str = "" 13 | TITLE = "网上股票交易系统5.0" 14 | 15 | TRADE_SECURITY_CONTROL_ID = 1032 16 | TRADE_PRICE_CONTROL_ID = 1033 17 | TRADE_AMOUNT_CONTROL_ID = 1034 18 | 19 | TRADE_SUBMIT_CONTROL_ID = 1006 20 | 21 | TRADE_MARKET_TYPE_CONTROL_ID = 1541 22 | 23 | COMMON_GRID_CONTROL_ID = 1047 24 | 25 | COMMON_GRID_LEFT_MARGIN = 10 26 | COMMON_GRID_FIRST_ROW_HEIGHT = 30 27 | COMMON_GRID_ROW_HEIGHT = 16 28 | 29 | BALANCE_MENU_PATH = ["查询[F4]", "资金股票"] 30 | POSITION_MENU_PATH = ["查询[F4]", "资金股票"] 31 | TODAY_ENTRUSTS_MENU_PATH = ["查询[F4]", "当日委托"] 32 | TODAY_TRADES_MENU_PATH = ["查询[F4]", "当日成交"] 33 | 34 | BALANCE_CONTROL_ID_GROUP = { 35 | "资金余额": 1012, 36 | "可用金额": 1016, 37 | "可取金额": 1017, 38 | "股票市值": 1014, 39 | "总资产": 1015, 40 | } 41 | 42 | POP_DIALOD_TITLE_CONTROL_ID = 1365 43 | 44 | GRID_DTYPE = { 45 | "操作日期": str, 46 | "委托编号": str, 47 | "申请编号": str, 48 | "合同编号": str, 49 | "证券代码": str, 50 | "股东代码": str, 51 | "资金帐号": str, 52 | "资金帐户": str, 53 | "发生日期": str, 54 | } 55 | 56 | CANCEL_ENTRUST_ENTRUST_FIELD = "合同编号" 57 | CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50 58 | CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30 59 | CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16 60 | 61 | AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098 62 | AUTO_IPO_BUTTON_CONTROL_ID = 1006 63 | AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"] 64 | 65 | 66 | class ZT(CommonConfig): 67 | DEFAULT_EXE_PATH = r"C:\双子星-中国银河证券\Binarystar.exe" 68 | 69 | BALANCE_GRID_CONTROL_ID = 1308 70 | 71 | GRID_DTYPE = { 72 | "操作日期": str, 73 | "委托编号": str, 74 | "申请编号": str, 75 | "合同编号": str, 76 | "证券代码": str, 77 | "股东代码": str, 78 | "资金帐号": str, 79 | "资金帐户": str, 80 | "发生日期": str, 81 | } 82 | 83 | AUTO_IPO_MENU_PATH = ["新股申购", "一键打新"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # real_trader 2 | 3 | 本地化的tick实盘交易解决方案 [历史行情数据 + 实时tick数据 + 实盘下单] 后续增加历史tick接口 4 | 5 | 目前支持的券商 中泰证券 6 | 7 | 支持聚宽策略代码直接使用 8 | 9 | 使用期间遇到问题 或 需要低佣金开户 欢迎加我微信沟通 10 | 11 | 12 | 13 | ### 安装 tesseract-ocr 14 | 15 | #### 1. 下载 tesseract-ocr 并配置环境变量 16 | 17 | 百度网盘下载地址 tesseract-ocr 18 | 19 | 配置环境变量可以参考如下链接 (感谢大佬 Front-biger 提供服务器资源) 20 | 21 | 如何配置tesseract-ocr环境变量 22 | 23 | #### 2. 下载券商客户端 24 | http://download.95538.cn/download/software/hx/ths_order.exe 25 | 26 | #### 3. 安装python依赖 27 | 28 | ``` 29 | ## 安装依赖 30 | cd real_trader && pip install -r requirements.txt 31 | ``` 32 | 33 | 期间可能出现 Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools 34 | 35 | 可以下载 visualcppbuildtools_full 解决此问题 36 | 37 | ### 云端部署 38 | 建议使用 TightVNC 微软自带的远程桌面有些技术问题 39 | 40 | ### 编写一个简单的例子 订阅贵州茅台的实时tick并下单 41 | 42 | 新建一个code.py 43 | 44 | 首先引入依赖 45 | 46 | ``` 47 | ## 引入所需的python类库 48 | from jqdatasdk import * 49 | from trade_bundle.live_trade import * 50 | from trade_order.order_api import * 51 | ``` 52 | 53 | 然后编写整体结构 54 | 55 | ``` 56 | ## 初始化时调用 57 | def initialize(context): 58 | 59 | ## 订阅贵州茅台的tick 60 | ## 通常是在开盘订阅 这里为了测试放在了初始化函数里订阅 61 | subscribe('600519.XSHG', 'tick') 62 | 63 | ## 开盘前调用 09:00 64 | def before_trading_start(context): 65 | pass 66 | 67 | ## 盘中tick触发调用 68 | def handle_tick(context, tick): 69 | 70 | ## 这里打印出订阅的股票代码和当前价格 71 | print('股票代码 => {} 当前价格 => {}'.format(tick.code, tick.current)) 72 | 73 | ## 查询实盘账号有多少可用资金 74 | cash = context.portfolio.available_cash 75 | print('当前可用资金 => {}'.format(cash)) 76 | 77 | ## 交易函数慎重调用 因为直接对接实盘 78 | ## 满仓市价买入贵州茅台 79 | ## order_value(tick.code, cash) 80 | 81 | ## 市价买入100股贵州茅台 82 | ## order(tick.code, 100) 83 | 84 | ## 收盘后半小时调用 15:30 85 | def after_trading_end(context): 86 | 87 | ## 收盘后取消所有标的订阅 88 | unsubscribe_all() 89 | ``` 90 | 91 | 紧跟着在代码最后添加配置信息 92 | 93 | ``` 94 | ## 初始化jqdatasdk 95 | ## 方便获取历史行情和财务数据 暂时从聚宽获取 96 | ## 这里需要申请一下 https://www.joinquant.com/default/index/sdk 97 | auth('聚宽账号','聚宽密码') 98 | 99 | ## 初始化实盘下单模块 100 | ## 这里填写实盘资金账号和密码 还有 券商客户端安装路径 101 | ## 客户端默认安装路径是 D:\中泰证券独立下单\xiadan.exe 102 | init_trader(g, context, '资金账号', '资金密码', r'D:\中泰证券独立下单\xiadan.exe') 103 | 104 | ## 初始化实时tick行情 105 | init_current_bundle(initialize, before_trading_start, after_trading_end, handle_tick) 106 | ``` 107 | 108 | 最后完整的代码应该是这样 使用python index.py 执行就可以了 109 | 110 | ``` 111 | ## 引入所需的python类库 112 | from jqdatasdk import * 113 | from trade_bundle.live_trade import * 114 | from trade_order.order_api import * 115 | 116 | ## 初始化时调用 117 | def initialize(context): 118 | 119 | ## 订阅贵州茅台的tick 120 | ## 通常是在开盘订阅 这里为了测试放在了初始化函数里订阅 121 | subscribe('600519.XSHG', 'tick') 122 | 123 | ## 开盘前调用 09:00 124 | def before_trading_start(context): 125 | pass 126 | 127 | ## 盘中tick触发调用 128 | def handle_tick(context, tick): 129 | 130 | ## 这里打印出订阅的股票代码和当前价格 131 | print('股票代码 => {} 当前价格 => {}'.format(tick.code, tick.current)) 132 | 133 | ## 查询实盘账号有多少可用资金 134 | cash = context.portfolio.available_cash 135 | print('当前可用资金 => {}'.format(cash)) 136 | 137 | ## 交易函数慎重调用 因为直接对接实盘 需要时解开注释 138 | ## 满仓市价买入贵州茅台 139 | ## order_value(tick.code, cash) 140 | 141 | ## 市价买入100股贵州茅台 142 | ## order(tick.code, 100) 143 | 144 | ## 收盘后半小时调用 15:30 145 | def after_trading_end(context): 146 | 147 | ## 收盘后取消所有标的订阅 148 | unsubscribe_all() 149 | 150 | ## 初始化jqdatasdk 151 | ## 方便获取历史行情和财务数据 暂时从聚宽获取 152 | ## 这里需要申请一下 https://www.joinquant.com/default/index/sdk 153 | auth('聚宽账号','聚宽密码') 154 | 155 | ## 初始化实盘下单模块 156 | ## 这里填写实盘资金账号和密码 还有 券商客户端安装路径 157 | ## 客户端默认安装路径是 D:\中泰证券独立下单\xiadan.exe 158 | init_trader(g, context, '资金账号', '资金密码', r'D:\中泰证券独立下单\xiadan.exe') 159 | 160 | ## 初始化实时tick行情 161 | init_current_bundle(initialize, before_trading_start, after_trading_end, handle_tick) 162 | ``` 163 | -------------------------------------------------------------------------------- /trade_order/src/index.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | from trade_order.src.zt_clienttrader import ZTClientTrader 4 | from trade_order.src.mail import Mail 5 | 6 | # 通用对象转化 7 | class X(object): 8 | 9 | def __init__(self, **kwargs): 10 | 11 | for name, value in kwargs.items(): 12 | setattr(self, name, value) 13 | 14 | class ZhongTaiTrader: 15 | 16 | # 中泰接口基础参数 17 | mail = None 18 | 19 | def __init__(self): 20 | self.trader = ZTClientTrader() 21 | 22 | def send_email(self, title, context): 23 | 24 | if self.mail != None: 25 | self.mail.send_email(title, context) 26 | else: 27 | print('### 未开启邮件通知 ###') 28 | 29 | # 启动客户端 30 | def run_client(self, client_path): 31 | self.trader.connect(client_path) 32 | 33 | # 启动下单邮件推送 34 | # account 发送者 35 | # password 发送者密码 36 | # 接收者 37 | def run_email(self, account, password, form_account): 38 | 39 | if account != None and password != None and form_account != None: 40 | self.mail = Mail(account, password, form_account) 41 | print('### 已开启邮件通知 ###') 42 | else: 43 | print('### 未开启邮件通知 ###') 44 | 45 | # 登录 46 | def login(self, account, password): 47 | self.trader.login(account, password) 48 | self.send_email('中泰账号 ' + account + ' 登录成功', '') 49 | 50 | # 限价买入 51 | def buy(self, security, amount, price): 52 | result = self.trader.buy(security, price, amount) 53 | 54 | if 'message' in result: 55 | self.send_email('中泰账号限价单买入失败', '错误原因' + result['message']) 56 | else: 57 | self.send_email('中泰账号限价单买入成功' + security, '买入股数' + str(amount) + ', 价格' + str(price)) 58 | 59 | print(result) 60 | return result 61 | 62 | # 限价卖出 63 | def sell(self, security, amount, price): 64 | result = self.trader.sell(security, price, amount) 65 | 66 | if 'message' in result: 67 | self.send_email('中泰账号限价单卖出失败', '错误原因' + result['message']) 68 | else: 69 | self.send_email('中泰账号限价单卖出成功' + security, '卖出股数' + str(amount) + ', 价格' + str(price)) 70 | 71 | print(result) 72 | return result 73 | 74 | # 市价买入 75 | def market_buy(self, security, amount): 76 | result = self.trader.market_buy(security, amount) 77 | 78 | if 'message' in result: 79 | self.send_email('中泰账号市价单买入失败', '错误原因' + result['message']) 80 | else: 81 | self.send_email('中泰账号市价单买入成功' + security, '买入股数' + str(amount)) 82 | 83 | print(result) 84 | return result 85 | 86 | # 市价卖出 87 | def market_sell(self, security, amount): 88 | result = self.trader.market_sell(security, amount) 89 | 90 | if 'message' in result: 91 | self.send_email('中泰账号市价单卖出失败', '错误原因' + result['message']) 92 | else: 93 | self.send_email('中泰账号市价单卖出成功' + security, '卖出股数' + str(amount)) 94 | 95 | print(result) 96 | return result 97 | 98 | # 撤单 99 | def cancel_entrust(self, entrust_id): 100 | result = self.trader.cancel_entrust(entrust_id) 101 | print(result) 102 | self.send_email('中泰账号撤单', '撤单结果: ' + json.dumps(result)) 103 | 104 | # 持仓 105 | def position(self): 106 | result = self.trader.get_position() 107 | data = result['cash'] 108 | positions_data = result['position'] 109 | portfolio_return = 0 110 | positions = {} 111 | 112 | for item in positions_data: 113 | security = self.parse_stock_code(item.get('证券代码')) 114 | positions[security] = { 115 | 'security_name': item.get('证券名称'), 116 | 'security': security, 117 | 'price': item.get('市价'), 118 | 'acc_avg_cost': item.get('参考成本'), 119 | 'avg_cost': item.get('参考成本'), 120 | 'locked_amount': int(item.get('持股数量')) - int(item.get('可用余额')), 121 | 'value': float(item.get('参考市值')), 122 | 'closeable_amount': float(item.get('可用余额')), 123 | 'total_amount': int(item.get('持股数量')), 124 | 'returns': float(item.get('累计盈亏2')), 125 | 'current_value': float(item.get('参考盈亏')), 126 | 'current_returns': float(item.get('盈亏比例(%)')) 127 | } 128 | 129 | portfolio_return = portfolio_return + float(item.get('参考盈亏')) 130 | 131 | portfolio = { 132 | 'total_value': float(data.get('总资产')), 133 | 'available_cash': float(data.get('可用金额')), 134 | 'transferable_cash': float(data.get('可取金额')), 135 | 'positions_value': float(data.get('股票市值')), 136 | 'returns': portfolio_return 137 | } 138 | 139 | result = { 140 | 'positions': positions, 141 | 'portfolio': portfolio 142 | } 143 | 144 | print(result) 145 | return result 146 | 147 | # 交易列表 148 | def trades(self): 149 | result = self.trader.today_trades() 150 | _list = [] 151 | data = self.parse_result_list(result) 152 | 153 | if 'data' in data: 154 | 155 | for item in data['data']: 156 | _list.append(self.parse({ 157 | 'trade_id': item.get('合同编号'), 158 | 'security': self.parse_stock_code(item.get('证券代码')) 159 | })) 160 | 161 | return _list 162 | else: 163 | return data 164 | 165 | # 委托列表 166 | def today_entrusts(self): 167 | result = self.trader.today_entrusts() 168 | _list = [] 169 | data = self.parse_result_list(result) 170 | 171 | if 'data' in data: 172 | 173 | for item in data['data']: 174 | _list.append(self.parse({ 175 | 'order_id': item.get('合同编号'), 176 | 'security': self.parse_stock_code(item.get('证券代码')), 177 | 'status': item.get('委托状态') 178 | })) 179 | return _list 180 | else: 181 | return data 182 | 183 | # 撤单列表 184 | def cancel_entrusts(self): 185 | result = self.trader.cancel_entrusts() 186 | _list = [] 187 | data = self.parse_result_list(result) 188 | 189 | if 'data' in data: 190 | 191 | for item in data['data']: 192 | _list.append(self.parse({ 193 | 'order_id': item.get('合同编号'), 194 | 'security': self.parse_stock_code(item.get('证券代码')), 195 | 'status': item.get('委托状态') 196 | })) 197 | 198 | return _list 199 | else: 200 | return data 201 | 202 | def auto_ipo(self): 203 | self.trader.auto_ipo() 204 | 205 | def parse_result_order(self, result): 206 | 207 | if result['Status'] == 0: 208 | return { 209 | 'entrust_id': result['Data'][0]['Wtbh'] 210 | } 211 | else: 212 | return { 213 | 'message': result['Message'] 214 | } 215 | 216 | def parse_result_list(self, result): 217 | 218 | # 如果是数组 说明数据正确返回 219 | if isinstance (result, list): 220 | return { 221 | 'data': result 222 | } 223 | else: 224 | return { 225 | 'message': result 226 | } 227 | 228 | # 处理股票代码为聚宽格式的 229 | def parse_stock_code(self, code): 230 | 231 | # 将正则表达式编译成Pattern对象 232 | regXSHE = re.compile('^(002|000|300|1599|1610)') 233 | regXSHG = re.compile('^(600|601|603|51)') 234 | 235 | matchXSHE = regXSHE.match(code) 236 | matchXSHG = regXSHG.match(code) 237 | 238 | if matchXSHE and len(matchXSHE.group()): 239 | return '.'.join([code, 'XSHE']) 240 | elif matchXSHG and len(matchXSHG.group()): 241 | return '.'.join([code, 'XSHG']) 242 | 243 | ## 格式化返回的数据 244 | def parse(self, data): 245 | result = json.loads(json.dumps(data), object_hook=lambda d: X(**d)) 246 | return result -------------------------------------------------------------------------------- /trade_bundle/live_trade.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | import datetime 4 | import json 5 | import pandas as pd 6 | import numpy as np 7 | from apscheduler.schedulers.background import BackgroundScheduler 8 | from apscheduler.schedulers.blocking import BlockingScheduler 9 | 10 | # 订阅的标的列表 11 | stock_list = [] 12 | session = None 13 | cookies = None 14 | headers = { 15 | 'Accept':'*/*', 16 | 'Origin':'https://xueqiu.com', 17 | 'Referer':'https://xueqiu.com/S/SH600519', 18 | 'Sec-Fetch-Mode':'cors', 19 | 'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' 20 | } 21 | 22 | NAN_DT = datetime.datetime(2200, 1, 1) 23 | 24 | class _Global(object): 25 | 26 | def __init__(self, **kwargs): 27 | 28 | for name, value in kwargs.items(): 29 | setattr(self, name, value) 30 | 31 | # 通用对象转化 32 | class _Context(object): 33 | 34 | def __init__(self, **kwargs): 35 | 36 | for name, value in kwargs.items(): 37 | setattr(self, name, value) 38 | 39 | class Tick(object): 40 | def __init__(self, security, tick): 41 | self._security = parse_xq_code(security) 42 | self._tick = tick 43 | 44 | @property 45 | def code(self): 46 | return self._security 47 | 48 | @property 49 | def time(self): 50 | try: 51 | return self._tick['time'] 52 | except: 53 | return NAN_DT 54 | 55 | @property 56 | def current(self): 57 | try: 58 | return self._tick['current'] 59 | except: 60 | return np.nan 61 | 62 | @property 63 | def high(self): 64 | try: 65 | return self._tick['high'] 66 | except: 67 | return np.nan 68 | 69 | @property 70 | def low(self): 71 | try: 72 | return self._tick['low'] 73 | except: 74 | return np.nan 75 | 76 | @property 77 | def trade_volume(self): 78 | try: 79 | return self._tick['trade_volume'] 80 | except: 81 | return np.nan 82 | 83 | @property 84 | def volume(self): 85 | try: 86 | return self._tick['volume'] 87 | except: 88 | return np.nan 89 | 90 | @property 91 | def money(self): 92 | try: 93 | return self._tick['money'] 94 | except: 95 | return np.nan 96 | 97 | # 通用对象转化 98 | class CurrentDict(object): 99 | 100 | def __init__(self, **kwargs): 101 | 102 | for name, value in kwargs.items(): 103 | setattr(self, name, value) 104 | 105 | # 当前行情对象 106 | class _CurrentDic(dict): 107 | 108 | def __init__(self, date): 109 | pass 110 | 111 | def __missing__(self, code): 112 | info = _global['session'].get('https://stock.xueqiu.com/v5/stock/quote.json?extend=detail&symbol=' + parse_code(code), cookies = _global['cookies'], headers = headers).json() 113 | quote = info['data']['quote'] 114 | stock = quote['symbol'] 115 | result = { 116 | 'name': quote['name'], 117 | 'time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(quote['timestamp'] / 1000)), 118 | 'current': quote['current'], 119 | 'high': quote['high'], 120 | 'low': quote['low'], 121 | 'volume': quote['volume'], 122 | 'money': quote['amount'], 123 | 'day_open': quote['open'], 124 | 'high_limit': quote['limit_up'], 125 | 'low_limit': quote['limit_down'], 126 | 'industry_code': quote['type'], 127 | 'is_st': quote['status'] == 2 128 | } 129 | 130 | return parse(result) 131 | 132 | # 初始化实时行情模块 133 | # 主要工作是 初始化爬虫cookie 初始化tick定时器 完善全局对象 134 | def init_current_bundle(initialize, before_trading_start, after_trading_end, handle_tick): 135 | _global['initialize'] = initialize 136 | _global['before_trading_start'] = before_trading_start 137 | _global['after_trading_end'] = after_trading_end 138 | _global['handle_tick'] = handle_tick 139 | cookies = get_cookie() 140 | 141 | # 执行初始化函数 142 | initialize(_global['context']) 143 | 144 | # 初始化tick定时器 145 | init_schedudler() 146 | 147 | # 创建定时器 148 | # 完成开盘 收盘 盘中3秒批量查询一次最新tick数据 等默认事件 149 | def init_schedudler(): 150 | schedudler = BlockingScheduler() 151 | schedudler.add_job(func = _global['before_trading_start'], args = [_global['context']], trigger = 'cron', hour = 9, minute = 9, day_of_week = 'mon-fri') 152 | schedudler.add_job(func = _global['after_trading_end'], args = [_global['context']], trigger = 'cron', hour = 15, minute=30, day_of_week = 'mon-fri') 153 | schedudler.add_job(_get_current_tick, 'cron', second = '*/3') 154 | schedudler.start() 155 | 156 | def get_cookie(): 157 | cookies = requests.cookies.RequestsCookieJar() 158 | _global['session'] = requests.session() 159 | r = _global['session'].get('https://xueqiu.com/k?q=SZ131810', headers = headers) 160 | _global['cookies'] = r.cookies 161 | return cookies 162 | 163 | # 抓取当日历史tick数据 164 | def get_ticks(security, end_dt, count, start_dt = None): 165 | print('### 自定义 get_ticks ###') 166 | result = [] 167 | 168 | # 大于100取东方财富的 稍后实现 169 | if count > 100: 170 | pass 171 | else: 172 | info = _global['session'].get('https://stock.xueqiu.com/v5/stock/history/trade.json?symbol=' + parse_code(security) + '&count=' + str(count), cookies = _global['cookies'], headers = headers).json() 173 | ticks = info['data']['items'] 174 | 175 | for tick in ticks: 176 | result.append({ 177 | 'time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(tick['timestamp'] / 1000)), 178 | 'current': tick['current'], 179 | 'trade_volume': tick['trade_volume'], 180 | }) 181 | 182 | return result 183 | 184 | # 获取最新tick数据 185 | def get_current_tick(stock, df = False): 186 | info = _global['session'].get('https://stock.xueqiu.com/v5/stock/realtime/quotec.json?symbol=' + parse_code(stock), cookies = _global['cookies'], headers = headers).json() 187 | quote = info['data'][0] 188 | stock = quote['symbol'] 189 | result = { 190 | 'time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(quote['timestamp'] / 1000)), 191 | 'current': quote['current'], 192 | 'high': quote['high'], 193 | 'low': quote['low'], 194 | 'trade_volume': quote['trade_volume'], 195 | 'volume': quote['volume'], 196 | 'money': quote['amount'] 197 | } 198 | 199 | return Tick(stock, result) 200 | 201 | # 获取当日最新数据 包含涨停跌停等 202 | def get_current_data(): 203 | print('### 自定义 get_current_data ###') 204 | current = _CurrentDic({}) 205 | return current 206 | 207 | # 获取最新tick数据 208 | def _get_current_tick(): 209 | stocks = [] 210 | 211 | if len(stock_list): 212 | 213 | for stock in stock_list: 214 | stocks.append(parse_code(stock)) 215 | 216 | info = _global['session'].get('https://stock.xueqiu.com/v5/stock/realtime/quotec.json?symbol=' + ','.join(stocks), cookies = _global['cookies'], headers = headers).json() 217 | quotes = info['data'] 218 | 219 | for quote in quotes: 220 | stock = quote['symbol'] 221 | result = { 222 | 'time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(quote['timestamp'] / 1000)), 223 | 'current': quote['current'], 224 | 'high': quote['high'], 225 | 'low': quote['low'], 226 | 'trade_volume': quote['trade_volume'], 227 | 'volume': quote['volume'], 228 | 'money': quote['amount'] 229 | } 230 | _global['handle_tick'](_global['context'], Tick(stock, result)) 231 | 232 | def parse_code(code): 233 | 234 | if code.endswith('XSHE'): 235 | return 'SZ' + code.split('.')[0] 236 | elif code.endswith('XSHG'): 237 | return 'SH' + code.split('.')[0] 238 | 239 | def parse_xq_code(code): 240 | 241 | if code.startswith('SZ'): 242 | return code[2:8] + '.XSHE' 243 | elif code.startswith('SH'): 244 | return code[2:8] + '.XSHG' 245 | 246 | # 要暴露的函数 247 | def subscribe(security, frequency): 248 | print('### 自定义 subscribe ###') 249 | 250 | # 加入队列 251 | stock_list.append(security) 252 | print('添加标的到队列 => ', security) 253 | # print('当前订阅的标的队列 => ', stock_list) 254 | 255 | # 取消订阅标的的 tick 事件 256 | def unsubcribe(security, frequency): 257 | print('### 自定义 unsubcribe ###') 258 | 259 | if security in stock_list: 260 | stock_list.remove(security) 261 | 262 | # 取消订阅所有 tick 事件 263 | def unsubscribe_all(): 264 | print('### 自定义 unsubscribe_all ###') 265 | stock_list = [] 266 | 267 | # 定时执行任务 268 | def run_daily(event, time): 269 | _time = time.split(':') 270 | hour = int(_time[0]) 271 | minute = int(_time[1]) 272 | schedudler = BackgroundScheduler() 273 | schedudler.add_job(func = event, args = [_global['context']], trigger = 'cron', hour = hour, minute = minute, day_of_week = 'mon-fri') 274 | schedudler.start() 275 | 276 | ## 格式化返回的数据 277 | def parse(data): 278 | result = json.loads(json.dumps(data), object_hook=lambda d: CurrentDict(**d)) 279 | return result 280 | 281 | ## 格式化返回的数据 282 | def _parse_global(data): 283 | result = json.loads(json.dumps(data), object_hook=lambda d: _Global(**d)) 284 | return result 285 | 286 | ## 格式化返回的数据 287 | def _parse_context(data): 288 | result = json.loads(json.dumps(data), object_hook=lambda d: _Context(**d)) 289 | return result 290 | 291 | # 暴露的全局变量 292 | g = _parse_global({}) 293 | context = _parse_context({}) 294 | 295 | # 保存一些内容 296 | _global = { 297 | 'session': None, 298 | 'cookies': None, 299 | 'context': context, 300 | 'g': g 301 | } -------------------------------------------------------------------------------- /trade_order/order_api.py: -------------------------------------------------------------------------------- 1 | # 封装一层函数 兼容聚宽的实盘和回测函数调用 2 | from trade_order.src.index import ZhongTaiTrader, X 3 | 4 | import requests 5 | import time 6 | import json 7 | from collections import namedtuple 8 | 9 | # 实时数据模块 [tick相关函数覆盖] 10 | from trade_bundle.live_trade import get_current_tick 11 | 12 | _order_global = {} 13 | 14 | # 委托单对象 15 | class RealOrders(): 16 | 17 | def __init__(self, value): 18 | self.value = value 19 | 20 | def values(self): 21 | return self.value 22 | 23 | # 初始化实盘模块 24 | def init_trader(g, context, account, password, path): 25 | print('##### 初始化实盘模块 #####') 26 | _order_global['trader'] = ZhongTaiTrader() 27 | _order_global['g'] = g 28 | _order_global['context'] = context 29 | print('准备登录资金账号 {}'.format(account)) 30 | run_client(path) 31 | login(account, password) 32 | time.sleep(1) 33 | handle_async_portfolio(True) 34 | 35 | # 初始化邮件模块 36 | def send_email(title, context): 37 | _order_global['trader'].send_email(title, context) 38 | 39 | def run_client(path): 40 | _order_global['trader'].run_client(path) 41 | 42 | # 登陆账号 43 | def login(userId, password): 44 | info = _order_global['trader'].login(userId, password) 45 | 46 | # 按数量买入 47 | # stock 600519.XSHG 股票代码 48 | # amount 委托数量 49 | # style MarketOrder 市价 / LimitOrder 限价 50 | def order(stock, amount, price = None): 51 | style = None 52 | info = order_type(stock, price, style) 53 | return order_amount(info.get('security'), amount, info.get('price'), info.get('style')) 54 | 55 | # 买入到指定数量 56 | # stock 600519.XSHG 股票代码 57 | # amount 委托数量 58 | # style MarketOrder 市价 / LimitOrder 限价 59 | def order_target(stock, amount, price = None): 60 | style = None 61 | info = order_type(stock, price, style) 62 | security = info.get('security') 63 | price = info.get('price') 64 | style = info.get('style') 65 | amount = order_target_amount(info.get('security'), amount) 66 | return order_amount(security, amount, price, style) 67 | 68 | # 按价值下单 69 | # stock 600519.XSHG 股票代码 70 | # value 下单金额 71 | # amount 委托数量 72 | # style MarketOrder 市价 / LimitOrder 限价 73 | def order_value(stock, value, price = None): 74 | style = None 75 | info = order_type(stock, price, style) 76 | amount = round_amount(value / info.get('price')) 77 | return order_amount(info.get('security'), amount, info.get('price'), info.get('style')) 78 | 79 | # 买入到指定价值 80 | # stock 600519.XSHG 股票代码 81 | # value 买入金额 82 | # amount 委托数量 83 | # style MarketOrder 市价 / LimitOrder 限价 84 | def order_target_value(stock, value, price = None): 85 | style = None 86 | info = order_type(stock, price, style) 87 | amount = round_amount(value / info.get('price')) 88 | amount = order_target_amount(info.get('security'), amount) 89 | return order_amount(info.get('security'), amount, info.get('price'), info.get('style')) 90 | 91 | # 撤单 92 | # order_id 单号id 93 | def cancel_order(order_id): 94 | _order_global['trader'].cancel_entrust(order_id) 95 | handle_after_order() 96 | 97 | # 实现一些钩子函数 98 | # TODO: 钩子函数 99 | # 更新实盘账户信息后 100 | def handle_after_async_portfolio(): 101 | _order_global['g'].async_portfolio_flag = False 102 | 103 | # TODO: 钩子函数 104 | # 同步实盘账户信息 105 | # is_force 是否强制更新持仓 默认只有下单后调用才会更新 106 | def handle_async_portfolio(is_force = False): 107 | 108 | # 如果是强制查询 109 | if is_force == True: 110 | _order_global['g'].async_portfolio_flag = True 111 | 112 | if (_order_global['g'].async_portfolio_flag): 113 | position = _order_global['trader'].position() 114 | 115 | ## 格式化持仓信息然后赋值给portfolio 116 | positions = parse_positions(position['positions']) 117 | 118 | ## 格式化资产信息 119 | _order_global['context'].portfolio = parse(position['portfolio']) 120 | _order_global['context'].portfolio.positions = positions 121 | _order_global['g'].context = _order_global['context'] 122 | 123 | # 查询后的钩子 124 | handle_after_async_portfolio() 125 | return _order_global['context'].portfolio 126 | 127 | # TODO: 钩子函数 128 | # 实盘下单 129 | def handle_order(): 130 | pass 131 | 132 | # 更新成交状态 下一个tick同步实盘账户信息 133 | # TODO: 钩子函数 134 | # 实盘下单后 135 | def handle_after_order(): 136 | # print('### 有下单动作 下一个tick进行持仓查询 ### ') 137 | _order_global['g'].async_portfolio_flag = True 138 | 139 | # 获取订单信息 140 | def get_orders(): 141 | return RealOrders(_order_global['trader'].today_entrusts()) 142 | 143 | # 获取未完成订单 144 | def get_open_orders(): 145 | return RealOrders(_order_global['trader'].cancel_entrusts()) 146 | 147 | # 获取成交信息 148 | def get_trades(): 149 | return RealOrders(_order_global['trader'].trades()) 150 | 151 | # 满额打新股 152 | def auto_ipo(): 153 | pass 154 | 155 | # 统一委托下单类型 156 | def order_type(stock, price, style): 157 | security = stock.split('.')[0] 158 | 159 | # 如果不传入价格 同时委托类型为市价 则传当前价格 160 | if (price == None): 161 | price = get_current_tick(stock).current 162 | style = 'MarketOrder' 163 | else: 164 | style = 'LimitOrder' 165 | 166 | return { 167 | 'security': security, 168 | 'style': style, 169 | 'price': price 170 | } 171 | 172 | # 根据持仓指定下单数量 173 | def order_target_amount(security, amount): 174 | # print(_order_global['context'].portfolio.positions) 175 | 176 | if _order_global['trader'].parse_stock_code(security) in _order_global['context'].portfolio.positions: 177 | 178 | # 获取当前持仓 179 | info = _order_global['context'].portfolio.positions[_order_global['trader'].parse_stock_code(security)] 180 | 181 | # print('info => ', info) 182 | 183 | # 获取持有数量 184 | total_amount = int(info['total_amount']) 185 | else: 186 | total_amount = 0 187 | 188 | # 如果预期数量等于0 则清仓 189 | if amount <= 0: 190 | amount = 0 - total_amount 191 | 192 | elif (amount > total_amount) or (amount < total_amount): 193 | amount = amount - total_amount 194 | 195 | # 如果预期数量等于持有数量 则忽略 196 | elif amount == total_amount: 197 | amount = 0 198 | 199 | return amount 200 | 201 | # 底层委托函数 202 | def order_amount(security, amount, price, style): 203 | amount = revision_amount(security, _order_global['context'].portfolio.available_cash, amount, price) 204 | result = {} 205 | 206 | if amount == 0: 207 | result = { 208 | 'message': '预期数量等于持有数量 忽略本次下单' 209 | } 210 | print('风控函数 标的: {} 回报信息: {}'.format(security, result['message'])) 211 | else: 212 | 213 | if amount > 0: 214 | 215 | if style == 'LimitOrder': 216 | result = _order_global['trader'].buy(security, amount, price) 217 | else: 218 | result = _order_global['trader'].market_buy(security, amount, price) 219 | 220 | elif amount < 0: 221 | amount = revision_closeable_amount(security, amount) 222 | 223 | if amount != 0: 224 | 225 | if style == 'LimitOrder': 226 | result = _order_global['trader'].sell(security, abs(amount), price) 227 | else: 228 | result = _order_global['trader'].market_sell(security, abs(amount), price) 229 | else: 230 | print('风控函数 标的: {} 委托方向: Sell 委托类型: {} 回报信息: {}'.format(security, style, '预期卖出数量等于 0 忽略本次下单')) 231 | 232 | if amount != 0: 233 | 234 | if 'message' in result: 235 | print('委托函数 委托失败 标的: {} 委托方向: {} 委托类型: {} 数量: {} 回报信息: {}'.format(security, 'Buy' if amount > 0 else 'Sell', style, amount, result['message'])) 236 | else: 237 | print('委托函数 委托成功 标的: {} 委托方向: {} 委托类型: {} 数量: {} 委托编号: {}'.format(security, 'Buy' if amount > 0 else 'Sell', style, amount, result['entrust_id'])) 238 | 239 | 240 | # 下一个tick更新账户信息 241 | handle_after_order() 242 | 243 | def parse_positions(data): 244 | result = {} 245 | positions = data 246 | 247 | for key in positions: 248 | 249 | if key != 'undefined': 250 | result[key] = parse(positions[key]) 251 | return result 252 | 253 | ## 格式化返回的数据 254 | def parse(data): 255 | result = json.loads(json.dumps(data), object_hook=lambda d: X(**d)) 256 | return result 257 | 258 | def parse_result(data): 259 | 260 | ## 如果存在message 261 | if ('message' in data): 262 | print(data['message']) 263 | return data['message'] 264 | else: 265 | return parse(data) 266 | 267 | ## 实盘的限价单函数 268 | def LimitOrderStyle(price): 269 | return price 270 | 271 | # 根据可用资金调整买入股数 272 | def revision_amount(stock, value, amount, price): 273 | 274 | # 如果买入股数 * 价格 * 手续费 大于可用余额 则调整买入股数 275 | if amount * price > value: 276 | print('风控函数 标的: {} 预期买入股数: {} 价格: {} 买入价值: {} '.format(stock, amount, price, amount * price)) 277 | # print('风控函数 标的: {} 预期买入股数: {} 价格: {} 可用资金: {}'.format(stock, amount, price, value)) 278 | amount = value / price 279 | amount = round_amount(amount) 280 | print('风控函数 大于可用余额: {} 调整买入股数为: {} '.format(value, amount)) 281 | 282 | return amount 283 | 284 | # 根据可卖数量调整卖出股数 285 | def revision_closeable_amount(stock, amount): 286 | 287 | if _order_global['trader'].parse_stock_code(stock) in _order_global['context'].portfolio.positions: 288 | info = _order_global['context'].portfolio.positions[_order_global['trader'].parse_stock_code(stock)] 289 | 290 | # 获取可卖数量 291 | closeable_amount = int(info.closeable_amount) 292 | 293 | # 如果绝对值卖出数量大于可卖数量 294 | if abs(amount) > closeable_amount: 295 | print('风控函数 标的: {} 预期卖出股数: {}'.format(stock, abs(amount))) 296 | print('风控函数 标的: {} 持仓可用股数: {}'.format(stock, closeable_amount)) 297 | print('风控函数 标的: {} 预期卖出股数大于持仓可用股数 卖出股数调整为持仓可用股数'.format(stock)) 298 | return 0 - closeable_amount 299 | else: 300 | return amount 301 | else: 302 | return 0 303 | 304 | # 整除股数 305 | def round_amount(amount): 306 | return int((amount) / 100) * 100 -------------------------------------------------------------------------------- /trade_order/src/zt_clienttrader.py: -------------------------------------------------------------------------------- 1 | # 券商客户端自动下单 2 | import pyautogui 3 | import functools 4 | from pywinauto.application import Application 5 | 6 | from typing import Type 7 | from trade_order.src.grid_strategy import IGridStrategy 8 | from trade_order.src.grid_strategy import Copy 9 | 10 | from trade_order.src.pop_dialog_handler import TradePopDialogHandler 11 | from trade_order.src.pop_dialog_handler import PopDialogHandler 12 | 13 | import time 14 | import easyutils 15 | 16 | from trade_order.src.client import * 17 | from trade_order.src.ocr import * 18 | 19 | class ZTClientTrader(): 20 | 21 | grid_strategy: Type[IGridStrategy] = Copy 22 | 23 | @property 24 | def broker_type(self): 25 | return "ths" 26 | 27 | @property 28 | def config(self): 29 | return self._config 30 | 31 | @property 32 | def app(self): 33 | return self._app 34 | 35 | @property 36 | def main(self): 37 | return self._main 38 | 39 | def __init__(self): 40 | self._config = create(self.broker_type) 41 | self._app = None 42 | self._main = None 43 | 44 | ''' 45 | 获取矩阵内容 46 | ''' 47 | def _get_grid_data(self, control_id): 48 | return self.grid_strategy(self).get(control_id) 49 | 50 | ''' 51 | 处理复制时产生的验证码 52 | ''' 53 | def _parse_grid_yzm(self): 54 | self.wait(0.3) 55 | self._main = self._app.top_window() 56 | yzm_control = self._main.children(class_name = "Static")[1] 57 | input_control = self._main.child_window(class_name = 'Edit') 58 | code = get_yzm_text(yzm_control) 59 | 60 | ## 如果识别长度不为4则重试 61 | if (len(code) != 4): 62 | self._main.children(title='取消')[0].click() 63 | return True 64 | else: 65 | input_control.set_text(code) 66 | self._main.children(title='确定')[0].click() 67 | self._main = self._app.top_window() 68 | error_control = self._main.children(class_name = "Static")[2].window_text() 69 | 70 | if error_control == '验证码错误!!': 71 | self._main.children(title='取消')[0].click() 72 | return True 73 | 74 | def _switch_left_menus(self, path, sleep=0.2): 75 | self._main = self._app.top_window() 76 | self._get_left_menus_handle().get_item(path).click() 77 | self._app.top_window().type_keys('{F5}') 78 | self.wait(sleep) 79 | 80 | @functools.lru_cache() 81 | def _get_left_menus_handle(self): 82 | while True: 83 | try: 84 | handle = self._main.child_window( 85 | control_id=129, class_name="SysTreeView32" 86 | ) 87 | # sometime can't find handle ready, must retry 88 | handle.wait("ready", 2) 89 | return handle 90 | # pylint: disable=broad-except 91 | except Exception: 92 | pass 93 | 94 | def wait(self, seconds): 95 | time.sleep(seconds) 96 | 97 | ''' 初始化运行交易客户端 ''' 98 | def connect(self, client_path = None): 99 | self._app = Application().start(client_path) 100 | self._main = self._app.top_window() 101 | 102 | ''' 登录交易客户端 ''' 103 | def login(self, account, password): 104 | 105 | # 选择营业部 106 | self._main.ComboBox2.select(15) 107 | 108 | ## 输入账号 109 | self._main.children(class_name='Edit')[0].type_keys(account) 110 | time.sleep(1) 111 | 112 | ## 输入密码 113 | self._main.children(class_name='Edit')[1].type_keys(password) 114 | time.sleep(1) 115 | 116 | yzm_control = self._main.children(class_name = "Static")[0] 117 | 118 | code = get_yzm_text(yzm_control) 119 | 120 | wins = self._main.children(class_name='Edit')[6] 121 | wins.type_keys(code) 122 | self._main.child_window(class_name='Button', title='确定(&Y)').click() 123 | 124 | # 重新赋值 125 | self._main = self._app.top_window() 126 | time.sleep(2) 127 | 128 | def refresh(self): 129 | self._switch_left_menus(["买入[F1]"], sleep=0.05) 130 | 131 | def _get_balance_from_statics(self): 132 | result = {} 133 | self._app.top_window().type_keys('{F5}') 134 | self.wait(0.3) 135 | 136 | for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items(): 137 | result[key] = self._main.child_window( 138 | control_id=control_id, class_name="Static" 139 | ).window_text() 140 | return result 141 | 142 | ''' 查询持仓 [包含资金详情和持仓详情] ''' 143 | def get_position(self): 144 | self.refresh() 145 | self._app.top_window().type_keys('{F5}') 146 | self.wait(0.3) 147 | self._switch_left_menus(["查询[F4]", "资金股票"]) 148 | 149 | ''' 获取资金详情 ''' 150 | cash = self._get_balance_from_statics() 151 | 152 | ''' 获取持仓 ''' 153 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 154 | isNotYzm = self._parse_grid_yzm() 155 | 156 | if isNotYzm == True: 157 | self._switch_left_menus(["查询[F4]", "资金股票"]) 158 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 159 | self._parse_grid_yzm() 160 | 161 | # 解析剪贴板上的数据 162 | content = self.grid_strategy(self)._get_clipboard_data() 163 | position = self.grid_strategy(self)._format_grid_data(content) 164 | 165 | result = { 166 | 'cash': cash, 167 | 'position': position 168 | } 169 | 170 | # 返回持仓的json数据 171 | return result 172 | else: 173 | 174 | # 解析剪贴板上的数据 175 | content = self.grid_strategy(self)._get_clipboard_data() 176 | position = self.grid_strategy(self)._format_grid_data(content) 177 | 178 | result = { 179 | 'cash': cash, 180 | 'position': position 181 | } 182 | 183 | # 返回持仓的json数据 184 | return result 185 | 186 | ''' 查询当日委托 ''' 187 | def today_entrusts(self): 188 | self._switch_left_menus(["查询[F4]", "当日委托"]) 189 | self.wait(0.1) 190 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 191 | self.wait(0.1) 192 | isNotYzm = self._parse_grid_yzm() 193 | 194 | if isNotYzm == True: 195 | self._switch_left_menus(["查询[F4]", "当日委托"]) 196 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 197 | self._parse_grid_yzm() 198 | else: 199 | 200 | # 解析剪贴板上的数据 201 | content = self.grid_strategy(self)._get_clipboard_data() 202 | result = self.grid_strategy(self)._format_grid_data(content) 203 | 204 | # 返回持仓的json数据 205 | return result 206 | 207 | ''' 查询当日成交 ''' 208 | def today_trades(self): 209 | self._switch_left_menus(["查询[F4]", "当日成交"]) 210 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 211 | isNotYzm = self._parse_grid_yzm() 212 | 213 | if isNotYzm == True: 214 | self._switch_left_menus(["查询[F4]", "当日成交"]) 215 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 216 | self._parse_grid_yzm() 217 | else: 218 | 219 | # 解析剪贴板上的数据 220 | content = self.grid_strategy(self)._get_clipboard_data() 221 | result = self.grid_strategy(self)._format_grid_data(content) 222 | 223 | # 返回持仓的json数据 224 | return result 225 | 226 | ''' 查询撤单列表 ''' 227 | def cancel_entrusts(self): 228 | self._app.top_window().type_keys('{F5}') 229 | self.wait(0.2) 230 | self.refresh() 231 | self.wait(0.2) 232 | self._switch_left_menus(["撤单[F3]"]) 233 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 234 | isNotYzm = self._parse_grid_yzm() 235 | 236 | # 如果验证码识别失败 237 | if isNotYzm == True: 238 | self._switch_left_menus(["撤单[F3]"]) 239 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 240 | self._parse_grid_yzm() 241 | else: 242 | 243 | # 解析剪贴板上的数据 244 | content = self.grid_strategy(self)._get_clipboard_data() 245 | result = self.grid_strategy(self)._format_grid_data(content) 246 | self._app.top_window().type_keys('{F5}') 247 | return result 248 | 249 | ''' 撤单 ''' 250 | def cancel_entrust(self, entrust_no): 251 | self._app.top_window().type_keys('{F5}') 252 | 253 | # 获取撤单列表 254 | for i, entrust in enumerate(self.cancel_entrusts()): 255 | 256 | if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == str(entrust_no): 257 | self._cancel_entrust_by_double_click(i) 258 | self._app.top_window().type_keys('{F5}') 259 | return self._handle_pop_dialogs() 260 | 261 | return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"} 262 | 263 | 264 | ''' 执行撤单操作 ''' 265 | def _cancel_entrust_by_double_click(self, row): 266 | self.wait(0.3) 267 | x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN 268 | y = ( 269 | self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT 270 | + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row 271 | ) 272 | 273 | self._app.top_window().child_window( 274 | control_id=self._config.COMMON_GRID_CONTROL_ID, 275 | class_name="CVirtualGridCtrl", 276 | ).double_click(coords=(x, y)) 277 | 278 | def _is_exist_pop_dialog(self): 279 | self.wait(0.1) 280 | return ( 281 | self._main.wrapper_object() 282 | != self._app.top_window().wrapper_object() 283 | ) 284 | 285 | def _get_pop_dialog_title(self): 286 | return ( 287 | self._app.top_window() 288 | .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID) 289 | .window_text() 290 | ) 291 | 292 | def _handle_pop_dialogs( 293 | self, handler_class=PopDialogHandler 294 | ): 295 | handler = handler_class(self._app) 296 | while self._is_exist_pop_dialog(): 297 | title = self._get_pop_dialog_title() 298 | 299 | result = handler.handle(title) 300 | 301 | if result: 302 | return result 303 | return {"message": "success"} 304 | 305 | def _submit_trade(self): 306 | time.sleep(0.05) 307 | self._main.child_window( 308 | control_id=self._config.TRADE_SUBMIT_CONTROL_ID, 309 | class_name="Button", 310 | ).click() 311 | 312 | def _set_trade_params(self, security, price, amount): 313 | code = security[-6:] 314 | 315 | self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) 316 | 317 | # wait security input finish 318 | self.wait(0.1) 319 | 320 | self._type_keys( 321 | self._config.TRADE_PRICE_CONTROL_ID, 322 | easyutils.round_price_by_code(price, code), 323 | ) 324 | self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) 325 | 326 | def _type_keys(self, control_id, text): 327 | self._main.child_window( 328 | control_id=control_id, class_name="Edit" 329 | ).set_edit_text(text) 330 | 331 | def trade(self, security, price, amount): 332 | self._set_trade_params(security, price, amount) 333 | self.wait(0.1) 334 | self._submit_trade() 335 | return self._handle_pop_dialogs( 336 | handler_class=TradePopDialogHandler 337 | ) 338 | 339 | def buy(self, security, price, amount, **kwargs): 340 | self._switch_left_menus(["买入[F1]"]) 341 | return self.trade(security, price, amount) 342 | 343 | def sell(self, security, price, amount, **kwargs): 344 | self._switch_left_menus(["卖出[F2]"]) 345 | return self.trade(security, price, amount) 346 | 347 | def market_buy(self, security, amount, ttype=None, **kwargs): 348 | """ 349 | 市价买入 350 | :param security: 六位证券代码 351 | :param amount: 交易数量 352 | :param ttype: 市价委托类型,默认客户端默认选择, 353 | 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 354 | 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] 355 | 356 | :return: {'entrust_no': '委托单号'} 357 | """ 358 | self._switch_left_menus(["市价委托", "买入"]) 359 | return self.market_trade(security, amount, ttype) 360 | 361 | def market_sell(self, security, amount, ttype=None, **kwargs): 362 | """ 363 | 市价卖出 364 | :param security: 六位证券代码 365 | :param amount: 交易数量 366 | :param ttype: 市价委托类型,默认客户端默认选择, 367 | 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 368 | 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] 369 | 370 | :return: {'entrust_no': '委托单号'} 371 | """ 372 | self._switch_left_menus(["市价委托", "卖出"]) 373 | 374 | return self.market_trade(security, amount, ttype) 375 | 376 | def market_trade(self, security, amount, ttype=None, **kwargs): 377 | """ 378 | 市价交易 379 | :param security: 六位证券代码 380 | :param amount: 交易数量 381 | :param ttype: 市价委托类型,默认客户端默认选择, 382 | 深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销'] 383 | 沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价'] 384 | 385 | :return: {'entrust_no': '委托单号'} 386 | """ 387 | 388 | self._set_market_trade_params(security, amount) 389 | self.wait(0.01) 390 | 391 | if ttype is not None: 392 | self._set_market_trade_type(ttype) 393 | self._submit_trade() 394 | # self.wait(0.3) 395 | return self._handle_pop_dialogs( 396 | handler_class=TradePopDialogHandler 397 | ) 398 | 399 | def _set_market_trade_type(self, ttype): 400 | """根据选择的市价交易类 型选择对应的下拉选项""" 401 | selects = self._main.child_window( 402 | control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, 403 | class_name="ComboBox", 404 | ) 405 | for i, text in selects.texts(): 406 | # skip 0 index, because 0 index is current select index 407 | if i == 0: 408 | continue 409 | if ttype in text: 410 | selects.select(i - 1) 411 | break 412 | else: 413 | raise TypeError("不支持对应的市价类型: {}".format(ttype)) 414 | 415 | def _set_market_trade_params(self, security, amount): 416 | code = security[-6:] 417 | 418 | self._type_keys(self._config.TRADE_SECURITY_CONTROL_ID, code) 419 | 420 | # wait security input finish 421 | self.wait(0.1) 422 | 423 | self._type_keys(self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))) 424 | 425 | def auto_ipo(self): 426 | # self.wait(3) 427 | self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) 428 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 429 | isNotYzm = self._parse_grid_yzm() 430 | 431 | if isNotYzm == True: 432 | self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH) 433 | self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID) 434 | self._parse_grid_yzm() 435 | else: 436 | # 解析剪贴板上的数据 437 | content = self.grid_strategy(self)._get_clipboard_data() 438 | stock_list = self.grid_strategy(self)._format_grid_data(content) 439 | 440 | if len(stock_list) == 0: 441 | return {"message": "今日无新股"} 442 | invalid_list_idx = [ 443 | i for i, v in enumerate(stock_list) if v["申购数量"] <= 0 444 | ] 445 | 446 | if len(stock_list) == len(invalid_list_idx): 447 | return {"message": "没有发现可以申购的新股"} 448 | 449 | self.wait(0.1) 450 | 451 | for row in invalid_list_idx: 452 | self._click_grid_by_row(row) 453 | self.wait(0.1) 454 | 455 | self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID) 456 | self.wait(0.1) 457 | 458 | return self._handle_pop_dialogs() 459 | 460 | def _click(self, control_id): 461 | self._app.top_window().child_window( 462 | control_id=control_id, class_name="Button" 463 | ).click() 464 | 465 | def _click_grid_by_row(self, row): 466 | x = self._config.COMMON_GRID_LEFT_MARGIN 467 | y = ( 468 | self._config.COMMON_GRID_FIRST_ROW_HEIGHT 469 | + self._config.COMMON_GRID_ROW_HEIGHT * row 470 | ) 471 | self._app.top_window().child_window( 472 | control_id=self._config.COMMON_GRID_CONTROL_ID, 473 | class_name="CVirtualGridCtrl", 474 | ).click(coords=(x, y)) 475 | --------------------------------------------------------------------------------