├── .gitignore ├── LICENSE ├── Logo ├── setting1_tdx.png ├── setting1_ths.png ├── setting2_tdx.png ├── setting2_ths.png ├── setting3_tdx.png ├── setting3_ths.png ├── setting4_tdx.png ├── setting4_ths.png ├── setting5_tdx.png ├── setting5_ths.png ├── setting6_tdx.png ├── setting6_ths.png ├── setting7_tdx.png └── trading_ths.png ├── README.md ├── pyautotrade_tdx.pyw ├── pyautotrade_tdx_new_version.pyw ├── pyautotrade_ths.pyw └── winguiauto.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /Logo/setting1_tdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting1_tdx.png -------------------------------------------------------------------------------- /Logo/setting1_ths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting1_ths.png -------------------------------------------------------------------------------- /Logo/setting2_tdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting2_tdx.png -------------------------------------------------------------------------------- /Logo/setting2_ths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting2_ths.png -------------------------------------------------------------------------------- /Logo/setting3_tdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting3_tdx.png -------------------------------------------------------------------------------- /Logo/setting3_ths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting3_ths.png -------------------------------------------------------------------------------- /Logo/setting4_tdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting4_tdx.png -------------------------------------------------------------------------------- /Logo/setting4_ths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting4_ths.png -------------------------------------------------------------------------------- /Logo/setting5_tdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting5_tdx.png -------------------------------------------------------------------------------- /Logo/setting5_ths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting5_ths.png -------------------------------------------------------------------------------- /Logo/setting6_tdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting6_tdx.png -------------------------------------------------------------------------------- /Logo/setting6_ths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting6_ths.png -------------------------------------------------------------------------------- /Logo/setting7_tdx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/setting7_tdx.png -------------------------------------------------------------------------------- /Logo/trading_ths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drongh/pyAutoTrading/0a194b18115f2dfca887061fbbfa35cee532567e/Logo/trading_ths.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyAutoTrading 2 | 股票交易软件辅助工具 3 | 4 | ## 简介 5 | 用于华泰证券独立交易软件,同时支持通达信版本和同花顺版本,以上版本都需有`双向委托`界面。软件可以一次监控5只股票,根据条件下单。每次下单耗时小于1s。 6 | 如果有疑问,或是建议,可以发邮件联系,邮箱:`ronghui.ding@outlook.com`,或加入QQ群:`486224275`。 7 | 开发环境:win10 64bit, python3 64bit、pywin32、tushare。 以前是用python 32bit开发的,现在好像python 64bit的也能用。 8 | 9 | ## 同花顺版使用说明 10 | * 华泰的最好用通用版,稳定性高,行情可以选用同花顺的行情,行情速度快了5s,对于追涨杀跌挺重要的。 11 | * 软件共有3个文件,`pyautotrade_ths.pyw`主程序,`stockInfo.dat`存盘文件,`winguiauto.py`是封装的winapi函数。 12 | * 交易软件启动后,按`F6`,进入`双向委托`界面,启动本程序后,不要再切换交易软件的操作界面。切换到其他界面后即使再切换回来,会不能正常获取句柄,无法下单。 13 | * 不写时间条件单,默认时间为凌晨1点。时间条件满足后才检查价格条件,如果只想要时间条件单而忽略价格条件单,可以写个始终满足条件的价格。 14 | * 股票数量最好为100的倍数,小于100股的不会交易,大于100、非整数倍的将取整, 比如150股将作为100股。 15 | * 时间为24小时制,形式为 `时:分:秒`, 每项都必须写, 后面的写法是错误的: `13:30`。 16 | * 同花顺版本支持获取持仓情况,需要把持仓表单设定为11列,通过设定`是否可见`,请看图。获取持仓数据时需要通过鼠标获取焦点(这是自动的),最小化的时会自动从任务栏恢复。 17 | 另外,在交易软件上,把`持仓`表单显示出来,不要放在`成交`或`其他表单`。 18 | * 想使用同花顺通用版的,在`pyautotrade_ths.pyw`文件中`Operation`类的`__init__`函数的第4或5行选择一行就可以。 19 | 20 | ## 通达信版使用说明 21 | * 软件共有3个文件,`pyautotrade_tdx.pyw`主程序,`stockInfo.dat`存盘文件,`winguiauto.py`是封装的winapi函数 22 | * 交易软件启动后,直接点击左边树形列表`对买对卖`,然后启动本程序,不要再切换到其它界面,始终在`对买对卖`界面上 23 | * 不写时间条件单,默认时间为凌晨1点。时间条件满足后才检查价格条件,如果只想要时间条件单而忽略价格条件单,可以写个始终满足条件的价格。 24 | * 股票数量为100的倍数, 如果输入150股将作为100股。默认为0股,也就说,股票数量由交易软件自动填写,当然需提前在交易软件里设定好,在`系统设置-仓位策略`里选择固定数量, 见图5 25 | * 时间为24小时制,形式为 `时:分:秒`, 每项都必须写, 后面的写法是错误的: `13:30` 26 | * 交易软件的委托价格由交易软件自动填写,在`系统设置-自动策略`, 启用`启用自动跟盘`, 自己决定选哪个价格 27 | * 自动交易时最好最大化窗口,否则不能获得持仓情况 28 | 29 | ## 版本 30 | * v 0.01 修正了股票价格实时显示问题。 31 | * v 0.02 重构了交易软件接口,目前在最小化状态下也可以下单,下单速度加快,增加委托日志。 32 | * v 0.03 重新布局了控件,修改委托日志控件。修复了少许Bug。 33 | * v 0.04 重新布局了控件,重构了monitor函数。现在一次可以下4个条件单。 34 | * v 0.05 加入时间条件单。 35 | * v 0.06 交易软件接口函数单独放winguiauto文件。 36 | * v 0.07 时间条件单和价格条件单相结合,添加保存和载入功能,存档和主文件在同一目录下,名为stockInfo.dat,是个二进制文件。 37 | * v 0.08 代码清理,添加了注释。现在可以同时监控5只股票。 38 | * v 0.09 加入自动刷新功能,每隔5分钟刷新一次,防止软件进入待机状态。 39 | * v 0.10 修改了几个bug,买卖价格改由python计算,加快了下单速度(0.6s),稳定性增加了不少。需更改交易软件设置,请看图。 40 | * v 0.11 同花顺版修复了一个严重的bug。 41 | * v 0.12 接口函数改用类重写。winguiauto.py和通达信一致, 修复一个UI显示错误。 42 | * v 0.13 同花顺版本支持获取持仓和资金了。通达信版没有变化。 43 | * v 0.14 增加通达信版改进版。此版用pywinauto改写。大幅简化代码。 44 | ## Bugs 45 | * 集合竞价时获取价格为零,导致误下单 46 | 47 | 48 | 49 | ----------------------------------- 50 | 51 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting1_ths.png) 52 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting2_ths.png) 53 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting3_ths.png) 54 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting4_ths.png) 55 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting5_ths.png) 56 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting6_ths.png) 57 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting1_tdx.png) 58 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting2_tdx.png) 59 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting3_tdx.png) 60 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting4_tdx.png) 61 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting5_tdx.png) 62 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting6_tdx.png) 63 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/setting7_tdx.png) 64 | ![image](https://github.com/drongh/PyAutoTrading/raw/master/Logo/trading_ths.png) 65 | -------------------------------------------------------------------------------- /pyautotrade_tdx.pyw: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf8 -*- 2 | # QQ群: 486224275 3 | __author__ = '人在江湖' 4 | 5 | import tkinter.messagebox 6 | from tkinter import * 7 | from tkinter.ttk import * 8 | import datetime 9 | import threading 10 | import pickle 11 | import time 12 | 13 | import tushare as ts 14 | 15 | from winguiauto import (dumpWindow, dumpWindows, getWindowText, 16 | getListViewInfo, setEditText, clickWindow, 17 | click, closePopupWindows, findTopWindow, 18 | restoreFocusWindow) 19 | 20 | is_start = False 21 | is_monitor = True 22 | set_stock_info = [] 23 | actual_stock_info = [] 24 | consignation_info = [] 25 | is_ordered = [1] * 5 # 1:未下单 0:已下单 26 | 27 | 28 | class Operation: 29 | def __init__(self, top_hwnd): 30 | 31 | self.__top_hwnd = top_hwnd 32 | self.__button = {'refresh': 180, 'position': 145, 'deal': 112, 'withdrawal': 83, 'sell': 50, 'buy': 20} 33 | windows = dumpWindows(self.__top_hwnd) 34 | temp_hwnd = 0 35 | for window in windows: 36 | child_hwnd, window_text, window_class = window 37 | if window_class == 'AfxMDIFrame42': 38 | temp_hwnd = child_hwnd 39 | break 40 | temp_hwnds = dumpWindow(temp_hwnd) 41 | temp_hwnds = dumpWindow(temp_hwnds[1][0]) 42 | self.__menu_hwnds = dumpWindow(temp_hwnds[0][0]) 43 | self.__buy_sell_hwnds = dumpWindow(temp_hwnds[4][0]) 44 | 45 | def __buy(self, code, quantity): 46 | """ 47 | 买入函数 48 | :param code: 股票代码,字符串 49 | :param quantity: 数量, 字符串 50 | """ 51 | setEditText(self.__buy_sell_hwnds[0][0], code) 52 | time.sleep(0.3) 53 | if quantity != '0': 54 | setEditText(self.__buy_sell_hwnds[3][0], quantity) 55 | time.sleep(0.3) 56 | click(self.__buy_sell_hwnds[5][0]) 57 | time.sleep(0.3) 58 | 59 | def __sell(self, code, quantity): 60 | """ 61 | 卖出函数 62 | :param code: 股票代码, 字符串 63 | :param quantity: 数量, 字符串 64 | """ 65 | setEditText(self.__buy_sell_hwnds[24][0], code) 66 | time.sleep(0.3) 67 | if quantity != '0': 68 | setEditText(self.__buy_sell_hwnds[27][0], quantity) 69 | time.sleep(0.3) 70 | click(self.__buy_sell_hwnds[29][0]) 71 | time.sleep(0.3) 72 | 73 | def order(self, code, direction, quantity): 74 | """ 75 | 下单函数 76 | :param code: 股票代码, 字符串 77 | :param direction: 买卖方向 78 | :param quantity: 数量, 字符串,数量为‘0’时,就交易软件指定数量 79 | """ 80 | restoreFocusWindow(self.__top_hwnd) 81 | self.clickRefreshButton() 82 | if direction == 'B': 83 | self.__buy(code, quantity) 84 | if direction == 'S': 85 | self.__sell(code, quantity) 86 | closePopupWindows(self.__top_hwnd) 87 | 88 | def clickRefreshButton(self): 89 | """ 90 | 点击刷新按钮 91 | """ 92 | restoreFocusWindow(self.__top_hwnd) 93 | clickWindow(self.__menu_hwnds[0][0], self.__button['refresh']) 94 | 95 | def getMoney(self): 96 | """ 97 | :return:可用资金 98 | """ 99 | restoreFocusWindow(self.__top_hwnd) 100 | self.clickRefreshButton() 101 | setEditText(self.__buy_sell_hwnds[24][0], '999999') # 测试时获得资金情况 102 | time.sleep(0.3) 103 | money = getWindowText(self.__buy_sell_hwnds[12][0]).strip() 104 | return float(money) 105 | 106 | def getPosition(self): 107 | """获取持仓股票信息 108 | """ 109 | # self.clickRefreshButton() 110 | restoreFocusWindow(self.__top_hwnd) 111 | return getListViewInfo(self.__buy_sell_hwnds[64][0], 5) 112 | 113 | 114 | def pickCodeFromItems(items_info): 115 | """ 116 | 提取股票代码 117 | :param items_info: UI下各项输入信息 118 | :return:股票代码列表 119 | """ 120 | stock_codes = [] 121 | for item in items_info: 122 | stock_codes.append(item[0]) 123 | return stock_codes 124 | 125 | 126 | def getStockData(items_info): 127 | """ 128 | 获取股票实时数据 129 | :param items_info:UI下各项输入信息 130 | :return:股票实时数据 131 | """ 132 | code_name_price = [] 133 | stock_codes = pickCodeFromItems(items_info) 134 | try: 135 | df = ts.get_realtime_quotes(stock_codes) 136 | df_len = len(df) 137 | for stock_code in stock_codes: 138 | is_found = False 139 | for i in range(df_len): 140 | actual_code = df['code'][i] 141 | if stock_code == actual_code: 142 | code_name_price.append((actual_code, df['name'][i], df['price'][i])) 143 | is_found = True 144 | break 145 | if is_found is False: 146 | code_name_price.append(('', '', '')) 147 | except: 148 | code_name_price = [('', '', '')] * 5 # 网络不行,返回空 149 | return code_name_price 150 | 151 | 152 | def monitor(): 153 | """ 154 | 实时监控函数 155 | :return: 156 | """ 157 | global actual_stock_info, consignation_info, is_ordered, set_stock_info 158 | count = 1 159 | 160 | top_hwnd = findTopWindow(wantedClass='TdxW_MainFrame_Class') 161 | if top_hwnd == 0: 162 | tkinter.messagebox.showerror('错误', '请先打开通达信交易软件,再运行本软件') 163 | else: 164 | operation = Operation(top_hwnd) 165 | 166 | while is_monitor and top_hwnd: 167 | 168 | if is_start: 169 | actual_stock_info = getStockData(set_stock_info) 170 | for row, (actual_code, actual_name, actual_price) in enumerate(actual_stock_info): 171 | if is_start and actual_code and is_ordered[row] == 1 \ 172 | and set_stock_info[row][1] and set_stock_info[row][2] > 0 \ 173 | and set_stock_info[row][3] and set_stock_info[row][4] \ 174 | and datetime.datetime.now().time() > set_stock_info[row][5]: 175 | if set_stock_info[row][1] == '>' and float(actual_price) > set_stock_info[row][2]: 176 | operation.order(actual_code, set_stock_info[row][3], set_stock_info[row][4]) 177 | dt = datetime.datetime.now() 178 | consignation_info.append( 179 | (dt.strftime('%x'), dt.strftime('%X'), actual_code, 180 | actual_name, set_stock_info[row][3], 181 | actual_price, set_stock_info[row][4], '已委托')) 182 | is_ordered[row] = 0 183 | 184 | if set_stock_info[row][1] == '<' and float(actual_price) < set_stock_info[row][2]: 185 | operation.order(actual_code, set_stock_info[row][3], set_stock_info[row][4]) 186 | dt = datetime.datetime.now() 187 | consignation_info.append( 188 | (dt.strftime('%x'), dt.strftime('%X'), actual_code, 189 | actual_name, set_stock_info[row][3], 190 | actual_price, set_stock_info[row][4], '已委托')) 191 | is_ordered[row] = 0 192 | 193 | if count % 200 == 0: 194 | operation.clickRefreshButton() 195 | time.sleep(3) 196 | count += 1 197 | 198 | 199 | class StockGui: 200 | def __init__(self): 201 | self.window = Tk() 202 | self.window.title("自动化股票交易") 203 | self.window.resizable(0, 0) 204 | 205 | frame1 = Frame(self.window) 206 | frame1.pack(padx=10, pady=10) 207 | 208 | Label(frame1, text="股票代码", width=8, justify=CENTER).grid( 209 | row=1, column=1, padx=5, pady=5) 210 | Label(frame1, text="股票名称", width=8, justify=CENTER).grid( 211 | row=1, column=2, padx=5, pady=5) 212 | Label(frame1, text="实时价格", width=8, justify=CENTER).grid( 213 | row=1, column=3, padx=5, pady=5) 214 | Label(frame1, text="关系", width=4, justify=CENTER).grid( 215 | row=1, column=4, padx=5, pady=5) 216 | Label(frame1, text="设定价格", width=8, justify=CENTER).grid( 217 | row=1, column=5, padx=5, pady=5) 218 | Label(frame1, text="方向", width=4, justify=CENTER).grid( 219 | row=1, column=6, padx=5, pady=5) 220 | Label(frame1, text="数量", width=8, justify=CENTER).grid( 221 | row=1, column=7, padx=5, pady=5) 222 | Label(frame1, text="时间可选", width=8, justify=CENTER).grid( 223 | row=1, column=8, padx=5, pady=5) 224 | Label(frame1, text="状态", width=6, justify=CENTER).grid( 225 | row=1, column=9, padx=5, pady=5) 226 | 227 | self.rows = 5 228 | self.cols = 9 229 | 230 | self.variable = [] 231 | for row in range(self.rows): 232 | self.variable.append([]) 233 | for col in range(self.cols): 234 | temp = StringVar() 235 | self.variable[row].append(temp) 236 | 237 | for row in range(self.rows): 238 | Entry(frame1, textvariable=self.variable[row][0], 239 | width=8).grid(row=row + 2, column=1, padx=5, pady=5) 240 | Entry(frame1, textvariable=self.variable[row][1], state=DISABLED, 241 | width=8).grid(row=row + 2, column=2, padx=5, pady=5) 242 | Entry(frame1, textvariable=self.variable[row][2], state=DISABLED, justify=RIGHT, 243 | width=8).grid(row=row + 2, column=3, padx=5, pady=5) 244 | Combobox(frame1, values=('<', '>'), textvariable=self.variable[row][3], 245 | width=2).grid(row=row + 2, column=4, padx=5, pady=5) 246 | Spinbox(frame1, from_=0, to=1000, textvariable=self.variable[row][4], 247 | increment=0.01, width=6).grid(row=row + 2, column=5, padx=5, pady=5) 248 | Combobox(frame1, values=('B', 'S'), textvariable=self.variable[row][5], 249 | width=2).grid(row=row + 2, column=6, padx=5, pady=5) 250 | Spinbox(frame1, from_=0, to=100000, textvariable=self.variable[row][6], 251 | increment=100, width=6).grid(row=row + 2, column=7, padx=5, pady=5) 252 | Entry(frame1, textvariable=self.variable[row][7], 253 | width=8).grid(row=row + 2, column=8, padx=5, pady=5) 254 | Entry(frame1, textvariable=self.variable[row][8], state=DISABLED, justify=CENTER, 255 | width=6).grid(row=row + 2, column=9, padx=5, pady=5) 256 | 257 | frame3 = Frame(self.window) 258 | frame3.pack(padx=10, pady=10) 259 | self.start_bt = Button(frame3, text="开始", command=self.start) 260 | self.start_bt.pack(side=LEFT) 261 | self.set_bt = Button(frame3, text='重置买卖', command=self.setFlags) 262 | self.set_bt.pack(side=LEFT) 263 | Button(frame3, text="历史记录", command=self.displayHisRecords).pack(side=LEFT) 264 | Button(frame3, text='保存', command=self.save).pack(side=LEFT) 265 | self.load_bt = Button(frame3, text='载入', command=self.load) 266 | self.load_bt.pack(side=LEFT) 267 | 268 | self.window.protocol(name="WM_DELETE_WINDOW", func=self.close) 269 | self.window.after(100, self.updateControls) 270 | self.window.mainloop() 271 | 272 | def displayHisRecords(self): 273 | """ 274 | 显示历史信息 275 | :return: 276 | """ 277 | global consignation_info 278 | tp = Toplevel() 279 | tp.title('历史记录') 280 | tp.resizable(0, 1) 281 | scrollbar = Scrollbar(tp) 282 | scrollbar.pack(side=RIGHT, fill=Y) 283 | col_name = ['日期', '时间', '证券代码', '证券名称', '方向', '价格', '数量', '备注'] 284 | tree = Treeview( 285 | tp, show='headings', columns=col_name, height=30, yscrollcommand=scrollbar.set) 286 | tree.pack(expand=1, fill=Y) 287 | scrollbar.config(command=tree.yview) 288 | for name in col_name: 289 | tree.heading(name, text=name) 290 | tree.column(name, width=70, anchor=CENTER) 291 | 292 | for msg in consignation_info: 293 | tree.insert('', 0, values=msg) 294 | 295 | def save(self): 296 | """ 297 | 保存设置 298 | :return: 299 | """ 300 | global set_stock_info, consignation_info 301 | self.getItems() 302 | with open('stockInfo.dat', 'wb') as fp: 303 | pickle.dump(set_stock_info, fp) 304 | pickle.dump(consignation_info, fp) 305 | 306 | def load(self): 307 | """ 308 | 载入设置 309 | :return: 310 | """ 311 | global set_stock_info, consignation_info 312 | try: 313 | with open('stockInfo.dat', 'rb') as fp: 314 | set_stock_info = pickle.load(fp) 315 | consignation_info = pickle.load(fp) 316 | except FileNotFoundError as error: 317 | tkinter.messagebox.showerror('错误', error) 318 | 319 | for row in range(self.rows): 320 | for col in range(self.cols): 321 | if col == 0: 322 | self.variable[row][col].set(set_stock_info[row][0]) 323 | elif col == 3: 324 | self.variable[row][col].set(set_stock_info[row][1]) 325 | elif col == 4: 326 | self.variable[row][col].set(set_stock_info[row][2]) 327 | elif col == 5: 328 | self.variable[row][col].set(set_stock_info[row][3]) 329 | elif col == 6: 330 | self.variable[row][col].set(set_stock_info[row][4]) 331 | elif col == 7: 332 | temp = set_stock_info[row][5].strftime('%X') 333 | if temp == '01:00:00': 334 | self.variable[row][col].set('') 335 | else: 336 | self.variable[row][col].set(temp) 337 | 338 | def setFlags(self): 339 | """ 340 | 重置买卖标志 341 | :return: 342 | """ 343 | global is_start, is_ordered 344 | if is_start is False: 345 | is_ordered = [1] * 5 346 | 347 | def updateControls(self): 348 | """ 349 | 实时股票名称、价格、状态信息 350 | :return: 351 | """ 352 | global actual_stock_info, is_start 353 | if is_start: 354 | for row, (actual_code, actual_name, actual_price) in enumerate(actual_stock_info): 355 | if actual_code: 356 | self.variable[row][1].set(actual_name) 357 | self.variable[row][2].set(actual_price) 358 | if is_ordered[row] == 1: 359 | self.variable[row][8].set('监控中') 360 | elif is_ordered[row] == 0: 361 | self.variable[row][8].set('已委托') 362 | else: 363 | self.variable[row][1].set('') 364 | self.variable[row][2].set('') 365 | self.variable[row][8].set('') 366 | 367 | self.window.after(3000, self.updateControls) 368 | 369 | def start(self): 370 | """ 371 | 启动停止 372 | :return: 373 | """ 374 | global is_start 375 | if is_start is False: 376 | is_start = True 377 | else: 378 | is_start = False 379 | 380 | if is_start: 381 | self.getItems() 382 | self.start_bt['text'] = '停止' 383 | self.set_bt['state'] = DISABLED 384 | self.load_bt['state'] = DISABLED 385 | else: 386 | self.start_bt['text'] = '开始' 387 | self.set_bt['state'] = NORMAL 388 | self.load_bt['state'] = NORMAL 389 | 390 | def close(self): 391 | """ 392 | 关闭程序时,停止monitor线程 393 | :return: 394 | """ 395 | global is_monitor 396 | is_monitor = False 397 | self.window.quit() 398 | 399 | def getItems(self): 400 | """ 401 | 获取UI上用户输入的各项数据, 402 | """ 403 | global set_stock_info 404 | set_stock_info = [] 405 | 406 | # 获取买卖价格数量输入项等 407 | for row in range(self.rows): 408 | set_stock_info.append([]) 409 | for col in range(self.cols): 410 | temp = self.variable[row][col].get().strip() 411 | if col == 0: 412 | if len(temp) == 6 and temp.isdigit(): # 判断股票代码是否为6位数 413 | set_stock_info[row].append(temp) 414 | else: 415 | set_stock_info[row].append('') 416 | elif col == 3: 417 | if temp in ('>', '<'): 418 | set_stock_info[row].append(temp) 419 | else: 420 | set_stock_info[row].append('') 421 | elif col == 4: 422 | try: 423 | price = float(temp) 424 | if price > 0: 425 | set_stock_info[row].append(price) # 把价格转为数字 426 | else: 427 | set_stock_info[row].append(0) 428 | except ValueError: 429 | set_stock_info[row].append(0) 430 | elif col == 5: 431 | if temp in ('B', 'S'): 432 | set_stock_info[row].append(temp) 433 | else: 434 | set_stock_info[row].append('') 435 | elif col == 6: 436 | if temp.isdigit() and int(temp) >= 0: 437 | set_stock_info[row].append(str(int(temp) // 100 * 100)) 438 | else: 439 | set_stock_info[row].append('') 440 | elif col == 7: 441 | try: 442 | set_stock_info[row].append(datetime.datetime.strptime(temp, '%H:%M:%S').time()) 443 | except ValueError: 444 | set_stock_info[row].append(datetime.datetime.strptime('1:00:00', '%H:%M:%S').time()) 445 | 446 | 447 | if __name__ == '__main__': 448 | t1 = threading.Thread(target=StockGui) 449 | t1.start() 450 | t1.join(2) 451 | t2 = threading.Thread(target=monitor) 452 | t2.start() 453 | -------------------------------------------------------------------------------- /pyautotrade_tdx_new_version.pyw: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf8 -*- 2 | # version 1.10 3 | 4 | import tkinter.messagebox 5 | from tkinter import * 6 | from tkinter.ttk import * 7 | import datetime 8 | import threading 9 | import pickle 10 | import time 11 | import tushare as ts 12 | import pywinauto 13 | import pywinauto.application 14 | 15 | NUM_OF_STOCKS = 5 # 自定义股票数量 16 | is_start = False 17 | is_monitor = True 18 | set_stocks_info = [] 19 | actual_stocks_info = [] 20 | consignation_info = [] 21 | is_ordered = [1] * NUM_OF_STOCKS # 1:未下单 0:已下单 22 | is_dealt = [0] * NUM_OF_STOCKS # 0: 未成交 负整数:卖出数量, 正整数:买入数量 23 | stock_codes = [''] * NUM_OF_STOCKS 24 | 25 | 26 | class OperationTdx: 27 | def __init__(self): 28 | self.__app = pywinauto.application.Application() 29 | self.__app.connect(class_name='TdxW_MainFrame_Class') 30 | top_hwnd = pywinauto.findwindows.find_window(class_name='TdxW_MainFrame_Class') 31 | temp_hwnd = pywinauto.findwindows.find_windows(top_level_only=False, class_name='AfxWnd42', parent=top_hwnd)[-1] 32 | wanted_hwnd = pywinauto.findwindows.find_windows(top_level_only=False, parent=temp_hwnd) 33 | if len(wanted_hwnd) != 70: 34 | tkinter.messagebox.showerror('错误', '无法获得通达信双向委托界面的窗口句柄') 35 | menu_bar = wanted_hwnd[1] 36 | controls = wanted_hwnd[6] 37 | self.__main_window = self.__app.window_(handle=top_hwnd) 38 | self.__menu_bar = self.__app.window_(handle=menu_bar) 39 | self.__dialog_window = self.__app.window_(handle=controls) 40 | 41 | def __buy(self, code, quantity): 42 | """ 43 | 买入函数 44 | :param code: 股票代码,字符串 45 | :param quantity: 数量, 字符串 46 | """ 47 | self.__dialog_window.Edit1.SetEditText(code) 48 | time.sleep(0.2) 49 | if quantity != '0': 50 | self.__dialog_window.Edit3.SetEditText(quantity) 51 | time.sleep(0.2) 52 | self.__dialog_window.Button1.Click() 53 | time.sleep(0.2) 54 | 55 | def __sell(self, code, quantity): 56 | """ 57 | 卖出函数 58 | :param code: 股票代码, 字符串 59 | :param quantity: 数量, 字符串 60 | """ 61 | self.__dialog_window.Edit4.SetEditText(code) 62 | time.sleep(0.2) 63 | if quantity != '0': 64 | self.__dialog_window.Edit6.SetEditText(quantity) 65 | time.sleep(0.2) 66 | self.__dialog_window.Button2.Click() 67 | time.sleep(0.2) 68 | 69 | def __closePopupWindow(self): 70 | """ 71 | 关闭一个弹窗。 72 | :return: 如果有弹出式对话框,返回True,否则返回False 73 | """ 74 | popup_hwnd = self.__main_window.PopupWindow() 75 | if popup_hwnd: 76 | popup_window = self.__app.window_(handle=popup_hwnd) 77 | popup_window.SetFocus() 78 | popup_window.Button.Click() 79 | return True 80 | return False 81 | 82 | def __closePopupWindows(self): 83 | while self.__closePopupWindow(): 84 | time.sleep(0.2) 85 | 86 | def order(self, code, direction, quantity): 87 | """ 88 | 下单函数 89 | :param code: 股票代码, 字符串 90 | :param direction: 买卖方向 91 | :param quantity: 数量, 字符串,数量为‘0’时,由交易软件指定数量 92 | """ 93 | if direction == 'B': 94 | self.__buy(code, quantity) 95 | if direction == 'S': 96 | self.__sell(code, quantity) 97 | self.__closePopupWindows() 98 | 99 | def maxWindow(self): 100 | """ 101 | 最大化窗口 102 | """ 103 | if self.__main_window.GetShowState() != 3: 104 | self.__main_window.Maximize() 105 | 106 | def minWindow(self): 107 | """ 108 | 最小化窗体 109 | """ 110 | if self.__main_window.GetShowState() != 2: 111 | self.__main_window.Minimize() 112 | 113 | def refresh(self, t=0.5): 114 | """点击刷新按钮 115 | """ 116 | self.__menu_bar.ClickInput(coords=(180, 12)) 117 | time.sleep(t) 118 | 119 | def getMoney(self): 120 | """获取可用资金 121 | """ 122 | self.__dialog_window.Edit1.SetEditText('999999') # 测试时获得资金情况 123 | time.sleep(0.2) 124 | money = self.__dialog_window.Static6.WindowText() 125 | return float(money) 126 | 127 | def getPosition(self): 128 | """获取持仓股票信息 129 | """ 130 | position = [] 131 | rows = self.__dialog_window.ListView.ItemCount() 132 | cols = 10 133 | info = self.__dialog_window.ListView.Texts()[1:] 134 | for row in range(rows): 135 | position.append(info[row * cols:(row + 1) * cols]) 136 | return position 137 | 138 | def getDeal(self, code, pre_position, cur_position): 139 | """ 140 | 获取成交数量 141 | :param code: 股票代码 142 | :param pre_position: 下单前的持仓 143 | :param cur_position: 下单后的持仓 144 | :return: 0-未成交, 正整数是买入的数量, 负整数是卖出的数量 145 | """ 146 | if pre_position == cur_position: 147 | return 0 148 | pre_len = len(pre_position) 149 | cur_len = len(cur_position) 150 | if pre_len == cur_len: 151 | for row in range(cur_len): 152 | if cur_position[row][0] == code: 153 | return int(float(cur_position[row][1]) - float(pre_position[row][1])) 154 | if cur_len > pre_len: 155 | return int(float(cur_position[-1][1])) 156 | 157 | 158 | # def getStockData(items_info): 159 | # """ 160 | # 获取股票实时数据 161 | # :param items_info: 股票信息,没写的股票用空字符代替 162 | # :return: 股票名称价格 163 | # """ 164 | # global stock_codes 165 | # code_name_price = [] 166 | # try: 167 | # df = ts.get_realtime_quotes(stock_codes) 168 | # df_len = len(df) 169 | # for stock_code in stock_codes: 170 | # is_found = False 171 | # for i in range(df_len): 172 | # actual_code = df['code'][i] 173 | # if stock_code == actual_code: 174 | # actual_name = df['name'][i] 175 | # pre_close = float(df['pre_close'][i]) 176 | # if 'ST' in actual_name: 177 | # highest = str(round(pre_close * 1.05, 2)) 178 | # lowest = str(round(pre_close * 0.95, 2)) 179 | # code_name_price.append((actual_code, actual_name, float(df['price'][i]), (highest, lowest))) 180 | # else: 181 | # highest = str(round(pre_close * 1.1, 2)) 182 | # lowest = str(round(pre_close * 0.9, 2)) 183 | # code_name_price.append((actual_code, actual_name, float(df['price'][i]), (highest, lowest))) 184 | # is_found = True 185 | # break 186 | # if is_found is False: 187 | # code_name_price.append(('', '', '', ('', ''))) 188 | # except: 189 | # code_name_price = [('', '', '', ('', ''))] * NUM_OF_STOCKS 190 | # return code_name_price 191 | 192 | 193 | def getStockData(): 194 | """ 195 | 获取股票实时数据 196 | :return:股票实时数据 197 | """ 198 | global stock_codes 199 | code_name_price = [] 200 | try: 201 | df = ts.get_realtime_quotes(stock_codes) 202 | df_len = len(df) 203 | for stock_code in stock_codes: 204 | is_found = False 205 | for i in range(df_len): 206 | actual_code = df['code'][i] 207 | if stock_code == actual_code: 208 | code_name_price.append((actual_code, df['name'][i], float(df['price'][i]))) 209 | is_found = True 210 | break 211 | if is_found is False: 212 | code_name_price.append(('', '', 0)) 213 | except: 214 | code_name_price = [('', '', 0)] * NUM_OF_STOCKS # 网络不行,返回空 215 | return code_name_price 216 | 217 | 218 | def monitor(): 219 | """ 220 | 实时监控函数 221 | """ 222 | global actual_stocks_info, consignation_info, is_ordered, is_dealt, set_stocks_info 223 | count = 1 224 | pre_position = [] 225 | try: 226 | operation = OperationTdx() 227 | operation.maxWindow() 228 | pre_position = operation.getPosition() 229 | except: 230 | tkinter.messagebox.showerror('错误', '无法获得交易软件句柄') 231 | while is_monitor: 232 | if is_start: 233 | actual_stocks_info = getStockData() 234 | for row, (actual_code, actual_name, actual_price) in enumerate(actual_stocks_info): 235 | if actual_code and is_start and is_ordered[row] == 1 and actual_price > 0 \ 236 | and set_stocks_info[row][1] and set_stocks_info[row][2] > 0 \ 237 | and set_stocks_info[row][3] and set_stocks_info[row][4] \ 238 | and datetime.datetime.now().time() > set_stocks_info[row][5]: 239 | if (set_stocks_info[row][1] == '>' and actual_price > set_stocks_info[row][2]) or \ 240 | (set_stocks_info[row][1] == '<' and float(actual_price) < set_stocks_info[row][2]): 241 | operation.maxWindow() 242 | operation.order(actual_code, set_stocks_info[row][3], set_stocks_info[row][4]) 243 | dt = datetime.datetime.now() 244 | is_ordered[row] = 0 245 | operation.refresh() 246 | cur_position = operation.getPosition() 247 | is_dealt[row] = operation.getDeal(actual_code, pre_position, cur_position) 248 | consignation_info.append( 249 | (dt.strftime('%x'), dt.strftime('%X'), actual_code, 250 | actual_name, set_stocks_info[row][3], 251 | actual_price, set_stocks_info[row][4], '已委托', is_dealt[row])) 252 | pre_position = cur_position 253 | 254 | if count % 200 == 0: 255 | operation.refresh() 256 | time.sleep(3) 257 | count += 1 258 | 259 | 260 | class StockGui: 261 | def __init__(self): 262 | self.window = Tk() 263 | self.window.title("自动化股票交易") 264 | self.window.resizable(0, 0) 265 | 266 | frame1 = Frame(self.window) 267 | frame1.pack(padx=10, pady=10) 268 | 269 | Label(frame1, text="股票代码", width=8, justify=CENTER).grid( 270 | row=1, column=1, padx=5, pady=5) 271 | Label(frame1, text="股票名称", width=8, justify=CENTER).grid( 272 | row=1, column=2, padx=5, pady=5) 273 | Label(frame1, text="实时价格", width=8, justify=CENTER).grid( 274 | row=1, column=3, padx=5, pady=5) 275 | Label(frame1, text="关系", width=4, justify=CENTER).grid( 276 | row=1, column=4, padx=5, pady=5) 277 | Label(frame1, text="设定价格", width=8, justify=CENTER).grid( 278 | row=1, column=5, padx=5, pady=5) 279 | Label(frame1, text="方向", width=4, justify=CENTER).grid( 280 | row=1, column=6, padx=5, pady=5) 281 | Label(frame1, text="数量", width=8, justify=CENTER).grid( 282 | row=1, column=7, padx=5, pady=5) 283 | Label(frame1, text="时间可选", width=8, justify=CENTER).grid( 284 | row=1, column=8, padx=5, pady=5) 285 | Label(frame1, text="委托", width=6, justify=CENTER).grid( 286 | row=1, column=9, padx=5, pady=5) 287 | Label(frame1, text="成交", width=6, justify=CENTER).grid( 288 | row=1, column=10, padx=5, pady=5) 289 | 290 | self.rows = NUM_OF_STOCKS 291 | self.cols = 10 292 | 293 | self.variable = [] 294 | for row in range(self.rows): 295 | self.variable.append([]) 296 | for col in range(self.cols): 297 | self.variable[row].append(StringVar()) 298 | 299 | for row in range(self.rows): 300 | Entry(frame1, textvariable=self.variable[row][0], 301 | width=8).grid(row=row + 2, column=1, padx=5, pady=5) 302 | Entry(frame1, textvariable=self.variable[row][1], state=DISABLED, 303 | width=8).grid(row=row + 2, column=2, padx=5, pady=5) 304 | Entry(frame1, textvariable=self.variable[row][2], state=DISABLED, justify=RIGHT, 305 | width=8).grid(row=row + 2, column=3, padx=5, pady=5) 306 | Combobox(frame1, values=('<', '>'), textvariable=self.variable[row][3], 307 | width=2).grid(row=row + 2, column=4, padx=5, pady=5) 308 | Spinbox(frame1, from_=0, to=1000, textvariable=self.variable[row][4], justify=RIGHT, 309 | increment=0.01, width=6).grid(row=row + 2, column=5, padx=5, pady=5) 310 | Combobox(frame1, values=('B', 'S'), textvariable=self.variable[row][5], 311 | width=2).grid(row=row + 2, column=6, padx=5, pady=5) 312 | Spinbox(frame1, from_=0, to=100000, textvariable=self.variable[row][6], justify=RIGHT, 313 | increment=100, width=6).grid(row=row + 2, column=7, padx=5, pady=5) 314 | Entry(frame1, textvariable=self.variable[row][7], 315 | width=8).grid(row=row + 2, column=8, padx=5, pady=5) 316 | Entry(frame1, textvariable=self.variable[row][8], state=DISABLED, justify=CENTER, 317 | width=6).grid(row=row + 2, column=9, padx=5, pady=5) 318 | Entry(frame1, textvariable=self.variable[row][9], state=DISABLED, justify=RIGHT, 319 | width=6).grid(row=row + 2, column=10, padx=5, pady=5) 320 | 321 | frame3 = Frame(self.window) 322 | frame3.pack(padx=10, pady=10) 323 | self.start_bt = Button(frame3, text="开始", command=self.start) 324 | self.start_bt.pack(side=LEFT) 325 | self.set_bt = Button(frame3, text='重置买卖', command=self.setFlags) 326 | self.set_bt.pack(side=LEFT) 327 | Button(frame3, text="历史记录", command=self.displayHisRecords).pack(side=LEFT) 328 | Button(frame3, text='保存', command=self.save).pack(side=LEFT) 329 | self.load_bt = Button(frame3, text='载入', command=self.load) 330 | self.load_bt.pack(side=LEFT) 331 | 332 | self.window.protocol(name="WM_DELETE_WINDOW", func=self.close) 333 | self.window.after(100, self.updateControls) 334 | self.window.mainloop() 335 | 336 | def displayHisRecords(self): 337 | """ 338 | 显示历史信息 339 | """ 340 | global consignation_info 341 | tp = Toplevel() 342 | tp.title('历史记录') 343 | tp.resizable(0, 1) 344 | scrollbar = Scrollbar(tp) 345 | scrollbar.pack(side=RIGHT, fill=Y) 346 | col_name = ['日期', '时间', '证券代码', '证券名称', '方向', '价格', '数量', '委托', '成交'] 347 | tree = Treeview( 348 | tp, show='headings', columns=col_name, height=30, yscrollcommand=scrollbar.set) 349 | tree.pack(expand=1, fill=Y) 350 | scrollbar.config(command=tree.yview) 351 | for name in col_name: 352 | tree.heading(name, text=name) 353 | tree.column(name, width=70, anchor=CENTER) 354 | 355 | for msg in consignation_info: 356 | tree.insert('', 0, values=msg) 357 | 358 | def save(self): 359 | """ 360 | 保存设置 361 | """ 362 | global set_stocks_info, consignation_info 363 | self.getItems() 364 | with open('stockInfo.dat', 'wb') as fp: 365 | pickle.dump(set_stocks_info, fp) 366 | pickle.dump(consignation_info, fp) 367 | 368 | def load(self): 369 | """ 370 | 载入设置 371 | """ 372 | global set_stocks_info, consignation_info 373 | try: 374 | with open('stockInfo.dat', 'rb') as fp: 375 | set_stocks_info = pickle.load(fp) 376 | consignation_info = pickle.load(fp) 377 | except FileNotFoundError as error: 378 | tkinter.messagebox.showerror('错误', error) 379 | 380 | for row in range(self.rows): 381 | for col in range(self.cols): 382 | if col == 0: 383 | self.variable[row][col].set(set_stocks_info[row][0]) 384 | elif col == 3: 385 | self.variable[row][col].set(set_stocks_info[row][1]) 386 | elif col == 4: 387 | self.variable[row][col].set(set_stocks_info[row][2]) 388 | elif col == 5: 389 | self.variable[row][col].set(set_stocks_info[row][3]) 390 | elif col == 6: 391 | self.variable[row][col].set(set_stocks_info[row][4]) 392 | elif col == 7: 393 | temp = set_stocks_info[row][5].strftime('%X') 394 | if temp == '01:00:00': 395 | self.variable[row][col].set('') 396 | else: 397 | self.variable[row][col].set(temp) 398 | 399 | def setFlags(self): 400 | """ 401 | 重置买卖标志 402 | """ 403 | global is_start, is_ordered 404 | if is_start is False: 405 | is_ordered = [1] * NUM_OF_STOCKS 406 | 407 | def updateControls(self): 408 | """ 409 | 实时股票名称、价格、状态信息 410 | """ 411 | global actual_stocks_info, is_start 412 | if is_start: 413 | for row, (actual_code, actual_name, actual_price) in enumerate(actual_stocks_info): 414 | if actual_code: 415 | self.variable[row][1].set(actual_name) 416 | self.variable[row][2].set(str(actual_price)) 417 | if is_ordered[row] == 1: 418 | self.variable[row][8].set('监控中') 419 | elif is_ordered[row] == 0: 420 | self.variable[row][8].set('已委托') 421 | self.variable[row][9].set(str(is_dealt[row])) 422 | else: 423 | self.variable[row][1].set('') 424 | self.variable[row][2].set('') 425 | self.variable[row][8].set('') 426 | self.variable[row][9].set('') 427 | 428 | self.window.after(3000, self.updateControls) 429 | 430 | @staticmethod 431 | def __pickCodeFromItems(items_info): 432 | """ 433 | 提取股票代码 434 | :param items_info: UI下各项输入信息 435 | :return:股票代码列表 436 | """ 437 | stock_codes = [] 438 | for item in items_info: 439 | stock_codes.append(item[0]) 440 | return stock_codes 441 | 442 | def start(self): 443 | """ 444 | 启动停止 445 | """ 446 | global is_start, stock_codes, set_stocks_info 447 | if is_start is False: 448 | is_start = True 449 | else: 450 | is_start = False 451 | 452 | if is_start: 453 | self.getItems() 454 | stock_codes = self.__pickCodeFromItems(set_stocks_info) 455 | self.start_bt['text'] = '停止' 456 | self.set_bt['state'] = DISABLED 457 | self.load_bt['state'] = DISABLED 458 | else: 459 | self.start_bt['text'] = '开始' 460 | self.set_bt['state'] = NORMAL 461 | self.load_bt['state'] = NORMAL 462 | 463 | def close(self): 464 | """ 465 | 关闭程序时,停止monitor线程 466 | """ 467 | global is_monitor 468 | is_monitor = False 469 | self.window.quit() 470 | 471 | def getItems(self): 472 | """ 473 | 获取UI上用户输入的各项数据, 474 | """ 475 | global set_stocks_info 476 | set_stocks_info = [] 477 | 478 | # 获取买卖价格数量输入项等 479 | for row in range(self.rows): 480 | set_stocks_info.append([]) 481 | for col in range(self.cols): 482 | temp = self.variable[row][col].get().strip() 483 | if col == 0: 484 | if len(temp) == 6 and temp.isdigit(): # 判断股票代码是否为6位数 485 | set_stocks_info[row].append(temp) 486 | else: 487 | set_stocks_info[row].append('') 488 | elif col == 3: 489 | if temp in ('>', '<'): 490 | set_stocks_info[row].append(temp) 491 | else: 492 | set_stocks_info[row].append('') 493 | elif col == 4: 494 | try: 495 | price = float(temp) 496 | if price > 0: 497 | set_stocks_info[row].append(price) # 把价格转为数字 498 | else: 499 | set_stocks_info[row].append(0) 500 | except ValueError: 501 | set_stocks_info[row].append(0) 502 | elif col == 5: 503 | if temp in ('B', 'S'): 504 | set_stocks_info[row].append(temp) 505 | else: 506 | set_stocks_info[row].append('') 507 | elif col == 6: 508 | if temp.isdigit() and int(temp) >= 0: 509 | set_stocks_info[row].append(str(int(temp) // 100 * 100)) 510 | else: 511 | set_stocks_info[row].append('') 512 | elif col == 7: 513 | try: 514 | set_stocks_info[row].append(datetime.datetime.strptime(temp, '%H:%M:%S').time()) 515 | except ValueError: 516 | set_stocks_info[row].append(datetime.datetime.strptime('1:00:00', '%H:%M:%S').time()) 517 | 518 | 519 | if __name__ == '__main__': 520 | t1 = threading.Thread(target=StockGui) 521 | t2 = threading.Thread(target=monitor) 522 | t1.start() 523 | t2.start() 524 | -------------------------------------------------------------------------------- /pyautotrade_ths.pyw: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf8 -*- 2 | # QQ群: 486224275 3 | __author__ = '人在江湖' 4 | 5 | import tkinter.messagebox 6 | from tkinter import * 7 | from tkinter.ttk import * 8 | import datetime 9 | import threading 10 | import pickle 11 | import time 12 | 13 | import win32con 14 | import tushare as ts 15 | 16 | from winguiauto import (dumpWindows, clickButton, click, setEditText, 17 | findSubWindows, closePopupWindow, clickWindow, 18 | findTopWindow, getTableData, sendKeyEvent, restoreFocusWindow) 19 | 20 | is_start = False 21 | is_monitor = True 22 | set_stock_info = [] 23 | consignation_info = [] 24 | actual_stock_info = [] 25 | is_ordered = [1] * 5 # 1:准备 0:交易成功 -1:交易失败 26 | 27 | 28 | class Operation: 29 | def __init__(self, top_hwnd): 30 | try: 31 | self.__top_hwnd = top_hwnd 32 | temp_hwnds = dumpWindows(top_hwnd) 33 | self.__wanted_hwnds = findSubWindows(temp_hwnds, 70) # 华泰专用版 34 | # self.__wanted_hwnds = findSubWindows(temp_hwnds, 73) # 同花顺通用版 35 | self.__control_hwnds = [] 36 | for hwnd, text_name, class_name in self.__wanted_hwnds: 37 | if class_name in ('Button', 'Edit'): 38 | self.__control_hwnds.append((hwnd, text_name, class_name)) 39 | except: 40 | tkinter.messagebox.showerror('错误', '无法获得双向委托界面的窗口句柄') 41 | 42 | def __buy(self, code, stop_price, quantity): 43 | """下买单 44 | :param code: 代码 45 | :param stop_price: 涨停价 46 | :param quantity: 数量 47 | :return: 48 | """ 49 | click(self.__control_hwnds[0][0]) 50 | setEditText(self.__control_hwnds[0][0], code) 51 | setEditText(self.__control_hwnds[1][0], stop_price) 52 | time.sleep(0.2) 53 | setEditText(self.__control_hwnds[2][0], quantity) 54 | time.sleep(0.2) 55 | clickButton(self.__control_hwnds[3][0]) 56 | time.sleep(1) 57 | 58 | def __sell(self, code, stop_price, quantity): 59 | """下卖单 60 | """ 61 | click(self.__control_hwnds[4][0]) 62 | setEditText(self.__control_hwnds[4][0], code) 63 | setEditText(self.__control_hwnds[5][0], stop_price) 64 | time.sleep(0.2) 65 | setEditText(self.__control_hwnds[6][0], quantity) 66 | time.sleep(0.2) 67 | clickButton(self.__control_hwnds[7][0]) 68 | time.sleep(1) 69 | 70 | def order(self, code, stop_prices, direction, quantity): 71 | """ 72 | 下单函数 73 | """ 74 | restoreFocusWindow(self.__top_hwnd) 75 | if direction == 'B': 76 | self.__buy(code, stop_prices[0], quantity) 77 | if direction == 'S': 78 | self.__sell(code, stop_prices[1], quantity) 79 | return not closePopupWindow(self.__top_hwnd) 80 | 81 | def clickRefreshButton(self): 82 | """ 83 | 点击刷新按钮 84 | """ 85 | restoreFocusWindow(self.__top_hwnd) 86 | clickButton(self.__control_hwnds[12][0]) 87 | 88 | def getMoney(self): 89 | """ 90 | 获取可用资金 91 | """ 92 | restoreFocusWindow(self.__top_hwnd) 93 | return float(self.__wanted_hwnds[51][1]) 94 | 95 | def getPosition(self): 96 | """ 97 | 获取股票持仓 98 | """ 99 | restoreFocusWindow(self.__top_hwnd) 100 | clickWindow(self.__wanted_hwnds[-2][0], 20) 101 | sendKeyEvent(win32con.VK_CONTROL, 0) 102 | sendKeyEvent(ord('C'), 0) 103 | sendKeyEvent(ord('C'), win32con.KEYEVENTF_KEYUP) 104 | sendKeyEvent(win32con.VK_CONTROL, win32con.KEYEVENTF_KEYUP) 105 | return getTableData(11) 106 | 107 | 108 | def pickCodeFromItems(items_info): 109 | """ 110 | 单独提取股票代码,没写的用空字符代替 111 | :param items_info: 用户填写的所有信息 112 | :return: 股票代码,包含空字符 113 | """ 114 | stock_codes = [] 115 | for item in items_info: 116 | stock_codes.append(item[0]) 117 | return stock_codes 118 | 119 | 120 | def getStockData(items_info): 121 | """ 122 | 获取股票实时数据 123 | :param items_info: 股票信息,没写的股票用空字符代替 124 | :return: 股票名称价格 125 | """ 126 | code_name_price = [] 127 | stock_codes = pickCodeFromItems(items_info) 128 | try: 129 | df = ts.get_realtime_quotes(stock_codes) 130 | df_len = len(df) 131 | for stock_code in stock_codes: 132 | is_found = False 133 | for i in range(df_len): 134 | actual_code = df['code'][i] 135 | if stock_code == actual_code: 136 | actual_name = df['name'][i] 137 | pre_close = float(df['pre_close'][i]) 138 | if 'ST' in actual_name: 139 | highest = str(round(pre_close * 1.05, 2)) 140 | lowest = str(round(pre_close * 0.95, 2)) 141 | code_name_price.append((actual_code, actual_name, df['price'][i], (highest, lowest))) 142 | else: 143 | highest = str(round(pre_close * 1.1, 2)) 144 | lowest = str(round(pre_close * 0.9, 2)) 145 | code_name_price.append((actual_code, actual_name, df['price'][i], (highest, lowest))) 146 | is_found = True 147 | break 148 | if is_found is False: 149 | code_name_price.append(('', '', '', ('', ''))) 150 | except: 151 | code_name_price = [('', '', '', ('', ''))] 152 | return code_name_price 153 | 154 | 155 | def monitor(): 156 | """ 157 | 监控函数,实时获取股价,满足条件就下单 158 | """ 159 | 160 | global actual_stock_info, consignation_info, is_ordered, set_stock_info 161 | count = 1 162 | 163 | top_hwnd = findTopWindow(wantedText='网上股票交易系统5.0') 164 | if top_hwnd == 0: 165 | tkinter.messagebox.showerror('错误', '请先打开华泰证券交易软件,再运行本软件') 166 | else: 167 | operation = Operation(top_hwnd) 168 | 169 | while is_monitor and top_hwnd: 170 | 171 | if is_start: 172 | actual_stock_info = getStockData(set_stock_info) 173 | for row, (actual_code, actual_name, actual_price, stop_prices) in enumerate(actual_stock_info): 174 | if is_start and actual_code and is_ordered[row] == 1 \ 175 | and set_stock_info[row][1] and set_stock_info[row][2] > 0 \ 176 | and set_stock_info[row][3] and set_stock_info[row][4] \ 177 | and datetime.datetime.now().time() > set_stock_info[row][5]: 178 | if set_stock_info[row][1] == '>' and float(actual_price) > set_stock_info[row][2]: 179 | dt = datetime.datetime.now() 180 | if operation.order(actual_code, stop_prices, 181 | set_stock_info[row][3], 182 | set_stock_info[row][4]): 183 | consignation_info.append( 184 | (dt.strftime('%x'), dt.strftime('%X'), actual_code, 185 | actual_name, actual_price, set_stock_info[row][3], set_stock_info[row][4], '委托成功')) 186 | is_ordered[row] = 0 187 | else: 188 | consignation_info.append( 189 | (dt.strftime('%x'), dt.strftime('%X'), actual_code, 190 | actual_name, actual_price, set_stock_info[row][3], set_stock_info[row][4], '委托失败')) 191 | is_ordered[row] = -1 192 | time.sleep(1) 193 | if set_stock_info[row][1] == '<' and float(actual_price) < set_stock_info[row][2]: 194 | dt = datetime.datetime.now() 195 | if operation.order(actual_code, stop_prices, 196 | set_stock_info[row][3], 197 | set_stock_info[row][4]): 198 | consignation_info.append( 199 | (dt.strftime('%x'), dt.strftime('%X'), actual_code, 200 | actual_name, actual_price, set_stock_info[row][3], set_stock_info[row][4], '委托成功')) 201 | is_ordered[row] = 0 202 | else: 203 | consignation_info.append( 204 | (dt.strftime('%x'), dt.strftime('%X'), actual_code, 205 | actual_name, actual_price, set_stock_info[row][3], set_stock_info[row][4], '委托失败')) 206 | is_ordered[row] = -1 207 | time.sleep(1) 208 | 209 | if count % 200 == 0: 210 | operation.clickRefreshButton() 211 | time.sleep(1) 212 | time.sleep(3) 213 | count += 1 214 | 215 | 216 | class StockGui: 217 | def __init__(self): 218 | self.window = Tk() 219 | self.window.title("自动化股票交易") 220 | self.window.resizable(0, 0) 221 | 222 | frame1 = Frame(self.window) 223 | frame1.pack(padx=10, pady=10) 224 | 225 | Label(frame1, text="股票代码", width=8, justify=CENTER).grid( 226 | row=1, column=1, padx=5, pady=5) 227 | Label(frame1, text="股票名称", width=8, justify=CENTER).grid( 228 | row=1, column=2, padx=5, pady=5) 229 | Label(frame1, text="当前价格", width=8, justify=CENTER).grid( 230 | row=1, column=3, padx=5, pady=5) 231 | Label(frame1, text="关系", width=4, justify=CENTER).grid( 232 | row=1, column=4, padx=5, pady=5) 233 | Label(frame1, text="价格", width=8, justify=CENTER).grid( 234 | row=1, column=5, padx=5, pady=5) 235 | Label(frame1, text="方向", width=4, justify=CENTER).grid( 236 | row=1, column=6, padx=5, pady=5) 237 | Label(frame1, text="数量", width=8, justify=CENTER).grid( 238 | row=1, column=7, padx=5, pady=5) 239 | Label(frame1, text="时间可选", width=8, justify=CENTER).grid( 240 | row=1, column=8, padx=5, pady=5) 241 | Label(frame1, text="状态", width=8, justify=CENTER).grid( 242 | row=1, column=9, padx=5, pady=5) 243 | 244 | self.rows = 5 245 | self.cols = 9 246 | 247 | self.variable = [] 248 | for row in range(self.rows): 249 | self.variable.append([]) 250 | for col in range(self.cols): 251 | temp = StringVar() 252 | self.variable[row].append(temp) 253 | 254 | for row in range(self.rows): 255 | Entry(frame1, textvariable=self.variable[row][0], 256 | width=8).grid(row=row + 2, column=1, padx=5, pady=5) 257 | Entry(frame1, textvariable=self.variable[row][1], state=DISABLED, 258 | width=8).grid(row=row + 2, column=2, padx=5, pady=5) 259 | Entry(frame1, textvariable=self.variable[row][2], state=DISABLED, 260 | width=8).grid(row=row + 2, column=3, padx=5, pady=5) 261 | Combobox(frame1, values=('<', '>'), textvariable=self.variable[row][3], 262 | width=2).grid(row=row + 2, column=4, padx=5, pady=5) 263 | Spinbox(frame1, from_=0, to=1000, textvariable=self.variable[row][4], 264 | increment=0.01, width=6).grid(row=row + 2, column=5, padx=5, pady=5) 265 | Combobox(frame1, values=('B', 'S'), textvariable=self.variable[row][5], 266 | width=2).grid(row=row + 2, column=6, padx=5, pady=5) 267 | Spinbox(frame1, from_=0, to=100000, textvariable=self.variable[row][6], 268 | increment=100, width=6).grid(row=row + 2, column=7, padx=5, pady=5) 269 | Entry(frame1, textvariable=self.variable[row][7], 270 | width=8).grid(row=row + 2, column=8, padx=5, pady=5) 271 | Entry(frame1, textvariable=self.variable[row][8], state=DISABLED, justify=CENTER, 272 | width=8).grid(row=row + 2, column=9, padx=5, pady=5) 273 | 274 | frame3 = Frame(self.window) 275 | frame3.pack(padx=10, pady=10) 276 | self.start_bt = Button(frame3, text="开始", command=self.start) 277 | self.start_bt.pack(side=LEFT) 278 | self.set_bt = Button(frame3, text='重置买卖', command=self.setFlags) 279 | self.set_bt.pack(side=LEFT) 280 | Button(frame3, text="历史记录", command=self.displayHisRecords).pack(side=LEFT) 281 | Button(frame3, text='保存', command=self.save).pack(side=LEFT) 282 | self.load_bt = Button(frame3, text='载入', command=self.load) 283 | self.load_bt.pack(side=LEFT) 284 | 285 | self.window.protocol(name="WM_DELETE_WINDOW", func=self.close) 286 | self.window.after(100, self.updateControls) 287 | self.window.mainloop() 288 | 289 | def displayHisRecords(self): 290 | """ 291 | 显示历史信息 292 | :return: 293 | """ 294 | global consignation_info 295 | tp = Toplevel() 296 | tp.title('历史记录') 297 | tp.resizable(0, 1) 298 | scrollbar = Scrollbar(tp) 299 | scrollbar.pack(side=RIGHT, fill=Y) 300 | col_name = ['日期', '时间', '证券代码', '证券名称', '价格', '方向', '数量', '备注'] 301 | tree = Treeview( 302 | tp, show='headings', columns=col_name, height=30, yscrollcommand=scrollbar.set) 303 | tree.pack(expand=1, fill=Y) 304 | scrollbar.config(command=tree.yview) 305 | for name in col_name: 306 | tree.heading(name, text=name) 307 | tree.column(name, width=70, anchor=CENTER) 308 | 309 | for msg in consignation_info: 310 | tree.insert('', 0, values=msg) 311 | 312 | def save(self): 313 | """ 314 | 保存设置 315 | :return: 316 | """ 317 | global set_stock_info, consignation_info, actual_stock_info 318 | self.getItems() 319 | 320 | with open('stockInfo.dat', 'wb') as fp: 321 | pickle.dump(set_stock_info, fp) 322 | pickle.dump(consignation_info, fp) 323 | 324 | def load(self): 325 | """ 326 | 载入设置 327 | :return: 328 | """ 329 | global set_stock_info, consignation_info, actual_stock_info 330 | try: 331 | with open('stockInfo.dat', 'rb') as fp: 332 | set_stock_info = pickle.load(fp) 333 | consignation_info = pickle.load(fp) 334 | except FileNotFoundError as error: 335 | tkinter.messagebox.showerror('错误', error) 336 | 337 | for row in range(self.rows): 338 | for col in range(self.cols): 339 | if col == 0: 340 | self.variable[row][col].set(set_stock_info[row][0]) 341 | elif col == 3: 342 | self.variable[row][col].set(set_stock_info[row][1]) 343 | elif col == 4: 344 | self.variable[row][col].set(set_stock_info[row][2]) 345 | elif col == 5: 346 | self.variable[row][col].set(set_stock_info[row][3]) 347 | elif col == 6: 348 | self.variable[row][col].set(set_stock_info[row][4]) 349 | elif col == 7: 350 | temp = set_stock_info[row][5].strftime('%X') 351 | if temp == '01:00:00': 352 | self.variable[row][col].set('') 353 | else: 354 | self.variable[row][col].set(temp) 355 | 356 | def setFlags(self): 357 | """ 358 | 重置买卖标志 359 | :return: 360 | """ 361 | global is_start, is_ordered 362 | if is_start is False: 363 | is_ordered = [1] * 5 364 | 365 | def updateControls(self): 366 | """ 367 | 实时刷新股票名称、价格、状态信息 368 | :return: 369 | """ 370 | global set_stock_info, actual_stock_info, is_start 371 | if is_start: 372 | for row, (actual_code, actual_name, actual_price, _) in enumerate(actual_stock_info): 373 | if actual_code: 374 | self.variable[row][1].set(actual_name) 375 | self.variable[row][2].set(actual_price) 376 | if is_ordered[row] == 1: 377 | self.variable[row][8].set('监控中') 378 | elif is_ordered[row] == -1: 379 | self.variable[row][8].set('委托失败') 380 | elif is_ordered[row] == 0: 381 | self.variable[row][8].set('委托成功') 382 | else: 383 | self.variable[row][1].set('') 384 | self.variable[row][2].set('') 385 | self.variable[row][8].set('') 386 | 387 | self.window.after(3000, self.updateControls) 388 | 389 | def start(self): 390 | global is_start 391 | 392 | if is_start is False: 393 | is_start = True 394 | else: 395 | is_start = False 396 | 397 | if is_start: 398 | self.getItems() 399 | self.start_bt['text'] = '停止' 400 | self.set_bt['state'] = DISABLED 401 | self.load_bt['state'] = DISABLED 402 | else: 403 | self.start_bt['text'] = '开始' 404 | self.set_bt['state'] = NORMAL 405 | self.load_bt['state'] = NORMAL 406 | 407 | def close(self): 408 | """ 409 | 关闭软件是终止monitor线程 410 | :return: 411 | """ 412 | global is_monitor 413 | is_monitor = False 414 | self.window.quit() 415 | 416 | def getItems(self): 417 | global set_stock_info 418 | set_stock_info = [] 419 | 420 | for row in range(self.rows): 421 | set_stock_info.append([]) 422 | for col in range(self.cols): 423 | temp = self.variable[row][col].get().strip() 424 | if col == 0: 425 | if len(temp) == 6 and temp.isdigit(): # 判断股票代码是否为6位数 426 | set_stock_info[row].append(temp) 427 | else: 428 | set_stock_info[row].append('') 429 | elif col == 3: 430 | if temp in ('>', '<'): 431 | set_stock_info[row].append(temp) 432 | else: 433 | set_stock_info[row].append('') 434 | elif col == 4: 435 | try: 436 | price = float(temp) 437 | if price > 0: 438 | set_stock_info[row].append(price) # 把价格转为数字 439 | else: 440 | set_stock_info[row].append(0) 441 | except ValueError: 442 | set_stock_info[row].append(0) 443 | elif col == 5: 444 | if temp in ('B', 'S'): 445 | set_stock_info[row].append(temp) 446 | else: 447 | set_stock_info[row].append('') 448 | elif col == 6: 449 | if temp.isdigit() and int(temp) >= 100: 450 | set_stock_info[row].append(str(int(temp) // 100 * 100)) 451 | else: 452 | set_stock_info[row].append('') 453 | elif col == 7: 454 | try: 455 | set_stock_info[row].append(datetime.datetime.strptime(temp, '%H:%M:%S').time()) 456 | except ValueError: 457 | set_stock_info[row].append(datetime.datetime.strptime('1:00:00', '%H:%M:%S').time()) 458 | 459 | 460 | if __name__ == '__main__': 461 | t1 = threading.Thread(target=StockGui) 462 | t1.start() 463 | t1.join(2) 464 | t2 = threading.Thread(target=monitor) 465 | t2.start() 466 | -------------------------------------------------------------------------------- /winguiauto.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf8 -*- 2 | 3 | import time 4 | import struct 5 | import win32api 6 | import win32gui 7 | import ctypes 8 | import win32clipboard 9 | 10 | import win32con 11 | import commctrl 12 | 13 | GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId 14 | VirtualAllocEx = ctypes.windll.kernel32.VirtualAllocEx 15 | VirtualFreeEx = ctypes.windll.kernel32.VirtualFreeEx 16 | OpenProcess = ctypes.windll.kernel32.OpenProcess 17 | WriteProcessMemory = ctypes.windll.kernel32.WriteProcessMemory 18 | ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory 19 | 20 | 21 | def restoreFocusWindow(hwnd): 22 | win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) 23 | win32gui.SetForegroundWindow(hwnd) 24 | time.sleep(0.2) 25 | 26 | def getTableData(cols): 27 | content = _getContentsFromClipboard() 28 | lst = content.strip().split()[:-1] 29 | matrix = [] 30 | for i in range(0, len(lst) // cols): 31 | matrix.append(lst[i * cols:(i + 1) * cols]) 32 | return matrix[1:] 33 | 34 | 35 | def getListViewInfo(hwnd, cols): 36 | """ 37 | 获取ListView的信息 38 | :param hwnd: sysListView句柄 39 | :param cols: 读取的列数 40 | :return: sysListView中的内容 41 | """ 42 | col_info = [] 43 | for col in range(cols): 44 | col_info.append(_readListViewItems(hwnd, col)) 45 | row_info = [] 46 | 47 | # 按行 48 | for row in range(len(col_info[0])): 49 | row_info.append([]) 50 | for col in range(len(col_info)): 51 | row_info[row].append(col_info[col][row].decode('GB2312')) 52 | return row_info 53 | 54 | 55 | def findTopWindow(wantedText=None, wantedClass=None): 56 | """ 57 | :param wantedText: 标题名字 58 | :param wantedClass: 窗口类名 59 | :return: 返回顶层窗口的句柄 60 | """ 61 | return win32gui.FindWindow(wantedClass, wantedText) 62 | 63 | 64 | def findPopupWindow(hwnd): 65 | """ 66 | :param hwnd: 父窗口句柄 67 | :return: 返回弹出式窗口的句柄 68 | """ 69 | return win32gui.GetWindow(hwnd, win32con.GW_ENABLEDPOPUP) 70 | 71 | 72 | def dumpWindow(hwnd, wantedText=None, wantedClass=None): 73 | """ 74 | :param hwnd: 窗口句柄 75 | :param wantedText: 指定子窗口名 76 | :param wantedClass: 指定子窗口类名 77 | :return: 返回父窗口下所有子窗体的句柄 78 | """ 79 | windows = [] 80 | hwndChild = None 81 | while True: 82 | hwndChild = win32gui.FindWindowEx(hwnd, hwndChild, wantedClass, wantedText) 83 | if hwndChild: 84 | textName = win32gui.GetWindowText(hwndChild) 85 | className = win32gui.GetClassName(hwndChild) 86 | windows.append((hwndChild, textName, className)) 87 | else: 88 | return windows 89 | 90 | 91 | def findSubWindows(windows, numChildWindows): 92 | """ 93 | 查找窗口列表中的窗口,此窗口的子窗口数量为指定 94 | :param windows: 句柄列表 95 | :param numChildWindows: 子窗口数量 96 | :return:子窗口列表,包括子窗口hwnd, title, className 97 | """ 98 | for window in windows: 99 | childHwnd, windowText, windowClass = window 100 | windowContent = dumpWindow(childHwnd) 101 | if len(windowContent) == numChildWindows: 102 | return windowContent 103 | 104 | 105 | def findSubWindow(windows, wantedText=None, wantedClass=None): 106 | """ 107 | 查找窗口列表中,含有wantedText和wantedClass 108 | :param windows: 窗口列表 109 | :param wantedText: 窗口文本 110 | :param wantedClass: 窗口类名 111 | :return:窗口句柄 112 | """ 113 | for window in windows: 114 | childHwnd, windowText, windowClass = window 115 | if windowText == wantedText and windowClass == wantedClass: 116 | return childHwnd 117 | 118 | 119 | def dumpWindows(hwnd): 120 | """Dump all controls from a window 121 | 122 | Useful during development, allowing to you discover the structure of the 123 | contents of a window, showing the text and class of all contained controls. 124 | 125 | Parameters 126 | ---------- 127 | hwnd 128 | The window handle of the top level window to dump. 129 | 130 | Returns 131 | ------- 132 | all windows 133 | 134 | Usage example:: 135 | 136 | replaceDialog = findTopWindow(wantedText='Replace') 137 | pprint.pprint(dumpWindow(replaceDialog)) 138 | """ 139 | windows = [] 140 | win32gui.EnumChildWindows(hwnd, _windowEnumerationHandler, windows) 141 | return windows 142 | 143 | 144 | def closePopupWindow(top_hwnd, wantedText=None, wantedClass=None): 145 | """ 146 | 关闭一个弹窗。 147 | :param top_hwnd: 主窗口句柄 148 | :param wantedText: 弹出对话框上的按钮文本 149 | :param wantedClass: 弹出对话框上的按钮类名 150 | :return: 如果有弹出式对话框,返回True,否则返回False 151 | """ 152 | hwnd_popup = findPopupWindow(top_hwnd) 153 | if hwnd_popup: 154 | hwnd_control = findControl(hwnd_popup, wantedText, wantedClass) 155 | clickButton(hwnd_control) 156 | return True 157 | return False 158 | 159 | 160 | def closePopupWindows(top_hwnd): 161 | """ 162 | 连续关闭多个弹出式对话框,直到没有弹窗 163 | :param top_hwnd: 主窗口句柄 164 | :return: 165 | """ 166 | while closePopupWindow(top_hwnd): 167 | time.sleep(0.3) 168 | 169 | 170 | def findControl(topHwnd, 171 | wantedText=None, 172 | wantedClass=None, 173 | selectionFunction=None): 174 | """Find a control. 175 | 176 | You can identify a control using caption, class, a custom selection 177 | function, or any combination of these. (Multiple selection criteria are 178 | ANDed. If this isn't what's wanted, use a selection function.) 179 | 180 | Parameters 181 | ---------- 182 | topHwnd 183 | The window handle of the top level window in which the 184 | required controls reside. 185 | wantedText 186 | Text which the required control's captions must contain. 187 | wantedClass 188 | Class to which the required control must belong. 189 | selectionFunction 190 | Control selection function. Reference to a function 191 | should be passed here. The function should take hwnd as 192 | an argument, and should return True when passed the 193 | hwnd of the desired control. 194 | 195 | Returns 196 | ------- 197 | The window handle of the first control matching the 198 | supplied selection criteria. 199 | 200 | Raises 201 | ------ 202 | WinGuiAutoError, when no control found. 203 | 204 | Usage example:: 205 | 206 | optDialog = findTopWindow(wantedText="Options") 207 | okButton = findControl(optDialog, 208 | wantedClass="Button", 209 | wantedText="OK") 210 | """ 211 | controls = findControls(topHwnd, 212 | wantedText=wantedText, 213 | wantedClass=wantedClass, 214 | selectionFunction=selectionFunction) 215 | if controls: 216 | return controls[0] 217 | else: 218 | raise WinGuiAutoError("No control found for topHwnd=" + 219 | repr(topHwnd) + 220 | ", wantedText=" + 221 | repr(wantedText) + 222 | ", wantedClass=" + 223 | repr(wantedClass) + 224 | ", selectionFunction=" + 225 | repr(selectionFunction)) 226 | 227 | 228 | def findControls(topHwnd, 229 | wantedText=None, 230 | wantedClass=None, 231 | selectionFunction=None): 232 | """Find controls. 233 | 234 | You can identify controls using captions, classes, a custom selection 235 | function, or any combination of these. (Multiple selection criteria are 236 | ANDed. If this isn't what's wanted, use a selection function.) 237 | 238 | Parameters 239 | ---------- 240 | topHwnd 241 | The window handle of the top level window in which the 242 | required controls reside. 243 | wantedText 244 | Text which the required controls' captions must contain. 245 | wantedClass 246 | Class to which the required controls must belong. 247 | selectionFunction 248 | Control selection function. Reference to a function 249 | should be passed here. The function should take hwnd as 250 | an argument, and should return True when passed the 251 | hwnd of a desired control. 252 | 253 | Returns 254 | ------- 255 | The window handles of the controls matching the 256 | supplied selection criteria. 257 | 258 | Usage example:: 259 | 260 | optDialog = findTopWindow(wantedText="Options") 261 | def findButtons(hwnd, windowText, windowClass): 262 | return windowClass == "Button" 263 | buttons = findControl(optDialog, wantedText="Button") 264 | """ 265 | 266 | def searchChildWindows(currentHwnd): 267 | results = [] 268 | childWindows = [] 269 | try: 270 | win32gui.EnumChildWindows(currentHwnd, 271 | _windowEnumerationHandler, 272 | childWindows) 273 | except win32gui.error: 274 | # This seems to mean that the control *cannot* have child windows, 275 | # i.e. not a container. 276 | return 277 | for childHwnd, windowText, windowClass in childWindows: 278 | descendentMatchingHwnds = searchChildWindows(childHwnd) 279 | if descendentMatchingHwnds: 280 | results += descendentMatchingHwnds 281 | 282 | if wantedText and \ 283 | not _normaliseText(wantedText) in _normaliseText(windowText): 284 | continue 285 | if wantedClass and \ 286 | not windowClass == wantedClass: 287 | continue 288 | if selectionFunction and \ 289 | not selectionFunction(childHwnd): 290 | continue 291 | results.append(childHwnd) 292 | return results 293 | 294 | return searchChildWindows(topHwnd) 295 | 296 | 297 | def clickButton(hwnd): 298 | """Simulates a single mouse click on a button 299 | 300 | Parameters 301 | ---------- 302 | hwnd 303 | Window handle of the required button. 304 | 305 | Usage example:: 306 | 307 | okButton = findControl(fontDialog, 308 | wantedClass="Button", 309 | wantedText="OK") 310 | clickButton(okButton) 311 | """ 312 | _sendNotifyMessage(hwnd, win32con.BN_CLICKED) 313 | 314 | 315 | def click(hwnd): 316 | """ 317 | 模拟鼠标左键单击 318 | :param hwnd: 要单击的控件、窗体句柄 319 | :return: 320 | """ 321 | win32gui.PostMessage(hwnd, win32con.WM_LBUTTONDOWN, None, None) 322 | time.sleep(0.2) 323 | win32gui.PostMessage(hwnd, win32con.WM_LBUTTONUP, None, None) 324 | time.sleep(0.2) 325 | 326 | 327 | def focusWindow(hwnd): 328 | """ 329 | 捕捉窗口焦点 330 | :param hwnd: 窗体句柄 331 | :return: 332 | """ 333 | win32gui.ShowWindow(hwnd, win32con.SW_SHOWMAXIMIZED) 334 | win32gui.SetForegroundWindow(hwnd) 335 | 336 | 337 | def clickWindow(hwnd, offset): 338 | left, top, right, bottom = win32gui.GetWindowRect(hwnd) 339 | # print('left, top, right, bottom', left, top, right, bottom) 340 | win32api.SetCursorPos([left + offset, (bottom - top) // 2 + top]) 341 | win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) 342 | time.sleep(0.2) 343 | win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) 344 | time.sleep(0.2) 345 | 346 | 347 | def sendKeyMsg(hwnd, key_code): 348 | """ 349 | 模拟按键 350 | :param hwnd: 窗体句柄 351 | :param key_code: 按键码,在win32con下,比如win32con.VK_F1 352 | :return: 353 | """ 354 | win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, key_code, 0) # 消息键盘 355 | time.sleep(0.2) 356 | win32gui.PostMessage(hwnd, win32con.WM_KEYUP, key_code, 0) 357 | time.sleep(0.2) 358 | 359 | 360 | def sendKeyEvent(key, command): 361 | win32api.keybd_event(key, 0, command, 0) 362 | time.sleep(0.2) 363 | 364 | 365 | def clickStatic(hwnd): 366 | """Simulates a single mouse click on a static 367 | 368 | Parameters 369 | ---------- 370 | hwnd 371 | Window handle of the required static. 372 | 373 | Usage example: TODO 374 | """ 375 | _sendNotifyMessage(hwnd, win32con.STN_CLICKED) 376 | 377 | 378 | def doubleClickStatic(hwnd): 379 | """Simulates a double mouse click on a static 380 | 381 | Parameters 382 | ---------- 383 | hwnd 384 | Window handle of the required static. 385 | 386 | Usage example: TODO 387 | """ 388 | _sendNotifyMessage(hwnd, win32con.STN_DBLCLK) 389 | 390 | 391 | # def getEditText(hwnd): 392 | # bufLen = win32gui.SendMessage(hwnd, win32con.WM_GETTEXTLENGTH, 0, 0) + 1 393 | # print(bufLen) 394 | # buffer = win32gui.PyMakeBuffer(bufLen) 395 | # win32gui.SendMessage(hwnd, win32con.WM_GETTEXT, bufLen, buffer) 396 | # 397 | # text = buffer[:bufLen] 398 | # return text 399 | 400 | 401 | def getWindowText(hwnd): 402 | """ 403 | 获取控件的标题 404 | """ 405 | return win32gui.GetWindowText(hwnd) 406 | 407 | 408 | def setEditText(hwnd, text): 409 | """ 410 | 设置Edit控件的文本,这个只能是单行文本 411 | :param hwnd: Edit控件句柄 412 | :param text: 要设置的文本 413 | :return: 414 | """ 415 | win32gui.SendMessage(hwnd, win32con.WM_SETTEXT, None, text) 416 | 417 | 418 | # def setEditText(hwnd, text, append=False): 419 | # '''Set an edit control's text. 420 | # 421 | # Parameters 422 | # ---------- 423 | # hwnd 424 | # The edit control's hwnd. 425 | # text 426 | # The text to send to the control. This can be a single 427 | # string, or a sequence of strings. If the latter, each will 428 | # be become a a seperate line in the control. 429 | # append 430 | # Should the new text be appended to the existing text? 431 | # Defaults to False, meaning that any existing text will be 432 | # replaced. If True, the new text will be appended to the end 433 | # of the existing text. 434 | # Note that the first line of the new text will be directly 435 | # appended to the end of the last line of the existing text. 436 | # If appending lines of text, you may wish to pass in an 437 | # empty string as the 1st element of the 'text' argument. 438 | # 439 | # Usage example:: 440 | # 441 | # print "Enter various bits of text." 442 | # setEditText(editArea, "Hello, again!") 443 | # time.sleep(.5) 444 | # setEditText(editArea, "You still there?") 445 | # time.sleep(.5) 446 | # setEditText(editArea, ["Here come", "two lines!"]) 447 | # time.sleep(.5) 448 | # 449 | # print "Add some..." 450 | # setEditText(editArea, ["", "And a 3rd one!"], append=True) 451 | # time.sleep(.5) 452 | # ''' 453 | 454 | # Ensure that text is a list 455 | # try: 456 | # text + '' 457 | # text = [text] 458 | # except TypeError: 459 | # pass 460 | # 461 | # # Set the current selection range, depending on append flag 462 | # if append: 463 | # win32gui.SendMessage(hwnd, 464 | # win32con.EM_SETSEL, 465 | # -1, 466 | # 0) 467 | # else: 468 | # win32gui.SendMessage(hwnd, 469 | # win32con.EM_SETSEL, 470 | # 0, 471 | # -1) 472 | # 473 | # # Send the text 474 | # win32gui.SendMessage(hwnd, 475 | # win32con.EM_REPLACESEL, 476 | # True, 477 | # os.linesep.join(text)) 478 | 479 | def _readListViewItems(hwnd, column_index=0): 480 | # Allocate virtual memory inside target process 481 | pid = ctypes.create_string_buffer(4) 482 | p_pid = ctypes.addressof(pid) 483 | GetWindowThreadProcessId(hwnd, p_pid) # process owning the given hwnd 484 | hProcHnd = OpenProcess(win32con.PROCESS_ALL_ACCESS, False, struct.unpack("i", pid)[0]) 485 | pLVI = VirtualAllocEx(hProcHnd, 0, 4096, win32con.MEM_RESERVE | win32con.MEM_COMMIT, win32con.PAGE_READWRITE) 486 | pBuffer = VirtualAllocEx(hProcHnd, 0, 4096, win32con.MEM_RESERVE | win32con.MEM_COMMIT, win32con.PAGE_READWRITE) 487 | 488 | # Prepare an LVITEM record and write it to target process memory 489 | lvitem_str = struct.pack('iiiiiiiii', *[0, 0, column_index, 0, 0, pBuffer, 4096, 0, 0]) 490 | lvitem_buffer = ctypes.create_string_buffer(lvitem_str) 491 | copied = ctypes.create_string_buffer(4) 492 | p_copied = ctypes.addressof(copied) 493 | WriteProcessMemory(hProcHnd, pLVI, ctypes.addressof(lvitem_buffer), ctypes.sizeof(lvitem_buffer), p_copied) 494 | 495 | # iterate items in the SysListView32 control 496 | num_items = win32gui.SendMessage(hwnd, commctrl.LVM_GETITEMCOUNT) 497 | item_texts = [] 498 | for item_index in range(num_items): 499 | win32gui.SendMessage(hwnd, commctrl.LVM_GETITEMTEXT, item_index, pLVI) 500 | target_buff = ctypes.create_string_buffer(4096) 501 | ReadProcessMemory(hProcHnd, pBuffer, ctypes.addressof(target_buff), 4096, p_copied) 502 | item_texts.append(target_buff.value) 503 | 504 | VirtualFreeEx(hProcHnd, pBuffer, 0, win32con.MEM_RELEASE) 505 | VirtualFreeEx(hProcHnd, pLVI, 0, win32con.MEM_RELEASE) 506 | win32api.CloseHandle(hProcHnd) 507 | return item_texts 508 | 509 | 510 | def _getContentsFromClipboard(): 511 | win32clipboard.OpenClipboard() 512 | content = win32clipboard.GetClipboardData() 513 | win32clipboard.CloseClipboard() 514 | return content 515 | 516 | 517 | def _windowEnumerationHandler(hwnd, resultList): 518 | """Pass to win32gui.EnumWindows() to generate list of window handle, 519 | window text, window class tuples.""" 520 | resultList.append((hwnd, 521 | win32gui.GetWindowText(hwnd), 522 | win32gui.GetClassName(hwnd))) 523 | 524 | 525 | def _buildWinLong(high, low): 526 | """Build a windows long parameter from high and low words. 527 | See http://support.microsoft.com/support/kb/articles/q189/1/70.asp 528 | """ 529 | # return ((high << 16) | low) 530 | return int(struct.unpack('>L', 531 | struct.pack('>2H', 532 | high, 533 | low))[0]) 534 | 535 | 536 | def _sendNotifyMessage(hwnd, nofifyMessage): 537 | """Send a notify message to a control.""" 538 | win32gui.SendMessage(win32gui.GetParent(hwnd), 539 | win32con.WM_COMMAND, 540 | _buildWinLong(nofifyMessage, 541 | win32api.GetWindowLong(hwnd, 542 | win32con.GWL_ID)), 543 | hwnd) 544 | 545 | 546 | def _normaliseText(controlText): 547 | """Remove '&' characters, and lower case. 548 | Useful for matching control text.""" 549 | return controlText.lower().replace('&', '') 550 | 551 | 552 | class Bunch(object): 553 | """See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52308""" 554 | 555 | def __init__(self, **kwds): 556 | self.__dict__.update(kwds) 557 | 558 | def __str__(self): 559 | state = ["%s=%r" % (attribute, value) 560 | for (attribute, value) 561 | in list(self.__dict__.items())] 562 | return '\n'.join(state) 563 | 564 | 565 | class WinGuiAutoError(Exception): 566 | pass 567 | --------------------------------------------------------------------------------