Repository: Raytone-D/puppet
Branch: master
Commit: 6212d4306d52
Files: 31
Total size: 105.1 KB
Directory structure:
gitextract_z9thwemw/
├── .gitignore
├── LICENSE
├── README.md
├── VERSION.md
├── abandon/
│ ├── archives.md
│ ├── auto_trade_example.py
│ ├── autologon.py
│ ├── autologon_raffle.py
│ ├── puppet_quickstart.txt
│ ├── puppet_tdx.py
│ ├── puppet_v0.3.5.py
│ ├── release_puppet_unity_ths.py
│ └── test.py
├── changelog.md
├── engine/
│ ├── README.md
│ ├── __init__.py
│ ├── broker.py
│ ├── data_source.py
│ ├── event_source.py
│ ├── matcher.py
│ ├── mod.py
│ └── utils.py
├── puppet/
│ ├── __init__.py
│ ├── client.py
│ ├── management.py
│ ├── option.py
│ ├── runner.py
│ └── util.py
├── setup.py
├── strategies/
│ └── buy_and_hold.py
└── test.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2016 睿瞳深邃
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 本项目为学习WIN32 API而写,擅自将puppet用于生产环境,后果自负!
# 推荐一站式解决方案 【策略易】 http://www.iguuu.com/e?x=19829
**快速入门:**
```python
import puppet
# 自动登录账户, comm_pwd 是可选参数
accinfo = {
'account_no': '你的账号',
'password': '登录密码',
'client_path': 'path/to/xiadan.exe',
# 'comm_pwd': '通讯密码'
}
acc = puppet.login(accinfo)
# 绑定已登录账户
acc = puppet.Account(title='')
acc.buy('000001', 12.68, 100)
acc.sell('000001', 12.68, 100)
# 只支持按代码撤单。
acc.cancel_buy('510050')
acc.cancel_all()
acc.query('historical_deal')
acc.query('position')
```
**使用环境:**
1、Python3.5及以上,强烈推荐使用Anaconda3的最新版本。
2、(不推荐!)Linux平台需安装最新的Wine,环境设为WIN7,并安装Windows平台的Anaconda3。
**安装:**
打开命令提示符或Windows PowerShell,然后执行:
```shell
pip install https://github.com/Raytone-D/puppet/archive/master.zip
```
或者
```shell
git clone https://github.com/Raytone-D/puppet.git
pip install -e puppet
```
**技术说明:**
1、本项目使用User32.dll, Kernel32.dll所涵盖的win32 API。
2、按MSDN的API说明,win32 API支持WIN2000及以上版本,建议Win 7+。
**鸣谢:**
* https://github.com/hardywu/ 写了个rqalpha的接入模板PR,多谢支持!
///////////////////////做事有底线///////////////////////////////////////
================================================
FILE: VERSION.md
================================================
Development Log
==
### 更新
2018/7/21 修改文档。移除废弃的文件。
2017/7/14 更新至v0.4.18,修改kill_popup(),按钮标题改为'是(&Y)'。
2017/7/13 更新至v0.4.17,增加kill_popup()函数,修改buy/sell函数。
2017/7/6 更新至v0.4.16,重新new、raffle(),cancel(),以及精简代码。不再支持连号打新。
2017/7/5 更新至v0.4.15,修复属性返回值错误。
2017/7/4 更新至v0.4.14,修改属性的返回值为字典。增加属性entrustment,可以查委托合同号。修改copy_data等方法。
2017/7/3 更新至v0.4.13,为buy/sell增加睡眠参数sec=0.3,更好地开车不翻车。复活了cancel_buy,cancel_sell, cancel_all。
2017/7/2 更新至v0.4.12,增加单独的buy/sell,原来的双向委托下单改为buy2/sell2。修改取持仓数据的逻辑,更稳不翻车。
2017/6/28 更新至v0.4.11,修改buy/sell函数,避免漏单。(追求一秒十单的,别用这个版本!)
2017/6/27 更新至v0.4.10,增加查询市值的属性:market_value
2017/5/9 更新至v0.4.9,简化cancel()方法的代码,撤买可以这样用了cancel(600006),撤卖cancel(600006, '撤卖')
2017/4/19 大改自动登录的逻辑,autologon更新至v0.4
2017/4/18 修复自动登录的逻辑错误,现在能从单帐号自动切换到多帐号了。
2017/4/15 更新至v0.4.8,增加支持同花顺官方交易客户端“多账户”登录模式下多个券商帐号的切换。增加autologon.py, multi_raffle.py, autologon_raffle.py, “图解同花顺多账户一键打新.PDF”。
2017/4/9 更新"扯线木偶API使用说明",主要是说明参数的用法。
2017/4/6 更新至v0.4.7,改善raffle()的兼容性,不支持银河证券的同花顺客户端打新,只能用同花顺官方的交易端打新。
2017/4/4 通达信版改一个控件代码,支持招商证券独立交易模式登录。
2017/4/2 更新至v0.4.6,增加bingo中签查询。
2017/4/1 更新至v0.4.5,修复了一个愚蠢的错误:symbol[0].startswith('')返回True,导致不打新股,一脸懵逼!
2017/3/28 更新至v0.4.4,支持buy()/sell()直接输数字下单,无需字符串。
2017/3/10 更新至v0.4.3,优化输出效果,更友好。
2017/3/10 更新至v0.4.2,raffle增加skip参数,跳过指定的市场新股。
2017/3/10 更新到v0.4.1,小幅修改,部分优化,默认改为单交易客户端模式。
2017/3/9 v0.4版发布!增加一键打新(raffle)、查新股(new)功能。大幅度修改优化,强化拟人化操作逻辑。
2017/2/23 V0.3.5发布!小幅修改,改善操作流畅度。
2017/2/22 v0.3发布!优化模拟人手交易的流程。
2017/2/21 v0.2.5发布!增加撤单(指定股票代码)功能。
2017/2/14 v0.2版发布!提供后台获取持仓数据。鸣谢网友liuyukuan博文中提供的AHK代码“SendMessage,0x111,57634,0,CVirtualGridCtrl2,同花顺”。
================================================
FILE: abandon/archives.md
================================================
存放旧文档的文件夹。
================================================
FILE: abandon/auto_trade_example.py
================================================
from engine import run
basConfig = {
"strategy_file": "./buy_and_hold.py",
"start_date": "2016-06-01",
"end_date": "2016-12-01",
"stock_starting_cash": 100000,
"benchmark": "000300.XSHG",
}
run(baseConfig)
================================================
FILE: abandon/autologon.py
================================================
"""
# autologon.py
# 目前仅支持同花顺官方的独立交易端的“多帐号”登录模式。
"""
__author__ = '睿瞳深邃'
__version__ = '0.4'
# coding: utf-8
import os
import subprocess
import time
import ctypes
api = ctypes.windll.user32
def autologon(target=None):
" 自动登录同花顺独立交易客户端 "
# 通过快捷方式运行独立交易客户端
path = os.path.split(os.path.realpath(__file__))[0]
for lnk in os.listdir(path):
if target in lnk:
subprocess.Popen(os.path.join(path, lnk), shell=True)
# 搜索交易客户端登录窗口
for i in range(30):
time.sleep(1)
main = api.FindWindowW(0, '网上股票交易系统5.0')
if main and not api.IsWindowVisible(main):
popup = api.GetLastActivePopup(main)
if api.IsWindowVisible(popup):
print("找到交易端登录窗口!")
break
else:
print("重试:{0}".format(i))
# 一键登录
logon = api.GetDlgItem(popup, 1015) # 一键登录按钮
for i in range(30):
if not api.IsWindowVisible(logon):
api.PostMessageW(popup, 273, 1014, api.GetDlgItem(popup, 1014))
time.sleep(0.2)
api.PostMessageW(popup, 273, 1015, logon)
if __name__ == '__main__':
autologon('同花顺交易.lnk')
================================================
FILE: abandon/autologon_raffle.py
================================================
"""
# 多账户打新专用脚本,支持v4+版本
# myRegister暂时没用上。暂时只支持同花顺交易端
"""
# coding: utf-8
import time
import os
import subprocess
import ctypes
from puppet_v4 import Puppet, switch_combo
from autologon import autologon
api = ctypes.windll.user32
buff = ctypes.create_unicode_buffer(32)
team = set()
def find(keyword):
""" 枚举所有已登录的交易端 """
@ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p)
def check(hwnd, keyword):
""" 筛选 """
if api.IsWindowVisible(hwnd)\
and api.GetWindowTextW(hwnd, buff, 32) > 6 and keyword in buff.value:
team.add(hwnd)
return 1
for i in range(10):
api.EnumWindows(check, keyword)
time.sleep(3)
if team:
break
return {Puppet(main) for main in team}
if __name__ == '__main__':
myRegister = {'券商登录号': '自定义名称',
'617145470': '东方不败',
'20941552121212': '西门吹雪'} # 交易端的登录帐号及昵称。
keyword = '网上股票交易'
autologon(target='同花顺交易')
traders = find(keyword)
for x in traders:
popup = api.GetLastActivePopup(x.main)
if popup:
api.PostMessageW(popup, 273, 2, api.GetDlgItem(popup, 2))
if api.IsWindowVisible(x.combo):
for i in range(x.count):
switch_combo(i, 2322, x.combo)
time.sleep(1)
x.raffle()
================================================
FILE: abandon/puppet_quickstart.txt
================================================
界面操控API
*********************************************************************************************************************************
* method:'买入': buy(), '卖出': sell(), '撤单': cancel(), '打新': raffle(), '下单': order(),'撤买': cancel_buy(), '撤卖': cancel_sell()
'全撤': cancel_all()
* property: '可用余额': balance, '持仓': position, '成交': deals, '可撤委托': cancelable, '新股': new, '中签': bingo, '帐号': account
'市值': market_value
*********************************************************************************************************************************
"""
# 扯线木偶操控API使用说明
# api会保持稳定,只会“增减参数”来优化。
# 实例方法必须有参数,否则跳出异常或输出属性的返回值。
# 属性没参数,直接trader.xxx
"""
def buy(self, symbol, price=0, qty=0):
pass
def sell(self, symbol, price=0, qty=0):
"""
# 功能:实现了和交易端相同的买/卖委托。
# self: 类的实例方法。
# symbol: 代码,字符串类型,非0开头的可以直接用整数。
# price: 价格, 字符串或整数。
# qty: 数量,字符串类型或整数。
# 限价委托:trader.buy('002236', 16.55, 100) # 最常用的下单方式。
# 扫五档委托:trader.buy('002236', 100) # 注:v4未实现
# 全仓扫五档:trader.buy('002236') # 注:v4未实现。
"""
pass
def cancel(self, symbol=None, way='撤买'):
"""
# 功能:只实现了交易端的“查代码”撤单功能。可全撤或者单独撤买或撤卖
# symbol: 股票代码
# way: 值可以是"撤买"、"撤卖"、"全撤"、"撤单",其中一个。
# trader.cancel('002236'), trader.cancel('600006', '撤卖')
"""
pass
def raffle(self, skip=None, way=True):
"""
# 功能:实现了交易端的“新股申购”功能。
# skip: 字符串,值可以是'7'(沪市)、'0'(深市主板)、'3'(创业板)其中之一。表示直接跳过申购。
# way: 布尔值,True表示申购,否则查新股名单。
# trader.raffle(skip='3')
"""
pass
构建流程:界面操控API ->> 预警交互API ->> 信号推送API ->> 策略中枢API ->> 历史数据API ->> 回测模块API
使用流程:手动登录客户端 --> 运行扯线木偶 --> 自动搜索已登录的客户端 --> 交易前的预备 --> 自动获取持仓数据 --> 查询预警名单 --> 客户端待命状态
# 普通使用只需要运行puppet_v4.py文件,即可。
目前已知查询中签bingo只适用于部分券商!请留意。
招商证券只测试过最新版能独立交易模式登录使用!辣鸡定制版不会增加任何支持了。国金、中信通达信据反馈资金明细不兼容。
暂不支持融资融券!
同花顺交易端:无任何限制!官方统一版或老版、券商定制版(银河、国泰君安、华泰、广发等)。
通达信交易端:无任何限制!目前不支持独立交易端。
多账户同时交易:完全支持!同一券商或多个券商。
注意:暂不支持一个交易端通过“添加”同一券商多个账户同时交易,只能交易当前的那一个账户。
# 使用最新版本的扯线木偶,以避免一些运行逻辑的BUG。
# 使用最新版本的Anaconda3,或者Python 3.5+。界面操作API能支持Python3.0+,
# 因为后面的其他模块会使用asyncio(3.4)或者await/async(3.5+),因此不建议使用。
# 按MSDN的API说明,WIN2000及以上版本都能正常使用,但注意了, windows xpsp3只能Python3.4以下,Python3.5+必须WIN7+。
# 默认只实例化一个客户端(同花顺或者通达信),有多个客户端需求的,请参考multi_clients_test.py。
# 00开头的股票需要用字符串。
使用流程:手动登录客户端 --> 运行扯线木偶 --> 自动搜索已登录的客户端 --> 交易前的预备 --> 自动获取持仓数据 --> 查询预警名单 --> 客户端待命状态
可用性测试:直接运行主文件puppet_v4.py,或者引用该文件。免验证码自动登录模式仅适用于打新,无法交易。
* https://github.com/hardywu/ 写了个rqalpha的接入模板PR,感谢支持鼓励!
推荐使用最新版的Anaconda3,或者Python 3.5+。系统要求:Windows平台,Win2000+;Linux平台,安装最新的WineHQ,环境设为WIN7。
Windows下不需要安装、配置。
Linux下需要安装最新版本的Wine,环境设为Windows 7,先安装同花顺交易客户端,能正常使用之后再安装Python for Windows。启动wineconsole,pip install pyperclip,之后就可以正常使用了。
""" 使用说明 """
# test.py是测试单账户的可用性。
# multi_test.py是测试多个账户的可用性。
# multi_raffle.py是多账户打新专用脚本,双击即可完成打新。
""" 同花顺独立交易客户端 """
# 由于同花顺理念的先进性,任何版本的同花顺都能正常被Puppet所调用。
# 同花顺是全后台调用的,只要登录后最小化即可,无需使用专用的主机或者桌面。
""" 通达信金融终端 """
# 暂不支持通达信独立交易端。
# 暂不支持招商【行情+交易】模式登录的客户端。
# 由于通达信交易客户端使用前置交易模式,请使用专用的虚拟桌面(比如Win10的虚拟桌面),以免干扰操作。
# 通达信金融终端(包括各种券商定制版)的交易窗口不能被遮挡,不能最小化,会导致程序抛出异常。
# 可能有些券商的独有的登录模式无法正常匹配,请发电邮或者在github通知我。
# 节点的二次索引将各个券商杂乱的节点布局影射为委托0,撤单1,资金股份2,当日成交3,新股申购4,中签查询5,批量申购6。
# 如果交易端左侧的功能节点标签没配置在config.py的TAG/SUBTAG里面,请自行增加到对应的索引位置。
# 广发证券查询中签,如果返回[],需要改动源码wait_a_second(0.5)。
# 撤单可选股票代码symbol或者委托编号number。买卖方向默认“卖出”。
================================================
FILE: abandon/puppet_tdx.py
================================================
"""
# the wrapper of A-shares local tdx client
"""
__project__ = 'Puppet'
__author__ = "睿瞳深邃(https://github.com/Raytone-D"
__version__ = "Fools' Day"
import ctypes
from functools import reduce
import time
import pywinauto
WM_SETTEXT = 12
WM_GETTEXT = 13
WM_SETCHECK = 241
WM_CLICK = 245
WM_KEYDOWN = 256
WM_KEYUP = 257
WM_COMMAND = 273
class Tdx:
""" 通达信常量定义 """
# 节点索引:0: '委托', 1: '撤单', 2: '资金股份', 3: '当日成交', 4: '新股申购', 5: '中签查询', 6: '批量申购',
INIT = 'updown'
GRID = 'SysListView32'
CLS = 'TdxW_MainFrame_Class'
TAG = ['对买对卖', '双向委托', '各种交易',
'撤单', '撤单[F3]',
'查询', '资金股份',
'当日成交',
'新股申购',
'中签查询'] # 节点标签
SUBTAG = ['资金股份', '资金股份F4',
'当日成交', '当日成交查询',
'新股申购',
'中签查询', '新股中签缴款',
'新股批量申购'] # 子节点标签。
CUSTOM = {'招商定制': 'msctls_updown32',
'定制1': '',
'off': 1157} # 交易面板定位。
BUY = {'代码': 12005,
'价格': 12006,
'全部': 1495,
'数量': 12007,
'下单': 2010}
SELL = {'代码': 2025,
'价格': 12039,
'全部': 3075,
'数量': 3030,
'下单': 3032}
CANCEL = {'撤单': 1136,
'全选': 14}
NEW = {'全部': 2203,
'申购': 11786,
'弹窗': '新股申购确认',
'确认': 7015} # '新股代码': 12023,'申购价格': 12024,'最大可申': 2202,'申购数量': 12025,
BATCH = {'申购': 39004,
'弹窗': '新股组合申购确认',
'确认': 7015} # 批量申购
BINGO = {'查询': 1140}
PATH = (59648, 0, 0, 59648, 59649, 0)
api = ctypes.windll.user32
buff = ctypes.create_unicode_buffer(96)
def confirm_popup(idButton=7015, title='提示'):
""" 确认弹窗 """
time.sleep(0.5)
popup = api.FindWindowW(0, title) or api.FindWindowW(0, '提示')
if api.IsWindowVisible(popup):
api.PostMessageW(popup, WM_COMMAND, idButton,
api.GetDlgItem(popup, idButton))
return True
else:
print('没找到弹窗orz')
return False
class Puppet:
"""
# method: '委买': buy(), '委卖': sell(), '撤单': cancel(), '打新': raffle(), '下单': order(),
# property: '帐号': account, '持仓': position, '可用余额': balance, '成交': deals,
# '可撤委托': cancelable, '新股': new, '中签': bingo
"""
def __init__(self, main=None, clsName='TdxW_MainFrame_Class'):
self.main = pywinauto.findwindows.find_window(class_name=clsName)
app = pywinauto.Application().connect(handle=self.main)
self._client = app.window(handle=self.main)
self.tv = self._client['SysTreeView32']
tag = [x.text() for x in self.tv.roots()] # 节点标签
self.tag = [x for x in Tdx.TAG if x in tag] # 筛选
self.tv.item(r'\对买对卖').click_input()
self._trade = api.GetParent(self._client[Tdx.INIT])
self.account = "暂不可用:("
def _get_data(self, on=1):
''' 通达信SysListView32 '''
lv = self._client['SysListView32']
time.sleep(0.5)
if on:
raw = [x.text() for x in lv.items()]
return list(zip(*[iter(raw)] * lv.column_count()))
api.GetDlgItemTextW(api.GetParent(lv.handle), 1576, buff, 96)
return dict([x.split(':') for x in buff.value.strip().split(' ')])
def order(self, symbol, price, qty=0, way='买入'):
""" 通达信下单 """
self.tv.item(r'\对买对卖').click_input()
_parts = Tdx.BUY if way == '买入' else Tdx.SELL
if qty == 0: # 暂时不可用。
print("全仓{0}".format(way))
print('{0:>>8} {1}, {2}, {3}'.format(way, symbol, price, qty))
print('限价委托') if price else print('市价委托')
time.sleep(0.5) # 必须滴。
api.SendMessageW(api.GetDlgItem(self._trade, _parts['代码']), WM_SETTEXT, 0, str(symbol))
api.SendMessageW(api.GetDlgItem(self._trade, _parts['价格']), WM_SETTEXT, 0, str(price))
api.SendMessageW(api.GetDlgItem(self._trade, _parts['数量']), WM_SETTEXT, 0, str(qty))
time.sleep(0.5)
api.PostMessageW(self._trade, WM_COMMAND, _parts['下单'],
api.GetDlgItem(self._trade, _parts['下单']))
while True:
if not confirm_popup(title='交易确认'):
print('下单完成:)')
break
def buy(self, symbol, price, qty=0):
return self.order(symbol, price, qty)
def sell(self, symbol, price=0, qty=0):
return self.order(symbol, price, qty, way='卖出')
def cancel(self, symbol=None, number=None, way='卖出', comfirm=True):
''' 通达信撤单 '''
self.tv.item(r'\撤单').click_input()
print("撤单:{0}".format('>'*8))
time.sleep(0.5)
lv = self._client[Tdx.GRID]
cancel = api.GetParent(lv.handle)
if comfirm:
temp = [{x[3]: [x[1], x[8]]} for x in self._get_data()]
temp = [{x[way][0]: x[way][1]} for x in temp if x.get(way)]
wanted = [temp.index(x) for x in temp if str(symbol) in x or str(number) in x.values()]
if wanted:
for x in wanted:
print('撤掉:{0} {1}'.format(way, temp[x]))
lv.item(x).click()
api.PostMessageW(cancel, WM_COMMAND, Tdx.CANCEL['撤单'],
api.GetDlgItem(cancel, Tdx.CANCEL['撤单']))
while True:
time.sleep(0.5)
if not confirm_popup():
print('撤单完成:)')
break
return self._get_data()
@property
def cancelable(self):
print('可撤委托: {0}'.format('$'*68))
return self.cancel(way=False)
@property
def position(self):
print('实时持仓: {0}'.format('$'*68))
self.tv.item(r'\查询\资金股份').click_input()
#time.sleep(0.5) # 不一定需要用,备用。
return self._get_data()
@property
def balance(self):
print('资金明细: {0}'.format('$'*68))
self.tv.item(r'\查询\资金股份').click_input()
return self._get_data(on=False)
@property
def deals(self):
print('当日成交: {0}'.format('$'*68))
self.tv.item(r'\查询\当日成交').click_input()
#time.sleep(0.5) # 不一定需要用,备用。
return self._get_data()
@property
def new(self):
print('新股名单: {0}'.format('$'*68))
self.tv.item(r'\新股申购\新股申购').click_input()
return self._get_data()
@property
def bingo(self):
print('新股中签: {0}'.format('$'*68))
self.tv.item(r'\新股申购\中签查询').click_input()
time.sleep(0.5)
bingo = api.GetParent(self._client['SysListView32'].handle)
api.PostMessageW(bingo, WM_COMMAND, Tdx.BINGO['查询'],
api.GetDlgItem(bingo, Tdx.BINGO['查询']))
return self._get_data()
def _batch(self):
""" 新股批量申购,自动切换无需指定,只有华泰、银河、招商、广发等券商可用。 """
self.tv.item(r'\新股申购\新股批量申购').click_input()
print("新股批量申购:{0}".format('>'*8))
print(self._get_data())
time.sleep(0.5)
batch = api.GetParent(self._client[Tdx.GRID].handle)
api.PostMessageW(batch, WM_COMMAND, Tdx.BATCH['申购'],
api.GetDlgItem(batch, Tdx.BATCH['申购']))
while True:
time.sleep(0.5)
if not confirm_popup(title=Tdx.BATCH['弹窗']):
print('申购完毕!请查询可撤委托单:)')
break
def raffle(self, skip='', way=True):
'通达信打新'
tag = [x.text() for x in self.tv.item(r'\新股申购').children()] # 子节点标签
if '新股批量申购' in tag:
self._batch()
else:
self.tv.item(r'\新股申购\新股申购').click_input()
print("新股申购: {0}".format('>'*8))
time.sleep(0.5)
lv = self._client['SysListView32']
raffle = api.GetParent(lv.handle)
count = lv.item_count()
_parts = [api.GetDlgItem(raffle, x) for x in Tdx.NEW.values()]
for x in range(count):
lv.item(x).click_input(double=True)
api.PostMessageW(raffle, WM_COMMAND, Tdx.NEW['全部'], _parts[0])
api.PostMessageW(raffle, WM_COMMAND, Tdx.NEW['申购'], _parts[1])
while True:
time.sleep(0.5)
if not confirm_popup(title=Tdx.NEW['弹窗']):
print('申购完毕!请查询可撤委托单:)')
break
if __name__ == '__main__':
trader = Puppet()
print(trader.position)
print(trader.balance)
print(trader.cancelable)
print(trader.deals)
print(trader.new)
print(trader.bingo)
#trader.raffle()
#trader.sell('002097', 11, 100) # 00股票代码需要用字符串。
trader.cancel(number=58) # 默认撤卖单。symbol股票代码,number委托编号。
================================================
FILE: abandon/puppet_v0.3.5.py
================================================
"""puppetrader for ths client uniform"""
__author__ = '睿瞳深邃(https://github.com/Raytone-D)'
__project__ = '扯线木偶(puppetrader for ths client unity)'
__version__ = "0.3.5"
'推荐使用:anaconda3 最新版,或者Python >= 3.6'
# coding: utf-8
import ctypes
from functools import reduce
import time
import pyperclip
import json
WM_COMMAND, WM_SETTEXT, WM_GETTEXT, WM_KEYDOWN, WM_KEYUP = \
273, 12, 13, 256, 257 # 命令
F1, F2, F3, F4, F5, F6 = \
112, 113, 114, 115, 116, 117 # keyCode(按键代码)
op = ctypes.windll.user32
wait_a_second = lambda sec= 0.1: time.sleep(sec)
def keystroke(hCtrl, keyCode, param=0): # 单击
op.PostMessageW(hCtrl, WM_KEYDOWN, keyCode, param)
op.PostMessageW(hCtrl, WM_KEYUP, keyCode, param)
class unity():
''' 多账户交易集中处理 '''
def __init__(self, main):
self.main = main
keystroke(main, F6) # 切换到双向委托
wait_a_second() # 可调整区间值(0.01~0.5)
self.buff = ctypes.create_unicode_buffer(32)
# 代码,价格,数量,买入,代码,价格,数量,卖出,全撤, 撤买, 撤卖
id_members = 1032, 1033, 1034, 1006, 1035, 1058, 1039, 1008, 30001, 30002, 30003, \
32790, 1038, 1047, 2053, 30022, 1019 # 刷新,余额、表格、最后一笔、撤相同
self.path_custom = 1047, 200, 1047
self.two_way = reduce(op.GetDlgItem, (59648, 59649), main)
self.members = {i: op.GetDlgItem(self.two_way, i) for i in id_members}
self.custom = reduce(op.GetDlgItem, self.path_custom, self.two_way)
# 获取登录账号
self.account = reduce(op.GetDlgItem, (59392, 0, 1711), main)
op.SendMessageW(self.account, WM_GETTEXT, 32, self.buff)
self.account = self.buff.value
# 撤单工具条
self.id_toolbar = {'全选': 1098, \
'撤单': 1099, \
'全撤': 30001, \
'撤买': 30002, \
'撤卖': 30003, \
'填单': 3348, \
'查单': 3349} #'撤尾单': 2053, '撤相同': 30022} # 华泰独有
op.SendMessageW(main, WM_COMMAND, 163, 0) # 切换到撤单操作台
wait_a_second()
self.cancel_panel = reduce(op.GetDlgItem, (59648, 59649), main)
self.cancel_toolbar = {k: op.GetDlgItem(self.cancel_panel, v) for k, v in self.id_toolbar.items()}
keystroke(main, F6) # 切换到双向委托
def buy(self, symbol, price, qty): # 买入(B)
# buy = order('b')
op.SendMessageW(self.members[1032], WM_SETTEXT, 0, symbol)
op.SendMessageW(self.members[1033], WM_SETTEXT, 0, price)
op.SendMessageW(self.members[1034], WM_SETTEXT, 0, qty)
op.PostMessageW(self.two_way, WM_COMMAND, 1006, self.members[1006])
def sell(self, symbol, price, qty): # 卖出(S)
# buy = order('s')
op.SendMessageW(self.members[1035], WM_SETTEXT, 0, symbol)
op.SendMessageW(self.members[1058], WM_SETTEXT, 0, price)
op.SendMessageW(self.members[1039], WM_SETTEXT, 0, qty)
op.PostMessageW(self.two_way, WM_COMMAND, 1008, self.members[1008])
def refresh(self): # 刷新(F5)
op.PostMessageW(self.two_way, WM_COMMAND, 32790, self.members[32790])
def cancel_order(self, symbol=''): # 撤单
op.SendMessageW(main, WM_COMMAND, 163, 0) # 切换到撤单操作台
if symbol:
op.SendMessageW(self.cancel_toolbar['填单'], WM_SETTEXT, 0, symbol)
sleep(0.1) # 必须有
op.PostMessageW(self.cancel_panel, WM_COMMAND, self.id_toolbar['查单'], self.cancel_toolbar['查单'])
op.PostMessageW(self.cancel_panel, WM_COMMAND, self.id_toolbar['撤单'], self.cancel_toolbar['撤单'])
keystroke(self.main, F6) # 必须返回双向委托操作台!
def cancelAll(self): # 全撤(Z)
op.PostMessageW(self.two_way, WM_COMMAND, 30001, self.members[30001])
def cancelBuy(self): # 撤买(X)
op.PostMessageW(self.two_way, WM_COMMAND, 30002, self.members[30002])
def cancelSell(self): # 撤卖(C)
op.PostMessageW(self.two_way, WM_COMMAND, 30003, self.members[30003])
def cancelLast(self): # 撤最后一笔,仅限华泰定制版有效
op.PostMessageW(self.two_way, WM_COMMAND, 2053, self.members[2053])
def cancelSame(self): # 撤相同代码,仅限华泰定制版
#op.PostMessageW(self.two_way, WM_COMMAND, 30022, self.members[30022])
pass
def balance(self): # 可用余额
op.SendMessageW(self.members[1038], WM_GETTEXT, 32, self.buff)
return self.buff.value
def get_data(self, key='W'):
"将CVirtualGridCtrl|Custom的数据复制到剪贴板,默认取持仓记录"
keystroke(self.two_way, ord(key)) # 切换到持仓('W')、成交('E')、委托('R')
wait_a_second() # 等待券商的数据返回...
op.SendMessageW(self.custom, WM_COMMAND, 57634, self.path_custom[-1]) # background mode
return pyperclip.paste()
def finder():
""" 枚举所有已登录的交易端并将其实例化 """
team = set()
buff = ctypes.create_unicode_buffer(32)
@ctypes.WINFUNCTYPE(ctypes.wintypes.BOOL, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)
def check(hwnd, extra):
if op.IsWindowVisible(hwnd):
op.GetWindowTextW(hwnd, buff, 32)
if '交易系统' in buff.value:
team.add(hwnd)
return 1
op.EnumWindows(check, 0)
return {unity(main) for main in team if team}
def to_dict(raw):
x = [row.split() for row in raw.splitlines()]
data = {}
for y in x[1:]:
data.update({y[0]: dict(zip(x[0], y))})
return data
if __name__ == '__main__':
myRegister = {'券商登录号': '自定义名称', \
'617145470': '东方不败', \
'20941552121212': '西门吹雪'}
ret = finder()
if ret:
# 如果没取到余额,尝试修改_init_函数里面sleep的值,或者查余额的id是不是变了。
trader = {myRegister[broker.account]: broker for broker in ret} # 给账户一个易记的外号
trader1 = {broker.account[-3:]: broker.balance() for broker in ret} # 以登录号3位尾数作代号
profile = {solo: {"交易帐号": trader[solo].account, \
"可用余额": trader[solo].balance()} \
for solo in trader}
print(profile)
#print(json.dumps(profile, indent=4, ensure_ascii=False, sort_keys=True))
raw = trader['东方不败'].get_data() # 只能“大写字母”,小写字母THS会崩溃,无语!
print(raw)
#print(to_dict(raw))
raw = trader['东方不败'].get_data('R')
print(raw)
#print(json.dumps(to_dict(raw), indent=4, ensure_ascii=False))
trader['东方不败'].cancel_order('')
else: print("老板,没发现已登录的交易端!")
================================================
FILE: abandon/release_puppet_unity_ths.py
================================================
__author__ = '睿瞳深邃(https://github.com/Raytone-D)'
__project__ = "扯线木偶(puppet for THS trader)"
#增加账户id_btn = 1691
# coding: utf-8
import ctypes
from ctypes.wintypes import BOOL, HWND, LPARAM
from time import sleep
import win32clipboard as cp
WM_COMMAND, WM_SETTEXT, WM_GETTEXT, WM_KEYDOWN, WM_KEYUP, VK_CONTROL = \
273, 12, 13, 256, 257, 17 # 消息命令
F1, F2, F3, F4, F5, F6 = \
112, 113, 114, 115, 116, 117 # keyCode
op = ctypes.windll.user32
buffer = ctypes.create_unicode_buffer
def keystroke(hCtrl, keyCode, param=0): # 击键
op.PostMessageW(hCtrl, WM_KEYDOWN, keyCode, param)
op.PostMessageW(hCtrl, WM_KEYUP, keyCode, param)
def get_data():
sleep(0.3) # 秒数关系到是否能复制成功。
op.keybd_event(17, 0, 0, 0)
op.keybd_event(67, 0, 0, 0)
sleep(0.1) # 没有这个就复制失败
op.keybd_event(67, 0, 2, 0)
op.keybd_event(17, 0, 2, 0)
cp.OpenClipboard(None)
raw = cp.GetClipboardData(13)
data = raw.split()
cp.CloseClipboard()
return data
class unity():
''' 大一统协同交易 '''
def __init__(self, hwnd):
keystroke(hwnd, F6) # 切换到双向委托
self.buff = buffer(32)
# 代码,价格,数量,买入,代码,价格,数量,卖出,全撤, 撤买, 撤卖
id_members = 1032, 1033, 1034, 1006, 1035, 1058, 1039, 1008, 30001, 30002, 30003, \
32790, 1038, 1047, 2053, 30022 # 刷新,余额、表格、最后一笔、撤相同
self.two_way = hwnd
sleep(0.1) # 按CPU的性能调整秒数(0.01~~0.5),才能获取正确的self.two_way。
for i in (59648, 59649):
self.two_way = op.GetDlgItem(self.two_way, i)
self.members = {i: op.GetDlgItem(self.two_way, i) for i in id_members}
def buy(self, symbol, price, qty): # 买入(B)
op.SendMessageW(self.members[1032], WM_SETTEXT, 0, symbol)
op.SendMessageW(self.members[1033], WM_SETTEXT, 0, price)
op.SendMessageW(self.members[1034], WM_SETTEXT, 0, qty)
op.PostMessageW(self.two_way, WM_COMMAND, 1006, self.members[1006])
def sell(self, *args): # 卖出(S)
op.SendMessageW(self.members[1035], WM_SETTEXT, 0, symbol)
op.SendMessageW(self.members[1058], WM_SETTEXT, 0, price)
op.SendMessageW(self.members[1039], WM_SETTEXT, 0, qty)
op.PostMessageW(self.two_way, WM_COMMAND, 1008, self.members[1008])
def refresh(self): # 刷新(F5)
op.PostMessageW(self.two_way, WM_COMMAND, 32790, self.members[32790])
def cancel(self, way=0): # 撤销下单
pass
def cancelAll(self): # 全撤(Z)
op.PostMessageW(self.two_way, WM_COMMAND, 30001, self.members[30001])
def cancelBuy(self): # 撤买(X)
op.PostMessageW(self.two_way, WM_COMMAND, 30002, self.members[30002])
def cancelSell(self): # 撤卖(C)
op.PostMessageW(self.two_way, WM_COMMAND, 30003, self.members[30003])
def cancelLast(self): # 撤最后一笔,仅限华泰定制版有效
op.PostMessageW(self.two_way, WM_COMMAND, 2053, self.members[2053])
def cancelSame(self): # 撤相同代码,仅限华泰定制版
#op.PostMessageW(self.two_way, WM_COMMAND, 30022, self.members[30022])
pass
def balance(self): # 可用余额
op.SendMessageW(self.members[1038], WM_GETTEXT, 32, self.buff)
return self.buff.value
def position(self): # 持仓(W)
keystroke(self.two_way, 87)
op.SetForegroundWindow(self.members[1047])
return get_data()
def tradeRecord(self): # 成交(E)
keystroke(self.two_way, 69)
op.SetForegroundWindow(self.members[1047])
return get_data()
def orderRecord(self): # 委托(R)
keystroke(self.two_way, 82)
op.SetForegroundWindow(self.members[1047])
return get_data()
def finder(register):
''' 枚举所有可用的broker交易端并实例化 '''
team = set()
buff = buffer(32)
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
def check(hwnd, extra):
if op.IsWindowVisible(hwnd):
op.GetWindowTextW(hwnd, buff, 32)
if '交易系统' in buff.value:
team.add(hwnd)
return 1
op.EnumWindows(check, 0)
def get_nickname(hwnd):
account = hwnd
for i in 59392, 0, 1711:
account = op.GetDlgItem(account, i)
op.SendMessageW(account, WM_GETTEXT, 32, buff)
return register.get(buff.value[-3:])
return {get_nickname(hwnd): unity(hwnd) for hwnd in team if hwnd}
if __name__ == '__main__':
myRegister = {'888': '股神','509': 'gf', '966': '女神', '167': '虚拟盘', '743': '西门吹雪'}
# 用来登录的号码(一般是券商客户号)最后3位数,不能有重复,nickname不能有重名!
trader = finder(myRegister)
if not trader:
print("没发现可用的交易端。")
else:
#print(trader.keys())
x = {nickname: broker.balance() for (nickname, broker) in trader.items()}
print("可用余额:%s" %x)
buy = '000078', '6.6', '300'
#trader['虚拟盘'].buy(*buy)
#p = trader['虚拟盘'].orderRecord()
#p = trader['虚拟盘'].tradeRecord()
p = trader['虚拟盘'].position()
print(p)
#trader['西门吹雪'].cancelLast()
================================================
FILE: abandon/test.py
================================================
"""
测试一个或者多个账号是否可用。
如果用来做多账号交易,需要先确认账号,再下单。
"""
# coding: utf-8
__author__ = '睿瞳深邃'
__version__ = '0.2'
import ctypes
from puppet_v4 import Puppet
api = ctypes.windll.user32
buff = ctypes.create_unicode_buffer(32)
def find(keyword='交易系统'):
""" 枚举所有已登录的交易端 """
clients = set()
@ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p)
def check(hwnd, keyword):
""" callback function, 用标题栏关键词筛选 """
if api.IsWindowVisible(hwnd) and api.GetWindowTextW(hwnd, buff, 32) > 6 and keyword in buff.value:
clients.add(hwnd)
return 1
api.EnumWindows(check, keyword)
return {Puppet(c) for c in clients} if clients else None
myRegister = {
'617145470': '东方不败',
'20941552121212': '西门吹雪'
} # 客户端的登录账号及自定义代号。
keyword = '广发证券' # 自定义关键词
traders = find()
if traders:
for x in traders:
print(x.account)
print(x.position)
#print(x.new)
#x.raffle()
else:
print('<木偶:没找到可用的客户端>')
================================================
FILE: changelog.md
================================================
Changelog
Puppet 1.0.1
Release date:
修复
query('new') # 海通证券查新股新债信息
Puppet 1.0.0
Release date: 2020-06-06
新增
runner.run()提供一个HTTP API
将相关的常量放入Ths类,并修改相关引用
query()新增默认参数summary
删改
query()的 category 参数可以为:'summary', 'position', 'order', 'deal', 'undone', 'historical_deal'
'delivery_order', 'new', 'bingo'其中之一
Client 改名为 Account
code 改名为 symbol
qty 改名为 quantity
remainder 改名为 leftover
num 改名为 id
order_num 改名为 order_id
raffle 改名为 purchase_new
to_dict 默认值改为 True
cancel_order 合并到 cancel
bind 、login的返回值由 self 改为 dict
answer、 purchase_new 的返回值由 tuple 类型改为 dict
将全局常量MSG修改为puppet_util.Msg类,并修改相关引用
修改初始化的设置项
删除 ATTRS常量
缩减 INIT常量
删改self.account相关代码
删除 position, deals, entrustment, cancelable, historical_deals 属性
删除 assets, balance, free_bal, market_value, client 属性
删除 summary()
================================================
FILE: engine/README.md
================================================
# Simple A Stock Realtime Trade Mod
使用该Mod可以接收实时行情进行触发。用于 RQAlpha 实时模拟交易,实盘交易。
这个是一个初级的DEMO。
使用`--run-type`或者`-rt`为`p`(PaperTrading),就可以激活改 mod。
```
rqalpha run -fq 1m -rt p -f ~/tmp/test_a.py -sc 100000 -l verbose
```
================================================
FILE: engine/__init__.py
================================================
import rqalpha
config = {
"extra": {
"log_level": "verbose",
},
"mod": {
"live_trade": {
"lib": "./mod",
"enabled": True,
"priority": 100,
}
}
}
def run(baseConf):
config["base"] = baseConf
return rqalpha.run(config)
================================================
FILE: engine/broker.py
================================================
# -*- coding: utf-8 -*-
#
# Copyright 2017 Ricequant, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import jsonpickle
from rqalpha.interface import AbstractBroker, Persistable
from rqalpha.utils import get_account_type
from rqalpha.utils.i18n import gettext as _
from rqalpha.events import EVENT
from rqalpha.const import MATCHING_TYPE, ORDER_STATUS
from rqalpha.const import ACCOUNT_TYPE
from rqalpha.environment import Environment
from rqalpha.model.account import BenchmarkAccount, StockAccount, FutureAccount
from .matcher import Matcher
def init_accounts(env):
accounts = {}
config = env.config
start_date = config.base.start_date
total_cash = 0
for account_type in config.base.account_list:
if account_type == ACCOUNT_TYPE.STOCK:
stock_starting_cash = config.base.stock_starting_cash
accounts[ACCOUNT_TYPE.STOCK] = StockAccount(env, stock_starting_cash, start_date)
total_cash += stock_starting_cash
elif account_type == ACCOUNT_TYPE.FUTURE:
future_starting_cash = config.base.future_starting_cash
accounts[ACCOUNT_TYPE.FUTURE] = FutureAccount(env, future_starting_cash, start_date)
total_cash += future_starting_cash
else:
raise NotImplementedError
if config.base.benchmark is not None:
accounts[ACCOUNT_TYPE.BENCHMARK] = BenchmarkAccount(env, total_cash, start_date)
return accounts
class Broker(AbstractBroker, Persistable):
def __init__(self, env):
self._env = env
if env.config.base.matching_type == MATCHING_TYPE.CURRENT_BAR_CLOSE:
self._matcher = Matcher(lambda bar: bar.close, env.config.validator.bar_limit)
self._match_immediately = True
else:
self._matcher = Matcher(lambda bar: bar.open, env.config.validator.bar_limit)
self._match_immediately = False
self._accounts = None
self._open_orders = []
self._board = None
self._turnover = {}
self._delayed_orders = []
self._frontend_validator = {}
# 该事件会触发策略的before_trading函数
self._env.event_bus.add_listener(EVENT.BEFORE_TRADING, self.before_trading)
# 该事件会触发策略的handle_bar函数
self._env.event_bus.add_listener(EVENT.BAR, self.bar)
# 该事件会触发策略的handel_tick函数
self._env.event_bus.add_listener(EVENT.TICK, self.tick)
# 该事件会触发策略的after_trading函数
self._env.event_bus.add_listener(EVENT.AFTER_TRADING, self.after_trading)
def get_accounts(self):
if self._accounts is None:
self._accounts = init_accounts(self._env)
return self._accounts
def get_open_orders(self):
return self._open_orders
def get_state(self):
return jsonpickle.dumps([o.order_id for _, o in self._delayed_orders]).encode('utf-8')
def set_state(self, state):
delayed_orders = jsonpickle.loads(state.decode('utf-8'))
for account in self._accounts.values():
for o in account.daily_orders.values():
if not o._is_final():
if o.order_id in delayed_orders:
self._delayed_orders.append((account, o))
else:
self._open_orders.append((account, o))
def _get_account_for(self, order_book_id):
account_type = get_account_type(order_book_id)
return self._accounts[account_type]
def submit_order(self, order):
account = self._get_account_for(order.order_book_id)
self._env.event_bus.publish_event(EVENT.ORDER_PENDING_NEW, account, order)
account.append_order(order)
if order._is_final():
return
# account.on_order_creating(order)
if self._env.config.base.frequency == '1d' and not self._match_immediately:
self._delayed_orders.append((account, order))
return
self._open_orders.append((account, order))
order._active()
self._env.event_bus.publish_event(EVENT.ORDER_CREATION_PASS, account, order)
if self._match_immediately:
self._match()
def cancel_order(self, order):
account = self._get_account_for(order.order_book_id)
self._env.event_bus.publish_event(EVENT.ORDER_PENDING_CANCEL, account, order)
# account.on_order_cancelling(order)
order._mark_cancelled(_("{order_id} order has been cancelled by user.").format(order_id=order.order_id))
self._env.event_bus.publish_event(EVENT.ORDER_CANCELLATION_PASS, account, order)
# account.on_order_cancellation_pass(order)
try:
self._open_orders.remove((account, order))
except ValueError:
try:
self._delayed_orders.remove((account, order))
except ValueError:
pass
def before_trading(self):
for account, order in self._open_orders:
order._active()
self._env.event_bus.publish_event(EVENT.ORDER_CREATION_PASS, account, order)
def after_trading(self):
for account, order in self._open_orders:
order._mark_rejected(_("Order Rejected: {order_book_id} can not match. Market close.").format(
order_book_id=order.order_book_id
))
self._env.event_bus.publish_event(EVENT.ORDER_UNSOLICITED_UPDATE, account, order)
self._open_orders = self._delayed_orders
self._delayed_orders = []
def bar(self, bar_dict):
env = Environment.get_instance()
self._matcher.update(env.calendar_dt, env.trading_dt, bar_dict)
self._match()
def tick(self, tick):
# TODO support tick matching
pass
# env = Environment.get_instance()
# self._matcher.update(env.calendar_dt, env.trading_dt, tick)
# self._match()
def _match(self):
self._matcher.match(self._open_orders)
final_orders = [(a, o) for a, o in self._open_orders if o._is_final()]
self._open_orders = [(a, o) for a, o in self._open_orders if not o._is_final()]
for account, order in final_orders:
if order.status == ORDER_STATUS.REJECTED or order.status == ORDER_STATUS.CANCELLED:
self._env.event_bus.publish_event(EVENT.ORDER_UNSOLICITED_UPDATE, account, order)
================================================
FILE: engine/data_source.py
================================================
import six
import tushare as ts
from datetime import date
from dateutil.relativedelta import relativedelta
from rqalpha.data.base_data_source import BaseDataSource
class DataSource(BaseDataSource):
def __init__(self, path):
super(TushareKDataSource, self).__init__(path)
@staticmethod
def get_tushare_k_data(instrument, start_dt, end_dt):
order_book_id = instrument.order_book_id
code = order_book_id.split(".")[0]
if instrument.type == 'CS':
index = False
elif instrument.type == 'INDX':
index = True
else:
return None
return ts.get_k_data(code, index=index, start=start_dt.strftime('%Y-%m-%d'), end=end_dt.strftime('%Y-%m-%d'))
def get_bar(self, instrument, dt, frequency):
if frequency != '1d':
return super(TushareKDataSource, self).get_bar(instrument, dt, frequency)
bar_data = self.get_tushare_k_data(instrument, dt, dt)
if bar_data is None or bar_data.empty:
return super(TushareKDataSource, self).get_bar(instrument, dt, frequency)
else:
return bar_data.iloc[0].to_dict()
def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspended=True):
if frequency != '1d' or not skip_suspended:
return super(TushareKDataSource, self).history_bars(instrument, bar_count, frequency, fields, dt, skip_suspended)
start_dt_loc = self.get_trading_calendar().get_loc(dt.replace(hour=0, minute=0, second=0, microsecond=0)) - bar_count + 1
start_dt = self.get_trading_calendar()[start_dt_loc]
bar_data = self.get_tushare_k_data(instrument, start_dt, dt)
if bar_data is None or bar_data.empty:
return super(TushareKDataSource, self).get_bar(instrument, dt, frequency)
else:
if isinstance(fields, six.string_types):
fields = [fields]
fields = [field for field in fields if field in bar_data.columns]
return bar_data[fields].as_matrix()
def available_data_range(self, frequency):
return date(2005, 1, 1), date.today() - relativedelta(days=1)
================================================
FILE: engine/event_source.py
================================================
# -*- coding: utf-8 -*-
#
# Copyright 2017 Ricequant, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import time
from threading import Thread
from six.moves.queue import Queue, Empty
from rqalpha.interface import AbstractEventSource
from rqalpha.environment import Environment
from rqalpha.utils.logger import system_log
from rqalpha.events import Event, EVENT
from rqalpha.execution_context import ExecutionContext
from rqalpha.utils import json as json_utils
from .utils import get_realtime_quotes, order_book_id_2_tushare_code, is_holiday_today, is_tradetime_now
class RealtimeEventSource(AbstractEventSource):
def __init__(self, fps):
self._env = Environment.get_instance()
self.fps = fps
self.event_queue = Queue()
self.before_trading_fire_date = datetime.date(2000, 1, 1)
self.after_trading_fire_date = datetime.date(2000, 1, 1)
self.clock_engine_thread = Thread(target=self.clock_worker)
self.clock_engine_thread.daemon = True
self.quotation_engine_thread = Thread(target=self.quotation_worker)
self.quotation_engine_thread.daemon = True
def set_state(self, state):
persist_dict = json_utils.convert_json_to_dict(state.decode('utf-8'))
self.before_trading_fire_date = persist_dict['before_trading_fire_date']
self.after_trading_fire_date = persist_dict['after_trading_fire_date']
def get_state(self):
return json_utils.convert_dict_to_json({
"before_trading_fire_date": self.before_trading_fire_date,
"after_trading_fire_date": self.after_trading_fire_date,
}).encode('utf-8')
def quotation_worker(self):
while True:
if not is_holiday_today() and is_tradetime_now():
order_book_id_list = sorted(ExecutionContext.data_proxy.all_instruments("CS").order_book_id.tolist())
code_list = [order_book_id_2_tushare_code(code) for code in order_book_id_list]
try:
self._env.data_source.realtime_quotes_df = get_realtime_quotes(code_list)
except Exception as e:
system_log.exception("get_realtime_quotes fail")
continue
time.sleep(1)
def clock_worker(self):
while True:
# wait for the first data ready
if not self._env.data_source.realtime_quotes_df.empty:
break
time.sleep(0.1)
while True:
time.sleep(self.fps)
if is_holiday_today():
time.sleep(60)
continue
dt = datetime.datetime.now()
if dt.strftime("%H:%M:%S") >= "08:30:00" and dt.date() > self.before_trading_fire_date:
self.event_queue.put((dt, EVENT.BEFORE_TRADING))
self.before_trading_fire_date = dt.date()
elif dt.strftime("%H:%M:%S") >= "15:10:00" and dt.date() > self.after_trading_fire_date:
self.event_queue.put((dt, EVENT.AFTER_TRADING))
self.after_trading_fire_date = dt.date()
if is_tradetime_now():
self.event_queue.put((dt, EVENT.BAR))
def events(self, start_date, end_date, frequency):
running = True
self.clock_engine_thread.start()
self.quotation_engine_thread.start()
while running:
real_dt = datetime.datetime.now()
while True:
try:
dt, event_type = self.event_queue.get(timeout=1)
break
except Empty:
continue
system_log.debug("real_dt {}, dt {}, event {}", real_dt, dt, event_type)
yield Event(event_type, real_dt, dt)
================================================
FILE: engine/matcher.py
================================================
# -*- coding: utf-8 -*-
#
# Copyright 2017 Ricequant, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import defaultdict
from rqalpha.utils.i18n import gettext as _
from rqalpha.const import ORDER_TYPE, SIDE, BAR_STATUS
from rqalpha.model.trade import Trade
from rqalpha.environment import Environment
from rqalpha.events import EVENT
class Matcher(object):
def __init__(self,
deal_price_decider,
bar_limit=True,
volume_percent=0.25):
self._board = None
self._turnover = defaultdict(int)
self._calendar_dt = None
self._trading_dt = None
self._deal_price_decider = deal_price_decider
self._volume_percent = volume_percent
self._bar_limit = bar_limit
def update(self, calendar_dt, trading_dt, bar_dict):
self._board = bar_dict
self._turnover.clear()
self._calendar_dt = calendar_dt
self._trading_dt = trading_dt
def match(self, open_orders):
for account, order in open_orders:
slippage_decider = account.slippage_decider
commission_decider = account.commission_decider
tax_decider = account.tax_decider
bar = self._board[order.order_book_id]
bar_status = bar._bar_status
if bar_status == BAR_STATUS.ERROR:
listed_date = bar.instrument.listed_date.date()
if listed_date == self._trading_dt.date():
reason = _("Order Cancelled: current security [{order_book_id}] can not be traded in listed date [{listed_date}]").format(
order_book_id=order.order_book_id,
listed_date=listed_date,
)
else:
reason = _("Order Cancelled: current bar [{order_book_id}] miss market data.").format(
order_book_id=order.order_book_id)
order._mark_rejected(reason)
continue
deal_price = self._deal_price_decider(bar)
if order.type == ORDER_TYPE.LIMIT:
if order.price > bar.limit_up:
reason = _(
"Order Rejected: limit order price {limit_price} is higher than limit up {limit_up}."
).format(
limit_price=order.price,
limit_up=bar.limit_up
)
order._mark_rejected(reason)
continue
if order.price < bar.limit_down:
reason = _(
"Order Rejected: limit order price {limit_price} is lower than limit down {limit_down}."
).format(
limit_price=order.price,
limit_down=bar.limit_down
)
order._mark_rejected(reason)
continue
if order.side == SIDE.BUY and order.price < deal_price:
continue
if order.side == SIDE.SELL and order.price > deal_price:
continue
else:
if self._bar_limit and order.side == SIDE.BUY and bar_status == BAR_STATUS.LIMIT_UP:
reason = _(
"Order Cancelled: current bar [{order_book_id}] reach the limit_up price."
).format(order_book_id=order.order_book_id)
order._mark_rejected(reason)
continue
elif self._bar_limit and order.side == SIDE.SELL and bar_status == BAR_STATUS.LIMIT_DOWN:
reason = _(
"Order Cancelled: current bar [{order_book_id}] reach the limit_down price."
).format(order_book_id=order.order_book_id)
order._mark_rejected(reason)
continue
if self._bar_limit:
if order.side == SIDE.BUY and bar_status == BAR_STATUS.LIMIT_UP:
continue
if order.side == SIDE.SELL and bar_status == BAR_STATUS.LIMIT_DOWN:
continue
volume_limit = round(bar.volume * self._volume_percent) - self._turnover[order.order_book_id]
round_lot = bar.instrument.round_lot
volume_limit = (volume_limit // round_lot) * round_lot
if volume_limit <= 0:
if order.type == ORDER_TYPE.MARKET:
reason = _('Order Cancelled: market order {order_book_id} volume {order_volume}'
' due to volume limit').format(
order_book_id=order.order_book_id,
order_volume=order.quantity
)
order._mark_cancelled(reason)
continue
unfilled = order.unfilled_quantity
fill = min(unfilled, volume_limit)
ct_amount = account.portfolio.positions[order.order_book_id]._cal_close_today_amount(fill, order.side)
price = slippage_decider.get_trade_price(order, deal_price)
trade = Trade.__from_create__(order=order, calendar_dt=self._calendar_dt, trading_dt=self._trading_dt,
price=price, amount=fill, close_today_amount=ct_amount)
trade._commission = commission_decider.get_commission(trade)
trade._tax = tax_decider.get_tax(trade)
order._fill(trade)
self._turnover[order.order_book_id] += fill
Environment.get_instance().event_bus.publish_event(EVENT.TRADE, account, trade)
if order.type == ORDER_TYPE.MARKET and order.unfilled_quantity != 0:
reason = _(
"Order Cancelled: market order {order_book_id} volume {order_volume} is"
" larger than 25 percent of current bar volume, fill {filled_volume} actually"
).format(
order_book_id=order.order_book_id,
order_volume=order.quantity,
filled_volume=order.filled_quantity
)
order._mark_cancelled(reason)
================================================
FILE: engine/mod.py
================================================
# -*- coding: utf-8 -*-
#
# Copyright 2017 Ricequant, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from rqalpha.interface import AbstractMod
from rqalpha.utils.disk_persist_provider import DiskPersistProvider
from rqalpha.const import RUN_TYPE, PERSIST_MODE
from .data_source import DataSource
from .event_source import RealtimeEventSource
from .simulation_broker import Broker
class RealtimeTradeMod(AbstractMod):
def start_up(self, env, mod_config):
if env.config.base.run_type == RUN_TYPE.PAPER_TRADING:
env.set_data_source(DataSource(env.config.base.data_bundle_path))
env.set_event_source(RealtimeEventSource(mod_config.fps))
env.set_broker(Broker(env))
persist_provider = DiskPersistProvider(mod_config.persist_path)
env.set_persist_provider(persist_provider)
env.config.base.persist = True
env.config.base.persist_mode = PERSIST_MODE.REAL_TIME
def tear_down(self, code, exception=None):
pass
def load_mod():
return RealtimeTradeMod()
================================================
FILE: engine/utils.py
================================================
# -*- coding: utf-8 -*-
#
# Copyright 2017 Ricequant, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import time
import datetime
try:
from functools import lru_cache
except Exception as e:
from fastcache import lru_cache
from six.moves import reduce
from rqalpha.environment import Environment
from rqalpha.utils.datetime_func import convert_dt_to_int
def is_holiday_today():
today = datetime.date.today()
df = Environment.get_instance().data_proxy.get_trading_dates(today, today)
return len(df) == 0
def is_tradetime_now():
now_time = time.localtime()
now = (now_time.tm_hour, now_time.tm_min, now_time.tm_sec)
if (9, 15, 0) <= now <= (11, 30, 0) or (13, 0, 0) <= now <= (15, 0, 0):
return True
return False
TUSHARE_CODE_MAPPING = {
"sh": "000001.XSHG",
"sz": "399001.XSHE",
"sz50": "000016.XSHG",
"hs300": "000300.XSHG",
"sz500": "000905.XSHG",
"zxb": "399005.XSHE",
"cyb": "399006.XSHE",
}
def tushare_code_2_order_book_id(code):
try:
return TUSHARE_CODE_MAPPING[code]
except KeyError:
if code.startswith("6"):
return "{}.XSHG".format(code)
elif code[0] in ["3", "0"]:
return "{}.XSHE".format(code)
else:
raise RuntimeError("Unknown code")
def order_book_id_2_tushare_code(order_book_id):
return order_book_id.split(".")[0]
def get_realtime_quotes(code_list, open_only=False):
import tushare as ts
max_len = 800
loop_cnt = int(math.ceil(float(len(code_list)) / max_len))
total_df = reduce(lambda df1, df2: df1.append(df2),
[ts.get_realtime_quotes([code for code in code_list[i::loop_cnt]])
for i in range(loop_cnt)])
total_df["is_index"] = False
index_symbol = ["sh", "sz", "hs300", "sz50", "zxb", "cyb"]
index_df = ts.get_realtime_quotes(index_symbol)
index_df["code"] = index_symbol
index_df["is_index"] = True
total_df = total_df.append(index_df)
total_df = total_df.set_index("code").sort_index()
columns = set(total_df.columns) - set(["name", "time", "date"])
# columns = filter(lambda x: "_v" not in x, columns)
for label in columns:
total_df[label] = total_df[label].map(lambda x: 0 if str(x).strip() == "" else x)
total_df[label] = total_df[label].astype(float)
total_df["chg"] = total_df["price"] / total_df["pre_close"] - 1
total_df["order_book_id"] = total_df.index
total_df["order_book_id"] = total_df["order_book_id"].apply(tushare_code_2_order_book_id)
total_df["datetime"] = total_df["date"] + " " + total_df["time"]
total_df["datetime"] = total_df["datetime"].apply(lambda x: convert_dt_to_int(datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")))
total_df["close"] = total_df["price"]
if open_only:
total_df = total_df[total_df.open > 0]
return total_df
================================================
FILE: puppet/__init__.py
================================================
from .client import __version__
from .client import Account, login
from .runner import run
from .util import check_config
from .management import Manager
from .option import Option
================================================
FILE: puppet/client.py
================================================
# -*- coding: utf-8 -*-
"""
扯线木偶界面自动化应用编程接口(Puppet UIAutomation API)
技术群:624585416
"""
__author__ = "睿瞳深邃(https://github.com/Raytone-D)"
__project__ = 'Puppet'
__version__ = "1.10.1"
__license__ = 'MIT'
import ctypes
import time
import io
import re
import random
import threading
from functools import reduce, lru_cache, partial
from . import util
user32 = ctypes.windll.user32
def login(accinfos):
return Account(accinfos)
class Ths:
'''Hithink RoyalFlush Trading Client'''
NODE = {
'buy': 161,
'sell': 162,
'cancel': 163,
'cancel_all': 163,
'cancel_buy': 163,
'cancel_sell': 163,
'undone': 163,
'order': 168,
'trade': 512,
'buy2': 512,
'sell2': 512,
'mkt': 165,
'summary': 165,
'balance': 165,
'cash': 165,
'position': 165,
'account': 165,
'equity': 165,
'assets': 165,
'deal': 167,
'historical_deal': 510,
'delivery_order': 176,
'new': 554,
'purchase_new': 554,
'reverse_repo': 717,
'purchase': 433,
'redeem': 434,
'margin': 454, # 融资融券、保证金
'margin_pos': 454,
'buy_on_margin': 448, # 融资买入
'sell_for_repayment': 449, # 卖券还款
'discount': 466, # 可充抵保证金证券折扣率
'margin_cancel_all': 465,
'margin_cancel_buy': 465,
'margin_cancel_sell': 465,
'batch': 5170,
'bingo': 1070
}
INIT = ('cancel', 'deal', 'order', 'buy', 'sell')
LOGIN = (1011, 1012, 1001, 1003, 1499)
ACCOUNT = (59392, 0, 1711)
ACCNAME = (59392, 0, 2322)
MKT = (59392, 0, 1003)
TABLE = (1047, 200, 1047)
SUMMARY_ = ('summary', 'margin')
SUMMARY = (('cash', 1016), ('frozen', 1013), ('balance', 1012),
('market_value', 1014), ('equity', 1015), ('position_pct', 1019))
# symbol, price, max_qty, quantity, quote
BUY = (1032, 1033, 1034, 0, 1018)
SELL = (1032, 1033, 1034, 0, 1038)
REVERSE_REPO = (1032, 1033, 1034, 0, 1018)
BUY2 = (3451, 1032, 1541, 1033, 1018, 1034)
SELL2 = (3453, 1035, 1542, 1058, 1019, 1039)
CANCEL = 3348,
PURCHASE = (1032, 1034)
REDEEM = (1032, 1034)
PAGE = 59648, 59649
FRESH = 32790
QUOTE = 1024
MARGIN = (('id', 10001), ('guarantee_rate', 10003), ('margin', 10006),
('cash', 10008), ('frozen', 10009), ('balance', 10007),
('market_value', 10010), ('equity', 10032), ('debts', 10005), ('assets', 10004))
BUTTON = {'cancel_all': '全撤(Z /)', 'cancel_buy': '撤买(X)', 'cancel_sell': '撤卖(C)',
'cancel': '撤单', 'buy': '买入[B]', 'sell': '卖出[S]', 'reverse_repo': '确定',
'margin_cancel_all': '全撤(Z /)', 'margin_cancel_buy': '撤买(X)', 'margin_cancel_sell': '撤卖(C)'}
ERROR = ['无可撤委托', '提交失败', '当前时间不允许委托']
WAY = {
0: "LIMIT 限价委托 沪深",
1: "BEST5_OR_CANCEL 最优五档即时成交剩余撤销 沪深",
2: "BEST5_OR_LIMIT 最优五档即时成交剩余转限价 上海",
20: "REVERSE_BEST_LIMIT 对方最优价格 深圳",
3: "FORWARD_BEST 本方最优价格 深圳",
4: "BEST_OR_CANCEL 即时成交剩余撤销 深圳",
5: "ALL_OR_CANCEL 全额成交或撤销 深圳"
}
class Account:
'''Puppet Trading Account API'''
def __init__(self, accinfos=None, enable_heartbeat=True, to_dict=False, dirname='',
keyboard=True, title=None, **kwargs):
self.accinfos = accinfos
self.enable_heartbeat = enable_heartbeat
self.to_dict = to_dict
self.dirname = dirname
self.keyboard = keyboard
self.title = title
self.kwargs = kwargs
self._post_init()
def _post_init(self):
self.heartbeat_stamp = time.time()
self.root = 0
self.ctx = Ths
self.filename = '{}\\table.xls'.format(
self.dirname or util.locate_folder())
self.loginfile = '{}\\login.json'.format(
self.dirname or util.locate_folder())
self.dxinput = __import__('pydirectinput')
self.cancel_all = partial(self.cancel, action='cancel_all')
self.cancel_buy = partial(self.cancel, action='cancel_buy')
self.cancel_sell = partial(self.cancel, action='cancel_sell')
self.cancel_buy.__doc__ = '撤销指定证券代码的买单'
self.cancel_sell.__doc__ = '撤销指定证券代码的卖单'
self.query = self.export_data if not self.kwargs.get('copy') else self.copy_data
if self.accinfos:
self.login(**self.accinfos)
elif isinstance(self.title, str):
self.bind(self.title)
@property
def status(self):
return user32.IsWindowVisible(self.root)
def login(self, account_no: str = '', password: str = '', **kwargs):
from PIL.ImageGrab import grab
pid = util.run_app(kwargs['client_path'])
self.root = util.find_one(visible=False)
id_proc = ctypes.c_ulong()
user32.GetWindowThreadProcessId(self.root, ctypes.byref(id_proc))
if id_proc.value == pid:
self.h_login = util.wait_for_popup(self.root)
if util.go_to_top(self.h_login) and util.wait_for_view(self.h_login):
time.sleep(1) # ! WARNING: DON'T DELETE! Wait for focus.
util.keyboard.send('up') # * Jump to the account_no
time.sleep(0.1) # ! WARNING: DON'T DELETE! Wait for focus.
# TODO: 后续支持4K等分辨率
captcha = kwargs.get('comm_pwd') or util.image_to_string(
grab(util.get_rect(user32.GetDlgItem(self.h_login, 1499))))
for text in (account_no, password, captcha):
util.keyboard.write(text)
time.sleep(0.5) # ! Old processor should ask for more.
util.keyboard.send('enter')
time.sleep(0.02)
print('{} 正在登录交易服务器...'.format(util.curr_time()))
if util.wait_for_view(self.root, timeout=9):
kwargs.update(account_no=account_no, password=password)
self.infos = kwargs
print('{} 已登入交易服务器。'.format(util.curr_time()))
self.init()
return {'puppet': "{} {}".format(
util.curr_time(), '准备就绪!'if self.status else '登录失败!')}
def exit(self):
"退出系统并关闭程序"
if self.root:
user32.PostMessageW(self.root, util.Msg.WM_CLOSE, 0, 0)
return {'puppet': "{} 客户端已退出!".format(util.curr_time())}
def fill_and_submit(self, *args, delay=0.1, label=''):
user32.SetForegroundWindow(self._page)
for text, handle in zip(args, self._handles):
util.fill(str(text), handle)
if delay:
for _ in range(9):
max_qty = util.get_text(self._handles[-1])
if max_qty not in (''):
break
time.sleep(delay)
time.sleep(0.1)
util.click_button(h_dialog=self._page, label=label)
return self
@property
def accno(self):
return util.get_text(reduce(user32.GetDlgItem, self.ctx.ACCOUNT, self.root))
def trade(self, action: str, *args) -> dict:
"""下单
委托策略(注意个别券商自定义索引)
0 LIMIT 限价委托 沪深
1 BEST5_OR_CANCEL 最优五档即时成交剩余撤销 上海
2 BEST5_OR_LIMIT 最优五档即时成交剩余转限价 上海
1 REVERSE_BEST_LIMIT 对方最优价格 深圳
2 FORWARD_BEST 本方最优价格 深圳
3 BEST_OR_CANCEL 即时成交剩余撤销 深圳
4 BEST5_OR_CANCEL 最优五档即时成交剩余撤销 深圳
5 ALL_OR_CANCEL 全额成交或撤销 深圳
"""
self.switch(action)
if util.go_to_top(self.root):
for idx, arg in enumerate(args):
util.keyboard.write(str(arg))
time.sleep(0.1)
if idx == 0:
h_name = user32.GetDlgItem(self._page, 1036)
for _ in range(99):
# * 等待检索市场归属和中文简称完成
time.sleep(0.02)
if util.get_text(h_name) != '':
break
util.keyboard.send('enter')
time.sleep(0.02)
return self.answer()
def buy(self, symbol: str, price, quantity: int) -> dict:
return self.trade('buy', symbol, price, quantity)
def sell(self, symbol: str, price, quantity: int) -> dict:
return self.trade('sell', symbol, price, quantity)
def reverse_repo(self, symbol: str, price: float, quantity: int, delay=0.2) -> dict:
"""逆回购 R-001 SZ '131810'; GC001 SH '204001' """
return self.trade('reverse_repo', symbol, price, quantity, delay=delay)
def cancel(self, symbol: str = None, action: str = 'cancel') -> dict:
'''撤销指定证券代码的委托单。两融户需要在action参数加上前缀 margin_
2020-12-08 重构撤单代码
'''
util.go_to_top(self.root)
self.switch('cancel', 1)
if isinstance(symbol, str):
h_edit = util.get_child_handle(self._page, id_ctrl=3348, clsname='Edit', visible=None)
util.fill(symbol, h_edit)
h_button = util.get_child_handle(self._page, label='撤单', clsname='Button')
for _ in range(9):
if user32.SendMessageW(h_edit, util.Msg.WM_GETTEXTLENGTH, 0, 0) == len(symbol):
util.click_button(h_dialog=self._page, label='查询代码')
break
time.sleep(0.05)
for _ in range(9):
if user32.IsWindowEnabled(h_button): # 撤单按钮是否可用
break
time.sleep(0.05)
util.click_button(h_dialog=self._page, label=self.ctx.BUTTON[action])
return self.answer()
def purchase_new(self):
"新股申购"
def func(ipo):
symbol = ipo.get('新股代码') or ipo.get('证券代码')
price = ipo.get('申购价格') or ipo.get('发行价格')
orders = self.query('order')
had = [order['证券代码'] for order in orders]
if symbol in had:
r = (0, '%s 已经申购' % symbol)
elif symbol not in had:
r = self.buy(symbol, price, 0)
else:
r = (0, '不可预测的申购错误')
return r
target = self.query('new')
if target:
return {'puppet': [func(ipo) for ipo in target]}
def fund_purchase(self, symbol: str, amount: int):
"""基金申购"""
return self.trade('purchase', symbol, amount)
def fund_redeem(self, symbol: str, share: int):
"""基金赎回"""
return self.trade('redeem', symbol, share)
def buy_on_margin(self, symbol: str, price, quantity: int) -> dict:
'''融资买入'''
return self.trade('buy_on_margin', symbol, price, quantity)
def sell_for_repayment(self, symbol: str, price, quantity: int) -> dict:
'''卖券还款'''
return self.trade('sell_for_repayment', symbol, price, quantity)
"Development"
def __repr__(self):
status = 'on-line' if self.status else 'off-line'
return "" % (self.__class__.__name__, __version__, status)
def bind(self, arg='', dirname: str = '', **kwargs):
""""
:arg: 客户端的标题或根句柄
:mkt: 交易市场的索引值
"""
if arg == '':
self.root = util.find_one()
elif 'title' in kwargs or isinstance(arg, str):
self.root = user32.FindWindowW(0, kwargs.get('title') or (
arg or '网上股票交易系统5.0'))
elif 'root' in kwargs or isinstance(arg, int):
self.root = kwargs.get('root') or arg
if self.visible(self.root):
self.birthtime = time.ctime()
self.title = util.get_text(self.root)
self.idx = 0
self.init()
self.filename = '{}\\table.xls'.format(dirname or util.locate_folder())
return {'puppet': "{} 木偶准备就绪!".format(util.curr_time())}
return {'puppet': '标题错误或者客户端失联'}
def visible(self, hwnd=None, times=0):
for _ in range(times or 1):
val = user32.IsWindowVisible(hwnd or self.root)
if val:
return True
elif times > 0:
time.sleep(0.5)
return False
def switch(self, name, delay=0.01):
self.heartbeat_stamp = time.time()
assert self.visible(), "客户端已关闭或账户已登出"
node = name if isinstance(name, int) else self.ctx.NODE[name]
if user32.SendMessageW(self.root, util.Msg.WM_COMMAND, 0x2000 << 16 | node, 0):
self._page = reduce(user32.GetDlgItem, self.ctx.PAGE, self.root)
time.sleep(delay)
return True
def init(self):
for name in self.ctx.INIT:
self.switch(name, 0.3)
if self.keyboard:
def func(*args, **kwargs):
user32.SetForegroundWindow(self._page)
for text in args:
util.keyboard.write('{}\n'.format(text))
return self
self.fill_and_submit = func
time.sleep(2) # ? 为了兼容银河证券临时加上
self.mkt = (0, 1) if util.get_text(
self.get_handle('mkt')).startswith('上海') else (1, 0)
user32.ShowOwnedPopups(self.root, False)
# 写入 table.xls 的绝对路径
self.location = True
self.query('deal')
self.location = False
self.make_heartbeat()
print("{} 木偶准备就绪!".format(util.curr_time()))
return self
def export_data(self, category: str = 'summary') -> dict:
"""export latest trading data
category: 'summary', 'position', 'order', 'deal', 'undone', 'historical_deal'
'delivery_order', 'new', 'bingo', 'margin', 'margin_pos', 'discount' 其中之一
"""
print('Querying {} on-line...'.format(category))
self.switch(category)
rtn = util.pd.Series({'puppet': False})
if util.go_to_top(self.root):
_l, ypos, xpos, _b = util.get_rect(self.root)
xpos -= 166
ypos += 166 if category in self.ctx.SUMMARY_ else 332
if util.get_text(user32.WindowFromPoint(ctypes.wintypes.POINT(xpos, ypos))) == '':
time.sleep(1) # temporary
rtn = dict((x, float(util.get_text(h_parent=self._page, id_child=y)))
for x, y in getattr(self.ctx, category.upper()))
rtn.update(login_id=self.accno, token=id(self))
else:
time.sleep(0.5)
util.click_context_menu('s', xpos, ypos)
string = util.export_data(self.filename, self.root, location=self.location)
if string != '':
rtn = util.normalize(string, self.to_dict)
return rtn
def copy_data(self, category: str = 'summary'):
'''复制CVirtualGridCtrl|Custom的文本数据到剪贴板
广发
'''
globals().update(pyperclip=__import__('pyperclip'))
pyperclip.copy('')
self.switch(category, 0.5)
user32.PostMessageW(self.get_handle(category),
util.Msg.WM_COMMAND, util.Msg.COPY, 0)
for _ in range(9):
# 关闭验证码弹窗
print('Removing copy protection...')
time.sleep(0.1) # have to
h_popup = util.wait_for_popup(self.root)
if util.wait_for_view(h_popup):
text = self.verify(self.grab(h_popup))
h_edit = user32.FindWindowExW(h_popup, None, 'Edit', "")
util.fill(text, h_edit)
util.click_button(h_dialog=h_popup, label='确定')
time.sleep(0.3) # have to wait!
break
_replace = {'参考市值': '市值', '最新市值': '市值'} # 兼容国金/平安"最新市值"、银河“参考市值”。
ret = pyperclip.paste().splitlines()
# 数据格式化
temp = (x.split('\t') for x in ret)
header = next(temp)
for tag, value in _replace.items():
if tag in header:
header.insert(header.index(tag), value)
header.remove(tag)
return [dict(zip(header, x)) for x in temp]
@lru_cache()
def get_handle(self, action: str):
"""
:action: 操作标识符
"""
if action in ('cancel_all', 'cancel_buy', 'cancel_sell'):
action = 'cancel'
self.switch(action)
m = getattr(self.ctx, action.upper(), self.ctx.TABLE)
if action in ('buy', 'buy2', 'sell', 'sell2', 'reverse_repo', 'cancel', 'purchase', 'redeem'):
data = [user32.GetDlgItem(self._page, i) for i in m]
else:
data = reduce(
user32.GetDlgItem, m,
self.root if action in ('account', 'mkt') else self._page)
return data
def grab(self, hParent=None):
"屏幕截图"
from PIL import ImageGrab
buf = io.BytesIO()
rect = ctypes.wintypes.RECT()
hImage = user32.FindWindowExW(hParent or self.hLogin, None, 'Static',
"")
user32.GetWindowRect(hImage, ctypes.byref(rect))
user32.SetForegroundWindow(hParent or self.hLogin)
screenshot = ImageGrab.grab(
(rect.left, rect.top, rect.right + (rect.right - rect.left) * 0.33,
rect.bottom))
screenshot.save(buf, 'png')
return buf.getvalue()
def verify(self, image, ocr=None):
try:
from aip import AipOcr
except Exception as e:
print(e, '\n请在命令行下执行: pip install baidu-aip')
conf = ocr or {
'appId': '11645803',
'apiKey': 'RUcxdYj0mnvrohEz6MrEERqz',
'secretKey': '4zRiYambxQPD1Z5HFh9VOoPXPK9AgBtZ'
}
ocr = AipOcr(**conf)
try:
r = ocr.basicGeneral(image).get('words_result')[0]['words']
except Exception as e:
print(e, '\n验证码图片无法识别!')
r = False
return r
def capture(self, root=None, label=''):
""" 捕捉弹窗的文本内容 """
buf = ctypes.create_unicode_buffer(64)
root = root or self.root
for _ in range(9):
time.sleep(0.1)
hPopup = user32.GetLastActivePopup(root)
if hPopup != root: # and self.visible(hPopup):
hTips = user32.FindWindowExW(hPopup, 0, 'Static', None)
# print(hex(hPopup).upper(), hex(hTips).upper())
hwndChildAfter = None
for _ in range(9):
hButton = user32.FindWindowExW(hPopup, hwndChildAfter, 'Button', 0)
user32.SendMessageW(hButton, util.Msg.WM_GETTEXT, 64, buf)
if buf.value in ('是(&Y)', '确定'):
label = buf.value
break
hwndChildAfter = hButton
user32.SendMessageW(hTips, util.Msg.WM_GETTEXT, 64, buf)
util.click_button(h_dialog=hPopup, label=label)
break
text = buf.value
return text if text else '请按提示修改:系统设置->快速交易->委托成功后是否弹出提示对话框->是'
def answer(self):
"""2020-2-10 修改逻辑确保回报窗口被关闭"""
for _ in range(3):
text = self.capture()
if '编号' in text:
return {'puppet': (re.findall(r'(\w*[0-9]+)\w*', text)[0], text)}
for x in self.ctx.ERROR:
if x in text:
return {'puppet': (0, text)}
return {'puppet': (0, '弹窗捕获失败,请用check_config()检查设置')}
def refresh(self):
print('Refreshing page...')
user32.PostMessageW(self.root, util.Msg.WM_COMMAND, self.ctx.FRESH, 0)
return self if self.visible() else False
def switch_mkt(self, symbol: str, handle: int):
"""
:Prefix:上交所: '5'基, '6'A, '7'申购, '11'转债', 9'B
适配银河|中山证券的默认值(0 ->上海A股)。注意全角字母A
"""
index = self.mkt[0] if symbol.startswith(
('6', '5', '7', '11')) else self.mkt[1]
return util.switch_combobox(index, handle)
def switch_way(self, index):
"""转为市价委托
index: {1, 2, 3, 4, 5}, 留意沪深市价单有所不同。
"""
if index in {1, 2, 3, 4, 5}:
return util.switch_combobox(index, next(self.members))
def if_fund(self, symbol, price):
if symbol.startswith('5'):
if len(str(price).split('.')[1]) == 3:
self.capture()
def make_heartbeat(self, time_interval=1680):
"""2019-6-6 新增方法制造心跳
"""
def refresh_page(time_interval):
while self.enable_heartbeat:
if not self.visible():
print("客户端离线(Off-line)!")
break
stamp = self.heartbeat_stamp
remainder = time_interval - (time.time() - stamp)
secs = random.uniform(remainder/2, remainder)
print('Refreshing after {} minutes.'.format(secs/60))
time.sleep(secs)
# 若在休眠期间心跳印记没被修改,则刷新页面并修改心跳印记
if self.visible() and stamp == self.heartbeat_stamp:
# print('Making heartbeat...')
self.refresh()
self.heartbeat_stamp = time.time()
threading.Thread(
target=refresh_page,
kwargs={'time_interval': time_interval},
name='heartbeat',
daemon=True).start()
def quote(self, codes, df_first=True):
"""有bug未修复! get latest deal price"""
self.switch('sell')
code_h, *_, page_h = self.get_handle('sell')
handle = user32.GetDlgItem(page_h, self.ctx.QUOTE)
names = ['code', 'price']
if isinstance(codes, str):
codes = [codes]
def _quote(code: str) -> float:
util.fill(code, code_h)
for _ in range(5):
text = util.get_text(handle)
if text != '-':
return float(text)
time.sleep(0.1)
data = [(code, _quote(code)) for code in codes]
if df_first:
data = util.pd.DataFrame(data, columns=names)
return data
def switch_account(self, serial_no: int):
"""切换账号
serial_no: 登录账号的顺序号,从1开始。
"""
cb = reduce(user32.GetDlgItem, self.ctx.ACCNAME, self.root)
count = user32.SendMessageW(cb, util.Msg.CB_GETCOUNT, 0, 0)
curr = user32.SendMessageW(cb, util.Msg.CB_GETCURSEL, 0, 0) + 1
if 0 < serial_no < count and serial_no != curr and util.go_to_top(self.root):
self.dxinput.keyDown('alt')
self.dxinput.press(str(serial_no))
self.dxinput.keyUp('alt')
time.sleep(0.1)
return self.accno, util.get_text(cb)
return curr, count
================================================
FILE: puppet/management.py
================================================
'''Author: Raytone-D
Date: 2020-10-21
'''
from . import Account
from .util import find_all
class Manager:
'''管理多个木偶实例的类'''
def __init__(self, accno: int =None, to_dict=True, keyboard=False):
self.acc_list = [Account(title=root, to_dict=to_dict, keyboard=keyboard) for root in find_all()]
self.accno_list = [int(acc.id) for acc in self.acc_list]
self.take(accno or self.accno_list[0])
def take(self, accno: int):
'''指定交易账号'''
if accno in self.accno_list:
self.acc = self.acc_list[self.accno_list.index(accno)]
return self.acc
def __getattr__(self, name: str):
return getattr(self.acc, name)
================================================
FILE: puppet/option.py
================================================
'''
option pal
需要将锁屏时间设为最大值
'''
__author__ = "睿瞳深邃(https://github.com/Raytone-D)"
__version__ = "1.2"
__license__ = 'MIT'
import ctypes
import io
import time
import functools
import keyboard as kb
from . import util
user32 = ctypes.windll.user32
class Option:
'''期权'''
send_order = 3001
cancel = 3002
undone= 3002
position = 3003
summary = 3003
deal = 3004
refresh = 3005
def __init__(self, keyword: str = '期权宝') -> None:
self.keyword = keyword
if isinstance(keyword, str):
self.initial(keyword)
def initial(self, keyword):
self.root = util.find_one(keyword)
util.go_to_top(self.root)
kb.send('F12') # 切换到交易界面
def get_page_by_trait(h_dialog: int, name: str, classname: str = 'Static') -> int:
'''利用子控件的特征获取窗口页面句柄'''
return user32.GetParent(util.find_single_handle(h_dialog, name, classname))
self.h_switcher = get_page_by_trait(self.root, '下单', 'Button')
h_menu = get_page_by_trait(self.root, '功能菜单', 'Button')
h_treeview = user32.FindWindowExW(h_menu, None, 'SysTreeView32', None)
rect = util.get_rect(h_treeview)
self.xpos = rect[2] - int((rect[2] - rect[0]) * 0.5)
self.ypos = rect[1] + 90 # 拆分申报 +90; 组合策略持仓查询 +100
# 初始化页面
for node in ('cancel', 'position', 'deal', 'send_order', 'portfolio'):
self.switch(node)
time.sleep(0.5) # compatible with low frequency
h_panel = get_page_by_trait(self.root, '买入开仓', 'Button')
self.h_edit = user32.FindWindowExW(h_panel, None, 'Edit', None)
handles = [user32.FindWindowExW(h_panel, None, 'Button', x) for x in ('重置', '买入', '卖出', '开仓', '平仓')]
self.h_button = dict(zip(['reset', 'buy', 'sell', 'open', 'close'], handles))
page = [get_page_by_trait(self.root, x) for x in ('撤单', '资金持仓', '当日成交')]
data = [user32.FindWindowExW(x, None, 'Button', '输出') for x in page]
self.h_split = util.find_single_handle(self.root, '拆分', 'Button')
page = user32.GetParent(self.h_split)
data.append(user32.FindWindowExW(page, None, 'Button', '输出'))
self.h_export = dict(zip(['undone', 'position', 'deal', 'portfolio'], data))
self.lv = util.find_single_handle(page, 'List1', 'SysListView32')
self.buy_close = functools.partial(self.buy_open, action='buy_close')
self.sell_open = functools.partial(self.buy_open, action='sell_open')
self.sell_close = functools.partial(self.buy_open, action='sell_close')
def switch(self, node: str, delay: int = 0.5):
if util.go_to_top(self.root):
if node in ('split', 'portfolio'):
self.switch('send_order').switch('position')
user32.SetCursorPos(self.xpos, self.ypos)
user32.mouse_event(util.Msg.MOUSEEVENTF_LEFTDOWN | util.Msg.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(delay)
else:
id_button = getattr(self, node)
user32.PostMessageW(self.h_switcher, util.Msg.WM_COMMAND, id_button, 0)
time.sleep(delay)
return self
def query(self, category: str = 'summary'):
'''查询交易数据
category: 'summary', 'position', 'undone', 'deal', 'portfolio'
'''
self.switch(category)
key = 'position' if category == 'summary' else category
user32.PostMessageW(self.h_export[key], util.Msg.BM_CLICK, 0, 0)
return self.normalize(self.handle_query_popup().export_csv(category))
def handle_query_popup(self, times: int = 9):
'''处理查询弹窗'''
self.path = False
buf = ctypes.create_unicode_buffer(64)
for _ in range(times):
h_popup: int = user32.GetLastActivePopup(self.root)
if h_popup != self.root and user32.IsWindowVisible(h_popup):
user32.SendMessageW(h_popup, util.Msg.WM_GETTEXT, 64, buf)
if buf.value == '数据输出':
h_checkbox: int = user32.FindWindowExW(h_popup, None, 'Button', '主动打开输出的文件')
if user32.SendMessageW(h_checkbox, util.Msg.BM_GETSTATE, 0, 0):
user32.PostMessageW(h_checkbox, util.Msg.BM_CLICK, 0, 0) # 不勾选
h_edit: int = user32.FindWindowExW(h_popup, None, 'Edit', None)
user32.SendMessageW(h_edit, util.Msg.WM_GETTEXT, 64, buf) # 获取文件的绝对路径
self.path: str = buf.value
kb.send('\r')
continue
elif buf.value == '数据导出':
kb.send('\r')
break
elif buf.value == '提示':
kb.send('\r')
break
time.sleep(0.1)
return self
def export_csv(self, category: str) -> str:
if self.path:
for _ in range(9):
try:
with open(self.path) as f:
text = f.read()
except Exception as e:
print(e)
time.sleep(0.1)
else:
if category in ('summary', 'position'):
x, y = text.split('\n\n', 1)
text = dict(summary=x, position=y.replace('没有查询到相应的数据!', '', 1))[category]
return text.expandtabs().strip()
def normalize(self, text: str):
'''将文本数据转为DataFrame格式'''
return util.pd.read_csv(io.StringIO(text), sep='\s+') if text else util.pd.DataFrame()
def buy_open(self, symbol, price=None, quantity=None, action='buy_open'):
'''
price: None 取默认值 卖1
quantity: None 取默认值 1
'''
self.switch('send_order')
user32.PostMessageW(self.h_button['reset'], util.Msg.BM_CLICK, 0, 0) # 清除默认的证券代码
for _ in range(9):
if user32.SendMessageW(self.h_edit, util.Msg.WM_GETTEXTLENGTH, 0, 0) == 0:
break
time.sleep(0.1)
kb.write(symbol)
for _ in range(9):
if user32.SendMessageW(self.h_edit, util.Msg.WM_GETTEXTLENGTH, 0, 0) == len(symbol):
break
time.sleep(0.05)
act, open_or_close = action.split('_')
user32.PostMessageW(self.h_button[act], util.Msg.BM_CLICK, 0, 0)
user32.PostMessageW(self.h_button[open_or_close], util.Msg.BM_CLICK, 0, 0) # 开平标志
if open_or_close == 'open':
kb.send('DOWN')
kb.send('DOWN')
kb.send('DOWN') # 跳过报价方式,默认限价
if price != None:
kb.write(str(price))
kb.send('\r')
if quantity != None:
kb.write(str(quantity))
kb.send('\r')
return self.handle_trading_popup()
def handle_trading_popup(self, times: int = 9):
'''处理下单弹窗'''
rtn = False
label = ('买入开仓', '买入平仓', '卖出开仓', '卖出平仓')
buf = ctypes.create_unicode_buffer(64)
for _ in range(times):
h_popup: int = user32.GetLastActivePopup(self.root)
if h_popup != self.root and user32.IsWindowVisible(h_popup):
user32.SendMessageW(h_popup, util.Msg.WM_GETTEXT, 64, buf)
if buf.value == '快速下单':
kb.send('\r')
elif buf.value in label:
kb.send('\r')
break
elif buf.value == '提示':
rtn = True
kb.send('\r')
break
elif buf.value == '拆分申报':
h_edit = user32.FindWindowExW(h_popup, None, 'Edit', None)
user32.SendMessageW(h_edit, util.Msg.WM_SETTEXT, 0, self.qty)
kb.send('\r')
elif buf.value == '警告':
rtn = True
kb.send('\r')
break
time.sleep(0.1)
kb.send('esc')
return rtn
def split_decl(self, strategy_id: int, quantity: int, delay=0.1):
'''拆分申报'''
df = self.query('portfolio')
if not df.empty:
rect = util.get_rect(self.lv)
user32.SetCursorPos(rect[0] + 9, rect[1] + 29)
user32.mouse_event(util.Msg.MOUSEEVENTF_LEFTDOWN | util.Msg.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(delay)
for x in range(df['序号'].to_list().index(strategy_id)):
if x > 0:
kb.send(kb.KEY_DOWN)
self.qty = str(quantity)
user32.PostMessageW(self.h_split, util.Msg.BM_CLICK, 0, 0)
return self.handle_trading_popup()
if __name__ == "__main__":
opt = Option()
time.sleep(3)
df = opt.query('position')
print(df)
================================================
FILE: puppet/runner.py
================================================
'''
Author: 睿瞳深邃(https://github.com/Raytone-D)
License: MIT
Release date: 2020-06-06
Version: 0.4
'''
from .client import Account, __version__
def run(host='127.0.0.1', port=10086):
'''Puppet's HTTP Service'''
from bottle import post, run, request, response
@post('/puppet')
def serve():
'''Puppet Web Trading Interface'''
task = request.json
if task:
try:
return getattr(acc, task.pop('action'))(**task)
except Exception as e:
response.bind(status=502)
return {'puppet': str(e)}
return {'puppet': '仅支持json格式'}
print('Puppet version:', __version__)
acc = Account(to_dict=True)
run(host=host, port=port)
if __name__ == "__main__":
run()
================================================
FILE: puppet/util.py
================================================
"""给木偶写的一些工具函数
"""
import configparser
import ctypes
import datetime
import io
import os
import subprocess
import threading
import time
import winreg
from ctypes.wintypes import BOOL, HWND, LPARAM
try:
import pandas as pd
except Exception as e:
print(e)
try:
import keyboard
except Exception as e:
print(e)
from pywinauto import keyboard
keyboard.write = keyboard.send_keys
class Msg:
WM_SETTEXT = 12
WM_GETTEXT = 13
WM_GETTEXTLENGTH = 14
WM_CLOSE = 16
WM_KEYDOWN = 256
WM_KEYUP = 257
WM_COMMAND = 273
BM_GETCHECK = 240
BM_SETCHECK = 241
BM_GETSTATE = 242
BM_CLICK = 245
BST_CHECKED = 1
CB_GETCOUNT = 326
CB_GETCURSEL = 327
CB_SETCURSEL = 334
CB_SHOWDROPDOWN = 335
CBN_SELCHANGE = 1
CBN_SELENDOK = 9
MOUSEEVENTF_LEFTDOWN = 2
MOUSEEVENTF_LEFTUP = 4
MOUSEEVENTF_RIGHTDOWN = 8
MOUSEEVENTF_RIGHTUP = 16
COLNAMES = {
'证券代码': 'symbol',
'证券名称': 'name',
'股票余额': 'quantity', # 海通证券
'当前持仓': 'quantity', # 银河证券
'可用余额': 'leftover',
'冻结数量': 'frozen',
'盈亏': 'profit',
'参考盈亏': 'profit', # 银河证券
'浮动盈亏': 'profit', # 广发证券
'市价': 'price',
'市值': 'amount',
'参考市值': 'amount', # 银河证券
'最新市值': 'amount', # 国金|平安证券
'成本价': 'cost',
'成交时间': 'time',
'成交日期': 'date',
'成交数量': 'quantity',
'成交均价': 'price',
'成交价格': 'price',
'成交金额': 'amount',
'成交编号': 'id',
'申报时间': 'time',
'委托日期': 'date',
'委托时间': 'time',
'委托价格': 'order_price',
'委托数量': 'order_qty',
'合同编号': 'order_id',
'委托编号': 'order_id', # 银河证券
'委托状态': 'status',
'操作': 'op',
'发生金额': 'total',
'手续费': 'commission',
'印花税': 'tax',
'其他杂费': 'fees',
'资金余额': 'balance', # 银河证券
'可用金额': 'cash',
'总市值': 'market_value',
'总资产': 'equity'
}
OPTIONS = {
'GUI_CHEDAN_CONFIRM': 'no',
'GUI_LOCK_TIMEOUT': '9999999999999999999999999999999999999999999999999999',
'GUI_ORDER_CONFIRM': 'no',
'GUI_REFRESH_TIME': '2',
'GUI_WT_YDTS': 'yes',
'SET_MCJG': 'empty',
'SET_MCSL': 'empty',
'SET_MRJG': 'empty',
'SET_MRSL': 'empty',
'SET_NOTIFY_DELAY': '1',
'SET_POPUP_CJHB': 'yes',
# 'SET_TOP_MOST': 'yes',
# 'SYS_ZCSX_ENABLE': 'yes' # 自动刷新资产数据 修改无效,会强制恢复为no!
}
user32 = ctypes.windll.user32
def capture_popup(time_interval=0.5):
''' 弹窗截获 '''
def capture(time_interval):
while True:
# secs = random.uniform(remainder/2, remainder)
# 若在休眠期间心跳印记没被修改,则刷新页面并修改心跳印记
time.sleep(secs)
threading.Thread(
target=capture,
kwargs={'time_interval': time_interval},
name='capture_popup',
daemon=True).start()
def normalize(string: str, to_dict=False):
'''标准化输出交易数据'''
df = pd.read_csv(io.StringIO(string), sep='\t', dtype={'证券代码': str})
df.drop(columns=[x for x in df.columns if x not in COLNAMES], inplace=True)
df.columns = [COLNAMES.get(x) for x in df.columns]
if 'amount' in df.columns:
df['ratio'] = (df['amount'] / df['amount'].sum()).round(2)
return df.to_dict('list') if to_dict else df
def check_input_mode(h_edit, text='000001'):
"""获取 输入模式"""
user32.SendMessageW(h_edit, 12, 0, text)
time.sleep(0.3)
return 'WM' if user32.SendMessageW(h_edit, 14, 0, 0) == len(text) else 'KB'
def find_one(keyword='交易系统', visible=True) -> int:
'''找到主窗口句柄
keyword: str or list
visible: {True, False, None}
'''
if isinstance(keyword, str):
keyword = [keyword]
buf = ctypes.create_unicode_buffer(64)
handle = ctypes.c_ulong()
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
def callback(hwnd, lparam):
user32.GetWindowTextW(hwnd, buf, 64)
for s in keyword:
if s in buf.value and (
visible is None or bool(user32.IsWindowVisible(hwnd))) is visible:
handle.value = hwnd
return False
return True
user32.EnumWindows(callback)
return handle.value
def curr_time():
return time.strftime('%Y-%m-%d %X') # backward
def get_today():
return datetime.date.today()
def get_text(handle=None, h_parent=None, id_child=None, classname: str = None, num=32) -> str:
'获取控件文本内容'
buf = ctypes.create_unicode_buffer(64)
if isinstance(classname, str):
handle: int = user32.FindWindowExW(h_parent, None, classname, None)
if isinstance(handle, int):
user32.SendMessageW(handle, Msg.WM_GETTEXT, num, buf)
elif isinstance(id_child, int):
user32.SendDlgItemMessageW(
h_parent, id_child, Msg.WM_GETTEXT, num, buf)
return buf.value.rstrip('%')
def check_config(folder=None, encoding='gb18030'):
"""检查客户端xiadan.ini文件是否符合木偶的要求
"""
with open(''.join([folder or os.getcwd(), r'\xiadan.ini']), encoding=encoding) as f:
string = ''.join(('[puppet]\n', f.read()))
conf = configparser.ConfigParser()
conf.read_string(string)
section = conf['SYSTEM_SET']
print('推荐修改下列选项:')
for key, value in OPTIONS.items():
name, val = section.get(key).split(';')[3:5]
if val != value:
print(name, val, '改为', value, '\n')
def find_all():
'''获取全部已登录的客户端的根句柄'''
def find(hwnd, extra):
buf = ctypes.create_unicode_buffer(32)
if user32.IsWindowVisible(hwnd):
user32.GetWindowTextW(hwnd, buf, 32)
if '交易系统' in buf.value:
# h_acc = reduce(user32.GetDlgItem, (59392, 0, 1711), hwnd)
# user32.SendMessageW(h_acc, 13, 32, buf)
# extra.update({int(buf.value): hwnd})
extra.append(hwnd)
return True
accounts = []
WNDENUMPROC = ctypes.WINFUNCTYPE(BOOL, HWND, ctypes.py_object)
user32.EnumChildWindows.argtypes = [HWND, WNDENUMPROC, ctypes.py_object]
user32.EnumChildWindows(0, WNDENUMPROC(find), accounts)
return accounts
def find_single_handle(h_dialog, keyword: str = '', classname='Static') -> int:
from ctypes.wintypes import BOOL, HWND, LPARAM
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
def callback(hwnd, lparam):
user32.SendMessageW(hwnd, Msg.WM_GETTEXT, 64, buf)
user32.GetClassNameW(hwnd, buf1, 64)
if buf.value == keyword and buf1.value == classname:
handle.value = hwnd
return False
return True
buf = ctypes.create_unicode_buffer(64)
buf1 = ctypes.create_unicode_buffer(64)
handle = ctypes.c_ulong()
user32.EnumChildWindows(h_dialog, callback)
return handle.value
def go_to_top(h_root: int, timeout: float = 1.0, interval: float = 0.01):
"""窗口置顶"""
keyboard.send('alt') # ! enables calls to SwitchToThisWindow
if user32.SwitchToThisWindow(h_root, True) != 0:
stop = int(timeout/interval)
for _ in range(stop):
if user32.GetForegroundWindow() == h_root:
return True
time.sleep(0.5 if user32.IsIconic(h_root) else interval) # ! DON'T REMOVE!
def image_to_string(image, token={
'appId': '11645803',
'apiKey': 'RUcxdYj0mnvrohEz6MrEERqz',
'secretKey': '4zRiYambxQPD1Z5HFh9VOoPXPK9AgBtZ'}):
if not isinstance(image, bytes):
buf = io.BytesIO()
image.save(buf, 'png')
image = buf.getvalue()
return __import__('aip').AipOcr(**token).basicGeneral(image).get(
'words_result')[0]['words']
def locate_folder(name='Personal'):
"""Personal Recent
"""
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders')
return winreg.QueryValueEx(key, name)[0] # dir, type
def simulate_shortcuts(key1, key2=None):
"""
VK_CONTROL = 17
VK_ALT = 18
VK_S = 83
"""
KEYEVENTF_KEYUP = 2
scan1 = user32.MapVirtualKeyW(key1, 0)
user32.keybd_event(key1, scan1, 0, 0)
if key2:
scan2 = user32.MapVirtualKeyW(key2, 0)
user32.keybd_event(key2, scan2, 0, 0)
user32.keybd_event(key2, scan2, KEYEVENTF_KEYUP, 0)
user32.keybd_event(key1, scan1, KEYEVENTF_KEYUP, 0)
def click_key(self, keyCode, param=0): # 单击按键
if keyCode:
user32.PostMessageW(self._page, Msg.WM_KEYDOWN, keyCode, param)
user32.PostMessageW(self._page, Msg.WM_KEYUP, keyCode, param)
return self
def get_rect(obj_handle):
'''locate the control
'''
rect = ctypes.wintypes.RECT()
user32.GetWindowRect(obj_handle, ctypes.byref(rect))
return rect.left, rect.top, rect.right, rect.bottom
def click_context_menu(text: str, x: int = 0, y: int = 0, delay: float = 0.5):
'''点选右键菜单
'''
user32.SetCursorPos(x, y)
user32.mouse_event(Msg.MOUSEEVENTF_RIGHTDOWN | Msg.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)
time.sleep(delay)
keyboard.write(text)
def wait_for_popup(root: int, popup_title=None, timeout: float = 3.0, interval: float = 0.01):
buf = ctypes.create_unicode_buffer(64)
start = time.time()
while True:
time.sleep(interval) # DON'T REMOVE
h_popup: int = user32.GetLastActivePopup(root)
if h_popup != root:
user32.SendMessageW(h_popup, Msg.WM_GETTEXT, 64, buf)
if popup_title is None or buf.value in popup_title:
return h_popup
if time.time() - start >= timeout:
break
def wait_for_view(handle: int, timeout: float = 3, interval: float = 0.1):
for _ in range(int(timeout / interval)):
if user32.IsWindowVisible(handle):
return True
time.sleep(interval)
return False
def get_child_handle(h_parent=None, label=None, clsname=None, id_ctrl=None, visible=None):
'''获取子窗口句柄
h_parent: None 表示桌面
'''
assert isinstance(h_parent, int), 'Wrong data type.'
if isinstance(id_ctrl, int):
return user32.GetDlgItem(h_parent, id_ctrl)
hwndChildAfter = None
group = []
for _ in range(9):
handle: int = user32.FindWindowExW(h_parent, hwndChildAfter, clsname, label)
if visible is None or bool(user32.IsWindowVisible(handle)) is visible:
return handle
if handle in group:
break
group.append(handle)
hwndChildAfter = handle
def click_button(h_button=None, h_dialog=None, label=None):
if isinstance(h_dialog, int):
h_button = get_child_handle(h_dialog, label, 'Button')
assert isinstance(h_button, int), TypeError('Must be a integer!', h_button)
user32.PostMessageW(h_button, Msg.BM_CLICK, 0, 0)
def fill(text: str, h_edit: int):
'''填写字符串'''
assert isinstance(text, str), TypeError('Must be a string!', text)
assert isinstance(h_edit, int), TypeError('Must be a integer!', h_edit)
user32.SendMessageW(h_edit, Msg.WM_SETTEXT, 0, text)
def export_data(path: str, root=None, label='另存为', location=False):
if os.path.isfile(path):
os.remove(path)
if user32.IsIconic(root):
print('如果返回空值,请先查一下"order"或"deal",再查其他的。')
h_popup = wait_for_popup(root, label)
if wait_for_view(h_popup):
if location:
fill(path, get_child_handle(h_popup, clsname='Edit'))
time.sleep(0.1)
click_button(h_dialog=h_popup, label='保存(&S)')
string = ''
for _ in range(99):
time.sleep(0.05)
if os.path.isfile(path):
try:
with open(path) as f: # try to acquire file lock and read file content
string = f.read()
except Exception:
continue
else:
break
return string
def switch_combobox(index: int, handle: int):
user32.SendMessageW(handle, Msg.CB_SETCURSEL, index, 0)
time.sleep(0.5)
user32.SendMessageW(
user32.GetParent(handle), Msg.WM_COMMAND,
Msg.CBN_SELCHANGE << 16 | user32.GetDlgCtrlID(handle), handle)
def run_app(path: str):
"""运行应用程序"""
assert os.path.exists(path), '客户端路径("%s")错误' % path
# print('{} 正在尝试运行客户端("{}")...'.format(curr_time(), path))
pid = subprocess.Popen(path).pid
OpenProcess = ctypes.windll.kernel32.OpenProcess
CloseHandle = ctypes.windll.kernel32.CloseHandle
PROCESS_QUERY_INFORMATION = 1024
h_proc = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid)
ret = user32.WaitForInputIdle(h_proc, -1)
CloseHandle(h_proc)
if ret == 0:
return pid
================================================
FILE: setup.py
================================================
# -*- coding: utf-8 -*-
from setuptools import setup
from puppet import __version__ as VERSION
REQUIRED = ['baidu-aip', 'pydirectinput']
REQUIRES_PYTHON = '>=3.4.0'
setup(
name='puppet',
version=VERSION,
description=("一个用来交易沪深A股的应用编程接口"),
license='MIT',
author='Raytone-D',
author_email='raytone@qq.com',
url='https://github.com/Raytone-D/puppet',
keywords="stock TraderApi Quant",
python_requires=REQUIRES_PYTHON,
install_requires=REQUIRED,
packages=['puppet'],
# test_suite='tests',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Win32 (MS Windows)',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
)
================================================
FILE: strategies/buy_and_hold.py
================================================
from rqalpha.api import *
# 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。
def init(context):
logger.info("init")
context.s1 = "000001.XSHE"
update_universe(context.s1)
# 是否已发送了order
context.fired = False
def before_trading(context):
pass
# 你选择的证券的数据更新将会触发此段逻辑,例如日或分钟历史数据切片或者是实时数据切片更新
def handle_bar(context, bar_dict):
# 开始编写你的主要的算法逻辑
# bar_dict[order_book_id] 可以拿到某个证券的bar信息
# context.portfolio 可以拿到现在的投资组合状态信息
# 使用order_shares(id_or_ins, amount)方法进行落单
# TODO: 开始编写你的算法吧!
if not context.fired:
# order_percent并且传入1代表买入该股票并且使其占有投资组合的100%
order_percent(context.s1, 1)
context.fired = True
================================================
FILE: test.py
================================================
# -*- coding: utf-8 -*-
"木偶测试文件,请直接在命令行执行 python puppet\test.py"
if __name__ == '__main__':
import platform
import time
import puppet
print('\n{}\nPython Version: {}'.format(
platform.platform(), platform.python_version()))
print('默认使用百度云OCR进行验证码识别')
print("\n注意!必须将client_path的值修改为你自己的交易客户端路径!\n")
bdy = {
'appId': '',
'apiKey': '',
'secretKey': ''
} # 百度云 OCR https://cloud.baidu.com/product/ocr
accinfos = {
'account_no': '198800',
'password': '123456',
'comm_pwd': True, # 模拟交易端必须为True
'client_path': r'你的交易客户端目录\xiadan.exe'
}
# 自动登录交易客户端
# acc = puppet.login(accinfos)
# acc = puppet.Account(accinfos)
# 绑定已经登录的交易客户端,旧版需要传入 title=''
acc = puppet.Client()
print(acc.query())
r = acc.buy('510500', 4.688, 100)
print(r)
r = acc.query('order')
print(r)
time.sleep(5)
r = acc.cancel_buy('510550')
print(r)