├── LICENSE ├── Puppet使用说明书.txt ├── README.md ├── auto_trade_example.py ├── autologon.py ├── autologon_raffle.py ├── buy_and_hold.py ├── engine ├── README.md ├── __init__.py ├── broker.py ├── data_source.py ├── event_source.py ├── matcher.py ├── mod.py └── utils.py ├── multi_raffle.py ├── puppet4tdx └── puppet4tdx.py ├── puppet_v4.py ├── puppetrader_v0.3.5.py ├── release_puppet_unity_ths.py ├── v5 ├── test.md └── test.py ├── 图解同花顺客户端多账号一键打新.jpg └── 扯线木偶API使用说明.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 睿瞳深邃 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Puppet使用说明书.txt: -------------------------------------------------------------------------------- 1 | """ 强烈推荐 """ 2 | 3 | # 使用最新版本的扯线木偶,以避免一些运行逻辑的BUG。 4 | # 使用最新版本的Anaconda3,或者Python 3.5+。界面操作API能支持Python3.0+, 5 | # 因为后面的其他模块会使用asyncio(3.4)或者await/async(3.5+),因此不建议使用。 6 | # 按MSDN的API说明,WIN2000及以上版本都能正常使用,但注意了, windows xpsp3只能Python3.4以下,Python3.5+必须WIN7+。 7 | # 默认只实例化一个客户端(同花顺或者通达信),有多个客户端需求的,请参考multi_clients_test.py。 8 | # 00开头的股票需要用字符串。 9 | 10 | """ 使用说明 """ 11 | # test.py是测试单账户的可用性。 12 | # multi_test.py是测试多个账户的可用性。 13 | # multi_raffle.py是多账户打新专用脚本,双击即可完成打新。 14 | 15 | """ 同花顺独立交易客户端 """ 16 | 17 | # 由于同花顺理念的先进性,任何版本的同花顺都能正常被Puppet所调用。 18 | # 同花顺是全后台调用的,只要登录后最小化即可,无需使用专用的主机或者桌面。 19 | 20 | 21 | """ 通达信金融终端 """ 22 | 23 | # 暂不支持通达信独立交易端。 24 | # 暂不支持招商【行情+交易】模式登录的客户端。 25 | # 由于通达信交易客户端使用前置交易模式,请使用专用的虚拟桌面(比如Win10的虚拟桌面),以免干扰操作。 26 | # 通达信金融终端(包括各种券商定制版)的交易窗口不能被遮挡,不能最小化,会导致程序抛出异常。 27 | # 可能有些券商的独有的登录模式无法正常匹配,请发电邮或者在github通知我。 28 | # 节点的二次索引将各个券商杂乱的节点布局影射为委托0,撤单1,资金股份2,当日成交3,新股申购4,中签查询5,批量申购6。 29 | # 如果交易端左侧的功能节点标签没配置在config.py的TAG/SUBTAG里面,请自行增加到对应的索引位置。 30 | # 广发证券查询中签,如果返回[],需要改动源码wait_a_second(0.5)。 31 | # 撤单可选股票代码symbol或者委托编号number。买卖方向默认“卖出”。 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 扯线木偶(puppet) 2 | == 3 | Puppet是一个基于商业免费软件(同花顺、通达信)构建而成的闭环的A股实盘交易框架。“目前”是在建项目(WIP)。 4 | -- 5 | 项目进度:界面操控API ->> 预警交互API 6 | 7 | 构建流程:界面操控API ->> 预警交互API ->> 信号推送API ->> 策略中枢API ->> 历史数据API ->> 回测模块API 8 | ********************************************************************************************************************************* 9 | 10 | 工作流程:手动登录客户端 --> 运行扯线木偶 --> 自动搜索已登录的客户端 --> 交易前的预备 --> 自动获取持仓数据 --> 查询预警名单 --> 客户端待命状态 11 | 12 | **推荐使用最新版的Anaconda3,或者Python 3.5+。系统要求:Windows平台,Win2000+;Linux平台,安装最新的WineHQ,环境设为WIN7。** 13 | 14 | 第三方库依赖:pyperclip(仅限于同花顺), pywinauto(仅限于通达信) 15 | 16 | 界面操控API 17 | ********************************************************************************************************************************* 18 | * method: '买入': buy(), '卖出': sell(), '撤单': cancel(), '打新': raffle(), '下单': order() 19 | 20 | * property: '可用余额': balance, '持仓': position, '成交': deals, '可撤委托': cancelable, '新股': new, '中签': bingo, '帐号': account 21 | ********************************************************************************************************************************* 22 | 23 | * 目前已知查询中签bingo只适用于部分券商!请留意。 24 | * 招商证券只测试过最新版能独立交易模式登录使用!辣鸡定制版不会增加任何支持了。国金、中信通达信据反馈资金明细不兼容。 25 | * 暂不支持融资融券! 26 | * 同花顺交易端:无任何限制!官方统一版或老版、券商定制版(银河、国泰君安、华泰、广发、东方财富等)。 27 | * 通达信交易端:无任何限制!目前不支持独立交易端。 28 | * 多账户同时交易:完全支持!同一券商或多个券商。 29 | * 注意:暂不支持一个交易端通过“添加”同一券商多个账户同时交易,只能交易当前的那一个账户。 30 | 31 | ### 更新 32 | 33 | 2017/4/18 修复自动登录的逻辑错误,现在能从单帐号自动切换到多帐号了。 34 | 35 | 2017/4/15 更新至v0.4.8,增加支持同花顺官方交易客户端“多账户”登录模式下多个券商帐号的切换。增加autologon.py, multi_raffle.py, autologon_raffle.py, “图解同花顺多账户一键打新.PDF”。 36 | 37 | 2017/4/9 更新"扯线木偶API使用说明",主要是说明参数的用法。 38 | 39 | 2017/4/6 更新至v0.4.7,改善raffle()的兼容性,不支持银河证券的同花顺客户端打新,只能用同花顺官方的交易端打新。 40 | 41 | 2017/4/4 通达信版改一个控件代码,支持招商证券独立交易模式登录。 42 | 43 | 2017/4/2 更新至v0.4.6,增加bingo中签查询。 44 | 45 | 2017/4/1 更新至v0.4.5,修复了一个愚蠢的错误:symbol[0].startswith('')返回True,导致不打新股,一脸懵逼! 46 | 47 | 2017/3/28 更新至v0.4.4,支持buy()/sell()直接输数字下单,无需字符串。 48 | 49 | 2017/3/10 更新至v0.4.3,优化输出效果,更友好。 50 | 51 | 2017/3/10 更新至v0.4.2,raffle增加skip参数,跳过指定的市场新股。 52 | 53 | 2017/3/10 更新到v0.4.1,小幅修改,部分优化,默认改为单交易客户端模式。 54 | 55 | 2017/3/9 v0.4版发布!增加一键打新(raffle)、查新股(new)功能。大幅度修改优化,强化拟人化操作逻辑。 56 | 57 | 2017/2/23 V0.3.5发布!小幅修改,改善操作流畅度。 58 | 59 | 2017/2/22 v0.3发布!优化模拟人手交易的流程。 60 | 61 | 2017/2/21 v0.2.5发布!增加撤单(指定股票代码)功能。 62 | 63 | 2017/2/14 v0.2版发布!提供后台获取持仓数据。鸣谢网友liuyukuan博文中提供的AHK代码“SendMessage,0x111,57634,0,CVirtualGridCtrl2,同花顺”。 64 | 65 | Windows下不需要安装、配置。 66 | 67 | Linux下需要安装最新版本的Wine,环境设为Windows 7,先安装同花顺交易客户端,能正常使用之后再安装Python for Windows。启动wineconsole,pip install pyperclip,之后就可以正常使用了。 68 | -------------------------------------------------------------------------------- /auto_trade_example.py: -------------------------------------------------------------------------------- 1 | from engine import run 2 | 3 | basConfig = { 4 | "strategy_file": "./buy_and_hold.py", 5 | "start_date": "2016-06-01", 6 | "end_date": "2016-12-01", 7 | "stock_starting_cash": 100000, 8 | "benchmark": "000300.XSHG", 9 | } 10 | 11 | run(baseConfig) 12 | -------------------------------------------------------------------------------- /autologon.py: -------------------------------------------------------------------------------- 1 | """ 2 | # autologon.py 3 | # 目前仅支持同花顺官方的独立交易端的“多帐号”登录模式。 4 | """ 5 | __author__ = '睿瞳深邃' 6 | __version__ = '0.2' 7 | 8 | # coding: utf-8 9 | import os 10 | import subprocess 11 | import time 12 | import ctypes 13 | 14 | api = ctypes.windll.user32 15 | 16 | def autologon(target=None): 17 | " 自动登录同花顺独立交易客户端 " 18 | path = os.path.split(os.path.realpath(__file__))[0] 19 | for lnk in os.listdir(path): 20 | if target in lnk: 21 | subprocess.Popen(os.path.join(path, lnk), shell=True) 22 | for i in range(10): 23 | main = api.FindWindowW(0, '网上股票交易系统5.0') 24 | if not main: 25 | time.sleep(1) 26 | else: break 27 | if not api.IsWindowVisible(main): 28 | popup = api.GetLastActivePopup(main) 29 | logon = api.GetDlgItem(popup, 1015) # 一键登录按钮 30 | for i in range(10): 31 | if not api.IsWindowVisible(logon): 32 | api.PostMessageW(popup, 273, 1014, api.GetDlgItem(popup, 1014)) 33 | time.sleep(0.2) 34 | api.PostMessageW(popup, 273, 1015, logon) 35 | 36 | if __name__ == '__main__': 37 | 38 | autologon('同花顺交易.lnk') 39 | -------------------------------------------------------------------------------- /autologon_raffle.py: -------------------------------------------------------------------------------- 1 | """ 2 | # 多账户打新专用脚本,支持v4+版本 3 | # myRegister暂时没用上。暂时只支持同花顺交易端 4 | """ 5 | # coding: utf-8 6 | import time 7 | import os 8 | import subprocess 9 | import ctypes 10 | 11 | from puppet_v4 import Puppet, switch_combo 12 | from autologon import autologon 13 | 14 | api = ctypes.windll.user32 15 | buff = ctypes.create_unicode_buffer(32) 16 | team = set() 17 | 18 | def find(keyword): 19 | """ 枚举所有已登录的交易端 """ 20 | @ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p) 21 | def check(hwnd, keyword): 22 | """ 筛选 """ 23 | if api.IsWindowVisible(hwnd)\ 24 | and api.GetWindowTextW(hwnd, buff, 32) > 6 and keyword in buff.value: 25 | team.add(hwnd) 26 | return 1 27 | for i in range(10): 28 | api.EnumWindows(check, keyword) 29 | time.sleep(3) 30 | if team: 31 | break 32 | return {Puppet(main) for main in team} 33 | 34 | if __name__ == '__main__': 35 | 36 | myRegister = {'券商登录号': '自定义名称', 37 | '617145470': '东方不败', 38 | '20941552121212': '西门吹雪'} # 交易端的登录帐号及昵称。 39 | keyword = '网上股票交易' 40 | 41 | autologon(target='同花顺交易') 42 | traders = find(keyword) 43 | for x in traders: 44 | popup = api.GetLastActivePopup(x.main) 45 | if popup: 46 | api.PostMessageW(popup, 273, 2, api.GetDlgItem(popup, 2)) 47 | if api.IsWindowVisible(x.combo): 48 | for i in range(x.count): 49 | switch_combo(i, 2322, x.combo) 50 | #print(x.account) 51 | #print(x.new) 52 | time.sleep(1) 53 | x.raffle() 54 | -------------------------------------------------------------------------------- /buy_and_hold.py: -------------------------------------------------------------------------------- 1 | from rqalpha.api import * 2 | 3 | 4 | # 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。 5 | def init(context): 6 | logger.info("init") 7 | context.s1 = "000001.XSHE" 8 | update_universe(context.s1) 9 | # 是否已发送了order 10 | context.fired = False 11 | 12 | 13 | def before_trading(context): 14 | pass 15 | 16 | 17 | # 你选择的证券的数据更新将会触发此段逻辑,例如日或分钟历史数据切片或者是实时数据切片更新 18 | def handle_bar(context, bar_dict): 19 | # 开始编写你的主要的算法逻辑 20 | 21 | # bar_dict[order_book_id] 可以拿到某个证券的bar信息 22 | # context.portfolio 可以拿到现在的投资组合状态信息 23 | 24 | # 使用order_shares(id_or_ins, amount)方法进行落单 25 | 26 | # TODO: 开始编写你的算法吧! 27 | if not context.fired: 28 | # order_percent并且传入1代表买入该股票并且使其占有投资组合的100% 29 | order_percent(context.s1, 1) 30 | context.fired = True 31 | -------------------------------------------------------------------------------- /engine/README.md: -------------------------------------------------------------------------------- 1 | # Simple A Stock Realtime Trade Mod 2 | 使用该Mod可以接收实时行情进行触发。用于 RQAlpha 实时模拟交易,实盘交易。 3 | 4 | 这个是一个初级的DEMO。 5 | 6 | 使用`--run-type`或者`-rt`为`p`(PaperTrading),就可以激活改 mod。 7 | 8 | ``` 9 | rqalpha run -fq 1m -rt p -f ~/tmp/test_a.py -sc 100000 -l verbose 10 | ``` 11 | -------------------------------------------------------------------------------- /engine/__init__.py: -------------------------------------------------------------------------------- 1 | import rqalpha 2 | 3 | config = { 4 | "extra": { 5 | "log_level": "verbose", 6 | }, 7 | "mod": { 8 | "live_trade": { 9 | "lib": "./mod", 10 | "enabled": True, 11 | "priority": 100, 12 | } 13 | } 14 | } 15 | 16 | def run(baseConf): 17 | config["base"] = baseConf 18 | return rqalpha.run(config) 19 | -------------------------------------------------------------------------------- /engine/broker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import jsonpickle 18 | 19 | from rqalpha.interface import AbstractBroker, Persistable 20 | from rqalpha.utils import get_account_type 21 | from rqalpha.utils.i18n import gettext as _ 22 | from rqalpha.events import EVENT 23 | from rqalpha.const import MATCHING_TYPE, ORDER_STATUS 24 | from rqalpha.const import ACCOUNT_TYPE 25 | from rqalpha.environment import Environment 26 | from rqalpha.model.account import BenchmarkAccount, StockAccount, FutureAccount 27 | 28 | from .matcher import Matcher 29 | 30 | 31 | def init_accounts(env): 32 | accounts = {} 33 | config = env.config 34 | start_date = config.base.start_date 35 | total_cash = 0 36 | for account_type in config.base.account_list: 37 | if account_type == ACCOUNT_TYPE.STOCK: 38 | stock_starting_cash = config.base.stock_starting_cash 39 | accounts[ACCOUNT_TYPE.STOCK] = StockAccount(env, stock_starting_cash, start_date) 40 | total_cash += stock_starting_cash 41 | elif account_type == ACCOUNT_TYPE.FUTURE: 42 | future_starting_cash = config.base.future_starting_cash 43 | accounts[ACCOUNT_TYPE.FUTURE] = FutureAccount(env, future_starting_cash, start_date) 44 | total_cash += future_starting_cash 45 | else: 46 | raise NotImplementedError 47 | if config.base.benchmark is not None: 48 | accounts[ACCOUNT_TYPE.BENCHMARK] = BenchmarkAccount(env, total_cash, start_date) 49 | 50 | return accounts 51 | 52 | 53 | class Broker(AbstractBroker, Persistable): 54 | def __init__(self, env): 55 | self._env = env 56 | if env.config.base.matching_type == MATCHING_TYPE.CURRENT_BAR_CLOSE: 57 | self._matcher = Matcher(lambda bar: bar.close, env.config.validator.bar_limit) 58 | self._match_immediately = True 59 | else: 60 | self._matcher = Matcher(lambda bar: bar.open, env.config.validator.bar_limit) 61 | self._match_immediately = False 62 | 63 | self._accounts = None 64 | self._open_orders = [] 65 | self._board = None 66 | self._turnover = {} 67 | self._delayed_orders = [] 68 | self._frontend_validator = {} 69 | 70 | # 该事件会触发策略的before_trading函数 71 | self._env.event_bus.add_listener(EVENT.BEFORE_TRADING, self.before_trading) 72 | # 该事件会触发策略的handle_bar函数 73 | self._env.event_bus.add_listener(EVENT.BAR, self.bar) 74 | # 该事件会触发策略的handel_tick函数 75 | self._env.event_bus.add_listener(EVENT.TICK, self.tick) 76 | # 该事件会触发策略的after_trading函数 77 | self._env.event_bus.add_listener(EVENT.AFTER_TRADING, self.after_trading) 78 | 79 | def get_accounts(self): 80 | if self._accounts is None: 81 | self._accounts = init_accounts(self._env) 82 | return self._accounts 83 | 84 | def get_open_orders(self): 85 | return self._open_orders 86 | 87 | def get_state(self): 88 | return jsonpickle.dumps([o.order_id for _, o in self._delayed_orders]).encode('utf-8') 89 | 90 | def set_state(self, state): 91 | delayed_orders = jsonpickle.loads(state.decode('utf-8')) 92 | for account in self._accounts.values(): 93 | for o in account.daily_orders.values(): 94 | if not o._is_final(): 95 | if o.order_id in delayed_orders: 96 | self._delayed_orders.append((account, o)) 97 | else: 98 | self._open_orders.append((account, o)) 99 | 100 | def _get_account_for(self, order_book_id): 101 | account_type = get_account_type(order_book_id) 102 | return self._accounts[account_type] 103 | 104 | def submit_order(self, order): 105 | account = self._get_account_for(order.order_book_id) 106 | 107 | self._env.event_bus.publish_event(EVENT.ORDER_PENDING_NEW, account, order) 108 | 109 | account.append_order(order) 110 | if order._is_final(): 111 | return 112 | 113 | # account.on_order_creating(order) 114 | if self._env.config.base.frequency == '1d' and not self._match_immediately: 115 | self._delayed_orders.append((account, order)) 116 | return 117 | 118 | self._open_orders.append((account, order)) 119 | order._active() 120 | self._env.event_bus.publish_event(EVENT.ORDER_CREATION_PASS, account, order) 121 | if self._match_immediately: 122 | self._match() 123 | 124 | def cancel_order(self, order): 125 | account = self._get_account_for(order.order_book_id) 126 | 127 | self._env.event_bus.publish_event(EVENT.ORDER_PENDING_CANCEL, account, order) 128 | 129 | # account.on_order_cancelling(order) 130 | order._mark_cancelled(_("{order_id} order has been cancelled by user.").format(order_id=order.order_id)) 131 | 132 | self._env.event_bus.publish_event(EVENT.ORDER_CANCELLATION_PASS, account, order) 133 | 134 | # account.on_order_cancellation_pass(order) 135 | try: 136 | self._open_orders.remove((account, order)) 137 | except ValueError: 138 | try: 139 | self._delayed_orders.remove((account, order)) 140 | except ValueError: 141 | pass 142 | 143 | def before_trading(self): 144 | for account, order in self._open_orders: 145 | order._active() 146 | self._env.event_bus.publish_event(EVENT.ORDER_CREATION_PASS, account, order) 147 | 148 | def after_trading(self): 149 | for account, order in self._open_orders: 150 | order._mark_rejected(_("Order Rejected: {order_book_id} can not match. Market close.").format( 151 | order_book_id=order.order_book_id 152 | )) 153 | self._env.event_bus.publish_event(EVENT.ORDER_UNSOLICITED_UPDATE, account, order) 154 | self._open_orders = self._delayed_orders 155 | self._delayed_orders = [] 156 | 157 | def bar(self, bar_dict): 158 | env = Environment.get_instance() 159 | self._matcher.update(env.calendar_dt, env.trading_dt, bar_dict) 160 | self._match() 161 | 162 | def tick(self, tick): 163 | # TODO support tick matching 164 | pass 165 | # env = Environment.get_instance() 166 | # self._matcher.update(env.calendar_dt, env.trading_dt, tick) 167 | # self._match() 168 | 169 | def _match(self): 170 | self._matcher.match(self._open_orders) 171 | final_orders = [(a, o) for a, o in self._open_orders if o._is_final()] 172 | self._open_orders = [(a, o) for a, o in self._open_orders if not o._is_final()] 173 | 174 | for account, order in final_orders: 175 | if order.status == ORDER_STATUS.REJECTED or order.status == ORDER_STATUS.CANCELLED: 176 | self._env.event_bus.publish_event(EVENT.ORDER_UNSOLICITED_UPDATE, account, order) 177 | -------------------------------------------------------------------------------- /engine/data_source.py: -------------------------------------------------------------------------------- 1 | import six 2 | import tushare as ts 3 | from datetime import date 4 | from dateutil.relativedelta import relativedelta 5 | from rqalpha.data.base_data_source import BaseDataSource 6 | 7 | 8 | class DataSource(BaseDataSource): 9 | def __init__(self, path): 10 | super(TushareKDataSource, self).__init__(path) 11 | 12 | @staticmethod 13 | def get_tushare_k_data(instrument, start_dt, end_dt): 14 | order_book_id = instrument.order_book_id 15 | code = order_book_id.split(".")[0] 16 | 17 | if instrument.type == 'CS': 18 | index = False 19 | elif instrument.type == 'INDX': 20 | index = True 21 | else: 22 | return None 23 | 24 | return ts.get_k_data(code, index=index, start=start_dt.strftime('%Y-%m-%d'), end=end_dt.strftime('%Y-%m-%d')) 25 | 26 | def get_bar(self, instrument, dt, frequency): 27 | if frequency != '1d': 28 | return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) 29 | 30 | bar_data = self.get_tushare_k_data(instrument, dt, dt) 31 | 32 | if bar_data is None or bar_data.empty: 33 | return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) 34 | else: 35 | return bar_data.iloc[0].to_dict() 36 | 37 | def history_bars(self, instrument, bar_count, frequency, fields, dt, skip_suspended=True): 38 | if frequency != '1d' or not skip_suspended: 39 | return super(TushareKDataSource, self).history_bars(instrument, bar_count, frequency, fields, dt, skip_suspended) 40 | 41 | start_dt_loc = self.get_trading_calendar().get_loc(dt.replace(hour=0, minute=0, second=0, microsecond=0)) - bar_count + 1 42 | start_dt = self.get_trading_calendar()[start_dt_loc] 43 | 44 | bar_data = self.get_tushare_k_data(instrument, start_dt, dt) 45 | 46 | if bar_data is None or bar_data.empty: 47 | return super(TushareKDataSource, self).get_bar(instrument, dt, frequency) 48 | else: 49 | if isinstance(fields, six.string_types): 50 | fields = [fields] 51 | fields = [field for field in fields if field in bar_data.columns] 52 | 53 | return bar_data[fields].as_matrix() 54 | 55 | def available_data_range(self, frequency): 56 | return date(2005, 1, 1), date.today() - relativedelta(days=1) 57 | 58 | -------------------------------------------------------------------------------- /engine/event_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import datetime 18 | import time 19 | from threading import Thread 20 | 21 | from six.moves.queue import Queue, Empty 22 | 23 | from rqalpha.interface import AbstractEventSource 24 | from rqalpha.environment import Environment 25 | from rqalpha.utils.logger import system_log 26 | from rqalpha.events import Event, EVENT 27 | from rqalpha.execution_context import ExecutionContext 28 | from rqalpha.utils import json as json_utils 29 | from .utils import get_realtime_quotes, order_book_id_2_tushare_code, is_holiday_today, is_tradetime_now 30 | 31 | 32 | class RealtimeEventSource(AbstractEventSource): 33 | 34 | def __init__(self, fps): 35 | self._env = Environment.get_instance() 36 | self.fps = fps 37 | self.event_queue = Queue() 38 | 39 | self.before_trading_fire_date = datetime.date(2000, 1, 1) 40 | self.after_trading_fire_date = datetime.date(2000, 1, 1) 41 | 42 | self.clock_engine_thread = Thread(target=self.clock_worker) 43 | self.clock_engine_thread.daemon = True 44 | 45 | self.quotation_engine_thread = Thread(target=self.quotation_worker) 46 | self.quotation_engine_thread.daemon = True 47 | 48 | def set_state(self, state): 49 | persist_dict = json_utils.convert_json_to_dict(state.decode('utf-8')) 50 | self.before_trading_fire_date = persist_dict['before_trading_fire_date'] 51 | self.after_trading_fire_date = persist_dict['after_trading_fire_date'] 52 | 53 | def get_state(self): 54 | return json_utils.convert_dict_to_json({ 55 | "before_trading_fire_date": self.before_trading_fire_date, 56 | "after_trading_fire_date": self.after_trading_fire_date, 57 | }).encode('utf-8') 58 | 59 | def quotation_worker(self): 60 | while True: 61 | if not is_holiday_today() and is_tradetime_now(): 62 | order_book_id_list = sorted(ExecutionContext.data_proxy.all_instruments("CS").order_book_id.tolist()) 63 | code_list = [order_book_id_2_tushare_code(code) for code in order_book_id_list] 64 | 65 | try: 66 | self._env.data_source.realtime_quotes_df = get_realtime_quotes(code_list) 67 | except Exception as e: 68 | system_log.exception("get_realtime_quotes fail") 69 | continue 70 | 71 | time.sleep(1) 72 | 73 | def clock_worker(self): 74 | while True: 75 | # wait for the first data ready 76 | if not self._env.data_source.realtime_quotes_df.empty: 77 | break 78 | time.sleep(0.1) 79 | 80 | while True: 81 | time.sleep(self.fps) 82 | 83 | if is_holiday_today(): 84 | time.sleep(60) 85 | continue 86 | 87 | dt = datetime.datetime.now() 88 | 89 | if dt.strftime("%H:%M:%S") >= "08:30:00" and dt.date() > self.before_trading_fire_date: 90 | self.event_queue.put((dt, EVENT.BEFORE_TRADING)) 91 | self.before_trading_fire_date = dt.date() 92 | elif dt.strftime("%H:%M:%S") >= "15:10:00" and dt.date() > self.after_trading_fire_date: 93 | self.event_queue.put((dt, EVENT.AFTER_TRADING)) 94 | self.after_trading_fire_date = dt.date() 95 | 96 | if is_tradetime_now(): 97 | self.event_queue.put((dt, EVENT.BAR)) 98 | 99 | def events(self, start_date, end_date, frequency): 100 | running = True 101 | 102 | self.clock_engine_thread.start() 103 | self.quotation_engine_thread.start() 104 | 105 | while running: 106 | real_dt = datetime.datetime.now() 107 | while True: 108 | try: 109 | dt, event_type = self.event_queue.get(timeout=1) 110 | break 111 | except Empty: 112 | continue 113 | 114 | system_log.debug("real_dt {}, dt {}, event {}", real_dt, dt, event_type) 115 | yield Event(event_type, real_dt, dt) 116 | -------------------------------------------------------------------------------- /engine/matcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from collections import defaultdict 18 | 19 | from rqalpha.utils.i18n import gettext as _ 20 | from rqalpha.const import ORDER_TYPE, SIDE, BAR_STATUS 21 | from rqalpha.model.trade import Trade 22 | from rqalpha.environment import Environment 23 | from rqalpha.events import EVENT 24 | 25 | 26 | class Matcher(object): 27 | def __init__(self, 28 | deal_price_decider, 29 | bar_limit=True, 30 | volume_percent=0.25): 31 | self._board = None 32 | self._turnover = defaultdict(int) 33 | self._calendar_dt = None 34 | self._trading_dt = None 35 | self._deal_price_decider = deal_price_decider 36 | self._volume_percent = volume_percent 37 | self._bar_limit = bar_limit 38 | 39 | def update(self, calendar_dt, trading_dt, bar_dict): 40 | self._board = bar_dict 41 | self._turnover.clear() 42 | self._calendar_dt = calendar_dt 43 | self._trading_dt = trading_dt 44 | 45 | def match(self, open_orders): 46 | for account, order in open_orders: 47 | slippage_decider = account.slippage_decider 48 | commission_decider = account.commission_decider 49 | tax_decider = account.tax_decider 50 | 51 | bar = self._board[order.order_book_id] 52 | bar_status = bar._bar_status 53 | 54 | if bar_status == BAR_STATUS.ERROR: 55 | listed_date = bar.instrument.listed_date.date() 56 | if listed_date == self._trading_dt.date(): 57 | reason = _("Order Cancelled: current security [{order_book_id}] can not be traded in listed date [{listed_date}]").format( 58 | order_book_id=order.order_book_id, 59 | listed_date=listed_date, 60 | ) 61 | else: 62 | reason = _("Order Cancelled: current bar [{order_book_id}] miss market data.").format( 63 | order_book_id=order.order_book_id) 64 | order._mark_rejected(reason) 65 | continue 66 | 67 | deal_price = self._deal_price_decider(bar) 68 | if order.type == ORDER_TYPE.LIMIT: 69 | if order.price > bar.limit_up: 70 | reason = _( 71 | "Order Rejected: limit order price {limit_price} is higher than limit up {limit_up}." 72 | ).format( 73 | limit_price=order.price, 74 | limit_up=bar.limit_up 75 | ) 76 | order._mark_rejected(reason) 77 | continue 78 | 79 | if order.price < bar.limit_down: 80 | reason = _( 81 | "Order Rejected: limit order price {limit_price} is lower than limit down {limit_down}." 82 | ).format( 83 | limit_price=order.price, 84 | limit_down=bar.limit_down 85 | ) 86 | order._mark_rejected(reason) 87 | continue 88 | 89 | if order.side == SIDE.BUY and order.price < deal_price: 90 | continue 91 | if order.side == SIDE.SELL and order.price > deal_price: 92 | continue 93 | else: 94 | if self._bar_limit and order.side == SIDE.BUY and bar_status == BAR_STATUS.LIMIT_UP: 95 | reason = _( 96 | "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." 97 | ).format(order_book_id=order.order_book_id) 98 | order._mark_rejected(reason) 99 | continue 100 | elif self._bar_limit and order.side == SIDE.SELL and bar_status == BAR_STATUS.LIMIT_DOWN: 101 | reason = _( 102 | "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." 103 | ).format(order_book_id=order.order_book_id) 104 | order._mark_rejected(reason) 105 | continue 106 | 107 | if self._bar_limit: 108 | if order.side == SIDE.BUY and bar_status == BAR_STATUS.LIMIT_UP: 109 | continue 110 | if order.side == SIDE.SELL and bar_status == BAR_STATUS.LIMIT_DOWN: 111 | continue 112 | 113 | volume_limit = round(bar.volume * self._volume_percent) - self._turnover[order.order_book_id] 114 | round_lot = bar.instrument.round_lot 115 | volume_limit = (volume_limit // round_lot) * round_lot 116 | if volume_limit <= 0: 117 | if order.type == ORDER_TYPE.MARKET: 118 | reason = _('Order Cancelled: market order {order_book_id} volume {order_volume}' 119 | ' due to volume limit').format( 120 | order_book_id=order.order_book_id, 121 | order_volume=order.quantity 122 | ) 123 | order._mark_cancelled(reason) 124 | continue 125 | 126 | unfilled = order.unfilled_quantity 127 | fill = min(unfilled, volume_limit) 128 | ct_amount = account.portfolio.positions[order.order_book_id]._cal_close_today_amount(fill, order.side) 129 | price = slippage_decider.get_trade_price(order, deal_price) 130 | trade = Trade.__from_create__(order=order, calendar_dt=self._calendar_dt, trading_dt=self._trading_dt, 131 | price=price, amount=fill, close_today_amount=ct_amount) 132 | trade._commission = commission_decider.get_commission(trade) 133 | trade._tax = tax_decider.get_tax(trade) 134 | order._fill(trade) 135 | self._turnover[order.order_book_id] += fill 136 | 137 | Environment.get_instance().event_bus.publish_event(EVENT.TRADE, account, trade) 138 | 139 | if order.type == ORDER_TYPE.MARKET and order.unfilled_quantity != 0: 140 | reason = _( 141 | "Order Cancelled: market order {order_book_id} volume {order_volume} is" 142 | " larger than 25 percent of current bar volume, fill {filled_volume} actually" 143 | ).format( 144 | order_book_id=order.order_book_id, 145 | order_volume=order.quantity, 146 | filled_volume=order.filled_quantity 147 | ) 148 | order._mark_cancelled(reason) 149 | -------------------------------------------------------------------------------- /engine/mod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from rqalpha.interface import AbstractMod 18 | from rqalpha.utils.disk_persist_provider import DiskPersistProvider 19 | from rqalpha.const import RUN_TYPE, PERSIST_MODE 20 | 21 | from .data_source import DataSource 22 | from .event_source import RealtimeEventSource 23 | from .simulation_broker import Broker 24 | 25 | 26 | class RealtimeTradeMod(AbstractMod): 27 | 28 | def start_up(self, env, mod_config): 29 | 30 | if env.config.base.run_type == RUN_TYPE.PAPER_TRADING: 31 | env.set_data_source(DataSource(env.config.base.data_bundle_path)) 32 | env.set_event_source(RealtimeEventSource(mod_config.fps)) 33 | env.set_broker(Broker(env)) 34 | 35 | persist_provider = DiskPersistProvider(mod_config.persist_path) 36 | env.set_persist_provider(persist_provider) 37 | 38 | env.config.base.persist = True 39 | env.config.base.persist_mode = PERSIST_MODE.REAL_TIME 40 | 41 | def tear_down(self, code, exception=None): 42 | pass 43 | 44 | 45 | def load_mod(): 46 | return RealtimeTradeMod() 47 | -------------------------------------------------------------------------------- /engine/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import math 18 | import time 19 | import datetime 20 | try: 21 | from functools import lru_cache 22 | except Exception as e: 23 | from fastcache import lru_cache 24 | 25 | from six.moves import reduce 26 | 27 | from rqalpha.environment import Environment 28 | from rqalpha.utils.datetime_func import convert_dt_to_int 29 | 30 | 31 | def is_holiday_today(): 32 | today = datetime.date.today() 33 | df = Environment.get_instance().data_proxy.get_trading_dates(today, today) 34 | 35 | return len(df) == 0 36 | 37 | 38 | def is_tradetime_now(): 39 | now_time = time.localtime() 40 | now = (now_time.tm_hour, now_time.tm_min, now_time.tm_sec) 41 | if (9, 15, 0) <= now <= (11, 30, 0) or (13, 0, 0) <= now <= (15, 0, 0): 42 | return True 43 | return False 44 | 45 | 46 | TUSHARE_CODE_MAPPING = { 47 | "sh": "000001.XSHG", 48 | "sz": "399001.XSHE", 49 | "sz50": "000016.XSHG", 50 | "hs300": "000300.XSHG", 51 | "sz500": "000905.XSHG", 52 | "zxb": "399005.XSHE", 53 | "cyb": "399006.XSHE", 54 | } 55 | 56 | 57 | def tushare_code_2_order_book_id(code): 58 | try: 59 | return TUSHARE_CODE_MAPPING[code] 60 | except KeyError: 61 | if code.startswith("6"): 62 | return "{}.XSHG".format(code) 63 | elif code[0] in ["3", "0"]: 64 | return "{}.XSHE".format(code) 65 | else: 66 | raise RuntimeError("Unknown code") 67 | 68 | 69 | def order_book_id_2_tushare_code(order_book_id): 70 | return order_book_id.split(".")[0] 71 | 72 | 73 | def get_realtime_quotes(code_list, open_only=False): 74 | import tushare as ts 75 | 76 | max_len = 800 77 | loop_cnt = int(math.ceil(float(len(code_list)) / max_len)) 78 | 79 | total_df = reduce(lambda df1, df2: df1.append(df2), 80 | [ts.get_realtime_quotes([code for code in code_list[i::loop_cnt]]) 81 | for i in range(loop_cnt)]) 82 | total_df["is_index"] = False 83 | 84 | index_symbol = ["sh", "sz", "hs300", "sz50", "zxb", "cyb"] 85 | index_df = ts.get_realtime_quotes(index_symbol) 86 | index_df["code"] = index_symbol 87 | index_df["is_index"] = True 88 | total_df = total_df.append(index_df) 89 | total_df = total_df.set_index("code").sort_index() 90 | 91 | columns = set(total_df.columns) - set(["name", "time", "date"]) 92 | # columns = filter(lambda x: "_v" not in x, columns) 93 | for label in columns: 94 | total_df[label] = total_df[label].map(lambda x: 0 if str(x).strip() == "" else x) 95 | total_df[label] = total_df[label].astype(float) 96 | 97 | total_df["chg"] = total_df["price"] / total_df["pre_close"] - 1 98 | 99 | total_df["order_book_id"] = total_df.index 100 | total_df["order_book_id"] = total_df["order_book_id"].apply(tushare_code_2_order_book_id) 101 | 102 | total_df["datetime"] = total_df["date"] + " " + total_df["time"] 103 | total_df["datetime"] = total_df["datetime"].apply(lambda x: convert_dt_to_int(datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S"))) 104 | 105 | total_df["close"] = total_df["price"] 106 | 107 | if open_only: 108 | total_df = total_df[total_df.open > 0] 109 | 110 | return total_df 111 | -------------------------------------------------------------------------------- /multi_raffle.py: -------------------------------------------------------------------------------- 1 | """ 2 | # 多账户打新专用脚本 支持v4+版本 3 | # myRegister暂时没用上。暂时只支持同花顺交易端 4 | """ 5 | __author__ = '睿瞳深邃' 6 | __version__ = '0.1' 7 | 8 | # coding: utf-8 9 | from puppet_v4 import Puppet 10 | import ctypes 11 | api = ctypes.windll.user32 12 | 13 | buff = ctypes.create_unicode_buffer(32) 14 | team = set() 15 | def find(keyword): 16 | """ 枚举所有已登录的交易端 """ 17 | @ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p) 18 | def check(hwnd, keyword): 19 | """ 筛选 """ 20 | if api.IsWindowVisible(hwnd)\ 21 | and api.GetWindowTextW(hwnd, buff, 32) > 6 and keyword in buff.value: 22 | team.add(hwnd) 23 | return 1 24 | api.EnumWindows(check, keyword) 25 | return {Puppet(main) for main in team} 26 | 27 | myRegister = {'券商登录号': '自定义名称', 28 | '617145470': '东方不败', 29 | '20941552121212': '西门吹雪'} # 交易端的登录帐号及昵称。 30 | keyword = '网上股票交易' 31 | 32 | traders = find(keyword) 33 | for x in traders: 34 | print(x.account) 35 | #print(x.new) 36 | x.raffle() 37 | -------------------------------------------------------------------------------- /puppet4tdx/puppet4tdx.py: -------------------------------------------------------------------------------- 1 | """ 2 | # the wrapper of A-shares local tdx client 3 | """ 4 | __project__ = 'Puppet' 5 | __author__ = "睿瞳深邃(https://github.com/Raytone-D" 6 | __version__ = "Fools' Day" 7 | 8 | import ctypes 9 | from functools import reduce 10 | import time 11 | import pywinauto 12 | 13 | WM_SETTEXT = 12 14 | WM_GETTEXT = 13 15 | WM_SETCHECK = 241 16 | WM_CLICK = 245 17 | WM_KEYDOWN = 256 18 | WM_KEYUP = 257 19 | WM_COMMAND = 273 20 | 21 | class Tdx: 22 | """ 通达信常量定义 """ 23 | # 节点索引:0: '委托', 1: '撤单', 2: '资金股份', 3: '当日成交', 4: '新股申购', 5: '中签查询', 6: '批量申购', 24 | INIT = 'updown' 25 | GRID = 'SysListView32' 26 | CLS = 'TdxW_MainFrame_Class' 27 | TAG = ['对买对卖', '双向委托', '各种交易', 28 | '撤单', '撤单[F3]', 29 | '查询', '资金股份', 30 | '当日成交', 31 | '新股申购', 32 | '中签查询'] # 节点标签 33 | SUBTAG = ['资金股份', '资金股份F4', 34 | '当日成交', '当日成交查询', 35 | '新股申购', 36 | '中签查询', '新股中签缴款', 37 | '新股批量申购'] # 子节点标签。 38 | CUSTOM = {'招商定制': 'msctls_updown32', 39 | '定制1': '', 40 | 'off': 1157} # 交易面板定位。 41 | BUY = {'代码': 12005, 42 | '价格': 12006, 43 | '全部': 1495, 44 | '数量': 12007, 45 | '下单': 2010} 46 | SELL = {'代码': 2025, 47 | '价格': 12039, 48 | '全部': 3075, 49 | '数量': 3030, 50 | '下单': 3032} 51 | CANCEL = {'撤单': 1136, 52 | '全选': 14} 53 | NEW = {'全部': 2203, 54 | '申购': 11786, 55 | '弹窗': '新股申购确认', 56 | '确认': 7015} # '新股代码': 12023,'申购价格': 12024,'最大可申': 2202,'申购数量': 12025, 57 | BATCH = {'申购': 39004, 58 | '弹窗': '新股组合申购确认', 59 | '确认': 7015} # 批量申购 60 | BINGO = {'查询': 1140} 61 | PATH = (59648, 0, 0, 59648, 59649, 0) 62 | 63 | api = ctypes.windll.user32 64 | buff = ctypes.create_unicode_buffer(96) 65 | 66 | def confirm_popup(idButton=7015, title='提示'): 67 | """ 确认弹窗 """ 68 | time.sleep(0.5) 69 | popup = api.FindWindowW(0, title) or api.FindWindowW(0, '提示') 70 | if api.IsWindowVisible(popup): 71 | api.PostMessageW(popup, WM_COMMAND, idButton, 72 | api.GetDlgItem(popup, idButton)) 73 | return True 74 | else: 75 | print('没找到弹窗orz') 76 | return False 77 | 78 | class Puppet: 79 | """ 80 | # method: '委买': buy(), '委卖': sell(), '撤单': cancel(), '打新': raffle(), '下单': order(), 81 | # property: '帐号': account, '持仓': position, '可用余额': balance, '成交': deals, 82 | # '可撤委托': cancelable, '新股': new, '中签': bingo 83 | """ 84 | def __init__(self, main=None, clsName='TdxW_MainFrame_Class'): 85 | self.main = pywinauto.findwindows.find_window(class_name=clsName) 86 | app = pywinauto.Application().connect(handle=self.main) 87 | self._client = app.window(handle=self.main) 88 | self.tv = self._client['SysTreeView32'] 89 | tag = [x.text() for x in self.tv.roots()] # 节点标签 90 | self.tag = [x for x in Tdx.TAG if x in tag] # 筛选 91 | self.tv.item(r'\对买对卖').click_input() 92 | self._trade = api.GetParent(self._client[Tdx.INIT]) 93 | self.account = "暂不可用:(" 94 | 95 | def _get_data(self, on=1): 96 | ''' 通达信SysListView32 ''' 97 | lv = self._client['SysListView32'] 98 | time.sleep(0.5) 99 | if on: 100 | raw = [x.text() for x in lv.items()] 101 | return list(zip(*[iter(raw)] * lv.column_count())) 102 | api.GetDlgItemTextW(api.GetParent(lv.handle), 1576, buff, 96) 103 | return dict([x.split(':') for x in buff.value.strip().split(' ')]) 104 | 105 | def order(self, symbol, price, qty=0, way='买入'): 106 | """ 通达信下单 """ 107 | self.tv.item(r'\对买对卖').click_input() 108 | _parts = Tdx.BUY if way == '买入' else Tdx.SELL 109 | if qty == 0: # 暂时不可用。 110 | print("全仓{0}".format(way)) 111 | print('{0:>>8} {1}, {2}, {3}'.format(way, symbol, price, qty)) 112 | print('限价委托') if price else print('市价委托') 113 | time.sleep(0.5) # 必须滴。 114 | api.SendMessageW(api.GetDlgItem(self._trade, _parts['代码']), WM_SETTEXT, 0, str(symbol)) 115 | api.SendMessageW(api.GetDlgItem(self._trade, _parts['价格']), WM_SETTEXT, 0, str(price)) 116 | api.SendMessageW(api.GetDlgItem(self._trade, _parts['数量']), WM_SETTEXT, 0, str(qty)) 117 | time.sleep(0.5) 118 | api.PostMessageW(self._trade, WM_COMMAND, _parts['下单'], 119 | api.GetDlgItem(self._trade, _parts['下单'])) 120 | while True: 121 | if not confirm_popup(title='交易确认'): 122 | print('下单完成:)') 123 | break 124 | 125 | def buy(self, symbol, price, qty=0): 126 | return self.order(symbol, price, qty) 127 | 128 | def sell(self, symbol, price=0, qty=0): 129 | return self.order(symbol, price, qty, way='卖出') 130 | 131 | def cancel(self, symbol=None, number=None, way='卖出', comfirm=True): 132 | ''' 通达信撤单 ''' 133 | self.tv.item(r'\撤单').click_input() 134 | print("撤单:{0}".format('>'*8)) 135 | time.sleep(0.5) 136 | lv = self._client[Tdx.GRID] 137 | cancel = api.GetParent(lv.handle) 138 | if comfirm: 139 | temp = [{x[3]: [x[1], x[8]]} for x in self._get_data()] 140 | temp = [{x[way][0]: x[way][1]} for x in temp if x.get(way)] 141 | wanted = [temp.index(x) for x in temp if str(symbol) in x or str(number) in x.values()] 142 | if wanted: 143 | for x in wanted: 144 | print('撤掉:{0} {1}'.format(way, temp[x])) 145 | lv.item(x).click() 146 | api.PostMessageW(cancel, WM_COMMAND, Tdx.CANCEL['撤单'], 147 | api.GetDlgItem(cancel, Tdx.CANCEL['撤单'])) 148 | while True: 149 | time.sleep(0.5) 150 | if not confirm_popup(): 151 | print('撤单完成:)') 152 | break 153 | return self._get_data() 154 | 155 | @property 156 | def cancelable(self): 157 | print('可撤委托: {0}'.format('$'*68)) 158 | return self.cancel(way=False) 159 | 160 | @property 161 | def position(self): 162 | print('实时持仓: {0}'.format('$'*68)) 163 | self.tv.item(r'\查询\资金股份').click_input() 164 | #time.sleep(0.5) # 不一定需要用,备用。 165 | return self._get_data() 166 | 167 | @property 168 | def balance(self): 169 | print('资金明细: {0}'.format('$'*68)) 170 | self.tv.item(r'\查询\资金股份').click_input() 171 | return self._get_data(on=False) 172 | 173 | @property 174 | def deals(self): 175 | print('当日成交: {0}'.format('$'*68)) 176 | self.tv.item(r'\查询\当日成交').click_input() 177 | #time.sleep(0.5) # 不一定需要用,备用。 178 | return self._get_data() 179 | 180 | @property 181 | def new(self): 182 | print('新股名单: {0}'.format('$'*68)) 183 | self.tv.item(r'\新股申购\新股申购').click_input() 184 | return self._get_data() 185 | 186 | @property 187 | def bingo(self): 188 | print('新股中签: {0}'.format('$'*68)) 189 | self.tv.item(r'\新股申购\中签查询').click_input() 190 | time.sleep(0.5) 191 | bingo = api.GetParent(self._client['SysListView32'].handle) 192 | api.PostMessageW(bingo, WM_COMMAND, Tdx.BINGO['查询'], 193 | api.GetDlgItem(bingo, Tdx.BINGO['查询'])) 194 | return self._get_data() 195 | 196 | def _batch(self): 197 | """ 新股批量申购,自动切换无需指定,只有华泰、银河、招商、广发等券商可用。 """ 198 | self.tv.item(r'\新股申购\新股批量申购').click_input() 199 | print("新股批量申购:{0}".format('>'*8)) 200 | print(self._get_data()) 201 | time.sleep(0.5) 202 | batch = api.GetParent(self._client[Tdx.GRID].handle) 203 | api.PostMessageW(batch, WM_COMMAND, Tdx.BATCH['申购'], 204 | api.GetDlgItem(batch, Tdx.BATCH['申购'])) 205 | while True: 206 | time.sleep(0.5) 207 | if not confirm_popup(title=Tdx.BATCH['弹窗']): 208 | print('申购完毕!请查询可撤委托单:)') 209 | break 210 | 211 | def raffle(self, skip='', way=True): 212 | '通达信打新' 213 | tag = [x.text() for x in self.tv.item(r'\新股申购').children()] # 子节点标签 214 | if '新股批量申购' in tag: 215 | self._batch() 216 | else: 217 | self.tv.item(r'\新股申购\新股申购').click_input() 218 | print("新股申购: {0}".format('>'*8)) 219 | time.sleep(0.5) 220 | lv = self._client['SysListView32'] 221 | raffle = api.GetParent(lv.handle) 222 | count = lv.item_count() 223 | _parts = [api.GetDlgItem(raffle, x) for x in Tdx.NEW.values()] 224 | for x in range(count): 225 | lv.item(x).click_input(double=True) 226 | api.PostMessageW(raffle, WM_COMMAND, Tdx.NEW['全部'], _parts[0]) 227 | api.PostMessageW(raffle, WM_COMMAND, Tdx.NEW['申购'], _parts[1]) 228 | while True: 229 | time.sleep(0.5) 230 | if not confirm_popup(title=Tdx.NEW['弹窗']): 231 | print('申购完毕!请查询可撤委托单:)') 232 | break 233 | 234 | if __name__ == '__main__': 235 | 236 | trader = Puppet() 237 | print(trader.position) 238 | print(trader.balance) 239 | print(trader.cancelable) 240 | print(trader.deals) 241 | print(trader.new) 242 | print(trader.bingo) 243 | #trader.raffle() 244 | #trader.sell('002097', 11, 100) # 00股票代码需要用字符串。 245 | trader.cancel(number=58) # 默认撤卖单。symbol股票代码,number委托编号。 246 | 247 | -------------------------------------------------------------------------------- /puppet_v4.py: -------------------------------------------------------------------------------- 1 | """ 2 | Puppet是一套以同花顺交易客户端为核心的完整的闭环实盘交易系统框架。 3 | """ 4 | __author__ = "睿瞳深邃(https://github.com/Raytone-D" 5 | __project__ = 'Puppet' 6 | __version__ = "0.4.8" 7 | 8 | # coding: utf-8 9 | 10 | import ctypes 11 | from functools import reduce 12 | import time 13 | import pyperclip 14 | 15 | CONSOLE = 59648, 59649 16 | GRID = 1047, 200, 1047 17 | ACCOUNT = 59392, 0, 1711 18 | COMBO = 59392, 0, 2322 19 | 20 | NODE = {'买入': 161, 21 | '卖出': 162, 22 | '撤单': 163, 23 | '双向委托': 512, 24 | '新股申购': 554, 25 | '中签查询': 1070} 26 | 27 | TWO_WAY = {'买入代码': 1032, 28 | '买入价格': 1033, 29 | '买入数量': 1034, 30 | '买入': 1006, 31 | '卖出代码': 1035, 32 | '卖出价格': 1058, 33 | '卖出数量': 1039, 34 | '卖出': 1008, 35 | '可用余额': 1038, 36 | '刷新': 32790, 37 | '全撤': 30001, 38 | '撤买': 30002, 39 | '撤卖': 30003, 40 | '报表': 1047} 41 | 42 | TAB = {'持仓': ord('W'), 43 | '成交': ord('E'), 44 | '委托': ord('R')} 45 | 46 | SCHEDULE = {'证券代码': '', 47 | '证券名称': '', 48 | '实际数量': '', 49 | '市值': ''} 50 | 51 | CANCEL = {'全选': 1098, 52 | '撤单': 1099, 53 | '全撤': 30001, 54 | '撤买': 30002, 55 | '撤卖': 30003, 56 | '空白': 3348, 57 | '查单': 3349} 58 | 59 | NEW = {'新股代码': 1032, 60 | '新股名称': 1036, 61 | '申购价格': 1033, 62 | '可申购数量': 1018, 63 | '申购数量': 1034, 64 | '申购': 1006} 65 | 66 | RAFFLE = ['新股代码', '证券代码', '申购价格', '申购上限'] 67 | 68 | MSG = {'WM_SETTEXT': 12, 69 | 'WM_GETTEXT': 13, 70 | 'WM_KEYDOWN': 256, 71 | 'WM_KEYUP': 257, 72 | 'WM_COMMAND': 273, 73 | 'CB_GETCOUNT': 326, 74 | 'CB_SETCURSEL': 334, 75 | 'CBN_SELCHANGE': 1} 76 | 77 | CMD = {'COPY': 57634} 78 | 79 | VKCODE = {'F1': 112, 80 | 'F2': 113, 81 | 'F3': 114, 82 | 'F4': 115, 83 | 'F5': 116, 84 | 'F6': 117} 85 | 86 | MKT = {'CYB': '3', 87 | 'SH': '7', 88 | 'SZ': '0', 89 | '创业板': '3', 90 | '沪市': '7', 91 | '深市': '0'} 92 | 93 | op = ctypes.windll.user32 94 | 95 | def switch_combo(index, idCombo, hCombo): 96 | op.SendMessageW(hCombo, MSG['CB_SETCURSEL'], index, 0) 97 | op.SendMessageW(op.GetParent(hCombo), MSG['WM_COMMAND'], MSG['CBN_SELCHANGE']<<16|idCombo, hCombo) 98 | 99 | class Puppet(): 100 | """ 101 | # 方法 # '委买': buy(), '委卖': sell(), '撤单': cancel(), '打新': raffle(), 102 | # 属性 # '帐号': account, '可用余额': balance, '持仓': position, '成交': deals, '可撤委托': cancelable, 103 | # # '新股': new, '中签': bingo, 104 | """ 105 | def __init__(self, main=0): 106 | 107 | print('我正在热身,稍等一下...') 108 | self.main = main if main else op.FindWindowW(0, "网上股票交易系统5.0") 109 | op.SendMessageW(self.main, MSG['WM_COMMAND'], NODE['双向委托'], 0) # 切换到交易操作台 110 | self.wait_a_second = lambda sec=0.2: time.sleep(sec) 111 | self.wait_a_second() # 可调整区间值(0.01~0.5) 112 | self.buff = ctypes.create_unicode_buffer(32) 113 | self.two_way = reduce(op.GetDlgItem, CONSOLE, self.main) 114 | self.members = {k: op.GetDlgItem(self.two_way, v) for k, v in TWO_WAY.items()} 115 | print('我准备好了,开干吧!人生巅峰在前面!') if self.main else print("没找到已登录的客户交易端,我先撤了!") 116 | # 获取登录账号 117 | self.account = reduce(op.GetDlgItem, ACCOUNT, self.main) 118 | op.SendMessageW(self.account, MSG['WM_GETTEXT'], 32, self.buff) 119 | self.account = self.buff.value 120 | self.combo = reduce(op.GetDlgItem, COMBO, self.main) 121 | self.count = op.SendMessageW(self.combo, MSG['CB_GETCOUNT']) 122 | 123 | def switch_tab(self, hCtrl, keyCode, param=0): # 单击 124 | op.PostMessageW(hCtrl, MSG['WM_KEYDOWN'], keyCode, param) 125 | self.wait_a_second(0.5) 126 | op.PostMessageW(hCtrl, MSG['WM_KEYUP'], keyCode, param) 127 | 128 | def copy_data(self, key=0): # background mode 129 | "将CVirtualGridCtrl|Custom的数据复制到剪贴板,默认取当前的表格" 130 | if key: 131 | self.switch_tab(self.two_way, key) # 切换到持仓('W')、成交('E')、委托('R') 132 | print("正在等待实时数据返回,请稍候...") 133 | self.wait_a_second(1) # 等待数据返回的秒数自行调整,一般sec>=1 134 | op.SendMessageW(reduce(op.GetDlgItem, CONSOLE+GRID, self.main), 135 | MSG['WM_COMMAND'], CMD['COPY'], GRID[-1]) 136 | 137 | return pyperclip.paste() 138 | 139 | def buy(self, symbol, price, qty): # 买入(B) 140 | op.SendMessageW(self.members['买入代码'], MSG['WM_SETTEXT'], 0, str(symbol)) 141 | op.SendMessageW(self.members['买入价格'], MSG['WM_SETTEXT'], 0, str(price)) 142 | op.SendMessageW(self.members['买入数量'], MSG['WM_SETTEXT'], 0, str(qty)) 143 | op.PostMessageW(self.two_way, MSG['WM_COMMAND'], TWO_WAY['买入'], self.members['买入']) 144 | 145 | def sell(self, symbol, price, qty): # 卖出(S) 146 | op.SendMessageW(self.members['卖出代码'], MSG['WM_SETTEXT'], 0, str(symbol)) 147 | op.SendMessageW(self.members['卖出价格'], MSG['WM_SETTEXT'], 0, str(price)) 148 | op.SendMessageW(self.members['卖出数量'], MSG['WM_SETTEXT'], 0, str(qty)) 149 | op.PostMessageW(self.two_way, MSG['WM_COMMAND'], TWO_WAY['卖出'], self.members['卖出']) 150 | 151 | def refresh(self): # 刷新(F5) 152 | op.PostMessageW(self.two_way, MSG['WM_COMMAND'], TWO_WAY['刷新'], self.members['刷新']) 153 | 154 | def cancel(self, way=CANCEL['撤买'], symbol='000000'): 155 | 156 | op.SendMessageW(self.main, MSG['WM_COMMAND'], NODE['撤单'], 0) # 切换到撤单操作台 157 | if way != CANCEL['查单'] and symbol != '000000' and symbol.isdecimal(): 158 | print(self.copy_data()) 159 | self.cancel_c = reduce(op.GetDlgItem, CONSOLE, self.main) 160 | self.cancel_ctrl = {v: op.GetDlgItem(self.cancel_c, v) for k, v in CANCEL.items()} 161 | op.SendMessageW(self.cancel_ctrl['填单'], MSG['WM_SETTEXT'], 0, symbol) 162 | self.wait_a_second() 163 | op.PostMessageW(self.cancel_c, MSG['WM_COMMAND'], CANCEL['查单'], self.cancel_ctrl['查单']) 164 | op.PostMessageW(self.cancel_c, MSG['WM_COMMAND'], way, self.cancel_ctrl[way]) 165 | schedule = self.copy_data() 166 | op.SendMessageW(self.main, MSG['WM_COMMAND'], NODE['双向委托'], 0) # 必须返回交易操作台 167 | return schedule 168 | 169 | @property 170 | def balance(self): 171 | print('可用余额: %s' % ('$'*68)) 172 | op.SendMessageW(self.members['可用余额'], MSG['WM_GETTEXT'], 32, self.buff) 173 | return self.buff.value 174 | 175 | @property 176 | def position(self): 177 | print('实时持仓: %s' % ('$'*68)) 178 | return self.copy_data(TAB['持仓']) 179 | 180 | @property 181 | def deals(self): 182 | print('当天成交: %s' % ('$'*68)) 183 | return self.copy_data(TAB['成交']) 184 | 185 | @property 186 | def cancelable(self): 187 | print('可撤委托: %s' % ('$'*68)) 188 | return self.cancel(way=CANCEL['查单']) 189 | 190 | @property 191 | def new(self): 192 | print('新股名单: %s' % ('$'*68)) 193 | return self.raffle(way=False) 194 | 195 | @property 196 | def bingo(self): 197 | print('新股中签: {0}'.format('$'*68)) 198 | op.SendMessageW(self.main, MSG['WM_COMMAND'], NODE['中签查询'], 0) 199 | return self.copy_data() 200 | 201 | def cancel_all(self): # 全撤(Z) 202 | op.PostMessageW(self.two_way, MSG['WM_COMMAND'], 30001, self.members[30001]) 203 | 204 | def cancel_buy(self): # 撤买(X) 205 | op.PostMessageW(self.two_way, MSG['WM_COMMAND'], 30002, self.members[30002]) 206 | 207 | def cancel_sell(self): # 撤卖(C) 208 | op.PostMessageW(self.two_way, MSG['WM_COMMAND'], 30003, self.members[30003]) 209 | 210 | def cancel_last(self): # 撤最后一笔,仅限华泰定制版有效 211 | op.PostMessageW(self.two_way, MSG['WM_COMMAND'], 2053, self.members[2053]) 212 | 213 | def cancel_same(self): # 撤相同代码,仅限华泰定制版 214 | #op.PostMessageW(self.two_way, WM_COMMAND, 30022, self.members[30022]) 215 | pass 216 | 217 | def raffle(self, skip=None, way=True): # 打新股。 218 | op.SendMessageW(self.main, MSG['WM_COMMAND'], NODE['新股申购'], 0) 219 | #close_pop() # 弹窗无需关闭,不影响交易。 220 | schedule = self.copy_data() 221 | if way: 222 | print("开始打新股%s" % ('>'*68)) 223 | print(schedule) 224 | self.raffle_c = reduce(op.GetDlgItem, CONSOLE, self.main) 225 | self.raffle_ctrl = {k: op.GetDlgItem(self.raffle_c, v) for k, v in NEW.items()} 226 | new = [x.split() for x in schedule.splitlines()] 227 | index = [new[0].index(x) for x in RAFFLE if x in new[0]] # 索引映射:代码0, 价格1, 数量2 228 | new = map(lambda x: [x[y] for y in index], new[1:]) 229 | for symbol, price, qty in new: 230 | if symbol[0] == skip: 231 | print({symbol: (qty, "跳过<%s>开头的新股!" % skip)}) 232 | continue 233 | if qty == '0': 234 | print({symbol: (qty, "数量为零")}) 235 | continue 236 | op.SendMessageW(self.raffle_ctrl['新股代码'], MSG['WM_SETTEXT'], 0, symbol) 237 | self.wait_a_second(1) 238 | #op.SendMessageW(self.raffle_ctrl['可申购数量'], MSG['WM_GETTEXT'], 32, self.buff) 239 | #qty = self.buff.value 240 | op.SendMessageW(self.raffle_ctrl['申购数量'], MSG['WM_SETTEXT'], 0, qty) 241 | self.wait_a_second() 242 | op.PostMessageW(self.raffle_c, MSG['WM_COMMAND'], NEW['申购'], self.raffle_ctrl['申购']) 243 | print({symbol: (qty, "已申购")}) 244 | print(self.cancelable) 245 | op.SendMessageW(self.main, MSG['WM_COMMAND'], NODE['双向委托'], 0) # 切换到交易操作台 246 | return schedule 247 | 248 | if __name__ == '__main__': 249 | 250 | trader = Puppet() 251 | if trader.account: 252 | print(trader.account) # 帐号 253 | #print(trader.new) # 查当天新股名单 254 | #trader.raffle(MKT['创业板']) # 确定打新股,跳过创业板不打。 255 | #print(trader.balance) # 可用余额 256 | #print(trader.position) # 实时持仓 257 | #print(trader.deals) # 当天成交 258 | #print(trader.cancelable) # 可撤委托 259 | -------------------------------------------------------------------------------- /puppetrader_v0.3.5.py: -------------------------------------------------------------------------------- 1 | """puppetrader for ths client uniform""" 2 | __author__ = '睿瞳深邃(https://github.com/Raytone-D)' 3 | __project__ = '扯线木偶(puppetrader for ths client unity)' 4 | __version__ = "0.3.5" 5 | '推荐使用:anaconda3 最新版,或者Python >= 3.6' 6 | # coding: utf-8 7 | 8 | import ctypes 9 | from functools import reduce 10 | import time 11 | import pyperclip 12 | import json 13 | 14 | WM_COMMAND, WM_SETTEXT, WM_GETTEXT, WM_KEYDOWN, WM_KEYUP = \ 15 | 273, 12, 13, 256, 257 # 命令 16 | 17 | F1, F2, F3, F4, F5, F6 = \ 18 | 112, 113, 114, 115, 116, 117 # keyCode(按键代码) 19 | 20 | op = ctypes.windll.user32 21 | wait_a_second = lambda sec= 0.1: time.sleep(sec) 22 | 23 | def keystroke(hCtrl, keyCode, param=0): # 单击 24 | op.PostMessageW(hCtrl, WM_KEYDOWN, keyCode, param) 25 | op.PostMessageW(hCtrl, WM_KEYUP, keyCode, param) 26 | 27 | class unity(): 28 | ''' 多账户交易集中处理 ''' 29 | 30 | def __init__(self, main): 31 | 32 | self.main = main 33 | keystroke(main, F6) # 切换到双向委托 34 | wait_a_second() # 可调整区间值(0.01~0.5) 35 | 36 | self.buff = ctypes.create_unicode_buffer(32) 37 | # 代码,价格,数量,买入,代码,价格,数量,卖出,全撤, 撤买, 撤卖 38 | id_members = 1032, 1033, 1034, 1006, 1035, 1058, 1039, 1008, 30001, 30002, 30003, \ 39 | 32790, 1038, 1047, 2053, 30022, 1019 # 刷新,余额、表格、最后一笔、撤相同 40 | self.path_custom = 1047, 200, 1047 41 | 42 | self.two_way = reduce(op.GetDlgItem, (59648, 59649), main) 43 | self.members = {i: op.GetDlgItem(self.two_way, i) for i in id_members} 44 | self.custom = reduce(op.GetDlgItem, self.path_custom, self.two_way) 45 | 46 | # 获取登录账号 47 | self.account = reduce(op.GetDlgItem, (59392, 0, 1711), main) 48 | op.SendMessageW(self.account, WM_GETTEXT, 32, self.buff) 49 | self.account = self.buff.value 50 | # 撤单工具条 51 | self.id_toolbar = {'全选': 1098, \ 52 | '撤单': 1099, \ 53 | '全撤': 30001, \ 54 | '撤买': 30002, \ 55 | '撤卖': 30003, \ 56 | '填单': 3348, \ 57 | '查单': 3349} #'撤尾单': 2053, '撤相同': 30022} # 华泰独有 58 | 59 | op.SendMessageW(main, WM_COMMAND, 163, 0) # 切换到撤单操作台 60 | wait_a_second() 61 | self.cancel_panel = reduce(op.GetDlgItem, (59648, 59649), main) 62 | self.cancel_toolbar = {k: op.GetDlgItem(self.cancel_panel, v) for k, v in self.id_toolbar.items()} 63 | keystroke(main, F6) # 切换到双向委托 64 | 65 | def buy(self, symbol, price, qty): # 买入(B) 66 | # buy = order('b') 67 | op.SendMessageW(self.members[1032], WM_SETTEXT, 0, symbol) 68 | op.SendMessageW(self.members[1033], WM_SETTEXT, 0, price) 69 | op.SendMessageW(self.members[1034], WM_SETTEXT, 0, qty) 70 | op.PostMessageW(self.two_way, WM_COMMAND, 1006, self.members[1006]) 71 | 72 | def sell(self, symbol, price, qty): # 卖出(S) 73 | # buy = order('s') 74 | op.SendMessageW(self.members[1035], WM_SETTEXT, 0, symbol) 75 | op.SendMessageW(self.members[1058], WM_SETTEXT, 0, price) 76 | op.SendMessageW(self.members[1039], WM_SETTEXT, 0, qty) 77 | op.PostMessageW(self.two_way, WM_COMMAND, 1008, self.members[1008]) 78 | 79 | def refresh(self): # 刷新(F5) 80 | op.PostMessageW(self.two_way, WM_COMMAND, 32790, self.members[32790]) 81 | 82 | def cancel_order(self, symbol=''): # 撤单 83 | op.SendMessageW(main, WM_COMMAND, 163, 0) # 切换到撤单操作台 84 | if symbol: 85 | op.SendMessageW(self.cancel_toolbar['填单'], WM_SETTEXT, 0, symbol) 86 | sleep(0.1) # 必须有 87 | op.PostMessageW(self.cancel_panel, WM_COMMAND, self.id_toolbar['查单'], self.cancel_toolbar['查单']) 88 | op.PostMessageW(self.cancel_panel, WM_COMMAND, self.id_toolbar['撤单'], self.cancel_toolbar['撤单']) 89 | keystroke(self.main, F6) # 必须返回双向委托操作台! 90 | 91 | def cancelAll(self): # 全撤(Z) 92 | op.PostMessageW(self.two_way, WM_COMMAND, 30001, self.members[30001]) 93 | 94 | def cancelBuy(self): # 撤买(X) 95 | op.PostMessageW(self.two_way, WM_COMMAND, 30002, self.members[30002]) 96 | 97 | def cancelSell(self): # 撤卖(C) 98 | op.PostMessageW(self.two_way, WM_COMMAND, 30003, self.members[30003]) 99 | 100 | def cancelLast(self): # 撤最后一笔,仅限华泰定制版有效 101 | op.PostMessageW(self.two_way, WM_COMMAND, 2053, self.members[2053]) 102 | 103 | def cancelSame(self): # 撤相同代码,仅限华泰定制版 104 | #op.PostMessageW(self.two_way, WM_COMMAND, 30022, self.members[30022]) 105 | pass 106 | 107 | def balance(self): # 可用余额 108 | op.SendMessageW(self.members[1038], WM_GETTEXT, 32, self.buff) 109 | return self.buff.value 110 | 111 | def get_data(self, key='W'): 112 | "将CVirtualGridCtrl|Custom的数据复制到剪贴板,默认取持仓记录" 113 | 114 | keystroke(self.two_way, ord(key)) # 切换到持仓('W')、成交('E')、委托('R') 115 | wait_a_second() # 等待券商的数据返回... 116 | op.SendMessageW(self.custom, WM_COMMAND, 57634, self.path_custom[-1]) # background mode 117 | 118 | return pyperclip.paste() 119 | 120 | def finder(): 121 | """ 枚举所有已登录的交易端并将其实例化 """ 122 | 123 | team = set() 124 | buff = ctypes.create_unicode_buffer(32) 125 | 126 | @ctypes.WINFUNCTYPE(ctypes.wintypes.BOOL, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM) 127 | def check(hwnd, extra): 128 | if op.IsWindowVisible(hwnd): 129 | op.GetWindowTextW(hwnd, buff, 32) 130 | if '交易系统' in buff.value: 131 | team.add(hwnd) 132 | return 1 133 | op.EnumWindows(check, 0) 134 | 135 | return {unity(main) for main in team if team} 136 | 137 | def to_dict(raw): 138 | x = [row.split() for row in raw.splitlines()] 139 | data = {} 140 | for y in x[1:]: 141 | data.update({y[0]: dict(zip(x[0], y))}) 142 | return data 143 | 144 | if __name__ == '__main__': 145 | 146 | myRegister = {'券商登录号': '自定义名称', \ 147 | '617145470': '东方不败', \ 148 | '20941552121212': '西门吹雪'} 149 | 150 | ret = finder() 151 | 152 | if ret: 153 | # 如果没取到余额,尝试修改_init_函数里面sleep的值,或者查余额的id是不是变了。 154 | trader = {myRegister[broker.account]: broker for broker in ret} # 给账户一个易记的外号 155 | trader1 = {broker.account[-3:]: broker.balance() for broker in ret} # 以登录号3位尾数作代号 156 | profile = {solo: {"交易帐号": trader[solo].account, \ 157 | "可用余额": trader[solo].balance()} \ 158 | for solo in trader} 159 | 160 | print(profile) 161 | #print(json.dumps(profile, indent=4, ensure_ascii=False, sort_keys=True)) 162 | 163 | raw = trader['东方不败'].get_data() # 只能“大写字母”,小写字母THS会崩溃,无语! 164 | print(raw) 165 | #print(to_dict(raw)) 166 | 167 | raw = trader['东方不败'].get_data('R') 168 | print(raw) 169 | #print(json.dumps(to_dict(raw), indent=4, ensure_ascii=False)) 170 | trader['东方不败'].cancel_order('') 171 | 172 | else: print("老板,没发现已登录的交易端!") 173 | -------------------------------------------------------------------------------- /release_puppet_unity_ths.py: -------------------------------------------------------------------------------- 1 | __author__ = '睿瞳深邃(https://github.com/Raytone-D)' 2 | __project__ = "扯线木偶(puppet for THS trader)" 3 | #增加账户id_btn = 1691 4 | # coding: utf-8 5 | 6 | import ctypes 7 | from ctypes.wintypes import BOOL, HWND, LPARAM 8 | from time import sleep 9 | import win32clipboard as cp 10 | 11 | WM_COMMAND, WM_SETTEXT, WM_GETTEXT, WM_KEYDOWN, WM_KEYUP, VK_CONTROL = \ 12 | 273, 12, 13, 256, 257, 17 # 消息命令 13 | F1, F2, F3, F4, F5, F6 = \ 14 | 112, 113, 114, 115, 116, 117 # keyCode 15 | op = ctypes.windll.user32 16 | buffer = ctypes.create_unicode_buffer 17 | 18 | def keystroke(hCtrl, keyCode, param=0): # 击键 19 | op.PostMessageW(hCtrl, WM_KEYDOWN, keyCode, param) 20 | op.PostMessageW(hCtrl, WM_KEYUP, keyCode, param) 21 | 22 | def get_data(): 23 | sleep(0.3) # 秒数关系到是否能复制成功。 24 | op.keybd_event(17, 0, 0, 0) 25 | op.keybd_event(67, 0, 0, 0) 26 | sleep(0.1) # 没有这个就复制失败 27 | op.keybd_event(67, 0, 2, 0) 28 | op.keybd_event(17, 0, 2, 0) 29 | 30 | cp.OpenClipboard(None) 31 | raw = cp.GetClipboardData(13) 32 | data = raw.split() 33 | cp.CloseClipboard() 34 | return data 35 | 36 | class unity(): 37 | ''' 大一统协同交易 ''' 38 | 39 | def __init__(self, hwnd): 40 | keystroke(hwnd, F6) # 切换到双向委托 41 | self.buff = buffer(32) 42 | # 代码,价格,数量,买入,代码,价格,数量,卖出,全撤, 撤买, 撤卖 43 | id_members = 1032, 1033, 1034, 1006, 1035, 1058, 1039, 1008, 30001, 30002, 30003, \ 44 | 32790, 1038, 1047, 2053, 30022 # 刷新,余额、表格、最后一笔、撤相同 45 | self.two_way = hwnd 46 | sleep(0.1) # 按CPU的性能调整秒数(0.01~~0.5),才能获取正确的self.two_way。 47 | for i in (59648, 59649): 48 | self.two_way = op.GetDlgItem(self.two_way, i) 49 | self.members = {i: op.GetDlgItem(self.two_way, i) for i in id_members} 50 | 51 | def buy(self, symbol, price, qty): # 买入(B) 52 | op.SendMessageW(self.members[1032], WM_SETTEXT, 0, symbol) 53 | op.SendMessageW(self.members[1033], WM_SETTEXT, 0, price) 54 | op.SendMessageW(self.members[1034], WM_SETTEXT, 0, qty) 55 | op.PostMessageW(self.two_way, WM_COMMAND, 1006, self.members[1006]) 56 | 57 | def sell(self, *args): # 卖出(S) 58 | op.SendMessageW(self.members[1035], WM_SETTEXT, 0, symbol) 59 | op.SendMessageW(self.members[1058], WM_SETTEXT, 0, price) 60 | op.SendMessageW(self.members[1039], WM_SETTEXT, 0, qty) 61 | op.PostMessageW(self.two_way, WM_COMMAND, 1008, self.members[1008]) 62 | 63 | def refresh(self): # 刷新(F5) 64 | op.PostMessageW(self.two_way, WM_COMMAND, 32790, self.members[32790]) 65 | 66 | def cancel(self, way=0): # 撤销下单 67 | pass 68 | 69 | def cancelAll(self): # 全撤(Z) 70 | op.PostMessageW(self.two_way, WM_COMMAND, 30001, self.members[30001]) 71 | 72 | def cancelBuy(self): # 撤买(X) 73 | op.PostMessageW(self.two_way, WM_COMMAND, 30002, self.members[30002]) 74 | 75 | def cancelSell(self): # 撤卖(C) 76 | op.PostMessageW(self.two_way, WM_COMMAND, 30003, self.members[30003]) 77 | 78 | def cancelLast(self): # 撤最后一笔,仅限华泰定制版有效 79 | op.PostMessageW(self.two_way, WM_COMMAND, 2053, self.members[2053]) 80 | 81 | def cancelSame(self): # 撤相同代码,仅限华泰定制版 82 | #op.PostMessageW(self.two_way, WM_COMMAND, 30022, self.members[30022]) 83 | pass 84 | 85 | def balance(self): # 可用余额 86 | op.SendMessageW(self.members[1038], WM_GETTEXT, 32, self.buff) 87 | return self.buff.value 88 | 89 | def position(self): # 持仓(W) 90 | keystroke(self.two_way, 87) 91 | op.SetForegroundWindow(self.members[1047]) 92 | return get_data() 93 | 94 | def tradeRecord(self): # 成交(E) 95 | keystroke(self.two_way, 69) 96 | op.SetForegroundWindow(self.members[1047]) 97 | return get_data() 98 | 99 | def orderRecord(self): # 委托(R) 100 | keystroke(self.two_way, 82) 101 | op.SetForegroundWindow(self.members[1047]) 102 | return get_data() 103 | 104 | 105 | def finder(register): 106 | ''' 枚举所有可用的broker交易端并实例化 ''' 107 | team = set() 108 | buff = buffer(32) 109 | @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) 110 | def check(hwnd, extra): 111 | if op.IsWindowVisible(hwnd): 112 | op.GetWindowTextW(hwnd, buff, 32) 113 | if '交易系统' in buff.value: 114 | team.add(hwnd) 115 | return 1 116 | op.EnumWindows(check, 0) 117 | 118 | def get_nickname(hwnd): 119 | account = hwnd 120 | for i in 59392, 0, 1711: 121 | account = op.GetDlgItem(account, i) 122 | op.SendMessageW(account, WM_GETTEXT, 32, buff) 123 | return register.get(buff.value[-3:]) 124 | 125 | return {get_nickname(hwnd): unity(hwnd) for hwnd in team if hwnd} 126 | 127 | 128 | if __name__ == '__main__': 129 | 130 | myRegister = {'888': '股神','509': 'gf', '966': '女神', '167': '虚拟盘', '743': '西门吹雪'} 131 | # 用来登录的号码(一般是券商客户号)最后3位数,不能有重复,nickname不能有重名! 132 | trader = finder(myRegister) 133 | if not trader: 134 | print("没发现可用的交易端。") 135 | else: 136 | #print(trader.keys()) 137 | x = {nickname: broker.balance() for (nickname, broker) in trader.items()} 138 | print("可用余额:%s" %x) 139 | buy = '000078', '6.6', '300' 140 | #trader['虚拟盘'].buy(*buy) 141 | #p = trader['虚拟盘'].orderRecord() 142 | #p = trader['虚拟盘'].tradeRecord() 143 | p = trader['虚拟盘'].position() 144 | print(p) 145 | #trader['西门吹雪'].cancelLast() 146 | -------------------------------------------------------------------------------- /v5/test.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /v5/test.py: -------------------------------------------------------------------------------- 1 | """ 单客户端测试 """ 2 | 3 | # coding: utf-8 4 | 5 | from puppet_v5 import Puppet 6 | 7 | trader = Puppet() 8 | 9 | print(trader.position) 10 | print(trader.balance) 11 | print(trader.cancelable) 12 | print(trader.deals) 13 | print(trader.new) 14 | print(trader.bingo) 15 | #trader.raffle() 16 | #trader.sell(300111, 4.99, 100) # 已经无需字符串。 17 | -------------------------------------------------------------------------------- /图解同花顺客户端多账号一键打新.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardywu/puppet/550fcb76cdf1f764a8c619d8064959b220703c1c/图解同花顺客户端多账号一键打新.jpg -------------------------------------------------------------------------------- /扯线木偶API使用说明.txt: -------------------------------------------------------------------------------- 1 | """ 2 | # 扯线木偶操控API使用说明 3 | # api会保持稳定,只会“增减参数”来优化。 4 | # 实例方法必须有参数,否则跳出异常或输出属性的返回值。 5 | # 属性没参数,直接trader.xxx 6 | """ 7 | 8 | def buy(self, symbol, price=0, qty=0): 9 | pass 10 | def sell(self, symbol, price=0, qty=0): 11 | """ 12 | # 功能:实现了和交易端相同的买/卖委托。 13 | # self: 类的实例方法。 14 | # symbol: 代码,字符串类型,非0开头的可以直接用整数。 15 | # price: 价格, 字符串或整数。 16 | # qty: 数量,字符串类型或整数。 17 | 18 | # 限价委托:trader.buy('002236', 16.55, 100) # 最常用的下单方式。 19 | # 扫五档委托:trader.buy('002236', 100) # 注:v4未实现 20 | # 全仓扫五档:trader.buy('002236') # 注:v4未实现。 21 | """ 22 | pass 23 | 24 | def cancel(self, way=CANCEL['撤买'], symbol=None): 25 | """ 26 | # 功能:只实现了交易端的“查代码”撤单功能。可单独撤买或撤卖。 27 | # way: 值可以是撤买按钮id或者撤卖按钮id,默认撤买,v4切换查单与撤买、撤卖。代码需要进一步简化。 28 | # symbol: 股票代码 29 | # trader.cancel('002236') 30 | """ 31 | pass 32 | 33 | def raffle(self, skip=None, way=True): 34 | """ 35 | # 功能:实现了交易端的“新股申购”功能。 36 | # skip: 字符串,值可以是'7'(沪市)、'0'(深市主板)、'3'(创业板)其中之一。表示直接跳过申购。 37 | # way: 布尔值,True表示申购,否则查新股名单。 38 | # trader.raffle(skip='3') 39 | """ 40 | pass 41 | --------------------------------------------------------------------------------