[
  {
    "path": ".bumpversion.cfg",
    "content": "[bumpversion]\ncurrent_version = 0.23.7\ncommit = True\nfiles = easytrader/__init__.py setup.py\ntag = True\ntag_name = {new_version}\n"
  },
  {
    "path": ".coveragerc",
    "content": "[run]\nbranch = True\ninclude = easytrader/*\nomit = tests/*\n\n[report]\nfail_under = -1 \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "## env\n\nOS: win7/ win10 / mac / linux\nPYTHON_VERSION: 3.x\nEASYTRADER_VERSION: 0.xx.xx\nBROKER_TYPE: gj / ht / xq / xxx\n\n## problem\n\n## how to repeat\n\n\n\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Project Guidelines\n\n## Overview\n\neasytrader is a Chinese stock trading automation library. It supports:\n- Windows desktop client brokers via pywinauto GUI automation (同花顺-based)\n- Xueqiu (雪球) web portfolio trading via REST API\n- MiniQMT (迅投) official quant SDK integration\n- Strategy following from JoinQuant / RiceQuant / Xueqiu\n- Remote client-server execution via Flask\n\n## Architecture\n\n**Entry points**: `easytrader.use(broker)` and `easytrader.follower(platform)` — factory functions in `easytrader/api.py`.\n\n**Three parallel branches**:\n\n| Branch | Base class | Purpose |\n|--------|-----------|---------|\n| Desktop GUI | `ClientTrader` → broker subclasses | pywinauto-driven Windows client automation |\n| Web/API | `WebTrader` → `XueQiuTrader`, `MiniqmtTrader` | HTTP/SDK-based trading |\n| Followers | `BaseFollower` → platform subclasses | Mirror strategies from quant platforms |\n\n**Remote**: `server.py` (Flask) + `remoteclient.py` enables controlling a Windows trader remotely.\n\n**Config**: `easytrader/config/client.py` holds broker-specific UI control IDs. JSON configs in `easytrader/config/` store API URLs.\n\n## Code Style\n\n- **Formatter**: `black -l 79` (79-char line limit)\n- **Import sorting**: `isort`\n- **Linter**: `pylint`\n- **Type checking**: `mypy` (with `ignore_missing_imports = True`)\n- **Language**: Comments, error messages, log messages, and docstrings are in **Chinese**. Follow this convention.\n- Type hints are used sparingly (mainly in newer code like `miniqmt/`). Match the style of the surrounding code.\n\n## Build and Test\n\n```bash\n# Install\npipenv install\n\n# Run tests (only xq_follower and xqtrader tests are runnable without Windows + broker)\nmake test                  # or: pipenv run test → pytest -vx --cov=easytrader tests\n\n# Lint & format\npipenv run lint            # pylint\npipenv run format          # black -l 79\npipenv run sort_imports    # isort\npipenv run type_check      # mypy\n```\n\nTests 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.\n\n## Conventions\n\n- **Factory dispatch** in `api.py` — broker modules are lazily imported inside `if` branches, not registered via a plugin system.\n- **ABC + strategy pattern**: `IGridStrategy`, `IRefreshStrategy` abstract bases with swappable implementations for grid reading and data refresh.\n- **Logging**: Single shared logger `logging.getLogger(\"easytrader\")` — do not create module-level loggers.\n- **Exceptions**: Use `TradeError` and `NotLoginError` from `easytrader/exceptions.py`.\n- **Perf timing**: `@perf_clock` decorator from `easytrader/utils/perf.py` for performance-critical operations.\n- **Session keepalive**: `WebTrader` uses a daemon heartbeat thread.\n\n## Platform Pitfalls\n\n- `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.\n- `xtquant` (miniqmt dependency) is a proprietary SDK installed from the QMT terminal, not from PyPI.\n- `pywinauto` is pinned to `==0.6.6` — do not upgrade without testing.\n"
  },
  {
    "path": ".gitignore",
    "content": "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.vscode\n.ipynb_checkpoints\nUntitled.ipynb\nuntitled.txt\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\naccount.json\naccount.session\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# cache\ntmp/\n\nsecrets/\n"
  },
  {
    "path": ".pylintrc",
    "content": "[MASTER]\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are loading into the active Python interpreter and may\n# run arbitrary code\nextension-pkg-whitelist=\n\n# Add files or directories to the blacklist. They should be base names, not\n# paths.\nignore=CVS\n\n# Add files or directories matching the regex patterns to the blacklist. The\n# regex matches against base names, not paths.\nignore-patterns=\\d{4}.+\\.py,\n                test,\n                apps.py,\n                __init__.py,\n                urls.py,\n                manage.py\n\n# Python code to execute, usually for sys.path manipulation such as\n# pygtk.require().\n#init-hook=\n\n# Use multiple processes to speed up Pylint.\njobs=0\n\n# List of plugins (as comma separated values of python modules names) to load,\n# usually to register additional checkers.\nload-plugins=\n\n# Pickle collected data for later comparisons.\npersistent=yes\n\n# Specify a configuration file.\n#rcfile=\n\n# When enabled, pylint would attempt to guess common misconfiguration and emit\n# user-friendly hints instead of false-positive error messages\nsuggestion-mode=yes\n\n# Allow loading of arbitrary C extensions. Extensions are imported into the\n# active Python interpreter and may run arbitrary code.\nunsafe-load-any-extension=no\n\n\n[MESSAGES CONTROL]\n\n# Only show warnings with the listed confidence levels. Leave empty to show\n# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED\nconfidence=\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifiers separated by comma (,) or put this\n# option multiple times (only on the command line, not in the configuration\n# file where it should appear only once).You can also use \"--disable=all\" to\n# disable everything first and then reenable specific checks. For example, if\n# you want to run only the similarities checker, you can use \"--disable=all\n# --enable=similarities\". If you want to run only the classes checker, but have\n# no Warning level messages displayed, use\"--disable=all --enable=classes\n# --disable=W\"\ndisable=too-many-public-methods,\n        len-as-condition,\n        unused-argument,\n        too-many-arguments,\n        arguments-differ,\n        line-too-long,\n        fixme,\n        missing-docstring,\n        invalid-envvar-default,\n        ungrouped-imports,\n        bad-continuation,\n        too-many-ancestors,\n        too-few-public-methods,\n        no-self-use,\n        #print-statement,\n        #parameter-unpacking,\n        #unpacking-in-except,\n        #old-raise-syntax,\n        #backtick,\n        #long-suffix,\n        #old-ne-operator,\n        #old-octal-literal,\n        #import-star-module-level,\n        #non-ascii-bytes-literal,\n        #raw-checker-failed,\n        #bad-inline-option,\n        #locally-disabled,\n        #locally-enabled,\n        #file-ignored,\n        #suppressed-message,\n        #useless-suppression,\n        #deprecated-pragma,\n        #apply-builtin,\n        #basestring-builtin,\n        #buffer-builtin,\n        #cmp-builtin,\n        #coerce-builtin,\n        #execfile-builtin,\n        #file-builtin,\n        #long-builtin,\n        #raw_input-builtin,\n        #reduce-builtin,\n        #standarderror-builtin,\n        #unicode-builtin,\n        #xrange-builtin,\n        #coerce-method,\n        #delslice-method,\n        #getslice-method,\n        #setslice-method,\n        #no-absolute-import,\n        #old-division,\n        #dict-iter-method,\n        #dict-view-method,\n        #next-method-called,\n        #metaclass-assignment,\n        #indexing-exception,\n        #raising-string,\n        #reload-builtin,\n        #oct-method,\n        #hex-method,\n        #nonzero-method,\n        #cmp-method,\n        #input-builtin,\n        #round-builtin,\n        #intern-builtin,\n        #unichr-builtin,\n        #map-builtin-not-iterating,\n        #zip-builtin-not-iterating,\n        #range-builtin-not-iterating,\n        #filter-builtin-not-iterating,\n        #using-cmp-argument,\n        #eq-without-hash,\n        #div-method,\n        #idiv-method,\n        #rdiv-method,\n        #exception-message-attribute,\n        #invalid-str-codec,\n        #sys-max-int,\n        #bad-python3-import,\n        #deprecated-string-function,\n        #deprecated-str-translate-call,\n        #deprecated-itertools-function,\n        #deprecated-types-field,\n        #next-method-defined,\n        #dict-items-not-iterating,\n        #dict-keys-not-iterating,\n        #dict-values-not-iterating\n\n# Enable the message, report, category or checker with the given id(s). You can\n# either give multiple identifier separated by comma (,) or put this option\n# multiple time (only on the command line, not in the configuration file where\n# it should appear only once). See also the \"--disable\" option for examples.\nenable=c-extension-no-member\n\n\n[REPORTS]\n\n# Python expression which should return a note less than 10 (10 is the highest\n# note). You have access to the variables errors warning, statement which\n# respectively contain the number of errors / warnings messages and the total\n# number of statements analyzed. This is used by the global evaluation report\n# (RP0004).\nevaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)\n\n# Template used to display messages. This is a python new-style format string\n# used to format the message information. See doc for all details\n#msg-template=\n\n# Set the output format. Available formats are text, parseable, colorized, json\n# and msvs (visual studio).You can also give a reporter class, eg\n# mypackage.mymodule.MyReporterClass.\noutput-format=text\n\n# Tells whether to display a full report or only the messages\nreports=no\n\n# Activate the evaluation score.\nscore=yes\n\n\n[REFACTORING]\n\n# Maximum number of nested blocks for function / method body\nmax-nested-blocks=5\n\n# Complete name of functions that never returns. When checking for\n# inconsistent-return-statements if a never returning function is called then\n# it will be considered as an explicit return statement and no message will be\n# printed.\nnever-returning-functions=optparse.Values,sys.exit\n\n\n[BASIC]\n\n# Naming style matching correct argument names\nargument-naming-style=snake_case\n\n# Regular expression matching correct argument names. Overrides argument-\n# naming-style\n#argument-rgx=\n\n# Naming style matching correct attribute names\nattr-naming-style=snake_case\n\n# Regular expression matching correct attribute names. Overrides attr-naming-\n# style\n#attr-rgx=\n\n# Bad variable names which should always be refused, separated by a comma\nbad-names=foo,\n          bar,\n          baz,\n          toto,\n          tutu,\n          tata\n\n# Naming style matching correct class attribute names\nclass-attribute-naming-style=any\n\n# Regular expression matching correct class attribute names. Overrides class-\n# attribute-naming-style\n#class-attribute-rgx=\n\n# Naming style matching correct class names\nclass-naming-style=PascalCase\n\n# Regular expression matching correct class names. Overrides class-naming-style\n#class-rgx=\n\n# Naming style matching correct constant names\nconst-naming-style=any\n\n# Regular expression matching correct constant names. Overrides const-naming-\n# style\n#const-rgx=\n\n# Minimum line length for functions/classes that require docstrings, shorter\n# ones are exempt.\ndocstring-min-length=5\n\n# Naming style matching correct function names\nfunction-naming-style=snake_case\n\n# Regular expression matching correct function names. Overrides function-\n# naming-style\n#function-rgx=\n\n# Good variable names which should always be accepted, separated by a comma\ngood-names=i,\n           do,\n           f,\n           df,\n           s,\n           j,\n           k,\n           ex,\n           Run,\n           _,\n           db,\n           r,\n           x,\n           y,\n           e\n\n# Include a hint for the correct naming format with invalid-name\ninclude-naming-hint=no\n\n# Naming style matching correct inline iteration names\ninlinevar-naming-style=any\n\n# Regular expression matching correct inline iteration names. Overrides\n# inlinevar-naming-style\n#inlinevar-rgx=\n\n# Naming style matching correct method names\nmethod-naming-style=snake_case\n\n# Regular expression matching correct method names. Overrides method-naming-\n# style\n#method-rgx=\n\n# Naming style matching correct module names\nmodule-naming-style=snake_case\n\n# Regular expression matching correct module names. Overrides module-naming-\n# style\n#module-rgx=\n\n# Colon-delimited sets of names that determine each other's naming style when\n# the name regexes allow several styles.\nname-group=\n\n# Regular expression which should only match function or class names that do\n# not require a docstring.\nno-docstring-rgx=^_\n\n# List of decorators that produce properties, such as abc.abstractproperty. Add\n# to this list to register other decorators that produce valid properties.\nproperty-classes=abc.abstractproperty\n\n# Naming style matching correct variable names\nvariable-naming-style=snake_case\n\n# Regular expression matching correct variable names. Overrides variable-\n# naming-style\n#variable-rgx=\n\n\n[FORMAT]\n\n# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.\nexpected-line-ending-format=\n\n# Regexp for a line that is allowed to be longer than the limit.\nignore-long-lines=^\\s*(# )?<?https?://\\S+>?$\n\n# Number of spaces of indent required inside a hanging  or continued line.\nindent-after-paren=4\n\n# String used as indentation unit. This is usually \"    \" (4 spaces) or \"\\t\" (1\n# tab).\nindent-string='    '\n\n# Maximum number of characters on a single line.\nmax-line-length=79\n\n# Maximum number of lines in a module\nmax-module-lines=1000\n\n# List of optional constructs for which whitespace checking is disabled. `dict-\n# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\\n222: 2}.\n# `trailing-comma` allows a space between comma and closing bracket: (a, ).\n# `empty-line` allows space-only lines.\nno-space-check=trailing-comma,\n               dict-separator\n\n# Allow the body of a class to be on the same line as the declaration if body\n# contains single statement.\nsingle-line-class-stmt=no\n\n# Allow the body of an if to be on the same line as the test if there is no\n# else.\nsingle-line-if-stmt=no\n\n\n[LOGGING]\n\n# Logging modules to check that the string format arguments are in logging\n# function parameter format\nlogging-modules=logging\n\n\n[MISCELLANEOUS]\n\n# List of note tags to take in consideration, separated by a comma.\nnotes=FIXME,\n      XXX,\n      TODO\n\n\n[SIMILARITIES]\n\n# Ignore comments when computing similarities.\nignore-comments=yes\n\n# Ignore docstrings when computing similarities.\nignore-docstrings=yes\n\n# Ignore imports when computing similarities.\nignore-imports=no\n\n# Minimum lines number of a similarity.\nmin-similarity-lines=4\n\n\n[SPELLING]\n\n# Limits count of emitted suggestions for spelling mistakes\nmax-spelling-suggestions=4\n\n# Spelling dictionary name. Available dictionaries: none. To make it working\n# install python-enchant package.\nspelling-dict=\n\n# List of comma separated words that should not be checked.\nspelling-ignore-words=\n\n# A path to a file that contains private dictionary; one word per line.\nspelling-private-dict-file=\n\n# Tells whether to store unknown words to indicated private dictionary in\n# --spelling-private-dict-file option instead of raising a message.\nspelling-store-unknown-words=no\n\n\n[TYPECHECK]\n\n# List of decorators that produce context managers, such as\n# contextlib.contextmanager. Add to this list to register other decorators that\n# produce valid context managers.\ncontextmanager-decorators=contextlib.contextmanager\n\n# List of members which are set dynamically and missed by pylint inference\n# system, and so shouldn't trigger E1101 when accessed. Python regular\n# expressions are accepted.\ngenerated-members=\n\n# Tells whether missing members accessed in mixin class should be ignored. A\n# mixin class is detected if its name ends with \"mixin\" (case insensitive).\nignore-mixin-members=yes\n\n# This flag controls whether pylint should warn about no-member and similar\n# checks whenever an opaque object is returned when inferring. The inference\n# can return multiple potential results while evaluating a Python object, but\n# some branches might not be evaluated, which results in partial inference. In\n# that case, it might be useful to still emit no-member and other checks for\n# the rest of the inferred objects.\nignore-on-opaque-inference=yes\n\n# List of class names for which member attributes should not be checked (useful\n# for classes with dynamically set attributes). This supports the use of\n# qualified names.\nignored-classes=optparse.Values,thread._local,_thread._local\n\n# List of module names for which member attributes should not be checked\n# (useful for modules/projects where namespaces are manipulated during runtime\n# and thus existing member attributes cannot be deduced by static analysis. It\n# supports qualified module names, as well as Unix pattern matching.\nignored-modules=\n\n# Show a hint with possible names when a member name was not found. The aspect\n# of finding the hint is based on edit distance.\nmissing-member-hint=yes\n\n# The minimum edit distance a name should have in order to be considered a\n# similar match for a missing member name.\nmissing-member-hint-distance=1\n\n# The total number of similar names that should be taken in consideration when\n# showing a hint for a missing member.\nmissing-member-max-choices=1\n\n\n[VARIABLES]\n\n# List of additional names supposed to be defined in builtins. Remember that\n# you should avoid to define new builtins when possible.\nadditional-builtins=\n\n# Tells whether unused global variables should be treated as a violation.\nallow-global-unused-variables=yes\n\n# List of strings which can identify a callback function by name. A callback\n# name must start or end with one of those strings.\ncallbacks=cb_,\n          _cb\n\n# A regular expression matching the name of dummy variables (i.e. expectedly\n# not used).\ndummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_\n\n# Argument names that match this expression will be ignored. Default to name\n# with leading underscore\nignored-argument-names=_.*|^ignored_|^unused_\n\n# Tells whether we should check for unused import in __init__ files.\ninit-import=no\n\n# List of qualified module names which can have objects that can redefine\n# builtins.\nredefining-builtins-modules=six.moves,past.builtins,future.builtins\n\n\n[CLASSES]\n\n# List of method names used to declare (i.e. assign) instance attributes.\ndefining-attr-methods=__init__,\n                      __new__,\n                      setUp\n\n# List of member names, which should be excluded from the protected access\n# warning.\nexclude-protected=_asdict,\n                  _fields,\n                  _replace,\n                  _source,\n                  _make\n\n# List of valid names for the first argument in a class method.\nvalid-classmethod-first-arg=cls\n\n# List of valid names for the first argument in a metaclass class method.\nvalid-metaclass-classmethod-first-arg=mcs\n\n\n[DESIGN]\n\n# Maximum number of arguments for function / method\nmax-args=5\n\n# Maximum number of attributes for a class (see R0902).\nmax-attributes=7\n\n# Maximum number of boolean expressions in a if statement\nmax-bool-expr=5\n\n# Maximum number of branch for function / method body\nmax-branches=20\n\n# Maximum number of locals for function / method body\nmax-locals=20\n\n# Maximum number of parents for a class (see R0901).\nmax-parents=7\n\n# Maximum number of public methods for a class (see R0904).\nmax-public-methods=20\n\n# Maximum number of return / yield for function / method body\nmax-returns=6\n\n# Maximum number of statements in function / method body\nmax-statements=50\n\n# Minimum number of public methods for a class (see R0903).\nmin-public-methods=2\n\n\n[IMPORTS]\n\n# Allow wildcard imports from modules that define __all__.\nallow-wildcard-with-all=no\n\n# Analyse import fallback blocks. This can be used to support both Python 2 and\n# 3 compatible code, which means that the block might have code that exists\n# only in one or another interpreter, leading to false positives when analysed.\nanalyse-fallback-blocks=no\n\n# Deprecated modules which should not be used, separated by a comma\ndeprecated-modules=regsub,\n                   TERMIOS,\n                   Bastion,\n                   rexec\n\n# Create a graph of external dependencies in the given file (report RP0402 must\n# not be disabled)\next-import-graph=\n\n# Create a graph of every (i.e. internal and external) dependencies in the\n# given file (report RP0402 must not be disabled)\nimport-graph=\n\n# Create a graph of internal dependencies in the given file (report RP0402 must\n# not be disabled)\nint-import-graph=\n\n# Force import order to recognize a module as part of the standard\n# compatibility libraries.\nknown-standard-library=\n\n# Force import order to recognize a module as part of a third party library.\nknown-third-party=enchant\n\n\n[EXCEPTIONS]\n\n# Exceptions that will emit a warning when being caught. Defaults to\n# \"Exception\"\novergeneral-exceptions=Exception\n\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.9\"\n\nmkdocs:\n  configuration: mkdocs.yml"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 shidenggui\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\n"
  },
  {
    "path": "Makefile",
    "content": "test:\n\tpytest -vx --cov=easytrader tests\n"
  },
  {
    "path": "Pipfile",
    "content": "[[source]]\nurl = \"http://mirrors.aliyun.com/pypi/simple/\"\nverify_ssl = false\nname = \"pypi\"\n\n[packages]\npywinauto = \"*\"\n\"bs4\" = \"*\"\nrequests = \"*\"\ndill = \"*\"\nclick = \"*\"\nsix = \"*\"\nflask = \"*\"\npillow = \"*\"\npytesseract = \"*\"\npandas = \"*\"\npyperclip = \"*\"\neasyutils = \"*\"\n\n[dev-packages]\npytest-cov = \"*\"\npre-commit = \"*\"\npytest = \"*\"\npylint = \"*\"\nmypy = \"*\"\nisort = \"*\"\nblack = \"==18.6b4\"\nipython = \"*\"\nbetter-exceptions = \"*\"\n\n[requires]\npython_version = \"3.6\"\n\n[scripts]\nsort_imports = \"bash -c 'isort \\\"$@\\\"; git add -u' --\"\nformat = \"bash -c 'black -l 79 \\\"$@\\\"; git add -u' --\"\nlint = \"pylint\"\ntype_check = \"mypy\"\ntest = \"bash -c 'pytest -vx --cov=easytrader tests'\"\nlock = \"bash -c 'pipenv lock -r > requirements.txt'\"\n"
  },
  {
    "path": "README.md",
    "content": "# easytrader\n\n[![Package](https://img.shields.io/pypi/v/easytrader.svg)](https://pypi.python.org/pypi/easytrader)\n[![License](https://img.shields.io/github/license/shidenggui/easytrader.svg)](https://github.com/shidenggui/easytrader/blob/master/LICENSE)\n\n* 进行股票量化交易\n* 通用的同花顺客户端模拟操作\n* 支持券商的 [miniqmt](https://easytrader.readthedocs.io/zh-cn/master/miniqmt/) 官方量化接口\n* 支持雪球组合调仓和跟踪\n* 支持远程操作客户端\n* 支持跟踪 `joinquant`, `ricequant` 的模拟交易\n\n\n### 微信群以及公众号\n\n欢迎大家扫码关注公众号「食灯鬼」，一起交流。进群可通过菜单加我好友，备注量化。\n\n![公众号二维码](https://camo.githubusercontent.com/6fad032c27b30b68a9d942ae77f8cc73933b95cea58e684657d31b94a300afd5/68747470733a2f2f67697465652e636f6d2f73686964656e676775692f6173736574732f7261772f6d61737465722f755069632f6d702d71722e706e67)\n\n若二维码因 Github 网络无法打开，请点击[公众号二维码](https://camo.githubusercontent.com/6fad032c27b30b68a9d942ae77f8cc73933b95cea58e684657d31b94a300afd5/68747470733a2f2f67697465652e636f6d2f73686964656e676775692f6173736574732f7261772f6d61737465722f755069632f6d702d71722e706e67)直接打开图片。\n\n### Author\n\n> Blog [@shidenggui](https://shidenggui.com) · Weibo [@食灯鬼](https://www.weibo.com/u/1651274491) · Twitter [@shidenggui](https://twitter.com/shidenggui)\n\n### 相关\n\n* [easyquotation 实时获取全市场股票行情](https://github.com/shidenggui/easyquotation)\n* [easyquant 简单的量化框架](https://github.com/shidenggui/easyquant)\n\n\n### 模拟交易\n\n* 雪球组合 by @[haogefeifei](https://github.com/haogefeifei)（[说明](docs/xueqiu.md)）\n\n### 使用文档\n\n[中文文档](https://easytrader.readthedocs.io/)\n"
  },
  {
    "path": "docs/follow.md",
    "content": "# 策略跟踪 \n\n## 跟踪 `joinquant` / `ricequant`  的模拟交易\n\n##### 1) 初始化跟踪的 trader\n\n这里以雪球为例, 也可以使用银河之类 `easytrader` 支持的券商\n\n```\nxq_user = easytrader.use('xq')\nxq_user.prepare('xq.json')\n```\n\n##### 2) 初始化跟踪 `joinquant` / `ricequant` 的 follower\n\n```\ntarget = 'jq'  # joinquant\ntarget = 'rq'  # ricequant\nfollower = easytrader.follower(target)\nfollower.login(user='rq/jq用户名', password='rq/jq密码')\n```\n\n##### 3) 连接 follower 和 trader\n\n##### joinquant\n```\nfollower.follow(xq_user, 'jq的模拟交易url')\n```\n\n注: jq的模拟交易url指的是对应模拟交易对应的可以查看持仓, 交易记录的页面, 类似 `https://www.joinquant.com/algorithm/live/index?backtestId=xxx`\n\n正常会输出\n\n![enjoy it](https://raw.githubusercontent.com/shidenggui/assets/master/easytrader/joinquant.jpg)\n\n注: 启动后发现跟踪策略无输出，那是因为今天模拟交易没有调仓或者接收到的调仓信号过期了，默认只处理120s内的信号，想要测试的可以用下面的命令：\n\n```python\njq_follower.follow(user, '模拟交易url',\n          trade_cmd_expire_seconds=100000000000, cmd_cache=False)\n```\n\n- trade_cmd_expire_seconds 默认处理多少秒内的信号\n\n- cmd_cache 是否读取已经执行过的命令缓存，以防止重复执行\n\n目录下产生的 cmd_cache.pk，是用来存储历史执行过的交易指令，防止在重启程序时重复执行交易过的指令，可以通过 `follower.follow(xxx, cmd_cache=False)` 来关闭。\n\n##### ricequant\n\n```\nfollower.follow(xq_user, run_id)\n```\n注：ricequant的run_id即PT列表中的ID。\n\n\n## 跟踪雪球的组合\n\n##### 1) 初始化跟踪的 trader\n\n同上\n\n##### 2) 初始化跟踪 雪球组合 的 follower\n\n```\nxq_follower = easytrader.follower('xq')\nxq_follower.login(cookies='雪球 cookies，登陆后获取，获取方式见 https://smalltool.github.io/2016/08/02/cookie/')\n```\n\n##### 3) 连接 follower 和 trader\n\n```\nxq_follower.follow(xq_user, 'xq组合ID，类似ZH123456', total_assets=100000)\n```\n\n\n注: 雪球组合是以百分比调仓的， 所以需要额外设置组合对应的资金额度\n\n* 这里可以设置 total_assets, 为当前组合的净值对应的总资金额度, 具体可以参考参数说明\n* 或者设置 initial_assets, 这时候总资金额度为 initial_assets * 组合净值\n\n* 雪球额外支持 adjust_sell 参数，决定是否根据用户的实际持仓数调整卖出股票数量，解决雪球根据百分比调仓时计算出的股数有偏差的问题。当卖出股票数大于实际持仓数时，调整为实际持仓数。目前仅在银河客户端测试通过。 当 users 为多个时，根据第一个 user 的持仓数决定\n\n\n#### 3. 多用户跟踪多策略\n\n```\nfollower.follow(users=[xq_user, yh_user], strategies=['组合1', '组合2'], total_assets=[10000, 10000])\n```\n\n#### 4. 其它与跟踪有关的问题\n\n使用市价单跟踪模式，目前仅支持银河\n\n```\nfollower.follow(***, entrust_prop='market')\n```\n\n调整下单间隔, 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足\n\n```\nfollower.follow(***, send_interval=30) # 设置下单间隔为 30 s\n```\n设置买卖时的滑点\n\n```\nfollower.follow(***, slippage=0.05) # 设置滑点为 5%\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "# 简介\n\n* 通用的同花顺客户端模拟操作\n* 支持券商的 [miniqmt](miniqmt.md) 官方量化接口\n* 支持雪球组合调仓和跟踪\n* 支持远程操作客户端\n* 支持跟踪 `joinquant`, `ricequant` 的模拟交易\n\n### 加微信群以及公众号\n\n欢迎大家扫码关注公众号\"食灯鬼\"，通过菜单加我好友，备注量化进群\n\n![JDRUhz](https://camo.githubusercontent.com/6fad032c27b30b68a9d942ae77f8cc73933b95cea58e684657d31b94a300afd5/68747470733a2f2f67697465652e636f6d2f73686964656e676775692f6173736574732f7261772f6d61737465722f755069632f6d702d71722e706e67)\n\n\n### 支持券商\n\n\n* 海通客户端(海通网上交易系统独立委托)\n* 华泰客户端(网上交易系统（专业版Ⅱ）)\n* 国金客户端(全能行证券交易终端PC版)\n* 通用同花顺客户端(同花顺免费版)\n* 其他券商专用同花顺客户端(需要手动登陆)\n\n\n### 模拟交易\n\n* 雪球组合 by @[haogefeifei](https://github.com/haogefeifei)（[说明](xueqiu.md)）\n\n\n\n### 作者\n\n> Blog [@shidenggui](https://shidenggui.com) · Weibo [@食灯鬼](https://www.weibo.com/u/1651274491) · Twitter [@shidenggui](https://twitter.com/shidenggui)\n>\n\n**其他作品**\n\n* [easyquotation 实时获取全市场股票行情](https://github.com/shidenggui/easyquotation)\n* [easyquant 简单的量化框架](https://github.com/shidenggui/easyqutant)\n\n\n"
  },
  {
    "path": "docs/install.md",
    "content": "# 安装\n\n### 同花顺客户端设置\n\n需要对客户端按以下设置，不然会导致下单时价格出错以及客户端超时锁定\n\n* 系统设置 > 界面设置: 界面不操作超时时间设为 0\n* 系统设置 > 交易设置: 默认买入价格/买入数量/卖出价格/卖出数量 都设置为 空\n\n同时客户端不能最小化也不能处于精简模式\n\n### 云端部署建议\n\n在云服务上部署时，使用自带的远程桌面会有问题，推荐使用 TightVNC\n\n### 登陆时的验证码识别\n\n券商如果登陆需要识别验证码的话需要安装 tesseract：\n\n* `tesseract` : 非 `pytesseract`, 需要单独安装, [地址](https://github.com/tesseract-ocr/tesseract/wiki),保证在命令行下 `tesseract` 可用\n\n或者你也可以手动登陆后在通过 `easytrader` 调用，此时 `easytrader` 在登陆过程中会直接识别到已登陆的窗口。\n\n### 安装\n\n```shell\npip install easytrader\n```\n\n### 升级\n\n```shell\npip install easytrader -U\n```\n\n"
  },
  {
    "path": "docs/miniqmt.md",
    "content": "# miniqmt\n\nminiqmt 是券商官方的低门槛 Python 量化交易接口，基于券商的讯投 QMT 服务。详情可以[进群](https://easytrader.readthedocs.io/zh-cn/master/#_2)交流。\n\n## 安装 miniqmt 组件\n\nminiqmt 功能依赖 `xtquant` 库，因为这个库比较大(100 MB+)，所以需要单独安装\n\n```python\npip install easytrader[miniqmt]\n``` \n\n## 引入\n\n```python\nimport easytrader\n```\n\n## 初始化客户端\n\n```python\nuser = easytrader.use('miniqmt')\n```\n\n## 连接 QMT 客户端\n\n需要通过 `connect` 方法连接到 QMT 客户端。\n\n**注意：登录 QMT 客户端时必须勾选极简模式/独立交易模式，否则无法连接**\n\n```python\nuser.connect(\n    miniqmt_path=r\"D:\\国金证券QMT交易端\\userdata_mini\",  # QMT 客户端下的 miniqmt 安装路径\n    stock_account=\"你的资金账号\",  # 资金账号\n    trader_callback=None, # 默认使用 `easytrader.miniqmt.DefaultXtQuantTraderCallback`\n)\n```\n\n**参数说明：**\n\n- `miniqmt_path`: QMT 客户端下的 miniqmt 安装路径，例如 `r\"D:\\国金证券QMT交易端\\userdata_mini\"`\n    - 注意：不建议安装在 C 盘。在 C 盘每次都需要用管理员权限运行客户端，才能正常连接\n- `stock_account`: 资金账号\n- `trader_callback`: 交易回调对象，默认使用 `easytrader.miniqmt.DefaultXtQuantTraderCallback`\n\n## 交易相关\n\n### 获取资金状况\n\n```python\nuser.balance\n\n# return\n# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%B5%84%E4%BA%A7xtasset\n[{\n  'total_asset': 1000000.0,  # 总资产\n  'market_value': 400000.0,  # 持仓市值\n  'cash': 600000.0,  # 可用资金\n  'frozen_cash': 0.0,  # 冻结资金\n  'account_type': 2,  # 账户类型\n  'account_id': '你的资金账号'  # 账户ID\n}]\n```\n\n### 获取持仓\n\n```python\nuser.position\n\n# return\n# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%8C%81%E4%BB%93xtposition\n[{'security': '162411',\n  'stock_code': '162411.SZ',\n  'volume': 100,\n  'can_use_volume': 100,\n  'open_price': 0.618,\n  'market_value': 63.8,\n  'frozen_volume': 0,\n  'on_road_volume': 0,\n  'yesterday_volume': 100,\n  'avg_price': 0.618,\n  'direction': 48,\n  'account_type': 2,\n  'account_id': '1111111111'}]\n\n```\n\n### 限价买入\n\n```python\nuser.buy('600036', price=35.5, amount=100)\n\n# return\n{'entrust_no': 123456}\n```\n\n**注意事项**\n\n- 成功发送委托后的订单编号为大于 0 的正整数，如果为 -1 表示委托失败，失败具体原因请查看 `DefaultXtQuantTraderCallback.on_order_error` 回调\n- 注：非交易时间下单可以拿到订单编号，但 `on_order_error` 回调会报错：\n  ```\n  下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]\n  ```\n\n### 限价卖出\n\n```python\nuser.sell('600036', price=36.0, amount=100)\n\n# return\n{'entrust_no': 123456}\n```\n\n### 市价买入\n\n```python\nuser.market_buy('600036', amount=100, ttype='对手方最优价格委托')\n\n# return\n{'entrust_no': 123456}\n```\n\n**市价委托类型（ttype）可选值**:\n\n深市可选:\n\n- 对手方最优价格委托（默认）\n- 本方最优价格委托\n- 即时成交剩余撤销委托\n- 最优五档即时成交剩余撤销\n- 全额成交或撤销委托\n\n沪市可选:\n\n- 对手方最优价格委托（默认）\n- 最优五档即时成交剩余撤销\n- 最优五档即时成交剩转限价\n- 本方最优价格委托\n\n### 市价卖出\n\n```python\nuser.market_sell('600036', amount=100, ttype='对手方最优价格委托')\n\n# return\n{'entrust_no': 123456}\n```\n\n### 撤单\n\n```python\nuser.cancel_entrust(123456)  # 传入之前买入或卖出时返回的订单编号\n\n# return\n{'success': True, 'message': 'success'} # 成功\n{'success': False, 'message': 'failed'} # 失败\n```\n\n### 查询当日委托\n\n```python\nuser.today_entrusts\n\n# return\n# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E5%A7%94%E6%89%98xtorder\n[{'security': '162411',\n  'stock_code': '162411.SZ',\n  'order_id': 3456,\n  'order_sysid': '1111',\n  'order_time': 1634278451,\n  'order_type': 23,\n  'order_type_name': '买入', # ['买入', '卖出']\n  'order_volume': 100,\n  'price_type': 50,\n  'price_type_name': '限价',\n  'price': 0.62,\n  'traded_volume': 100,\n  'traded_price': 0.613,\n  'order_status': 56,\n  'order_status_name': '已成', # ['已报', '已成', '部成', '已撤', '部撤']\n  'status_msg': '',\n  'offset_flag': 48,\n  'offset_flag_name': '买入', # ['买入', '卖出']\n  'strategy_name': '',\n  'order_remark': '',\n  'direction': 48,\n  'direction_name': '多', # ['多', '空']\n  'account_type': 2,\n  'account_id': '1111111111'}]\n```\n\n### 查询当日成交\n\n```python\nuser.today_trades\n\n# return\n# qmt 官网文档 https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%88%90%E4%BA%A4xttrade\n[{'security': '162411',\n  'stock_code': '162411.SZ',\n  'traded_id': '0303222200422222',\n  'traded_time': 1634278451,\n  'traded_price': 0.613,\n  'traded_volume': 100,\n  'traded_amount': 61.3,\n  'order_id': 1111,\n  'order_type': 23,\n  'order_type_name': '买入',\n  'offset_flag': 48,\n  'offset_flag_name': '买入',\n  'account_id': '1111111111',\n  'account_type': 2,\n  'order_sysid': '1111',\n  'strategy_name': '',\n  'order_remark': ''}]\n```\n\n\n## 进阶功能\n\n### 获取原始交易对象\n\n通过获取原始对象，可以直接调用 miniqmt 的接口进行更多高级操作，具体请参考 [miniqmt 官方文档](https://dict.thinktrader.net/nativeApi/xttrader.html)\n\n```python\n# 获取 XtQuantTrader 对象\ntrader = user.trader\n\n# 获取 StockAccount 对象\naccount = user.account\n```\n\n\n### 2. 交易回调处理\n\nMiniqmtTrader 默认使用 `DefaultXtQuantTraderCallback` 类处理交易回调，但您可以通过继承 `XtQuantTraderCallback` 类来创建自定义回调处理：\n\n```python\nfrom xtquant.xttrader import XtQuantTraderCallback\n\nclass MyTraderCallback(XtQuantTraderCallback):\n    def on_disconnected(self):\n        print(\"连接断开\")\n\n    def on_account_status(self, status):\n        print(f\"账户状态: {status.account_id}, 状态: {status.status}\")\n\n    def on_stock_order(self, order):\n        print(f\"委托回调: {order.stock_code}, 状态: {order.order_status}\")\n\n    def on_stock_trade(self, trade):\n        print(f\"成交回调: {trade.stock_code}, 价格: {trade.traded_price}\")\n\n    def on_order_error(self, order_error):\n        print(f\"下单失败: {order_error.order_id}, 错误: {order_error.error_msg}\")\n\n    def on_cancel_error(self, cancel_error):\n        print(f\"撤单失败: {cancel_error.order_id}, 错误: {cancel_error.error_msg}\")\n\n# 连接时使用自定义回调\nuser.connect(\n    miniqmt_path=r\"D:\\国金证券QMT交易端\\userdata_mini\",\n    stock_account=\"你的资金账号\",\n    trader_callback=MyTraderCallback()\n)\n```"
  },
  {
    "path": "docs/remote.md",
    "content": "# 远端服务模式\n\n远端服务模式是交易服务端和量化策略端分离的模式。\n\n**交易服务端**通常是有固定`IP`地址的云服务器，该服务器上运行着`easytrader`交易服务。而**量化策略端**可能是`JoinQuant、RiceQuant、Vn.Py`，物理上与交易服务端不在同一台电脑上。交易服务端被动或主动获取交易信号，并驱动**交易软件**（交易软件包括运行在同一服务器上的下单软件，比如同花顺`xiadan.exe`，或者运行在另一台服务器上的雪球`xq`）。\n\n\n## 交易服务端——启动服务\n\n```python\nfrom easytrader import server\n\nserver.run(port=1430) # 默认端口为 1430\n```\n\n## 量化策略端——调用服务\n\n```python\nfrom easytrader import remoteclient\n\nuser = remoteclient.use('使用客户端类型，可选 yh_client, ht_client, ths, xq等', host='服务器ip', port='服务器端口，默认为1430')\n\nuser.buy(......)\n\nuser.sell(......)\n```\n\n\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# 使用\n\n## 引入\n\n```python\nimport easytrader\n```\n\n## 设置同花顺客户端类型\n\n**通用同花顺客户端**\n\n```python\nuser = easytrader.use('universal_client') \n```\n\n注: 通用同花顺客户端是指同花顺官网提供的客户端软件内的下单程序，内含对多个券商的交易支持，适用于券商不直接提供同花顺客户端时的后备方案。\n\n**其他券商专用同花顺客户端**\n\n```python\nuser = easytrader.use('ths')\n```\n\n注: 其他券商专用同花顺客户端是指对应券商官网提供的基于同花顺修改的软件版本，类似银河的双子星(同花顺版本)，国金证券网上交易独立下单程序（核新PC版）等。\n\n\n**雪球组合**\n\n```python\nuser = easytrader.use('xq')\n```\n\n**国金客户端**\n\n```python\nuser = easytrader.use('gj_client') \n```\n\n**海通客户端**\n\n```python\nuser = easytrader.use('htzq_client')\n```\n\n**华泰客户端**\n\n```python\nuser = easytrader.use('ht_client')\n```\n\n\n## 启动并连接客户端\n\n### （一）其他券商专用同花顺客户端\n\n其他券商专用同花顺客户端不支持自动登录，需要先手动登录。\n\n请手动打开并登录客户端后，运用connect函数连接客户端。\n\n```python\nuser.connect(r'客户端xiadan.exe路径') # 类似 r'C:\\htzqzyb2\\xiadan.exe'\n```\n\n### （二）通用同花顺客户端\n\n需要先手动登录一次：添加券商，填入账户号、密码、验证码，勾选“保存密码”\n\n第一次登录后，上述信息被缓存，可以调用prepare函数自动登录（仅需账户号、客户端路径，密码随意输入）。\n\n### （三）其它\n\n非同花顺的客户端，可以调用prepare函数自动登录。\n\n调用prepare时所需的参数，可以通过`函数参数` 或 `配置文件` 赋予。\n\n**1. 函数参数(推荐)**\n\n```\nuser.prepare(user='用户名', password='雪球、银河客户端为明文密码', comm_password='华泰通讯密码，其他券商不用')\n```\n\n注: 雪球比较特殊，见下列配置文件格式\n\n**2. 配置文件**\n\n```python\nuser.prepare('/path/to/your/yh_client.json')  # 配置文件路径\n```\n\n注: 配置文件需自己用编辑器编辑生成, **请勿使用记事本**, 推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/) 。\n\n**配置文件格式如下：**\n\n银河/国金客户端\n\n```\n{\n  \"user\": \"用户名\",\n  \"password\": \"明文密码\"\n}\n\n```\n\n华泰客户端\n\n```\n{\n   \"user\": \"华泰用户名\",\n   \"password\": \"华泰明文密码\",\n   \"comm_password\": \"华泰通讯密码\"\n}\n\n```\n\n雪球\n\n```\n{\n  \"cookies\": \"雪球 cookies，登陆后获取，获取方式见 https://smalltool.github.io/2016/08/02/cookie/\",\n  \"portfolio_code\": \"组合代码(例:ZH818559)\",\n  \"portfolio_market\": \"交易市场(例:us 或者 cn 或者 hk)\"\n}\n```\n\n## 交易相关\n\n有些客户端无法通过默认方法输入文本，可以通过开启 type_keys 的方法绕过，开启方式\n\n```python\nuser.enable_type_keys_for_editor()\n```\n\n###  获取资金状况\n\n```python\nuser.balance\n\n# return\n[{'参考市值': 21642.0,\n  '可用资金': 28494.21,\n  '币种': '0',\n  '总资产': 50136.21,\n  '股份参考盈亏': -90.21,\n  '资金余额': 28494.21,\n  '资金帐号': 'xxx'}]\n```\n\n### 获取持仓\n\n```python\nuser.position\n\n# return\n[{'买入冻结': 0,\n  '交易市场': '沪A',\n  '卖出冻结': '0',\n  '参考市价': 4.71,\n  '参考市值': 10362.0,\n  '参考成本价': 4.672,\n  '参考盈亏': 82.79,\n  '当前持仓': 2200,\n  '盈亏比例(%)': '0.81%',\n  '股东代码': 'xxx',\n  '股份余额': 2200,\n  '股份可用': 2200,\n  '证券代码': '601398',\n  '证券名称': '工商银行'}]\n```\n\n### 买入\n\n```python\nuser.buy('162411', price=0.55, amount=100)\n\n# return\n{'entrust_no': 'xxxxxxxx'}\n```\n\n注: 系统可以配置是否返回成交回报。如果没配的话默认返回 `{\"message\": \"success\"}`\n\n### 卖出\n\n```python\nuser.sell('162411', price=0.55, amount=100)\n\n# return\n{'entrust_no': 'xxxxxxxx'}\n```\n\n\n### 撤单\n\n```python\nuser.cancel_entrust('buy/sell 获取的 entrust_no')\n\n# return\n{'message': 'success'}\n```\n\n### 查询当日成交\n\n```python\nuser.today_trades\n\n# return\n[{'买卖标志': '买入',\n  '交易市场': '深A',\n  '委托序号': '12345',\n  '成交价格': 0.626,\n  '成交数量': 100,\n  '成交日期': '20170313',\n  '成交时间': '09:50:30',\n  '成交金额': 62.60,\n  '股东代码': 'xxx',\n  '证券代码': '162411',\n  '证券名称': '华宝油气'}]\n```\n\n### 查询当日委托\n\n```python\nuser.today_entrusts\n\n# return\n[{'买卖标志': '买入',\n  '交易市场': '深A',\n  '委托价格': 0.627,\n  '委托序号': '111111',\n  '委托数量': 100,\n  '委托日期': '20170313',\n  '委托时间': '09:50:30',\n  '成交数量': 100,\n  '撤单数量': 0,\n  '状态说明': '已成',\n  '股东代码': 'xxxxx',\n  '证券代码': '162411',\n  '证券名称': '华宝油气'},\n {'买卖标志': '买入',\n  '交易市场': '深A',\n  '委托价格': 0.6,\n  '委托序号': '1111',\n  '委托数量': 100,\n  '委托日期': '20170313',\n  '委托时间': '09:40:30',\n  '成交数量': 0,\n  '撤单数量': 100,\n  '状态说明': '已撤',\n  '股东代码': 'xxx',\n  '证券代码': '162411',\n  '证券名称': '华宝油气'}]\n```\n\n\n### 查询今日可申购新股\n\n```python\nfrom easytrader.utils.stock import get_today_ipo_data\nget_today_ipo_data()\n\n# return\n[{'stock_code': '股票代码',\n  'stock_name': '股票名称',\n  'price': 发行价,\n  'apply_code': '申购代码'}]\n```\n\n### 一键打新\n\n```python\nuser.auto_ipo()\n```\n\n### 刷新数据\n\n```python\nuser.refresh()\n```\n\n### 雪球组合比例调仓 \n\n```python\nuser.adjust_weight('股票代码', 目标比例)\n```\n\n例如 `user.adjust_weight('000001', 10)`是将平安银行在组合中的持仓比例调整到10%。\n\n## 退出客户端软件\n\n```python\nuser.exit()\n```\n\n## 常见问题\n\n### 某些同花顺客户端不允许拷贝 `Grid` 数据\n\n现在默认获取 `Grid` 数据的策略是通过剪切板拷贝，有些券商不允许这种方式，导致无法获取持仓等数据。为解决此问题，额外实现了一种通过将 `Grid` 数据存为文件再读取的策略，\n使用方式如下:\n\n```python\nfrom easytrader import grid_strategies\n\nuser.grid_strategy = grid_strategies.Xls\n```\n\n### 通过工具栏刷新按钮刷新数据\n\n当前的刷新数据方式是通过切换菜单栏实现，通用但是比较缓慢，可以选择通过点击工具栏的刷新按钮来刷新\n\n```python\nfrom easytrader import refresh_strategies\n\n# refresh_btn_index 指的是刷新按钮在工具栏的排序，默认为第四个，根据客户端实际情况调整\nuser.refresh_strategy = refresh_strategies.Toolbar(refresh_btn_index=4)\n```\n\n### 无法保存对应的 xls 文件\n\n有些系统默认的临时文件目录过长，使用 xls 策略时无法正常保存，可通过如下方式修改为自定义目录\n\n```\nuser.grid_strategy_instance.tmp_folder = 'C:\\\\custom_folder'\n```\n\n### 如何关闭 debug 日志的输出\n\n```python\nuser = easytrader.use('yh', debug=False)\n\n```\n\n\n# 编辑配置文件，运行后出现 `json` 解码报错\n\n\n出现如下错误\n\n```python\nraise JSONDecodeError(\"Expecting value\", s, err.value) from None\n\nJSONDecodeError: Expecting value\n```\n\n请勿使用 `记事本` 编辑账户的 `json` 配置文件，推荐使用 [notepad++](https://notepad-plus-plus.org/zh/) 或者 [sublime text](http://www.sublimetext.com/)\n\n"
  },
  {
    "path": "docs/xueqiu.md",
    "content": "# 雪球组合模拟交易\n\n因为雪球组合是按比例调仓的,所以模拟成券商实盘接口会有一些要注意的问题\n\n* 接口基本与其他券商接口调用参数返回一致\n* 委托单不支持挂高挂低(开盘时间都是直接市价成交的)\n* 初始资金是按组合净值 1:1000000 换算来的, 可以通过 `easytrader.use('xq', initial_assets=初始资金值)` 来调整\n* 委托单的委托价格和委托数量目前换算回来都是按1手拆的(雪球是按比例调仓的)\n* 持仓价格和持仓数量问题同上, 但持股市值是对的.\n* 一些不合理的操作会直接抛TradeError,注意看错误信息\n          \n----------------\n20160909 新增函数adjust_weight，用于雪球组合比例调仓\n             \nadjust_weight函数包含两个参数，stock_code 指定调仓股票代码，weight 指定调仓比例      \n\n"
  },
  {
    "path": "easytrader/__init__.py",
    "content": "# -*- coding: utf-8 -*-\nimport urllib3\n\nfrom easytrader import exceptions\nfrom easytrader.api import use, follower\nfrom easytrader.log import logger\n\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\n__version__ = \"0.23.7\"\n__author__ = \"shidenggui\"\n"
  },
  {
    "path": "easytrader/api.py",
    "content": "# -*- coding: utf-8 -*-\nimport logging\nimport sys\n\nimport six\n\nfrom easytrader.joinquant_follower import JoinQuantFollower\nfrom easytrader.log import logger\nfrom easytrader.ricequant_follower import RiceQuantFollower\nfrom easytrader.xq_follower import XueQiuFollower\nfrom easytrader.xqtrader import XueQiuTrader\n\nif sys.version_info <= (3, 5):\n    raise TypeError(\"不支持 Python3.5 及以下版本，请升级\")\n\n\ndef use(broker, debug=False, **kwargs):\n    \"\"\"用于生成特定的券商对象\n    :param broker: 券商名支持 \n        例如 ['miniqmt', 'xq', '雪球', 'gj_client', '国金客户端', \"universal_client\", \"通用同花顺客户端\", \"ths\", \"同花顺客户端\"] 等\n    :param debug: 控制 debug 日志的显示, 默认为 False\n    :param initial_assets: [雪球参数] 控制雪球初始资金，默认为一百万\n    :return the class of trader\n\n    Usage::\n\n        >>> import easytrader\n        >>> user = easytrader.use('xq')\n        >>> user.prepare('xq.json')\n    \"\"\"\n    if debug:\n        logger.setLevel(logging.DEBUG)\n\n    if broker.lower() in [\"xq\", \"雪球\"]:\n        return XueQiuTrader(**kwargs)\n\n    if broker.lower() in [\"yh_client\", \"银河客户端\"]:\n        from .yh_clienttrader import YHClientTrader\n\n        return YHClientTrader()\n\n    if broker.lower() in [\"ht_client\", \"华泰客户端\"]:\n        from .ht_clienttrader import HTClientTrader\n\n        return HTClientTrader()\n\n    if broker.lower() in [\"wk_client\", \"五矿客户端\"]:\n        from easytrader.wk_clienttrader import WKClientTrader\n\n        return WKClientTrader()\n\n    if broker.lower() in [\"htzq_client\", \"海通证券客户端\"]:\n        from easytrader.htzq_clienttrader import HTZQClientTrader\n\n        return HTZQClientTrader()\n\n    if broker.lower() in [\"gj_client\", \"国金客户端\"]:\n        from .gj_clienttrader import GJClientTrader\n\n        return GJClientTrader()\n\n    if broker.lower() in [\"gf_client\", \"广发客户端\"]:\n        from .gf_clienttrader import GFClientTrader\n\n        return GFClientTrader()\n\n    if broker.lower() in [\"universal_client\", \"通用同花顺客户端\"]:\n        from easytrader.universal_clienttrader import UniversalClientTrader\n\n        return UniversalClientTrader()\n\n    if broker.lower() in [\"ths\", \"同花顺客户端\"]:\n        from .clienttrader import ClientTrader\n\n        return ClientTrader()\n    \n    if broker.lower() in [\"miniqmt\"]:\n        try:\n            import xtquant\n        except:\n            logger.error(\"miniqmt 相关组件 xtqimt 未安装, 请执行 pip install easytrader[xtquant]安装\")\n        from easytrader.miniqmt.miniqmt_trader import MiniqmtTrader\n\n        return MiniqmtTrader()\n\n    raise NotImplementedError\n\n\ndef follower(platform, **kwargs):\n    \"\"\"用于生成特定的券商对象\n    :param platform:平台支持 ['jq', 'joinquant', '聚宽’]\n    :param initial_assets: [雪球参数] 控制雪球初始资金，默认为一万,\n        总资金由 initial_assets * 组合当前净值 得出\n    :param total_assets: [雪球参数] 控制雪球总资金，无默认值,\n        若设置则覆盖 initial_assets\n    :return the class of follower\n\n    Usage::\n\n        >>> import easytrader\n        >>> user = easytrader.use('xq')\n        >>> user.prepare('xq.json')\n        >>> jq = easytrader.follower('jq')\n        >>> jq.login(user='username', password='password')\n        >>> jq.follow(users=user, strategies=['strategies_link'])\n    \"\"\"\n    if platform.lower() in [\"rq\", \"ricequant\", \"米筐\"]:\n        return RiceQuantFollower()\n    if platform.lower() in [\"jq\", \"joinquant\", \"聚宽\"]:\n        return JoinQuantFollower()\n    if platform.lower() in [\"xq\", \"xueqiu\", \"雪球\"]:\n        return XueQiuFollower(**kwargs)\n    raise NotImplementedError\n"
  },
  {
    "path": "easytrader/clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\nimport abc\nimport functools\nimport logging\nimport os\nimport re\nimport sys\nimport time\nfrom typing import Type, Union\n\nimport hashlib, binascii\n\nimport easyutils\nfrom pywinauto import findwindows, timings\n\nfrom easytrader import grid_strategies, pop_dialog_handler, refresh_strategies\nfrom easytrader.config import client\nfrom easytrader.grid_strategies import IGridStrategy\nfrom easytrader.log import logger\nfrom easytrader.refresh_strategies import IRefreshStrategy\nfrom easytrader.utils.misc import file2dict\nfrom easytrader.utils.perf import perf_clock\n\nif not sys.platform.startswith(\"darwin\"):\n    import pywinauto\n    import pywinauto.clipboard\n\nclass IClientTrader(abc.ABC):\n    @property\n    @abc.abstractmethod\n    def app(self):\n        \"\"\"Return current app instance\"\"\"\n        pass\n\n    @property\n    @abc.abstractmethod\n    def main(self):\n        \"\"\"Return current main window instance\"\"\"\n        pass\n\n    @property\n    @abc.abstractmethod\n    def config(self):\n        \"\"\"Return current config instance\"\"\"\n        pass\n\n    @abc.abstractmethod\n    def wait(self, seconds: float):\n        \"\"\"Wait for operation return\"\"\"\n        pass\n\n    @abc.abstractmethod\n    def refresh(self):\n        \"\"\"Refresh data\"\"\"\n        pass\n\n    @abc.abstractmethod\n    def is_exist_pop_dialog(self):\n        pass\n\n\nclass ClientTrader(IClientTrader):\n    _editor_need_type_keys = False\n    # The strategy to use for getting grid data\n    grid_strategy: Union[IGridStrategy, Type[IGridStrategy]] = grid_strategies.Copy\n    _grid_strategy_instance: IGridStrategy = None\n    refresh_strategy: IRefreshStrategy = refresh_strategies.Switch()\n\n    def enable_type_keys_for_editor(self):\n        \"\"\"\n        有些客户端无法通过 set_edit_text 方法输入内容，可以通过使用 type_keys 方法绕过\n        \"\"\"\n        self._editor_need_type_keys = True\n\n    @property\n    def grid_strategy_instance(self):\n        if self._grid_strategy_instance is None:\n            self._grid_strategy_instance = (\n                self.grid_strategy\n                if isinstance(self.grid_strategy, IGridStrategy)\n                else self.grid_strategy()\n            )\n            self._grid_strategy_instance.set_trader(self)\n        return self._grid_strategy_instance\n\n    def __init__(self):\n        self._config = client.create(self.broker_type)\n        self._app = None\n        self._main = None\n        self._toolbar = None\n\n    @property\n    def app(self):\n        return self._app\n\n    @property\n    def main(self):\n        return self._main\n\n    @property\n    def config(self):\n        return self._config\n\n    def connect(self, exe_path=None, **kwargs):\n        \"\"\"\n        直接连接登陆后的客户端\n        :param exe_path: 客户端路径类似 r'C:\\\\htzqzyb2\\\\xiadan.exe', 默认 r'C:\\\\htzqzyb2\\\\xiadan.exe'\n        :return:\n        \"\"\"\n        connect_path = exe_path or self._config.DEFAULT_EXE_PATH\n        if connect_path is None:\n            raise ValueError(\n                \"参数 exe_path 未设置，请设置客户端对应的 exe 地址,类似 C:\\\\客户端安装目录\\\\xiadan.exe\"\n            )\n\n        self._app = pywinauto.Application().connect(path=connect_path, timeout=10)\n        self._close_prompt_windows()\n        self._main = self._app.top_window()\n        self._init_toolbar()\n\n    @property\n    def broker_type(self):\n        return \"ths\"\n\n    @property\n    def balance(self):\n        self._switch_left_menus([\"查询[F4]\", \"资金股票\"])\n\n        return self._get_balance_from_statics()\n\n    def _init_toolbar(self):\n        self._toolbar = self._main.child_window(class_name=\"ToolbarWindow32\")\n\n    def _get_balance_from_statics(self):\n        result = {}\n        for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items():\n            result[key] = float(\n                self._main.child_window(\n                    control_id=control_id, class_name=\"Static\"\n                ).window_text()\n            )\n        return result\n\n    @property\n    def position(self):\n        self._switch_left_menus([\"查询[F4]\", \"资金股票\"])\n\n        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)\n\n    @property\n    def today_entrusts(self):\n        self._switch_left_menus([\"查询[F4]\", \"当日委托\"])\n\n        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)\n\n    @property\n    def today_trades(self):\n        self._switch_left_menus([\"查询[F4]\", \"当日成交\"])\n\n        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)\n\n    @property\n    def cancel_entrusts(self):\n        self.refresh()\n        self._switch_left_menus([\"撤单[F3]\"])\n\n        return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)\n\n    @perf_clock\n    def cancel_entrust(self, entrust_no):\n        self.refresh()\n        for i, entrust in enumerate(self.cancel_entrusts):\n            if entrust[self._config.CANCEL_ENTRUST_ENTRUST_FIELD] == entrust_no:\n                self._cancel_entrust_by_double_click(i)\n                return self._handle_pop_dialogs()\n        return {\"message\": \"委托单状态错误不能撤单, 该委托单可能已经成交或者已撤\"}\n\n    def cancel_all_entrusts(self):\n        self.refresh()\n        self._switch_left_menus([\"撤单[F3]\"])\n\n        # 点击全部撤销控件\n        self._app.top_window().child_window(\n            control_id=self._config.TRADE_CANCEL_ALL_ENTRUST_CONTROL_ID, class_name=\"Button\", title_re=\"\"\"全撤.*\"\"\"\n        ).click()\n        self.wait(0.2)\n\n        # 等待出现 确认兑换框\n        if self.is_exist_pop_dialog():\n            # 点击是 按钮\n            w = self._app.top_window()\n            if w is not None:\n                btn = w[\"是(Y)\"]\n                if btn is not None:\n                    btn.click()\n                    self.wait(0.2)\n\n        # 如果出现了确认窗口\n        self.close_pop_dialog()\n\n    @perf_clock\n    def repo(self, security, price, amount, **kwargs):\n        self._switch_left_menus([\"债券回购\", \"融资回购（正回购）\"])\n\n        return self.trade(security, price, amount)\n\n    @perf_clock\n    def reverse_repo(self, security, price, amount, **kwargs):\n        self._switch_left_menus([\"债券回购\", \"融劵回购（逆回购）\"])\n\n        return self.trade(security, price, amount)\n\n    @perf_clock\n    def buy(self, security, price, amount, **kwargs):\n        self._switch_left_menus([\"买入[F1]\"])\n\n        return self.trade(security, price, amount)\n\n    @perf_clock\n    def sell(self, security, price, amount, **kwargs):\n        self._switch_left_menus([\"卖出[F2]\"])\n\n        return self.trade(security, price, amount)\n\n    @perf_clock\n    def market_buy(self, security, amount, ttype=None, limit_price=None, **kwargs):\n        \"\"\"\n        市价买入\n        :param security: 六位证券代码\n        :param amount: 交易数量\n        :param ttype: 市价委托类型，默认客户端默认选择，\n                     深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销']\n                     沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价']\n        :param limit_price: 科创板 限价\n\n        :return: {'entrust_no': '委托单号'}\n        \"\"\"\n        self._switch_left_menus([\"市价委托\", \"买入\"])\n\n        return self.market_trade(security, amount, ttype, limit_price=limit_price)\n\n    @perf_clock\n    def market_sell(self, security, amount, ttype=None, limit_price=None, **kwargs):\n        \"\"\"\n        市价卖出\n        :param security: 六位证券代码\n        :param amount: 交易数量\n        :param ttype: 市价委托类型，默认客户端默认选择，\n                     深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销']\n                     沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价']\n        :param limit_price: 科创板 限价\n        :return: {'entrust_no': '委托单号'}\n        \"\"\"\n        self._switch_left_menus([\"市价委托\", \"卖出\"])\n\n        return self.market_trade(security, amount, ttype, limit_price=limit_price)\n\n    def market_trade(self, security, amount, ttype=None, limit_price=None, **kwargs):\n        \"\"\"\n        市价交易\n        :param security: 六位证券代码\n        :param amount: 交易数量\n        :param ttype: 市价委托类型，默认客户端默认选择，\n                     深市可选 ['对手方最优价格', '本方最优价格', '即时成交剩余撤销', '最优五档即时成交剩余 '全额成交或撤销']\n                     沪市可选 ['最优五档成交剩余撤销', '最优五档成交剩余转限价']\n\n        :return: {'entrust_no': '委托单号'}\n        \"\"\"\n        code = security[-6:]\n        self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code)\n        if ttype is not None:\n            retry = 0\n            retry_max = 10\n            while retry < retry_max:\n                try:\n                    self._set_market_trade_type(ttype)\n                    break\n                except:\n                    retry += 1\n                    self.wait(0.1)\n        self._set_market_trade_params(security, amount, limit_price=limit_price)\n        self._submit_trade()\n\n        return self._handle_pop_dialogs(\n            handler_class=pop_dialog_handler.TradePopDialogHandler\n        )\n\n    def _set_market_trade_type(self, ttype):\n        \"\"\"根据选择的市价交易类型选择对应的下拉选项\"\"\"\n        selects = self._main.child_window(\n            control_id=self._config.TRADE_MARKET_TYPE_CONTROL_ID, class_name=\"ComboBox\"\n        )\n        for i, text in enumerate(selects.texts()):\n            # skip 0 index, because 0 index is current select index\n            if i == 0:\n                if re.search(ttype, text):  # 当前已经选中\n                    return\n                else:\n                    continue\n            if re.search(ttype, text):\n                selects.select(i - 1)\n                return\n        raise TypeError(\"不支持对应的市价类型: {}\".format(ttype))\n\n    def _set_stock_exchange_type(self, ttype):\n        \"\"\"根据选择的市价交易类型选择对应的下拉选项\"\"\"\n        selects = self._main.child_window(\n            control_id=self._config.TRADE_STOCK_EXCHANGE_CONTROL_ID, class_name=\"ComboBox\"\n        )\n\n        for i, text in enumerate(selects.texts()):\n            # skip 0 index, because 0 index is current select index\n            if i == 0:\n                if ttype.strip() == text.strip():  # 当前已经选中\n                    return\n                else:\n                    continue\n            if ttype.strip() == text.strip():\n                selects.select(i - 1)\n                return\n        raise TypeError(\"不支持对应的市场类型: {}\".format(ttype))\n\n    def auto_ipo(self):\n        self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH)\n\n        stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)\n\n        if len(stock_list) == 0:\n            return {\"message\": \"今日无新股\"}\n        invalid_list_idx = [\n            i for i, v in enumerate(stock_list) if v[self.config.AUTO_IPO_NUMBER] <= 0\n        ]\n\n        if len(stock_list) == len(invalid_list_idx):\n            return {\"message\": \"没有发现可以申购的新股\"}\n\n        self._click(self._config.AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID)\n        self.wait(0.1)\n\n        for row in invalid_list_idx:\n            self._click_grid_by_row(row)\n        self.wait(0.1)\n\n        self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID)\n        self.wait(0.1)\n\n        return self._handle_pop_dialogs()\n\n    def _click_grid_by_row(self, row):\n        x = self._config.COMMON_GRID_LEFT_MARGIN\n        y = (\n            self._config.COMMON_GRID_FIRST_ROW_HEIGHT\n            + self._config.COMMON_GRID_ROW_HEIGHT * row\n        )\n        self._app.top_window().child_window(\n            control_id=self._config.COMMON_GRID_CONTROL_ID,\n            class_name=\"CVirtualGridCtrl\",\n        ).click(coords=(x, y))\n\n    @perf_clock\n    def is_exist_pop_dialog(self):\n        self.wait(0.5)  # wait dialog display\n        try:\n            return (\n                self._main.wrapper_object() != self._app.top_window().wrapper_object()\n            )\n        except (\n            findwindows.ElementNotFoundError,\n            timings.TimeoutError,\n            RuntimeError,\n        ) as ex:\n            logger.exception(\"check pop dialog timeout\")\n            return False\n\n    @perf_clock\n    def close_pop_dialog(self):\n        try:\n            if self._main.wrapper_object() != self._app.top_window().wrapper_object():\n                w = self._app.top_window()\n                if w is not None:\n                    w.close()\n                    self.wait(0.2)\n        except (\n                findwindows.ElementNotFoundError,\n                timings.TimeoutError,\n                RuntimeError,\n        ) as ex:\n            pass\n\n    def _run_exe_path(self, exe_path):\n        return os.path.join(os.path.dirname(exe_path), \"xiadan.exe\")\n\n    def wait(self, seconds):\n        time.sleep(seconds)\n\n    def exit(self):\n        self._app.kill()\n\n    def _close_prompt_windows(self):\n        self.wait(1)\n        for window in self._app.windows(class_name=\"#32770\", visible_only=True):\n            title = window.window_text()\n            if title != self._config.TITLE:\n                logging.info(\"close window %s\" % title)\n                window.close()\n                self.wait(0.2)\n        self.wait(1)\n\n    def close_pormpt_window_no_wait(self):\n        for window in self._app.windows(class_name=\"#32770\"):\n            if window.window_text() != self._config.TITLE:\n                window.close()\n\n    def trade(self, security, price, amount):\n        self._set_trade_params(security, price, amount)\n\n        self._submit_trade()\n\n        return self._handle_pop_dialogs(\n            handler_class=pop_dialog_handler.TradePopDialogHandler\n        )\n\n    def _click(self, control_id):\n        self._app.top_window().child_window(\n            control_id=control_id, class_name=\"Button\"\n        ).click()\n\n    @perf_clock\n    def _submit_trade(self):\n        time.sleep(0.2)\n        self._main.child_window(\n            control_id=self._config.TRADE_SUBMIT_CONTROL_ID, class_name=\"Button\"\n        ).click()\n\n    @perf_clock\n    def __get_top_window_pop_dialog(self):\n        return self._app.top_window().window(\n            control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID\n        )\n\n    @perf_clock\n    def _get_pop_dialog_title(self):\n        return (\n            self._app.top_window()\n            .child_window(control_id=self._config.POP_DIALOD_TITLE_CONTROL_ID)\n            .window_text()\n        )\n\n    def _set_trade_params(self, security, price, amount):\n        code = security[-6:]\n\n        self._type_edit_control_keys(self._config.TRADE_SECURITY_CONTROL_ID, code)\n\n        # wait security input finish\n        self.wait(0.1)\n\n        # 设置交易所\n        # if security.lower().startswith(\"sz\"):\n        #     self._set_stock_exchange_type(\"深圳Ａ股\")\n        # if security.lower().startswith(\"sh\"):\n        #     self._set_stock_exchange_type(\"上海Ａ股\")\n        #\n        # self.wait(0.1)\n\n        self._type_edit_control_keys(\n            self._config.TRADE_PRICE_CONTROL_ID,\n            easyutils.round_price_by_code(price, code),\n        )\n        self._type_edit_control_keys(\n            self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))\n        )\n\n    def _set_market_trade_params(self, security, amount, limit_price=None):\n        self._type_edit_control_keys(\n            self._config.TRADE_AMOUNT_CONTROL_ID, str(int(amount))\n        )\n        self.wait(0.1)\n        price_control = None\n        if str(security).startswith(\"68\"):  # 科创板存在限价\n            try:\n                price_control = self._main.child_window(\n                    control_id=self._config.TRADE_PRICE_CONTROL_ID, class_name=\"Edit\"\n                )\n            except:\n                pass\n        if price_control is not None:\n            price_control.set_edit_text(limit_price)\n\n    def _get_grid_data(self, control_id):\n        return self.grid_strategy_instance.get(control_id)\n\n    def _type_keys(self, control_id, text):\n        self._main.child_window(control_id=control_id, class_name=\"Edit\").set_edit_text(\n            text\n        )\n\n    def _type_edit_control_keys(self, control_id, text):\n        if not self._editor_need_type_keys:\n            self._main.child_window(\n                control_id=control_id, class_name=\"Edit\"\n            ).set_edit_text(text)\n        else:\n            editor = self._main.child_window(control_id=control_id, class_name=\"Edit\")\n            editor.select()\n            editor.type_keys(text)\n\n    def type_edit_control_keys(self, editor, text):\n        if not self._editor_need_type_keys:\n            editor.set_edit_text(text)\n        else:\n            editor.select()\n            editor.type_keys(text)\n\n    def _collapse_left_menus(self):\n        items = self._get_left_menus_handle().roots()\n        for item in items:\n            item.collapse()\n\n    @perf_clock\n    def _switch_left_menus(self, path, sleep=0.2):\n        self.close_pop_dialog()\n        self._get_left_menus_handle().get_item(path).select()\n        self._app.top_window().type_keys('{F5}')\n        self.wait(sleep)\n\n    def _switch_left_menus_by_shortcut(self, shortcut, sleep=0.5):\n        self.close_pop_dialog()\n        self._app.top_window().type_keys(shortcut)\n        self.wait(sleep)\n\n    @functools.lru_cache()\n    def _get_left_menus_handle(self):\n        count = 2\n        while True:\n            try:\n                handle = self._main.child_window(\n                    control_id=129, class_name=\"SysTreeView32\"\n                )\n                if count <= 0:\n                    return handle\n                # sometime can't find handle ready, must retry\n                handle.wait(\"ready\", 2)\n                return handle\n            # pylint: disable=broad-except\n            except Exception as ex:\n                logger.exception(\"error occurred when trying to get left menus\")\n            count = count - 1\n\n    def _cancel_entrust_by_double_click(self, row):\n        x = self._config.CANCEL_ENTRUST_GRID_LEFT_MARGIN\n        y = (\n            self._config.CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT\n            + self._config.CANCEL_ENTRUST_GRID_ROW_HEIGHT * row\n        )\n        self._app.top_window().child_window(\n            control_id=self._config.COMMON_GRID_CONTROL_ID,\n            class_name=\"CVirtualGridCtrl\",\n        ).double_click(coords=(x, y))\n\n    def refresh(self):\n        self.refresh_strategy.set_trader(self)\n        self.refresh_strategy.refresh()\n\n    @perf_clock\n    def _handle_pop_dialogs(self, handler_class=pop_dialog_handler.PopDialogHandler):\n        handler = handler_class(self._app)\n\n        while self.is_exist_pop_dialog():\n            try:\n                title = self._get_pop_dialog_title()\n            except pywinauto.findwindows.ElementNotFoundError:\n                return {\"message\": \"success\"}\n\n            result = handler.handle(title)\n            if result:\n                return result\n        return {\"message\": \"success\"}\n\n\nclass BaseLoginClientTrader(ClientTrader):\n    @abc.abstractmethod\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\n        \"\"\"Login Client Trader\"\"\"\n        pass\n\n    def prepare(\n        self,\n        config_path=None,\n        user=None,\n        password=None,\n        exe_path=None,\n        comm_password=None,\n        **kwargs\n    ):\n        \"\"\"\n        登陆客户端\n        :param config_path: 登陆配置文件，跟参数登陆方式二选一\n        :param user: 账号\n        :param password: 明文密码\n        :param exe_path: 客户端路径类似 r'C:\\\\htzqzyb2\\\\xiadan.exe', 默认 r'C:\\\\htzqzyb2\\\\xiadan.exe'\n        :param comm_password: 通讯密码\n        :return:\n        \"\"\"\n        if config_path is not None:\n            account = file2dict(config_path)\n            user = account[\"user\"]\n            password = account[\"password\"]\n            comm_password = account.get(\"comm_password\")\n            exe_path = account.get(\"exe_path\")\n        self.login(\n            user,\n            password,\n            exe_path or self._config.DEFAULT_EXE_PATH,\n            comm_password,\n            **kwargs\n        )\n        self._init_toolbar()\n"
  },
  {
    "path": "easytrader/config/__init__.py",
    "content": ""
  },
  {
    "path": "easytrader/config/client.py",
    "content": "# -*- coding: utf-8 -*-\ndef create(broker):\n    if broker == \"yh\":\n        return YH\n    if broker == \"ht\":\n        return HT\n    if broker == \"gj\":\n        return GJ\n    if broker == \"gf\":\n        return GF\n    if broker == \"ths\":\n        return CommonConfig\n    if broker == \"wk\":\n        return WK\n    if broker == \"htzq\":\n        return HTZQ\n    if broker == \"universal\":\n        return UNIVERSAL\n    raise NotImplementedError\n\n\nclass CommonConfig:\n    DEFAULT_EXE_PATH: str = \"\"\n    TITLE = \"网上股票交易系统5.0\"\n\n    # 交易所类型。 深圳A股、上海A股\n    TRADE_STOCK_EXCHANGE_CONTROL_ID = 1003\n\n    # 撤销界面上， 全部撤销按钮\n    TRADE_CANCEL_ALL_ENTRUST_CONTROL_ID = 30001\n\n    TRADE_SECURITY_CONTROL_ID = 1032\n    TRADE_PRICE_CONTROL_ID = 1033\n    TRADE_AMOUNT_CONTROL_ID = 1034\n\n    TRADE_SUBMIT_CONTROL_ID = 1006\n\n    TRADE_MARKET_TYPE_CONTROL_ID = 1541\n\n    COMMON_GRID_CONTROL_ID = 1047\n\n    COMMON_GRID_LEFT_MARGIN = 10\n    COMMON_GRID_FIRST_ROW_HEIGHT = 30\n    COMMON_GRID_ROW_HEIGHT = 16\n\n    BALANCE_MENU_PATH = [\"查询[F4]\", \"资金股票\"]\n    POSITION_MENU_PATH = [\"查询[F4]\", \"资金股票\"]\n    TODAY_ENTRUSTS_MENU_PATH = [\"查询[F4]\", \"当日委托\"]\n    TODAY_TRADES_MENU_PATH = [\"查询[F4]\", \"当日成交\"]\n\n    BALANCE_CONTROL_ID_GROUP = {\n        \"资金余额\": 1012,\n        \"可用金额\": 1016,\n        \"可取金额\": 1017,\n        \"股票市值\": 1014,\n        \"总资产\": 1015,\n    }\n\n    POP_DIALOD_TITLE_CONTROL_ID = 1365\n\n    GRID_DTYPE = {\n        \"操作日期\": str,\n        \"委托编号\": str,\n        \"申请编号\": str,\n        \"合同编号\": str,\n        \"证券代码\": str,\n        \"股东代码\": str,\n        \"资金帐号\": str,\n        \"资金帐户\": str,\n        \"发生日期\": str,\n    }\n\n    CANCEL_ENTRUST_ENTRUST_FIELD = \"合同编号\"\n    CANCEL_ENTRUST_GRID_LEFT_MARGIN = 50\n    CANCEL_ENTRUST_GRID_FIRST_ROW_HEIGHT = 30\n    CANCEL_ENTRUST_GRID_ROW_HEIGHT = 16\n\n    AUTO_IPO_SELECT_ALL_BUTTON_CONTROL_ID = 1098\n    AUTO_IPO_BUTTON_CONTROL_ID = 1006\n    AUTO_IPO_MENU_PATH = [\"新股申购\", \"批量新股申购\"]\n    AUTO_IPO_NUMBER = '申购数量'\n\n\nclass YH(CommonConfig):\n    DEFAULT_EXE_PATH = r\"C:\\双子星-中国银河证券\\Binarystar.exe\"\n\n    BALANCE_GRID_CONTROL_ID = 1308\n\n    GRID_DTYPE = {\n        \"操作日期\": str,\n        \"委托编号\": str,\n        \"申请编号\": str,\n        \"合同编号\": str,\n        \"证券代码\": str,\n        \"股东代码\": str,\n        \"资金帐号\": str,\n        \"资金帐户\": str,\n        \"发生日期\": str,\n    }\n\n    AUTO_IPO_MENU_PATH = [\"新股申购\", \"一键打新\"]\n\n\nclass HT(CommonConfig):\n    DEFAULT_EXE_PATH = r\"C:\\htzqzyb2\\xiadan.exe\"\n\n    BALANCE_CONTROL_ID_GROUP = {\n        \"资金余额\": 1012,\n        \"冻结资金\": 1013,\n        \"可用金额\": 1016,\n        \"可取金额\": 1017,\n        \"股票市值\": 1014,\n        \"总资产\": 1015,\n    }\n\n    GRID_DTYPE = {\n        \"操作日期\": str,\n        \"委托编号\": str,\n        \"申请编号\": str,\n        \"合同编号\": str,\n        \"证券代码\": str,\n        \"股东代码\": str,\n        \"资金帐号\": str,\n        \"资金帐户\": str,\n        \"发生日期\": str,\n    }\n\n    AUTO_IPO_MENU_PATH = [\"新股申购\", \"批量新股申购\"]\n\n\nclass GJ(CommonConfig):\n    DEFAULT_EXE_PATH = \"C:\\\\全能行证券交易终端\\\\xiadan.exe\"\n\n    GRID_DTYPE = {\n        \"操作日期\": str,\n        \"委托编号\": str,\n        \"申请编号\": str,\n        \"合同编号\": str,\n        \"证券代码\": str,\n        \"股东代码\": str,\n        \"资金帐号\": str,\n        \"资金帐户\": str,\n        \"发生日期\": str,\n    }\n\n    AUTO_IPO_MENU_PATH = [\"新股申购\", \"新股批量申购\"]\n\nclass GF(CommonConfig):\n    DEFAULT_EXE_PATH = \"C:\\\\gfzqrzrq\\\\xiadan.exe\"\n    TITLE = \"核新网上交易系统\"\n\n    GRID_DTYPE = {\n        \"操作日期\": str,\n        \"委托编号\": str,\n        \"申请编号\": str,\n        \"合同编号\": str,\n        \"证券代码\": str,\n        \"股东代码\": str,\n        \"资金帐号\": str,\n        \"资金帐户\": str,\n        \"发生日期\": str,\n    }\n\n    AUTO_IPO_MENU_PATH = [\"新股申购\", \"批量新股申购\"]\n\nclass WK(HT):\n    pass\n\n\nclass HTZQ(CommonConfig):\n    DEFAULT_EXE_PATH = r\"c:\\\\海通证券委托\\\\xiadan.exe\"\n\n    BALANCE_CONTROL_ID_GROUP = {\n        \"资金余额\": 1012,\n        \"可用金额\": 1016,\n        \"可取金额\": 1017,\n        \"总资产\": 1015,\n    }\n\n    AUTO_IPO_NUMBER = '可申购数量'\n\n\nclass UNIVERSAL(CommonConfig):\n    DEFAULT_EXE_PATH = r\"c:\\\\ths\\\\xiadan.exe\"\n\n    BALANCE_CONTROL_ID_GROUP = {\n        \"资金余额\": 1012,\n        \"可用金额\": 1016,\n        \"可取金额\": 1017,\n        \"总资产\": 1015,\n    }\n\n    AUTO_IPO_NUMBER = '可申购数量'\n"
  },
  {
    "path": "easytrader/config/global.json",
    "content": "{\n  \"response_format\": {\n    \"int\": [\n      \"current_amount\",\n      \"enable_amount\",\n      \"entrust_amount\",\n      \"business_amount\",\n      \"成交数量\",\n      \"撤单数量\",\n      \"委托数量\",\n      \"股份可用\",\n      \"买入冻结\",\n      \"卖出冻结\",\n      \"当前持仓\",\n      \"股份余额\"\n    ],\n    \"float\": [\n      \"current_balance\",\n      \"enable_balance\",\n      \"fetch_balance\",\n      \"market_value\",\n      \"asset_balance\",\n      \"av_buy_price\",\n      \"cost_price\",\n      \"income_balance\",\n      \"market_value\",\n      \"entrust_price\",\n      \"business_price\",\n      \"business_balance\",\n      \"fare1\",\n      \"occur_balance\",\n      \"farex\",\n      \"fare0\",\n      \"occur_amount\",\n      \"post_balance\",\n      \"fare2\",\n      \"fare3\",\n      \"资金余额\",\n      \"可用资金\",\n      \"参考市值\",\n      \"总资产\",\n      \"股份参考盈亏\",\n      \"委托价格\",\n      \"成交价格\",\n      \"成交金额\",\n      \"参考盈亏\",\n      \"参考成本价\",\n      \"参考市价\",\n      \"参考市值\"\n    ]\n  }\n}\n"
  },
  {
    "path": "easytrader/config/xq.json",
    "content": "{\n  \"login_api\": \"https://xueqiu.com/user/login\",\n  \"prefix\": \"https://xueqiu.com/user/login\",\n  \"portfolio_url\": \"https://xueqiu.com/p/\",\n  \"search_stock_url\": \"https://xueqiu.com/stock/p/search.json\",\n  \"rebalance_url\": \"https://xueqiu.com/cubes/rebalancing/create.json\",\n  \"history_url\": \"https://xueqiu.com/cubes/rebalancing/history.json\",\n  \"referer\": \"https://xueqiu.com/p/update?action=holdings&symbol=%s\",\n  \"portfolio_url_new\": \"https://xueqiu.com/cubes/rebalancing/current.json\",\n  \"portfolio_quote\": \"https://xueqiu.com/cubes/quote.json\"\n}\n"
  },
  {
    "path": "easytrader/exceptions.py",
    "content": "# -*- coding: utf-8 -*-\n\n\nclass TradeError(IOError):\n    pass\n\n\nclass NotLoginError(Exception):\n    def __init__(self, result=None):\n        super(NotLoginError, self).__init__()\n        self.result = result\n"
  },
  {
    "path": "easytrader/follower.py",
    "content": "# -*- coding: utf-8 -*-\nimport abc\nimport datetime\nimport os\nimport pickle\nimport queue\nimport re\nimport threading\nimport time\nfrom typing import List\n\nimport requests\n\nfrom easytrader import exceptions\nfrom easytrader.log import logger\n\n\nclass BaseFollower(metaclass=abc.ABCMeta):\n    \"\"\"\n    slippage: 滑点，取值范围为 [0, 1]\n    \"\"\"\n\n    LOGIN_PAGE = \"\"\n    LOGIN_API = \"\"\n    TRANSACTION_API = \"\"\n    CMD_CACHE_FILE = \"cmd_cache.pk\"\n    WEB_REFERER = \"\"\n    WEB_ORIGIN = \"\"\n\n    def __init__(self):\n        self.trade_queue = queue.Queue()\n        self.expired_cmds = set()\n\n        self.s = requests.Session()\n        self.s.verify = False\n\n        self.slippage: float = 0.0\n\n    def login(self, user=None, password=None, **kwargs):\n        \"\"\"\n        登陆接口\n        :param user: 用户名\n        :param password: 密码\n        :param kwargs: 其他参数\n        :return:\n        \"\"\"\n        headers = self._generate_headers()\n        self.s.headers.update(headers)\n\n        # init cookie\n        self.s.get(self.LOGIN_PAGE)\n\n        # post for login\n        params = self.create_login_params(user, password, **kwargs)\n        rep = self.s.post(self.LOGIN_API, data=params)\n\n        self.check_login_success(rep)\n        logger.info(\"登录成功\")\n\n    def _generate_headers(self):\n        headers = {\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n            \"Accept-Encoding\": \"gzip, deflate, br\",\n            \"Accept-Language\": \"en-US,en;q=0.8\",\n            \"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64) \"\n            \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n            \"Chrome/54.0.2840.100 Safari/537.36\",\n            \"Referer\": self.WEB_REFERER,\n            \"X-Requested-With\": \"XMLHttpRequest\",\n            \"Origin\": self.WEB_ORIGIN,\n            \"Content-Type\": \"application/x-www-form-urlencoded; charset=UTF-8\",\n        }\n        return headers\n\n    def check_login_success(self, rep):\n        \"\"\"检查登录状态是否成功\n        :param rep: post login 接口返回的 response 对象\n        :raise 如果登录失败应该抛出 NotLoginError \"\"\"\n        pass\n\n    def create_login_params(self, user, password, **kwargs) -> dict:\n        \"\"\"生成 post 登录接口的参数\n        :param user: 用户名\n        :param password: 密码\n        :return dict 登录参数的字典\n        \"\"\"\n        return {}\n\n    def follow(\n        self,\n        users,\n        strategies,\n        track_interval=1,\n        trade_cmd_expire_seconds=120,\n        cmd_cache=True,\n        slippage: float = 0.0,\n        **kwargs\n    ):\n        \"\"\"跟踪平台对应的模拟交易，支持多用户多策略\n\n        :param users: 支持easytrader的用户对象，支持使用 [] 指定多个用户\n        :param strategies: 雪球组合名, 类似 ZH123450\n        :param total_assets: 雪球组合对应的总资产， 格式 [ 组合1对应资金, 组合2对应资金 ]\n            若 strategies=['ZH000001', 'ZH000002'] 设置 total_assets=[10000, 10000], 则表明每个组合对应的资产为 1w 元，\n            假设组合 ZH000001 加仓 价格为 p 股票 A 10%, 则对应的交易指令为 买入 股票 A 价格 P 股数 1w * 10% / p 并按 100 取整\n        :param initial_assets:雪球组合对应的初始资产, 格式 [ 组合1对应资金, 组合2对应资金 ]\n            总资产由 初始资产 × 组合净值 算得， total_assets 会覆盖此参数\n        :param track_interval: 轮询模拟交易时间，单位为秒\n        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒\n        :param cmd_cache: 是否读取存储历史执行过的指令，防止重启时重复执行已经交易过的指令\n        :param slippage: 滑点，0.0 表示无滑点, 0.05 表示滑点为 5%\n        \"\"\"\n        self.slippage = slippage\n\n    def _calculate_price_by_slippage(self, action: str, price: float) -> float:\n        \"\"\"\n        计算考虑滑点之后的价格\n        :param action: 交易动作， 支持 ['buy', 'sell']\n        :param price: 原始交易价格\n        :return: 考虑滑点后的交易价格\n        \"\"\"\n        if action == \"buy\":\n            return price * (1 + self.slippage)\n        if action == \"sell\":\n            return price * (1 - self.slippage)\n        return price\n\n    def load_expired_cmd_cache(self):\n        if os.path.exists(self.CMD_CACHE_FILE):\n            with open(self.CMD_CACHE_FILE, \"rb\") as f:\n                self.expired_cmds = pickle.load(f)\n\n    def start_trader_thread(\n        self,\n        users,\n        trade_cmd_expire_seconds,\n        entrust_prop=\"limit\",\n        send_interval=0,\n    ):\n        trader = threading.Thread(\n            target=self.trade_worker,\n            args=[users],\n            kwargs={\n                \"expire_seconds\": trade_cmd_expire_seconds,\n                \"entrust_prop\": entrust_prop,\n                \"send_interval\": send_interval,\n            },\n        )\n        trader.setDaemon(True)\n        trader.start()\n\n    @staticmethod\n    def warp_list(value):\n        if not isinstance(value, list):\n            value = [value]\n        return value\n\n    @staticmethod\n    def extract_strategy_id(strategy_url):\n        \"\"\"\n        抽取 策略 id，一般用于获取策略相关信息\n        :param strategy_url: 策略 url\n        :return: str 策略 id\n        \"\"\"\n        pass\n\n    def extract_strategy_name(self, strategy_url):\n        \"\"\"\n        抽取 策略名，主要用于日志打印，便于识别\n        :param strategy_url:\n        :return: str 策略名\n        \"\"\"\n        pass\n\n    def track_strategy_worker(self, strategy, name, interval=10, **kwargs):\n        \"\"\"跟踪下单worker\n        :param strategy: 策略id\n        :param name: 策略名字\n        :param interval: 轮询策略的时间间隔，单位为秒\"\"\"\n        while True:\n            try:\n                transactions = self.query_strategy_transaction(\n                    strategy, **kwargs\n                )\n            # pylint: disable=broad-except\n            except Exception as e:\n                logger.exception(\"无法获取策略 %s 调仓信息, 错误: %s, 跳过此次调仓查询\", name, e)\n                time.sleep(3)\n                continue\n            for transaction in transactions:\n                trade_cmd = {\n                    \"strategy\": strategy,\n                    \"strategy_name\": name,\n                    \"action\": transaction[\"action\"],\n                    \"stock_code\": transaction[\"stock_code\"],\n                    \"amount\": transaction[\"amount\"],\n                    \"price\": transaction[\"price\"],\n                    \"datetime\": transaction[\"datetime\"],\n                }\n                if self.is_cmd_expired(trade_cmd):\n                    continue\n                logger.info(\n                    \"策略 [%s] 发送指令到交易队列, 股票: %s 动作: %s 数量: %s 价格: %s 信号产生时间: %s\",\n                    name,\n                    trade_cmd[\"stock_code\"],\n                    trade_cmd[\"action\"],\n                    trade_cmd[\"amount\"],\n                    trade_cmd[\"price\"],\n                    trade_cmd[\"datetime\"],\n                )\n                self.trade_queue.put(trade_cmd)\n                self.add_cmd_to_expired_cmds(trade_cmd)\n            try:\n                for _ in range(interval):\n                    time.sleep(1)\n            except KeyboardInterrupt:\n                logger.info(\"程序退出\")\n                break\n\n    @staticmethod\n    def generate_expired_cmd_key(cmd):\n        return \"{}_{}_{}_{}_{}_{}\".format(\n            cmd[\"strategy_name\"],\n            cmd[\"stock_code\"],\n            cmd[\"action\"],\n            cmd[\"amount\"],\n            cmd[\"price\"],\n            cmd[\"datetime\"],\n        )\n\n    def is_cmd_expired(self, cmd):\n        key = self.generate_expired_cmd_key(cmd)\n        return key in self.expired_cmds\n\n    def add_cmd_to_expired_cmds(self, cmd):\n        key = self.generate_expired_cmd_key(cmd)\n        self.expired_cmds.add(key)\n\n        with open(self.CMD_CACHE_FILE, \"wb\") as f:\n            pickle.dump(self.expired_cmds, f)\n\n    @staticmethod\n    def _is_number(s):\n        try:\n            float(s)\n            return True\n        except ValueError:\n            return False\n\n    def _execute_trade_cmd(\n        self, trade_cmd, users, expire_seconds, entrust_prop, send_interval\n    ):\n        \"\"\"分发交易指令到对应的 user 并执行\n        :param trade_cmd:\n        :param users:\n        :param expire_seconds:\n        :param entrust_prop:\n        :param send_interval:\n        :return:\n        \"\"\"\n        for user in users:\n            # check expire\n            now = datetime.datetime.now()\n            expire = (now - trade_cmd[\"datetime\"]).total_seconds()\n            if expire > expire_seconds:\n                logger.warning(\n                    \"策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时，指令产生时间: %s 当前时间: %s, 超过设置的最大过期时间 %s 秒, 被丢弃\",\n                    trade_cmd[\"strategy_name\"],\n                    trade_cmd[\"stock_code\"],\n                    trade_cmd[\"action\"],\n                    trade_cmd[\"amount\"],\n                    trade_cmd[\"price\"],\n                    trade_cmd[\"datetime\"],\n                    now,\n                    expire_seconds,\n                )\n                break\n\n            # check price\n            price = trade_cmd[\"price\"]\n            if not self._is_number(price) or price <= 0:\n                logger.warning(\n                    \"策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时，指令产生时间: %s 当前时间: %s, 价格无效 , 被丢弃\",\n                    trade_cmd[\"strategy_name\"],\n                    trade_cmd[\"stock_code\"],\n                    trade_cmd[\"action\"],\n                    trade_cmd[\"amount\"],\n                    trade_cmd[\"price\"],\n                    trade_cmd[\"datetime\"],\n                    now,\n                )\n                break\n\n            # check amount\n            if trade_cmd[\"amount\"] <= 0:\n                logger.warning(\n                    \"策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格: %s)超时，指令产生时间: %s 当前时间: %s, 买入股数无效 , 被丢弃\",\n                    trade_cmd[\"strategy_name\"],\n                    trade_cmd[\"stock_code\"],\n                    trade_cmd[\"action\"],\n                    trade_cmd[\"amount\"],\n                    trade_cmd[\"price\"],\n                    trade_cmd[\"datetime\"],\n                    now,\n                )\n                break\n\n            actual_price = self._calculate_price_by_slippage(\n                trade_cmd[\"action\"], trade_cmd[\"price\"]\n            )\n            args = {\n                \"security\": trade_cmd[\"stock_code\"],\n                \"price\": actual_price,\n                \"amount\": trade_cmd[\"amount\"],\n                \"entrust_prop\": entrust_prop,\n            }\n            try:\n                response = getattr(user, trade_cmd[\"action\"])(**args)\n            except exceptions.TradeError as e:\n                trader_name = type(user).__name__\n                err_msg = \"{}: {}\".format(type(e).__name__, e.args)\n                logger.error(\n                    \"%s 执行 策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 失败, 错误信息: %s\",\n                    trader_name,\n                    trade_cmd[\"strategy_name\"],\n                    trade_cmd[\"stock_code\"],\n                    trade_cmd[\"action\"],\n                    trade_cmd[\"amount\"],\n                    actual_price,\n                    trade_cmd[\"datetime\"],\n                    err_msg,\n                )\n            else:\n                logger.info(\n                    \"策略 [%s] 指令(股票: %s 动作: %s 数量: %s 价格(考虑滑点): %s 指令产生时间: %s) 执行成功, 返回: %s\",\n                    trade_cmd[\"strategy_name\"],\n                    trade_cmd[\"stock_code\"],\n                    trade_cmd[\"action\"],\n                    trade_cmd[\"amount\"],\n                    actual_price,\n                    trade_cmd[\"datetime\"],\n                    response,\n                )\n\n    def trade_worker(\n        self, users, expire_seconds=120, entrust_prop=\"limit\", send_interval=0\n    ):\n        \"\"\"\n        :param send_interval: 交易发送间隔， 默认为0s。调大可防止卖出买入时买出单没有及时成交导致的买入金额不足\n        \"\"\"\n        while True:\n            trade_cmd = self.trade_queue.get()\n            self._execute_trade_cmd(\n                trade_cmd, users, expire_seconds, entrust_prop, send_interval\n            )\n            time.sleep(send_interval)\n\n    def query_strategy_transaction(self, strategy, **kwargs):\n        params = self.create_query_transaction_params(strategy)\n\n        rep = self.s.get(self.TRANSACTION_API, params=params)\n        history = rep.json()\n\n        transactions = self.extract_transactions(history)\n        self.project_transactions(transactions, **kwargs)\n        return self.order_transactions_sell_first(transactions)\n\n    def extract_transactions(self, history) -> List[str]:\n        \"\"\"\n        抽取接口返回中的调仓记录列表\n        :param history: 调仓接口返回信息的字典对象\n        :return: [] 调参历史记录的列表\n        \"\"\"\n        return []\n\n    def create_query_transaction_params(self, strategy) -> dict:\n        \"\"\"\n        生成用于查询调参记录的参数\n        :param strategy: 策略 id\n        :return: dict 调参记录参数\n        \"\"\"\n        return {}\n\n    @staticmethod\n    def re_find(pattern, string, dtype=str):\n        return dtype(re.search(pattern, string).group())\n\n    @staticmethod\n    def re_search(pattern, string, dtype=str):\n        return dtype(re.search(pattern,string).group(1))\n\n    def project_transactions(self, transactions, **kwargs):\n        \"\"\"\n        修证调仓记录为内部使用的统一格式\n        :param transactions: [] 调仓记录的列表\n        :return: [] 修整后的调仓记录\n        \"\"\"\n        pass\n\n    def order_transactions_sell_first(self, transactions):\n        # 调整调仓记录的顺序为先卖再买\n        sell_first_transactions = []\n        for transaction in transactions:\n            if transaction[\"action\"] == \"sell\":\n                sell_first_transactions.insert(0, transaction)\n            else:\n                sell_first_transactions.append(transaction)\n        return sell_first_transactions\n"
  },
  {
    "path": "easytrader/gf_clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\nimport re\nimport tempfile\nimport time\nimport os\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader import clienttrader\nfrom easytrader.utils.captcha import recognize_verify_code\n\n\nclass GFClientTrader(clienttrader.BaseLoginClientTrader):\n    @property\n    def broker_type(self):\n        return \"gf\"\n\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\n        \"\"\"\n        登陆客户端\n\n        :param user: 账号\n        :param password: 明文密码\n        :param exe_path: 客户端路径类似 'C:\\\\中国银河证券双子星3.2\\\\Binarystar.exe',\n            默认 'C:\\\\中国银河证券双子星3.2\\\\Binarystar.exe'\n        :param comm_password: 通讯密码, 华泰需要，可不设\n        :return:\n        \"\"\"\n        try:\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=1\n            )\n        # pylint: disable=broad-except\n        except Exception:\n            self._app = pywinauto.Application().start(exe_path)\n\n            # wait login window ready\n            while True:\n                try:\n                    self._app.top_window().Edit1.wait(\"ready\")\n                    break\n                except RuntimeError:\n                    pass\n\n            self.type_edit_control_keys(self._app.top_window().Edit1, user)\n            self.type_edit_control_keys(self._app.top_window().Edit2, password)\n            edit3 = self._app.top_window().window(control_id=0x3eb)\n            while True:\n                try:\n                    code = self._handle_verify_code()\n                    self.type_edit_control_keys(edit3, code)\n                    time.sleep(1)\n                    self._app.top_window()[\"登录(Y)\"].click()\n                    # detect login is success or not\n                    try:\n                        self._app.top_window().wait_not(\"exists\", 5)\n                        break\n\n                    # pylint: disable=broad-except\n                    except Exception:\n                        self._app.top_window()[\"确定\"].click()\n\n                # pylint: disable=broad-except\n                except Exception:\n                    pass\n\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=10\n            )\n        self._main = self._app.window(title_re=\"\"\"{title}.*\"\"\".format(title=self._config.TITLE))\n        self.close_pop_dialog()\n\n    def _handle_verify_code(self):\n        control = self._app.top_window().window(control_id=0x5db)\n        control.click()\n        time.sleep(0.2)\n        file_path = tempfile.mktemp() + \".jpg\"\n        control.capture_as_image().save(file_path)\n        time.sleep(0.2)\n        vcode = recognize_verify_code(file_path, \"gf_client\")\n        if os.path.exists(file_path):\n            os.remove(file_path)\n        return \"\".join(re.findall(\"[a-zA-Z0-9]+\", vcode))\n"
  },
  {
    "path": "easytrader/gj_clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\nimport re\nimport tempfile\nimport time\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader import clienttrader\nfrom easytrader.utils.captcha import recognize_verify_code\n\n\nclass GJClientTrader(clienttrader.BaseLoginClientTrader):\n    @property\n    def broker_type(self):\n        return \"gj\"\n\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\n        \"\"\"\n        登陆客户端\n\n        :param user: 账号\n        :param password: 明文密码\n        :param exe_path: 客户端路径类似 'C:\\\\中国银河证券双子星3.2\\\\Binarystar.exe',\n            默认 'C:\\\\中国银河证券双子星3.2\\\\Binarystar.exe'\n        :param comm_password: 通讯密码, 华泰需要，可不设\n        :return:\n        \"\"\"\n        try:\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=1\n            )\n        # pylint: disable=broad-except\n        except Exception:\n            self._app = pywinauto.Application().start(exe_path)\n\n            # wait login window ready\n            while True:\n                try:\n                    self._app.top_window().Edit1.wait(\"ready\")\n                    break\n                except RuntimeError:\n                    pass\n\n            self._app.top_window().Edit1.type_keys(user)\n            self._app.top_window().Edit2.type_keys(password)\n            edit3 = self._app.top_window().window(control_id=0x3eb)\n            while True:\n                try:\n                    code = self._handle_verify_code()\n                    edit3.type_keys(code)\n                    time.sleep(1)\n                    self._app.top_window()[\"确定(Y)\"].click()\n                    # detect login is success or not\n                    try:\n                        self._app.top_window().wait_not(\"exists\", 5)\n                        break\n\n                    # pylint: disable=broad-except\n                    except Exception:\n                        self._app.top_window()[\"确定\"].click()\n\n                # pylint: disable=broad-except\n                except Exception:\n                    pass\n\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=10\n            )\n        self._main = self._app.window(title=\"网上股票交易系统5.0\")\n\n    def _handle_verify_code(self):\n        control = self._app.top_window().window(control_id=0x5db)\n        control.click()\n        time.sleep(0.2)\n        file_path = tempfile.mktemp() + \".jpg\"\n        control.capture_as_image().save(file_path)\n        time.sleep(0.2)\n        vcode = recognize_verify_code(file_path, \"gj_client\")\n        return \"\".join(re.findall(\"[a-zA-Z0-9]+\", vcode))\n"
  },
  {
    "path": "easytrader/grid_strategies.py",
    "content": "# -*- coding: utf-8 -*-\nimport abc\nimport io\nimport tempfile\nfrom io import StringIO\nfrom typing import TYPE_CHECKING, Dict, List, Optional\n\nimport pandas as pd\nimport pywinauto.keyboard\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader.log import logger\nfrom easytrader.utils.captcha import captcha_recognize\nfrom easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines\n\nif TYPE_CHECKING:\n    # pylint: disable=unused-import\n    from easytrader import clienttrader\n\n\nclass IGridStrategy(abc.ABC):\n    @abc.abstractmethod\n    def get(self, control_id: int) -> List[Dict]:\n        \"\"\"\n        获取 grid 数据并格式化返回\n\n        :param control_id: grid 的 control id\n        :return: grid 数据\n        \"\"\"\n        pass\n\n    @abc.abstractmethod\n    def set_trader(self, trader: \"clienttrader.IClientTrader\"):\n        pass\n\n\nclass BaseStrategy(IGridStrategy):\n    def __init__(self):\n        self._trader = None\n\n    def set_trader(self, trader: \"clienttrader.IClientTrader\"):\n        self._trader = trader\n\n    @abc.abstractmethod\n    def get(self, control_id: int) -> List[Dict]:\n        \"\"\"\n        :param control_id: grid 的 control id\n        :return: grid 数据\n        \"\"\"\n        pass\n\n    def _get_grid(self, control_id: int):\n        grid = self._trader.main.child_window(\n            control_id=control_id, class_name=\"CVirtualGridCtrl\"\n        )\n        return grid\n\n    def _set_foreground(self, grid=None):\n        try:\n            if grid is None:\n                grid = self._trader.main\n            if grid.has_style(win32defines.WS_MINIMIZE):  # if minimized\n                ShowWindow(grid.wrapper_object(), 9)  # restore window state\n            else:\n                SetForegroundWindow(grid.wrapper_object())  # bring to front\n        except:\n            pass\n\n\nclass Copy(BaseStrategy):\n    \"\"\"\n    通过复制 grid 内容到剪切板再读取来获取 grid 内容\n    \"\"\"\n\n    _need_captcha_reg = True\n\n    def get(self, control_id: int) -> List[Dict]:\n        grid = self._get_grid(control_id)\n        self._set_foreground(grid)\n        grid.type_keys(\"^A^C\", set_foreground=False, pause=0.2)\n        content = self._get_clipboard_data()\n        return self._format_grid_data(content)\n\n    def _format_grid_data(self, data: str) -> List[Dict]:\n        try:\n            df = pd.read_csv(\n                io.StringIO(data),\n                delimiter=\"\\t\",\n                dtype=self._trader.config.GRID_DTYPE,\n                na_filter=False,\n            )\n            return df.to_dict(\"records\")\n        except:\n            Copy._need_captcha_reg = True\n\n    def _get_clipboard_data(self) -> str:\n        if Copy._need_captcha_reg:\n            if (\n                    self._trader.app.top_window().window(class_name=\"Static\", title_re=\"验证码\").exists(timeout=1)\n            ):\n                file_path = \"tmp.png\"\n                count = 5\n                found = False\n                while count > 0:\n                    self._trader.app.top_window().window(\n                        control_id=0x965, class_name=\"Static\"\n                    ).capture_as_image().save(\n                        file_path\n                    )  # 保存验证码\n\n                    captcha_num = captcha_recognize(file_path).strip()  # 识别验证码\n                    captcha_num = \"\".join(captcha_num.split())\n                    logger.info(\"captcha result-->\" + captcha_num)\n                    if len(captcha_num) == 4:\n                        editor = self._trader.app.top_window().window(\n                            control_id=0x964, class_name=\"Edit\"\n                        ) # 验证码输入框\n                        editor.set_focus() # 焦点移到验证码输入框 (也可不聚焦防止键盘误触输入，不聚焦type_edit_control_keys也可正常输入)\n                        self._trader.wait(0.1) # 输入前短暂等待\n                        self._trader.type_edit_control_keys(\n                            editor,\n                            captcha_num\n                        )  # 模拟输入验证码\n\n                        self._trader.wait(0.1) # 输完后短暂等待\n                        self._trader.app.top_window().type_keys(\"{ENTER}\", pause=0.1)  # 模拟发送enter，点击确定\n                        if not editor.exists(timeout=1):  # 窗体消失\n                            logger.info(\"验证码验证成功-->\" + captcha_num)\n                            found = True\n                            break\n                    count -= 1\n                    self._trader.wait(0.1)\n                    self._trader.app.top_window().window(\n                        control_id=0x965, class_name=\"Static\"\n                    ).click()\n                if not found:\n                    self._trader.app.top_window().Button2.click()  # 点击取消\n            else:\n                pass\n                # 不要将 Copy._need_captcha_reg 置为 False, 因为它是类方法, 一旦置为 False, 后续操作都不再进行验证码识别\n                # Copy._need_captcha_reg = False\n        count = 5\n        while count > 0:\n            try:\n                return pywinauto.clipboard.GetData()\n            # pylint: disable=broad-except\n            except Exception as e:\n                count -= 1\n                logger.exception(\"%s, retry ......\", e)\n\n\nclass WMCopy(Copy):\n    \"\"\"\n    通过复制 grid 内容到剪切板再读取来获取 grid 内容\n    \"\"\"\n\n    def get(self, control_id: int) -> List[Dict]:\n        grid = self._get_grid(control_id)\n        grid.post_message(win32defines.WM_COMMAND, 0xE122, 0)\n        self._trader.wait(0.1)\n        content = self._get_clipboard_data()\n        return self._format_grid_data(content)\n\n\nclass Xls(BaseStrategy):\n    \"\"\"\n    通过将 Grid 另存为 xls 文件再读取的方式获取 grid 内容\n    \"\"\"\n\n    def __init__(self, tmp_folder: Optional[str] = None):\n        \"\"\"\n        :param tmp_folder: 用于保持临时文件的文件夹\n        \"\"\"\n        super().__init__()\n        self.tmp_folder = tmp_folder\n\n    def get(self, control_id: int) -> List[Dict]:\n        grid = self._get_grid(control_id)\n\n        # ctrl+s 保存 grid 内容为 xls 文件\n        self._set_foreground(grid)  # setFocus buggy, instead of SetForegroundWindow\n        grid.type_keys(\"^s\", set_foreground=False)\n        count = 10\n        while count > 0:\n            if self._trader.is_exist_pop_dialog():\n                break\n            self._trader.wait(0.2)\n            count -= 1\n\n        temp_path = tempfile.mktemp(suffix=\".xls\", dir=self.tmp_folder)\n        self._set_foreground(self._trader.app.top_window())\n\n        # alt+s保存，alt+y替换已存在的文件\n        self._trader.app.top_window().Edit1.set_edit_text(temp_path)\n        self._trader.wait(0.1)\n        self._trader.app.top_window().type_keys(\"%{s}%{y}\", set_foreground=False)\n        # Wait until file save complete otherwise pandas can not find file\n        self._trader.wait(0.2)\n        if self._trader.is_exist_pop_dialog():\n            self._trader.app.top_window().Button2.click()\n            self._trader.wait(0.2)\n\n        return self._format_grid_data(temp_path)\n\n    def _format_grid_data(self, data: str) -> List[Dict]:\n        with open(data, encoding=\"gbk\", errors=\"replace\") as f:\n            content = f.read()\n\n        df = pd.read_csv(\n            StringIO(content),\n            delimiter=\"\\t\",\n            dtype=self._trader.config.GRID_DTYPE,\n            na_filter=False,\n        )\n        return df.to_dict(\"records\")\n"
  },
  {
    "path": "easytrader/ht_clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\r\n\r\nimport pywinauto\r\nimport pywinauto.clipboard\r\n\r\nfrom easytrader import grid_strategies\r\nfrom . import clienttrader\r\n\r\n\r\nclass HTClientTrader(clienttrader.BaseLoginClientTrader):\r\n    grid_strategy = grid_strategies.Xls\r\n\r\n    @property\r\n    def broker_type(self):\r\n        return \"ht\"\r\n\r\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\r\n        \"\"\"\r\n        :param user: 用户名\r\n        :param password: 密码\r\n        :param exe_path: 客户端路径, 类似\r\n        :param comm_password:\r\n        :param kwargs:\r\n        :return:\r\n        \"\"\"\r\n        self._editor_need_type_keys = False\r\n        if comm_password is None:\r\n            raise ValueError(\"华泰必须设置通讯密码\")\r\n\r\n        try:\r\n            self._app = pywinauto.Application().connect(\r\n                path=self._run_exe_path(exe_path), timeout=1\r\n            )\r\n        # pylint: disable=broad-except\r\n        except Exception:\r\n            self._app = pywinauto.Application().start(exe_path)\r\n\r\n            # wait login window ready\r\n            while True:\r\n                try:\r\n                    self._app.top_window().Edit1.wait(\"ready\")\r\n                    break\r\n                except RuntimeError:\r\n                    pass\r\n            self._app.top_window().Edit1.set_focus()\r\n            self._app.top_window().Edit1.type_keys(user)\r\n            self._app.top_window().Edit2.type_keys(password)\r\n\r\n            self._app.top_window().Edit3.set_edit_text(comm_password)\r\n\r\n            self._app.top_window().button0.click()\r\n\r\n            self._app = pywinauto.Application().connect(\r\n                path=self._run_exe_path(exe_path), timeout=10\r\n            )\r\n        self._main = self._app.window(title=\"网上股票交易系统5.0\")\r\n        self._main.wait ( \"exists enabled visible ready\" , timeout=100 )\r\n        self._close_prompt_windows ( )\r\n\r\n    @property\r\n    def balance(self):\r\n        self._switch_left_menus(self._config.BALANCE_MENU_PATH)\r\n\r\n        return self._get_balance_from_statics()\r\n\r\n    def _get_balance_from_statics(self):\r\n        result = {}\r\n        for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items():\r\n            result[key] = float(\r\n                self._main.child_window(\r\n                    control_id=control_id, class_name=\"Static\"\r\n                ).window_text()\r\n            )\r\n        return result\r\n\r\n\r\n"
  },
  {
    "path": "easytrader/htzq_clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader import grid_strategies\nfrom . import clienttrader\n\n\nclass HTZQClientTrader(clienttrader.BaseLoginClientTrader):\n    grid_strategy = grid_strategies.Xls\n\n    @property\n    def broker_type(self):\n        return \"htzq\"\n\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\n        \"\"\"\n        :param user: 用户名\n        :param password: 密码\n        :param exe_path: 客户端路径, 类似\n        :param comm_password:\n        :param kwargs:\n        :return:\n        \"\"\"\n        self._editor_need_type_keys = False\n        if comm_password is None:\n            raise ValueError(\"必须设置通讯密码\")\n\n        try:\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=1\n            )\n        # pylint: disable=broad-except\n        except Exception:\n            self._app = pywinauto.Application().start(exe_path)\n\n            # wait login window ready\n            while True:\n                try:\n                    self._app.top_window().Edit1.wait(\"ready\")\n                    break\n                except RuntimeError:\n                    pass\n            self._app.top_window().Edit1.set_focus()\n            self._app.top_window().Edit1.type_keys(user)\n            self._app.top_window().Edit2.type_keys(password)\n\n            self._app.top_window().Edit3.type_keys(comm_password)\n\n            self._app.top_window().button0.click()\n\n            # detect login is success or not\n            self._app.top_window().wait_not(\"exists\", 100)\n\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=10\n            )\n        self._close_prompt_windows()\n        self._main = self._app.window(title=\"网上股票交易系统5.0\")\n\n"
  },
  {
    "path": "easytrader/joinquant_follower.py",
    "content": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\nfrom threading import Thread\n\nfrom easytrader import exceptions\nfrom easytrader.follower import BaseFollower\nfrom easytrader.log import logger\n\n\nclass JoinQuantFollower(BaseFollower):\n    LOGIN_PAGE = \"https://www.joinquant.com\"\n    LOGIN_API = \"https://www.joinquant.com/user/login/doLogin?ajax=1\"\n    TRANSACTION_API = (\n        \"https://www.joinquant.com/algorithm/live/transactionDetail\"\n    )\n    WEB_REFERER = \"https://www.joinquant.com/user/login/index\"\n    WEB_ORIGIN = \"https://www.joinquant.com\"\n\n    def create_login_params(self, user, password, **kwargs):\n        params = {\n            \"CyLoginForm[username]\": user,\n            \"CyLoginForm[pwd]\": password,\n            \"ajax\": 1,\n        }\n        return params\n\n    def check_login_success(self, rep):\n        set_cookie = rep.headers[\"set-cookie\"]\n        if len(set_cookie) < 50:\n            raise exceptions.NotLoginError(\"登录失败，请检查用户名和密码\")\n        self.s.headers.update({\"cookie\": set_cookie})\n\n    def follow(\n            self,\n            users,\n            strategies,\n            track_interval=1,\n            trade_cmd_expire_seconds=120,\n            cmd_cache=True,\n            entrust_prop=\"limit\",\n            send_interval=0,\n    ):\n        \"\"\"跟踪joinquant对应的模拟交易，支持多用户多策略\n        :param users: 支持easytrader的用户对象，支持使用 [] 指定多个用户\n        :param strategies: joinquant 的模拟交易地址，支持使用 [] 指定多个模拟交易,\n            地址类似 https://www.joinquant.com/algorithm/live/index?backtestId=xxx\n        :param track_interval: 轮训模拟交易时间，单位为秒\n        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒\n        :param cmd_cache: 是否读取存储历史执行过的指令，防止重启时重复执行已经交易过的指令\n        :param entrust_prop: 委托方式, 'limit' 为限价，'market' 为市价, 仅在银河实现\n        :param send_interval: 交易发送间隔， 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足\n        \"\"\"\n        users = self.warp_list(users)\n        strategies = self.warp_list(strategies)\n\n        if cmd_cache:\n            self.load_expired_cmd_cache()\n\n        self.start_trader_thread(\n            users, trade_cmd_expire_seconds, entrust_prop, send_interval\n        )\n\n        workers = []\n        for strategy_url in strategies:\n            try:\n                strategy_id = self.extract_strategy_id(strategy_url)\n                strategy_name = self.extract_strategy_name(strategy_url)\n            except:\n                logger.error(\"抽取交易id和策略名失败, 无效的模拟交易url: %s\", strategy_url)\n                raise\n            strategy_worker = Thread(\n                target=self.track_strategy_worker,\n                args=[strategy_id, strategy_name],\n                kwargs={\"interval\": track_interval},\n            )\n            strategy_worker.start()\n            workers.append(strategy_worker)\n            logger.info(\"开始跟踪策略: %s\", strategy_name)\n        for worker in workers:\n            worker.join()\n\n    # @staticmethod\n    # def extract_strategy_id(strategy_url):\n    #     return re.search(r\"(?<=backtestId=)\\w+\", strategy_url).group()\n    #\n    # def extract_strategy_name(self, strategy_url):\n    #     rep = self.s.get(strategy_url)\n    #     return self.re_find(\n    #         r'(?<=title=\"点击修改策略名称\"\\>).*(?=\\</span)', rep.content.decode(\"utf8\")\n    #     )\n    def extract_strategy_id(self, strategy_url):\n        rep = self.s.get(strategy_url)\n        return self.re_search(r'name=\"backtest\\[backtestId\\]\"\\s+?value=\"(.*?)\">', rep.content.decode(\"utf8\"))\n\n    def extract_strategy_name(self, strategy_url):\n        rep = self.s.get(strategy_url)\n        return self.re_search(r'class=\"backtest_name\".+?>(.*?)</span>', rep.content.decode(\"utf8\"))\n\n    def create_query_transaction_params(self, strategy):\n        today_str = datetime.today().strftime(\"%Y-%m-%d\")\n        params = {\"backtestId\": strategy, \"date\": today_str, \"ajax\": 1}\n        return params\n\n    def extract_transactions(self, history):\n        transactions = history[\"data\"][\"transaction\"]\n        return transactions\n\n    @staticmethod\n    def stock_shuffle_to_prefix(stock):\n        assert (\n                len(stock) == 11\n        ), \"stock {} must like 123456.XSHG or 123456.XSHE\".format(stock)\n        code = stock[:6]\n        if stock.find(\"XSHG\") != -1:\n            return \"sh\" + code\n\n        if stock.find(\"XSHE\") != -1:\n            return \"sz\" + code\n        raise TypeError(\"not valid stock code: {}\".format(code))\n\n    def project_transactions(self, transactions, **kwargs):\n        for transaction in transactions:\n            transaction[\"amount\"] = self.re_find(\n                r\"\\d+\", transaction[\"amount\"], dtype=int\n            )\n\n            time_str = \"{} {}\".format(transaction[\"date\"], transaction[\"time\"])\n            transaction[\"datetime\"] = datetime.strptime(\n                time_str, \"%Y-%m-%d %H:%M:%S\"\n            )\n\n            stock = self.re_find(r\"\\d{6}\\.\\w{4}\", transaction[\"stock\"])\n            transaction[\"stock_code\"] = self.stock_shuffle_to_prefix(stock)\n\n            transaction[\"action\"] = (\n                \"buy\" if transaction[\"transaction\"] == \"买\" else \"sell\"\n            )\n            transaction[\"price\"] = (\n                transaction[\"price\"] if isinstance(transaction[\"transaction\"] ,float) else float(transaction[\"price\"])\n            )\n"
  },
  {
    "path": "easytrader/log.py",
    "content": "# -*- coding: utf-8 -*-\nimport logging\n\nlogger = logging.getLogger(\"easytrader\")\nlogger.setLevel(logging.INFO)\nlogger.propagate = False\n\nfmt = logging.Formatter(\n    \"%(asctime)s [%(levelname)s] %(filename)s %(lineno)s: %(message)s\"\n)\nch = logging.StreamHandler()\n\nch.setFormatter(fmt)\nlogger.handlers.append(ch)\n"
  },
  {
    "path": "easytrader/miniqmt/__init__.py",
    "content": "from easytrader.miniqmt.miniqmt_trader import MiniqmtTrader, DefaultXtQuantTraderCallback"
  },
  {
    "path": "easytrader/miniqmt/miniqmt_trader.py",
    "content": "from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback\nfrom xtquant.xttype import StockAccount\nfrom xtquant import xtconstant\nimport random\nfrom easytrader.log import logger\nfrom easytrader.utils.perf import perf_clock\nfrom easytrader.utils.stock import get_stock_type\n\n# 市价委托类型映射\nMARKET_ORDER_TYPE_NAME_MAP = {\n    \"sh\": {\n        \"对手方最优价格委托\": xtconstant.MARKET_PEER_PRICE_FIRST,\n        \"本方最优价格委托\": xtconstant.MARKET_MINE_PRICE_FIRST,\n        \"最优五档即时成交剩余撤销\": xtconstant.MARKET_SH_CONVERT_5_CANCEL,\n        \"最优五档即时成交剩转限价\": xtconstant.MARKET_SH_CONVERT_5_LIMIT,\n    },\n    \"sz\": {\n        \"对手方最优价格委托\": xtconstant.MARKET_PEER_PRICE_FIRST,\n        \"本方最优价格委托\": xtconstant.MARKET_MINE_PRICE_FIRST,\n        \"即时成交剩余撤销委托\": xtconstant.MARKET_SZ_INSTBUSI_RESTCANCEL,\n        \"最优五档即时成交剩余撤销\": xtconstant.MARKET_SZ_CONVERT_5_CANCEL,\n        \"全额成交或撤销委托\": xtconstant.MARKET_SZ_FULL_OR_CANCEL,\n    },\n}\n\n# 市价委托类型反向映射（不区分交易所）\nMARKET_ORDER_TYPE_MAP = {\n    xtconstant.STOCK_BUY: \"买入\",\n    xtconstant.STOCK_SELL: \"卖出\",\n    xtconstant.MARKET_PEER_PRICE_FIRST: \"对手方最优价格委托\",\n    xtconstant.MARKET_MINE_PRICE_FIRST: \"本方最优价格委托\",\n    xtconstant.MARKET_SH_CONVERT_5_CANCEL: \"最优五档即时成交剩余撤销\",\n    xtconstant.MARKET_SH_CONVERT_5_LIMIT: \"最优五档即时成交剩转限价\",\n    xtconstant.MARKET_SZ_INSTBUSI_RESTCANCEL: \"即时成交剩余撤销委托\",\n    xtconstant.MARKET_SZ_CONVERT_5_CANCEL: \"最优五档即时成交剩余撤销\",\n    xtconstant.MARKET_SZ_FULL_OR_CANCEL: \"全额成交或撤销委托\",\n}\n\n# 交易操作(offset_flag)映射\nOFFSET_FLAG_MAP = {\n    xtconstant.OFFSET_FLAG_OPEN: \"买入\",\n    xtconstant.OFFSET_FLAG_CLOSE: \"卖出\",\n    xtconstant.OFFSET_FLAG_FORCECLOSE: \"强平\",\n    xtconstant.OFFSET_FLAG_CLOSETODAY: \"平今\",\n    xtconstant.OFFSET_FLAG_ClOSEYESTERDAY: \"平昨\",\n    xtconstant.OFFSET_FLAG_FORCEOFF: \"强减\",\n    xtconstant.OFFSET_FLAG_LOCALFORCECLOSE: \"本地强平\",\n}\n# 委托状态(order_status)映射\nORDER_STATUS_MAP = {\n    xtconstant.ORDER_UNREPORTED: \"未报\",\n    xtconstant.ORDER_WAIT_REPORTING: \"待报\",\n    xtconstant.ORDER_REPORTED: \"已报\",\n    xtconstant.ORDER_REPORTED_CANCEL: \"已报待撤\",\n    xtconstant.ORDER_PARTSUCC_CANCEL: \"部成待撤\",\n    xtconstant.ORDER_PART_CANCEL: \"部撤\",\n    xtconstant.ORDER_CANCELED: \"已撤\",\n    xtconstant.ORDER_PART_SUCC: \"部成\",\n    xtconstant.ORDER_SUCCEEDED: \"已成\",\n    xtconstant.ORDER_JUNK: \"废单\",\n    xtconstant.ORDER_UNKNOWN: \"未知\"\n}\n\n# 多空方向(direction)映射\nDIRECTION_MAP = {\n    xtconstant.DIRECTION_FLAG_LONG: \"多\",\n    xtconstant.DIRECTION_FLAG_SHORT: \"空\",\n}\n\n# 券商价格类型(price_type)映射\n# 官网文档见 https://dict.thinktrader.net/innerApi/enum_constants.html?id=7zqjlm#enum-ebrokerpricetype-%E4%BB%B7%E6%A0%BC%E7%B1%BB%E5%9E%8B\nBROKER_PRICE_TYPE_MAP = {\n    49: \"市价\",  # enum_EBrokerPriceType.BROKER_PRICE_ANY\n    50: \"限价\",  # enum_EBrokerPriceType.BROKER_PRICE_LIMIT\n    51: \"最优价\",  # enum_EBrokerPriceType.BROKER_PRICE_BEST\n    52: \"配股\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_ALLOTMENT\n    53: \"转托\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_REFER\n    54: \"申购\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_SUBSCRIBE\n    55: \"回购\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_BUYBACK\n    56: \"配售\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_PLACING\n    57: \"指定\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DECIDE\n    58: \"转股\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_EQUITY\n    59: \"回售\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_SELLBACK\n    60: \"股息\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DIVIDEND\n    68: \"深圳配售确认\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_SHENZHEN_PLACING\n    69: \"配售放弃\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_CANCEL_PLACING\n    70: \"无冻质押\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_WDZY\n    71: \"冻结质押\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DJZY\n    72: \"无冻解押\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_WDJY\n    73: \"解冻解押\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_JDJY\n    75: \"投票\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_VOTE\n    77: \"预售要约解除\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_YSYYJC\n    78: \"基金设红\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FUND_DEVIDEND\n    79: \"基金申赎\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FUND_ENTRUST\n    80: \"跨市转托\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_CROSS_MARKET\n    81: \"ETF申购\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_ETF\n    83: \"权证行权\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_EXERCIS\n    84: \"对手方最优价格\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_PEER_PRICE_FIRST\n    85: \"最优五档即时成交剩余转限价\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_L5_FIRST_LIMITPX\n    86: \"本方最优价格\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_MIME_PRICE_FIRST\n    87: \"即时成交剩余撤销\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_INSTBUSI_RESTCANCEL\n    88: \"最优五档即时成交剩余撤销\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_L5_FIRST_CANCEL\n    89: \"全额成交并撤单\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FULL_REAL_CANCEL\n    90: \"基金拆合\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_FUND_CHAIHE\n    91: \"债转股\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DEBT_CONVERSION\n    92: \"港股通竞价限价\",  # BROKER_PRICE_BID_LIMIT\n    93: \"港股通增强限价\",  # enum_EBrokerPriceType.BROKER_PRICE_ENHANCED_LIMIT\n    94: \"港股通零股限价\",  # enum_EBrokerPriceType.BROKER_PRICE_RETAIL_LIMIT\n    101: \"直接还券\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_DIRECT_SECU_REPAY\n    107: \"担保品划转\",  # enum_EBrokerPriceType.BROKER_PRICE_PROP_COLLATERAL_TRANSFER\n    'j': \"增发\",\n    'w': \"定价\",  # 全国股转 - 挂牌公司交易 - 协议转让\n    'x': \"成交确认\",  # 全国股转 - 挂牌公司交易 - 协议转让\n    'y': \"互报成交确认\",  # 全国股转 - 挂牌公司交易 - 协议转让\n    'z': \"限价\",  # 用于挂牌公司交易 - 做市转让 - 限价买卖和两网及退市交易-限价买卖\n}\n\n\nclass DefaultXtQuantTraderCallback(XtQuantTraderCallback):\n    \"\"\"\n    XtQuantTrader回调类的默认实现\n    \"\"\"\n\n    def on_disconnected(self):\n        \"\"\"\n        连接状态回调\n        :return:\n        \"\"\"\n        logger.info(\"连接断开\")\n\n    def on_account_status(self, status):\n        \"\"\"\n        账号状态信息推送\n        :param response: XtAccountStatus 对象\n        :return:\n        \"\"\"\n        logger.info(\n            f\"账户状态信息: account_id={status.account_id}, account_type={status.account_type}, status={status.status}\"\n        )\n\n    def on_stock_order(self, order):\n        \"\"\"\n        委托信息推送\n        :param order: XtOrder对象\n        :return:\n        \"\"\"\n        logger.info(\n            f\"委托回调: stock_code={order.stock_code}, order_status={order.order_status}, order_sysid={order.order_sysid}\"\n        )\n\n    def on_stock_trade(self, trade):\n        \"\"\"\n        成交信息推送\n        :param trade: XtTrade对象\n        :return:\n        \"\"\"\n        logger.info(\n            f\"成交回调: account_id={trade.account_id}, stock_code={trade.stock_code}, order_id={trade.order_id}\"\n        )\n\n    def on_order_error(self, order_error):\n        \"\"\"\n        下单失败信息推送\n        :param order_error:XtOrderError 对象\n        :return:\n        \"\"\"\n        logger.info(\n            f\"下单失败回调: order_id={order_error.order_id}, error_id={order_error.error_id}, error_msg={order_error.error_msg}\"\n        )\n\n    def on_cancel_error(self, cancel_error):\n        \"\"\"\n        撤单失败信息推送\n        :param cancel_error: XtCancelError 对象\n        :return:\n        \"\"\"\n        logger.info(\n            f\"撤单失败回调: order_id={cancel_error.order_id}, error_id={cancel_error.error_id}, error_msg={cancel_error.error_msg}\"\n        )\n\n    def on_order_stock_async_response(self, response):\n        \"\"\"\n        异步下单回报推送\n        :param response: XtOrderResponse 对象\n        :return:\n        \"\"\"\n        logger.info(f\"异步下单回报: account_id={response.account_id}, order_id={response.order_id}, seq={response.seq}\")\n\n    def on_smt_appointment_async_response(self, response):\n        \"\"\"\n        :param response: XtAppointmentResponse 对象\n        :return:\n        \"\"\"\n        logger.info(\n            f\"预约委托异步回报: account_id={response.account_id}, order_sysid={response.order_sysid}, error_id={response.error_id}, error_msg={response.error_msg}, seq={response.seq}\"\n        )\n\n\nclass MiniqmtTrader:\n    broker_type = \"miniqmt\"\n\n    def __init__(self):\n        self._account: StockAccount = None\n        self._trader: XtQuantTrader = None\n\n    def prepare(self, **json_data):\n        \"\"\"\n        allow remoteClient to pass param to miniqmt\n        \"\"\"\n        self.connect(**json_data.get('kwargs',{}))\n\n    def connect(\n        self,\n        miniqmt_path: str = r\"D:\\国金证券QMT交易端\\userdata_mini\",\n        stock_account: str = None,\n        trader_callback: XtQuantTraderCallback = DefaultXtQuantTraderCallback(),\n    ):\n        \"\"\"\n        连接到 miniqmt 交易端\n        注意：登录qmt客户端时必须勾选极简模式/独立交易模式，否则无法连接\n        :param miniqmt_path: miniqmt 安装路径，类似 r\"D:\\\\国金证券QMT交易端\\\\userdata_mini\"\n            注意：不建议安装在C盘。安装在C盘的话，每次都需要用管理员权限运行策略，才能正常连接，否则有权限问题\n        :param stock_account: 资金账号\n        :param trader_callback: 交易回调对象，默认使用 DefaultXtQuantTraderCallback\n        :return: None\n        \"\"\"\n        session_id = int(random.randint(100000, 999999))\n        self._trader = XtQuantTrader(miniqmt_path, session_id, callback=trader_callback)\n        self._trader.start()\n\n        if self._trader.connect() == 0:\n            logger.info(f'成功连接到 miniqmt, 账号 {stock_account}')\n            self._account = StockAccount(stock_account)\n            self._trader.subscribe(self._account)\n        else:\n            logger.error('连接失败，请检查路径或其他情况')\n\n    @property\n    def trader(self) -> XtQuantTrader:\n        \"\"\"\n        获取交易对象\n        :return: XtQuantTrader 对象\n        \"\"\"\n        return self._trader\n\n    @property\n    def account(self) -> StockAccount:\n        \"\"\"\n        获取账户对象\n        :return: StockAccount 对象\n        \"\"\"\n        return self._account\n\n    @property\n    def balance(self):\n        \"\"\"\n        获取账户资产信息。\n        qmt 官方文档：https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E8%B5%84%E4%BA%A7%E6%9F%A5%E8%AF%A2\n\n        :return:\n            list of dict: 包含账户资产信息的字典，包括:\n            - total_asset: 总资产\n            - market_value: 持仓市值\n            - cash: 可用资金\n            - frozen_cash: 冻结资金\n            - account_type: 账户类型\n            - account_id: 账户ID\n        \"\"\"\n        asset = self._trader.query_stock_asset(self._account)\n        return [\n            {\n                'total_asset': asset.total_asset,\n                'market_value': asset.market_value,\n                'cash': asset.cash,\n                'frozen_cash': asset.frozen_cash,\n                'account_type': asset.account_type,\n                'account_id': asset.account_id,\n            }\n        ]\n\n    @property\n    def position(self):\n        \"\"\"\n        获取账户持仓信息。\n        qmt 官方文档： https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%8C%81%E4%BB%93%E6%9F%A5%E8%AF%A2\n\n        :return:\n            list of dict: 包含账户持仓信息的字典列表，每个字典包括:\n            - stock_code: 证券代码\n            - security: 六位证券代码\n            - volume: 持仓数量\n            - can_use_volume: 可用数量\n            - open_price: 开仓价\n            - market_value: 市值\n            - frozen_volume: 冻结数量\n            - on_road_volume: 在途股份\n            - yesterday_volume: 昨夜拥股\n            - avg_price: 成本价\n            - direction: 多空方向\n            - account_type: 账号类型\n            - account_id: 资金账号\n        \"\"\"\n        xt_positions = self._trader.query_stock_positions(self._account)\n        positions = []\n        for pos in xt_positions:\n            positions.append(\n                {\n                    'stock_code': pos.stock_code,\n                    'security': pos.stock_code[:6],\n                    'volume': pos.volume,\n                    'can_use_volume': pos.can_use_volume,\n                    'open_price': pos.open_price,\n                    'market_value': pos.market_value,\n                    'frozen_volume': pos.frozen_volume,\n                    'on_road_volume': pos.on_road_volume,\n                    'yesterday_volume': pos.yesterday_volume,\n                    'avg_price': pos.avg_price,\n                    'direction': pos.direction,\n                    'account_type': pos.account_type,\n                    'account_id': pos.account_id,\n                }\n            )\n        return positions\n\n    @property\n    def today_entrusts(self):\n        \"\"\"\n        获取今日委托列表。\n        qmt 官方文档： https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E5%A7%94%E6%89%98%E6%9F%A5%E8%AF%A2\n\n        :return:\n            list of dict: 包含委托信息的字典列表，每个字典包括:\n            - stock_code: 证券代码\n            - security: 六位证券代码\n            - order_id: 订单编号\n            - order_sysid: 柜台合同编号\n            - order_time: 报单时间\n            - order_type: 委托类型\n            - order_type_name: 委托类型名称\n            - order_volume: 委托数量\n            - price_type: 报价类型\n            - price_type_name: 报价类型名称\n            - price: 委托价格\n            - traded_volume: 成交数量\n            - traded_price: 成交均价\n            - order_status: 委托状态\n            - order_status_name: 委托状态名称\n            - status_msg: 委托状态描述\n            - offset_flag: 交易操作\n            - offset_flag_name: 交易操作名称\n            - strategy_name: 策略名称\n            - order_remark: 委托备注\n            - direction: 多空方向\n            - direction_name: 多空方向名称\n            - account_type: 账号类型\n            - account_id: 资金账号\n        \"\"\"\n        xt_orders = self._trader.query_stock_orders(self._account, False)\n        if xt_orders is None:\n            return []\n\n        orders = []\n        for order in xt_orders:\n            orders.append(\n                {\n                    'security': order.stock_code[:6],\n                    'stock_code': order.stock_code,\n                    'order_id': order.order_id,\n                    'order_sysid': order.order_sysid,\n                    'order_time': order.order_time,\n                    'order_type': order.order_type,\n                    'order_type_name': MARKET_ORDER_TYPE_MAP.get(order.order_type, order.order_type),\n                    'order_volume': order.order_volume,\n                    'price_type': order.price_type,\n                    'price_type_name': BROKER_PRICE_TYPE_MAP.get(order.price_type, order.price_type),\n                    'price': order.price,\n                    'traded_volume': order.traded_volume,\n                    'traded_price': order.traded_price,\n                    'order_status': order.order_status,\n                    'order_status_name': ORDER_STATUS_MAP.get(order.order_status, order.order_status),\n                    'status_msg': order.status_msg,\n                    'offset_flag': order.offset_flag,\n                    'offset_flag_name': OFFSET_FLAG_MAP.get(order.offset_flag, order.offset_flag),\n                    'strategy_name': order.strategy_name,\n                    'order_remark': order.order_remark,\n                    'direction': order.direction,\n                    'direction_name': DIRECTION_MAP.get(order.direction, order.direction),\n                    'account_type': order.account_type,\n                    'account_id': order.account_id,\n                }\n            )\n        return orders\n\n    @property\n    def today_trades(self):\n        \"\"\"\n        获取今日成交列表。\n        qmt 官方文档： https://dict.thinktrader.net/nativeApi/xttrader.html?id=7zqjlm#%E6%88%90%E4%BA%A4%E6%9F%A5%E8%AF%A2\n\n        :return:\n            list of dict: 包含成交信息的字典列表，每个字典包括:\n            - stock_code: 证券代码\n            - security: 六位证券代码\n            - traded_id: 成交编号\n            - traded_time: 成交时间\n            - traded_price: 成交均价\n            - traded_volume: 成交数量\n            - traded_amount: 成交金额\n            - order_id: 订单编号\n            - order_type: 委托类型\n            - order_type_name: 委托类型名称\n            - offset_flag: 交易操作（买入/卖出）\n            - offset_flag_name: 交易操作名称\n            - account_id: 资金账号\n            - account_type: 账号类型\n            - order_sysid: 柜台合同编号\n            - strategy_name: 策略名称\n            - order_remark: 委托备注\n        \"\"\"\n        xt_trades = self._trader.query_stock_trades(self._account)\n        if xt_trades is None:\n            return []\n\n        trades = []\n        for trade in xt_trades:\n            trades.append(\n                {\n                    'security': trade.stock_code[:6],\n                    'stock_code': trade.stock_code,\n                    'traded_id': trade.traded_id,\n                    'traded_time': trade.traded_time,\n                    'traded_price': trade.traded_price,\n                    'traded_volume': trade.traded_volume,\n                    'traded_amount': trade.traded_amount,\n                    'order_id': trade.order_id,\n                    'order_type': trade.order_type,\n                    'order_type_name': MARKET_ORDER_TYPE_MAP.get(trade.order_type, trade.order_type),\n                    'offset_flag': trade.offset_flag,\n                    'offset_flag_name': OFFSET_FLAG_MAP.get(trade.offset_flag, trade.offset_flag),\n                    'account_id': trade.account_id,\n                    'account_type': trade.account_type,\n                    'order_sysid': trade.order_sysid,\n                    'strategy_name': trade.strategy_name,\n                    'order_remark': trade.order_remark,\n                }\n            )\n        return trades\n\n    @perf_clock\n    def buy(self, security: str, price: float, amount: int, **kwargs):\n        \"\"\"\n        限价买入\n        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\n\n        :param security: 六位证券代码\n        :param price: 交易价格\n        :param amount: 交易数量\n        :return: {'entrust_no': '订单编号'}\n            系统生成的订单编号，成功发送委托后的订单编号为大于0的正整数，如果为-1表示委托失败\n            注：有订单编号不一定表示成功，具体成功与否需要查看下单回调 on_order_error。\n            例如非交易时间下单可以拿到订单编号，但 on_order_error 回调会报错：\n            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]\n        \"\"\"\n        return self.trade(security, price, amount, is_buy=True)\n\n    @perf_clock\n    def sell(self, security, price, amount, **kwargs):\n        \"\"\"\n        限价卖出\n        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\n\n        :param security: 六位证券代码\n        :param price: 交易价格\n        :param amount: 交易数量\n        :return: {'entrust_no': '订单编号'}\n            系统生成的订单编号，成功发送委托后的订单编号为大于0的正整数，如果为-1表示委托失败\n            注：有订单编号不一定表示成功，具体成功与否需要查看下单回调 on_order_error。\n            例如非交易时间下单可以拿到订单编号，但 on_order_error 回调会报错：\n            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]\n        \"\"\"\n\n        return self.trade(security, price, amount, is_buy=False)\n\n    def trade(self, security: str, price: float, amount: int, *, is_buy: bool) -> int:\n        \"\"\"\n        限价交易\n        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\n\n        :param security: 六位证券代码\n        :param price: 交易价格\n        :param amount: 交易数量\n        :param is_buy: 是否为买入\n        :return: {'entrust_no': '订单编号'}\n            系统生成的订单编号，成功发送委托后的订单编号为大于0的正整数，如果为-1表示委托失败\n            注：有订单编号不一定表示成功，具体成功与否需要查看下单回调 on_order_error。\n            例如非交易时间下单可以拿到订单编号，但 on_order_error 回调会报错：\n            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]\n        \"\"\"\n        action = \"买入\" if is_buy else \"卖出\"\n        logger.info(f\"限价{action}请求: 股票代码={security}, 价格={price}, 数量={amount}\")\n        \n        order_id = self._trader.order_stock(\n            account=self._account,\n            stock_code=self._get_stock_code(security),\n            order_type=xtconstant.STOCK_BUY if is_buy else xtconstant.STOCK_SELL,\n            order_volume=amount,\n            price_type=xtconstant.FIX_PRICE,\n            price=price,\n        )\n        \n        if order_id > 0:\n            logger.info(f\"限价{action}委托成功: 股票代码={security}, 委托单号={order_id}\")\n        else:\n            logger.error(f\"限价{action}委托失败: 股票代码={security}, 错误码={order_id}\")\n            \n        return {'entrust_no': order_id}\n\n    @perf_clock\n    def market_buy(self, security, amount, ttype=None):\n        \"\"\"\n        市价买入\n        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\n\n        :param security: 六位证券代码\n        :param amount: 交易数量\n        :param ttype: 市价委托类型，默认'对手方最优价格委托'\n                 深市可选:\n                - 对手方最优价格委托\n                - 本方最优价格委托\n                - 即时成交剩余撤销委托\n                - 最优五档即时成交剩余撤销\n                - 全额成交或撤销委托\n                 沪市可选:\n                - 对手方最优价格委托\n                - 最优五档即时成交剩余撤销\n                - 最优五档即时成交剩转限价\n                - 本方最优价格委托\n        :return: {'entrust_no': '订单编号'}\n            系统生成的订单编号，成功发送委托后的订单编号为大于0的正整数，如果为-1表示委托失败\n            注：有订单编号不一定表示成功，具体成功与否需要查看下单回调 on_order_error。\n            例如非交易时间下单可以拿到订单编号，但 on_order_error 回调会报错：\n            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]\n        \"\"\"\n\n        return self.market_trade(security, amount, ttype, is_buy=True)\n\n    @perf_clock\n    def market_sell(self, security, amount, ttype=None):\n        \"\"\"\n        市价卖出\n        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\n\n        :param security: 六位证券代码\n        :param amount: 交易数量\n        :param ttype: 市价委托类型，默认'对手方最优价格委托'\n                 深市可选:\n                - 对手方最优价格委托\n                - 本方最优价格委托\n                - 即时成交剩余撤销委托\n                - 最优五档即时成交剩余撤销\n                - 全额成交或撤销委托\n                 沪市可选:\n                - 对手方最优价格委托\n                - 最优五档即时成交剩余撤销\n                - 最优五档即时成交剩转限价\n                - 本方最优价格委托\n        :return: {'entrust_no': '订单编号'}\n            系统生成的订单编号，成功发送委托后的订单编号为大于0的正整数，如果为-1表示委托失败\n            注：有订单编号不一定表示成功，具体成功与否需要查看下单回调 on_order_error。\n            例如非交易时间下单可以拿到订单编号，但 on_order_error 回调会报错：\n            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]\n        \"\"\"\n\n        return self.market_trade(security, amount, ttype, is_buy=False)\n\n    def market_trade(self, security: str, amount: int, ttype: str = None, *, is_buy: bool):\n        \"\"\"\n        市价交易\n        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\n\n        :param security: 六位证券代码\n        :param amount: 交易数量\n        :param ttype: 市价委托类型，默认'对手方最优价格委托'\n                 深市可选:\n                - 对手方最优价格委托\n                - 本方最优价格委托\n                - 即时成交剩余撤销委托\n                - 最优五档即时成交剩余撤销\n                - 全额成交或撤销委托\n                 沪市可选:\n                - 对手方最优价格委托\n                - 最优五档即时成交剩余撤销\n                - 最优五档即时成交剩转限价\n                - 本方最优价格委托\n        :return: {'entrust_no': '订单编号'}\n            系统生成的订单编号，成功发送委托后的订单编号为大于0的正整数，如果为-1表示委托失败\n            注：有订单编号不一定表示成功，具体成功与否需要查看下单回调 on_order_error。\n            例如非交易时间下单可以拿到订单编号，但 on_order_error 回调会报错：\n            下单失败回调: order_id=10231, error_id=-61, error_msg=限价买入 [SZ162411] [COUNTER] [12313][当前时间不允许此类证券交易]\n        \"\"\"\n        if ttype is None:\n            ttype = '对手方最优价格委托'\n\n        action = \"市价买入\" if is_buy else \"市价卖出\"\n        logger.info(f\"{action}请求: 股票代码={security}, 委托类型={ttype}, 数量={amount}\")\n\n        def _get_price_type(security: str, ttype: str) -> int:\n            \"\"\"报价类型\"\"\"\n            exchange = get_stock_type(security)\n            if ttype not in MARKET_ORDER_TYPE_NAME_MAP[exchange]:\n                raise ValueError(f\"{exchange}市场不支持的市价委托类型: {ttype}\")\n            return MARKET_ORDER_TYPE_NAME_MAP[exchange][ttype]\n\n        order_id = self._trader.order_stock(\n            account=self._account,\n            stock_code=self._get_stock_code(security),\n            order_type=xtconstant.STOCK_BUY if is_buy else xtconstant.STOCK_SELL,\n            order_volume=amount,\n            price_type=_get_price_type(security, ttype),\n            price=0,\n        )\n        \n        if order_id > 0:\n            logger.info(f\"{action}委托成功: 股票代码={security}, 委托单号={order_id}\")\n        else:\n            logger.error(f\"{action}委托失败: 股票代码={security}, 错误码={order_id}\")\n            \n        return {'entrust_no': order_id}\n\n    @perf_clock\n    def cancel_entrust(self, entrust_no: int):\n        \"\"\"\n        撤销委托单\n        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\n\n        :param entrust_no: 委托单号，由买入或卖出函数返回\n        :return: {'success': True/False, 'message': '撤单结果'}\n                 True: 成功发出撤单指令，False: 撤单失败\n        \"\"\"\n        result = self._trader.cancel_order_stock(self._account, entrust_no)\n        # 根据官方文档，0表示成功，-1表示失败\n        if result == 0:\n            return {'success': True, 'message': 'success'}\n        else:\n            return {'success': False, 'message': 'failed'}\n\n    def _get_stock_code(self, security: str) -> str:\n        \"\"\"\n        获取股票代码\n        :param security: 六位证券代码\n        :return: 格式化的股票代码\n        \"\"\"\n        return f'{security}.{get_stock_type(security).upper()}'\n"
  },
  {
    "path": "easytrader/pop_dialog_handler.py",
    "content": "# coding:utf-8\nimport re\nimport time\nfrom typing import Optional\n\nfrom easytrader import exceptions\nfrom easytrader.utils.perf import perf_clock\nfrom easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines\n\n\nclass PopDialogHandler:\n    def __init__(self, app):\n        self._app = app\n\n    @staticmethod\n    def _set_foreground(window):\n        if window.has_style(win32defines.WS_MINIMIZE):  # if minimized\n            ShowWindow(window.wrapper_object(), 9)  # restore window state\n        else:\n            SetForegroundWindow(window.wrapper_object())  # bring to front\n\n    @perf_clock\n    def handle(self, title):\n        if any(s in title for s in {\"提示信息\", \"委托确认\", \"网上交易用户协议\", \"撤单确认\"}):\n            self._submit_by_shortcut()\n            return None\n\n        if \"提示\" in title:\n            content = self._extract_content()\n            self._submit_by_click()\n            return {\"message\": content}\n\n        content = self._extract_content()\n        self._close()\n        return {\"message\": \"unknown message: {}\".format(content)}\n\n    def _extract_content(self):\n        return self._app.top_window().Static.window_text()\n\n    @staticmethod\n    def _extract_entrust_id(content):\n        return re.search(r\"[\\da-zA-Z]+\", content).group()\n\n    def _submit_by_click(self):\n        try:\n            self._app.top_window()[\"确定\"].click()\n        except Exception as ex:\n            self._app.Window_(best_match=\"Dialog\", top_level_only=True).ChildWindow(\n                best_match=\"确定\"\n            ).click()\n\n    def _submit_by_shortcut(self):\n        self._set_foreground(self._app.top_window())\n        self._app.top_window().type_keys(\"%Y\", set_foreground=False)\n\n    def _close(self):\n        self._app.top_window().close()\n\n\nclass TradePopDialogHandler(PopDialogHandler):\n    @perf_clock\n    def handle(self, title) -> Optional[dict]:\n        if title == \"委托确认\":\n            self._submit_by_shortcut()\n            return None\n\n        if title == \"提示信息\":\n            content = self._extract_content()\n            if \"超出涨跌停\" in content:\n                self._submit_by_shortcut()\n                return None\n\n            if \"委托价格的小数价格应为\" in content:\n                self._submit_by_shortcut()\n                return None\n\n            if \"逆回购\" in content:\n                self._submit_by_shortcut()\n                return None\n\n            if \"正回购\" in content:\n                self._submit_by_shortcut()\n                return None\n\n            return None\n\n        if title == \"提示\":\n            content = self._extract_content()\n            if \"成功\" in content:\n                entrust_no = self._extract_entrust_id(content)\n                self._submit_by_click()\n                return {\"entrust_no\": entrust_no}\n\n            self._submit_by_click()\n            time.sleep(0.05)\n            raise exceptions.TradeError(content)\n        self._close()\n        return None\n"
  },
  {
    "path": "easytrader/refresh_strategies.py",
    "content": "# -*- coding: utf-8 -*-\nimport abc\nimport io\nimport tempfile\nfrom io import StringIO\nfrom typing import TYPE_CHECKING, Dict, List, Optional\n\nimport pandas as pd\nimport pywinauto.keyboard\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader.log import logger\nfrom easytrader.utils.captcha import captcha_recognize\nfrom easytrader.utils.win_gui import SetForegroundWindow, ShowWindow, win32defines\n\nif TYPE_CHECKING:\n    # pylint: disable=unused-import\n    from easytrader import clienttrader\n\n\nclass IRefreshStrategy(abc.ABC):\n    _trader: \"clienttrader.ClientTrader\"\n\n    @abc.abstractmethod\n    def refresh(self):\n        \"\"\"\n        刷新数据\n        \"\"\"\n        pass\n\n    def set_trader(self, trader: \"clienttrader.ClientTrader\"):\n        self._trader = trader\n\n\n# noinspection PyProtectedMember\nclass Switch(IRefreshStrategy):\n    \"\"\"通过切换菜单栏刷新\"\"\"\n\n    def __init__(self, sleep: float = 0.1):\n        self.sleep = sleep\n\n    def refresh(self):\n        self._trader._switch_left_menus_by_shortcut(\"{F5}\", sleep=self.sleep)\n\n\n# noinspection PyProtectedMember\nclass Toolbar(IRefreshStrategy):\n    \"\"\"通过点击工具栏刷新按钮刷新\"\"\"\n\n    def __init__(self, refresh_btn_index: int = 4):\n        \"\"\"\n        :param refresh_btn_index:\n            交易客户端工具栏中“刷新”排序，默认为第4个，请根据自己实际调整\n        \"\"\"\n        self.refresh_btn_index = refresh_btn_index\n\n    def refresh(self):\n        self._trader._toolbar.button(self.refresh_btn_index - 1).click()\n"
  },
  {
    "path": "easytrader/remoteclient.py",
    "content": "# -*- coding: utf-8 -*-\nimport requests\n\nfrom easytrader.utils.misc import file2dict\n\n\ndef use(broker, host, port=1430, **kwargs):\n    return RemoteClient(broker, host, port, **kwargs)\n\n\nclass RemoteClient:\n    def __init__(self, broker, host, port=1430, **kwargs):\n        self._s = requests.session()\n        # 支持 basic auth 或 其它 auth 方法\n        if kwargs.get(\"user\") and kwargs.get(\"passwd\"):\n            self._s.auth = requests.auth.HTTPBasicAuth(\n                kwargs.get(\"user\"), kwargs.get(\"passwd\")\n            )\n        elif kwargs.get(\"auth\"):\n            self._s.auth = kwargs.get(\"auth\")\n\n        # 支持 ssl (有时候需要过某些反向代理要用https协议)\n        self._api = f\"http{'s' if kwargs.get('ssl') is True else ''}://{host}:{port}\"\n        self._broker = broker\n\n    def prepare(\n        self,\n        config_path=None,\n        user=None,\n        password=None,\n        exe_path=None,\n        comm_password=None,\n        **kwargs,\n    ):\n        \"\"\"\n        登陆客户端\n        :param config_path: 登陆配置文件，跟参数登陆方式二选一\n        :param user: 账号\n        :param password: 明文密码\n        :param exe_path: 客户端路径类似 r'C:\\\\htzqzyb2\\\\xiadan.exe',\n            默认 r'C:\\\\htzqzyb2\\\\xiadan.exe'\n        :param comm_password: 通讯密码\n        :return:\n        \"\"\"\n        params = locals().copy()\n        params.pop(\"self\")\n\n        if config_path is not None:\n            account = file2dict(config_path)\n            params[\"user\"] = account[\"user\"]\n            params[\"password\"] = account[\"password\"]\n\n        params[\"broker\"] = self._broker\n\n        response = self._s.post(self._api + \"/prepare\", json=params)\n        if response.status_code >= 300:\n            raise Exception(response.json()[\"error\"])\n        return response.json()\n\n    @property\n    def balance(self):\n        return self.common_get(\"balance\")\n\n    @property\n    def position(self):\n        return self.common_get(\"position\")\n\n    @property\n    def today_entrusts(self):\n        return self.common_get(\"today_entrusts\")\n\n    @property\n    def today_trades(self):\n        return self.common_get(\"today_trades\")\n\n    @property\n    def cancel_entrusts(self):\n        return self.common_get(\"cancel_entrusts\")\n\n    def auto_ipo(self):\n        return self.common_get(\"auto_ipo\")\n\n    def exit(self):\n        return self.common_get(\"exit\")\n\n    def common_get(self, endpoint):\n        response = self._s.get(self._api + \"/\" + endpoint)\n        if response.status_code >= 300:\n            raise Exception(response.json()[\"error\"])\n        return response.json()\n\n    def buy(self, security, price, amount, **kwargs):\n        params = locals().copy()\n        params.pop(\"self\")\n\n        response = self._s.post(self._api + \"/buy\", json=params)\n        if response.status_code >= 300:\n            raise Exception(response.json()[\"error\"])\n        return response.json()\n\n    def sell(self, security, price, amount, **kwargs):\n        params = locals().copy()\n        params.pop(\"self\")\n\n        response = self._s.post(self._api + \"/sell\", json=params)\n        if response.status_code >= 300:\n            raise Exception(response.json()[\"error\"])\n        return response.json()\n\n    def market_buy(self, security, amount, **kwargs):\n        params = locals().copy()\n        params.pop(\"self\")\n\n        response = self._s.post(self._api + \"/market_buy\", json=params)\n        if response.status_code >= 300:\n            raise Exception(response.json()[\"error\"])\n        return response.json()\n\n    def market_sell(self, security, amount, **kwargs):\n        params = locals().copy()\n        params.pop(\"self\")\n\n        response = self._s.post(self._api + \"/market_sell\", json=params)\n        if response.status_code >= 300:\n            raise Exception(response.json()[\"error\"])\n        return response.json()\n\n    def cancel_entrust(self, entrust_no):\n        params = locals().copy()\n        params.pop(\"self\")\n\n        response = self._s.post(self._api + \"/cancel_entrust\", json=params)\n        if response.status_code >= 300:\n            raise Exception(response.json()[\"error\"])\n        return response.json()\n"
  },
  {
    "path": "easytrader/ricequant_follower.py",
    "content": "# -*- coding: utf-8 -*-\n\nfrom datetime import datetime\nfrom threading import Thread\n\nfrom easytrader.follower import BaseFollower\nfrom easytrader.log import logger\n\n\nclass RiceQuantFollower(BaseFollower):\n    def __init__(self):\n        super().__init__()\n        self.client = None\n\n    def login(self, user=None, password=None, **kwargs):\n        from rqopen_client import RQOpenClient\n\n        self.client = RQOpenClient(user, password, logger=logger)\n\n    def follow(\n        self,\n        users,\n        run_id,\n        track_interval=1,\n        trade_cmd_expire_seconds=120,\n        cmd_cache=True,\n        entrust_prop=\"limit\",\n        send_interval=0,\n    ):\n        \"\"\"跟踪ricequant对应的模拟交易，支持多用户多策略\n        :param users: 支持easytrader的用户对象，支持使用 [] 指定多个用户\n        :param run_id: ricequant 的模拟交易ID，支持使用 [] 指定多个模拟交易\n        :param track_interval: 轮训模拟交易时间，单位为秒\n        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒\n        :param cmd_cache: 是否读取存储历史执行过的指令，防止重启时重复执行已经交易过的指令\n        :param entrust_prop: 委托方式, 'limit' 为限价，'market' 为市价, 仅在银河实现\n        :param send_interval: 交易发送间隔， 默认为0s。调大可防止卖出买入时卖出单没有及时成交导致的买入金额不足\n        \"\"\"\n        users = self.warp_list(users)\n        run_ids = self.warp_list(run_id)\n\n        if cmd_cache:\n            self.load_expired_cmd_cache()\n\n        self.start_trader_thread(\n            users, trade_cmd_expire_seconds, entrust_prop, send_interval\n        )\n\n        workers = []\n        for id_ in run_ids:\n            strategy_name = self.extract_strategy_name(id_)\n            strategy_worker = Thread(\n                target=self.track_strategy_worker,\n                args=[id_, strategy_name],\n                kwargs={\"interval\": track_interval},\n            )\n            strategy_worker.start()\n            workers.append(strategy_worker)\n            logger.info(\"开始跟踪策略: %s\", strategy_name)\n        for worker in workers:\n            worker.join()\n\n    def extract_strategy_name(self, run_id):\n        ret_json = self.client.get_positions(run_id)\n        if ret_json[\"code\"] != 200:\n            logger.error(\n                \"fetch data from run_id %s fail, msg %s\",\n                run_id,\n                ret_json[\"msg\"],\n            )\n            raise RuntimeError(ret_json[\"msg\"])\n        return ret_json[\"resp\"][\"name\"]\n\n    def extract_day_trades(self, run_id):\n        ret_json = self.client.get_day_trades(run_id)\n        if ret_json[\"code\"] != 200:\n            logger.error(\n                \"fetch day trades from run_id %s fail, msg %s\",\n                run_id,\n                ret_json[\"msg\"],\n            )\n            raise RuntimeError(ret_json[\"msg\"])\n        return ret_json[\"resp\"][\"trades\"]\n\n    def query_strategy_transaction(self, strategy, **kwargs):\n        transactions = self.extract_day_trades(strategy)\n        transactions = self.project_transactions(transactions, **kwargs)\n        return self.order_transactions_sell_first(transactions)\n\n    @staticmethod\n    def stock_shuffle_to_prefix(stock):\n        assert (\n            len(stock) == 11\n        ), \"stock {} must like 123456.XSHG or 123456.XSHE\".format(stock)\n        code = stock[:6]\n        if stock.find(\"XSHG\") != -1:\n            return \"sh\" + code\n        if stock.find(\"XSHE\") != -1:\n            return \"sz\" + code\n        raise TypeError(\"not valid stock code: {}\".format(code))\n\n    def project_transactions(self, transactions, **kwargs):\n        new_transactions = []\n        for transaction in transactions:\n            new_transaction = {}\n            new_transaction[\"price\"] = transaction[\"price\"]\n            new_transaction[\"amount\"] = int(abs(transaction[\"quantity\"]))\n            new_transaction[\"datetime\"] = datetime.strptime(\n                transaction[\"time\"], \"%Y-%m-%d %H:%M:%S\"\n            )\n            new_transaction[\"stock_code\"] = self.stock_shuffle_to_prefix(\n                transaction[\"order_book_id\"]\n            )\n            new_transaction[\"action\"] = (\n                \"buy\" if transaction[\"quantity\"] > 0 else \"sell\"\n            )\n            new_transactions.append(new_transaction)\n\n        return new_transactions\n"
  },
  {
    "path": "easytrader/server.py",
    "content": "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 = Flask(__name__)\r\n\r\nglobal_store = {}\r\n\r\n\r\ndef error_handle(func):\r\n    @functools.wraps(func)\r\n    def wrapper(*args, **kwargs):\r\n        try:\r\n            return func(*args, **kwargs)\r\n        # pylint: disable=broad-except\r\n        except Exception as e:\r\n            logger.exception(\"server error\")\r\n            message = \"{}: {}\".format(e.__class__, e)\r\n            return jsonify({\"error\": message}), 400\r\n\r\n    return wrapper\r\n\r\n\r\n@app.route(\"/prepare\", methods=[\"POST\"])\r\n@error_handle\r\ndef post_prepare():\r\n    json_data = request.get_json(force=True)\r\n\r\n    user = api.use(json_data.pop(\"broker\"))\r\n    user.prepare(**json_data)\r\n\r\n    global_store[\"user\"] = user\r\n    return jsonify({\"msg\": \"login success\"}), 201\r\n\r\n\r\n@app.route(\"/balance\", methods=[\"GET\"])\r\n@error_handle\r\ndef get_balance():\r\n    user = global_store[\"user\"]\r\n    balance = user.balance\r\n\r\n    return jsonify(balance), 200\r\n\r\n\r\n@app.route(\"/position\", methods=[\"GET\"])\r\n@error_handle\r\ndef get_position():\r\n    user = global_store[\"user\"]\r\n    position = user.position\r\n\r\n    return jsonify(position), 200\r\n\r\n\r\n@app.route(\"/auto_ipo\", methods=[\"GET\"])\r\n@error_handle\r\ndef get_auto_ipo():\r\n    user = global_store[\"user\"]\r\n    res = user.auto_ipo()\r\n\r\n    return jsonify(res), 200\r\n\r\n\r\n@app.route(\"/today_entrusts\", methods=[\"GET\"])\r\n@error_handle\r\ndef get_today_entrusts():\r\n    user = global_store[\"user\"]\r\n    today_entrusts = user.today_entrusts\r\n\r\n    return jsonify(today_entrusts), 200\r\n\r\n\r\n@app.route(\"/today_trades\", methods=[\"GET\"])\r\n@error_handle\r\ndef get_today_trades():\r\n    user = global_store[\"user\"]\r\n    today_trades = user.today_trades\r\n\r\n    return jsonify(today_trades), 200\r\n\r\n\r\n@app.route(\"/cancel_entrusts\", methods=[\"GET\"])\r\n@error_handle\r\ndef get_cancel_entrusts():\r\n    user = global_store[\"user\"]\r\n    cancel_entrusts = user.cancel_entrusts\r\n\r\n    return jsonify(cancel_entrusts), 200\r\n\r\n\r\n@app.route(\"/buy\", methods=[\"POST\"])\r\n@error_handle\r\ndef post_buy():\r\n    json_data = request.get_json(force=True)\r\n    user = global_store[\"user\"]\r\n    res = user.buy(**json_data)\r\n\r\n    return jsonify(res), 201\r\n\r\n\r\n@app.route(\"/sell\", methods=[\"POST\"])\r\n@error_handle\r\ndef post_sell():\r\n    json_data = request.get_json(force=True)\r\n\r\n    user = global_store[\"user\"]\r\n    res = user.sell(**json_data)\r\n\r\n    return jsonify(res), 201\r\n\r\n\r\n@app.route(\"/cancel_entrust\", methods=[\"POST\"])\r\n@error_handle\r\ndef post_cancel_entrust():\r\n    json_data = request.get_json(force=True)\r\n\r\n    user = global_store[\"user\"]\r\n    res = user.cancel_entrust(**json_data)\r\n\r\n    return jsonify(res), 201\r\n\r\n\r\n@app.route(\"/exit\", methods=[\"GET\"])\r\n@error_handle\r\ndef get_exit():\r\n    user = global_store[\"user\"]\r\n    user.exit()\r\n\r\n    return jsonify({\"msg\": \"exit success\"}), 200\r\n\r\n\r\ndef run(port=1430):\r\n    app.run(host=\"0.0.0.0\", port=port)\r\n"
  },
  {
    "path": "easytrader/universal_clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport pywinauto\nimport pywinauto.clipboard\n\nfrom easytrader import grid_strategies\nfrom . import clienttrader\n\n\nclass UniversalClientTrader(clienttrader.BaseLoginClientTrader):\n    grid_strategy = grid_strategies.Xls\n\n    @property\n    def broker_type(self):\n        return \"universal\"\n\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\n        \"\"\"\n        :param user: 用户名\n        :param password: 密码\n        :param exe_path: 客户端路径, 类似\n        :param comm_password:\n        :param kwargs:\n        :return:\n        \"\"\"\n        self._editor_need_type_keys = False\n\n        try:\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=1\n            )\n        # pylint: disable=broad-except\n        except Exception:\n            self._app = pywinauto.Application().start(exe_path)\n\n            # wait login window ready\n            while True:\n                try:\n                    login_window = pywinauto.findwindows.find_window(class_name='#32770', found_index=1)\n                    break\n                except:\n                    self.wait(1)\n\n            self.wait(1)\n            self._app.window(handle=login_window).Edit1.set_focus()\n            self._app.window(handle=login_window).Edit1.type_keys(user)\n\n            self._app.window(handle=login_window).button7.click()\n\n            # detect login is success or not\n            # self._app.top_window().wait_not(\"exists\", 100)\n            self.wait(5)\n\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=10\n            )\n\n        self._close_prompt_windows()\n        self._main = self._app.window(title=\"网上股票交易系统5.0\")\n\n"
  },
  {
    "path": "easytrader/utils/__init__.py",
    "content": "\r\n"
  },
  {
    "path": "easytrader/utils/captcha.py",
    "content": "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):\r\n    import pytesseract\r\n\r\n    im = Image.open(img_path).convert(\"L\")\r\n    # 1. threshold the image\r\n    threshold = 200\r\n    table = []\r\n    for i in range(256):\r\n        if i < threshold:\r\n            table.append(0)\r\n        else:\r\n            table.append(1)\r\n\r\n    out = im.point(table, \"1\")\r\n    # 2. recognize with tesseract\r\n    num = pytesseract.image_to_string(out)\r\n    return num\r\n\r\n\r\ndef recognize_verify_code(image_path, broker=\"ht\"):\r\n    \"\"\"识别验证码，返回识别后的字符串，使用 tesseract 实现\r\n    :param image_path: 图片路径\r\n    :param broker: 券商 ['ht', 'yjb', 'gf', 'yh']\r\n    :return recognized: verify code string\"\"\"\r\n\r\n    if broker == \"gf\":\r\n        return detect_gf_result(image_path)\r\n    if broker in [\"yh_client\", \"gj_client\"]:\r\n        return detect_yh_client_result(image_path)\r\n    # 调用 tesseract 识别\r\n    return default_verify_code_detect(image_path)\r\n\r\n\r\ndef detect_yh_client_result(image_path):\r\n    \"\"\"封装了tesseract的识别，部署在阿里云上，\r\n    服务端源码地址为： https://github.com/shidenggui/yh_verify_code_docker\"\"\"\r\n    api = \"http://yh.ez.shidenggui.com:5000/yh_client\"\r\n    with open(image_path, \"rb\") as f:\r\n        rep = requests.post(api, files={\"image\": f})\r\n    if rep.status_code != 201:\r\n        error = rep.json()[\"message\"]\r\n        raise exceptions.TradeError(\"request {} error: {}\".format(api, error))\r\n    return rep.json()[\"result\"]\r\n\r\n\r\ndef input_verify_code_manual(image_path):\r\n    from PIL import Image\r\n\r\n    image = Image.open(image_path)\r\n    image.show()\r\n    code = input(\r\n        \"image path: {}, input verify code answer:\".format(image_path)\r\n    )\r\n    return code\r\n\r\n\r\ndef default_verify_code_detect(image_path):\r\n    from PIL import Image\r\n\r\n    img = Image.open(image_path)\r\n    return invoke_tesseract_to_recognize(img)\r\n\r\n\r\ndef detect_gf_result(image_path):\r\n    from PIL import ImageFilter, Image\r\n\r\n    img = Image.open(image_path)\r\n    if hasattr(img, \"width\"):\r\n        width, height = img.width, img.height\r\n    else:\r\n        width, height = img.size\r\n    for x in range(width):\r\n        for y in range(height):\r\n            if img.getpixel((x, y)) < (100, 100, 100):\r\n                img.putpixel((x, y), (256, 256, 256))\r\n    gray = img.convert(\"L\")\r\n    two = gray.point(lambda p: 0 if 68 < p < 90 else 256)\r\n    min_res = two.filter(ImageFilter.MinFilter)\r\n    med_res = min_res.filter(ImageFilter.MedianFilter)\r\n    for _ in range(2):\r\n        med_res = med_res.filter(ImageFilter.MedianFilter)\r\n    return invoke_tesseract_to_recognize(med_res)\r\n\r\n\r\ndef invoke_tesseract_to_recognize(img):\r\n    import pytesseract\r\n\r\n    try:\r\n        res = pytesseract.image_to_string(img)\r\n    except FileNotFoundError:\r\n        raise Exception(\r\n            \"tesseract 未安装，请至 https://github.com/tesseract-ocr/tesseract/wiki 查看安装教程\"\r\n        )\r\n    valid_chars = re.findall(\"[0-9a-z]\", res, re.IGNORECASE)\r\n    return \"\".join(valid_chars)\r\n"
  },
  {
    "path": "easytrader/utils/misc.py",
    "content": "# coding:utf-8\nimport json\n\n\ndef parse_cookies_str(cookies):\n    \"\"\"\n    parse cookies str to dict\n    :param cookies: cookies str\n    :type cookies: str\n    :return: cookie dict\n    :rtype: dict\n    \"\"\"\n    cookie_dict = {}\n    for record in cookies.split(\";\"):\n        key, value = record.strip().split(\"=\", 1)\n        cookie_dict[key] = value\n    return cookie_dict\n\n\ndef file2dict(path):\n    with open(path, encoding=\"utf-8\") as f:\n        return json.load(f)\n\n\ndef grep_comma(num_str):\n    return num_str.replace(\",\", \"\")\n\n\ndef str2num(num_str, convert_type=\"float\"):\n    num = float(grep_comma(num_str))\n    return num if convert_type == \"float\" else int(num)\n"
  },
  {
    "path": "easytrader/utils/perf.py",
    "content": "# coding:utf-8\nimport functools\nimport inspect\nimport logging\nimport timeit\n\nfrom easytrader import logger\n\ntry:\n    from time import process_time\nexcept:\n    from time import clock as process_time\n\n\ndef perf_clock(f):\n    @functools.wraps(f)\n    def wrapper(*args, **kwargs):\n        if not logger.isEnabledFor(logging.DEBUG):\n            return f(*args, **kwargs)\n\n        ts = timeit.default_timer()\n        cs = process_time()\n        ex = None\n        result = None\n\n        try:\n            result = f(*args, **kwargs)\n        except Exception as ex1:\n            ex = ex1\n\n        te = timeit.default_timer()\n        ce = process_time()\n        logger.debug(\n            \"%r consume %2.4f sec, cpu %2.4f sec. args %s, extra args %s\"\n            % (\n                f.__name__,\n                te - ts,\n                ce - cs,\n                args[1:],\n                kwargs,\n            )\n        )\n        if ex is not None:\n            raise ex\n        return result\n    \n    wrapper.__signature__ = inspect.signature(f)\n    return wrapper\n"
  },
  {
    "path": "easytrader/utils/stock.py",
    "content": "# coding:utf-8\nimport datetime\nimport json\nimport random\n\nimport requests\n\n\ndef get_stock_type(stock_code):\n    \"\"\"判断股票ID对应的证券市场\n    匹配规则\n    ['4'， '8'] 为 bj\n    ['5', '6', '7', '9', '110', '113', '118', '132', '204'] 为 sh\n    其余为 sz\n    :param stock_code:股票ID, 若以 'sz', 'sh', 'bj' 开头直接返回对应类型，否则使用内置规则判断\n    :return 'bj', 'sh' or 'sz'\"\"\"\n    assert isinstance(stock_code, str), \"stock code need str type\"\n    bj_head = (\"43\", \"83\", \"87\", \"92\")\n    sh_head = (\"5\", \"6\", \"7\", \"9\", \"110\", \"113\", \"118\", \"132\", \"204\")\n    if stock_code.startswith((\"sh\", \"sz\", \"zz\", \"bj\")):\n        return stock_code[:2]\n    elif stock_code.startswith(bj_head):\n        return \"bj\"\n    elif stock_code.startswith(sh_head):\n        return \"sh\"\n    return \"sz\"\n\ndef get_30_date():\n    \"\"\"\n    获得用于查询的默认日期, 今天的日期, 以及30天前的日期\n    用于查询的日期格式通常为 20160211\n    :return:\n    \"\"\"\n    now = datetime.datetime.now()\n    end_date = now.date()\n    start_date = end_date - datetime.timedelta(days=30)\n    return start_date.strftime(\"%Y%m%d\"), end_date.strftime(\"%Y%m%d\")\n\n\ndef get_today_ipo_data():\n    \"\"\"\n    查询今天可以申购的新股信息\n    :return: 今日可申购新股列表 apply_code申购代码 price发行价格\n    \"\"\"\n\n    agent = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0\"\n    send_headers = {\n        \"Host\": \"xueqiu.com\",\n        \"User-Agent\": agent,\n        \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n        \"Accept-Language\": \"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3\",\n        \"Accept-Encoding\": \"deflate\",\n        \"Cache-Control\": \"no-cache\",\n        \"X-Requested-With\": \"XMLHttpRequest\",\n        \"Referer\": \"https://xueqiu.com/hq\",\n        \"Connection\": \"keep-alive\",\n    }\n\n    timestamp = random.randint(1000000000000, 9999999999999)\n    home_page_url = \"https://xueqiu.com\"\n    ipo_data_url = (\n        \"https://xueqiu.com/proipo/query.json?column=symbol,name,onl_subcode,onl_subbegdate,actissqty,onl\"\n        \"_actissqty,onl_submaxqty,iss_price,onl_lotwiner_stpub_date,onl_lotwinrt,onl_lotwin_amount,stock_\"\n        \"income&orderBy=onl_subbegdate&order=desc&stockType=&page=1&size=30&_=%s\"\n        % (str(timestamp))\n    )\n\n    session = requests.session()\n    session.get(home_page_url, headers=send_headers)  # 产生cookies\n    ipo_response = session.post(ipo_data_url, headers=send_headers)\n\n    json_data = json.loads(ipo_response.text)\n    today_ipo = []\n\n    for line in json_data.get(\"data\", []):\n        if datetime.datetime.now().strftime(\"%a %b %d\") == line[3][:10]:\n            today_ipo.append(\n                {\n                    \"stock_code\": line[0],\n                    \"stock_name\": line[1],\n                    \"apply_code\": line[2],\n                    \"price\": line[7],\n                }\n            )\n\n    return today_ipo\n"
  },
  {
    "path": "easytrader/utils/win_gui.py",
    "content": "# coding:utf-8\nfrom pywinauto import win32defines\nfrom pywinauto.win32functions import SetForegroundWindow, ShowWindow\n"
  },
  {
    "path": "easytrader/webtrader.py",
    "content": "# -*- coding: utf-8 -*-\nimport abc\nimport logging\nimport os\nimport re\nimport time\nfrom threading import Thread\n\nimport requests\nimport requests.exceptions\n\nfrom easytrader import exceptions\nfrom easytrader.log import logger\nfrom easytrader.utils.misc import file2dict, str2num\nfrom easytrader.utils.stock import get_30_date\n\n\n# noinspection PyIncorrectDocstring\nclass WebTrader(metaclass=abc.ABCMeta):\n    global_config_path = os.path.dirname(__file__) + \"/config/global.json\"\n    config_path = \"\"\n\n    def __init__(self, debug=True):\n        self.__read_config()\n        self.trade_prefix = self.config[\"prefix\"]\n        self.account_config = \"\"\n        self.heart_active = True\n        self.heart_thread = Thread(target=self.send_heartbeat)\n        self.heart_thread.setDaemon(True)\n\n        self.log_level = logging.DEBUG if debug else logging.INFO\n\n    def read_config(self, path):\n        try:\n            self.account_config = file2dict(path)\n        except ValueError:\n            logger.error(\"配置文件格式有误，请勿使用记事本编辑，推荐 sublime text\")\n        for value in self.account_config:\n            if isinstance(value, int):\n                logger.warning(\"配置文件的值最好使用双引号包裹，使用字符串，否则可能导致不可知问题\")\n\n    def prepare(self, config_file=None, user=None, password=None, **kwargs):\n        \"\"\"登录的统一接口\n        :param config_file 登录数据文件，若无则选择参数登录模式\n        :param user: 各家券商的账号\n        :param password: 密码, 券商为加密后的密码\n        :param cookies: [雪球登录需要]雪球登录需要设置对应的 cookies\n        :param portfolio_code: [雪球登录需要]组合代码\n        :param portfolio_market: [雪球登录需要]交易市场，\n            可选['cn', 'us', 'hk'] 默认 'cn'\n        \"\"\"\n        if config_file is not None:\n            self.read_config(config_file)\n        else:\n            self._prepare_account(user, password, **kwargs)\n        self.autologin()\n\n    def _prepare_account(self, user, password, **kwargs):\n        \"\"\"映射用户名密码到对应的字段\"\"\"\n        raise Exception(\"支持参数登录需要实现此方法\")\n\n    def autologin(self, limit=10):\n        \"\"\"实现自动登录\n        :param limit: 登录次数限制\n        \"\"\"\n        for _ in range(limit):\n            if self.login():\n                break\n        else:\n            raise exceptions.NotLoginError(\n                \"登录失败次数过多, 请检查密码是否正确 / 券商服务器是否处于维护中 / 网络连接是否正常\"\n            )\n        self.keepalive()\n\n    def login(self):\n        pass\n\n    def keepalive(self):\n        \"\"\"启动保持在线的进程 \"\"\"\n        if self.heart_thread.is_alive():\n            self.heart_active = True\n        else:\n            self.heart_thread.start()\n\n    def send_heartbeat(self):\n        \"\"\"每隔10秒查询指定接口保持 token 的有效性\"\"\"\n        while True:\n            if self.heart_active:\n                self.check_login()\n            else:\n                time.sleep(1)\n\n    def check_login(self, sleepy=30):\n        logger.setLevel(logging.ERROR)\n        try:\n            response = self.heartbeat()\n            self.check_account_live(response)\n        except requests.exceptions.ConnectionError:\n            pass\n        except requests.exceptions.RequestException as e:\n            logger.setLevel(self.log_level)\n            logger.error(\"心跳线程发现账户出现错误: %s %s, 尝试重新登陆\", e.__class__, e)\n            self.autologin()\n        finally:\n            logger.setLevel(self.log_level)\n        time.sleep(sleepy)\n\n    def heartbeat(self):\n        return self.balance\n\n    def check_account_live(self, response):\n        pass\n\n    def exit(self):\n        \"\"\"结束保持 token 在线的进程\"\"\"\n        self.heart_active = False\n\n    def __read_config(self):\n        \"\"\"读取 config\"\"\"\n        self.config = file2dict(self.config_path)\n        self.global_config = file2dict(self.global_config_path)\n        self.config.update(self.global_config)\n\n    @property\n    def balance(self):\n        return self.get_balance()\n\n    def get_balance(self):\n        \"\"\"获取账户资金状况\"\"\"\n        return self.do(self.config[\"balance\"])\n\n    @property\n    def position(self):\n        return self.get_position()\n\n    def get_position(self):\n        \"\"\"获取持仓\"\"\"\n        return self.do(self.config[\"position\"])\n\n    @property\n    def entrust(self):\n        return self.get_entrust()\n\n    def get_entrust(self):\n        \"\"\"获取当日委托列表\"\"\"\n        return self.do(self.config[\"entrust\"])\n\n    @property\n    def current_deal(self):\n        return self.get_current_deal()\n\n    def get_current_deal(self):\n        \"\"\"获取当日委托列表\"\"\"\n        # return self.do(self.config['current_deal'])\n        logger.warning(\"目前仅在 佣金宝/银河子类 中实现, 其余券商需要补充\")\n\n    @property\n    def exchangebill(self):\n        \"\"\"\n        默认提供最近30天的交割单, 通常只能返回查询日期内最新的 90 天数据。\n        :return:\n        \"\"\"\n        # TODO 目前仅在 华泰子类 中实现\n        start_date, end_date = get_30_date()\n        return self.get_exchangebill(start_date, end_date)\n\n    def get_exchangebill(self, start_date, end_date):\n        \"\"\"\n        查询指定日期内的交割单\n        :param start_date: 20160211\n        :param end_date: 20160211\n        :return:\n        \"\"\"\n        logger.warning(\"目前仅在 华泰子类 中实现, 其余券商需要补充\")\n\n    def get_ipo_limit(self, stock_code):\n        \"\"\"\n        查询新股申购额度申购上限\n        :param stock_code: 申购代码 ID\n        :return:\n        \"\"\"\n        logger.warning(\"目前仅在 佣金宝子类 中实现, 其余券商需要补充\")\n\n    def do(self, params):\n        \"\"\"发起对 api 的请求并过滤返回结果\n        :param params: 交易所需的动态参数\"\"\"\n        request_params = self.create_basic_params()\n        request_params.update(params)\n        response_data = self.request(request_params)\n        try:\n            format_json_data = self.format_response_data(response_data)\n        # pylint: disable=broad-except\n        except Exception:\n            # Caused by server force logged out\n            return None\n        return_data = self.fix_error_data(format_json_data)\n        try:\n            self.check_login_status(return_data)\n        except exceptions.NotLoginError:\n            self.autologin()\n        return return_data\n\n    def create_basic_params(self) -> dict:\n        \"\"\"生成基本的参数\"\"\"\n        return {}\n\n    def request(self, params) -> dict:\n        \"\"\"请求并获取 JSON 数据\n        :param params: Get 参数\"\"\"\n        return {}\n\n    def format_response_data(self, data):\n        \"\"\"格式化返回的 json 数据\n        :param data: 请求返回的数据 \"\"\"\n        return data\n\n    def fix_error_data(self, data):\n        \"\"\"若是返回错误移除外层的列表\n        :param data: 需要判断是否包含错误信息的数据\"\"\"\n        return data\n\n    def format_response_data_type(self, response_data):\n        \"\"\"格式化返回的值为正确的类型\n        :param response_data: 返回的数据\n        \"\"\"\n        if isinstance(response_data, list) and not isinstance(\n            response_data, str\n        ):\n            return response_data\n\n        int_match_str = \"|\".join(self.config[\"response_format\"][\"int\"])\n        float_match_str = \"|\".join(self.config[\"response_format\"][\"float\"])\n        for item in response_data:\n            for key in item:\n                try:\n                    if re.search(int_match_str, key) is not None:\n                        item[key] = str2num(item[key], \"int\")\n                    elif re.search(float_match_str, key) is not None:\n                        item[key] = str2num(item[key], \"float\")\n                except ValueError:\n                    continue\n        return response_data\n\n    def check_login_status(self, return_data):\n        pass\n"
  },
  {
    "path": "easytrader/wk_clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\nimport pywinauto\n\nfrom easytrader.ht_clienttrader import HTClientTrader\n\n\nclass WKClientTrader(HTClientTrader):\n    @property\n    def broker_type(self):\n        return \"wk\"\n\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\n        \"\"\"\n                :param user: 用户名\n                :param password: 密码\n                :param exe_path: 客户端路径, 类似\n                :param comm_password:\n                :param kwargs:\n                :return:\n                \"\"\"\n        self._editor_need_type_keys = False\n        if comm_password is None:\n            raise ValueError(\"五矿必须设置通讯密码\")\n\n        try:\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=1\n            )\n        # pylint: disable=broad-except\n        except Exception:\n            self._app = pywinauto.Application().start(exe_path)\n\n            # wait login window ready\n            while True:\n                try:\n                    self._app.top_window().Edit1.wait(\"ready\")\n                    break\n                except RuntimeError:\n                    pass\n\n            self._app.top_window().Edit1.set_focus()\n            self._app.top_window().Edit1.set_edit_text(user)\n            self._app.top_window().Edit2.set_edit_text(password)\n\n            self._app.top_window().Edit3.set_edit_text(comm_password)\n\n            self._app.top_window().Button1.click()\n\n            # detect login is success or not\n            self._app.top_window().wait_not(\"exists\", 100)\n\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=10\n            )\n        self._close_prompt_windows()\n        self._main = self._app.window(title=\"网上股票交易系统5.0\")"
  },
  {
    "path": "easytrader/xq_follower.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import division, print_function, unicode_literals\n\nimport json\nimport re\nfrom datetime import datetime\nfrom numbers import Number\nfrom threading import Thread\n\nfrom easytrader.follower import BaseFollower\nfrom easytrader.log import logger\nfrom easytrader.utils.misc import parse_cookies_str\n\n\nclass XueQiuFollower(BaseFollower):\n    LOGIN_PAGE = \"https://www.xueqiu.com\"\n    LOGIN_API = \"https://xueqiu.com/snowman/login\"\n    TRANSACTION_API = \"https://xueqiu.com/cubes/rebalancing/history.json\"\n    PORTFOLIO_URL = \"https://xueqiu.com/p/\"\n    WEB_REFERER = \"https://www.xueqiu.com\"\n\n    def __init__(self):\n        super().__init__()\n        self._adjust_sell = None\n        self._users = None\n\n    def login(self, user=None, password=None, **kwargs):\n        \"\"\"\n        雪球登陆， 需要设置 cookies\n        :param cookies: 雪球登陆需要设置 cookies， 具体见\n            https://smalltool.github.io/2016/08/02/cookie/\n        :return:\n        \"\"\"\n        cookies = kwargs.get(\"cookies\")\n        if cookies is None:\n            raise TypeError(\n                \"雪球登陆需要设置 cookies， 具体见\" \"https://smalltool.github.io/2016/08/02/cookie/\"\n            )\n        headers = self._generate_headers()\n        self.s.headers.update(headers)\n\n        self.s.get(self.LOGIN_PAGE)\n\n        cookie_dict = parse_cookies_str(cookies)\n        self.s.cookies.update(cookie_dict)\n\n        logger.info(\"登录成功\")\n\n    def follow(  # type: ignore\n        self,\n        users,\n        strategies,\n        total_assets=10000,\n        initial_assets=None,\n        adjust_sell=False,\n        track_interval=10,\n        trade_cmd_expire_seconds=120,\n        cmd_cache=True,\n        slippage: float = 0.0,\n    ):\n        \"\"\"跟踪 joinquant 对应的模拟交易，支持多用户多策略\n        :param users: 支持 easytrader 的用户对象，支持使用 [] 指定多个用户\n        :param strategies: 雪球组合名, 类似 ZH123450\n        :param total_assets: 雪球组合对应的总资产， 格式 [组合1对应资金, 组合2对应资金]\n            若 strategies=['ZH000001', 'ZH000002'],\n                设置 total_assets=[10000, 10000], 则表明每个组合对应的资产为 1w 元\n            假设组合 ZH000001 加仓 价格为 p 股票 A 10%,\n                则对应的交易指令为 买入 股票 A 价格 P 股数 1w * 10% / p 并按 100 取整\n        :param adjust_sell: 是否根据用户的实际持仓数调整卖出股票数量，\n            当卖出股票数大于实际持仓数时，调整为实际持仓数。目前仅在银河客户端测试通过。\n            当 users 为多个时，根据第一个 user 的持仓数决定\n        :type adjust_sell: bool\n        :param initial_assets: 雪球组合对应的初始资产,\n            格式 [ 组合1对应资金, 组合2对应资金 ]\n            总资产由 初始资产 × 组合净值 算得， total_assets 会覆盖此参数\n        :param track_interval: 轮训模拟交易时间，单位为秒\n        :param trade_cmd_expire_seconds: 交易指令过期时间, 单位为秒\n        :param cmd_cache: 是否读取存储历史执行过的指令，防止重启时重复执行已经交易过的指令\n        :param slippage: 滑点，0.0 表示无滑点, 0.05 表示滑点为 5%\n        \"\"\"\n        super().follow(\n            users=users,\n            strategies=strategies,\n            track_interval=track_interval,\n            trade_cmd_expire_seconds=trade_cmd_expire_seconds,\n            cmd_cache=cmd_cache,\n            slippage=slippage,\n        )\n\n        self._adjust_sell = adjust_sell\n\n        self._users = self.warp_list(users)\n\n        strategies = self.warp_list(strategies)\n        total_assets = self.warp_list(total_assets)\n        initial_assets = self.warp_list(initial_assets)\n\n        if cmd_cache:\n            self.load_expired_cmd_cache()\n\n        self.start_trader_thread(self._users, trade_cmd_expire_seconds)\n\n        for strategy_url, strategy_total_assets, strategy_initial_assets in zip(\n            strategies, total_assets, initial_assets\n        ):\n            assets = self.calculate_assets(\n                strategy_url, strategy_total_assets, strategy_initial_assets\n            )\n            try:\n                strategy_id = self.extract_strategy_id(strategy_url)\n                strategy_name = self.extract_strategy_name(strategy_url)\n            except:\n                logger.error(\"抽取交易id和策略名失败, 无效模拟交易url: %s\", strategy_url)\n                raise\n            strategy_worker = Thread(\n                target=self.track_strategy_worker,\n                args=[strategy_id, strategy_name],\n                kwargs={\"interval\": track_interval, \"assets\": assets},\n            )\n            strategy_worker.start()\n            logger.info(\"开始跟踪策略: %s\", strategy_name)\n\n    def calculate_assets(self, strategy_url, total_assets=None, initial_assets=None):\n        # 都设置时优先选择 total_assets\n        if total_assets is None and initial_assets is not None:\n            net_value = self._get_portfolio_net_value(strategy_url)\n            total_assets = initial_assets * net_value\n        if not isinstance(total_assets, Number):\n            raise TypeError(\"input assets type must be number(int, float)\")\n        if total_assets < 1e3:\n            raise ValueError(\"雪球总资产不能小于1000元，当前预设值 {}\".format(total_assets))\n        return total_assets\n\n    @staticmethod\n    def extract_strategy_id(strategy_url):\n        return strategy_url\n\n    def extract_strategy_name(self, strategy_url):\n        base_url = \"https://xueqiu.com/cubes/nav_daily/all.json?cube_symbol={}\"\n        url = base_url.format(strategy_url)\n        rep = self.s.get(url)\n        info_index = 0\n        return rep.json()[info_index][\"name\"]\n\n    def extract_transactions(self, history):\n        if history[\"count\"] <= 0:\n            return []\n        rebalancing_index = 0\n        raw_transactions = history[\"list\"][rebalancing_index][\"rebalancing_histories\"]\n        transactions = []\n        for transaction in raw_transactions:\n            if transaction[\"price\"] is None:\n                logger.info(\"该笔交易无法获取价格，疑似未成交，跳过。交易详情: %s\", transaction)\n                continue\n            transactions.append(transaction)\n\n        return transactions\n\n    def create_query_transaction_params(self, strategy):\n        params = {\"cube_symbol\": strategy, \"page\": 1, \"count\": 1}\n        return params\n\n    # noinspection PyMethodOverriding\n    def none_to_zero(self, data):\n        if data is None:\n            return 0\n        return data\n\n    # noinspection PyMethodOverriding\n    def project_transactions(self, transactions, assets):\n        for transaction in transactions:\n            weight_diff = self.none_to_zero(transaction[\"weight\"]) - self.none_to_zero(\n                transaction[\"prev_weight\"]\n            )\n\n            initial_amount = abs(weight_diff) / 100 * assets / transaction[\"price\"]\n\n            transaction[\"datetime\"] = datetime.fromtimestamp(\n                transaction[\"created_at\"] // 1000\n            )\n\n            transaction[\"stock_code\"] = transaction[\"stock_symbol\"].lower()\n\n            transaction[\"action\"] = \"buy\" if weight_diff > 0 else \"sell\"\n\n            transaction[\"amount\"] = int(round(initial_amount, -2))\n            if transaction[\"action\"] == \"sell\" and self._adjust_sell:\n                transaction[\"amount\"] = self._adjust_sell_amount(\n                    transaction[\"stock_code\"], transaction[\"amount\"]\n                )\n\n    def _adjust_sell_amount(self, stock_code, amount):\n        \"\"\"\n        根据实际持仓值计算雪球卖出股数\n          因为雪球的交易指令是基于持仓百分比，在取近似值的情况下可能出现不精确的问题。\n        导致如下情况的产生，计算出的指令为买入 1049 股，取近似值买入 1000 股。\n        而卖出的指令计算出为卖出 1051 股，取近似值卖出 1100 股，超过 1000 股的买入量，\n        导致卖出失败\n        :param stock_code: 证券代码\n        :type stock_code: str\n        :param amount: 卖出股份数\n        :type amount: int\n        :return: 考虑实际持仓之后的卖出股份数\n        :rtype: int\n        \"\"\"\n        stock_code = stock_code[-6:]\n        user = self._users[0]\n        position = user.position\n        try:\n            stock = next(s for s in position if s[\"证券代码\"] == stock_code)\n        except StopIteration:\n            logger.info(\"根据持仓调整 %s 卖出额，发现未持有股票 %s, 不做任何调整\", stock_code, stock_code)\n            return amount\n\n        available_amount = stock[\"可用余额\"]\n        if available_amount >= amount:\n            return amount\n\n        adjust_amount = available_amount // 100 * 100\n        logger.info(\n            \"股票 %s 实际可用余额 %s, 指令卖出股数为 %s, 调整为 %s\",\n            stock_code,\n            available_amount,\n            amount,\n            adjust_amount,\n        )\n        return adjust_amount\n\n    def _get_portfolio_info(self, portfolio_code):\n        \"\"\"\n        获取组合信息\n        \"\"\"\n        url = self.PORTFOLIO_URL + portfolio_code\n        portfolio_page = self.s.get(url)\n        match_info = re.search(r\"(?<=SNB.cubeInfo = ).*(?=;\\n)\", portfolio_page.text)\n        if match_info is None:\n            raise Exception(\"cant get portfolio info, portfolio url : {}\".format(url))\n        try:\n            portfolio_info = json.loads(match_info.group())\n        except Exception as e:\n            raise Exception(\"get portfolio info error: {}\".format(e))\n        return portfolio_info\n\n    def _get_portfolio_net_value(self, portfolio_code):\n        \"\"\"\n        获取组合信息\n        \"\"\"\n        portfolio_info = self._get_portfolio_info(portfolio_code)\n        return portfolio_info[\"net_value\"]\n"
  },
  {
    "path": "easytrader/xqtrader.py",
    "content": "# -*- 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\n\r\nfrom easytrader import exceptions, webtrader\r\nfrom easytrader.log import logger\r\nfrom easytrader.utils.misc import parse_cookies_str\r\n\r\n\r\nclass XueQiuTrader(webtrader.WebTrader):\r\n    config_path = os.path.dirname(__file__) + \"/config/xq.json\"\r\n\r\n    _HEADERS = {\r\n        \"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) \"\r\n        \"AppleWebKit/537.36 (KHTML, like Gecko) \"\r\n        \"Chrome/64.0.3282.167 Safari/537.36\",\r\n        \"Host\": \"xueqiu.com\",\r\n        \"Pragma\": \"no-cache\",\r\n        \"Connection\": \"keep-alive\",\r\n        \"Accept\": \"*/*\",\r\n        \"Accept-Encoding\": \"gzip, deflate, br\",\r\n        \"Accept-Language\": \"zh-CN,zh;q=0.9,en;q=0.8\",\r\n        \"Cache-Control\": \"no-cache\",\r\n        \"Referer\": \"https://xueqiu.com/P/ZH004612\",\r\n        \"X-Requested-With\": \"XMLHttpRequest\",\r\n    }\r\n\r\n    def __init__(self, **kwargs):\r\n        super(XueQiuTrader, self).__init__()\r\n        self.position_list = []\r\n\r\n        # 资金换算倍数\r\n        self.multiple = (\r\n            kwargs[\"initial_assets\"] if \"initial_assets\" in kwargs else 1000000\r\n        )\r\n        if not isinstance(self.multiple, numbers.Number):\r\n            raise TypeError(\"initial assets must be number(int, float)\")\r\n        if self.multiple < 1e3:\r\n            raise ValueError(\"雪球初始资产不能小于1000元，当前预设值 {}\".format(self.multiple))\r\n\r\n        self.s = requests.Session()\r\n        self.s.verify = False\r\n        self.s.headers.update(self._HEADERS)\r\n        self.account_config = None\r\n\r\n    def autologin(self, **kwargs):\r\n        \"\"\"\r\n        使用 cookies 之后不需要自动登陆\r\n        :return:\r\n        \"\"\"\r\n        self._set_cookies(self.account_config[\"cookies\"])\r\n\r\n    def _set_cookies(self, cookies):\r\n        \"\"\"设置雪球 cookies，代码来自于\r\n        https://github.com/shidenggui/easytrader/issues/269\r\n        :param cookies: 雪球 cookies\r\n        :type cookies: str\r\n        \"\"\"\r\n        cookie_dict = parse_cookies_str(cookies)\r\n        self.s.cookies.update(cookie_dict)\r\n\r\n    def _prepare_account(self, user=\"\", password=\"\", **kwargs):\r\n        \"\"\"\r\n        转换参数到登录所需的字典格式\r\n        :param cookies: 雪球登陆需要设置 cookies， 具体见\r\n            https://smalltool.github.io/2016/08/02/cookie/\r\n        :param portfolio_code: 组合代码\r\n        :param portfolio_market: 交易市场， 可选['cn', 'us', 'hk'] 默认 'cn'\r\n        :return:\r\n        \"\"\"\r\n        if \"portfolio_code\" not in kwargs:\r\n            raise TypeError(\"雪球登录需要设置 portfolio_code(组合代码) 参数\")\r\n        if \"portfolio_market\" not in kwargs:\r\n            kwargs[\"portfolio_market\"] = \"cn\"\r\n        if \"cookies\" not in kwargs:\r\n            raise TypeError(\r\n                \"雪球登陆需要设置 cookies， 具体见\"\r\n                \"https://smalltool.github.io/2016/08/02/cookie/\"\r\n            )\r\n        self.account_config = {\r\n            \"cookies\": kwargs[\"cookies\"],\r\n            \"portfolio_code\": kwargs[\"portfolio_code\"],\r\n            \"portfolio_market\": kwargs[\"portfolio_market\"],\r\n        }\r\n\r\n    def _virtual_to_balance(self, virtual):\r\n        \"\"\"\r\n        虚拟净值转化为资金\r\n        :param virtual: 雪球组合净值\r\n        :return: 换算的资金\r\n        \"\"\"\r\n        return virtual * self.multiple\r\n\r\n    def _get_html(self, url):\r\n        return self.s.get(url).text\r\n\r\n    def _search_stock_info(self, code):\r\n        \"\"\"\r\n        通过雪球的接口获取股票详细信息\r\n        :param code: 股票代码 000001\r\n        :return: 查询到的股票 {'stock_id': 1000279, 'code': 'SH600325',\r\n            'name': '华发股份', 'ind_color': '#d9633b', 'chg': -1.09,\r\n            'ind_id': 100014, 'percent': -9.31, 'current': 10.62,\r\n            'ind_name': '房地产'}\r\n         ** flag : 未上市(0)、正常(1)、停牌(2)、涨跌停(3)、退市(4)\r\n        \"\"\"\r\n        data = {\r\n            \"code\": str(code),\r\n            \"size\": \"300\",\r\n            \"key\": \"47bce5c74f\",\r\n            \"market\": self.account_config[\"portfolio_market\"],\r\n        }\r\n        r = self.s.get(self.config[\"search_stock_url\"], params=data)\r\n        stocks = json.loads(r.text)\r\n        stocks = stocks[\"stocks\"]\r\n        stock = None\r\n        if len(stocks) > 0:\r\n            stock = stocks[0]\r\n        return stock\r\n\r\n    def _get_portfolio_info(self, portfolio_code):\r\n        \"\"\"\r\n        获取组合信息\r\n        :return: 字典\r\n        \"\"\"\r\n        data_rb = {'cube_symbol': portfolio_code}\r\n        rb = self.s.get(self.config[\"portfolio_url_new\"], params=data_rb)\r\n        data_qt = {'code': portfolio_code}\r\n        qt = self.s.get(self.config[\"portfolio_quote\"], params=data_qt)\r\n        try:\r\n            rebalance_info = json.loads(rb.text)\r\n            quote_info = json.loads(qt.text)\r\n            net_value = quote_info[portfolio_code]['net_value']\r\n            portfolio_info = rebalance_info\r\n            portfolio_info['net_value'] = net_value\r\n        except Exception as e:\r\n            raise Exception(\"get portfolio info error: {}\".format(e))\r\n        return portfolio_info\r\n\r\n    def get_balance(self):\r\n        \"\"\"\r\n        获取账户资金状况\r\n        :return:\r\n        \"\"\"\r\n        portfolio_code = self.account_config.get(\"portfolio_code\", \"ch\")\r\n        portfolio_info = self._get_portfolio_info(portfolio_code)\r\n        asset_balance = self._virtual_to_balance(\r\n            float(portfolio_info[\"net_value\"])\r\n        )  # 总资产\r\n        position = portfolio_info[\"last_rb\"]  # 仓位结构\r\n        cash = asset_balance * float(position[\"cash\"]) / 100\r\n        market = asset_balance - cash\r\n        return [\r\n            {\r\n                \"asset_balance\": asset_balance,\r\n                \"current_balance\": cash,\r\n                \"enable_balance\": cash,\r\n                \"market_value\": market,\r\n                \"money_type\": u\"人民币\",\r\n                \"pre_interest\": 0.25,\r\n            }\r\n        ]\r\n\r\n    @property\r\n    def cash_weight(self):\r\n        portfolio_code = self.account_config.get(\"portfolio_code\", \"ch\")\r\n        portfolio_info = self._get_portfolio_info(portfolio_code)\r\n        position = portfolio_info[\"last_rb\"]\r\n        return float(position[\"cash\"])\r\n\r\n    def _get_position(self):\r\n        \"\"\"\r\n        获取雪球持仓\r\n        :return:\r\n        \"\"\"\r\n        portfolio_code = self.account_config[\"portfolio_code\"]\r\n        portfolio_info = self._get_portfolio_info(portfolio_code)\r\n        position = portfolio_info[\"last_rb\"]  # 仓位结构\r\n        stocks = position[\"holdings\"]  # 持仓股票\r\n        return stocks\r\n\r\n    @staticmethod\r\n    def _time_strftime(time_stamp):\r\n        try:\r\n            local_time = time.localtime(time_stamp / 1000)\r\n            return time.strftime(\"%Y-%m-%d %H:%M:%S\", local_time)\r\n        # pylint: disable=broad-except\r\n        except Exception:\r\n            return time.strftime(\"%Y-%m-%d %H:%M:%S\", time.localtime())\r\n\r\n    def get_position(self):\r\n        \"\"\"\r\n        获取持仓\r\n        :return:\r\n        \"\"\"\r\n        xq_positions = self._get_position()\r\n        balance = self.get_balance()[0]\r\n        position_list = []\r\n        for pos in xq_positions:\r\n            volume = pos[\"weight\"] * balance[\"asset_balance\"] / 100\r\n            position_list.append(\r\n                {\r\n                    \"cost_price\": volume / 100,\r\n                    \"current_amount\": 100,\r\n                    \"enable_amount\": 100,\r\n                    \"income_balance\": 0,\r\n                    \"keep_cost_price\": volume / 100,\r\n                    \"last_price\": volume / 100,\r\n                    \"market_value\": volume,\r\n                    \"position_str\": \"random\",\r\n                    \"stock_code\": pos[\"stock_symbol\"],\r\n                    \"stock_name\": pos[\"stock_name\"],\r\n                }\r\n            )\r\n        return position_list\r\n\r\n    def _get_xq_history(self):\r\n        \"\"\"\r\n        获取雪球调仓历史\r\n        :param instance:\r\n        :param owner:\r\n        :return:\r\n        \"\"\"\r\n        data = {\r\n            \"cube_symbol\": str(self.account_config[\"portfolio_code\"]),\r\n            \"count\": 20,\r\n            \"page\": 1,\r\n        }\r\n        resp = self.s.get(self.config[\"history_url\"], params=data)\r\n        res = json.loads(resp.text)\r\n        return res[\"list\"]\r\n\r\n    @property\r\n    def history(self):\r\n        return self._get_xq_history()\r\n\r\n    def get_entrust(self):\r\n        \"\"\"\r\n        获取委托单(目前返回20次调仓的结果)\r\n        操作数量都按1手模拟换算的\r\n        :return:\r\n        \"\"\"\r\n        xq_entrust_list = self._get_xq_history()\r\n        entrust_list = []\r\n        replace_none = lambda s: s or 0\r\n        for xq_entrusts in xq_entrust_list:\r\n            status = xq_entrusts[\"status\"]  # 调仓状态\r\n            if status == \"pending\":\r\n                status = \"已报\"\r\n            elif status in [\"canceled\", \"failed\"]:\r\n                status = \"废单\"\r\n            else:\r\n                status = \"已成\"\r\n            for entrust in xq_entrusts[\"rebalancing_histories\"]:\r\n                price = entrust[\"price\"]\r\n                entrust_list.append(\r\n                    {\r\n                        \"entrust_no\": entrust[\"id\"],\r\n                        \"entrust_bs\": u\"买入\"\r\n                        if entrust[\"target_weight\"]\r\n                        > replace_none(entrust[\"prev_weight\"])\r\n                        else u\"卖出\",\r\n                        \"report_time\": self._time_strftime(\r\n                            entrust[\"updated_at\"]\r\n                        ),\r\n                        \"entrust_status\": status,\r\n                        \"stock_code\": entrust[\"stock_symbol\"],\r\n                        \"stock_name\": entrust[\"stock_name\"],\r\n                        \"business_amount\": 100,\r\n                        \"business_price\": price,\r\n                        \"entrust_amount\": 100,\r\n                        \"entrust_price\": price,\r\n                    }\r\n                )\r\n        return entrust_list\r\n\r\n    def cancel_entrust(self, entrust_no):\r\n        \"\"\"\r\n        对未成交的调仓进行伪撤单\r\n        :param entrust_no:\r\n        :return:\r\n        \"\"\"\r\n        xq_entrust_list = self._get_xq_history()\r\n        is_have = False\r\n        for xq_entrusts in xq_entrust_list:\r\n            status = xq_entrusts[\"status\"]  # 调仓状态\r\n            for entrust in xq_entrusts[\"rebalancing_histories\"]:\r\n                if entrust[\"id\"] == entrust_no and status == \"pending\":\r\n                    is_have = True\r\n                    buy_or_sell = (\r\n                        \"buy\"\r\n                        if entrust[\"target_weight\"] < entrust[\"weight\"]\r\n                        else \"sell\"\r\n                    )\r\n                    if (\r\n                        entrust[\"target_weight\"] == 0\r\n                        and entrust[\"weight\"] == 0\r\n                    ):\r\n                        raise exceptions.TradeError(u\"移除的股票操作无法撤销,建议重新买入\")\r\n                    balance = self.get_balance()[0]\r\n                    volume = (\r\n                        abs(entrust[\"target_weight\"] - entrust[\"weight\"])\r\n                        * balance[\"asset_balance\"]\r\n                        / 100\r\n                    )\r\n                    r = self._trade(\r\n                        security=entrust[\"stock_symbol\"],\r\n                        volume=volume,\r\n                        entrust_bs=buy_or_sell,\r\n                    )\r\n                    if len(r) > 0 and \"error_info\" in r[0]:\r\n                        raise exceptions.TradeError(\r\n                            u\"撤销失败!%s\" % (\"error_info\" in r[0])\r\n                        )\r\n        if not is_have:\r\n            raise exceptions.TradeError(u\"撤销对象已失效\")\r\n        return True\r\n\r\n    def adjust_weight(self, stock_code, weight, fetch_position=True):\r\n        \"\"\"\r\n        雪球组合调仓, weight 为调整后的仓位比例\r\n        :param stock_code: str 股票代码\r\n        :param weight: float 调整之后的持仓百分比， 0 - 100 之间的浮点数\r\n        \"\"\"\r\n\r\n        stock = self._search_stock_info(stock_code)\r\n        if stock is None:\r\n            raise exceptions.TradeError(u\"没有查询要操作的股票信息\")\r\n        if stock[\"flag\"] != 1:\r\n            raise exceptions.TradeError(u\"未上市、停牌、涨跌停、退市的股票无法操作。\")\r\n\r\n        # 仓位比例向下取两位数\r\n        weight = round(weight, 2)\r\n        # 获取原有仓位信息\r\n        if fetch_position:\r\n            self.position_list = self._get_position()\r\n\r\n        # 调整后的持仓\r\n        for position in self.position_list:\r\n            if position[\"stock_id\"] == stock[\"stock_id\"]:\r\n                position[\"proactive\"] = True\r\n                position[\"weight\"] = weight\r\n\r\n        if weight != 0 and stock[\"stock_id\"] not in [\r\n            k[\"stock_id\"] for k in self.position_list\r\n        ]:\r\n            self.position_list.append(\r\n                {\r\n                    \"code\": stock[\"code\"],\r\n                    \"name\": stock[\"name\"],\r\n                    \"flag\": stock[\"flag\"],\r\n                    \"current\": stock[\"current\"],\r\n                    \"chg\": stock[\"chg\"],\r\n                    \"percent\": str(stock[\"percent\"]),\r\n                    \"stock_id\": stock[\"stock_id\"],\r\n                    \"ind_id\": stock[\"ind_id\"],\r\n                    \"ind_name\": stock[\"ind_name\"],\r\n                    \"ind_color\": stock[\"ind_color\"],\r\n                    \"textname\": stock[\"name\"],\r\n                    \"segment_name\": stock[\"ind_name\"],\r\n                    \"weight\": weight,\r\n                    \"url\": \"/S/\" + stock[\"code\"],\r\n                    \"proactive\": True,\r\n                    \"price\": str(stock[\"current\"]),\r\n                }\r\n            )\r\n\r\n        remain_weight = 100 - sum(i.get(\"weight\") for i in self.position_list)\r\n        cash = round(remain_weight, 2)\r\n        logger.info(\"调仓比例:%f, 剩余持仓 :%f\", weight, remain_weight)\r\n        data = {\r\n            \"cash\": cash,\r\n            \"holdings\": str(json.dumps(self.position_list)),\r\n            \"cube_symbol\": str(self.account_config[\"portfolio_code\"]),\r\n            \"segment\": \"true\",\r\n            \"comment\": \"\",\r\n        }\r\n\r\n        try:\r\n            resp = self.s.post(self.config[\"rebalance_url\"], data=data)\r\n        # pylint: disable=broad-except\r\n        except Exception as e:\r\n            logger.warning(\"调仓失败: %s \", e)\r\n            return None\r\n        logger.info(\"调仓 %s: 持仓比例%d\", stock[\"name\"], weight)\r\n        resp_json = json.loads(resp.text)\r\n        if \"error_description\" in resp_json and resp.status_code != 200:\r\n            logger.error(\"调仓错误: %s\", resp_json[\"error_description\"])\r\n            return [\r\n                {\r\n                    \"error_no\": resp_json[\"error_code\"],\r\n                    \"error_info\": resp_json[\"error_description\"],\r\n                }\r\n            ]\r\n        logger.info(\"调仓成功 %s: 持仓比例%d\", stock[\"name\"], weight)\r\n        return None\r\n\r\n    def _trade(self, security, price=0, amount=0, volume=0, entrust_bs=\"buy\"):\r\n        \"\"\"\r\n        调仓\r\n        :param security:\r\n        :param price:\r\n        :param amount:\r\n        :param volume:\r\n        :param entrust_bs:\r\n        :return:\r\n        \"\"\"\r\n        stock = self._search_stock_info(security)\r\n        balance = self.get_balance()[0]\r\n        if stock is None:\r\n            raise exceptions.TradeError(u\"没有查询要操作的股票信息\")\r\n        if not volume:\r\n            volume = int(float(price) * amount)  # 可能要取整数\r\n        if balance[\"current_balance\"] < volume and entrust_bs == \"buy\":\r\n            raise exceptions.TradeError(u\"没有足够的现金进行操作\")\r\n        if stock[\"flag\"] != 1:\r\n            raise exceptions.TradeError(u\"未上市、停牌、涨跌停、退市的股票无法操作。\")\r\n        if volume == 0:\r\n            raise exceptions.TradeError(u\"操作金额不能为零\")\r\n\r\n        # 计算调仓调仓份额\r\n        weight = volume / balance[\"asset_balance\"] * 100\r\n        weight = round(weight, 2)\r\n\r\n        # 获取原有仓位信息\r\n        position_list = self._get_position()\r\n\r\n        # 调整后的持仓\r\n        is_have = False\r\n        for position in position_list:\r\n            if position[\"stock_id\"] == stock[\"stock_id\"]:\r\n                is_have = True\r\n                position[\"proactive\"] = True\r\n                old_weight = position[\"weight\"]\r\n                if entrust_bs == \"buy\":\r\n                    position[\"weight\"] = weight + old_weight\r\n                else:\r\n                    if weight > old_weight:\r\n                        raise exceptions.TradeError(u\"操作数量大于实际可卖出数量\")\r\n                    else:\r\n                        position[\"weight\"] = old_weight - weight\r\n                position[\"weight\"] = round(position[\"weight\"], 2)\r\n        if not is_have:\r\n            if entrust_bs == \"buy\":\r\n                position_list.append(\r\n                    {\r\n                        \"code\": stock[\"code\"],\r\n                        \"name\": stock[\"name\"],\r\n                        \"enName\": stock[\"enName\"],\r\n                        \"hasexist\": stock[\"hasexist\"],\r\n                        \"flag\": stock[\"flag\"],\r\n                        \"type\": stock[\"type\"],\r\n                        \"current\": stock[\"current\"],\r\n                        \"chg\": stock[\"chg\"],\r\n                        \"percent\": str(stock[\"percent\"]),\r\n                        \"stock_id\": stock[\"stock_id\"],\r\n                        \"ind_id\": stock[\"ind_id\"],\r\n                        \"ind_name\": stock[\"ind_name\"],\r\n                        \"ind_color\": stock[\"ind_color\"],\r\n                        \"textname\": stock[\"name\"],\r\n                        \"segment_name\": stock[\"ind_name\"],\r\n                        \"weight\": round(weight, 2),\r\n                        \"url\": \"/S/\" + stock[\"code\"],\r\n                        \"proactive\": True,\r\n                        \"price\": str(stock[\"current\"]),\r\n                    }\r\n                )\r\n            else:\r\n                raise exceptions.TradeError(u\"没有持有要卖出的股票\")\r\n\r\n        if entrust_bs == \"buy\":\r\n            cash = (\r\n                (balance[\"current_balance\"] - volume)\r\n                / balance[\"asset_balance\"]\r\n                * 100\r\n            )\r\n        else:\r\n            cash = (\r\n                (balance[\"current_balance\"] + volume)\r\n                / balance[\"asset_balance\"]\r\n                * 100\r\n            )\r\n        cash = round(cash, 2)\r\n        logger.info(\"weight:%f, cash:%f\", weight, cash)\r\n\r\n        data = {\r\n            \"cash\": cash,\r\n            \"holdings\": str(json.dumps(position_list)),\r\n            \"cube_symbol\": str(self.account_config[\"portfolio_code\"]),\r\n            \"segment\": 1,\r\n            \"comment\": \"\",\r\n        }\r\n\r\n        try:\r\n            resp = self.s.post(self.config[\"rebalance_url\"], data=data)\r\n        # pylint: disable=broad-except\r\n        except Exception as e:\r\n            logger.warning(\"调仓失败: %s \", e)\r\n            return None\r\n        else:\r\n            logger.info(\r\n                \"调仓 %s%s: %d\", entrust_bs, stock[\"name\"], resp.status_code\r\n            )\r\n            resp_json = json.loads(resp.text)\r\n            if \"error_description\" in resp_json and resp.status_code != 200:\r\n                logger.error(\"调仓错误: %s\", resp_json[\"error_description\"])\r\n                return [\r\n                    {\r\n                        \"error_no\": resp_json[\"error_code\"],\r\n                        \"error_info\": resp_json[\"error_description\"],\r\n                    }\r\n                ]\r\n            return [\r\n                {\r\n                    \"entrust_no\": resp_json[\"id\"],\r\n                    \"init_date\": self._time_strftime(resp_json[\"created_at\"]),\r\n                    \"batch_no\": \"委托批号\",\r\n                    \"report_no\": \"申报号\",\r\n                    \"seat_no\": \"席位编号\",\r\n                    \"entrust_time\": self._time_strftime(\r\n                        resp_json[\"updated_at\"]\r\n                    ),\r\n                    \"entrust_price\": price,\r\n                    \"entrust_amount\": amount,\r\n                    \"stock_code\": security,\r\n                    \"entrust_bs\": \"买入\",\r\n                    \"entrust_type\": \"雪球虚拟委托\",\r\n                    \"entrust_status\": \"-\",\r\n                }\r\n            ]\r\n\r\n    def buy(self, security, price=0, amount=0, volume=0, entrust_prop=0):\r\n        \"\"\"买入卖出股票\r\n        :param security: 股票代码\r\n        :param price: 买入价格\r\n        :param amount: 买入股数\r\n        :param volume: 买入总金额 由 volume / price 取整， 若指定 price 则此参数无效\r\n        :param entrust_prop:\r\n        \"\"\"\r\n        return self._trade(security, price, amount, volume, \"buy\")\r\n\r\n    def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0):\r\n        \"\"\"卖出股票\r\n        :param security: 股票代码\r\n        :param price: 卖出价格\r\n        :param amount: 卖出股数\r\n        :param volume: 卖出总金额 由 volume / price 取整， 若指定 price 则此参数无效\r\n        :param entrust_prop:\r\n        \"\"\"\r\n        return self._trade(security, price, amount, volume, \"sell\")\r\n\r\n\r\n    def adjust_weights(self, weights, ignore_minor=0.0, fetch_position=True):\r\n        \"\"\"\r\n        雪球组合调仓, weights 为调整后的仓位比例\r\n        :param weights: dict[str, float] 股票代码 -> 调整之后的持仓百分比\r\n        \"\"\"\r\n\r\n        # 获取原有仓位信息\r\n        if fetch_position:\r\n            self.position_list = self._get_position()\r\n\r\n        position_dict = {position[\"stock_id\"]: position for position in self.position_list}\r\n        new_position_list = []\r\n\r\n        for stock_code, weight in weights.items():\r\n            stock = self._search_stock_info(stock_code)\r\n            if stock is None:\r\n                raise exceptions.TradeError(u\"没有查询要操作的股票信息\")\r\n            if stock[\"flag\"] != 1:\r\n               raise exceptions.TradeError(f\"未上市、停牌、涨跌停、退市的股票无法操作: {stock['name']}\")\r\n\r\n            if stock[\"stock_id\"] in position_dict:\r\n                # 调仓\r\n                position = position_dict[stock[\"stock_id\"]]\r\n                current_weight = position[\"weight\"]\r\n                if weight > 0 and abs(weight - current_weight) > ignore_minor:\r\n                    position[\"proactive\"] = True\r\n                    position[\"weight\"] = weight\r\n                    logger.info(\"调仓 %s %.2f -> %.2f\", position['stock_name'], current_weight, weight)\r\n                    new_position_list.append(position)\r\n                elif weight > 0:\r\n                    position[\"proactive\"] = False\r\n                    new_position_list.append(position)\r\n                elif weight == 0.0:\r\n                    logger.info(\"平仓 %s %.2f -> %.2f\", position['stock_name'], current_weight, weight)\r\n            else:\r\n                # 开仓\r\n                new_position_list.append(\r\n                    {\r\n                        \"code\": stock[\"code\"],\r\n                        \"name\": stock[\"name\"],\r\n                        \"flag\": stock[\"flag\"],\r\n                        \"current\": stock[\"current\"],\r\n                        \"chg\": stock[\"chg\"],\r\n                        \"percent\": str(stock[\"percent\"]),\r\n                        \"stock_id\": stock[\"stock_id\"],\r\n                        \"ind_id\": stock[\"ind_id\"],\r\n                        \"ind_name\": stock[\"ind_name\"],\r\n                        \"ind_color\": stock[\"ind_color\"],\r\n                        \"textname\": stock[\"name\"],\r\n                        \"segment_name\": stock[\"ind_name\"],\r\n                        \"weight\": weights[stock_code],\r\n                        \"url\": \"/S/\" + stock[\"code\"],\r\n                        \"proactive\": True,\r\n                        \"price\": str(stock[\"current\"]),\r\n                    }\r\n                )\r\n                logger.info(\"开仓 %s 比例: %.2f\", stock[\"name\"], weight)\r\n\r\n        remain_weight = 100 - sum(i.get(\"weight\") for i in new_position_list)\r\n        cash = round(remain_weight, 2)\r\n        assert cash >= 0\r\n        data = {\r\n            \"cash\": cash,\r\n            \"holdings\": str(json.dumps(new_position_list)),\r\n            \"cube_symbol\": str(self.account_config[\"portfolio_code\"]),\r\n            \"segment\": \"true\",\r\n            \"comment\": \"\",\r\n        }\r\n        try:\r\n            resp = self.s.post(self.config[\"rebalance_url\"], data=data)\r\n        # pylint: disable=broad-except\r\n        except Exception as e:\r\n            logger.warning(\"调仓失败: %s \", e)\r\n            return None\r\n        logger.info(\"剩余仓位: %f\", cash)\r\n        resp_json = json.loads(resp.text)\r\n        if \"error_description\" in resp_json and resp.status_code != 200:\r\n            logger.error(\"调仓错误: %s\", resp_json[\"error_description\"])\r\n            return [\r\n                {\r\n                    \"error_no\": resp_json[\"error_code\"],\r\n                    \"error_info\": resp_json[\"error_description\"],\r\n                }\r\n            ]\r\n        logger.info(\"调仓成功\")\r\n        return None"
  },
  {
    "path": "easytrader/yh_clienttrader.py",
    "content": "# -*- coding: utf-8 -*-\nimport re\nimport tempfile\n\nimport pywinauto\n\nfrom easytrader import clienttrader, grid_strategies\nfrom easytrader.utils.captcha import recognize_verify_code\n\n\nclass YHClientTrader(clienttrader.BaseLoginClientTrader):\n    \"\"\"\n    Changelog:\n\n    2018.07.01:\n        银河客户端 2018.5.11 更新后不再支持通过剪切板复制获取 Grid 内容，\n        改为使用保存为 Xls 再读取的方式获取\n    \"\"\"\n\n    grid_strategy = grid_strategies.Xls\n\n    @property\n    def broker_type(self):\n        return \"yh\"\n\n    def login(self, user, password, exe_path, comm_password=None, **kwargs):\n        \"\"\"\n        登陆客户端\n        :param user: 账号\n        :param password: 明文密码\n        :param exe_path: 客户端路径类似 'C:\\\\中国银河证券双子星3.2\\\\Binarystar.exe',\n            默认 'C:\\\\中国银河证券双子星3.2\\\\Binarystar.exe'\n        :param comm_password: 通讯密码, 华泰需要，可不设\n        :return:\n        \"\"\"\n        try:\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=1\n            )\n        # pylint: disable=broad-except\n        except Exception:\n            self._app = pywinauto.Application().start(exe_path)\n            is_xiadan = True if \"xiadan.exe\" in exe_path else False\n            # wait login window ready\n            while True:\n                try:\n                    self._app.top_window().Edit1.wait(\"ready\")\n                    break\n                except RuntimeError:\n                    pass\n\n            self._app.top_window().Edit1.type_keys(user)\n            self._app.top_window().Edit2.type_keys(password)\n            while True:\n                self._app.top_window().Edit3.type_keys(\n                    self._handle_verify_code(is_xiadan)\n                )\n                if is_xiadan:\n                    self._app.top_window().child_window(control_id=1006, class_name=\"Button\").click()\n                else:\n                    self._app.top_window()[\"登录\"].click()\n\n                # detect login is success or not\n                try:\n                    self._app.top_window().wait_not(\"exists visible\", 10)\n                    break\n                # pylint: disable=broad-except\n                except Exception:\n                    if is_xiadan:\n                        self._app.top_window()[\"确定\"].click()\n\n            self._app = pywinauto.Application().connect(\n                path=self._run_exe_path(exe_path), timeout=10\n            )\n        self._close_prompt_windows()\n        self._main = self._app.window(title=\"网上股票交易系统5.0\")\n        try:\n            self._main.child_window(\n                control_id=129, class_name=\"SysTreeView32\"\n            ).wait(\"ready\", 2)\n        # pylint: disable=broad-except\n        except Exception:\n            self.wait(2)\n            self._switch_window_to_normal_mode()\n\n    def _switch_window_to_normal_mode(self):\n        self._app.top_window().child_window(\n            control_id=32812, class_name=\"Button\"\n        ).click()\n\n    def _handle_verify_code(self, is_xiadan):\n        control = self._app.top_window().child_window(\n            control_id=1499 if is_xiadan else 22202\n        )\n        control.click()\n\n        file_path = tempfile.mktemp()\n        if is_xiadan:\n            rect = control.element_info.rectangle\n            rect.right = round(\n                rect.right + (rect.right - rect.left) * 0.3\n            )  # 扩展验证码控件截图范围为4个字符\n            control.capture_as_image(rect).save(file_path, \"jpeg\")\n        else:\n            control.capture_as_image().save(file_path, \"jpeg\")\n        verify_code = recognize_verify_code(file_path, \"yh_client\")\n        return \"\".join(re.findall(r\"\\d+\", verify_code))\n\n    @property\n    def balance(self):\n        self._switch_left_menus(self._config.BALANCE_MENU_PATH)\n        return self._get_grid_data(self._config.BALANCE_GRID_CONTROL_ID)\n\n    def auto_ipo(self):\n        self._switch_left_menus(self._config.AUTO_IPO_MENU_PATH)\n        stock_list = self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)\n        if len(stock_list) == 0:\n            return {\"message\": \"今日无新股\"}\n        invalid_list_idx = [\n            i for i, v in enumerate(stock_list) if v[\"申购数量\"] <= 0\n        ]\n        if len(stock_list) == len(invalid_list_idx):\n            return {\"message\": \"没有发现可以申购的新股\"}\n        self.wait(0.1)\n        # for row in invalid_list_idx:\n        # self._click_grid_by_row(row)\n        self._click(self._config.AUTO_IPO_BUTTON_CONTROL_ID)\n        self.wait(0.1)\n        return self._handle_pop_dialogs()\n"
  },
  {
    "path": "gj_client.json",
    "content": "{\n  \"user\": \"国金用户名\",\n  \"password\": \"国金明文密码\"\n}"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: easytrader\nnav:\n  - 简介: index.md\n  - 安装: install.md\n  - 使用: usage.md\n  - miniqmt 量化接口: miniqmt.md\n  - 雪球组合模拟交易: xueqiu.md\n  - 远端服务模式: remote.md\n  - 策略跟踪: follow.md\ntheme: readthedocs\nmarkdown_extensions:\n  - toc:\n      permalink: true\n      toc_depth: 3\n"
  },
  {
    "path": "mypy.ini",
    "content": "[mypy]\nignore_missing_imports = True\n"
  },
  {
    "path": "readthedocs-requirements.txt",
    "content": ""
  },
  {
    "path": "requirements.txt",
    "content": "-i http://mirrors.aliyun.com/pypi/simple/\n--trusted-host mirrors.aliyun.com\nbeautifulsoup4==4.6.0\nbs4==0.0.1\ncertifi==2018.4.16\nchardet==3.0.4\nclick==6.7\ncssselect==1.0.3; python_version != '3.3.*'\ndill==0.2.8.2\neasyutils==0.1.7\nflask==1.0.2\nidna==2.7\nitsdangerous==0.24\njinja2==2.10\nlxml==4.2.3\nmarkupsafe==1.0\nnumpy==1.15.0; python_version >= '2.7'\npandas==0.23.3\npillow==5.2.0\npyperclip==1.6.4\npyquery==1.4.0; python_version != '3.0.*'\npytesseract==0.2.4\npython-dateutil==2.7.3\npython-xlib==0.23\npytz==2018.5\npywinauto==0.6.6\nrequests==2.19.1\nsix==1.11.0\nurllib3==1.23; python_version != '3.1.*'\nwerkzeug==0.14.1\n\n"
  },
  {
    "path": "setup.py",
    "content": "# 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=\"A utility for China Stock Trade\",\r\n    long_description=open(\"README.md\").read(),\r\n    long_description_content_type=\"text/markdown\",\r\n    author=\"shidenggui\",\r\n    author_email=\"longlyshidenggui@gmail.com\",\r\n    license=\"BSD\",\r\n    url=\"https://github.com/shidenggui/easytrader\",\r\n    keywords=\"China stock trade\",\r\n    install_requires=[\r\n        \"requests\",\r\n        \"six\",\r\n        \"easyutils\",\r\n        \"flask\",\r\n        \"pywinauto==0.6.6\",\r\n        \"pillow\",\r\n        \"pandas\",\r\n    ],\r\n    extras_require={\r\n        \"miniqmt\": [\"xtquant\"],\r\n    },\r\n    classifiers=[\r\n        \"Development Status :: 4 - Beta\",\r\n        \"Programming Language :: Python :: 3.5\",\r\n        \"License :: OSI Approved :: BSD License\",\r\n    ],\r\n    packages=[\r\n        \"easytrader\",\r\n        \"easytrader.config\",\r\n        \"easytrader.utils\",\r\n        \"easytrader.miniqmt\",\r\n    ],\r\n    package_data={\r\n        \"\": [\"*.jar\", \"*.json\"],\r\n        \"config\": [\"config/*.json\"],\r\n        \"thirdlibrary\": [\"thirdlibrary/*.jar\"],\r\n    },\r\n)\r\n"
  },
  {
    "path": "test-requirements.txt",
    "content": "-r requirements.txt\n\npytest\npytest-cov\n\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "# coding:utf8\n"
  },
  {
    "path": "tests/test_easytrader.py",
    "content": "# coding: utf-8\nimport os\nimport sys\nimport time\nimport unittest\n\nsys.path.append(\".\")\n\nTEST_CLIENTS = set(os.environ.get(\"EZ_TEST_CLIENTS\", \"\").split(\",\"))\n\nIS_WIN_PLATFORM = sys.platform != \"darwin\"\n\n\n@unittest.skipUnless(\"yh\" in TEST_CLIENTS and IS_WIN_PLATFORM, \"skip yh test\")\nclass TestYhClientTrader(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        import easytrader\n\n        if \"yh\" not in TEST_CLIENTS:\n            return\n\n        # input your test account and password\n        cls._ACCOUNT = os.environ.get(\"EZ_TEST_YH_ACCOUNT\") or \"your account\"\n        cls._PASSWORD = os.environ.get(\"EZ_TEST_YH_PASSWORD\") or \"your password\"\n\n        cls._user = easytrader.use(\"yh_client\")\n        cls._user.prepare(user=cls._ACCOUNT, password=cls._PASSWORD)\n\n    def test_balance(self):\n        time.sleep(3)\n        result = self._user.balance\n\n    def test_today_entrusts(self):\n        result = self._user.today_entrusts\n\n    def test_today_trades(self):\n        result = self._user.today_trades\n\n    def test_cancel_entrusts(self):\n        result = self._user.cancel_entrusts\n\n    def test_cancel_entrust(self):\n        result = self._user.cancel_entrust(\"123456789\")\n\n    def test_invalid_buy(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.buy(\"511990\", 1, 1e10)\n\n    def test_invalid_sell(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.sell(\"162411\", 200, 1e10)\n\n    def test_auto_ipo(self):\n        self._user.auto_ipo()\n\n\n@unittest.skipUnless(\"ht\" in TEST_CLIENTS and IS_WIN_PLATFORM, \"skip ht test\")\nclass TestHTClientTrader(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        import easytrader\n\n        if \"ht\" not in TEST_CLIENTS:\n            return\n\n        # input your test account and password\n        cls._ACCOUNT = os.environ.get(\"EZ_TEST_HT_ACCOUNT\") or \"your account\"\n        cls._PASSWORD = os.environ.get(\"EZ_TEST_HT_PASSWORD\") or \"your password\"\n        cls._COMM_PASSWORD = (\n            os.environ.get(\"EZ_TEST_HT_COMM_PASSWORD\") or \"your comm password\"\n        )\n\n        cls._user = easytrader.use(\"ht_client\")\n        cls._user.prepare(\n            user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD\n        )\n\n    def test_balance(self):\n        time.sleep(3)\n        result = self._user.balance\n\n    def test_today_entrusts(self):\n        result = self._user.today_entrusts\n\n    def test_today_trades(self):\n        result = self._user.today_trades\n\n    def test_cancel_entrusts(self):\n        result = self._user.cancel_entrusts\n\n    def test_cancel_entrust(self):\n        result = self._user.cancel_entrust(\"123456789\")\n\n    def test_invalid_buy(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.buy(\"511990\", 1, 1e10)\n\n    def test_invalid_sell(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.sell(\"162411\", 200, 1e10)\n\n    def test_auto_ipo(self):\n        self._user.auto_ipo()\n\n    def test_invalid_repo(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.repo(\"204001\", 100, 1)\n\n    def test_invalid_reverse_repo(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.reverse_repo(\"204001\", 1, 100)\n\n\n@unittest.skipUnless(\"htzq\" in TEST_CLIENTS and IS_WIN_PLATFORM, \"skip htzq test\")\nclass TestHTZQClientTrader(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        import easytrader\n\n        if \"htzq\" not in TEST_CLIENTS:\n            return\n\n        # input your test account and password\n        cls._ACCOUNT = os.environ.get(\"EZ_TEST_HTZQ_ACCOUNT\") or \"your account\"\n        cls._PASSWORD = os.environ.get(\"EZ_TEST_HTZQ_PASSWORD\") or \"your password\"\n        cls._COMM_PASSWORD = (\n            os.environ.get(\"EZ_TEST_HTZQ_COMM_PASSWORD\") or \"your comm password\"\n        )\n\n        cls._user = easytrader.use(\"htzq_client\")\n\n        cls._user.prepare(\n            user=cls._ACCOUNT, password=cls._PASSWORD, comm_password=cls._COMM_PASSWORD\n        )\n\n    def test_balance(self):\n        time.sleep(3)\n        result = self._user.balance\n\n    def test_today_entrusts(self):\n        result = self._user.today_entrusts\n\n    def test_today_trades(self):\n        result = self._user.today_trades\n\n    def test_cancel_entrusts(self):\n        result = self._user.cancel_entrusts\n\n    def test_cancel_entrust(self):\n        result = self._user.cancel_entrust(\"123456789\")\n\n    def test_invalid_buy(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.buy(\"511990\", 1, 1e10)\n\n    def test_invalid_sell(self):\n        import easytrader\n\n        with self.assertRaises(easytrader.exceptions.TradeError):\n            result = self._user.sell(\"162411\", 200, 1e10)\n\n    def test_auto_ipo(self):\n        self._user.auto_ipo()\n\n\nif __name__ == \"__main__\":\n    unittest.main(verbosity=2)\n"
  },
  {
    "path": "tests/test_xq_follower.py",
    "content": "# coding:utf-8\nimport datetime\nimport os\nimport time\nimport unittest\nfrom unittest import mock\n\nfrom easytrader.xq_follower import XueQiuFollower\n\n\nclass TestXueQiuTrader(unittest.TestCase):\n    def test_adjust_sell_amount_without_enable(self):\n        follower = XueQiuFollower()\n\n        mock_user = mock.MagicMock()\n        follower._users = [mock_user]\n\n        follower._adjust_sell = False\n        amount = follower._adjust_sell_amount(\"169101\", 1000)\n        self.assertEqual(amount, amount)\n\n    def test_adjust_sell_should_only_work_when_sell(self):\n        follower = XueQiuFollower()\n        follower._adjust_sell = True\n        test_transaction = {\n            \"weight\": 10,\n            \"prev_weight\": 0,\n            \"price\": 10,\n            \"stock_symbol\": \"162411\",\n            \"created_at\": int(time.time() * 1000),\n        }\n        test_assets = 1000\n\n        mock_adjust_sell_amount = mock.MagicMock()\n        follower._adjust_sell_amount = mock_adjust_sell_amount\n\n        follower.project_transactions(\n            transactions=[test_transaction], assets=test_assets\n        )\n        mock_adjust_sell_amount.assert_not_called()\n\n        mock_adjust_sell_amount.reset_mock()\n        test_transaction[\"prev_weight\"] = test_transaction[\"weight\"] + 1\n        follower.project_transactions(\n            transactions=[test_transaction], assets=test_assets\n        )\n        mock_adjust_sell_amount.assert_called()\n\n    def test_adjust_sell_amount(self):\n        follower = XueQiuFollower()\n\n        mock_user = mock.MagicMock()\n        follower._users = [mock_user]\n        mock_user.position = TEST_POSITION\n\n        follower._adjust_sell = True\n        test_cases = [\n            (\"169101\", 600, 600),\n            (\"169101\", 700, 600),\n            (\"000000\", 100, 100),\n            (\"sh169101\", 700, 600),\n        ]\n        for stock_code, sell_amount, excepted_amount in test_cases:\n            amount = follower._adjust_sell_amount(stock_code, sell_amount)\n            self.assertEqual(amount, excepted_amount)\n\n    def test_slippage_with_default(self):\n        follower = XueQiuFollower()\n        mock_user = mock.MagicMock()\n\n        # test default no slippage\n        test_price = 1.0\n        test_trade_cmd = {\n            \"strategy\": \"test_strategy\",\n            \"strategy_name\": \"test_strategy\",\n            \"action\": \"buy\",\n            \"stock_code\": \"162411\",\n            \"amount\": 100,\n            \"price\": 1.0,\n            \"datetime\": datetime.datetime.now(),\n        }\n        follower._execute_trade_cmd(\n            trade_cmd=test_trade_cmd,\n            users=[mock_user],\n            expire_seconds=10,\n            entrust_prop=\"limit\",\n            send_interval=10,\n        )\n        _, kwargs = getattr(mock_user, test_trade_cmd[\"action\"]).call_args\n        self.assertAlmostEqual(kwargs[\"price\"], test_price)\n\n    def test_slippage(self):\n        follower = XueQiuFollower()\n        mock_user = mock.MagicMock()\n\n        test_price = 1.0\n        follower.slippage = 0.05\n\n        # test buy\n        test_trade_cmd = {\n            \"strategy\": \"test_strategy\",\n            \"strategy_name\": \"test_strategy\",\n            \"action\": \"buy\",\n            \"stock_code\": \"162411\",\n            \"amount\": 100,\n            \"price\": 1.0,\n            \"datetime\": datetime.datetime.now(),\n        }\n        follower._execute_trade_cmd(\n            trade_cmd=test_trade_cmd,\n            users=[mock_user],\n            expire_seconds=10,\n            entrust_prop=\"limit\",\n            send_interval=10,\n        )\n        excepted_price = test_price * (1 + follower.slippage)\n        _, kwargs = getattr(mock_user, test_trade_cmd[\"action\"]).call_args\n        self.assertAlmostEqual(kwargs[\"price\"], excepted_price)\n\n        # test sell\n        test_trade_cmd[\"action\"] = \"sell\"\n        follower._execute_trade_cmd(\n            trade_cmd=test_trade_cmd,\n            users=[mock_user],\n            expire_seconds=10,\n            entrust_prop=\"limit\",\n            send_interval=10,\n        )\n        excepted_price = test_price * (1 - follower.slippage)\n        _, kwargs = getattr(mock_user, test_trade_cmd[\"action\"]).call_args\n        self.assertAlmostEqual(kwargs[\"price\"], excepted_price)\n\n\nclass TestXqFollower(unittest.TestCase):\n    def setUp(self):\n        self.follower = XueQiuFollower()\n        cookies = os.getenv(\"EZ_TEST_XQ_COOKIES\")\n        if not cookies:\n            return\n        self.follower.login(cookies=cookies)\n\n    def test_extract_transactions(self):\n        result = self.follower.extract_transactions(TEST_XQ_PORTOFOLIO_HISTORY)\n        self.assertTrue(len(result) == 1)\n\n\nTEST_POSITION = [\n    {\n        \"Unnamed: 14\": \"\",\n        \"买入冻结\": 0,\n        \"交易市场\": \"深Ａ\",\n        \"卖出冻结\": 0,\n        \"参考市价\": 1.464,\n        \"参考市值\": 919.39,\n        \"参考成本价\": 1.534,\n        \"参考盈亏\": -43.77,\n        \"可用余额\": 628,\n        \"当前持仓\": 628,\n        \"盈亏比例(%)\": -4.544,\n        \"股东代码\": \"0000000000\",\n        \"股份余额\": 628,\n        \"证券代码\": \"169101\",\n    }\n]\n\nTEST_XQ_PORTOFOLIO_HISTORY = {\n    \"count\": 1,\n    \"page\": 1,\n    \"totalCount\": 17,\n    \"list\": [\n        {\n            \"id\": 1,\n            \"status\": \"pending\",\n            \"cube_id\": 1,\n            \"prev_bebalancing_id\": 1,\n            \"category\": \"user_rebalancing\",\n            \"exe_strategy\": \"intraday_all\",\n            \"created_at\": 1,\n            \"updated_at\": 1,\n            \"cash_value\": 0.1,\n            \"cash\": 100.0,\n            \"error_code\": \"1\",\n            \"error_message\": None,\n            \"error_status\": None,\n            \"holdings\": None,\n            \"rebalancing_histories\": [\n                {\n                    \"id\": 1,\n                    \"rebalancing_id\": 1,\n                    \"stock_id\": 1023662,\n                    \"stock_name\": \"华宝油气\",\n                    \"stock_symbol\": \"SZ162411\",\n                    \"volume\": 0.0,\n                    \"price\": None,\n                    \"net_value\": 0.0,\n                    \"weight\": 0.0,\n                    \"target_weight\": 0.1,\n                    \"prev_weight\": None,\n                    \"prev_target_weight\": None,\n                    \"prev_weight_adjusted\": None,\n                    \"prev_volume\": None,\n                    \"prev_price\": None,\n                    \"prev_net_value\": None,\n                    \"proactive\": True,\n                    \"created_at\": 1554339333333,\n                    \"updated_at\": 1554339233333,\n                    \"target_volume\": 0.00068325,\n                    \"prev_target_volume\": None,\n                },\n                {\n                    \"id\": 2,\n                    \"rebalancing_id\": 1,\n                    \"stock_id\": 1023662,\n                    \"stock_name\": \"华宝油气\",\n                    \"stock_symbol\": \"SZ162411\",\n                    \"volume\": 0.0,\n                    \"price\": 0.55,\n                    \"net_value\": 0.0,\n                    \"weight\": 0.0,\n                    \"target_weight\": 0.1,\n                    \"prev_weight\": None,\n                    \"prev_target_weight\": None,\n                    \"prev_weight_adjusted\": None,\n                    \"prev_volume\": None,\n                    \"prev_price\": None,\n                    \"prev_net_value\": None,\n                    \"proactive\": True,\n                    \"created_at\": 1554339333333,\n                    \"updated_at\": 1554339233333,\n                    \"target_volume\": 0.00068325,\n                    \"prev_target_volume\": None,\n                },\n            ],\n            \"comment\": \"\",\n            \"diff\": 0.0,\n            \"new_buy_count\": 0,\n        }\n    ],\n    \"maxPage\": 17,\n}\n"
  },
  {
    "path": "tests/test_xqtrader.py",
    "content": "# coding: utf-8\nimport unittest\n\nfrom easytrader.xqtrader import XueQiuTrader\n\n\nclass TestXueQiuTrader(unittest.TestCase):\n    def test_prepare_account(self):\n        user = XueQiuTrader()\n        params_without_cookies = dict(\n            portfolio_code=\"ZH123456\", portfolio_market=\"cn\"\n        )\n        with self.assertRaises(TypeError):\n            user._prepare_account(**params_without_cookies)\n\n        params_without_cookies.update(cookies=\"123\")\n        user._prepare_account(**params_without_cookies)\n        self.assertEqual(params_without_cookies, user.account_config)\n"
  },
  {
    "path": "xq.json",
    "content": "{\n  \"cookies\": \"雪球 cookies，登陆后获取，获取方式见 https://smalltool.github.io/2016/08/02/cookie/\",\n  \"portfolio_code\": \"组合代码(例:ZH818559)\",\n  \"portfolio_market\": \"交易市场(例:us 或者 cn 或者 hk)\"\n}\n"
  },
  {
    "path": "yh_client.json",
    "content": "{\n  \"user\": \"银河用户名\",\n  \"password\": \"银河明文密码\"\n}"
  }
]