├── 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 |
--------------------------------------------------------------------------------