Full Code of shidenggui/easytrader for AI

master def5a8edc5eb cached
67 files
209.8 KB
59.2k tokens
376 symbols
1 requests
Download .txt
Showing preview only (225K chars total). Download the full file or copy to clipboard to get everything.
Repository: shidenggui/easytrader
Branch: master
Commit: def5a8edc5eb
Files: 67
Total size: 209.8 KB

Directory structure:
gitextract_z2l9h_jr/

├── .bumpversion.cfg
├── .coveragerc
├── .github/
│   ├── ISSUE_TEMPLATE.md
│   └── copilot-instructions.md
├── .gitignore
├── .pylintrc
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── Makefile
├── Pipfile
├── README.md
├── docs/
│   ├── follow.md
│   ├── index.md
│   ├── install.md
│   ├── miniqmt.md
│   ├── remote.md
│   ├── usage.md
│   └── xueqiu.md
├── easytrader/
│   ├── __init__.py
│   ├── api.py
│   ├── clienttrader.py
│   ├── config/
│   │   ├── __init__.py
│   │   ├── client.py
│   │   ├── global.json
│   │   └── xq.json
│   ├── exceptions.py
│   ├── follower.py
│   ├── gf_clienttrader.py
│   ├── gj_clienttrader.py
│   ├── grid_strategies.py
│   ├── ht_clienttrader.py
│   ├── htzq_clienttrader.py
│   ├── joinquant_follower.py
│   ├── log.py
│   ├── miniqmt/
│   │   ├── __init__.py
│   │   └── miniqmt_trader.py
│   ├── pop_dialog_handler.py
│   ├── refresh_strategies.py
│   ├── remoteclient.py
│   ├── ricequant_follower.py
│   ├── server.py
│   ├── universal_clienttrader.py
│   ├── utils/
│   │   ├── __init__.py
│   │   ├── captcha.py
│   │   ├── misc.py
│   │   ├── perf.py
│   │   ├── stock.py
│   │   └── win_gui.py
│   ├── webtrader.py
│   ├── wk_clienttrader.py
│   ├── xq_follower.py
│   ├── xqtrader.py
│   └── yh_clienttrader.py
├── gj_client.json
├── mkdocs.yml
├── mypy.ini
├── readthedocs-requirements.txt
├── requirements.txt
├── setup.py
├── test-requirements.txt
├── tests/
│   ├── __init__.py
│   ├── test_easytrader.py
│   ├── test_xq_follower.py
│   └── test_xqtrader.py
├── xq.json
└── yh_client.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .bumpversion.cfg
================================================
[bumpversion]
current_version = 0.23.7
commit = True
files = easytrader/__init__.py setup.py
tag = True
tag_name = {new_version}


