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