================================================
FILE: .coveragerc
================================================
[run]
branch = True
include = easytrader/*
omit = tests/*

[report]
fail_under = -1 


================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
## env

OS: win7/ win10 / mac / linux
PYTHON_VERSION: 3.x
EASYTRADER_VERSION: 0.xx.xx
BROKER_TYPE: gj / ht / xq / xxx

## problem

## how to repeat





================================================
FILE: .github/copilot-instructions.md
================================================
# Project Guidelines

## Overview

easytrader is a Chinese stock trading automation library. It supports:
- Windows desktop client brokers via pywinauto GUI automation (同花顺-based)
- Xueqiu (雪球) web portfolio trading via REST API
- MiniQMT (迅投) official quant SDK integration
- Strategy following from JoinQuant / RiceQuant / Xueqiu
- Remote client-server execution via Flask

## Architecture

**Entry points**: `easytrader.use(broker)` and `easytrader.follower(platform)` — factory functions in `easytrader/api.py`.

**Three parallel branches**:

| Branch | Base class | Purpose |
|--------|-----------|---------|
| Desktop GUI | `ClientTrader` → broker subclasses | pywinauto-driven Windows client automation |
| Web/API | `WebTrader` → `XueQiuTrader`, `MiniqmtTrader` | HTTP/SDK-based trading |
| Followers | `BaseFollower` → platform subclasses | Mirror strategies from quant platforms |

**Remote**: `server.py` (Flask) + `remoteclient.py` enables controlling a Windows trader remotely.

**Config**: `easytrader/config/client.py` holds broker-specific UI control IDs. JSON configs in `easytrader/config/` store API URLs.

## Code Style

- **Formatter**: `black -l 79` (79-char line limit)
- **Import sorting**: `isort`
- **Linter**: `pylint`
- **Type checking**: `mypy` (with `ignore_missing_imports = True`)
- **Language**: Comments, error messages, log messages, and docstrings are in **Chinese**. Follow this convention.
- Type hints are used sparingly (mainly in newer code like `miniqmt/`). Match the style of the surrounding code.

## Build and Test

```bash
# Install
pipenv install

# Run tests (only xq_follower and xqtrader tests are runnable without Windows + broker)
make test                  # or: pipenv run test → pytest -vx --cov=easytrader tests

# Lint & format
pipenv run lint            # pylint
pipenv run format          # black -l 79
pipenv run sort_imports    # isort
pipenv run type_check      # mypy
```

Tests use `unittest` (run via pytest). Desktop client tests require Windows + live broker sessions and are gated by `@unittest.skipUnless` with `EZ_TEST_CLIENTS` env var.

## Conventions

- **Factory dispatch** in `api.py` — broker modules are lazily imported inside `if` branches, not registered via a plugin system.
- **ABC + strategy pattern**: `IGridStrategy`, `IRefreshStrategy` abstract bases with swappable implementations for grid reading and data refresh.
- **Logging**: Single shared logger `logging.getLogger("easytrader")` — do not create module-level loggers.
- **Exceptions**: Use `TradeError` and `NotLoginError` from `easytrader/exceptions.py`.
- **Perf timing**: `@perf_clock` decorator from `easytrader/utils/perf.py` for performance-critical operations.
- **Session keepalive**: `WebTrader` uses a daemon heartbeat thread.

## Platform Pitfalls

- `ClientTrader`, `grid_strategies.py`, `refresh_strategies.py`, `pop_dialog_handler.py` require **pywinauto** and Win32 — non-functional on macOS/Linux. Guard platform-specific imports appropriately.
- `xtquant` (miniqmt dependency) is a proprietary SDK installed from the QMT terminal, not from PyPI.
- `pywinauto` is pinned to `==0.6.6` — do not upgrade without testing.


================================================
FILE: .gitignore
================================================
site
cmd_cache.pk
bak
.mypy_cache
.pyre
.pytest_cache
yjb_account.json
htt.json
gft.json
test.py
ht_account.json
.idea
.vscode
.ipynb_checkpoints
Untitled.ipynb
untitled.txt
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
account.json
account.session
# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# cache
tmp/

secrets/


================================================
FILE: .pylintrc
================================================
[MASTER]

# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=

# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS

# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=\d{4}.+\.py,
                test,
                apps.py,
                __init__.py,
                urls.py,
                manage.py

# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=

# Use multiple processes to speed up Pylint.
jobs=0

# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=

# Pickle collected data for later comparisons.
persistent=yes

# Specify a configuration file.
#rcfile=

# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages
suggestion-mode=yes

# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no


[MESSAGES CONTROL]

# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=

# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=too-many-public-methods,
        len-as-condition,
        unused-argument,
        too-many-arguments,
        arguments-differ,
        line-too-long,
        fixme,
        missing-docstring,
        invalid-envvar-default,
        ungrouped-imports,
        bad-continuation,
        too-many-ancestors,
        too-few-public-methods,
        no-self-use,
        #print-statement,
        #parameter-unpacking,
        #unpacking-in-except,
        #old-raise-syntax,
        #backtick,
        #long-suffix,
        #old-ne-operator,
        #old-octal-literal,
        #import-star-module-level,
        #non-ascii-bytes-literal,
        #raw-checker-failed,
        #bad-inline-option,
        #locally-disabled,
        #locally-enabled,
        #file-ignored,
        #suppressed-message,
        #useless-suppression,
        #deprecated-pragma,
        #apply-builtin,
        #basestring-builtin,
        #buffer-builtin,
        #cmp-builtin,
        #coerce-builtin,
        #execfile-builtin,
        #file-builtin,
        #long-builtin,
        #raw_input-builtin,
        #reduce-builtin,
        #standarderror-builtin,
        #unicode-builtin,
        #xrange-builtin,
        #coerce-method,
        #delslice-method,
        #getslice-method,
        #setslice-method,
        #no-absolute-import,
        #old-division,
        #dict-iter-method,
        #dict-view-method,
        #next-method-called,
        #metaclass-assignment,
        #indexing-exception,
        #raising-string,
        #reload-builtin,
        #oct-method,
        #hex-method,
        #nonzero-method,
        #cmp-method,
        #input-builtin,
        #round-builtin,
        #intern-builtin,
        #unichr-builtin,
        #map-builtin-not-iterating,
        #zip-builtin-not-iterating,
        #range-builtin-not-iterating,
        #filter-builtin-not-iterating,
        #using-cmp-argument,
        #eq-without-hash,
        #div-method,
        #idiv-method,
        #rdiv-method,
        #exception-message-attribute,
        #invalid-str-codec,
        #sys-max-int,
        #bad-python3-import,
        #deprecated-string-function,
        #deprecated-str-translate-call,
        #deprecated-itertools-function,
        #deprecated-types-field,
        #next-method-defined,
        #dict-items-not-iterating,
        #dict-keys-not-iterating,
        #dict-values-not-iterating

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member


[REPORTS]

# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)

# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=

# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text

# Tells whether to display a full report or only the messages
reports=no

# Activate the evaluation score.
score=yes


[REFACTORING]

# Maximum number of nested blocks for function / method body
max-nested-blocks=5

# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=optparse.Values,sys.exit


[BASIC]

# Naming style matching correct argument names
argument-naming-style=snake_case

# Regular expression matching correct argument names. Overrides argument-
# naming-style
#argument-rgx=

# Naming style matching correct attribute names
attr-naming-style=snake_case

# Regular expression matching correct attribute names. Overrides attr-naming-
# style
#attr-rgx=

# Bad variable names which should always be refused, separated by a comma
bad-names=foo,
          bar,
          baz,
          toto,
          tutu,
          tata

# Naming style matching correct class attribute names
class-attribute-naming-style=any

# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style
#class-attribute-rgx=

# Naming style matching correct class names
class-naming-style=PascalCase

# Regular expression matching correct class names. Overrides class-naming-style
#class-rgx=

# Naming style matching correct constant names
const-naming-style=any

# Regular expression matching correct constant names. Overrides const-naming-
# style
#const-rgx=

# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=5

# Naming style matching correct function names
function-naming-style=snake_case

# Regular expression matching correct function names. Overrides function-
# naming-style
#function-rgx=

# Good variable names which should always be accepted, separated by a comma
good-names=i,
           do,
           f,
           df,
           s,
           j,
           k,
           ex,
           Run,
           _,
           db,
           r,
           x,
           y,
           e

# Include a hint for the correct naming format with invalid-name
include-naming-hint=no

# Naming style matching correct inline iteration names
inlinevar-naming-style=any

# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style
#inlinevar-rgx=

# Naming style matching correct method names
method-naming-style=snake_case

# Regular expression matching correct method names. Overrides method-naming-
# style
#method-rgx=

# Naming style matching correct module names
module-naming-style=snake_case

# Regular expression matching correct module names. Overrides module-naming-
# style
#module-rgx=

# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=

# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_

# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty

# Naming style matching correct variable names
variable-naming-style=snake_case

# Regular expression matching correct variable names. Overrides variable-
# naming-style
#variable-rgx=


[FORMAT]

# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=

# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$

# Number of spaces of indent required inside a hanging  or continued line.
indent-after-paren=4

# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
# tab).
indent-string='    '

# Maximum number of characters on a single line.
max-line-length=79

# Maximum number of lines in a module
max-module-lines=1000

# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
               dict-separator

# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no

# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no


[LOGGING]

# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging


[MISCELLANEOUS]

# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
      XXX,
      TODO


[SIMILARITIES]

# Ignore comments when computing similarities.
ignore-comments=yes

# Ignore docstrings when computing similarities.
ignore-docstrings=yes

# Ignore imports when computing similarities.
ignore-imports=no

# Minimum lines number of a similarity.
min-similarity-lines=4


[SPELLING]

# Limits count of emitted suggestions for spelling mistakes
max-spelling-suggestions=4

# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=

# List of comma separated words that should not be checked.
spelling-ignore-words=

# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=

# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no


[TYPECHECK]

# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager

# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=

# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes

# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes

# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local

# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=

# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes

# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1

# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1


[VARIABLES]

# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=

# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes

# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
          _cb

# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_

# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_

# Tells whether we should check for unused import in __init__ files.
init-import=no

# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins


[CLASSES]

# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
                      __new__,
                      setUp

# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
                  _fields,
                  _replace,
                  _source,
                  _make

# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls

# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs


[DESIGN]

# Maximum number of arguments for function / method
max-args=5

# Maximum number of attributes for a class (see R0902).
max-attributes=7

# Maximum number of boolean expressions in a if statement
max-bool-expr=5

# Maximum number of branch for function / method body
max-branches=20

# Maximum number of locals for function / method body
max-locals=20

# Maximum number of parents for a class (see R0901).
max-parents=7

# Maximum number of public methods for a class (see R0904).
max-public-methods=20

# Maximum number of return / yield for function / method body
max-returns=6

# Maximum number of statements in function / method body
max-statements=50

# Minimum number of public methods for a class (see R0903).
min-public-methods=2


[IMPORTS]

# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no

# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no

# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
                   TERMIOS,
                   Bastion,
                   rexec

# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=

# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=

# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=

# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=

# Force import order to recognize a module as part of a third party library.
known-third-party=enchant


[EXCEPTIONS]

# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception



================================================
FILE: .readthedocs.yaml
================================================
version: 2

build:
  os: ubuntu-22.04
  tools:
    python: "3.9"

mkdocs:
  configuration: mkdocs.yml

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 shidenggui

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include README.md


================================================
FILE: Makefile
================================================
test:
	pytest -vx --cov=easytrader tests


================================================
FILE: Pipfile
================================================
[[source]]
url = "http://mirrors.aliyun.com/pypi/simple/"
verify_ssl = false
name = "pypi"

[packages]
pywinauto = "*"
"bs4" = "*"
requests = "*"
dill = "*"
click = "*"
six = "*"
flask = "*"
pillow = "*"
pytesseract = "*"
pandas = "*"
pyperclip = "*"
easyutils = "*"

[dev-packages]
pytest-cov = "*"
pre-commit = "*"
pytest = "*"
pylint = "*"
mypy = "*"
isort = "*"
black = "==18.6b4"
ipython = "*"
better-exceptions = "*"

[requires]
python_version = "3.6"

[scripts]
sort_imports = "bash -c 'isort \"$@\"; git add -u' --"
format = "bash -c 'black -l 79 \"$@\"; git add -u' --"
lint = "pylint"
type_check = "mypy"
test = "bash -c 'pytest -vx --cov=easytrader tests'"
lock = "bash -c 'pipenv lock -r > requirements.txt'"


================================================
FILE: README.md
================================================
# easytrader

[![Package](https://img.shields.io/pypi/v/easytrader.svg)](https://pypi.python.org/pypi/easytrader)
[![License](https://img.shields.io/github/license/shidenggui/easytrader.svg)](https://github.com/shidenggui/easytrader/blob/master/LICENSE)

* 进行股票量化交易
* 通用的同花顺客户端模拟操作
* 支持券商的 [miniqmt](https://easytrader.readthedocs.io/zh-cn/master/miniqmt/) 官方量化接口
* 支持雪球组合调仓和跟踪
* 支持远程操作客户端
* 支持跟踪 `joinquant`, `ricequant` 的模拟交易


### 微信群以及公众号

欢迎大家扫码关注公众号「食灯鬼」,一起交流。进群可通过菜单加我好友,备注量化。

![公众号二维码](https://camo.githubusercontent.com/6fad032c27b30b68a9d942ae77f8cc73933b95cea58e684657d31b94a300afd5/68747470733a2f2f67697465652e636f6d2f73686964656e676775692f6173736574732f7261772f6d61737465722f755069632f6d702d71722e706e67)

若二维码因 Github 网络无法打开,请点击[公众号二维码](https://camo.githubusercontent.com/6fad032c27b30b68a9d942ae77f8cc73933b95cea58e684657d31b94a300afd5/68747470733a2f2f67697465652e636f6d2f73686964656e676775692f6173736574732f7261772f6d61737465722f755069632f6d702d71722e706e67)直接打开图片。

### Author

> Blog [@shidenggui](https://shidenggui.com) · Weibo [@食灯鬼](https://www.weibo.com/u/1651274491) · Twitter [@shidenggui](https://twitter.com/shidenggui)

### 相关

* [easyquotation 实时获取全市场股票行情](https://github.com/shidenggui/easyquotation)
* [easyquant 简单的量化框架](https://github.com/shidenggui/easyquant)


### 模拟交易

* 雪球组合 by @[haogefeifei](https://github.com/haogefeifei)([说明](docs/xueqiu.md))

### 使用文档

[中文文档](https://easytrader.readthedocs.io/)


================================================
FILE: docs/follow.md
================================================
# 策略跟踪 

## 跟踪 `joinquant` / `ricequant`  的模拟交易

##### 1) 初始化跟踪的 trader

这里以雪球为例, 也可以使用银河之类 `easytrader` 支持的券商

```
xq_user = easytrader.use('xq')
xq_user.prepare('xq.json')
```

##### 2) 初始化跟踪 `joinquant` / `ricequant` 的 follower

```
target = 'jq'  # joinquant
target = 'rq'  # ricequant
follower = easytrader.follower(target)
follower.login(user='rq/jq用户名', password='rq/jq密码')
```

##### 3) 连接 follower 和 trader

##### joinquant
```
follower.follow(xq_user, 'jq的模拟交易url')
```

注: jq的模拟交易url指的是对应模拟交易对应的可以查看持仓, 交易记录的页面, 类似 `https://www.joinquant.com/algorithm/live/index?backtestId=xxx`

正常会输出

![enjoy it](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/joinquant.jpg)

注: 启动后发现跟踪策略无输出,那是因为今天模拟交易没有调仓或者接收到的调仓信号过期了,默认只处理120s内的信号,想要测试的可以用下面的命令:

```python
jq_follower.follow(user, '模拟交易url',
          trade_cmd_expire_seconds=100000000000, cmd_cache=False)
```

- trade_cmd_expire_seconds 默认处理多少秒内的信号

- cmd_cache 是否读取已经执行过的命令缓存,以防止重复执行

目录下产生的 cmd_cache.pk,是用来存储历史执行过的交易指令,防止在重启程序时重复执行交易过的指令,可以通过 `follower.follow(xxx, cmd_cache=False)` 来关闭。

##### ricequant

```
follower.follow(xq_user, run_id)
```
注:ricequant的run_id即PT列表中的ID。


## 跟踪雪球的组合

##### 1) 初始化跟踪的 trader

同上

##### 2) 初始化跟踪 雪球组合 的 follower

```
xq_follower = easytrader.follower('xq')
xq_follower.login(cookies='雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/')
```

##### 3) 连接 follower 和 trader

```
xq_follower.follow(xq_user, 'xq组合ID,类似ZH123456', total_assets=100000)
```


注: 雪球组合是以百分比调仓的, 所以需要额外设置组合对应的资金额度

* 这里可以设置 total_assets, 为当前组合的净值对应的总资金额度, 具体可以参考参数说明
* 或者设置 initial_assets, 这时候总资金额度为 initial_assets * 组合净值

* 雪球额外支持 adjust_sell 参数,决定是否根据用户的实际持仓数调整卖出股票数量,解决雪球根据百分比调仓时计算出的股数有偏差的问题。当卖出股票数大于实际持仓数时,调整为实际持仓数。目前仅在银河客户端测试通过。 当 users 为多个时,根据第一个 user 的持仓数决定


#### 3. 多用户跟踪多策略

```
follower.follow(users=[xq_user, yh_user], strategies=['组合1', '组合2'], total_assets=[10000, 10000])
```

#### 4. 其它与跟踪有关的问题

使用市价单跟踪模式,目前仅支持银河

```
follower.follow(***, entrust_prop='market')
```

调整下单间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足

```
follower.follow(***, send_interval=30) # 设置下单间隔为 30 s
```
设置买卖时的滑点

```
follower.follow(***, slippage=0.05) # 设置滑点为 5%
```


================================================
FILE: docs/index.md
================================================
# 简介

* 通用的同花顺客户端模拟操作
* 支持券商的 [miniqmt](miniqmt.md) 官方量化接口
* 支持雪球组合调仓和跟踪
* 支持远程操作客户端
* 支持跟踪 `joinquant`, `ricequant` 的模拟交易

### 加微信群以及公众号

欢迎大家扫码关注公众号"食灯鬼",通过菜单加我好友,备注量化进群

![JDRUhz](https://camo.githubusercontent.com/6fad032c27b30b68a9d942ae77f8cc73933b95cea58e684657d31b94a300afd5/68747470733a2f2f67697465652e636f6d2f73686964656e676775692f6173736574732f7261772f6d61737465722f755069632f6d702d71722e706e67)


### 支持券商


* 海通客户端(海通网上交易系统独立委托)
* 华泰客户端(网上交易系统(专业版Ⅱ))
* 国金客户端(全能行证券交易终端PC版)
* 通用同花顺客户端(同花顺免费版)
* 其他券商专用同花顺客户端(需要手动登陆)


### 模拟交易

* 雪球组合 by @[haogefeifei](https://github.com/haogefeifei)([说明](xueqiu.md))



### 作者

> Blog [@shidenggui](https://shidenggui.com) · Weibo [@食灯鬼](https://www.weibo.com/u/1651274491) · Twitter [@shidenggui](https://twitter.com/shidenggui)
>

**其他作品**

* [easyquotation 实时获取全市场股票行情](https://github.com/shidenggui/easyquotation)
* [easyquant 简单的量化框架](https://github.com/shidenggui/easyqutant)




================================================
FILE: docs/install.md
================================================
# 安装

### 同花顺客户端设置

需要对客户端按以下设置,不然会导致下单时价格出错以及客户端超时锁定

* 系统设置 > 界面设置: 界面不操作超时时间设为 0
* 系统设置 > 交易设置: 默认买入价格/买入数量/卖出价格/卖出数量 都设置为 空

同时客户端不能最小化也不能处于精简模式

### 云端部署建议

在云服务上部署时,使用自带的远程桌面会有问题,推荐使用 TightVNC

### 登陆时的验证码识别

券商如果登陆需要识别验证码的话需要安装 tesseract:

* `tesseract` : 非 `pytesseract`, 需要单独安装, [地址](https://github.com/tesseract-ocr/tesseract/wiki),保证在命令行下 `tesseract` 可用

或者你也可以手动登陆后在通过 `easytrader` 调用,此时 `easytrader` 在登陆过程中会直接识别到已登陆的窗口。

### 安装

```shell
pip install easytrader
```

### 升级

```shell
pip install easytrader -U
```



================================================
FILE: docs/miniqmt.md
================================================
# miniqmt

miniqmt 是券商官方的低门槛 Python 量化交易接口,基于券商的讯投 QMT 服务。详情可以[进群](https://easytrader.readthedocs.io/zh-cn/master/#_2)交流。

## 安装 miniqmt 组件

miniqmt 功能依赖 `xtquant` 库,因为这个库比较大(100 MB+),所以需要单独安装

```python
pip install easytrader[miniqmt]
``` 

## 引入

```python
import easytrader
```

## 初始化客户端

```python
user = easytrader.use('miniqmt')
```

## 连接 QMT 客户端

需要通过 `connect` 方法连接到 QMT 客户端。

**注意:登录 QMT 客户端时必须勾选极简模式/独立交易模式,否则无法连接**

```python
user.connect(
    miniqmt_path=r"D:\国金证券QMT交易端\userdata_mini",  # QMT 客户端下的 miniqmt 安装路径
    stock_account="你的资金账号",  # 资金账号
    trader_callback=None, # 默认使用 `easytrader.miniqmt.DefaultXtQuantTraderCallback`
)
```

**参数说明:**

- `miniqmt_path`: QMT 客户端下的 miniqmt 安装路径,例如 `r"D:\国金证券QMT交易端\userdata_mini"`
    - 注意:不建议安装在 C 盘。在 C 盘每次都需要用管理员权限运行客户端,才能正常连接
- `stock_account`: 资金账号
- `trader_callback`: 交易回调对象,默认使用 `easytrader.miniqmt.DefaultXtQuantTraderCallback`

## 交易相关

### 获取资金状况

```python
user.balance

# return
# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%B5%84%E4%BA%A7xtasset
[{
  'total_asset': 1000000.0,  # 总资产
  'market_value': 400000.0,  # 持仓市值
  'cash': 600000.0,  # 可用资金
  'frozen_cash': 0.0,  # 冻结资金
  'account_type': 2,  # 账户类型
  'account_id': '你的资金账号'  # 账户ID
}]
```

### 获取持仓

```python
user.position

# return
# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%8C%81%E4%BB%93xtposition
[{'security': '162411',
  'stock_code': '162411.SZ',
  'volume': 100,
  'can_use_volume': 100,
  'open_price': 0.618,
  'market_value': 63.8,
  'frozen_volume': 0,
  'on_road_volume': 0,
  'yesterday_volume': 100,
  'avg_price': 0.618,
  'direction': 48,
  'account_type': 2,
  'account_id': '1111111111'}]

```

### 限价买入

```python
user.buy('600036', price=35.5, amount=100)

# return
{'entrust_no': 123456}
```

**注意事项**

- 成功发送委托后的订单编号为大于 0 的正整数,如果为 -1 表示委托失败,失败具体原因请查看 `DefaultXtQuantTraderCallback.on_order_error` 回调
- 注:非交易时间下单可以拿到订单编号,但 `on_order_error` 回调会报错:
  ```
  下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]
  ```

### 限价卖出

```python
user.sell('600036', price=36.0, amount=100)

# return
{'entrust_no': 123456}
```

### 市价买入

```python
user.market_buy('600036', amount=100, ttype='对手方最优价格委托')

# return
{'entrust_no': 123456}
```

**市价委托类型(ttype)可选值**:

深市可选:

- 对手方最优价格委托(默认)
- 本方最优价格委托
- 即时成交剩余撤销委托
- 最优五档即时成交剩余撤销
- 全额成交或撤销委托

沪市可选:

- 对手方最优价格委托(默认)
- 最优五档即时成交剩余撤销
- 最优五档即时成交剩转限价
- 本方最优价格委托

### 市价卖出

```python
user.market_sell('600036', amount=100, ttype='对手方最优价格委托')

# return
{'entrust_no': 123456}
```

### 撤单

```python
user.cancel_entrust(123456)  # 传入之前买入或卖出时返回的订单编号

# return
{'success': True, 'message': 'success'} # 成功
{'success': False, 'message': 'failed'} # 失败
```

### 查询当日委托

```python
user.today_entrusts

# return
# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E5%A7%94%E6%89%98xtorder
[{'security': '162411',
  'stock_code': '162411.SZ',
  'order_id': 3456,
  'order_sysid': '1111',
  'order_time': 1634278451,
  'order_type': 23,
  'order_type_name': '买入', # ['买入', '卖出']
  'order_volume': 100,
  'price_type': 50,
  'price_type_name': '限价',
  'price': 0.62,
  'traded_volume': 100,
  'traded_price': 0.613,
  'order_status': 56,
  'order_status_name': '已成', # ['已报', '已成', '部成', '已撤', '部撤']
  'status_msg': '',
  'offset_flag': 48,
  'offset_flag_name': '买入', # ['买入', '卖出']
  'strategy_name': '',
  'order_remark': '',
  'direction': 48,
  'direction_name': '多', # ['多', '空']
  'account_type': 2,
  'account_id': '1111111111'}]
```

### 查询当日成交

```python
user.today_trades

# return
# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%88%90%E4%BA%A4xttrade
[{'security': '162411',
  'stock_code': '162411.SZ',
  'traded_id': '0303222200422222',
  'traded_time': 1634278451,
  'traded_price': 0.613,
  'traded_volume': 100,
  'traded_amount': 61.3,
  'order_id': 1111,
  'order_type': 23,
  'order_type_name': '买入',
  'offset_flag': 48,
  'offset_flag_name': '买入',
  'account_id': '1111111111',
  'account_type': 2,
  'order_sysid': '1111',
  'strategy_name': '',
  'order_remark': ''}]
```


## 进阶功能

### 获取原始交易对象

通过获取原始对象,可以直接调用 miniqmt 的接口进行更多高级操作,具体请参考 [miniqmt 官方文档](https://dict.thinktrader.net/nativeApi/xttrader.html)

```python
# 获取 XtQuantTrader 对象
trader = user.trader

# 获取 StockAccount 对象
account = user.account
```


### 2. 交易回调处理

MiniqmtTrader 默认使用 `DefaultXtQuantTraderCallback` 类处理交易回调,但您可以通过继承 `XtQuantTraderCallback` 类来创建自定义回调处理:

```python
from xtquant.xttrader import XtQuantTraderCallback

class MyTraderCallback(XtQuantTraderCallback):
    def on_disconnected(self):
        print("连接断开")

    def on_account_status(self, status):
        print(f"账户状态: {status.account_id}, 状态: {status.status}")

    def on_stock_order(self, order):
        print(f"委托回调: {order.stock_code}, 状态: {order.order_status}")

    def on_stock_trade(self, trade):
        print(f"成交回调: {trade.stock_code}, 价格: {trade.traded_price}")

    def on_order_error(self, order_error):
        print(f"下单失败: {order_error.order_id}, 错误: {order_error.error_msg}")

    def on_cancel_error(self, cancel_error):
        print(f"撤单失败: {cancel_error.order_id}, 错误: {cancel_error.error_msg}")

# 连接时使用自定义回调
user.connect(
    miniqmt_path=r"D:\国金证券QMT交易端\userdata_mini",
    stock_account="你的资金账号",
    trader_callback=MyTraderCallback()
)
```

================================================
FILE: docs/remote.md
================================================
# 远端服务模式

远端服务模式是交易服务端和量化策略端分离的模式。

**交易服务端**通常是有固定`IP`地址的云服务器,该服务器上运行着`easytrader`交易服务。而**量化策略端**可能是`JoinQuant、RiceQuant、Vn.Py`,物理上与交易服务端不在同一台电脑上。交易服务端被动或主动获取交易信号,并驱动**交易软件**(交易软件包括运行在同一服务器上的下单软件,比如同花顺`xiadan.exe`,或者运行在另一台服务器上的雪球`xq`)。


## 交易服务端——启动服务

```python
from easytrader import server

server.run(port=1430) # 默认端口为 1430
```

## 量化策略端——调用服务

```python
from easytrader import remoteclient

user = remoteclient.use('使用客户端类型,可选 yh_client, ht_client, ths, xq等', host='服务器ip', port='服务器端口,默认为1430')

user.buy(......)

user.sell(......)
```




================================================
FILE: docs/usage.md
================================================
# 使用

## 引入

```python
import easytrader
```

## 设置同花顺客户端类型

**通用同花顺客户端**

```python
user = easytrader.use('universal_client') 
```

注: 通用同花顺客户端是指同花顺官网提供的客户端软件内的下单程序,内含对多个券商的交易支持,适用于券商不直接提供同花顺客户端时的后备方案。

**其他券商专用同花顺客户端**

```python
user = easytrader.use('ths')
```

注: 其他券商专用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本,类似银河的双子星(同花顺版本),国金证券网上交易独立下单程序(核新PC版)等。


**雪球组合**

```python
user = easytrader.use('xq')
```

**国金客户端**

```python
user = easytrader.use('gj_client') 
```

**海通客户端**

```python
user = easytrader.use('htzq_client')
```

**华泰客户端**

```python
user = easytrader.use('ht_client')
```


## 启动并连接客户端

### (一)其他券商专用同花顺客户端

其他券商专用同花顺客户端不支持自动登录,需要先手动登录。

请手动打开并登录客户端后,运用connect函数连接客户端。

```python
user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe'
```

### (二)通用同花顺客户端

需要先手动登录一次:添加券商,填入账户号、密码、验证码,勾选“保存密码”

第一次登录后,上述信息被缓存,可以调用prepare函数自动登录(仅需账户号、客户端路径,密码随意输入)。

### (三)其它

非同花顺的客户端,可以调用prepare函数自动登录。

调用prepare时所需的参数,可以通过`函数参数` 或 `配置文件` 赋予。

**1. 函数参数(推荐)**

```
user.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码,其他券商不用')
```

注: 雪球比较特殊,见下列配置文件格式

**2. 配置文件**

```python
user.prepare('/path/to/your/yh_client.json')  # 配置文件路径
```

注: 配置文件需自己用编辑器编辑生成, **请勿使用记事本**, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) 。

**配置文件格式如下:**

银河/国金客户端

```
{
  "user": "用户名",
  "password": "明文密码"
}

```

华泰客户端

```
{
   "user": "华泰用户名",
   "password": "华泰明文密码",
   "comm_password": "华泰通讯密码"
}

```

雪球

```
{
  "cookies": "雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/",
  "portfolio_code": "组合代码(例:ZH818559)",
  "portfolio_market": "交易市场(例:us 或者 cn 或者 hk)"
}
```

## 交易相关

有些客户端无法通过默认方法输入文本,可以通过开启 type_keys 的方法绕过,开启方式

```python
user.enable_type_keys_for_editor()
```

###  获取资金状况

```python
user.balance

# return
[{'参考市值': 21642.0,
  '可用资金': 28494.21,
  '币种': '0',
  '总资产': 50136.21,
  '股份参考盈亏': -90.21,
  '资金余额': 28494.21,
  '资金帐号': 'xxx'}]
```

### 获取持仓

```python
user.position

# return
[{'买入冻结': 0,
  '交易市场': '沪A',
  '卖出冻结': '0',
  '参考市价': 4.71,
  '参考市值': 10362.0,
  '参考成本价': 4.672,
  '参考盈亏': 82.79,
  '当前持仓': 2200,
  '盈亏比例(%)': '0.81%',
  '股东代码': 'xxx',
  '股份余额': 2200,
  '股份可用': 2200,
  '证券代码': '601398',
  '证券名称': '工商银行'}]
```

### 买入

```python
user.buy('162411', price=0.55, amount=100)

# return
{'entrust_no': 'xxxxxxxx'}
```

注: 系统可以配置是否返回成交回报。如果没配的话默认返回 `{"message": "success"}`

### 卖出

```python
user.sell('162411', price=0.55, amount=100)

# return
{'entrust_no': 'xxxxxxxx'}
```


### 撤单

```python
user.cancel_entrust('buy/sell 获取的 entrust_no')

# return
{'message': 'success'}
```

### 查询当日成交

```python
user.today_trades

# return
[{'买卖标志': '买入',
  '交易市场': '深A',
  '委托序号': '12345',
  '成交价格': 0.626,
  '成交数量': 100,
  '成交日期': '20170313',
  '成交时间': '09:50:30',
  '成交金额': 62.60,
  '股东代码': 'xxx',
  '证券代码': '162411',
  '证券名称': '华宝油气'}]
```

### 查询当日委托

```python
user.today_entrusts

# return
[{'买卖标志': '买入',
  '交易市场': '深A',
  '委托价格': 0.627,
  '委托序号': '111111',
  '委托数量': 100,
  '委托日期': '20170313',
  '委托时间': '09:50:30',
  '成交数量': 100,
  '撤单数量': 0,
  '状态说明': '已成',
  '股东代码': 'xxxxx',
  '证券代码': '162411',
  '证券名称': '华宝油气'},
 {'买卖标志': '买入',
  '交易市场': '深A',
  '委托价格': 0.6,
  '委托序号': '1111',
  '委托数量': 100,
  '委托日期': '20170313',
  '委托时间': '09:40:30',
  '成交数量': 0,
  '撤单数量': 100,
  '状态说明': '已撤',
  '股东代码': 'xxx',
  '证券代码': '162411',
  '证券名称': '华宝油气'}]
```


### 查询今日可申购新股

```python
from easytrader.utils.stock import get_today_ipo_data
get_today_ipo_data()

# return
[{'stock_code': '股票代码',
  'stock_name': '股票名称',
  'price': 发行价,
  'apply_code': '申购代码'}]
```

### 一键打新

```python
user.auto_ipo()
```

### 刷新数据

```python
user.refresh()
```

### 雪球组合比例调仓 

```python
user.adjust_weight('股票代码', 目标比例)
```

例如 `user.adjust_weight('000001', 10)`是将平安银行在组合中的持仓比例调整到10%。

## 退出客户端软件

```python
user.exit()
```

## 常见问题

### 某些同花顺客户端不允许拷贝 `Grid` 数据

现在默认获取 `Grid` 数据的策略是通过剪切板拷贝,有些券商不允许这种方式,导致无法获取持仓等数据。为解决此问题,额外实现了一种通过将 `Grid` 数据存为文件再读取的策略,
使用方式如下:

```python
from easytrader import grid_strategies

user.grid_strategy = grid_strategies.Xls
```

### 通过工具栏刷新按钮刷新数据

当前的刷新数据方式是通过切换菜单栏实现,通用但是比较缓慢,可以选择通过点击工具栏的刷新按钮来刷新

```python
from easytrader import refresh_strategies

# refresh_btn_index 指的是刷新按钮在工具栏的排序,默认为第四个,根据客户端实际情况调整
user.refresh_strategy = refresh_strategies.Toolbar(refresh_btn_index=4)
```

### 无法保存对应的 xls 文件

有些系统默认的临时文件目录过长,使用 xls 策略时无法正常保存,可通过如下方式修改为自定义目录

```
user.grid_strategy_instance.tmp_folder = 'C:\\custom_folder'
```

### 如何关闭 debug 日志的输出

```python
user = easytrader.use('yh', debug=False)

```


# 编辑配置文件,运行后出现 `json` 解码报错


出现如下错误

```python
raise JSONDecodeError("Expecting value", s, err.value) from None

JSONDecodeError: Expecting value
```

请勿使用 `记事本` 编辑账户的 `json` 配置文件,推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/)



================================================
FILE: docs/xueqiu.md
================================================
# 雪球组合模拟交易

因为雪球组合是按比例调仓的,所以模拟成券商实盘接口会有一些要注意的问题

* 接口基本与其他券商接口调用参数返回一致
* 委托单不支持挂高挂低(开盘时间都是直接市价成交的)
* 初始资金是按组合净值 1:1000000 换算来的, 可以通过 `easytrader.use('xq', initial_assets=初始资金值)` 来调整
* 委托单的委托价格和委托数量目前换算回来都是按1手拆的(雪球是按比例调仓的)
* 持仓价格和持仓数量问题同上, 但持股市值是对的.
* 一些不合理的操作会直接抛TradeError,注意看错误信息
          
----------------
20160909 新增函数adjust_weight,用于雪球组合比例调仓
             
adjust_weight函数包含两个参数,stock_code 指定调仓股票代码,weight 指定调仓比例      



================================================
FILE: easytrader/__init__.py
================================================
# -*- coding: utf-8 -*-
import urllib3

from easytrader import exceptions
from easytrader.api import use, follower
from easytrader.log import logger

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

__version__ = "0.23.7"
__author__ = "shidenggui"


================================================
FILE: easytrader/api.py
================================================
# -*- coding: utf-8 -*-
import logging
import sys

import six

from easytrader.joinquant_follower import JoinQuantFollower
from easytrader.log import logger
from easytrader.ricequant_follower import RiceQuantFollower
from easytrader.xq_follower import XueQiuFollower
from easytrader.xqtrader import XueQiuTrader

if sys.version_info <= (3, 5):
    raise TypeError("不支持 Python3.5 及以下版本,请升级")


def use(broker, debug=False, **kwargs):
    """用于生成特定的券商对象
    :param broker: 券商名支持 
        例如 ['miniqmt', 'xq', '雪球', 'gj_client', '国金客户端', "universal_client", "通用同花顺客户端", "ths", "同花顺客户端"] 等
    :param debug: 控制 debug 日志的显示, 默认为 False
    :param initial_assets: [雪球参数] 控制雪球初始资金,默认为一百万
    :return the class of trader

    Usage::

        >>> import easytrader
        >>> user = easytrader.use('xq')
        >>> user.prepare('xq.json')
    """
    if debug:
        logger.setLevel(logging.DEBUG)

    if broker.lower() in ["xq", "雪球"]:
        return XueQiuTrader(**kwargs)

    if broker.lower() in ["yh_client", "银河客户端"]:
        from .yh_clienttrader import YHClientTrader

        return YHClientTrader()

    if broker.lower() in ["ht_client", "华泰客户端"]:
        from .ht_clienttrader import HTClientTrader

        return HTClientTrader()

    if broker.lower() in ["wk_client", "五矿客户端"]:
        from easytrader.wk_clienttrader import WKClientTrader

        return WKClientTrader()

    if broker.lower() in ["htzq_client", "海通证券客户端"]:
        from easytrader.htzq_clienttrader import HTZQClientTrader

        return HTZQClientTrader()

    if broker.lower() in ["gj_client", "国金客户端"]:
        from .gj_clienttrader import GJClientTrader

        return GJClientTrader()

    if broker.lower() in ["gf_client", "广发客户端"]:
        from .gf_clienttrader import GFClientTrader

        return GFClientTrader()

    if broker.lower() in ["universal_client", "通用同花顺客户端"]:
        from easytrader.universal_clienttrader import UniversalClientTrader

        return UniversalClientTrader()

    if broker.lower() in ["ths", "同花顺客户端"]:
        from .clienttrader import ClientTrader

        return ClientTrader()
    
    if broker.lower() in ["miniqmt"]:
        try:
            import xtquant
        except:
            logger.error("miniqmt 相关组件 xtqimt 未安装, 请执行 pip install easytrader[xtquant]安装")
        from easytrader.miniqmt.miniqmt_trader import MiniqmtTrader

        return MiniqmtTrader()

    raise NotImplementedError


def follower(platform, **kwargs):
    """用于生成特定的券商对象
    :param platform:平台支持 ['jq', 'joinquant', '聚宽’]
    :param initial_assets: [雪球参数] 控制雪球初始资金,默认为一万,
        总资金由 initial_assets * 组合当前净值 得出
    :param total_assets: [雪球参数] 控制雪球总资金,无默认值,
        若设置则覆盖 initial_assets
    :return the class of follower

    Usage::

        >>> import easytrader
        >>> user = easytrader.use('xq')
        >>> user.prepare('xq.json')
        >>> jq = easytrader.follower('jq')
        >>> jq.login(user='username', password='password')
        >>> jq.follow(users=user, strategies=['strategies_link'])
    """
    if platform.lower() in ["rq", "ricequant", "米筐"]:
        return RiceQuantFollower()
    if platform.lower() in ["jq", "joinquant", "聚宽"]:
        return JoinQuantFollower()
    if platform.lower() in ["xq", "xueqiu", "雪球"]:
        return XueQiuFollower(**kwargs)
    raise NotImplementedError


================================================
FILE: easytrader/clienttrader.py
================================================
# -*- coding: utf-8 -*-
import abc
import functools
import logging
import os
import re
import sys
import time
from typing import Type, Union

import hashlib, binascii

import easyutils
from pywinauto import findwindows, timings

from easytrader import grid_strategies, pop_dialog_handler, refresh_strategies
from easytrader.config import client
from easytrader.grid_strategies import IGridStrategy
from easytrader.log import logger
from easytrader.refresh_strategies import IRefreshStrategy
from easytrader.utils.misc import file2dict
from easytrader.utils.perf import perf_clock

if not sys.platform.startswith("darwin"):
    import pywinauto
    import pywinauto.clipboard

class IClientTrader(abc.ABC):
    @property
    @abc.abstractmethod
    def app(self):
        """Return current app instance"""
        pass

    @property
    @abc.abstractmethod
    def main(self):
        """Return current main window instance"""
        pass

    @property
    @abc.abstractmethod
    def config(self):
        """Return current config instance"""
        pass

    @abc.abstractmethod
    def wait(self, seconds: float):
        """Wait for operation return"""
        pass

    @abc.abstractmethod
    def refresh(self):
        """Refresh data"""
        pass

    @abc.abstractmethod
    def is_exist_pop_dialog(self):
        pass


class ClientTrader(IClientTrader):
    _editor_need_type_keys = False
    # The strategy to use for getting grid data
    grid_strategy: Union[IGridStrategy, Type[IGridStrategy]] = grid_strategies.Copy
    _grid_strategy_instance: IGridStrategy = None
    refresh_strategy: IRefreshStrategy = refresh_strategies.Switch()

    def enable_type_keys_for_editor(self):
        """
        有些客户端无法通过 set_edit_text 方法输入内容,可以通过使用 type_keys 方法绕过
        """
        self._editor_need_type_keys = True

    @property
    def grid_strategy_instance(self):
        if self._grid_strategy_instance is None:
            self._grid_strategy_instance = (
                self.grid_strategy
                if isinstance(self.grid_strategy, IGridStrategy)
                else self.grid_strategy()
            )
            self._grid_strategy_instance.set_trader(self)
        return self._grid_strategy_instance

    def __init__(self):
        self._config = client.create(self.broker_type)
        self._app = None
        self._main = None
        self._toolbar = None

    @property
    def app(self):
        return self._app

    @property
    def main(self):
        return self._main

    @property
    def config(self):
        return self._config

    def connect(self, exe_path=None, **kwargs):
        """
        直接连接登陆后的客户端
        :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe'
        :return:
        """
        connect_path = exe_path or self._config.DEFAULT_EXE_PATH
        if connect_path is None:
            raise ValueError(
                "参数 exe_path 未设置,请设置客户端对应的 exe 地址,类似 C:\\客户端安装目录\\xiadan.exe"
            )

        self._app = pywinauto.Application().connect(path=connect_path, timeout=10)
        self._close_prompt_windows()
        self._main = self._app.top_window()
        self._init_toolbar()

    @property
    def broker_type(self):
        return "ths"

    @property
    def balance(self):
        self._switch_left_menus(["查询[F4]", "资金股票"])

        return self._get_balance_from_statics()

    def _init_toolbar(self):
        self._toolbar = self._main.child_window(class_name="ToolbarWindow32")

    def _get_balance_from_statics(self):
        result = {}
        for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items():
            result[key] = float(
                self._main.child_window(
                    control_id=control_id, class_name="Static"
                ).window_text()
            )
        return result

    @property
    def position(self):
        self._switch_left_menus(["查询[F4]", "资金股票"])

        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)

    @property
    def today_entrusts(self):
        self._switch_left_menus(["查询[F4]", "当日委托"])

        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)

    @property
    def today_trades(self):
        self._switch_left_menus(["查询[F4]", "当日成交"])

        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)

    @property
    def cancel_entrusts(self):
        self.refresh()
        self._switch_left_menus(["撤单[F3]"])

        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)

    @perf_clock
    def cancel_entrust(self, entrust_no):
        self.refresh()
        for i, entrust in enumerate(self.cancel_entrusts):
            if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no:
                self._cancel_entrust_by_double_click(i)
                return self._handle_pop_dialogs()
        return {"message": "委托单状态错误不能撤单, 该委托单可能已经成交或者已撤"}

    def cancel_all_entrusts(self):
        self.refresh()
        self._switch_left_menus(["撤单[F3]"])

        # 点击全部撤销控件
        self._app.top_window().child_window(
            control_id=self._config.TRADE_CANCEL_ALL_ENTRUST_CONTROL_ID, class_name="Button", title_re="""全撤.*"""
        ).click()
        self.wait(0.2)

        # 等待出现 确认兑换框
        if self.is_exist_pop_dialog():
            # 点击是 按钮
            w = self._app.top_window()
            if w is not None:
                btn = w["是(Y)"]
                if btn is not None:
                    btn.click()
                    self.wait(0.2)

        # 如果出现了确认窗口
        self.close_pop_dialog()

    @perf_clock
    def repo(self, security, price, amount, **kwargs):
        self._switch_left_menus(["债券回购", "融资回购(正回购)"])

        return self.trade(security, price, amount)

    @perf_clock
    def reverse_repo(self, security, price, amount, **kwargs):
        self._switch_left_menus(["债券回购", "融劵回购(逆回购)"])

        return self.trade(security, price, amount)

    @perf_clock
    def buy(self, security, price, amount, **kwargs):
        self._switch_left_menus(["买入[F1]"])

        return self.trade(security, price, amount)

    @perf_clock
    def sell(self, security, price, amount, **kwargs):
        self._switch_left_menus(["卖出[F2]"])

        return self.trade(security, price, amount)

    @perf_clock
    def market_buy(self, security, amount, ttype=None, limit_price=None, **kwargs):
        """
        市价买入
        :param security: 六位证券代码
        :param amount: 交易数量
        :param ttype: 市价委托类型,默认客户端默认选择,
                     深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销']
                     沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价']
        :param limit_price: 科创板 限价

        :return: {'entrust_no': '委托单号'}
        """
        self._switch_left_menus(["市价委托", "买入"])

        return self.market_trade(security, amount, ttype, limit_price=limit_price)

    @perf_clock
    def market_sell(self, security, amount, ttype=None, limit_price=None, **kwargs):
        """
        市价卖出
        :param security: 六位证券代码
        :param amount: 交易数量
        :param ttype: 市价委托类型,默认客户端默认选择,
                     深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销']
                     沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价']
        :param limit_price: 科创板 限价
        :return: {'entrust_no': '委托单号'}
        """
        self._switch_left_menus(["市价委托", "卖出"])

        return self.market_trade(security, amount, ttype, limit_price=limit_price)

    def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs):
        """
        市价交易
        :param security: 六位证券代码
        :param amount: 交易数量
        :param ttype: 市价委托类型,默认客户端默认选择,
                     深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销']
                     沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价']

        :return: {'entrust_no': '委托单号'}
        """
        code = security[-6:]
        self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code)
        if ttype is not None:
            retry = 0
            retry_max = 10
            while retry < retry_max:
                try:
                    self._set_market_trade_type(ttype)
                    break
                except:
                    retry += 1
                    self.wait(0.1)
        self._set_market_trade_params(security, amount, limit_price=limit_price)
        self._submit_trade()

        return self._handle_pop_dialogs(
            handler_class=pop_dialog_handler.TradePopDialogHandler
        )

    def _set_market_trade_type(self, ttype):
        """根据选择的市价交易类型选择对应的下拉选项"""
        selects = self._main.child_window(
            control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, class_name="ComboBox"
        )
        for i, text in enumerate(selects.texts()):
            # skip 0 index, because 0 index is current select index
            if i == 0:
                if re.search(ttype, text):  # 当前已经选中
                    return
                else:
                    continue
            if re.search(ttype, text):
                selects.select(i - 1)
                return
        raise TypeError("不支持对应的市价类型: {}".format(ttype))

    def _set_stock_exchange_type(self, ttype):
        """根据选择的市价交易类型选择对应的下拉选项"""
        selects = self._main.child_window(
            control_id=self._config.TRADE_STOCK_EXCHANGE_CONTROL_ID, class_name="ComboBox"
        )

        for i, text in enumerate(selects.texts()):
            # skip 0 index, because 0 index is current select index
            if i == 0:
                if ttype.strip() == text.strip():  # 当前已经选中
                    return
                else:
                    continue
            if ttype.strip() == text.strip():
                selects.select(i - 1)
                return
        raise TypeError("不支持对应的市场类型: {}".format(ttype))

    def auto_ipo(self):
        self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH)

        stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)

        if len(stock_list) == 0:
            return {"message": "今日无新股"}
        invalid_list_idx = [
            i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0
        ]

        if len(stock_list) == len(invalid_list_idx):
            return {"message": "没有发现可以申购的新股"}

        self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID)
        self.wait(0.1)

        for row in invalid_list_idx:
            self._click_grid_by_row(row)
        self.wait(0.1)

        self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID)
        self.wait(0.1)

        return self._handle_pop_dialogs()

    def _click_grid_by_row(self, row):
        x = self._config.COMMON_GRID_LEFT_MARGIN
        y = (
            self._config.COMMON_GRID_FIRST_ROW_HEIGHT
            + self._config.COMMON_GRID_ROW_HEIGHT * row
        )
        self._app.top_window().child_window(
            control_id=self._config.COMMON_GRID_CONTROL_ID,
            class_name="CVirtualGridCtrl",
        ).click(coords=(x, y))

    @perf_clock
    def is_exist_pop_dialog(self):
        self.wait(0.5)  # wait dialog display
        try:
            return (
                self._main.wrapper_object() != self._app.top_window().wrapper_object()
            )
        except (
            findwindows.ElementNotFoundError,
            timings.TimeoutError,
            RuntimeError,
        ) as ex:
            logger.exception("check pop dialog timeout")
            return False

    @perf_clock
    def close_pop_dialog(self):
        try:
            if self._main.wrapper_object() != self._app.top_window().wrapper_object():
                w = self._app.top_window()
                if w is not None:
                    w.close()
                    self.wait(0.2)
        except (
                findwindows.ElementNotFoundError,
                timings.TimeoutError,
                RuntimeError,
        ) as ex:
            pass

    def _run_exe_path(self, exe_path):
        return os.path.join(os.path.dirname(exe_path), "xiadan.exe")

    def wait(self, seconds):
        time.sleep(seconds)

    def exit(self):
        self._app.kill()

    def _close_prompt_windows(self):
        self.wait(1)
        for window in self._app.windows(class_name="#32770", visible_only=True):
            title = window.window_text()
            if title != self._config.TITLE:
                logging.info("close window %s" % title)
                window.close()
                self.wait(0.2)
        self.wait(1)

    def close_pormpt_window_no_wait(self):
        for window in self._app.windows(class_name="#32770"):
            if window.window_text() != self._config.TITLE:
                window.close()

    def trade(self, security, price, amount):
        self._set_trade_params(security, price, amount)

        self._submit_trade()

        return self._handle_pop_dialogs(
            handler_class=pop_dialog_handler.TradePopDialogHandler
        )

    def _click(self, control_id):
        self._app.top_window().child_window(
            control_id=control_id, class_name="Button"
        ).click()

    @perf_clock
    def _submit_trade(self):
        time.sleep(0.2)
        self._main.child_window(
            control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name="Button"
        ).click()

    @perf_clock
    def __get_top_window_pop_dialog(self):
        return self._app.top_window().window(
            control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID
        )

    @perf_clock
    def _get_pop_dialog_title(self):
        return (
            self._app.top_window()
            .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID)
            .window_text()
        )

    def _set_trade_params(self, security, price, amount):
        code = security[-6:]

        self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code)

        # wait security input finish
        self.wait(0.1)

        # 设置交易所
        # if security.lower().startswith("sz"):
        #     self._set_stock_exchange_type("深圳A股")
        # if security.lower().startswith("sh"):
        #     self._set_stock_exchange_type("上海A股")
        #
        # self.wait(0.1)

        self._type_edit_control_keys(
            self._config.TRADE_PRICE_CONTROL_ID,
            easyutils.round_price_by_code(price, code),
        )
        self._type_edit_control_keys(
            self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))
        )

    def _set_market_trade_params(self, security, amount, limit_price=None):
        self._type_edit_control_keys(
            self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))
        )
        self.wait(0.1)
        price_control = None
        if str(security).startswith("68"):  # 科创板存在限价
            try:
                price_control = self._main.child_window(
                    control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name="Edit"
                )
            except:
                pass
        if price_control is not None:
            price_control.set_edit_text(limit_price)

    def _get_grid_data(self, control_id):
        return self.grid_strategy_instance.get(control_id)

    def _type_keys(self, control_id, text):
        self._main.child_window(control_id=control_id, class_name="Edit").set_edit_text(
            text
        )

    def _type_edit_control_keys(self, control_id, text):
        if not self._editor_need_type_keys:
            self._main.child_window(
                control_id=control_id, class_name="Edit"
            ).set_edit_text(text)
        else:
            editor = self._main.child_window(control_id=control_id, class_name="Edit")
            editor.select()
            editor.type_keys(text)

    def type_edit_control_keys(self, editor, text):
        if not self._editor_need_type_keys:
            editor.set_edit_text(text)
        else:
            editor.select()
            editor.type_keys(text)

    def _collapse_left_menus(self):
        items = self._get_left_menus_handle().roots()
        for item in items:
            item.collapse()

    @perf_clock
    def _switch_left_menus(self, path, sleep=0.2):
        self.close_pop_dialog()
        self._get_left_menus_handle().get_item(path).select()
        self._app.top_window().type_keys('{F5}')
        self.wait(sleep)

    def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5):
        self.close_pop_dialog()
        self._app.top_window().type_keys(shortcut)
        self.wait(sleep)

    @functools.lru_cache()
    def _get_left_menus_handle(self):
        count = 2
        while True:
            try:
                handle = self._main.child_window(
                    control_id=129, class_name="SysTreeView32"
                )
                if count <= 0:
                    return handle
                # sometime can't find handle ready, must retry
                handle.wait("ready", 2)
                return handle
            # pylint: disable=broad-except
            except Exception as ex:
                logger.exception("error occurred when trying to get left menus")
            count = count - 1

    def _cancel_entrust_by_double_click(self, row):
        x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN
        y = (
            self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT
            + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row
        )
        self._app.top_window().child_window(
            control_id=self._config.COMMON_GRID_CONTROL_ID,
            class_name="CVirtualGridCtrl",
        ).double_click(coords=(x, y))

    def refresh(self):
        self.refresh_strategy.set_trader(self)
        self.refresh_strategy.refresh()

    @perf_clock
    def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler):
        handler = handler_class(self._app)

        while self.is_exist_pop_dialog():
            try:
                title = self._get_pop_dialog_title()
            except pywinauto.findwindows.ElementNotFoundError:
                return {"message": "success"}

            result = handler.handle(title)
            if result:
                return result
        return {"message": "success"}


class BaseLoginClientTrader(ClientTrader):
    @abc.abstractmethod
    def login(self, user, password, exe_path, comm_password=None, **kwargs):
        """Login Client Trader"""
        pass

    def prepare(
        self,
        config_path=None,
        user=None,
        password=None,
        exe_path=None,
        comm_password=None,
        **kwargs
    ):
        """
        登陆客户端
        :param config_path: 登陆配置文件,跟参数登陆方式二选一
        :param user: 账号
        :param password: 明文密码
        :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe', 默认 r'C:\\htzqzyb2\\xiadan.exe'
        :param comm_password: 通讯密码
        :return:
        """
        if config_path is not None:
            account = file2dict(config_path)
            user = account["user"]
            password = account["password"]
            comm_password = account.get("comm_password")
            exe_path = account.get("exe_path")
        self.login(
            user,
            password,
            exe_path or self._config.DEFAULT_EXE_PATH,
            comm_password,
            **kwargs
        )
        self._init_toolbar()


================================================
FILE: easytrader/config/__init__.py
================================================


================================================
FILE: easytrader/config/client.py
================================================
# -*- coding: utf-8 -*-
def create(broker):
    if broker == "yh":
        return YH
    if broker == "ht":
        return HT
    if broker == "gj":
        return GJ
    if broker == "gf":
        return GF
    if broker == "ths":
        return CommonConfig
    if broker == "wk":
        return WK
    if broker == "htzq":
        return HTZQ
    if broker == "universal":
        return UNIVERSAL
    raise NotImplementedError


class CommonConfig:
    DEFAULT_EXE_PATH: str = ""
    TITLE = "网上股票交易系统5.0"

    # 交易所类型。 深圳A股、上海A股
    TRADE_STOCK_EXCHANGE_CONTROL_ID = 1003

    # 撤销界面上, 全部撤销按钮
    TRADE_CANCEL_ALL_ENTRUST_CONTROL_ID = 30001

    TRADE_SECURITY_CONTROL_ID = 1032
    TRADE_PRICE_CONTROL_ID = 1033
    TRADE_AMOUNT_CONTROL_ID = 1034

    TRADE_SUBMIT_CONTROL_ID = 1006

    TRADE_MARKET_TYPE_CONTROL_ID = 1541

    COMMON_GRID_CONTROL_ID = 1047

    COMMON_GRID_LEFT_MARGIN = 10
    COMMON_GRID_FIRST_ROW_HEIGHT = 30
    COMMON_GRID_ROW_HEIGHT = 16

    BALANCE_MENU_PATH = ["查询[F4]", "资金股票"]
    POSITION_MENU_PATH = ["查询[F4]", "资金股票"]
    TODAY_ENTRUSTS_MENU_PATH = ["查询[F4]", "当日委托"]
    TODAY_TRADES_MENU_PATH = ["查询[F4]", "当日成交"]

    BALANCE_CONTROL_ID_GROUP = {
        "资金余额": 1012,
        "可用金额": 1016,
        "可取金额": 1017,
        "股票市值": 1014,
        "总资产": 1015,
    }

    POP_DIALOD_TITLE_CONTROL_ID = 1365

    GRID_DTYPE = {
        "操作日期": str,
        "委托编号": str,
        "申请编号": str,
        "合同编号": str,
        "证券代码": str,
        "股东代码": str,
        "资金帐号": str,
        "资金帐户": str,
        "发生日期": str,
    }

    CANCEL_ENTRUST_ENTRUST_FIELD = "合同编号"
    CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50
    CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30
    CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16

    AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098
    AUTO_IPO_BUTTON_CONTROL_ID = 1006
    AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"]
    AUTO_IPO_NUMBER = '申购数量'


class YH(CommonConfig):
    DEFAULT_EXE_PATH = r"C:\双子星-中国银河证券\Binarystar.exe"

    BALANCE_GRID_CONTROL_ID = 1308

    GRID_DTYPE = {
        "操作日期": str,
        "委托编号": str,
        "申请编号": str,
        "合同编号": str,
        "证券代码": str,
        "股东代码": str,
        "资金帐号": str,
        "资金帐户": str,
        "发生日期": str,
    }

    AUTO_IPO_MENU_PATH = ["新股申购", "一键打新"]


class HT(CommonConfig):
    DEFAULT_EXE_PATH = r"C:\htzqzyb2\xiadan.exe"

    BALANCE_CONTROL_ID_GROUP = {
        "资金余额": 1012,
        "冻结资金": 1013,
        "可用金额": 1016,
        "可取金额": 1017,
        "股票市值": 1014,
        "总资产": 1015,
    }

    GRID_DTYPE = {
        "操作日期": str,
        "委托编号": str,
        "申请编号": str,
        "合同编号": str,
        "证券代码": str,
        "股东代码": str,
        "资金帐号": str,
        "资金帐户": str,
        "发生日期": str,
    }

    AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"]


class GJ(CommonConfig):
    DEFAULT_EXE_PATH = "C:\\全能行证券交易终端\\xiadan.exe"

    GRID_DTYPE = {
        "操作日期": str,
        "委托编号": str,
        "申请编号": str,
        "合同编号": str,
        "证券代码": str,
        "股东代码": str,
        "资金帐号": str,
        "资金帐户": str,
        "发生日期": str,
    }

    AUTO_IPO_MENU_PATH = ["新股申购", "新股批量申购"]

class GF(CommonConfig):
    DEFAULT_EXE_PATH = "C:\\gfzqrzrq\\xiadan.exe"
    TITLE = "核新网上交易系统"

    GRID_DTYPE = {
        "操作日期": str,
        "委托编号": str,
        "申请编号": str,
        "合同编号": str,
        "证券代码": str,
        "股东代码": str,
        "资金帐号": str,
        "资金帐户": str,
        "发生日期": str,
    }

    AUTO_IPO_MENU_PATH = ["新股申购", "批量新股申购"]

class WK(HT):
    pass


class HTZQ(CommonConfig):
    DEFAULT_EXE_PATH = r"c:\\海通证券委托\\xiadan.exe"

    BALANCE_CONTROL_ID_GROUP = {
        "资金余额": 1012,
        "可用金额": 1016,
        "可取金额": 1017,
        "总资产": 1015,
    }

    AUTO_IPO_NUMBER = '可申购数量'


class UNIVERSAL(CommonConfig):
    DEFAULT_EXE_PATH = r"c:\\ths\\xiadan.exe"

    BALANCE_CONTROL_ID_GROUP = {
        "资金余额": 1012,
        "可用金额": 1016,
        "可取金额": 1017,
        "总资产": 1015,
    }

    AUTO_IPO_NUMBER = '可申购数量'


================================================
FILE: easytrader/config/global.json
================================================
{
  "response_format": {
    "int": [
      "current_amount",
      "enable_amount",
      "entrust_amount",
      "business_amount",
      "成交数量",
      "撤单数量",
      "委托数量",
      "股份可用",
      "买入冻结",
      "卖出冻结",
      "当前持仓",
      "股份余额"
    ],
    "float": [
      "current_balance",
      "enable_balance",
      "fetch_balance",
      "market_value",
      "asset_balance",
      "av_buy_price",
      "cost_price",
      "income_balance",
      "market_value",
      "entrust_price",
      "business_price",
      "business_balance",
      "fare1",
      "occur_balance",
      "farex",
      "fare0",
      "occur_amount",
      "post_balance",
      "fare2",
      "fare3",
      "资金余额",
      "可用资金",
      "参考市值",
      "总资产",
      "股份参考盈亏",
      "委托价格",
      "成交价格",
      "成交金额",
      "参考盈亏",
      "参考成本价",
      "参考市价",
      "参考市值"
    ]
  }
}


================================================
FILE: easytrader/config/xq.json
================================================
{
  "login_api": "https://xueqiu.com/user/login",
  "prefix": "https://xueqiu.com/user/login",
  "portfolio_url": "https://xueqiu.com/p/",
  "search_stock_url": "https://xueqiu.com/stock/p/search.json",
  "rebalance_url": "https://xueqiu.com/cubes/rebalancing/create.json",
  "history_url": "https://xueqiu.com/cubes/rebalancing/history.json",
  "referer": "https://xueqiu.com/p/update?action=holdings&symbol=%s",
  "portfolio_url_new": "https://xueqiu.com/cubes/rebalancing/current.json",
  "portfolio_quote": "https://xueqiu.com/cubes/quote.json"
}


================================================
FILE: easytrader/exceptions.py
================================================
# -*- coding: utf-8 -*-


class TradeError(IOError):
    pass


class NotLoginError(Exception):
    def __init__(self, result=None):
        super(NotLoginError, self).__init__()
        self.result = result


================================================
FILE: easytrader/follower.py
================================================
# -*- coding: utf-8 -*-
import abc
import datetime
import os
import pickle
import queue
import re
import threading
import time
from typing import List

import requests

from easytrader import exceptions
from easytrader.log import logger


class BaseFollower(metaclass=abc.ABCMeta):
    """
    slippage: 滑点,取值范围为 [0, 1]
    """

    LOGIN_PAGE = ""
    LOGIN_API = ""
    TRANSACTION_API = ""
    CMD_CACHE_FILE = "cmd_cache.pk"
    WEB_REFERER = ""
    WEB_ORIGIN = ""

    def __init__(self):
        self.trade_queue = queue.Queue()
        self.expired_cmds = set()

        self.s = requests.Session()
        self.s.verify = False

        self.slippage: float = 0.0

    def login(self, user=None, password=None, **kwargs):
        """
        登陆接口
        :param user: 用户名
        :param password: 密码
        :param kwargs: 其他参数
        :return:
        """
        headers = self._generate_headers()
        self.s.headers.update(headers)

        # init cookie
        self.s.get(self.LOGIN_PAGE)

        # post for login
        params = self.create_login_params(user, password, **kwargs)
        rep = self.s.post(self.LOGIN_API, data=params)

        self.check_login_success(rep)
        logger.info("登录成功")

    def _generate_headers(self):
        headers = {
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "Accept-Encoding": "gzip, deflate, br",
            "Accept-Language": "en-US,en;q=0.8",
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/54.0.2840.100 Safari/537.36",
            "Referer": self.WEB_REFERER,
            "X-Requested-With": "XMLHttpRequest",
            "Origin": self.WEB_ORIGIN,
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        }
        return headers

    def check_login_success(self, rep):
        """检查登录状态是否成功
        :param rep: post login 接口返回的 response 对象
        :raise 如果登录失败应该抛出 NotLoginError """
        pass

    def create_login_params(self, user, password, **kwargs) -> dict:
        """生成 post 登录接口的参数
        :param user: 用户名
        :param password: 密码
        :return dict 登录参数的字典
        """
        return {}

    def follow(
        self,
        users,
        strategies,
        track_interval=1,
        trade_cmd_expire_seconds=120,
        cmd_cache=True,
        slippage: float = 0.0,
        **kwargs
    ):
        """跟踪平台对应的模拟交易,支持多用户多策略

        :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户
        :param strategies: 雪球组合名, 类似 ZH123450
        :param total_assets: 雪球组合对应的总资产, 格式 [ 组合1对应资金, 组合2对应资金 ]
            若 strategies=['ZH000001', 'ZH000002'] 设置 total_assets=[10000, 10000], 则表明每个组合对应的资产为 1w 元,
            假设组合 ZH000001 加仓 价格为 p 股票 A 10%, 则对应的交易指令为 买入 股票 A 价格 P 股数 1w * 10% / p 并按 100 取整
        :param initial_assets:雪球组合对应的初始资产, 格式 [ 组合1对应资金, 组合2对应资金 ]
            总资产由 初始资产 × 组合净值 算得, total_assets 会覆盖此参数
        :param track_interval: 轮询模拟交易时间,单位为秒
        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒
        :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令
        :param slippage: 滑点,0.0 表示无滑点, 0.05 表示滑点为 5%
        """
        self.slippage = slippage

    def _calculate_price_by_slippage(self, action: str, price: float) -> float:
        """
        计算考虑滑点之后的价格
        :param action: 交易动作, 支持 ['buy', 'sell']
        :param price: 原始交易价格
        :return: 考虑滑点后的交易价格
        """
        if action == "buy":
            return price * (1 + self.slippage)
        if action == "sell":
            return price * (1 - self.slippage)
        return price

    def load_expired_cmd_cache(self):
        if os.path.exists(self.CMD_CACHE_FILE):
            with open(self.CMD_CACHE_FILE, "rb") as f:
                self.expired_cmds = pickle.load(f)

    def start_trader_thread(
        self,
        users,
        trade_cmd_expire_seconds,
        entrust_prop="limit",
        send_interval=0,
    ):
        trader = threading.Thread(
            target=self.trade_worker,
            args=[users],
            kwargs={
                "expire_seconds": trade_cmd_expire_seconds,
                "entrust_prop": entrust_prop,
                "send_interval": send_interval,
            },
        )
        trader.setDaemon(True)
        trader.start()

    @staticmethod
    def warp_list(value):
        if not isinstance(value, list):
            value = [value]
        return value

    @staticmethod
    def extract_strategy_id(strategy_url):
        """
        抽取 策略 id,一般用于获取策略相关信息
        :param strategy_url: 策略 url
        :return: str 策略 id
        """
        pass

    def extract_strategy_name(self, strategy_url):
        """
        抽取 策略名,主要用于日志打印,便于识别
        :param strategy_url:
        :return: str 策略名
        """
        pass

    def track_strategy_worker(self, strategy, name, interval=10, **kwargs):
        """跟踪下单worker
        :param strategy: 策略id
        :param name: 策略名字
        :param interval: 轮询策略的时间间隔,单位为秒"""
        while True:
            try:
                transactions = self.query_strategy_transaction(
                    strategy, **kwargs
                )
            # pylint: disable=broad-except
            except Exception as e:
                logger.exception("无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询", name, e)
                time.sleep(3)
                continue
            for transaction in transactions:
                trade_cmd = {
                    "strategy": strategy,
                    "strategy_name": name,
                    "action": transaction["action"],
                    "stock_code": transaction["stock_code"],
                    "amount": transaction["amount"],
                    "price": transaction["price"],
                    "datetime": transaction["datetime"],
                }
                if self.is_cmd_expired(trade_cmd):
                    continue
                logger.info(
                    "策略 [%s] 发送指令到交易队列, 股票: %s 动作: %s 数量: %s 价格: %s 信号产生时间: %s",
                    name,
                    trade_cmd["stock_code"],
                    trade_cmd["action"],
                    trade_cmd["amount"],
                    trade_cmd["price"],
                    trade_cmd["datetime"],
                )
                self.trade_queue.put(trade_cmd)
                self.add_cmd_to_expired_cmds(trade_cmd)
            try:
                for _ in range(interval):
                    time.sleep(1)
            except KeyboardInterrupt:
                logger.info("程序退出")
                break

    @staticmethod
    def generate_expired_cmd_key(cmd):
        return "{}_{}_{}_{}_{}_{}".format(
            cmd["strategy_name"],
            cmd["stock_code"],
            cmd["action"],
            cmd["amount"],
            cmd["price"],
            cmd["datetime"],
        )

    def is_cmd_expired(self, cmd):
        key = self.generate_expired_cmd_key(cmd)
        return key in self.expired_cmds

    def add_cmd_to_expired_cmds(self, cmd):
        key = self.generate_expired_cmd_key(cmd)
        self.expired_cmds.add(key)

        with open(self.CMD_CACHE_FILE, "wb") as f:
            pickle.dump(self.expired_cmds, f)

    @staticmethod
    def _is_number(s):
        try:
            float(s)
            return True
        except ValueError:
            return False

    def _execute_trade_cmd(
        self, trade_cmd, users, expire_seconds, entrust_prop, send_interval
    ):
        """分发交易指令到对应的 user 并执行
        :param trade_cmd:
        :param users:
        :param expire_seconds:
        :param entrust_prop:
        :param send_interval:
        :return:
        """
        for user in users:
            # check expire
            now = datetime.datetime.now()
            expire = (now - trade_cmd["datetime"]).total_seconds()
            if expire > expire_seconds:
                logger.warning(
                    "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 超过设置的最大过期时间 %s 秒, 被丢弃",
                    trade_cmd["strategy_name"],
                    trade_cmd["stock_code"],
                    trade_cmd["action"],
                    trade_cmd["amount"],
                    trade_cmd["price"],
                    trade_cmd["datetime"],
                    now,
                    expire_seconds,
                )
                break

            # check price
            price = trade_cmd["price"]
            if not self._is_number(price) or price <= 0:
                logger.warning(
                    "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 价格无效 , 被丢弃",
                    trade_cmd["strategy_name"],
                    trade_cmd["stock_code"],
                    trade_cmd["action"],
                    trade_cmd["amount"],
                    trade_cmd["price"],
                    trade_cmd["datetime"],
                    now,
                )
                break

            # check amount
            if trade_cmd["amount"] <= 0:
                logger.warning(
                    "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时,指令产生时间: %s 当前时间: %s, 买入股数无效 , 被丢弃",
                    trade_cmd["strategy_name"],
                    trade_cmd["stock_code"],
                    trade_cmd["action"],
                    trade_cmd["amount"],
                    trade_cmd["price"],
                    trade_cmd["datetime"],
                    now,
                )
                break

            actual_price = self._calculate_price_by_slippage(
                trade_cmd["action"], trade_cmd["price"]
            )
            args = {
                "security": trade_cmd["stock_code"],
                "price": actual_price,
                "amount": trade_cmd["amount"],
                "entrust_prop": entrust_prop,
            }
            try:
                response = getattr(user, trade_cmd["action"])(**args)
            except exceptions.TradeError as e:
                trader_name = type(user).__name__
                err_msg = "{}: {}".format(type(e).__name__, e.args)
                logger.error(
                    "%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 失败, 错误信息: %s",
                    trader_name,
                    trade_cmd["strategy_name"],
                    trade_cmd["stock_code"],
                    trade_cmd["action"],
                    trade_cmd["amount"],
                    actual_price,
                    trade_cmd["datetime"],
                    err_msg,
                )
            else:
                logger.info(
                    "策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 执行成功, 返回: %s",
                    trade_cmd["strategy_name"],
                    trade_cmd["stock_code"],
                    trade_cmd["action"],
                    trade_cmd["amount"],
                    actual_price,
                    trade_cmd["datetime"],
                    response,
                )

    def trade_worker(
        self, users, expire_seconds=120, entrust_prop="limit", send_interval=0
    ):
        """
        :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时买出单没有及时成交导致的买入金额不足
        """
        while True:
            trade_cmd = self.trade_queue.get()
            self._execute_trade_cmd(
                trade_cmd, users, expire_seconds, entrust_prop, send_interval
            )
            time.sleep(send_interval)

    def query_strategy_transaction(self, strategy, **kwargs):
        params = self.create_query_transaction_params(strategy)

        rep = self.s.get(self.TRANSACTION_API, params=params)
        history = rep.json()

        transactions = self.extract_transactions(history)
        self.project_transactions(transactions, **kwargs)
        return self.order_transactions_sell_first(transactions)

    def extract_transactions(self, history) -> List[str]:
        """
        抽取接口返回中的调仓记录列表
        :param history: 调仓接口返回信息的字典对象
        :return: [] 调参历史记录的列表
        """
        return []

    def create_query_transaction_params(self, strategy) -> dict:
        """
        生成用于查询调参记录的参数
        :param strategy: 策略 id
        :return: dict 调参记录参数
        """
        return {}

    @staticmethod
    def re_find(pattern, string, dtype=str):
        return dtype(re.search(pattern, string).group())

    @staticmethod
    def re_search(pattern, string, dtype=str):
        return dtype(re.search(pattern,string).group(1))

    def project_transactions(self, transactions, **kwargs):
        """
        修证调仓记录为内部使用的统一格式
        :param transactions: [] 调仓记录的列表
        :return: [] 修整后的调仓记录
        """
        pass

    def order_transactions_sell_first(self, transactions):
        # 调整调仓记录的顺序为先卖再买
        sell_first_transactions = []
        for transaction in transactions:
            if transaction["action"] == "sell":
                sell_first_transactions.insert(0, transaction)
            else:
                sell_first_transactions.append(transaction)
        return sell_first_transactions


================================================
FILE: easytrader/gf_clienttrader.py
================================================
# -*- coding: utf-8 -*-
import re
import tempfile
import time
import os

import pywinauto
import pywinauto.clipboard

from easytrader import clienttrader
from easytrader.utils.captcha import recognize_verify_code


class GFClientTrader(clienttrader.BaseLoginClientTrader):
    @property
    def broker_type(self):
        return "gf"

    def login(self, user, password, exe_path, comm_password=None, **kwargs):
        """
        登陆客户端

        :param user: 账号
        :param password: 明文密码
        :param exe_path: 客户端路径类似 'C:\\中国银河证券双子星3.2\\Binarystar.exe',
            默认 'C:\\中国银河证券双子星3.2\\Binarystar.exe'
        :param comm_password: 通讯密码, 华泰需要,可不设
        :return:
        """
        try:
            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=1
            )
        # pylint: disable=broad-except
        except Exception:
            self._app = pywinauto.Application().start(exe_path)

            # wait login window ready
            while True:
                try:
                    self._app.top_window().Edit1.wait("ready")
                    break
                except RuntimeError:
                    pass

            self.type_edit_control_keys(self._app.top_window().Edit1, user)
            self.type_edit_control_keys(self._app.top_window().Edit2, password)
            edit3 = self._app.top_window().window(control_id=0x3eb)
            while True:
                try:
                    code = self._handle_verify_code()
                    self.type_edit_control_keys(edit3, code)
                    time.sleep(1)
                    self._app.top_window()["登录(Y)"].click()
                    # detect login is success or not
                    try:
                        self._app.top_window().wait_not("exists", 5)
                        break

                    # pylint: disable=broad-except
                    except Exception:
                        self._app.top_window()["确定"].click()

                # pylint: disable=broad-except
                except Exception:
                    pass

            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=10
            )
        self._main = self._app.window(title_re="""{title}.*""".format(title=self._config.TITLE))
        self.close_pop_dialog()

    def _handle_verify_code(self):
        control = self._app.top_window().window(control_id=0x5db)
        control.click()
        time.sleep(0.2)
        file_path = tempfile.mktemp() + ".jpg"
        control.capture_as_image().save(file_path)
        time.sleep(0.2)
        vcode = recognize_verify_code(file_path, "gf_client")
        if os.path.exists(file_path):
            os.remove(file_path)
        return "".join(re.findall("[a-zA-Z0-9]+", vcode))


================================================
FILE: easytrader/gj_clienttrader.py
================================================
# -*- coding: utf-8 -*-
import re
import tempfile
import time

import pywinauto
import pywinauto.clipboard

from easytrader import clienttrader
from easytrader.utils.captcha import recognize_verify_code


class GJClientTrader(clienttrader.BaseLoginClientTrader):
    @property
    def broker_type(self):
        return "gj"

    def login(self, user, password, exe_path, comm_password=None, **kwargs):
        """
        登陆客户端

        :param user: 账号
        :param password: 明文密码
        :param exe_path: 客户端路径类似 'C:\\中国银河证券双子星3.2\\Binarystar.exe',
            默认 'C:\\中国银河证券双子星3.2\\Binarystar.exe'
        :param comm_password: 通讯密码, 华泰需要,可不设
        :return:
        """
        try:
            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=1
            )
        # pylint: disable=broad-except
        except Exception:
            self._app = pywinauto.Application().start(exe_path)

            # wait login window ready
            while True:
                try:
                    self._app.top_window().Edit1.wait("ready")
                    break
                except RuntimeError:
                    pass

            self._app.top_window().Edit1.type_keys(user)
            self._app.top_window().Edit2.type_keys(password)
            edit3 = self._app.top_window().window(control_id=0x3eb)
            while True:
                try:
                    code = self._handle_verify_code()
                    edit3.type_keys(code)
                    time.sleep(1)
                    self._app.top_window()["确定(Y)"].click()
                    # detect login is success or not
                    try:
                        self._app.top_window().wait_not("exists", 5)
                        break

                    # pylint: disable=broad-except
                    except Exception:
                        self._app.top_window()["确定"].click()

                # pylint: disable=broad-except
                except Exception:
                    pass

            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=10
            )
        self._main = self._app.window(title="网上股票交易系统5.0")

    def _handle_verify_code(self):
        control = self._app.top_window().window(control_id=0x5db)
        control.click()
        time.sleep(0.2)
        file_path = tempfile.mktemp() + ".jpg"
        control.capture_as_image().save(file_path)
        time.sleep(0.2)
        vcode = recognize_verify_code(file_path, "gj_client")
        return "".join(re.findall("[a-zA-Z0-9]+", vcode))


================================================
FILE: easytrader/grid_strategies.py
================================================
# -*- coding: utf-8 -*-
import abc
import io
import tempfile
from io import StringIO
from typing import TYPE_CHECKING, Dict, List, Optional

import pandas as pd
import pywinauto.keyboard
import pywinauto
import pywinauto.clipboard

from easytrader.log import logger
from easytrader.utils.captcha import captcha_recognize
from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines

if TYPE_CHECKING:
    # pylint: disable=unused-import
    from easytrader import clienttrader


class IGridStrategy(abc.ABC):
    @abc.abstractmethod
    def get(self, control_id: int) -> List[Dict]:
        """
        获取 grid 数据并格式化返回

        :param control_id: grid 的 control id
        :return: grid 数据
        """
        pass

    @abc.abstractmethod
    def set_trader(self, trader: "clienttrader.IClientTrader"):
        pass


class BaseStrategy(IGridStrategy):
    def __init__(self):
        self._trader = None

    def set_trader(self, trader: "clienttrader.IClientTrader"):
        self._trader = trader

    @abc.abstractmethod
    def get(self, control_id: int) -> List[Dict]:
        """
        :param control_id: grid 的 control id
        :return: grid 数据
        """
        pass

    def _get_grid(self, control_id: int):
        grid = self._trader.main.child_window(
            control_id=control_id, class_name="CVirtualGridCtrl"
        )
        return grid

    def _set_foreground(self, grid=None):
        try:
            if grid is None:
                grid = self._trader.main
            if grid.has_style(win32defines.WS_MINIMIZE):  # if minimized
                ShowWindow(grid.wrapper_object(), 9)  # restore window state
            else:
                SetForegroundWindow(grid.wrapper_object())  # bring to front
        except:
            pass


class Copy(BaseStrategy):
    """
    通过复制 grid 内容到剪切板再读取来获取 grid 内容
    """

    _need_captcha_reg = True

    def get(self, control_id: int) -> List[Dict]:
        grid = self._get_grid(control_id)
        self._set_foreground(grid)
        grid.type_keys("^A^C", set_foreground=False, pause=0.2)
        content = self._get_clipboard_data()
        return self._format_grid_data(content)

    def _format_grid_data(self, data: str) -> List[Dict]:
        try:
            df = pd.read_csv(
                io.StringIO(data),
                delimiter="\t",
                dtype=self._trader.config.GRID_DTYPE,
                na_filter=False,
            )
            return df.to_dict("records")
        except:
            Copy._need_captcha_reg = True

    def _get_clipboard_data(self) -> str:
        if Copy._need_captcha_reg:
            if (
                    self._trader.app.top_window().window(class_name="Static", title_re="验证码").exists(timeout=1)
            ):
                file_path = "tmp.png"
                count = 5
                found = False
                while count > 0:
                    self._trader.app.top_window().window(
                        control_id=0x965, class_name="Static"
                    ).capture_as_image().save(
                        file_path
                    )  # 保存验证码

                    captcha_num = captcha_recognize(file_path).strip()  # 识别验证码
                    captcha_num = "".join(captcha_num.split())
                    logger.info("captcha result-->" + captcha_num)
                    if len(captcha_num) == 4:
                        editor = self._trader.app.top_window().window(
                            control_id=0x964, class_name="Edit"
                        ) # 验证码输入框
                        editor.set_focus() # 焦点移到验证码输入框 (也可不聚焦防止键盘误触输入,不聚焦type_edit_control_keys也可正常输入)
                        self._trader.wait(0.1) # 输入前短暂等待
                        self._trader.type_edit_control_keys(
                            editor,
                            captcha_num
                        )  # 模拟输入验证码

                        self._trader.wait(0.1) # 输完后短暂等待
                        self._trader.app.top_window().type_keys("{ENTER}", pause=0.1)  # 模拟发送enter,点击确定
                        if not editor.exists(timeout=1):  # 窗体消失
                            logger.info("验证码验证成功-->" + captcha_num)
                            found = True
                            break
                    count -= 1
                    self._trader.wait(0.1)
                    self._trader.app.top_window().window(
                        control_id=0x965, class_name="Static"
                    ).click()
                if not found:
                    self._trader.app.top_window().Button2.click()  # 点击取消
            else:
                pass
                # 不要将 Copy._need_captcha_reg 置为 False, 因为它是类方法, 一旦置为 False, 后续操作都不再进行验证码识别
                # Copy._need_captcha_reg = False
        count = 5
        while count > 0:
            try:
                return pywinauto.clipboard.GetData()
            # pylint: disable=broad-except
            except Exception as e:
                count -= 1
                logger.exception("%s, retry ......", e)


class WMCopy(Copy):
    """
    通过复制 grid 内容到剪切板再读取来获取 grid 内容
    """

    def get(self, control_id: int) -> List[Dict]:
        grid = self._get_grid(control_id)
        grid.post_message(win32defines.WM_COMMAND, 0xE122, 0)
        self._trader.wait(0.1)
        content = self._get_clipboard_data()
        return self._format_grid_data(content)


class Xls(BaseStrategy):
    """
    通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容
    """

    def __init__(self, tmp_folder: Optional[str] = None):
        """
        :param tmp_folder: 用于保持临时文件的文件夹
        """
        super().__init__()
        self.tmp_folder = tmp_folder

    def get(self, control_id: int) -> List[Dict]:
        grid = self._get_grid(control_id)

        # ctrl+s 保存 grid 内容为 xls 文件
        self._set_foreground(grid)  # setFocus buggy, instead of SetForegroundWindow
        grid.type_keys("^s", set_foreground=False)
        count = 10
        while count > 0:
            if self._trader.is_exist_pop_dialog():
                break
            self._trader.wait(0.2)
            count -= 1

        temp_path = tempfile.mktemp(suffix=".xls", dir=self.tmp_folder)
        self._set_foreground(self._trader.app.top_window())

        # alt+s保存,alt+y替换已存在的文件
        self._trader.app.top_window().Edit1.set_edit_text(temp_path)
        self._trader.wait(0.1)
        self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False)
        # Wait until file save complete otherwise pandas can not find file
        self._trader.wait(0.2)
        if self._trader.is_exist_pop_dialog():
            self._trader.app.top_window().Button2.click()
            self._trader.wait(0.2)

        return self._format_grid_data(temp_path)

    def _format_grid_data(self, data: str) -> List[Dict]:
        with open(data, encoding="gbk", errors="replace") as f:
            content = f.read()

        df = pd.read_csv(
            StringIO(content),
            delimiter="\t",
            dtype=self._trader.config.GRID_DTYPE,
            na_filter=False,
        )
        return df.to_dict("records")


================================================
FILE: easytrader/ht_clienttrader.py
================================================
# -*- coding: utf-8 -*-

import pywinauto
import pywinauto.clipboard

from easytrader import grid_strategies
from . import clienttrader


class HTClientTrader(clienttrader.BaseLoginClientTrader):
    grid_strategy = grid_strategies.Xls

    @property
    def broker_type(self):
        return "ht"

    def login(self, user, password, exe_path, comm_password=None, **kwargs):
        """
        :param user: 用户名
        :param password: 密码
        :param exe_path: 客户端路径, 类似
        :param comm_password:
        :param kwargs:
        :return:
        """
        self._editor_need_type_keys = False
        if comm_password is None:
            raise ValueError("华泰必须设置通讯密码")

        try:
            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=1
            )
        # pylint: disable=broad-except
        except Exception:
            self._app = pywinauto.Application().start(exe_path)

            # wait login window ready
            while True:
                try:
                    self._app.top_window().Edit1.wait("ready")
                    break
                except RuntimeError:
                    pass
            self._app.top_window().Edit1.set_focus()
            self._app.top_window().Edit1.type_keys(user)
            self._app.top_window().Edit2.type_keys(password)

            self._app.top_window().Edit3.set_edit_text(comm_password)

            self._app.top_window().button0.click()

            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=10
            )
        self._main = self._app.window(title="网上股票交易系统5.0")
        self._main.wait ( "exists enabled visible ready" , timeout=100 )
        self._close_prompt_windows ( )

    @property
    def balance(self):
        self._switch_left_menus(self._config.BALANCE_MENU_PATH)

        return self._get_balance_from_statics()

    def _get_balance_from_statics(self):
        result = {}
        for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items():
            result[key] = float(
                self._main.child_window(
                    control_id=control_id, class_name="Static"
                ).window_text()
            )
        return result




================================================
FILE: easytrader/htzq_clienttrader.py
================================================
# -*- coding: utf-8 -*-

import pywinauto
import pywinauto.clipboard

from easytrader import grid_strategies
from . import clienttrader


class HTZQClientTrader(clienttrader.BaseLoginClientTrader):
    grid_strategy = grid_strategies.Xls

    @property
    def broker_type(self):
        return "htzq"

    def login(self, user, password, exe_path, comm_password=None, **kwargs):
        """
        :param user: 用户名
        :param password: 密码
        :param exe_path: 客户端路径, 类似
        :param comm_password:
        :param kwargs:
        :return:
        """
        self._editor_need_type_keys = False
        if comm_password is None:
            raise ValueError("必须设置通讯密码")

        try:
            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=1
            )
        # pylint: disable=broad-except
        except Exception:
            self._app = pywinauto.Application().start(exe_path)

            # wait login window ready
            while True:
                try:
                    self._app.top_window().Edit1.wait("ready")
                    break
                except RuntimeError:
                    pass
            self._app.top_window().Edit1.set_focus()
            self._app.top_window().Edit1.type_keys(user)
            self._app.top_window().Edit2.type_keys(password)

            self._app.top_window().Edit3.type_keys(comm_password)

            self._app.top_window().button0.click()

            # detect login is success or not
            self._app.top_window().wait_not("exists", 100)

            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=10
            )
        self._close_prompt_windows()
        self._main = self._app.window(title="网上股票交易系统5.0")



================================================
FILE: easytrader/joinquant_follower.py
================================================
# -*- coding: utf-8 -*-
from datetime import datetime
from threading import Thread

from easytrader import exceptions
from easytrader.follower import BaseFollower
from easytrader.log import logger


class JoinQuantFollower(BaseFollower):
    LOGIN_PAGE = "https://www.joinquant.com"
    LOGIN_API = "https://www.joinquant.com/user/login/doLogin?ajax=1"
    TRANSACTION_API = (
        "https://www.joinquant.com/algorithm/live/transactionDetail"
    )
    WEB_REFERER = "https://www.joinquant.com/user/login/index"
    WEB_ORIGIN = "https://www.joinquant.com"

    def create_login_params(self, user, password, **kwargs):
        params = {
            "CyLoginForm[username]": user,
            "CyLoginForm[pwd]": password,
            "ajax": 1,
        }
        return params

    def check_login_success(self, rep):
        set_cookie = rep.headers["set-cookie"]
        if len(set_cookie) < 50:
            raise exceptions.NotLoginError("登录失败,请检查用户名和密码")
        self.s.headers.update({"cookie": set_cookie})

    def follow(
            self,
            users,
            strategies,
            track_interval=1,
            trade_cmd_expire_seconds=120,
            cmd_cache=True,
            entrust_prop="limit",
            send_interval=0,
    ):
        """跟踪joinquant对应的模拟交易,支持多用户多策略
        :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户
        :param strategies: joinquant 的模拟交易地址,支持使用 [] 指定多个模拟交易,
            地址类似 https://www.joinquant.com/algorithm/live/index?backtestId=xxx
        :param track_interval: 轮训模拟交易时间,单位为秒
        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒
        :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令
        :param entrust_prop: 委托方式, 'limit' 为限价,'market' 为市价, 仅在银河实现
        :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足
        """
        users = self.warp_list(users)
        strategies = self.warp_list(strategies)

        if cmd_cache:
            self.load_expired_cmd_cache()

        self.start_trader_thread(
            users, trade_cmd_expire_seconds, entrust_prop, send_interval
        )

        workers = []
        for strategy_url in strategies:
            try:
                strategy_id = self.extract_strategy_id(strategy_url)
                strategy_name = self.extract_strategy_name(strategy_url)
            except:
                logger.error("抽取交易id和策略名失败, 无效的模拟交易url: %s", strategy_url)
                raise
            strategy_worker = Thread(
                target=self.track_strategy_worker,
                args=[strategy_id, strategy_name],
                kwargs={"interval": track_interval},
            )
            strategy_worker.start()
            workers.append(strategy_worker)
            logger.info("开始跟踪策略: %s", strategy_name)
        for worker in workers:
            worker.join()

    # @staticmethod
    # def extract_strategy_id(strategy_url):
    #     return re.search(r"(?<=backtestId=)\w+", strategy_url).group()
    #
    # def extract_strategy_name(self, strategy_url):
    #     rep = self.s.get(strategy_url)
    #     return self.re_find(
    #         r'(?<=title="点击修改策略名称"\>).*(?=\</span)', rep.content.decode("utf8")
    #     )
    def extract_strategy_id(self, strategy_url):
        rep = self.s.get(strategy_url)
        return self.re_search(r'name="backtest\[backtestId\]"\s+?value="(.*?)">', rep.content.decode("utf8"))

    def extract_strategy_name(self, strategy_url):
        rep = self.s.get(strategy_url)
        return self.re_search(r'class="backtest_name".+?>(.*?)</span>', rep.content.decode("utf8"))

    def create_query_transaction_params(self, strategy):
        today_str = datetime.today().strftime("%Y-%m-%d")
        params = {"backtestId": strategy, "date": today_str, "ajax": 1}
        return params

    def extract_transactions(self, history):
        transactions = history["data"]["transaction"]
        return transactions

    @staticmethod
    def stock_shuffle_to_prefix(stock):
        assert (
                len(stock) == 11
        ), "stock {} must like 123456.XSHG or 123456.XSHE".format(stock)
        code = stock[:6]
        if stock.find("XSHG") != -1:
            return "sh" + code

        if stock.find("XSHE") != -1:
            return "sz" + code
        raise TypeError("not valid stock code: {}".format(code))

    def project_transactions(self, transactions, **kwargs):
        for transaction in transactions:
            transaction["amount"] = self.re_find(
                r"\d+", transaction["amount"], dtype=int
            )

            time_str = "{} {}".format(transaction["date"], transaction["time"])
            transaction["datetime"] = datetime.strptime(
                time_str, "%Y-%m-%d %H:%M:%S"
            )

            stock = self.re_find(r"\d{6}\.\w{4}", transaction["stock"])
            transaction["stock_code"] = self.stock_shuffle_to_prefix(stock)

            transaction["action"] = (
                "buy" if transaction["transaction"] == "买" else "sell"
            )
            transaction["price"] = (
                transaction["price"] if isinstance(transaction["transaction"] ,float) else float(transaction["price"])
            )


================================================
FILE: easytrader/log.py
================================================
# -*- coding: utf-8 -*-
import logging

logger = logging.getLogger("easytrader")
logger.setLevel(logging.INFO)
logger.propagate = False

fmt = logging.Formatter(
    "%(asctime)s [%(levelname)s] %(filename)s %(lineno)s: %(message)s"
)
ch = logging.StreamHandler()

ch.setFormatter(fmt)
logger.handlers.append(ch)


================================================
FILE: easytrader/miniqmt/__init__.py
================================================
from easytrader.miniqmt.miniqmt_trader import MiniqmtTrader, DefaultXtQuantTraderCallback

================================================
FILE: easytrader/miniqmt/miniqmt_trader.py
================================================
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant
import random
from easytrader.log import logger
from easytrader.utils.perf import perf_clock
from easytrader.utils.stock import get_stock_type

# 市价委托类型映射
MARKET_ORDER_TYPE_NAME_MAP = {
    "sh": {
        "对手方最优价格委托": xtconstant.MARKET_PEER_PRICE_FIRST,
        "本方最优价格委托": xtconstant.MARKET_MINE_PRICE_FIRST,
        "最优五档即时成交剩余撤销": xtconstant.MARKET_SH_CONVERT_5_CANCEL,
        "最优五档即时成交剩转限价": xtconstant.MARKET_SH_CONVERT_5_LIMIT,
    },
    "sz": {
        "对手方最优价格委托": xtconstant.MARKET_PEER_PRICE_FIRST,
        "本方最优价格委托": xtconstant.MARKET_MINE_PRICE_FIRST,
        "即时成交剩余撤销委托": xtconstant.MARKET_SZ_INSTBUSI_RESTCANCEL,
        "最优五档即时成交剩余撤销": xtconstant.MARKET_SZ_CONVERT_5_CANCEL,
        "全额成交或撤销委托": xtconstant.MARKET_SZ_FULL_OR_CANCEL,
    },
}

# 市价委托类型反向映射(不区分交易所)
MARKET_ORDER_TYPE_MAP = {
    xtconstant.STOCK_BUY: "买入",
    xtconstant.STOCK_SELL: "卖出",
    xtconstant.MARKET_PEER_PRICE_FIRST: "对手方最优价格委托",
    xtconstant.MARKET_MINE_PRICE_FIRST: "本方最优价格委托",
    xtconstant.MARKET_SH_CONVERT_5_CANCEL: "最优五档即时成交剩余撤销",
    xtconstant.MARKET_SH_CONVERT_5_LIMIT: "最优五档即时成交剩转限价",
    xtconstant.MARKET_SZ_INSTBUSI_RESTCANCEL: "即时成交剩余撤销委托",
    xtconstant.MARKET_SZ_CONVERT_5_CANCEL: "最优五档即时成交剩余撤销",
    xtconstant.MARKET_SZ_FULL_OR_CANCEL: "全额成交或撤销委托",
}

# 交易操作(offset_flag)映射
OFFSET_FLAG_MAP = {
    xtconstant.OFFSET_FLAG_OPEN: "买入",
    xtconstant.OFFSET_FLAG_CLOSE: "卖出",
    xtconstant.OFFSET_FLAG_FORCECLOSE: "强平",
    xtconstant.OFFSET_FLAG_CLOSETODAY: "平今",
    xtconstant.OFFSET_FLAG_ClOSEYESTERDAY: "平昨",
    xtconstant.OFFSET_FLAG_FORCEOFF: "强减",
    xtconstant.OFFSET_FLAG_LOCALFORCECLOSE: "本地强平",
}
# 委托状态(order_status)映射
ORDER_STATUS_MAP = {
    xtconstant.ORDER_UNREPORTED: "未报",
    xtconstant.ORDER_WAIT_REPORTING: "待报",
    xtconstant.ORDER_REPORTED: "已报",
    xtconstant.ORDER_REPORTED_CANCEL: "已报待撤",
    xtconstant.ORDER_PARTSUCC_CANCEL: "部成待撤",
    xtconstant.ORDER_PART_CANCEL: "部撤",
    xtconstant.ORDER_CANCELED: "已撤",
    xtconstant.ORDER_PART_SUCC: "部成",
    xtconstant.ORDER_SUCCEEDED: "已成",
    xtconstant.ORDER_JUNK: "废单",
    xtconstant.ORDER_UNKNOWN: "未知"
}

# 多空方向(direction)映射
DIRECTION_MAP = {
    xtconstant.DIRECTION_FLAG_LONG: "多",
    xtconstant.DIRECTION_FLAG_SHORT: "空",
}

# 券商价格类型(price_type)映射
# 官网文档见 https://dict.thinktrader.net/innerApi/enum_constants.html?id=7zqjlm#enum-ebrokerpricetype-%E4%BB%B7%E6%A0%BC%E7%B1%BB%E5%9E%8B
BROKER_PRICE_TYPE_MAP = {
    49: "市价",  # enum_EBrokerPriceType.BROKER_PRICE_ANY
    50: "限价",  # enum_EBrokerPriceType.BROKER_PRICE_LIMIT
    51: "最优价",  # enum_EBrokerPriceType.BROKER_PRICE_BEST
    52: "配股",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_ALLOTMENT
    53: "转托",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_REFER
    54: "申购",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_SUBSCRIBE
    55: "回购",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_BUYBACK
    56: "配售",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_PLACING
    57: "指定",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DECIDE
    58: "转股",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_EQUITY
    59: "回售",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_SELLBACK
    60: "股息",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DIVIDEND
    68: "深圳配售确认",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_SHENZHEN_PLACING
    69: "配售放弃",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_CANCEL_PLACING
    70: "无冻质押",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_WDZY
    71: "冻结质押",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DJZY
    72: "无冻解押",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_WDJY
    73: "解冻解押",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_JDJY
    75: "投票",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_VOTE
    77: "预售要约解除",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_YSYYJC
    78: "基金设红",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FUND_DEVIDEND
    79: "基金申赎",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FUND_ENTRUST
    80: "跨市转托",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_CROSS_MARKET
    81: "ETF申购",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_ETF
    83: "权证行权",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_EXERCIS
    84: "对手方最优价格",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_PEER_PRICE_FIRST
    85: "最优五档即时成交剩余转限价",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_L5_FIRST_LIMITPX
    86: "本方最优价格",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_MIME_PRICE_FIRST
    87: "即时成交剩余撤销",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_INSTBUSI_RESTCANCEL
    88: "最优五档即时成交剩余撤销",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_L5_FIRST_CANCEL
    89: "全额成交并撤单",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FULL_REAL_CANCEL
    90: "基金拆合",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FUND_CHAIHE
    91: "债转股",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DEBT_CONVERSION
    92: "港股通竞价限价",  # BROKER_PRICE_BID_LIMIT
    93: "港股通增强限价",  # enum_EBrokerPriceType.BROKER_PRICE_ENHANCED_LIMIT
    94: "港股通零股限价",  # enum_EBrokerPriceType.BROKER_PRICE_RETAIL_LIMIT
    101: "直接还券",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DIRECT_SECU_REPAY
    107: "担保品划转",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_COLLATERAL_TRANSFER
    'j': "增发",
    'w': "定价",  # 全国股转 - 挂牌公司交易 - 协议转让
    'x': "成交确认",  # 全国股转 - 挂牌公司交易 - 协议转让
    'y': "互报成交确认",  # 全国股转 - 挂牌公司交易 - 协议转让
    'z': "限价",  # 用于挂牌公司交易 - 做市转让 - 限价买卖和两网及退市交易-限价买卖
}


class DefaultXtQuantTraderCallback(XtQuantTraderCallback):
    """
    XtQuantTrader回调类的默认实现
    """

    def on_disconnected(self):
        """
        连接状态回调
        :return:
        """
        logger.info("连接断开")

    def on_account_status(self, status):
        """
        账号状态信息推送
        :param response: XtAccountStatus 对象
        :return:
        """
        logger.info(
            f"账户状态信息: account_id={status.account_id}, account_type={status.account_type}, status={status.status}"
        )

    def on_stock_order(self, order):
        """
        委托信息推送
        :param order: XtOrder对象
        :return:
        """
        logger.info(
            f"委托回调: stock_code={order.stock_code}, order_status={order.order_status}, order_sysid={order.order_sysid}"
        )

    def on_stock_trade(self, trade):
        """
        成交信息推送
        :param trade: XtTrade对象
        :return:
        """
        logger.info(
            f"成交回调: account_id={trade.account_id}, stock_code={trade.stock_code}, order_id={trade.order_id}"
        )

    def on_order_error(self, order_error):
        """
        下单失败信息推送
        :param order_error:XtOrderError 对象
        :return:
        """
        logger.info(
            f"下单失败回调: order_id={order_error.order_id}, error_id={order_error.error_id}, error_msg={order_error.error_msg}"
        )

    def on_cancel_error(self, cancel_error):
        """
        撤单失败信息推送
        :param cancel_error: XtCancelError 对象
        :return:
        """
        logger.info(
            f"撤单失败回调: order_id={cancel_error.order_id}, error_id={cancel_error.error_id}, error_msg={cancel_error.error_msg}"
        )

    def on_order_stock_async_response(self, response):
        """
        异步下单回报推送
        :param response: XtOrderResponse 对象
        :return:
        """
        logger.info(f"异步下单回报: account_id={response.account_id}, order_id={response.order_id}, seq={response.seq}")

    def on_smt_appointment_async_response(self, response):
        """
        :param response: XtAppointmentResponse 对象
        :return:
        """
        logger.info(
            f"预约委托异步回报: account_id={response.account_id}, order_sysid={response.order_sysid}, error_id={response.error_id}, error_msg={response.error_msg}, seq={response.seq}"
        )


class MiniqmtTrader:
    broker_type = "miniqmt"

    def __init__(self):
        self._account: StockAccount = None
        self._trader: XtQuantTrader = None

    def prepare(self, **json_data):
        """
        allow remoteClient to pass param to miniqmt
        """
        self.connect(**json_data.get('kwargs',{}))

    def connect(
        self,
        miniqmt_path: str = r"D:\国金证券QMT交易端\userdata_mini",
        stock_account: str = None,
        trader_callback: XtQuantTraderCallback = DefaultXtQuantTraderCallback(),
    ):
        """
        连接到 miniqmt 交易端
        注意:登录qmt客户端时必须勾选极简模式/独立交易模式,否则无法连接
        :param miniqmt_path: miniqmt 安装路径,类似 r"D:\\国金证券QMT交易端\\userdata_mini"
            注意:不建议安装在C盘。安装在C盘的话,每次都需要用管理员权限运行策略,才能正常连接,否则有权限问题
        :param stock_account: 资金账号
        :param trader_callback: 交易回调对象,默认使用 DefaultXtQuantTraderCallback
        :return: None
        """
        session_id = int(random.randint(100000, 999999))
        self._trader = XtQuantTrader(miniqmt_path, session_id, callback=trader_callback)
        self._trader.start()

        if self._trader.connect() == 0:
            logger.info(f'成功连接到 miniqmt, 账号 {stock_account}')
            self._account = StockAccount(stock_account)
            self._trader.subscribe(self._account)
        else:
            logger.error('连接失败,请检查路径或其他情况')

    @property
    def trader(self) -> XtQuantTrader:
        """
        获取交易对象
        :return: XtQuantTrader 对象
        """
        return self._trader

    @property
    def account(self) -> StockAccount:
        """
        获取账户对象
        :return: StockAccount 对象
        """
        return self._account

    @property
    def balance(self):
        """
        获取账户资产信息。
        qmt 官方文档:https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%B5%84%E4%BA%A7%E6%9F%A5%E8%AF%A2

        :return:
            list of dict: 包含账户资产信息的字典,包括:
            - total_asset: 总资产
            - market_value: 持仓市值
            - cash: 可用资金
            - frozen_cash: 冻结资金
            - account_type: 账户类型
            - account_id: 账户ID
        """
        asset = self._trader.query_stock_asset(self._account)
        return [
            {
                'total_asset': asset.total_asset,
                'market_value': asset.market_value,
                'cash': asset.cash,
                'frozen_cash': asset.frozen_cash,
                'account_type': asset.account_type,
                'account_id': asset.account_id,
            }
        ]

    @property
    def position(self):
        """
        获取账户持仓信息。
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%8C%81%E4%BB%93%E6%9F%A5%E8%AF%A2

        :return:
            list of dict: 包含账户持仓信息的字典列表,每个字典包括:
            - stock_code: 证券代码
            - security: 六位证券代码
            - volume: 持仓数量
            - can_use_volume: 可用数量
            - open_price: 开仓价
            - market_value: 市值
            - frozen_volume: 冻结数量
            - on_road_volume: 在途股份
            - yesterday_volume: 昨夜拥股
            - avg_price: 成本价
            - direction: 多空方向
            - account_type: 账号类型
            - account_id: 资金账号
        """
        xt_positions = self._trader.query_stock_positions(self._account)
        positions = []
        for pos in xt_positions:
            positions.append(
                {
                    'stock_code': pos.stock_code,
                    'security': pos.stock_code[:6],
                    'volume': pos.volume,
                    'can_use_volume': pos.can_use_volume,
                    'open_price': pos.open_price,
                    'market_value': pos.market_value,
                    'frozen_volume': pos.frozen_volume,
                    'on_road_volume': pos.on_road_volume,
                    'yesterday_volume': pos.yesterday_volume,
                    'avg_price': pos.avg_price,
                    'direction': pos.direction,
                    'account_type': pos.account_type,
                    'account_id': pos.account_id,
                }
            )
        return positions

    @property
    def today_entrusts(self):
        """
        获取今日委托列表。
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E5%A7%94%E6%89%98%E6%9F%A5%E8%AF%A2

        :return:
            list of dict: 包含委托信息的字典列表,每个字典包括:
            - stock_code: 证券代码
            - security: 六位证券代码
            - order_id: 订单编号
            - order_sysid: 柜台合同编号
            - order_time: 报单时间
            - order_type: 委托类型
            - order_type_name: 委托类型名称
            - order_volume: 委托数量
            - price_type: 报价类型
            - price_type_name: 报价类型名称
            - price: 委托价格
            - traded_volume: 成交数量
            - traded_price: 成交均价
            - order_status: 委托状态
            - order_status_name: 委托状态名称
            - status_msg: 委托状态描述
            - offset_flag: 交易操作
            - offset_flag_name: 交易操作名称
            - strategy_name: 策略名称
            - order_remark: 委托备注
            - direction: 多空方向
            - direction_name: 多空方向名称
            - account_type: 账号类型
            - account_id: 资金账号
        """
        xt_orders = self._trader.query_stock_orders(self._account, False)
        if xt_orders is None:
            return []

        orders = []
        for order in xt_orders:
            orders.append(
                {
                    'security': order.stock_code[:6],
                    'stock_code': order.stock_code,
                    'order_id': order.order_id,
                    'order_sysid': order.order_sysid,
                    'order_time': order.order_time,
                    'order_type': order.order_type,
                    'order_type_name': MARKET_ORDER_TYPE_MAP.get(order.order_type, order.order_type),
                    'order_volume': order.order_volume,
                    'price_type': order.price_type,
                    'price_type_name': BROKER_PRICE_TYPE_MAP.get(order.price_type, order.price_type),
                    'price': order.price,
                    'traded_volume': order.traded_volume,
                    'traded_price': order.traded_price,
                    'order_status': order.order_status,
                    'order_status_name': ORDER_STATUS_MAP.get(order.order_status, order.order_status),
                    'status_msg': order.status_msg,
                    'offset_flag': order.offset_flag,
                    'offset_flag_name': OFFSET_FLAG_MAP.get(order.offset_flag, order.offset_flag),
                    'strategy_name': order.strategy_name,
                    'order_remark': order.order_remark,
                    'direction': order.direction,
                    'direction_name': DIRECTION_MAP.get(order.direction, order.direction),
                    'account_type': order.account_type,
                    'account_id': order.account_id,
                }
            )
        return orders

    @property
    def today_trades(self):
        """
        获取今日成交列表。
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%88%90%E4%BA%A4%E6%9F%A5%E8%AF%A2

        :return:
            list of dict: 包含成交信息的字典列表,每个字典包括:
            - stock_code: 证券代码
            - security: 六位证券代码
            - traded_id: 成交编号
            - traded_time: 成交时间
            - traded_price: 成交均价
            - traded_volume: 成交数量
            - traded_amount: 成交金额
            - order_id: 订单编号
            - order_type: 委托类型
            - order_type_name: 委托类型名称
            - offset_flag: 交易操作(买入/卖出)
            - offset_flag_name: 交易操作名称
            - account_id: 资金账号
            - account_type: 账号类型
            - order_sysid: 柜台合同编号
            - strategy_name: 策略名称
            - order_remark: 委托备注
        """
        xt_trades = self._trader.query_stock_trades(self._account)
        if xt_trades is None:
            return []

        trades = []
        for trade in xt_trades:
            trades.append(
                {
                    'security': trade.stock_code[:6],
                    'stock_code': trade.stock_code,
                    'traded_id': trade.traded_id,
                    'traded_time': trade.traded_time,
                    'traded_price': trade.traded_price,
                    'traded_volume': trade.traded_volume,
                    'traded_amount': trade.traded_amount,
                    'order_id': trade.order_id,
                    'order_type': trade.order_type,
                    'order_type_name': MARKET_ORDER_TYPE_MAP.get(trade.order_type, trade.order_type),
                    'offset_flag': trade.offset_flag,
                    'offset_flag_name': OFFSET_FLAG_MAP.get(trade.offset_flag, trade.offset_flag),
                    'account_id': trade.account_id,
                    'account_type': trade.account_type,
                    'order_sysid': trade.order_sysid,
                    'strategy_name': trade.strategy_name,
                    'order_remark': trade.order_remark,
                }
            )
        return trades

    @perf_clock
    def buy(self, security: str, price: float, amount: int, **kwargs):
        """
        限价买入
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%82%A1%E7%A5%A8%E5%90%8C%E6%AD%A5%E6%8A%A5%E5%8D%95

        :param security: 六位证券代码
        :param price: 交易价格
        :param amount: 交易数量
        :return: {'entrust_no': '订单编号'}
            系统生成的订单编号,成功发送委托后的订单编号为大于0的正整数,如果为-1表示委托失败
            注:有订单编号不一定表示成功,具体成功与否需要查看下单回调 on_order_error。
            例如非交易时间下单可以拿到订单编号,但 on_order_error 回调会报错:
            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]
        """
        return self.trade(security, price, amount, is_buy=True)

    @perf_clock
    def sell(self, security, price, amount, **kwargs):
        """
        限价卖出
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%82%A1%E7%A5%A8%E5%90%8C%E6%AD%A5%E6%8A%A5%E5%8D%95

        :param security: 六位证券代码
        :param price: 交易价格
        :param amount: 交易数量
        :return: {'entrust_no': '订单编号'}
            系统生成的订单编号,成功发送委托后的订单编号为大于0的正整数,如果为-1表示委托失败
            注:有订单编号不一定表示成功,具体成功与否需要查看下单回调 on_order_error。
            例如非交易时间下单可以拿到订单编号,但 on_order_error 回调会报错:
            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]
        """

        return self.trade(security, price, amount, is_buy=False)

    def trade(self, security: str, price: float, amount: int, *, is_buy: bool) -> int:
        """
        限价交易
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%82%A1%E7%A5%A8%E5%90%8C%E6%AD%A5%E6%8A%A5%E5%8D%95

        :param security: 六位证券代码
        :param price: 交易价格
        :param amount: 交易数量
        :param is_buy: 是否为买入
        :return: {'entrust_no': '订单编号'}
            系统生成的订单编号,成功发送委托后的订单编号为大于0的正整数,如果为-1表示委托失败
            注:有订单编号不一定表示成功,具体成功与否需要查看下单回调 on_order_error。
            例如非交易时间下单可以拿到订单编号,但 on_order_error 回调会报错:
            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]
        """
        action = "买入" if is_buy else "卖出"
        logger.info(f"限价{action}请求: 股票代码={security}, 价格={price}, 数量={amount}")
        
        order_id = self._trader.order_stock(
            account=self._account,
            stock_code=self._get_stock_code(security),
            order_type=xtconstant.STOCK_BUY if is_buy else xtconstant.STOCK_SELL,
            order_volume=amount,
            price_type=xtconstant.FIX_PRICE,
            price=price,
        )
        
        if order_id > 0:
            logger.info(f"限价{action}委托成功: 股票代码={security}, 委托单号={order_id}")
        else:
            logger.error(f"限价{action}委托失败: 股票代码={security}, 错误码={order_id}")
            
        return {'entrust_no': order_id}

    @perf_clock
    def market_buy(self, security, amount, ttype=None):
        """
        市价买入
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%82%A1%E7%A5%A8%E5%90%8C%E6%AD%A5%E6%8A%A5%E5%8D%95

        :param security: 六位证券代码
        :param amount: 交易数量
        :param ttype: 市价委托类型,默认'对手方最优价格委托'
                 深市可选:
                - 对手方最优价格委托
                - 本方最优价格委托
                - 即时成交剩余撤销委托
                - 最优五档即时成交剩余撤销
                - 全额成交或撤销委托
                 沪市可选:
                - 对手方最优价格委托
                - 最优五档即时成交剩余撤销
                - 最优五档即时成交剩转限价
                - 本方最优价格委托
        :return: {'entrust_no': '订单编号'}
            系统生成的订单编号,成功发送委托后的订单编号为大于0的正整数,如果为-1表示委托失败
            注:有订单编号不一定表示成功,具体成功与否需要查看下单回调 on_order_error。
            例如非交易时间下单可以拿到订单编号,但 on_order_error 回调会报错:
            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]
        """

        return self.market_trade(security, amount, ttype, is_buy=True)

    @perf_clock
    def market_sell(self, security, amount, ttype=None):
        """
        市价卖出
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%82%A1%E7%A5%A8%E5%90%8C%E6%AD%A5%E6%8A%A5%E5%8D%95

        :param security: 六位证券代码
        :param amount: 交易数量
        :param ttype: 市价委托类型,默认'对手方最优价格委托'
                 深市可选:
                - 对手方最优价格委托
                - 本方最优价格委托
                - 即时成交剩余撤销委托
                - 最优五档即时成交剩余撤销
                - 全额成交或撤销委托
                 沪市可选:
                - 对手方最优价格委托
                - 最优五档即时成交剩余撤销
                - 最优五档即时成交剩转限价
                - 本方最优价格委托
        :return: {'entrust_no': '订单编号'}
            系统生成的订单编号,成功发送委托后的订单编号为大于0的正整数,如果为-1表示委托失败
            注:有订单编号不一定表示成功,具体成功与否需要查看下单回调 on_order_error。
            例如非交易时间下单可以拿到订单编号,但 on_order_error 回调会报错:
            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]
        """

        return self.market_trade(security, amount, ttype, is_buy=False)

    def market_trade(self, security: str, amount: int, ttype: str = None, *, is_buy: bool):
        """
        市价交易
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%82%A1%E7%A5%A8%E5%90%8C%E6%AD%A5%E6%8A%A5%E5%8D%95

        :param security: 六位证券代码
        :param amount: 交易数量
        :param ttype: 市价委托类型,默认'对手方最优价格委托'
                 深市可选:
                - 对手方最优价格委托
                - 本方最优价格委托
                - 即时成交剩余撤销委托
                - 最优五档即时成交剩余撤销
                - 全额成交或撤销委托
                 沪市可选:
                - 对手方最优价格委托
                - 最优五档即时成交剩余撤销
                - 最优五档即时成交剩转限价
                - 本方最优价格委托
        :return: {'entrust_no': '订单编号'}
            系统生成的订单编号,成功发送委托后的订单编号为大于0的正整数,如果为-1表示委托失败
            注:有订单编号不一定表示成功,具体成功与否需要查看下单回调 on_order_error。
            例如非交易时间下单可以拿到订单编号,但 on_order_error 回调会报错:
            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]
        """
        if ttype is None:
            ttype = '对手方最优价格委托'

        action = "市价买入" if is_buy else "市价卖出"
        logger.info(f"{action}请求: 股票代码={security}, 委托类型={ttype}, 数量={amount}")

        def _get_price_type(security: str, ttype: str) -> int:
            """报价类型"""
            exchange = get_stock_type(security)
            if ttype not in MARKET_ORDER_TYPE_NAME_MAP[exchange]:
                raise ValueError(f"{exchange}市场不支持的市价委托类型: {ttype}")
            return MARKET_ORDER_TYPE_NAME_MAP[exchange][ttype]

        order_id = self._trader.order_stock(
            account=self._account,
            stock_code=self._get_stock_code(security),
            order_type=xtconstant.STOCK_BUY if is_buy else xtconstant.STOCK_SELL,
            order_volume=amount,
            price_type=_get_price_type(security, ttype),
            price=0,
        )
        
        if order_id > 0:
            logger.info(f"{action}委托成功: 股票代码={security}, 委托单号={order_id}")
        else:
            logger.error(f"{action}委托失败: 股票代码={security}, 错误码={order_id}")
            
        return {'entrust_no': order_id}

    @perf_clock
    def cancel_entrust(self, entrust_no: int):
        """
        撤销委托单
        qmt 官方文档: https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%82%A1%E7%A5%A8%E5%90%8C%E6%AD%A5%E6%92%A4%E5%8D%95

        :param entrust_no: 委托单号,由买入或卖出函数返回
        :return: {'success': True/False, 'message': '撤单结果'}
                 True: 成功发出撤单指令,False: 撤单失败
        """
        result = self._trader.cancel_order_stock(self._account, entrust_no)
        # 根据官方文档,0表示成功,-1表示失败
        if result == 0:
            return {'success': True, 'message': 'success'}
        else:
            return {'success': False, 'message': 'failed'}

    def _get_stock_code(self, security: str) -> str:
        """
        获取股票代码
        :param security: 六位证券代码
        :return: 格式化的股票代码
        """
        return f'{security}.{get_stock_type(security).upper()}'


================================================
FILE: easytrader/pop_dialog_handler.py
================================================
# coding:utf-8
import re
import time
from typing import Optional

from easytrader import exceptions
from easytrader.utils.perf import perf_clock
from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines


class PopDialogHandler:
    def __init__(self, app):
        self._app = app

    @staticmethod
    def _set_foreground(window):
        if window.has_style(win32defines.WS_MINIMIZE):  # if minimized
            ShowWindow(window.wrapper_object(), 9)  # restore window state
        else:
            SetForegroundWindow(window.wrapper_object())  # bring to front

    @perf_clock
    def handle(self, title):
        if any(s in title for s in {"提示信息", "委托确认", "网上交易用户协议", "撤单确认"}):
            self._submit_by_shortcut()
            return None

        if "提示" in title:
            content = self._extract_content()
            self._submit_by_click()
            return {"message": content}

        content = self._extract_content()
        self._close()
        return {"message": "unknown message: {}".format(content)}

    def _extract_content(self):
        return self._app.top_window().Static.window_text()

    @staticmethod
    def _extract_entrust_id(content):
        return re.search(r"[\da-zA-Z]+", content).group()

    def _submit_by_click(self):
        try:
            self._app.top_window()["确定"].click()
        except Exception as ex:
            self._app.Window_(best_match="Dialog", top_level_only=True).ChildWindow(
                best_match="确定"
            ).click()

    def _submit_by_shortcut(self):
        self._set_foreground(self._app.top_window())
        self._app.top_window().type_keys("%Y", set_foreground=False)

    def _close(self):
        self._app.top_window().close()


class TradePopDialogHandler(PopDialogHandler):
    @perf_clock
    def handle(self, title) -> Optional[dict]:
        if title == "委托确认":
            self._submit_by_shortcut()
            return None

        if title == "提示信息":
            content = self._extract_content()
            if "超出涨跌停" in content:
                self._submit_by_shortcut()
                return None

            if "委托价格的小数价格应为" in content:
                self._submit_by_shortcut()
                return None

            if "逆回购" in content:
                self._submit_by_shortcut()
                return None

            if "正回购" in content:
                self._submit_by_shortcut()
                return None

            return None

        if title == "提示":
            content = self._extract_content()
            if "成功" in content:
                entrust_no = self._extract_entrust_id(content)
                self._submit_by_click()
                return {"entrust_no": entrust_no}

            self._submit_by_click()
            time.sleep(0.05)
            raise exceptions.TradeError(content)
        self._close()
        return None


================================================
FILE: easytrader/refresh_strategies.py
================================================
# -*- coding: utf-8 -*-
import abc
import io
import tempfile
from io import StringIO
from typing import TYPE_CHECKING, Dict, List, Optional

import pandas as pd
import pywinauto.keyboard
import pywinauto
import pywinauto.clipboard

from easytrader.log import logger
from easytrader.utils.captcha import captcha_recognize
from easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines

if TYPE_CHECKING:
    # pylint: disable=unused-import
    from easytrader import clienttrader


class IRefreshStrategy(abc.ABC):
    _trader: "clienttrader.ClientTrader"

    @abc.abstractmethod
    def refresh(self):
        """
        刷新数据
        """
        pass

    def set_trader(self, trader: "clienttrader.ClientTrader"):
        self._trader = trader


# noinspection PyProtectedMember
class Switch(IRefreshStrategy):
    """通过切换菜单栏刷新"""

    def __init__(self, sleep: float = 0.1):
        self.sleep = sleep

    def refresh(self):
        self._trader._switch_left_menus_by_shortcut("{F5}", sleep=self.sleep)


# noinspection PyProtectedMember
class Toolbar(IRefreshStrategy):
    """通过点击工具栏刷新按钮刷新"""

    def __init__(self, refresh_btn_index: int = 4):
        """
        :param refresh_btn_index:
            交易客户端工具栏中“刷新”排序,默认为第4个,请根据自己实际调整
        """
        self.refresh_btn_index = refresh_btn_index

    def refresh(self):
        self._trader._toolbar.button(self.refresh_btn_index - 1).click()


================================================
FILE: easytrader/remoteclient.py
================================================
# -*- coding: utf-8 -*-
import requests

from easytrader.utils.misc import file2dict


def use(broker, host, port=1430, **kwargs):
    return RemoteClient(broker, host, port, **kwargs)


class RemoteClient:
    def __init__(self, broker, host, port=1430, **kwargs):
        self._s = requests.session()
        # 支持 basic auth 或 其它 auth 方法
        if kwargs.get("user") and kwargs.get("passwd"):
            self._s.auth = requests.auth.HTTPBasicAuth(
                kwargs.get("user"), kwargs.get("passwd")
            )
        elif kwargs.get("auth"):
            self._s.auth = kwargs.get("auth")

        # 支持 ssl (有时候需要过某些反向代理要用https协议)
        self._api = f"http{'s' if kwargs.get('ssl') is True else ''}://{host}:{port}"
        self._broker = broker

    def prepare(
        self,
        config_path=None,
        user=None,
        password=None,
        exe_path=None,
        comm_password=None,
        **kwargs,
    ):
        """
        登陆客户端
        :param config_path: 登陆配置文件,跟参数登陆方式二选一
        :param user: 账号
        :param password: 明文密码
        :param exe_path: 客户端路径类似 r'C:\\htzqzyb2\\xiadan.exe',
            默认 r'C:\\htzqzyb2\\xiadan.exe'
        :param comm_password: 通讯密码
        :return:
        """
        params = locals().copy()
        params.pop("self")

        if config_path is not None:
            account = file2dict(config_path)
            params["user"] = account["user"]
            params["password"] = account["password"]

        params["broker"] = self._broker

        response = self._s.post(self._api + "/prepare", json=params)
        if response.status_code >= 300:
            raise Exception(response.json()["error"])
        return response.json()

    @property
    def balance(self):
        return self.common_get("balance")

    @property
    def position(self):
        return self.common_get("position")

    @property
    def today_entrusts(self):
        return self.common_get("today_entrusts")

    @property
    def today_trades(self):
        return self.common_get("today_trades")

    @property
    def cancel_entrusts(self):
        return self.common_get("cancel_entrusts")

    def auto_ipo(self):
        return self.common_get("auto_ipo")

    def exit(self):
        return self.common_get("exit")

    def common_get(self, endpoint):
        response = self._s.get(self._api + "/" + endpoint)
        if response.status_code >= 300:
            raise Exception(response.json()["error"])
        return response.json()

    def buy(self, security, price, amount, **kwargs):
        params = locals().copy()
        params.pop("self")

        response = self._s.post(self._api + "/buy", json=params)
        if response.status_code >= 300:
            raise Exception(response.json()["error"])
        return response.json()

    def sell(self, security, price, amount, **kwargs):
        params = locals().copy()
        params.pop("self")

        response = self._s.post(self._api + "/sell", json=params)
        if response.status_code >= 300:
            raise Exception(response.json()["error"])
        return response.json()

    def market_buy(self, security, amount, **kwargs):
        params = locals().copy()
        params.pop("self")

        response = self._s.post(self._api + "/market_buy", json=params)
        if response.status_code >= 300:
            raise Exception(response.json()["error"])
        return response.json()

    def market_sell(self, security, amount, **kwargs):
        params = locals().copy()
        params.pop("self")

        response = self._s.post(self._api + "/market_sell", json=params)
        if response.status_code >= 300:
            raise Exception(response.json()["error"])
        return response.json()

    def cancel_entrust(self, entrust_no):
        params = locals().copy()
        params.pop("self")

        response = self._s.post(self._api + "/cancel_entrust", json=params)
        if response.status_code >= 300:
            raise Exception(response.json()["error"])
        return response.json()


================================================
FILE: easytrader/ricequant_follower.py
================================================
# -*- coding: utf-8 -*-

from datetime import datetime
from threading import Thread

from easytrader.follower import BaseFollower
from easytrader.log import logger


class RiceQuantFollower(BaseFollower):
    def __init__(self):
        super().__init__()
        self.client = None

    def login(self, user=None, password=None, **kwargs):
        from rqopen_client import RQOpenClient

        self.client = RQOpenClient(user, password, logger=logger)

    def follow(
        self,
        users,
        run_id,
        track_interval=1,
        trade_cmd_expire_seconds=120,
        cmd_cache=True,
        entrust_prop="limit",
        send_interval=0,
    ):
        """跟踪ricequant对应的模拟交易,支持多用户多策略
        :param users: 支持easytrader的用户对象,支持使用 [] 指定多个用户
        :param run_id: ricequant 的模拟交易ID,支持使用 [] 指定多个模拟交易
        :param track_interval: 轮训模拟交易时间,单位为秒
        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒
        :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令
        :param entrust_prop: 委托方式, 'limit' 为限价,'market' 为市价, 仅在银河实现
        :param send_interval: 交易发送间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足
        """
        users = self.warp_list(users)
        run_ids = self.warp_list(run_id)

        if cmd_cache:
            self.load_expired_cmd_cache()

        self.start_trader_thread(
            users, trade_cmd_expire_seconds, entrust_prop, send_interval
        )

        workers = []
        for id_ in run_ids:
            strategy_name = self.extract_strategy_name(id_)
            strategy_worker = Thread(
                target=self.track_strategy_worker,
                args=[id_, strategy_name],
                kwargs={"interval": track_interval},
            )
            strategy_worker.start()
            workers.append(strategy_worker)
            logger.info("开始跟踪策略: %s", strategy_name)
        for worker in workers:
            worker.join()

    def extract_strategy_name(self, run_id):
        ret_json = self.client.get_positions(run_id)
        if ret_json["code"] != 200:
            logger.error(
                "fetch data from run_id %s fail, msg %s",
                run_id,
                ret_json["msg"],
            )
            raise RuntimeError(ret_json["msg"])
        return ret_json["resp"]["name"]

    def extract_day_trades(self, run_id):
        ret_json = self.client.get_day_trades(run_id)
        if ret_json["code"] != 200:
            logger.error(
                "fetch day trades from run_id %s fail, msg %s",
                run_id,
                ret_json["msg"],
            )
            raise RuntimeError(ret_json["msg"])
        return ret_json["resp"]["trades"]

    def query_strategy_transaction(self, strategy, **kwargs):
        transactions = self.extract_day_trades(strategy)
        transactions = self.project_transactions(transactions, **kwargs)
        return self.order_transactions_sell_first(transactions)

    @staticmethod
    def stock_shuffle_to_prefix(stock):
        assert (
            len(stock) == 11
        ), "stock {} must like 123456.XSHG or 123456.XSHE".format(stock)
        code = stock[:6]
        if stock.find("XSHG") != -1:
            return "sh" + code
        if stock.find("XSHE") != -1:
            return "sz" + code
        raise TypeError("not valid stock code: {}".format(code))

    def project_transactions(self, transactions, **kwargs):
        new_transactions = []
        for transaction in transactions:
            new_transaction = {}
            new_transaction["price"] = transaction["price"]
            new_transaction["amount"] = int(abs(transaction["quantity"]))
            new_transaction["datetime"] = datetime.strptime(
                transaction["time"], "%Y-%m-%d %H:%M:%S"
            )
            new_transaction["stock_code"] = self.stock_shuffle_to_prefix(
                transaction["order_book_id"]
            )
            new_transaction["action"] = (
                "buy" if transaction["quantity"] > 0 else "sell"
            )
            new_transactions.append(new_transaction)

        return new_transactions


================================================
FILE: easytrader/server.py
================================================
import functools

from flask import Flask, jsonify, request

from . import api
from .log import logger

app = Flask(__name__)

global_store = {}


def error_handle(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        # pylint: disable=broad-except
        except Exception as e:
            logger.exception("server error")
            message = "{}: {}".format(e.__class__, e)
            return jsonify({"error": message}), 400

    return wrapper


@app.route("/prepare", methods=["POST"])
@error_handle
def post_prepare():
    json_data = request.get_json(force=True)

    user = api.use(json_data.pop("broker"))
    user.prepare(**json_data)

    global_store["user"] = user
    return jsonify({"msg": "login success"}), 201


@app.route("/balance", methods=["GET"])
@error_handle
def get_balance():
    user = global_store["user"]
    balance = user.balance

    return jsonify(balance), 200


@app.route("/position", methods=["GET"])
@error_handle
def get_position():
    user = global_store["user"]
    position = user.position

    return jsonify(position), 200


@app.route("/auto_ipo", methods=["GET"])
@error_handle
def get_auto_ipo():
    user = global_store["user"]
    res = user.auto_ipo()

    return jsonify(res), 200


@app.route("/today_entrusts", methods=["GET"])
@error_handle
def get_today_entrusts():
    user = global_store["user"]
    today_entrusts = user.today_entrusts

    return jsonify(today_entrusts), 200


@app.route("/today_trades", methods=["GET"])
@error_handle
def get_today_trades():
    user = global_store["user"]
    today_trades = user.today_trades

    return jsonify(today_trades), 200


@app.route("/cancel_entrusts", methods=["GET"])
@error_handle
def get_cancel_entrusts():
    user = global_store["user"]
    cancel_entrusts = user.cancel_entrusts

    return jsonify(cancel_entrusts), 200


@app.route("/buy", methods=["POST"])
@error_handle
def post_buy():
    json_data = request.get_json(force=True)
    user = global_store["user"]
    res = user.buy(**json_data)

    return jsonify(res), 201


@app.route("/sell", methods=["POST"])
@error_handle
def post_sell():
    json_data = request.get_json(force=True)

    user = global_store["user"]
    res = user.sell(**json_data)

    return jsonify(res), 201


@app.route("/cancel_entrust", methods=["POST"])
@error_handle
def post_cancel_entrust():
    json_data = request.get_json(force=True)

    user = global_store["user"]
    res = user.cancel_entrust(**json_data)

    return jsonify(res), 201


@app.route("/exit", methods=["GET"])
@error_handle
def get_exit():
    user = global_store["user"]
    user.exit()

    return jsonify({"msg": "exit success"}), 200


def run(port=1430):
    app.run(host="0.0.0.0", port=port)


================================================
FILE: easytrader/universal_clienttrader.py
================================================
# -*- coding: utf-8 -*-

import pywinauto
import pywinauto.clipboard

from easytrader import grid_strategies
from . import clienttrader


class UniversalClientTrader(clienttrader.BaseLoginClientTrader):
    grid_strategy = grid_strategies.Xls

    @property
    def broker_type(self):
        return "universal"

    def login(self, user, password, exe_path, comm_password=None, **kwargs):
        """
        :param user: 用户名
        :param password: 密码
        :param exe_path: 客户端路径, 类似
        :param comm_password:
        :param kwargs:
        :return:
        """
        self._editor_need_type_keys = False

        try:
            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=1
            )
        # pylint: disable=broad-except
        except Exception:
            self._app = pywinauto.Application().start(exe_path)

            # wait login window ready
            while True:
                try:
                    login_window = pywinauto.findwindows.find_window(class_name='#32770', found_index=1)
                    break
                except:
                    self.wait(1)

            self.wait(1)
            self._app.window(handle=login_window).Edit1.set_focus()
            self._app.window(handle=login_window).Edit1.type_keys(user)

            self._app.window(handle=login_window).button7.click()

            # detect login is success or not
            # self._app.top_window().wait_not("exists", 100)
            self.wait(5)

            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=10
            )

        self._close_prompt_windows()
        self._main = self._app.window(title="网上股票交易系统5.0")



================================================
FILE: easytrader/utils/__init__.py
================================================



================================================
FILE: easytrader/utils/captcha.py
================================================
import re

import requests
from PIL import Image

from easytrader import exceptions


def captcha_recognize(img_path):
    import pytesseract

    im = Image.open(img_path).convert("L")
    # 1. threshold the image
    threshold = 200
    table = []
    for i in range(256):
        if i < threshold:
            table.append(0)
        else:
            table.append(1)

    out = im.point(table, "1")
    # 2. recognize with tesseract
    num = pytesseract.image_to_string(out)
    return num


def recognize_verify_code(image_path, broker="ht"):
    """识别验证码,返回识别后的字符串,使用 tesseract 实现
    :param image_path: 图片路径
    :param broker: 券商 ['ht', 'yjb', 'gf', 'yh']
    :return recognized: verify code string"""

    if broker == "gf":
        return detect_gf_result(image_path)
    if broker in ["yh_client", "gj_client"]:
        return detect_yh_client_result(image_path)
    # 调用 tesseract 识别
    return default_verify_code_detect(image_path)


def detect_yh_client_result(image_path):
    """封装了tesseract的识别,部署在阿里云上,
    服务端源码地址为: https://github.com/shidenggui/yh_verify_code_docker"""
    api = "http://yh.ez.shidenggui.com:5000/yh_client"
    with open(image_path, "rb") as f:
        rep = requests.post(api, files={"image": f})
    if rep.status_code != 201:
        error = rep.json()["message"]
        raise exceptions.TradeError("request {} error: {}".format(api, error))
    return rep.json()["result"]


def input_verify_code_manual(image_path):
    from PIL import Image

    image = Image.open(image_path)
    image.show()
    code = input(
        "image path: {}, input verify code answer:".format(image_path)
    )
    return code


def default_verify_code_detect(image_path):
    from PIL import Image

    img = Image.open(image_path)
    return invoke_tesseract_to_recognize(img)


def detect_gf_result(image_path):
    from PIL import ImageFilter, Image

    img = Image.open(image_path)
    if hasattr(img, "width"):
        width, height = img.width, img.height
    else:
        width, height = img.size
    for x in range(width):
        for y in range(height):
            if img.getpixel((x, y)) < (100, 100, 100):
                img.putpixel((x, y), (256, 256, 256))
    gray = img.convert("L")
    two = gray.point(lambda p: 0 if 68 < p < 90 else 256)
    min_res = two.filter(ImageFilter.MinFilter)
    med_res = min_res.filter(ImageFilter.MedianFilter)
    for _ in range(2):
        med_res = med_res.filter(ImageFilter.MedianFilter)
    return invoke_tesseract_to_recognize(med_res)


def invoke_tesseract_to_recognize(img):
    import pytesseract

    try:
        res = pytesseract.image_to_string(img)
    except FileNotFoundError:
        raise Exception(
            "tesseract 未安装,请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程"
        )
    valid_chars = re.findall("[0-9a-z]", res, re.IGNORECASE)
    return "".join(valid_chars)


================================================
FILE: easytrader/utils/misc.py
================================================
# coding:utf-8
import json


def parse_cookies_str(cookies):
    """
    parse cookies str to dict
    :param cookies: cookies str
    :type cookies: str
    :return: cookie dict
    :rtype: dict
    """
    cookie_dict = {}
    for record in cookies.split(";"):
        key, value = record.strip().split("=", 1)
        cookie_dict[key] = value
    return cookie_dict


def file2dict(path):
    with open(path, encoding="utf-8") as f:
        return json.load(f)


def grep_comma(num_str):
    return num_str.replace(",", "")


def str2num(num_str, convert_type="float"):
    num = float(grep_comma(num_str))
    return num if convert_type == "float" else int(num)


================================================
FILE: easytrader/utils/perf.py
================================================
# coding:utf-8
import functools
import inspect
import logging
import timeit

from easytrader import logger

try:
    from time import process_time
except:
    from time import clock as process_time


def perf_clock(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        if not logger.isEnabledFor(logging.DEBUG):
            return f(*args, **kwargs)

        ts = timeit.default_timer()
        cs = process_time()
        ex = None
        result = None

        try:
            result = f(*args, **kwargs)
        except Exception as ex1:
            ex = ex1

        te = timeit.default_timer()
        ce = process_time()
        logger.debug(
            "%r consume %2.4f sec, cpu %2.4f sec. args %s, extra args %s"
            % (
                f.__name__,
                te - ts,
                ce - cs,
                args[1:],
                kwargs,
            )
        )
        if ex is not None:
            raise ex
        return result
    
    wrapper.__signature__ = inspect.signature(f)
    return wrapper


================================================
FILE: easytrader/utils/stock.py
================================================
# coding:utf-8
import datetime
import json
import random

import requests


def get_stock_type(stock_code):
    """判断股票ID对应的证券市场
    匹配规则
    ['4', '8'] 为 bj
    ['5', '6', '7', '9', '110', '113', '118', '132', '204'] 为 sh
    其余为 sz
    :param stock_code:股票ID, 若以 'sz', 'sh', 'bj' 开头直接返回对应类型,否则使用内置规则判断
    :return 'bj', 'sh' or 'sz'"""
    assert isinstance(stock_code, str), "stock code need str type"
    bj_head = ("43", "83", "87", "92")
    sh_head = ("5", "6", "7", "9", "110", "113", "118", "132", "204")
    if stock_code.startswith(("sh", "sz", "zz", "bj")):
        return stock_code[:2]
    elif stock_code.startswith(bj_head):
        return "bj"
    elif stock_code.startswith(sh_head):
        return "sh"
    return "sz"

def get_30_date():
    """
    获得用于查询的默认日期, 今天的日期, 以及30天前的日期
    用于查询的日期格式通常为 20160211
    :return:
    """
    now = datetime.datetime.now()
    end_date = now.date()
    start_date = end_date - datetime.timedelta(days=30)
    return start_date.strftime("%Y%m%d"), end_date.strftime("%Y%m%d")


def get_today_ipo_data():
    """
    查询今天可以申购的新股信息
    :return: 今日可申购新股列表 apply_code申购代码 price发行价格
    """

    agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0"
    send_headers = {
        "Host": "xueqiu.com",
        "User-Agent": agent,
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",
        "Accept-Encoding": "deflate",
        "Cache-Control": "no-cache",
        "X-Requested-With": "XMLHttpRequest",
        "Referer": "https://xueqiu.com/hq",
        "Connection": "keep-alive",
    }

    timestamp = random.randint(1000000000000, 9999999999999)
    home_page_url = "https://xueqiu.com"
    ipo_data_url = (
        "https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl"
        "_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_"
        "income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s"
        % (str(timestamp))
    )

    session = requests.session()
    session.get(home_page_url, headers=send_headers)  # 产生cookies
    ipo_response = session.post(ipo_data_url, headers=send_headers)

    json_data = json.loads(ipo_response.text)
    today_ipo = []

    for line in json_data.get("data", []):
        if datetime.datetime.now().strftime("%a %b %d") == line[3][:10]:
            today_ipo.append(
                {
                    "stock_code": line[0],
                    "stock_name": line[1],
                    "apply_code": line[2],
                    "price": line[7],
                }
            )

    return today_ipo


================================================
FILE: easytrader/utils/win_gui.py
================================================
# coding:utf-8
from pywinauto import win32defines
from pywinauto.win32functions import SetForegroundWindow, ShowWindow


================================================
FILE: easytrader/webtrader.py
================================================
# -*- coding: utf-8 -*-
import abc
import logging
import os
import re
import time
from threading import Thread

import requests
import requests.exceptions

from easytrader import exceptions
from easytrader.log import logger
from easytrader.utils.misc import file2dict, str2num
from easytrader.utils.stock import get_30_date


# noinspection PyIncorrectDocstring
class WebTrader(metaclass=abc.ABCMeta):
    global_config_path = os.path.dirname(__file__) + "/config/global.json"
    config_path = ""

    def __init__(self, debug=True):
        self.__read_config()
        self.trade_prefix = self.config["prefix"]
        self.account_config = ""
        self.heart_active = True
        self.heart_thread = Thread(target=self.send_heartbeat)
        self.heart_thread.setDaemon(True)

        self.log_level = logging.DEBUG if debug else logging.INFO

    def read_config(self, path):
        try:
            self.account_config = file2dict(path)
        except ValueError:
            logger.error("配置文件格式有误,请勿使用记事本编辑,推荐 sublime text")
        for value in self.account_config:
            if isinstance(value, int):
                logger.warning("配置文件的值最好使用双引号包裹,使用字符串,否则可能导致不可知问题")

    def prepare(self, config_file=None, user=None, password=None, **kwargs):
        """登录的统一接口
        :param config_file 登录数据文件,若无则选择参数登录模式
        :param user: 各家券商的账号
        :param password: 密码, 券商为加密后的密码
        :param cookies: [雪球登录需要]雪球登录需要设置对应的 cookies
        :param portfolio_code: [雪球登录需要]组合代码
        :param portfolio_market: [雪球登录需要]交易市场,
            可选['cn', 'us', 'hk'] 默认 'cn'
        """
        if config_file is not None:
            self.read_config(config_file)
        else:
            self._prepare_account(user, password, **kwargs)
        self.autologin()

    def _prepare_account(self, user, password, **kwargs):
        """映射用户名密码到对应的字段"""
        raise Exception("支持参数登录需要实现此方法")

    def autologin(self, limit=10):
        """实现自动登录
        :param limit: 登录次数限制
        """
        for _ in range(limit):
            if self.login():
                break
        else:
            raise exceptions.NotLoginError(
                "登录失败次数过多, 请检查密码是否正确 / 券商服务器是否处于维护中 / 网络连接是否正常"
            )
        self.keepalive()

    def login(self):
        pass

    def keepalive(self):
        """启动保持在线的进程 """
        if self.heart_thread.is_alive():
            self.heart_active = True
        else:
            self.heart_thread.start()

    def send_heartbeat(self):
        """每隔10秒查询指定接口保持 token 的有效性"""
        while True:
            if self.heart_active:
                self.check_login()
            else:
                time.sleep(1)

    def check_login(self, sleepy=30):
        logger.setLevel(logging.ERROR)
        try:
            response = self.heartbeat()
            self.check_account_live(response)
        except requests.exceptions.ConnectionError:
            pass
        except requests.exceptions.RequestException as e:
            logger.setLevel(self.log_level)
            logger.error("心跳线程发现账户出现错误: %s %s, 尝试重新登陆", e.__class__, e)
            self.autologin()
        finally:
            logger.setLevel(self.log_level)
        time.sleep(sleepy)

    def heartbeat(self):
        return self.balance

    def check_account_live(self, response):
        pass

    def exit(self):
        """结束保持 token 在线的进程"""
        self.heart_active = False

    def __read_config(self):
        """读取 config"""
        self.config = file2dict(self.config_path)
        self.global_config = file2dict(self.global_config_path)
        self.config.update(self.global_config)

    @property
    def balance(self):
        return self.get_balance()

    def get_balance(self):
        """获取账户资金状况"""
        return self.do(self.config["balance"])

    @property
    def position(self):
        return self.get_position()

    def get_position(self):
        """获取持仓"""
        return self.do(self.config["position"])

    @property
    def entrust(self):
        return self.get_entrust()

    def get_entrust(self):
        """获取当日委托列表"""
        return self.do(self.config["entrust"])

    @property
    def current_deal(self):
        return self.get_current_deal()

    def get_current_deal(self):
        """获取当日委托列表"""
        # return self.do(self.config['current_deal'])
        logger.warning("目前仅在 佣金宝/银河子类 中实现, 其余券商需要补充")

    @property
    def exchangebill(self):
        """
        默认提供最近30天的交割单, 通常只能返回查询日期内最新的 90 天数据。
        :return:
        """
        # TODO 目前仅在 华泰子类 中实现
        start_date, end_date = get_30_date()
        return self.get_exchangebill(start_date, end_date)

    def get_exchangebill(self, start_date, end_date):
        """
        查询指定日期内的交割单
        :param start_date: 20160211
        :param end_date: 20160211
        :return:
        """
        logger.warning("目前仅在 华泰子类 中实现, 其余券商需要补充")

    def get_ipo_limit(self, stock_code):
        """
        查询新股申购额度申购上限
        :param stock_code: 申购代码 ID
        :return:
        """
        logger.warning("目前仅在 佣金宝子类 中实现, 其余券商需要补充")

    def do(self, params):
        """发起对 api 的请求并过滤返回结果
        :param params: 交易所需的动态参数"""
        request_params = self.create_basic_params()
        request_params.update(params)
        response_data = self.request(request_params)
        try:
            format_json_data = self.format_response_data(response_data)
        # pylint: disable=broad-except
        except Exception:
            # Caused by server force logged out
            return None
        return_data = self.fix_error_data(format_json_data)
        try:
            self.check_login_status(return_data)
        except exceptions.NotLoginError:
            self.autologin()
        return return_data

    def create_basic_params(self) -> dict:
        """生成基本的参数"""
        return {}

    def request(self, params) -> dict:
        """请求并获取 JSON 数据
        :param params: Get 参数"""
        return {}

    def format_response_data(self, data):
        """格式化返回的 json 数据
        :param data: 请求返回的数据 """
        return data

    def fix_error_data(self, data):
        """若是返回错误移除外层的列表
        :param data: 需要判断是否包含错误信息的数据"""
        return data

    def format_response_data_type(self, response_data):
        """格式化返回的值为正确的类型
        :param response_data: 返回的数据
        """
        if isinstance(response_data, list) and not isinstance(
            response_data, str
        ):
            return response_data

        int_match_str = "|".join(self.config["response_format"]["int"])
        float_match_str = "|".join(self.config["response_format"]["float"])
        for item in response_data:
            for key in item:
                try:
                    if re.search(int_match_str, key) is not None:
                        item[key] = str2num(item[key], "int")
                    elif re.search(float_match_str, key) is not None:
                        item[key] = str2num(item[key], "float")
                except ValueError:
                    continue
        return response_data

    def check_login_status(self, return_data):
        pass


================================================
FILE: easytrader/wk_clienttrader.py
================================================
# -*- coding: utf-8 -*-
import pywinauto

from easytrader.ht_clienttrader import HTClientTrader


class WKClientTrader(HTClientTrader):
    @property
    def broker_type(self):
        return "wk"

    def login(self, user, password, exe_path, comm_password=None, **kwargs):
        """
                :param user: 用户名
                :param password: 密码
                :param exe_path: 客户端路径, 类似
                :param comm_password:
                :param kwargs:
                :return:
                """
        self._editor_need_type_keys = False
        if comm_password is None:
            raise ValueError("五矿必须设置通讯密码")

        try:
            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=1
            )
        # pylint: disable=broad-except
        except Exception:
            self._app = pywinauto.Application().start(exe_path)

            # wait login window ready
            while True:
                try:
                    self._app.top_window().Edit1.wait("ready")
                    break
                except RuntimeError:
                    pass

            self._app.top_window().Edit1.set_focus()
            self._app.top_window().Edit1.set_edit_text(user)
            self._app.top_window().Edit2.set_edit_text(password)

            self._app.top_window().Edit3.set_edit_text(comm_password)

            self._app.top_window().Button1.click()

            # detect login is success or not
            self._app.top_window().wait_not("exists", 100)

            self._app = pywinauto.Application().connect(
                path=self._run_exe_path(exe_path), timeout=10
            )
        self._close_prompt_windows()
        self._main = self._app.window(title="网上股票交易系统5.0")

================================================
FILE: easytrader/xq_follower.py
================================================
# -*- coding: utf-8 -*-
from __future__ import division, print_function, unicode_literals

import json
import re
from datetime import datetime
from numbers import Number
from threading import Thread

from easytrader.follower import BaseFollower
from easytrader.log import logger
from easytrader.utils.misc import parse_cookies_str


class XueQiuFollower(BaseFollower):
    LOGIN_PAGE = "https://www.xueqiu.com"
    LOGIN_API = "https://xueqiu.com/snowman/login"
    TRANSACTION_API = "https://xueqiu.com/cubes/rebalancing/history.json"
    PORTFOLIO_URL = "https://xueqiu.com/p/"
    WEB_REFERER = "https://www.xueqiu.com"

    def __init__(self):
        super().__init__()
        self._adjust_sell = None
        self._users = None

    def login(self, user=None, password=None, **kwargs):
        """
        雪球登陆, 需要设置 cookies
        :param cookies: 雪球登陆需要设置 cookies, 具体见
            https://smalltool.github.io/2016/08/02/cookie/
        :return:
        """
        cookies = kwargs.get("cookies")
        if cookies is None:
            raise TypeError(
                "雪球登陆需要设置 cookies, 具体见" "https://smalltool.github.io/2016/08/02/cookie/"
            )
        headers = self._generate_headers()
        self.s.headers.update(headers)

        self.s.get(self.LOGIN_PAGE)

        cookie_dict = parse_cookies_str(cookies)
        self.s.cookies.update(cookie_dict)

        logger.info("登录成功")

    def follow(  # type: ignore
        self,
        users,
        strategies,
        total_assets=10000,
        initial_assets=None,
        adjust_sell=False,
        track_interval=10,
        trade_cmd_expire_seconds=120,
        cmd_cache=True,
        slippage: float = 0.0,
    ):
        """跟踪 joinquant 对应的模拟交易,支持多用户多策略
        :param users: 支持 easytrader 的用户对象,支持使用 [] 指定多个用户
        :param strategies: 雪球组合名, 类似 ZH123450
        :param total_assets: 雪球组合对应的总资产, 格式 [组合1对应资金, 组合2对应资金]
            若 strategies=['ZH000001', 'ZH000002'],
                设置 total_assets=[10000, 10000], 则表明每个组合对应的资产为 1w 元
            假设组合 ZH000001 加仓 价格为 p 股票 A 10%,
                则对应的交易指令为 买入 股票 A 价格 P 股数 1w * 10% / p 并按 100 取整
        :param adjust_sell: 是否根据用户的实际持仓数调整卖出股票数量,
            当卖出股票数大于实际持仓数时,调整为实际持仓数。目前仅在银河客户端测试通过。
            当 users 为多个时,根据第一个 user 的持仓数决定
        :type adjust_sell: bool
        :param initial_assets: 雪球组合对应的初始资产,
            格式 [ 组合1对应资金, 组合2对应资金 ]
            总资产由 初始资产 × 组合净值 算得, total_assets 会覆盖此参数
        :param track_interval: 轮训模拟交易时间,单位为秒
        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒
        :param cmd_cache: 是否读取存储历史执行过的指令,防止重启时重复执行已经交易过的指令
        :param slippage: 滑点,0.0 表示无滑点, 0.05 表示滑点为 5%
        """
        super().follow(
            users=users,
            strategies=strategies,
            track_interval=track_interval,
            trade_cmd_expire_seconds=trade_cmd_expire_seconds,
            cmd_cache=cmd_cache,
            slippage=slippage,
        )

        self._adjust_sell = adjust_sell

        self._users = self.warp_list(users)

        strategies = self.warp_list(strategies)
        total_assets = self.warp_list(total_assets)
        initial_assets = self.warp_list(initial_assets)

        if cmd_cache:
            self.load_expired_cmd_cache()

        self.start_trader_thread(self._users, trade_cmd_expire_seconds)

        for strategy_url, strategy_total_assets, strategy_initial_assets in zip(
            strategies, total_assets, initial_assets
        ):
            assets = self.calculate_assets(
                strategy_url, strategy_total_assets, strategy_initial_assets
            )
            try:
                strategy_id = self.extract_strategy_id(strategy_url)
                strategy_name = self.extract_strategy_name(strategy_url)
            except:
                logger.error("抽取交易id和策略名失败, 无效模拟交易url: %s", strategy_url)
                raise
            strategy_worker = Thread(
                target=self.track_strategy_worker,
                args=[strategy_id, strategy_name],
                kwargs={"interval": track_interval, "assets": assets},
            )
            strategy_worker.start()
            logger.info("开始跟踪策略: %s", strategy_name)

    def calculate_assets(self, strategy_url, total_assets=None, initial_assets=None):
        # 都设置时优先选择 total_assets
        if total_assets is None and initial_assets is not None:
            net_value = self._get_portfolio_net_value(strategy_url)
            total_assets = initial_assets * net_value
        if not isinstance(total_assets, Number):
            raise TypeError("input assets type must be number(int, float)")
        if total_assets < 1e3:
            raise ValueError("雪球总资产不能小于1000元,当前预设值 {}".format(total_assets))
        return total_assets

    @staticmethod
    def extract_strategy_id(strategy_url):
        return strategy_url

    def extract_strategy_name(self, strategy_url):
        base_url = "https://xueqiu.com/cubes/nav_daily/all.json?cube_symbol={}"
        url = base_url.format(strategy_url)
        rep = self.s.get(url)
        info_index = 0
        return rep.json()[info_index]["name"]

    def extract_transactions(self, history):
        if history["count"] <= 0:
            return []
        rebalancing_index = 0
        raw_transactions = history["list"][rebalancing_index]["rebalancing_histories"]
        transactions = []
        for transaction in raw_transactions:
            if transaction["price"] is None:
                logger.info("该笔交易无法获取价格,疑似未成交,跳过。交易详情: %s", transaction)
                continue
            transactions.append(transaction)

        return transactions

    def create_query_transaction_params(self, strategy):
        params = {"cube_symbol": strategy, "page": 1, "count": 1}
        return params

    # noinspection PyMethodOverriding
    def none_to_zero(self, data):
        if data is None:
            return 0
        return data

    # noinspection PyMethodOverriding
    def project_transactions(self, transactions, assets):
        for transaction in transactions:
            weight_diff = self.none_to_zero(transaction["weight"]) - self.none_to_zero(
                transaction["prev_weight"]
            )

            initial_amount = abs(weight_diff) / 100 * assets / transaction["price"]

            transaction["datetime"] = datetime.fromtimestamp(
                transaction["created_at"] // 1000
            )

            transaction["stock_code"] = transaction["stock_symbol"].lower()

            transaction["action"] = "buy" if weight_diff > 0 else "sell"

            transaction["amount"] = int(round(initial_amount, -2))
            if transaction["action"] == "sell" and self._adjust_sell:
                transaction["amount"] = self._adjust_sell_amount(
                    transaction["stock_code"], transaction["amount"]
                )

    def _adjust_sell_amount(self, stock_code, amount):
        """
        根据实际持仓值计算雪球卖出股数
          因为雪球的交易指令是基于持仓百分比,在取近似值的情况下可能出现不精确的问题。
        导致如下情况的产生,计算出的指令为买入 1049 股,取近似值买入 1000 股。
        而卖出的指令计算出为卖出 1051 股,取近似值卖出 1100 股,超过 1000 股的买入量,
        导致卖出失败
        :param stock_code: 证券代码
        :type stock_code: str
        :param amount: 卖出股份数
        :type amount: int
        :return: 考虑实际持仓之后的卖出股份数
        :rtype: int
        """
        stock_code = stock_code[-6:]
        user = self._users[0]
        position = user.position
        try:
            stock = next(s for s in position if s["证券代码"] == stock_code)
        except StopIteration:
            logger.info("根据持仓调整 %s 卖出额,发现未持有股票 %s, 不做任何调整", stock_code, stock_code)
            return amount

        available_amount = stock["可用余额"]
        if available_amount >= amount:
            return amount

        adjust_amount = available_amount // 100 * 100
        logger.info(
            "股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s",
            stock_code,
            available_amount,
            amount,
            adjust_amount,
        )
        return adjust_amount

    def _get_portfolio_info(self, portfolio_code):
        """
        获取组合信息
        """
        url = self.PORTFOLIO_URL + portfolio_code
        portfolio_page = self.s.get(url)
        match_info = re.search(r"(?<=SNB.cubeInfo = ).*(?=;\n)", portfolio_page.text)
        if match_info is None:
            raise Exception("cant get portfolio info, portfolio url : {}".format(url))
        try:
            portfolio_info = json.loads(match_info.group())
        except Exception as e:
            raise Exception("get portfolio info error: {}".format(e))
        return portfolio_info

    def _get_portfolio_net_value(self, portfolio_code):
        """
        获取组合信息
        """
        portfolio_info = self._get_portfolio_info(portfolio_code)
        return portfolio_info["net_value"]


================================================
FILE: easytrader/xqtrader.py
================================================
# -*- coding: utf-8 -*-
import json
import numbers
import os
import re
import time
import math

import requests

from easytrader import exceptions, webtrader
from easytrader.log import logger
from easytrader.utils.misc import parse_cookies_str


class XueQiuTrader(webtrader.WebTrader):
    config_path = os.path.dirname(__file__) + "/config/xq.json"

    _HEADERS = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/64.0.3282.167 Safari/537.36",
        "Host": "xueqiu.com",
        "Pragma": "no-cache",
        "Connection": "keep-alive",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Cache-Control": "no-cache",
        "Referer": "https://xueqiu.com/P/ZH004612",
        "X-Requested-With": "XMLHttpRequest",
    }

    def __init__(self, **kwargs):
        super(XueQiuTrader, self).__init__()
        self.position_list = []

        # 资金换算倍数
        self.multiple = (
            kwargs["initial_assets"] if "initial_assets" in kwargs else 1000000
        )
        if not isinstance(self.multiple, numbers.Number):
            raise TypeError("initial assets must be number(int, float)")
        if self.multiple < 1e3:
            raise ValueError("雪球初始资产不能小于1000元,当前预设值 {}".format(self.multiple))

        self.s = requests.Session()
        self.s.verify = False
        self.s.headers.update(self._HEADERS)
        self.account_config = None

    def autologin(self, **kwargs):
        """
        使用 cookies 之后不需要自动登陆
        :return:
        """
        self._set_cookies(self.account_config["cookies"])

    def _set_cookies(self, cookies):
        """设置雪球 cookies,代码来自于
        https://github.com/shidenggui/easytrader/issues/269
        :param cookies: 雪球 cookies
        :type cookies: str
        """
        cookie_dict = parse_cookies_str(cookies)
        self.s.cookies.update(cookie_dict)

    def _prepare_account(self, user="", password="", **kwargs):
        """
        转换参数到登录所需的字典格式
        :param cookies: 雪球登陆需要设置 cookies, 具体见
            https://smalltool.github.io/2016/08/02/cookie/
        :param portfolio_code: 组合代码
        :param portfolio_market: 交易市场, 可选['cn', 'us', 'hk'] 默认 'cn'
        :return:
        """
        if "portfolio_code" not in kwargs:
            raise TypeError("雪球登录需要设置 portfolio_code(组合代码) 参数")
        if "portfolio_market" not in kwargs:
            kwargs["portfolio_market"] = "cn"
        if "cookies" not in kwargs:
            raise TypeError(
                "雪球登陆需要设置 cookies, 具体见"
                "https://smalltool.github.io/2016/08/02/cookie/"
            )
        self.account_config = {
            "cookies": kwargs["cookies"],
            "portfolio_code": kwargs["portfolio_code"],
            "portfolio_market": kwargs["portfolio_market"],
        }

    def _virtual_to_balance(self, virtual):
        """
        虚拟净值转化为资金
        :param virtual: 雪球组合净值
        :return: 换算的资金
        """
        return virtual * self.multiple

    def _get_html(self, url):
        return self.s.get(url).text

    def _search_stock_info(self, code):
        """
        通过雪球的接口获取股票详细信息
        :param code: 股票代码 000001
        :return: 查询到的股票 {'stock_id': 1000279, 'code': 'SH600325',
            'name': '华发股份', 'ind_color': '#d9633b', 'chg': -1.09,
            'ind_id': 100014, 'percent': -9.31, 'current': 10.62,
            'ind_name': '房地产'}
         ** flag : 未上市(0)、正常(1)、停牌(2)、涨跌停(3)、退市(4)
        """
        data = {
            "code": str(code),
            "size": "300",
            "key": "47bce5c74f",
            "market": self.account_config["portfolio_market"],
        }
        r = self.s.get(self.config["search_stock_url"], params=data)
        stocks = json.loads(r.text)
        stocks = stocks["stocks"]
        stock = None
        if len(stocks) > 0:
            stock = stocks[0]
        return stock

    def _get_portfolio_info(self, portfolio_code):
        """
        获取组合信息
        :return: 字典
        """
        data_rb = {'cube_symbol': portfolio_code}
        rb = self.s.get(self.config["portfolio_url_new"], params=data_rb)
        data_qt = {'code': portfolio_code}
        qt = self.s.get(self.config["portfolio_quote"], params=data_qt)
        try:
            rebalance_info = json.loads(rb.text)
            quote_info = json.loads(qt.text)
            net_value = quote_info[portfolio_code]['net_value']
            portfolio_info = rebalance_info
            portfolio_info['net_value'] = net_value
        except Exception as e:
            raise Exception("get portfolio info error: {}".format(e))
        return portfolio_info

    def get_balance(self):
        """
        获取账户资金状况
        :return:
        """
        portfolio_code = self.account_config.get("portfolio_code", "ch")
        portfolio_info = self._get_portfolio_info(portfolio_code)
        asset_balance = self._virtual_to_balance(
            float(portfolio_info["net_value"])
        )  # 总资产
        position = portfolio_info["last_rb"]  # 仓位结构
        cash = asset_balance * float(position["cash"]) / 100
        market = asset_balance - cash
        return [
            {
                "asset_balance": asset_balance,
                "current_balance": cash,
                "enable_balance": cash,
                "market_value": market,
                "money_type": u"人民币",
                "pre_interest": 0.25,
            }
        ]

    @property
    def cash_weight(self):
        portfolio_code = self.account_config.get("portfolio_code", "ch")
        portfolio_info = self._get_portfolio_info(portfolio_code)
        position = portfolio_info["last_rb"]
        return float(position["cash"])

    def _get_position(self):
        """
        获取雪球持仓
        :return:
        """
        portfolio_code = self.account_config["portfolio_code"]
        portfolio_info = self._get_portfolio_info(portfolio_code)
        position = portfolio_info["last_rb"]  # 仓位结构
        stocks = position["holdings"]  # 持仓股票
        return stocks

    @staticmethod
    def _time_strftime(time_stamp):
        try:
            local_time = time.localtime(time_stamp / 1000)
            return time.strftime("%Y-%m-%d %H:%M:%S", local_time)
        # pylint: disable=broad-except
        except Exception:
            return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())

    def get_position(self):
        """
        获取持仓
        :return:
        """
        xq_positions = self._get_position()
        balance = self.get_balance()[0]
        position_list = []
        for pos in xq_positions:
            volume = pos["weight"] * balance["asset_balance"] / 100
            position_list.append(
                {
                    "cost_price": volume / 100,
                    "current_amount": 100,
                    "enable_amount": 100,
                    "income_balance": 0,
                    "keep_cost_price": volume / 100,
                    "last_price": volume / 100,
                    "market_value": volume,
                    "position_str": "random",
                    "stock_code": pos["stock_symbol"],
                    "stock_name": pos["stock_name"],
                }
            )
        return position_list

    def _get_xq_history(self):
        """
        获取雪球调仓历史
        :param instance:
        :param owner:
        :return:
        """
        data = {
            "cube_symbol": str(self.account_config["portfolio_code"]),
            "count": 20,
            "page": 1,
        }
        resp = self.s.get(self.config["history_url"], params=data)
        res = json.loads(resp.text)
        return res["list"]

    @property
    def history(self):
        return self._get_xq_history()

    def get_entrust(self):
        """
        获取委托单(目前返回20次调仓的结果)
        操作数量都按1手模拟换算的
        :return:
        """
        xq_entrust_list = self._get_xq_history()
        entrust_list = []
        replace_none = lambda s: s or 0
        for xq_entrusts in xq_entrust_list:
            status = xq_entrusts["status"]  # 调仓状态
            if status == "pending":
                status = "已报"
            elif status in ["canceled", "failed"]:
                status = "废单"
            else:
                status = "已成"
            for entrust in xq_entrusts["rebalancing_histories"]:
                price = entrust["price"]
                entrust_list.append(
                    {
                        "entrust_no": entrust["id"],
                        "entrust_bs": u"买入"
                        if entrust["target_weight"]
                        > replace_none(entrust["prev_weight"])
                        else u"卖出",
                        "report_time": self._time_strftime(
                            entrust["updated_at"]
                        ),
                        "entrust_status": status,
                        "stock_code": entrust["stock_symbol"],
                        "stock_name": entrust["stock_name"],
                        "business_amount": 100,
                        "business_price": price,
                        "entrust_amount": 100,
                        "entrust_price": price,
                    }
                )
        return entrust_list

    def cancel_entrust(self, entrust_no):
        """
        对未成交的调仓进行伪撤单
        :param entrust_no:
        :return:
        """
        xq_entrust_list = self._get_xq_history()
        is_have = False
        for xq_entrusts in xq_entrust_list:
            status = xq_entrusts["status"]  # 调仓状态
            for entrust in xq_entrusts["rebalancing_histories"]:
                if entrust["id"] == entrust_no and status == "pending":
                    is_have = True
                    buy_or_sell = (
                        "buy"
                        if entrust["target_weight"] < entrust["weight"]
                        else "sell"
                    )
                    if (
                        entrust["target_weight"] == 0
                        and entrust["weight"] == 0
                    ):
                        raise exceptions.TradeError(u"移除的股票操作无法撤销,建议重新买入")
                    balance = self.get_balance()[0]
                    volume = (
                        abs(entrust["target_weight"] - entrust["weight"])
                        * balance["asset_balance"]
                        / 100
                    )
                    r = self._trade(
                        security=entrust["stock_symbol"],
                        volume=volume,
                        entrust_bs=buy_or_sell,
                    )
                    if len(r) > 0 and "error_info" in r[0]:
                        raise exceptions.TradeError(
                            u"撤销失败!%s" % ("error_info" in r[0])
                        )
        if not is_have:
            raise exceptions.TradeError(u"撤销对象已失效")
        return True

    def adjust_weight(self, stock_code, weight, fetch_position=True):
        """
        雪球组合调仓, weight 为调整后的仓位比例
        :param stock_code: str 股票代码
        :param weight: float 调整之后的持仓百分比, 0 - 100 之间的浮点数
        """

        stock = self._search_stock_info(stock_code)
        if stock is None:
            raise exceptions.TradeError(u"没有查询要操作的股票信息")
        if stock["flag"] != 1:
            raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。")

        # 仓位比例向下取两位数
        weight = round(weight, 2)
        # 获取原有仓位信息
        if fetch_position:
            self.position_list = self._get_position()

        # 调整后的持仓
        for position in self.position_list:
            if position["stock_id"] == stock["stock_id"]:
                position["proactive"] = True
                position["weight"] = weight

        if weight != 0 and stock["stock_id"] not in [
            k["stock_id"] for k in self.position_list
        ]:
            self.position_list.append(
                {
                    "code": stock["code"],
                    "name": stock["name"],
                    "flag": stock["flag"],
                    "current": stock["current"],
                    "chg": stock["chg"],
                    "percent": str(stock["percent"]),
                    "stock_id": stock["stock_id"],
                    "ind_id": stock["ind_id"],
                    "ind_name": stock["ind_name"],
                    "ind_color": stock["ind_color"],
                    "textname": stock["name"],
                    "segment_name": stock["ind_name"],
                    "weight": weight,
                    "url": "/S/" + stock["code"],
                    "proactive": True,
                    "price": str(stock["current"]),
                }
            )

        remain_weight = 100 - sum(i.get("weight") for i in self.position_list)
        cash = round(remain_weight, 2)
        logger.info("调仓比例:%f, 剩余持仓 :%f", weight, remain_weight)
        data = {
            "cash": cash,
            "holdings": str(json.dumps(self.position_list)),
            "cube_symbol": str(self.account_config["portfolio_code"]),
            "segment": "true",
            "comment": "",
        }

        try:
            resp = self.s.post(self.config["rebalance_url"], data=data)
        # pylint: disable=broad-except
        except Exception as e:
            logger.warning("调仓失败: %s ", e)
            return None
        logger.info("调仓 %s: 持仓比例%d", stock["name"], weight)
        resp_json = json.loads(resp.text)
        if "error_description" in resp_json and resp.status_code != 200:
            logger.error("调仓错误: %s", resp_json["error_description"])
            return [
                {
                    "error_no": resp_json["error_code"],
                    "error_info": resp_json["error_description"],
                }
            ]
        logger.info("调仓成功 %s: 持仓比例%d", stock["name"], weight)
        return None

    def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"):
        """
        调仓
        :param security:
        :param price:
        :param amount:
        :param volume:
        :param entrust_bs:
        :return:
        """
        stock = self._search_stock_info(security)
        balance = self.get_balance()[0]
        if stock is None:
            raise exceptions.TradeError(u"没有查询要操作的股票信息")
        if not volume:
            volume = int(float(price) * amount)  # 可能要取整数
        if balance["current_balance"] < volume and entrust_bs == "buy":
            raise exceptions.TradeError(u"没有足够的现金进行操作")
        if stock["flag"] != 1:
            raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。")
        if volume == 0:
            raise exceptions.TradeError(u"操作金额不能为零")

        # 计算调仓调仓份额
        weight = volume / balance["asset_balance"] * 100
        weight = round(weight, 2)

        # 获取原有仓位信息
        position_list = self._get_position()

        # 调整后的持仓
        is_have = False
        for position in position_list:
            if position["stock_id"] == stock["stock_id"]:
                is_have = True
                position["proactive"] = True
                old_weight = position["weight"]
                if entrust_bs == "buy":
                    position["weight"] = weight + old_weight
                else:
                    if weight > old_weight:
                        raise exceptions.TradeError(u"操作数量大于实际可卖出数量")
                    else:
                        position["weight"] = old_weight - weight
                position["weight"] = round(position["weight"], 2)
        if not is_have:
            if entrust_bs == "buy":
                position_list.append(
                    {
                        "code": stock["code"],
                        "name": stock["name"],
                        "enName": stock["enName"],
                        "hasexist": stock["hasexist"],
                        "flag": stock["flag"],
                        "type": stock["type"],
                        "current": stock["current"],
                        "chg": stock["chg"],
                        "percent": str(stock["percent"]),
                        "stock_id": stock["stock_id"],
                        "ind_id": stock["ind_id"],
                        "ind_name": stock["ind_name"],
                        "ind_color": stock["ind_color"],
                        "textname": stock["name"],
                        "segment_name": stock["ind_name"],
                        "weight": round(weight, 2),
                        "url": "/S/" + stock["code"],
                        "proactive": True,
                        "price": str(stock["current"]),
                    }
                )
            else:
                raise exceptions.TradeError(u"没有持有要卖出的股票")

        if entrust_bs == "buy":
            cash = (
                (balance["current_balance"] - volume)
                / balance["asset_balance"]
                * 100
            )
        else:
            cash = (
                (balance["current_balance"] + volume)
                / balance["asset_balance"]
                * 100
            )
        cash = round(cash, 2)
        logger.info("weight:%f, cash:%f", weight, cash)

        data = {
            "cash": cash,
            "holdings": str(json.dumps(position_list)),
            "cube_symbol": str(self.account_config["portfolio_code"]),
            "segment": 1,
            "comment": "",
        }

        try:
            resp = self.s.post(self.config["rebalance_url"], data=data)
        # pylint: disable=broad-except
        except Exception as e:
            logger.warning("调仓失败: %s ", e)
            return None
        else:
            logger.info(
                "调仓 %s%s: %d", entrust_bs, stock["name"], resp.status_code
            )
            resp_json = json.loads(resp.text)
            if "error_description" in resp_json and resp.status_code != 200:
                logger.error("调仓错误: %s", resp_json["error_description"])
                return [
                    {
                        "error_no": resp_json["error_code"],
                        "error_info": resp_json["error_description"],
                    }
                ]
            return [
                {
                    "entrust_no": resp_json["id"],
                    "init_date": self._time_strftime(resp_json["created_at"]),
                    "batch_no": "委托批号",
                    "report_no": "申报号",
                    "seat_no": "席位编号",
                    "entrust_time": self._time_strftime(
                        resp_json["updated_at"]
                    ),
                    "entrust_price": price,
                    "entrust_amount": amount,
                    "stock_code": security,
                    "entrust_bs": "买入",
                    "entrust_type": "雪球虚拟委托",
                    "entrust_status": "-",
                }
            ]

    def buy(self, security, price=0, amount=0, volume=0, entrust_prop=0):
        """买入卖出股票
        :param security: 股票代码
        :param price: 买入价格
        :param amount: 买入股数
        :param volume: 买入总金额 由 volume / price 取整, 若指定 price 则此参数无效
        :param entrust_prop:
        """
        return self._trade(security, price, amount, volume, "buy")

    def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0):
        """卖出股票
        :param security: 股票代码
        :param price: 卖出价格
        :param amount: 卖出股数
        :param volume: 卖出总金额 由 volume / price 取整, 若指定 price 则此参数无效
        :param entrust_prop:
        """
        return self._trade(security, price, amount, volume, "sell")


    def adjust_weights(self, weights, ignore_minor=0.0, fetch_position=True):
        """
        雪球组合调仓, weights 为调整后的仓位比例
        :param weights: dict[str, float] 股票代码 -> 调整之后的持仓百分比
        """

        # 获取原有仓位信息
        if fetch_position:
            self.position_list = self._get_position()

        position_dict = {position["stock_id"]: position for position in self.position_list}
        new_position_list = []

        for stock_code, weight in weights.items():
            stock = self._search_stock_info(stock_code)
            if stock is None:
                raise exceptions.TradeError(u"没有查询要操作的股票信息")
            if stock["flag"] != 1:
               raise exceptions.TradeError(f"未上市、停牌、涨跌停、退市的股票无法操作: {stock['name']}")

            if stock["stock_id"] in position_dict:
                # 调仓
                position = position_dict[stock["stock_id"]]
                current_weight = position["weight"]
                if weight > 0 and abs(weight - current_weight) > ignore_minor:
                    position["proactive"] = True
                    position["weight"] = weight
                    logger.info("调仓 %s %.2f -> %.2f", position['stock_name'], current_weight, weight)
                    new_position_list.append(position)
                elif weight > 0:
                    position["proactive"] = False
                    new_position_list.append(position)
                elif weight == 0.0:
                    logger.info("平仓 %s %.2f -> %.2f", position['stock_name'], current_weight, weight)
            else:
                # 开仓
                new_position_list.append(
                    {
                        "code": stock["code"],
                        "name": stock["name"],
                        "flag": stock["flag"],
                        "current": stock["current"],
                        "chg": stock["chg"],
                        "percent": str(stock["percent"]),
                        "stock_id": stock["stock_id"],
                        "ind_id": stock["ind_id"],
                        "ind_name": stock["ind_name"],
                        "ind_color": stock["ind_color"],
                        "textname": stock["name"],
                        "segment_name": stock["ind_nam
Download .txt
gitextract_z2l9h_jr/

├── .bumpversion.cfg
├── .coveragerc
├── .github/
│   ├── ISSUE_TEMPLATE.md
│   └── copilot-instructions.md
├── .gitignore
├── .pylintrc
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── Makefile
├── Pipfile
├── README.md
├── docs/
│   ├── follow.md
│   ├── index.md
│   ├── install.md
│   ├── miniqmt.md
│   ├── remote.md
│   ├── usage.md
│   └── xueqiu.md
├── easytrader/
│   ├── __init__.py
│   ├── api.py
│   ├── clienttrader.py
│   ├── config/
│   │   ├── __init__.py
│   │   ├── client.py
│   │   ├── global.json
│   │   └── xq.json
│   ├── exceptions.py
│   ├── follower.py
│   ├── gf_clienttrader.py
│   ├── gj_clienttrader.py
│   ├── grid_strategies.py
│   ├── ht_clienttrader.py
│   ├── htzq_clienttrader.py
│   ├── joinquant_follower.py
│   ├── log.py
│   ├── miniqmt/
│   │   ├── __init__.py
│   │   └── miniqmt_trader.py
│   ├── pop_dialog_handler.py
│   ├── refresh_strategies.py
│   ├── remoteclient.py
│   ├── ricequant_follower.py
│   ├── server.py
│   ├── universal_clienttrader.py
│   ├── utils/
│   │   ├── __init__.py
│   │   ├── captcha.py
│   │   ├── misc.py
│   │   ├── perf.py
│   │   ├── stock.py
│   │   └── win_gui.py
│   ├── webtrader.py
│   ├── wk_clienttrader.py
│   ├── xq_follower.py
│   ├── xqtrader.py
│   └── yh_clienttrader.py
├── gj_client.json
├── mkdocs.yml
├── mypy.ini
├── readthedocs-requirements.txt
├── requirements.txt
├── setup.py
├── test-requirements.txt
├── tests/
│   ├── __init__.py
│   ├── test_easytrader.py
│   ├── test_xq_follower.py
│   └── test_xqtrader.py
├── xq.json
└── yh_client.json
Download .txt
SYMBOL INDEX (376 symbols across 30 files)

FILE: easytrader/api.py
  function use (line 17) | def use(broker, debug=False, **kwargs):
  function follower (line 89) | def follower(platform, **kwargs):

FILE: easytrader/clienttrader.py
  class IClientTrader (line 28) | class IClientTrader(abc.ABC):
    method app (line 31) | def app(self):
    method main (line 37) | def main(self):
    method config (line 43) | def config(self):
    method wait (line 48) | def wait(self, seconds: float):
    method refresh (line 53) | def refresh(self):
    method is_exist_pop_dialog (line 58) | def is_exist_pop_dialog(self):
  class ClientTrader (line 62) | class ClientTrader(IClientTrader):
    method enable_type_keys_for_editor (line 69) | def enable_type_keys_for_editor(self):
    method grid_strategy_instance (line 76) | def grid_strategy_instance(self):
    method __init__ (line 86) | def __init__(self):
    method app (line 93) | def app(self):
    method main (line 97) | def main(self):
    method config (line 101) | def config(self):
    method connect (line 104) | def connect(self, exe_path=None, **kwargs):
    method broker_type (line 122) | def broker_type(self):
    method balance (line 126) | def balance(self):
    method _init_toolbar (line 131) | def _init_toolbar(self):
    method _get_balance_from_statics (line 134) | def _get_balance_from_statics(self):
    method position (line 145) | def position(self):
    method today_entrusts (line 151) | def today_entrusts(self):
    method today_trades (line 157) | def today_trades(self):
    method cancel_entrusts (line 163) | def cancel_entrusts(self):
    method cancel_entrust (line 170) | def cancel_entrust(self, entrust_no):
    method cancel_all_entrusts (line 178) | def cancel_all_entrusts(self):
    method repo (line 202) | def repo(self, security, price, amount, **kwargs):
    method reverse_repo (line 208) | def reverse_repo(self, security, price, amount, **kwargs):
    method buy (line 214) | def buy(self, security, price, amount, **kwargs):
    method sell (line 220) | def sell(self, security, price, amount, **kwargs):
    method market_buy (line 226) | def market_buy(self, security, amount, ttype=None, limit_price=None, *...
    method market_sell (line 243) | def market_sell(self, security, amount, ttype=None, limit_price=None, ...
    method market_trade (line 258) | def market_trade(self, security, amount, ttype=None, limit_price=None,...
    method _set_market_trade_type (line 288) | def _set_market_trade_type(self, ttype):
    method _set_stock_exchange_type (line 305) | def _set_stock_exchange_type(self, ttype):
    method auto_ipo (line 323) | def auto_ipo(self):
    method _click_grid_by_row (line 349) | def _click_grid_by_row(self, row):
    method is_exist_pop_dialog (line 361) | def is_exist_pop_dialog(self):
    method close_pop_dialog (line 376) | def close_pop_dialog(self):
    method _run_exe_path (line 390) | def _run_exe_path(self, exe_path):
    method wait (line 393) | def wait(self, seconds):
    method exit (line 396) | def exit(self):
    method _close_prompt_windows (line 399) | def _close_prompt_windows(self):
    method close_pormpt_window_no_wait (line 409) | def close_pormpt_window_no_wait(self):
    method trade (line 414) | def trade(self, security, price, amount):
    method _click (line 423) | def _click(self, control_id):
    method _submit_trade (line 429) | def _submit_trade(self):
    method __get_top_window_pop_dialog (line 436) | def __get_top_window_pop_dialog(self):
    method _get_pop_dialog_title (line 442) | def _get_pop_dialog_title(self):
    method _set_trade_params (line 449) | def _set_trade_params(self, security, price, amount):
    method _set_market_trade_params (line 473) | def _set_market_trade_params(self, security, amount, limit_price=None):
    method _get_grid_data (line 489) | def _get_grid_data(self, control_id):
    method _type_keys (line 492) | def _type_keys(self, control_id, text):
    method _type_edit_control_keys (line 497) | def _type_edit_control_keys(self, control_id, text):
    method type_edit_control_keys (line 507) | def type_edit_control_keys(self, editor, text):
    method _collapse_left_menus (line 514) | def _collapse_left_menus(self):
    method _switch_left_menus (line 520) | def _switch_left_menus(self, path, sleep=0.2):
    method _switch_left_menus_by_shortcut (line 526) | def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5):
    method _get_left_menus_handle (line 532) | def _get_left_menus_handle(self):
    method _cancel_entrust_by_double_click (line 549) | def _cancel_entrust_by_double_click(self, row):
    method refresh (line 560) | def refresh(self):
    method _handle_pop_dialogs (line 565) | def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDial...
  class BaseLoginClientTrader (line 580) | class BaseLoginClientTrader(ClientTrader):
    method login (line 582) | def login(self, user, password, exe_path, comm_password=None, **kwargs):
    method prepare (line 586) | def prepare(

FILE: easytrader/config/client.py
  function create (line 2) | def create(broker):
  class CommonConfig (line 22) | class CommonConfig:
  class YH (line 84) | class YH(CommonConfig):
  class HT (line 104) | class HT(CommonConfig):
  class GJ (line 131) | class GJ(CommonConfig):
  class GF (line 148) | class GF(CommonConfig):
  class WK (line 166) | class WK(HT):
  class HTZQ (line 170) | class HTZQ(CommonConfig):
  class UNIVERSAL (line 183) | class UNIVERSAL(CommonConfig):

FILE: easytrader/exceptions.py
  class TradeError (line 4) | class TradeError(IOError):
  class NotLoginError (line 8) | class NotLoginError(Exception):
    method __init__ (line 9) | def __init__(self, result=None):

FILE: easytrader/follower.py
  class BaseFollower (line 18) | class BaseFollower(metaclass=abc.ABCMeta):
    method __init__ (line 30) | def __init__(self):
    method login (line 39) | def login(self, user=None, password=None, **kwargs):
    method _generate_headers (line 60) | def _generate_headers(self):
    method check_login_success (line 75) | def check_login_success(self, rep):
    method create_login_params (line 81) | def create_login_params(self, user, password, **kwargs) -> dict:
    method follow (line 89) | def follow(
    method _calculate_price_by_slippage (line 115) | def _calculate_price_by_slippage(self, action: str, price: float) -> f...
    method load_expired_cmd_cache (line 128) | def load_expired_cmd_cache(self):
    method start_trader_thread (line 133) | def start_trader_thread(
    method warp_list (line 153) | def warp_list(value):
    method extract_strategy_id (line 159) | def extract_strategy_id(strategy_url):
    method extract_strategy_name (line 167) | def extract_strategy_name(self, strategy_url):
    method track_strategy_worker (line 175) | def track_strategy_worker(self, strategy, name, interval=10, **kwargs):
    method generate_expired_cmd_key (line 221) | def generate_expired_cmd_key(cmd):
    method is_cmd_expired (line 231) | def is_cmd_expired(self, cmd):
    method add_cmd_to_expired_cmds (line 235) | def add_cmd_to_expired_cmds(self, cmd):
    method _is_number (line 243) | def _is_number(s):
    method _execute_trade_cmd (line 250) | def _execute_trade_cmd(
    method trade_worker (line 345) | def trade_worker(
    method query_strategy_transaction (line 358) | def query_strategy_transaction(self, strategy, **kwargs):
    method extract_transactions (line 368) | def extract_transactions(self, history) -> List[str]:
    method create_query_transaction_params (line 376) | def create_query_transaction_params(self, strategy) -> dict:
    method re_find (line 385) | def re_find(pattern, string, dtype=str):
    method re_search (line 389) | def re_search(pattern, string, dtype=str):
    method project_transactions (line 392) | def project_transactions(self, transactions, **kwargs):
    method order_transactions_sell_first (line 400) | def order_transactions_sell_first(self, transactions):

FILE: easytrader/gf_clienttrader.py
  class GFClientTrader (line 14) | class GFClientTrader(clienttrader.BaseLoginClientTrader):
    method broker_type (line 16) | def broker_type(self):
    method login (line 19) | def login(self, user, password, exe_path, comm_password=None, **kwargs):
    method _handle_verify_code (line 74) | def _handle_verify_code(self):

FILE: easytrader/gj_clienttrader.py
  class GJClientTrader (line 13) | class GJClientTrader(clienttrader.BaseLoginClientTrader):
    method broker_type (line 15) | def broker_type(self):
    method login (line 18) | def login(self, user, password, exe_path, comm_password=None, **kwargs):
    method _handle_verify_code (line 72) | def _handle_verify_code(self):

FILE: easytrader/grid_strategies.py
  class IGridStrategy (line 22) | class IGridStrategy(abc.ABC):
    method get (line 24) | def get(self, control_id: int) -> List[Dict]:
    method set_trader (line 34) | def set_trader(self, trader: "clienttrader.IClientTrader"):
  class BaseStrategy (line 38) | class BaseStrategy(IGridStrategy):
    method __init__ (line 39) | def __init__(self):
    method set_trader (line 42) | def set_trader(self, trader: "clienttrader.IClientTrader"):
    method get (line 46) | def get(self, control_id: int) -> List[Dict]:
    method _get_grid (line 53) | def _get_grid(self, control_id: int):
    method _set_foreground (line 59) | def _set_foreground(self, grid=None):
  class Copy (line 71) | class Copy(BaseStrategy):
    method get (line 78) | def get(self, control_id: int) -> List[Dict]:
    method _format_grid_data (line 85) | def _format_grid_data(self, data: str) -> List[Dict]:
    method _get_clipboard_data (line 97) | def _get_clipboard_data(self) -> str:
  class WMCopy (line 153) | class WMCopy(Copy):
    method get (line 158) | def get(self, control_id: int) -> List[Dict]:
  class Xls (line 166) | class Xls(BaseStrategy):
    method __init__ (line 171) | def __init__(self, tmp_folder: Optional[str] = None):
    method get (line 178) | def get(self, control_id: int) -> List[Dict]:
    method _format_grid_data (line 206) | def _format_grid_data(self, data: str) -> List[Dict]:

FILE: easytrader/ht_clienttrader.py
  class HTClientTrader (line 10) | class HTClientTrader(clienttrader.BaseLoginClientTrader):
    method broker_type (line 14) | def broker_type(self):
    method login (line 17) | def login(self, user, password, exe_path, comm_password=None, **kwargs):
    method balance (line 61) | def balance(self):
    method _get_balance_from_statics (line 66) | def _get_balance_from_statics(self):

FILE: easytrader/htzq_clienttrader.py
  class HTZQClientTrader (line 10) | class HTZQClientTrader(clienttrader.BaseLoginClientTrader):
    method broker_type (line 14) | def broker_type(self):
    method login (line 17) | def login(self, user, password, exe_path, comm_password=None, **kwargs):

FILE: easytrader/joinquant_follower.py
  class JoinQuantFollower (line 10) | class JoinQuantFollower(BaseFollower):
    method create_login_params (line 19) | def create_login_params(self, user, password, **kwargs):
    method check_login_success (line 27) | def check_login_success(self, rep):
    method follow (line 33) | def follow(
    method extract_strategy_id (line 91) | def extract_strategy_id(self, strategy_url):
    method extract_strategy_name (line 95) | def extract_strategy_name(self, strategy_url):
    method create_query_transaction_params (line 99) | def create_query_transaction_params(self, strategy):
    method extract_transactions (line 104) | def extract_transactions(self, history):
    method stock_shuffle_to_prefix (line 109) | def stock_shuffle_to_prefix(stock):
    method project_transactions (line 121) | def project_transactions(self, transactions, **kwargs):

FILE: easytrader/miniqmt/miniqmt_trader.py
  class DefaultXtQuantTraderCallback (line 119) | class DefaultXtQuantTraderCallback(XtQuantTraderCallback):
    method on_disconnected (line 124) | def on_disconnected(self):
    method on_account_status (line 131) | def on_account_status(self, status):
    method on_stock_order (line 141) | def on_stock_order(self, order):
    method on_stock_trade (line 151) | def on_stock_trade(self, trade):
    method on_order_error (line 161) | def on_order_error(self, order_error):
    method on_cancel_error (line 171) | def on_cancel_error(self, cancel_error):
    method on_order_stock_async_response (line 181) | def on_order_stock_async_response(self, response):
    method on_smt_appointment_async_response (line 189) | def on_smt_appointment_async_response(self, response):
  class MiniqmtTrader (line 199) | class MiniqmtTrader:
    method __init__ (line 202) | def __init__(self):
    method prepare (line 206) | def prepare(self, **json_data):
    method connect (line 212) | def connect(
    method trader (line 239) | def trader(self) -> XtQuantTrader:
    method account (line 247) | def account(self) -> StockAccount:
    method balance (line 255) | def balance(self):
    method position (line 282) | def position(self):
    method today_entrusts (line 326) | def today_entrusts(self):
    method today_trades (line 395) | def today_trades(self):
    method buy (line 450) | def buy(self, security: str, price: float, amount: int, **kwargs):
    method sell (line 467) | def sell(self, security, price, amount, **kwargs):
    method trade (line 484) | def trade(self, security: str, price: float, amount: int, *, is_buy: b...
    method market_buy (line 519) | def market_buy(self, security, amount, ttype=None):
    method market_sell (line 548) | def market_sell(self, security, amount, ttype=None):
    method market_trade (line 576) | def market_trade(self, security: str, amount: int, ttype: str = None, ...
    method cancel_entrust (line 631) | def cancel_entrust(self, entrust_no: int):
    method _get_stock_code (line 647) | def _get_stock_code(self, security: str) -> str:

FILE: easytrader/pop_dialog_handler.py
  class PopDialogHandler (line 11) | class PopDialogHandler:
    method __init__ (line 12) | def __init__(self, app):
    method _set_foreground (line 16) | def _set_foreground(window):
    method handle (line 23) | def handle(self, title):
    method _extract_content (line 37) | def _extract_content(self):
    method _extract_entrust_id (line 41) | def _extract_entrust_id(content):
    method _submit_by_click (line 44) | def _submit_by_click(self):
    method _submit_by_shortcut (line 52) | def _submit_by_shortcut(self):
    method _close (line 56) | def _close(self):
  class TradePopDialogHandler (line 60) | class TradePopDialogHandler(PopDialogHandler):
    method handle (line 62) | def handle(self, title) -> Optional[dict]:

FILE: easytrader/refresh_strategies.py
  class IRefreshStrategy (line 22) | class IRefreshStrategy(abc.ABC):
    method refresh (line 26) | def refresh(self):
    method set_trader (line 32) | def set_trader(self, trader: "clienttrader.ClientTrader"):
  class Switch (line 37) | class Switch(IRefreshStrategy):
    method __init__ (line 40) | def __init__(self, sleep: float = 0.1):
    method refresh (line 43) | def refresh(self):
  class Toolbar (line 48) | class Toolbar(IRefreshStrategy):
    method __init__ (line 51) | def __init__(self, refresh_btn_index: int = 4):
    method refresh (line 58) | def refresh(self):

FILE: easytrader/remoteclient.py
  function use (line 7) | def use(broker, host, port=1430, **kwargs):
  class RemoteClient (line 11) | class RemoteClient:
    method __init__ (line 12) | def __init__(self, broker, host, port=1430, **kwargs):
    method prepare (line 26) | def prepare(
    method balance (line 61) | def balance(self):
    method position (line 65) | def position(self):
    method today_entrusts (line 69) | def today_entrusts(self):
    method today_trades (line 73) | def today_trades(self):
    method cancel_entrusts (line 77) | def cancel_entrusts(self):
    method auto_ipo (line 80) | def auto_ipo(self):
    method exit (line 83) | def exit(self):
    method common_get (line 86) | def common_get(self, endpoint):
    method buy (line 92) | def buy(self, security, price, amount, **kwargs):
    method sell (line 101) | def sell(self, security, price, amount, **kwargs):
    method market_buy (line 110) | def market_buy(self, security, amount, **kwargs):
    method market_sell (line 119) | def market_sell(self, security, amount, **kwargs):
    method cancel_entrust (line 128) | def cancel_entrust(self, entrust_no):

FILE: easytrader/ricequant_follower.py
  class RiceQuantFollower (line 10) | class RiceQuantFollower(BaseFollower):
    method __init__ (line 11) | def __init__(self):
    method login (line 15) | def login(self, user=None, password=None, **kwargs):
    method follow (line 20) | def follow(
    method extract_strategy_name (line 63) | def extract_strategy_name(self, run_id):
    method extract_day_trades (line 74) | def extract_day_trades(self, run_id):
    method query_strategy_transaction (line 85) | def query_strategy_transaction(self, strategy, **kwargs):
    method stock_shuffle_to_prefix (line 91) | def stock_shuffle_to_prefix(stock):
    method project_transactions (line 102) | def project_transactions(self, transactions, **kwargs):

FILE: easytrader/server.py
  function error_handle (line 13) | def error_handle(func):
  function post_prepare (line 29) | def post_prepare():
  function get_balance (line 41) | def get_balance():
  function get_position (line 50) | def get_position():
  function get_auto_ipo (line 59) | def get_auto_ipo():
  function get_today_entrusts (line 68) | def get_today_entrusts():
  function get_today_trades (line 77) | def get_today_trades():
  function get_cancel_entrusts (line 86) | def get_cancel_entrusts():
  function post_buy (line 95) | def post_buy():
  function post_sell (line 105) | def post_sell():
  function post_cancel_entrust (line 116) | def post_cancel_entrust():
  function get_exit (line 127) | def get_exit():
  function run (line 134) | def run(port=1430):

FILE: easytrader/universal_clienttrader.py
  class UniversalClientTrader (line 10) | class UniversalClientTrader(clienttrader.BaseLoginClientTrader):
    method broker_type (line 14) | def broker_type(self):
    method login (line 17) | def login(self, user, password, exe_path, comm_password=None, **kwargs):

FILE: easytrader/utils/captcha.py
  function captcha_recognize (line 9) | def captcha_recognize(img_path):
  function recognize_verify_code (line 28) | def recognize_verify_code(image_path, broker="ht"):
  function detect_yh_client_result (line 42) | def detect_yh_client_result(image_path):
  function input_verify_code_manual (line 54) | def input_verify_code_manual(image_path):
  function default_verify_code_detect (line 65) | def default_verify_code_detect(image_path):
  function detect_gf_result (line 72) | def detect_gf_result(image_path):
  function invoke_tesseract_to_recognize (line 93) | def invoke_tesseract_to_recognize(img):

FILE: easytrader/utils/misc.py
  function parse_cookies_str (line 5) | def parse_cookies_str(cookies):
  function file2dict (line 20) | def file2dict(path):
  function grep_comma (line 25) | def grep_comma(num_str):
  function str2num (line 29) | def str2num(num_str, convert_type="float"):

FILE: easytrader/utils/perf.py
  function perf_clock (line 15) | def perf_clock(f):

FILE: easytrader/utils/stock.py
  function get_stock_type (line 9) | def get_stock_type(stock_code):
  function get_30_date (line 28) | def get_30_date():
  function get_today_ipo_data (line 40) | def get_today_ipo_data():

FILE: easytrader/webtrader.py
  class WebTrader (line 19) | class WebTrader(metaclass=abc.ABCMeta):
    method __init__ (line 23) | def __init__(self, debug=True):
    method read_config (line 33) | def read_config(self, path):
    method prepare (line 42) | def prepare(self, config_file=None, user=None, password=None, **kwargs):
    method _prepare_account (line 58) | def _prepare_account(self, user, password, **kwargs):
    method autologin (line 62) | def autologin(self, limit=10):
    method login (line 75) | def login(self):
    method keepalive (line 78) | def keepalive(self):
    method send_heartbeat (line 85) | def send_heartbeat(self):
    method check_login (line 93) | def check_login(self, sleepy=30):
    method heartbeat (line 108) | def heartbeat(self):
    method check_account_live (line 111) | def check_account_live(self, response):
    method exit (line 114) | def exit(self):
    method __read_config (line 118) | def __read_config(self):
    method balance (line 125) | def balance(self):
    method get_balance (line 128) | def get_balance(self):
    method position (line 133) | def position(self):
    method get_position (line 136) | def get_position(self):
    method entrust (line 141) | def entrust(self):
    method get_entrust (line 144) | def get_entrust(self):
    method current_deal (line 149) | def current_deal(self):
    method get_current_deal (line 152) | def get_current_deal(self):
    method exchangebill (line 158) | def exchangebill(self):
    method get_exchangebill (line 167) | def get_exchangebill(self, start_date, end_date):
    method get_ipo_limit (line 176) | def get_ipo_limit(self, stock_code):
    method do (line 184) | def do(self, params):
    method create_basic_params (line 203) | def create_basic_params(self) -> dict:
    method request (line 207) | def request(self, params) -> dict:
    method format_response_data (line 212) | def format_response_data(self, data):
    method fix_error_data (line 217) | def fix_error_data(self, data):
    method format_response_data_type (line 222) | def format_response_data_type(self, response_data):
    method check_login_status (line 244) | def check_login_status(self, return_data):

FILE: easytrader/wk_clienttrader.py
  class WKClientTrader (line 7) | class WKClientTrader(HTClientTrader):
    method broker_type (line 9) | def broker_type(self):
    method login (line 12) | def login(self, user, password, exe_path, comm_password=None, **kwargs):

FILE: easytrader/xq_follower.py
  class XueQiuFollower (line 15) | class XueQiuFollower(BaseFollower):
    method __init__ (line 22) | def __init__(self):
    method login (line 27) | def login(self, user=None, password=None, **kwargs):
    method follow (line 49) | def follow(  # type: ignore
    method calculate_assets (line 123) | def calculate_assets(self, strategy_url, total_assets=None, initial_as...
    method extract_strategy_id (line 135) | def extract_strategy_id(strategy_url):
    method extract_strategy_name (line 138) | def extract_strategy_name(self, strategy_url):
    method extract_transactions (line 145) | def extract_transactions(self, history):
    method create_query_transaction_params (line 159) | def create_query_transaction_params(self, strategy):
    method none_to_zero (line 164) | def none_to_zero(self, data):
    method project_transactions (line 170) | def project_transactions(self, transactions, assets):
    method _adjust_sell_amount (line 192) | def _adjust_sell_amount(self, stock_code, amount):
    method _get_portfolio_info (line 229) | def _get_portfolio_info(self, portfolio_code):
    method _get_portfolio_net_value (line 244) | def _get_portfolio_net_value(self, portfolio_code):

FILE: easytrader/xqtrader.py
  class XueQiuTrader (line 16) | class XueQiuTrader(webtrader.WebTrader):
    method __init__ (line 34) | def __init__(self, **kwargs):
    method autologin (line 52) | def autologin(self, **kwargs):
    method _set_cookies (line 59) | def _set_cookies(self, cookies):
    method _prepare_account (line 68) | def _prepare_account(self, user="", password="", **kwargs):
    method _virtual_to_balance (line 92) | def _virtual_to_balance(self, virtual):
    method _get_html (line 100) | def _get_html(self, url):
    method _search_stock_info (line 103) | def _search_stock_info(self, code):
    method _get_portfolio_info (line 127) | def _get_portfolio_info(self, portfolio_code):
    method get_balance (line 146) | def get_balance(self):
    method cash_weight (line 171) | def cash_weight(self):
    method _get_position (line 177) | def _get_position(self):
    method _time_strftime (line 189) | def _time_strftime(time_stamp):
    method get_position (line 197) | def get_position(self):
    method _get_xq_history (line 223) | def _get_xq_history(self):
    method history (line 240) | def history(self):
    method get_entrust (line 243) | def get_entrust(self):
    method cancel_entrust (line 283) | def cancel_entrust(self, entrust_no):
    method adjust_weight (line 325) | def adjust_weight(self, stock_code, weight, fetch_position=True):
    method _trade (line 404) | def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="bu...
    method buy (line 538) | def buy(self, security, price=0, amount=0, volume=0, entrust_prop=0):
    method sell (line 548) | def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0):
    method adjust_weights (line 559) | def adjust_weights(self, weights, ignore_minor=0.0, fetch_position=True):

FILE: easytrader/yh_clienttrader.py
  class YHClientTrader (line 11) | class YHClientTrader(clienttrader.BaseLoginClientTrader):
    method broker_type (line 23) | def broker_type(self):
    method login (line 26) | def login(self, user, password, exe_path, comm_password=None, **kwargs):
    method _switch_window_to_normal_mode (line 86) | def _switch_window_to_normal_mode(self):
    method _handle_verify_code (line 91) | def _handle_verify_code(self, is_xiadan):
    method balance (line 110) | def balance(self):
    method auto_ipo (line 114) | def auto_ipo(self):

FILE: tests/test_easytrader.py
  class TestYhClientTrader (line 15) | class TestYhClientTrader(unittest.TestCase):
    method setUpClass (line 17) | def setUpClass(cls):
    method test_balance (line 30) | def test_balance(self):
    method test_today_entrusts (line 34) | def test_today_entrusts(self):
    method test_today_trades (line 37) | def test_today_trades(self):
    method test_cancel_entrusts (line 40) | def test_cancel_entrusts(self):
    method test_cancel_entrust (line 43) | def test_cancel_entrust(self):
    method test_invalid_buy (line 46) | def test_invalid_buy(self):
    method test_invalid_sell (line 52) | def test_invalid_sell(self):
    method test_auto_ipo (line 58) | def test_auto_ipo(self):
  class TestHTClientTrader (line 63) | class TestHTClientTrader(unittest.TestCase):
    method setUpClass (line 65) | def setUpClass(cls):
    method test_balance (line 83) | def test_balance(self):
    method test_today_entrusts (line 87) | def test_today_entrusts(self):
    method test_today_trades (line 90) | def test_today_trades(self):
    method test_cancel_entrusts (line 93) | def test_cancel_entrusts(self):
    method test_cancel_entrust (line 96) | def test_cancel_entrust(self):
    method test_invalid_buy (line 99) | def test_invalid_buy(self):
    method test_invalid_sell (line 105) | def test_invalid_sell(self):
    method test_auto_ipo (line 111) | def test_auto_ipo(self):
    method test_invalid_repo (line 114) | def test_invalid_repo(self):
    method test_invalid_reverse_repo (line 120) | def test_invalid_reverse_repo(self):
  class TestHTZQClientTrader (line 128) | class TestHTZQClientTrader(unittest.TestCase):
    method setUpClass (line 130) | def setUpClass(cls):
    method test_balance (line 149) | def test_balance(self):
    method test_today_entrusts (line 153) | def test_today_entrusts(self):
    method test_today_trades (line 156) | def test_today_trades(self):
    method test_cancel_entrusts (line 159) | def test_cancel_entrusts(self):
    method test_cancel_entrust (line 162) | def test_cancel_entrust(self):
    method test_invalid_buy (line 165) | def test_invalid_buy(self):
    method test_invalid_sell (line 171) | def test_invalid_sell(self):
    method test_auto_ipo (line 177) | def test_auto_ipo(self):

FILE: tests/test_xq_follower.py
  class TestXueQiuTrader (line 11) | class TestXueQiuTrader(unittest.TestCase):
    method test_adjust_sell_amount_without_enable (line 12) | def test_adjust_sell_amount_without_enable(self):
    method test_adjust_sell_should_only_work_when_sell (line 22) | def test_adjust_sell_should_only_work_when_sell(self):
    method test_adjust_sell_amount (line 49) | def test_adjust_sell_amount(self):
    method test_slippage_with_default (line 67) | def test_slippage_with_default(self):
    method test_slippage (line 92) | def test_slippage(self):
  class TestXqFollower (line 134) | class TestXqFollower(unittest.TestCase):
    method setUp (line 135) | def setUp(self):
    method test_extract_transactions (line 142) | def test_extract_transactions(self):

FILE: tests/test_xqtrader.py
  class TestXueQiuTrader (line 7) | class TestXueQiuTrader(unittest.TestCase):
    method test_prepare_account (line 8) | def test_prepare_account(self):
Condensed preview — 67 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (230K chars).
[
  {
    "path": ".bumpversion.cfg",
    "chars": 129,
    "preview": "[bumpversion]\ncurrent_version = 0.23.7\ncommit = True\nfiles = easytrader/__init__.py setup.py\ntag = True\ntag_name = {new_"
  },
  {
    "path": ".coveragerc",
    "chars": 85,
    "preview": "[run]\nbranch = True\ninclude = easytrader/*\nomit = tests/*\n\n[report]\nfail_under = -1 \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 151,
    "preview": "## env\n\nOS: win7/ win10 / mac / linux\nPYTHON_VERSION: 3.x\nEASYTRADER_VERSION: 0.xx.xx\nBROKER_TYPE: gj / ht / xq / xxx\n\n#"
  },
  {
    "path": ".github/copilot-instructions.md",
    "chars": 3175,
    "preview": "# Project Guidelines\n\n## Overview\n\neasytrader is a Chinese stock trading automation library. It supports:\n- Windows desk"
  },
  {
    "path": ".gitignore",
    "chars": 928,
    "preview": "site\ncmd_cache.pk\nbak\n.mypy_cache\n.pyre\n.pytest_cache\nyjb_account.json\nhtt.json\ngft.json\ntest.py\nht_account.json\n.idea\n."
  },
  {
    "path": ".pylintrc",
    "chars": 17100,
    "preview": "[MASTER]\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are lo"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 101,
    "preview": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.9\"\n\nmkdocs:\n  configuration: mkdocs.yml"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2018 shidenggui\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "MANIFEST.in",
    "chars": 18,
    "preview": "include README.md\n"
  },
  {
    "path": "Makefile",
    "chars": 41,
    "preview": "test:\n\tpytest -vx --cov=easytrader tests\n"
  },
  {
    "path": "Pipfile",
    "chars": 721,
    "preview": "[[source]]\nurl = \"http://mirrors.aliyun.com/pypi/simple/\"\nverify_ssl = false\nname = \"pypi\"\n\n[packages]\npywinauto = \"*\"\n\""
  },
  {
    "path": "README.md",
    "chars": 1440,
    "preview": "# easytrader\n\n[![Package](https://img.shields.io/pypi/v/easytrader.svg)](https://pypi.python.org/pypi/easytrader)\n[![Lic"
  },
  {
    "path": "docs/follow.md",
    "chars": 2165,
    "preview": "# 策略跟踪 \n\n## 跟踪 `joinquant` / `ricequant`  的模拟交易\n\n##### 1) 初始化跟踪的 trader\n\n这里以雪球为例, 也可以使用银河之类 `easytrader` 支持的券商\n\n```\nxq_u"
  },
  {
    "path": "docs/index.md",
    "chars": 931,
    "preview": "# 简介\n\n* 通用的同花顺客户端模拟操作\n* 支持券商的 [miniqmt](miniqmt.md) 官方量化接口\n* 支持雪球组合调仓和跟踪\n* 支持远程操作客户端\n* 支持跟踪 `joinquant`, `ricequant` 的模拟"
  },
  {
    "path": "docs/install.md",
    "chars": 527,
    "preview": "# 安装\n\n### 同花顺客户端设置\n\n需要对客户端按以下设置,不然会导致下单时价格出错以及客户端超时锁定\n\n* 系统设置 > 界面设置: 界面不操作超时时间设为 0\n* 系统设置 > 交易设置: 默认买入价格/买入数量/卖出价格/卖出数量"
  },
  {
    "path": "docs/miniqmt.md",
    "chars": 5373,
    "preview": "# miniqmt\n\nminiqmt 是券商官方的低门槛 Python 量化交易接口,基于券商的讯投 QMT 服务。详情可以[进群](https://easytrader.readthedocs.io/zh-cn/master/#_2)交流"
  },
  {
    "path": "docs/remote.md",
    "chars": 547,
    "preview": "# 远端服务模式\n\n远端服务模式是交易服务端和量化策略端分离的模式。\n\n**交易服务端**通常是有固定`IP`地址的云服务器,该服务器上运行着`easytrader`交易服务。而**量化策略端**可能是`JoinQuant、RiceQuan"
  },
  {
    "path": "docs/usage.md",
    "chars": 4805,
    "preview": "# 使用\n\n## 引入\n\n```python\nimport easytrader\n```\n\n## 设置同花顺客户端类型\n\n**通用同花顺客户端**\n\n```python\nuser = easytrader.use('universal_cl"
  },
  {
    "path": "docs/xueqiu.md",
    "chars": 425,
    "preview": "# 雪球组合模拟交易\n\n因为雪球组合是按比例调仓的,所以模拟成券商实盘接口会有一些要注意的问题\n\n* 接口基本与其他券商接口调用参数返回一致\n* 委托单不支持挂高挂低(开盘时间都是直接市价成交的)\n* 初始资金是按组合净值 1:100000"
  },
  {
    "path": "easytrader/__init__.py",
    "chars": 268,
    "preview": "# -*- coding: utf-8 -*-\nimport urllib3\n\nfrom easytrader import exceptions\nfrom easytrader.api import use, follower\nfrom "
  },
  {
    "path": "easytrader/api.py",
    "chars": 3332,
    "preview": "# -*- coding: utf-8 -*-\nimport logging\nimport sys\n\nimport six\n\nfrom easytrader.joinquant_follower import JoinQuantFollow"
  },
  {
    "path": "easytrader/clienttrader.py",
    "chars": 19333,
    "preview": "# -*- coding: utf-8 -*-\nimport abc\nimport functools\nimport logging\nimport os\nimport re\nimport sys\nimport time\nfrom typin"
  },
  {
    "path": "easytrader/config/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "easytrader/config/client.py",
    "chars": 3962,
    "preview": "# -*- coding: utf-8 -*-\ndef create(broker):\n    if broker == \"yh\":\n        return YH\n    if broker == \"ht\":\n        retu"
  },
  {
    "path": "easytrader/config/global.json",
    "chars": 868,
    "preview": "{\n  \"response_format\": {\n    \"int\": [\n      \"current_amount\",\n      \"enable_amount\",\n      \"entrust_amount\",\n      \"busi"
  },
  {
    "path": "easytrader/config/xq.json",
    "chars": 551,
    "preview": "{\n  \"login_api\": \"https://xueqiu.com/user/login\",\n  \"prefix\": \"https://xueqiu.com/user/login\",\n  \"portfolio_url\": \"https"
  },
  {
    "path": "easytrader/exceptions.py",
    "chars": 208,
    "preview": "# -*- coding: utf-8 -*-\n\n\nclass TradeError(IOError):\n    pass\n\n\nclass NotLoginError(Exception):\n    def __init__(self, r"
  },
  {
    "path": "easytrader/follower.py",
    "chars": 13030,
    "preview": "# -*- coding: utf-8 -*-\nimport abc\nimport datetime\nimport os\nimport pickle\nimport queue\nimport re\nimport threading\nimpor"
  },
  {
    "path": "easytrader/gf_clienttrader.py",
    "chars": 2835,
    "preview": "# -*- coding: utf-8 -*-\nimport re\nimport tempfile\nimport time\nimport os\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfr"
  },
  {
    "path": "easytrader/gj_clienttrader.py",
    "chars": 2627,
    "preview": "# -*- coding: utf-8 -*-\nimport re\nimport tempfile\nimport time\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytra"
  },
  {
    "path": "easytrader/grid_strategies.py",
    "chars": 7123,
    "preview": "# -*- coding: utf-8 -*-\nimport abc\nimport io\nimport tempfile\nfrom io import StringIO\nfrom typing import TYPE_CHECKING, D"
  },
  {
    "path": "easytrader/ht_clienttrader.py",
    "chars": 2362,
    "preview": "# -*- coding: utf-8 -*-\r\n\r\nimport pywinauto\r\nimport pywinauto.clipboard\r\n\r\nfrom easytrader import grid_strategies\r\nfrom "
  },
  {
    "path": "easytrader/htzq_clienttrader.py",
    "chars": 1817,
    "preview": "# -*- coding: utf-8 -*-\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader import grid_strategies\nfrom . impo"
  },
  {
    "path": "easytrader/joinquant_follower.py",
    "chars": 5182,
    "preview": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\nfrom threading import Thread\n\nfrom easytrader import exceptions\nfr"
  },
  {
    "path": "easytrader/log.py",
    "chars": 313,
    "preview": "# -*- coding: utf-8 -*-\nimport logging\n\nlogger = logging.getLogger(\"easytrader\")\nlogger.setLevel(logging.INFO)\nlogger.pr"
  },
  {
    "path": "easytrader/miniqmt/__init__.py",
    "chars": 89,
    "preview": "from easytrader.miniqmt.miniqmt_trader import MiniqmtTrader, DefaultXtQuantTraderCallback"
  },
  {
    "path": "easytrader/miniqmt/miniqmt_trader.py",
    "chars": 24626,
    "preview": "from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback\nfrom xtquant.xttype import StockAccount\nfrom xtquant i"
  },
  {
    "path": "easytrader/pop_dialog_handler.py",
    "chars": 2897,
    "preview": "# coding:utf-8\nimport re\nimport time\nfrom typing import Optional\n\nfrom easytrader import exceptions\nfrom easytrader.util"
  },
  {
    "path": "easytrader/refresh_strategies.py",
    "chars": 1424,
    "preview": "# -*- coding: utf-8 -*-\nimport abc\nimport io\nimport tempfile\nfrom io import StringIO\nfrom typing import TYPE_CHECKING, D"
  },
  {
    "path": "easytrader/remoteclient.py",
    "chars": 4045,
    "preview": "# -*- coding: utf-8 -*-\nimport requests\n\nfrom easytrader.utils.misc import file2dict\n\n\ndef use(broker, host, port=1430, "
  },
  {
    "path": "easytrader/ricequant_follower.py",
    "chars": 4087,
    "preview": "# -*- coding: utf-8 -*-\n\nfrom datetime import datetime\nfrom threading import Thread\n\nfrom easytrader.follower import Bas"
  },
  {
    "path": "easytrader/server.py",
    "chars": 2942,
    "preview": "import functools\r\n\r\nfrom flask import Flask, jsonify, request\r\n\r\nfrom . import api\r\nfrom .log import logger\r\n\r\napp = Fla"
  },
  {
    "path": "easytrader/universal_clienttrader.py",
    "chars": 1760,
    "preview": "# -*- coding: utf-8 -*-\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader import grid_strategies\nfrom . impo"
  },
  {
    "path": "easytrader/utils/__init__.py",
    "chars": 2,
    "preview": "\r\n"
  },
  {
    "path": "easytrader/utils/captcha.py",
    "chars": 2988,
    "preview": "import re\r\n\r\nimport requests\r\nfrom PIL import Image\r\n\r\nfrom easytrader import exceptions\r\n\r\n\r\ndef captcha_recognize(img_"
  },
  {
    "path": "easytrader/utils/misc.py",
    "chars": 666,
    "preview": "# coding:utf-8\nimport json\n\n\ndef parse_cookies_str(cookies):\n    \"\"\"\n    parse cookies str to dict\n    :param cookies: c"
  },
  {
    "path": "easytrader/utils/perf.py",
    "chars": 1051,
    "preview": "# coding:utf-8\nimport functools\nimport inspect\nimport logging\nimport timeit\n\nfrom easytrader import logger\n\ntry:\n    fro"
  },
  {
    "path": "easytrader/utils/stock.py",
    "chars": 2735,
    "preview": "# coding:utf-8\nimport datetime\nimport json\nimport random\n\nimport requests\n\n\ndef get_stock_type(stock_code):\n    \"\"\"判断股票I"
  },
  {
    "path": "easytrader/utils/win_gui.py",
    "chars": 119,
    "preview": "# coding:utf-8\nfrom pywinauto import win32defines\nfrom pywinauto.win32functions import SetForegroundWindow, ShowWindow\n"
  },
  {
    "path": "easytrader/webtrader.py",
    "chars": 7095,
    "preview": "# -*- coding: utf-8 -*-\nimport abc\nimport logging\nimport os\nimport re\nimport time\nfrom threading import Thread\n\nimport r"
  },
  {
    "path": "easytrader/wk_clienttrader.py",
    "chars": 1781,
    "preview": "# -*- coding: utf-8 -*-\nimport pywinauto\n\nfrom easytrader.ht_clienttrader import HTClientTrader\n\n\nclass WKClientTrader(H"
  },
  {
    "path": "easytrader/xq_follower.py",
    "chars": 8783,
    "preview": "# -*- coding: utf-8 -*-\nfrom __future__ import division, print_function, unicode_literals\n\nimport json\nimport re\nfrom da"
  },
  {
    "path": "easytrader/xqtrader.py",
    "chars": 23955,
    "preview": "# -*- coding: utf-8 -*-\r\nimport json\r\nimport numbers\r\nimport os\r\nimport re\r\nimport time\r\nimport math\r\n\r\nimport requests\r"
  },
  {
    "path": "easytrader/yh_clienttrader.py",
    "chars": 4437,
    "preview": "# -*- coding: utf-8 -*-\nimport re\nimport tempfile\n\nimport pywinauto\n\nfrom easytrader import clienttrader, grid_strategie"
  },
  {
    "path": "gj_client.json",
    "chars": 45,
    "preview": "{\n  \"user\": \"国金用户名\",\n  \"password\": \"国金明文密码\"\n}"
  },
  {
    "path": "mkdocs.yml",
    "chars": 265,
    "preview": "site_name: easytrader\nnav:\n  - 简介: index.md\n  - 安装: install.md\n  - 使用: usage.md\n  - miniqmt 量化接口: miniqmt.md\n  - 雪球组合模拟交"
  },
  {
    "path": "mypy.ini",
    "chars": 37,
    "preview": "[mypy]\nignore_missing_imports = True\n"
  },
  {
    "path": "readthedocs-requirements.txt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "requirements.txt",
    "chars": 617,
    "preview": "-i http://mirrors.aliyun.com/pypi/simple/\n--trusted-host mirrors.aliyun.com\nbeautifulsoup4==4.6.0\nbs4==0.0.1\ncertifi==20"
  },
  {
    "path": "setup.py",
    "chars": 1137,
    "preview": "# coding:utf8\r\nfrom setuptools import setup\r\n\r\n\r\nsetup(\r\n    name=\"easytrader\",\r\n    version=\"0.23.7\",\r\n    description="
  },
  {
    "path": "test-requirements.txt",
    "chars": 40,
    "preview": "-r requirements.txt\n\npytest\npytest-cov\n\n"
  },
  {
    "path": "tests/__init__.py",
    "chars": 14,
    "preview": "# coding:utf8\n"
  },
  {
    "path": "tests/test_easytrader.py",
    "chars": 5258,
    "preview": "# coding: utf-8\nimport os\nimport sys\nimport time\nimport unittest\n\nsys.path.append(\".\")\n\nTEST_CLIENTS = set(os.environ.ge"
  },
  {
    "path": "tests/test_xq_follower.py",
    "chars": 7552,
    "preview": "# coding:utf-8\nimport datetime\nimport os\nimport time\nimport unittest\nfrom unittest import mock\n\nfrom easytrader.xq_follo"
  },
  {
    "path": "tests/test_xqtrader.py",
    "chars": 582,
    "preview": "# coding: utf-8\nimport unittest\n\nfrom easytrader.xqtrader import XueQiuTrader\n\n\nclass TestXueQiuTrader(unittest.TestCase"
  },
  {
    "path": "xq.json",
    "chars": 177,
    "preview": "{\n  \"cookies\": \"雪球 cookies,登陆后获取,获取方式见 https://smalltool.github.io/2016/08/02/cookie/\",\n  \"portfolio_code\": \"组合代码(例:ZH81"
  },
  {
    "path": "yh_client.json",
    "chars": 45,
    "preview": "{\n  \"user\": \"银河用户名\",\n  \"password\": \"银河明文密码\"\n}"
  }
]

About this extraction

This page contains the full source code of the shidenggui/easytrader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 67 files (209.8 KB), approximately 59.2k tokens, and a symbol index with 376 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!