emtl 0.1.5.dev1__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {emtl-0.1.5.dev1 → emtl-0.2.1}/.bumpversion.cfg +3 -3
  2. {emtl-0.1.5.dev1 → emtl-0.2.1}/.cookiecutterrc +1 -1
  3. {emtl-0.1.5.dev1 → emtl-0.2.1}/.github/workflows/github-actions.yml +0 -36
  4. emtl-0.2.1/.github/workflows/release.yml +29 -0
  5. {emtl-0.1.5.dev1 → emtl-0.2.1}/.pre-commit-config.yaml +1 -1
  6. {emtl-0.1.5.dev1 → emtl-0.2.1}/CHANGELOG.rst +22 -0
  7. {emtl-0.1.5.dev1/src/emtl.egg-info → emtl-0.2.1}/PKG-INFO +32 -6
  8. {emtl-0.1.5.dev1 → emtl-0.2.1}/README.rst +10 -3
  9. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/conf.py +1 -1
  10. emtl-0.2.1/docs/usage.rst +12 -0
  11. {emtl-0.1.5.dev1 → emtl-0.2.1}/setup.py +4 -4
  12. emtl-0.2.1/src/emtl/__init__.py +23 -0
  13. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl/_version.py +2 -2
  14. emtl-0.2.1/src/emtl/cli.py +27 -0
  15. emtl-0.2.1/src/emtl/core.py +233 -0
  16. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl/tests/test_cli.py +1 -1
  17. emtl-0.2.1/src/emtl/tests/test_core.py +88 -0
  18. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl/utils.py +17 -22
  19. {emtl-0.1.5.dev1 → emtl-0.2.1/src/emtl.egg-info}/PKG-INFO +32 -6
  20. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl.egg-info/SOURCES.txt +1 -0
  21. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl.egg-info/entry_points.txt +0 -1
  22. {emtl-0.1.5.dev1 → emtl-0.2.1}/tox.ini +1 -3
  23. emtl-0.1.5.dev1/docs/usage.rst +0 -12
  24. emtl-0.1.5.dev1/src/emtl/__init__.py +0 -11
  25. emtl-0.1.5.dev1/src/emtl/cli.py +0 -30
  26. emtl-0.1.5.dev1/src/emtl/core.py +0 -141
  27. emtl-0.1.5.dev1/src/emtl/tests/test_core.py +0 -27
  28. {emtl-0.1.5.dev1 → emtl-0.2.1}/.coveragerc +0 -0
  29. {emtl-0.1.5.dev1 → emtl-0.2.1}/.editorconfig +0 -0
  30. {emtl-0.1.5.dev1 → emtl-0.2.1}/.github/workflows/document.yml +0 -0
  31. {emtl-0.1.5.dev1 → emtl-0.2.1}/.gitignore +0 -0
  32. {emtl-0.1.5.dev1 → emtl-0.2.1}/.readthedocs.yml +0 -0
  33. {emtl-0.1.5.dev1 → emtl-0.2.1}/AUTHORS.rst +0 -0
  34. {emtl-0.1.5.dev1 → emtl-0.2.1}/CONTRIBUTING.rst +0 -0
  35. {emtl-0.1.5.dev1 → emtl-0.2.1}/LICENSE +0 -0
  36. {emtl-0.1.5.dev1 → emtl-0.2.1}/ci/bootstrap.py +0 -0
  37. {emtl-0.1.5.dev1 → emtl-0.2.1}/ci/requirements.txt +0 -0
  38. {emtl-0.1.5.dev1 → emtl-0.2.1}/ci/templates/.github/workflows/github-actions.yml +0 -0
  39. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/authors.rst +0 -0
  40. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/changelog.rst +0 -0
  41. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/contributing.rst +0 -0
  42. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/index.rst +0 -0
  43. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/installation.rst +0 -0
  44. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/readme.rst +0 -0
  45. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/reference/emtl.rst +0 -0
  46. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/reference/index.rst +0 -0
  47. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/requirements.txt +0 -0
  48. {emtl-0.1.5.dev1 → emtl-0.2.1}/docs/spelling_wordlist.txt +0 -0
  49. {emtl-0.1.5.dev1 → emtl-0.2.1}/pyproject.toml +0 -0
  50. {emtl-0.1.5.dev1 → emtl-0.2.1}/pytest.ini +0 -0
  51. {emtl-0.1.5.dev1 → emtl-0.2.1}/setup.cfg +0 -0
  52. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl/__main__.py +0 -0
  53. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl/const.py +0 -0
  54. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl/tests/__init__.py +0 -0
  55. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl.egg-info/dependency_links.txt +0 -0
  56. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl.egg-info/not-zip-safe +0 -0
  57. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl.egg-info/requires.txt +3 -3
  58. {emtl-0.1.5.dev1 → emtl-0.2.1}/src/emtl.egg-info/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.1.4
2
+ current_version = 0.2.1
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -24,5 +24,5 @@ search = __version__ = "{current_version}"
24
24
  replace = __version__ = "{new_version}"
25
25
 
26
26
  [bumpversion:file:.cookiecutterrc]
27
- search = version: {current_version}
28
- replace = version: {new_version}
27
+ search = version: "{current_version}"
28
+ replace = version: "{new_version}"
@@ -54,7 +54,7 @@ default_context:
54
54
  sphinx_theme: "alabaster"
55
55
  test_matrix_separate_coverage: "yes"
56
56
  tests_inside_package: "yes"
57
- version: "0.1.3"
57
+ version: "0.2.1"
58
58
  version_manager: "bump2version"
59
59
  website: "riiy.gihub.io/emtl"
60
60
  year_from: "2024"
@@ -32,48 +32,12 @@ jobs:
32
32
  python_arch: 'x64'
33
33
  tox_env: 'py310-cover'
34
34
  os: 'ubuntu-latest'
35
- - name: 'py310-nocov (ubuntu)'
36
- python: '3.10'
37
- toxpython: 'python3.10'
38
- python_arch: 'x64'
39
- tox_env: 'py310-nocov'
40
- os: 'ubuntu-latest'
41
- - name: 'py310-cover (macos)'
42
- python: '3.10'
43
- toxpython: 'python3.10'
44
- python_arch: 'x64'
45
- tox_env: 'py310-cover'
46
- os: 'macos-latest'
47
- - name: 'py310-nocov (macos)'
48
- python: '3.10'
49
- toxpython: 'python3.10'
50
- python_arch: 'x64'
51
- tox_env: 'py310-nocov'
52
- os: 'macos-latest'
53
35
  - name: 'py311-cover (ubuntu)'
54
36
  python: '3.11'
55
37
  toxpython: 'python3.11'
56
38
  python_arch: 'x64'
57
39
  tox_env: 'py311-cover'
58
40
  os: 'ubuntu-latest'
59
- - name: 'py311-nocov (ubuntu)'
60
- python: '3.11'
61
- toxpython: 'python3.11'
62
- python_arch: 'x64'
63
- tox_env: 'py311-nocov'
64
- os: 'ubuntu-latest'
65
- - name: 'py311-cover (macos)'
66
- python: '3.11'
67
- toxpython: 'python3.11'
68
- python_arch: 'x64'
69
- tox_env: 'py311-cover'
70
- os: 'macos-latest'
71
- - name: 'py311-nocov (macos)'
72
- python: '3.11'
73
- toxpython: 'python3.11'
74
- python_arch: 'x64'
75
- tox_env: 'py311-nocov'
76
- os: 'macos-latest'
77
41
  steps:
78
42
  - uses: actions/checkout@v4
79
43
  with:
@@ -0,0 +1,29 @@
1
+ name: Publish Python 🐍 distributions 📦 to PyPI
2
+ on:
3
+ push:
4
+ tags:
5
+ - '*'
6
+ jobs:
7
+ pypi-publish:
8
+ name: Upload release to PyPI
9
+ runs-on: ubuntu-latest
10
+ environment:
11
+ name: pypi
12
+ url: https://pypi.org/p/emtl
13
+ permissions:
14
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
15
+ steps:
16
+ - uses: actions/checkout@master
17
+ - name: Set up Python 3.10
18
+ uses: actions/setup-python@v3
19
+ with:
20
+ python-version: '3.10'
21
+ - name: Install pypa/setuptools
22
+ run: >-
23
+ python -m
24
+ pip install wheel
25
+ - name: Build a binary wheel
26
+ run: >-
27
+ python setup.py sdist bdist_wheel
28
+ - name: Publish package distributions to PyPI
29
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -6,7 +6,7 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)'
6
6
  # Note the order is intentional to avoid multiple passes of the hooks
7
7
  repos:
8
8
  - repo: https://github.com/astral-sh/ruff-pre-commit
9
- rev: v0.3.7
9
+ rev: v0.4.1
10
10
  hooks:
11
11
  - id: ruff
12
12
  args: [--fix, --exit-non-zero-on-fix, --show-fixes]
@@ -22,3 +22,25 @@ Changelog
22
22
  ------------------
23
23
 
24
24
  * update docs theme
25
+
26
+ 0.1.5 (2024-04-24)
27
+ ------------------
28
+
29
+ * update login refrence
30
+
31
+ 0.1.7 (2024-04-27)
32
+ ------------------
33
+
34
+ * add insert order api
35
+
36
+
37
+ 0.1.8 (2024-04-30)
38
+ ------------------
39
+
40
+ * delete macos actions
41
+
42
+
43
+ 0.2.0 (2024-05-21)
44
+ ------------------
45
+
46
+ * complete all method
@@ -1,17 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emtl
3
- Version: 0.1.5.dev1
4
- Summary: 东方财富自动交易系统
3
+ Version: 0.2.1
4
+ Summary: 东方财富自动交易接口
5
5
  Home-page: https://github.com/riiy/emtl
6
- Author: Eastmoney Trade Library
6
+ Author: Riiy
7
7
  Author-email: riiyzhou@gmail.com
8
8
  License: MIT
9
9
  Project-URL: Documentation, https://emtl.readthedocs.io/
10
10
  Project-URL: Changelog, https://emtl.readthedocs.io/en/latest/changelog.html
11
11
  Project-URL: Issue Tracker, https://github.com/riiy/emtl/issues
12
12
  Keywords: autotrade,trade,东方财富,股票,交易,交易机器人
13
- Platform: UNKNOWN
14
- Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Development Status :: 5 - Production/Stable
15
14
  Classifier: Intended Audience :: Developers
16
15
  Classifier: License :: OSI Approved :: MIT License
17
16
  Classifier: Operating System :: Unix
@@ -33,7 +32,7 @@ Overview
33
32
 
34
33
 
35
34
 
36
- 东方财富自动交易系统
35
+ 东方财富自动交易接口
37
36
 
38
37
  * Free software: MIT license
39
38
 
@@ -81,6 +80,13 @@ Note, to combine the coverage data from all the tox environments run:
81
80
  PYTEST_ADDOPTS=--cov-append tox
82
81
 
83
82
 
83
+ Getting help
84
+ ============
85
+
86
+
87
+ Join `Telegram group <https://t.me/em_trade_lib>`_. Asking a question here is often the quickest way to get a pointer in the right direction.
88
+
89
+
84
90
  Changelog
85
91
  =========
86
92
 
@@ -105,4 +111,24 @@ Changelog
105
111
 
106
112
  * update docs theme
107
113
 
114
+ 0.1.5 (2024-04-24)
115
+ ------------------
116
+
117
+ * update login refrence
118
+
119
+ 0.1.7 (2024-04-27)
120
+ ------------------
121
+
122
+ * add insert order api
123
+
124
+
125
+ 0.1.8 (2024-04-30)
126
+ ------------------
127
+
128
+ * delete macos actions
129
+
130
+
131
+ 0.2.0 (2024-05-21)
132
+ ------------------
108
133
 
134
+ * complete all method
@@ -41,15 +41,15 @@ Overview
41
41
  :alt: Supported implementations
42
42
  :target: https://pypi.org/project/emtl
43
43
 
44
- .. |commits-since| image:: https://img.shields.io/github/commits-since/riiy/emtl/v0.1.4.svg
44
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/riiy/emtl/v0.2.1.svg
45
45
  :alt: Commits since latest release
46
- :target: https://github.com/riiy/emtl/compare/v0.1.4...master
46
+ :target: https://github.com/riiy/emtl/compare/v0.2.1...master
47
47
 
48
48
 
49
49
 
50
50
  .. end-badges
51
51
 
52
- 东方财富自动交易系统
52
+ 东方财富自动交易接口
53
53
 
54
54
  * Free software: MIT license
55
55
 
@@ -95,3 +95,10 @@ Note, to combine the coverage data from all the tox environments run:
95
95
  - ::
96
96
 
97
97
  PYTEST_ADDOPTS=--cov-append tox
98
+
99
+
100
+ Getting help
101
+ ============
102
+
103
+
104
+ Join `Telegram group <https://t.me/em_trade_lib>`_. Asking a question here is often the quickest way to get a pointer in the right direction.
@@ -25,7 +25,7 @@ except Exception:
25
25
  import traceback
26
26
 
27
27
  traceback.print_exc()
28
- version = release = "0.1.4"
28
+ version = release = "0.2.1"
29
29
 
30
30
  pygments_style = "trac"
31
31
  templates_path = ["."]
@@ -0,0 +1,12 @@
1
+ =====
2
+ Usage
3
+ =====
4
+
5
+ To use the project:
6
+
7
+ .. code-block:: python
8
+
9
+ import emtl
10
+ emtl.login("2020123456789", "123456")
11
+ asset = emtl.query_asset_and_position()
12
+ print(asset)
@@ -16,15 +16,15 @@ setup(
16
16
  use_scm_version={
17
17
  "local_scheme": "dirty-tag",
18
18
  "write_to": "src/emtl/_version.py",
19
- "fallback_version": "0.1.4",
19
+ "fallback_version": "0.2.1",
20
20
  },
21
21
  license="MIT",
22
- description="东方财富自动交易系统",
22
+ description="东方财富自动交易接口",
23
23
  long_description="{}\n{}".format(
24
24
  re.compile("^.. start-badges.*^.. end-badges", re.M | re.S).sub("", read("README.rst")),
25
25
  re.sub(":[a-z]+:`~?(.*?)`", r"``\1``", read("CHANGELOG.rst")),
26
26
  ),
27
- author="Eastmoney Trade Library",
27
+ author="Riiy",
28
28
  author_email="riiyzhou@gmail.com",
29
29
  url="https://github.com/riiy/emtl",
30
30
  packages=find_packages("src"),
@@ -34,7 +34,7 @@ setup(
34
34
  zip_safe=False,
35
35
  classifiers=[
36
36
  # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers
37
- "Development Status :: 2 - Pre-Alpha",
37
+ "Development Status :: 5 - Production/Stable",
38
38
  "Intended Audience :: Developers",
39
39
  "License :: OSI Approved :: MIT License",
40
40
  "Operating System :: Unix",
@@ -0,0 +1,23 @@
1
+ __version__ = "0.2.1"
2
+
3
+ from .core import cancel_order
4
+ from .core import insert_order
5
+ from .core import login
6
+ from .core import query_asset_and_position
7
+ from .core import query_funds_flow
8
+ from .core import query_history_orders
9
+ from .core import query_history_trades
10
+ from .core import query_orders
11
+ from .core import query_trades
12
+
13
+ __all__ = [
14
+ "login",
15
+ "query_asset_and_position",
16
+ "query_orders",
17
+ "query_history_orders",
18
+ "query_trades",
19
+ "query_history_trades",
20
+ "query_funds_flow",
21
+ "insert_order",
22
+ "cancel_order",
23
+ ]
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1.5.dev1'
16
- __version_tuple__ = version_tuple = (0, 1, 5, 'dev1')
15
+ __version__ = version = '0.2.1'
16
+ __version_tuple__ = version_tuple = (0, 2, 1)
@@ -0,0 +1,27 @@
1
+ import argparse
2
+
3
+ from .utils import get_logger
4
+
5
+ logger = get_logger(__name__)
6
+
7
+
8
+ logger.info("cli")
9
+ parser = argparse.ArgumentParser(prog="emt", description="东方财富交易接口", epilog="东方财富交易接口")
10
+ parser.add_argument("-u", "--user", required=False)
11
+ parser.add_argument("-p", "--password", required=False)
12
+ group = parser.add_mutually_exclusive_group()
13
+ group.add_argument("-b", "--buy", help="买入股票,格式为:代码-价格-数量。例子买入一手平安银行:000001-10.21-100")
14
+ group.add_argument("-s", "--sell", help="卖出股票,格式为:代码-价格-数量。卖一手平安银行:000001-10.21-100")
15
+ group.add_argument("-q", "--query", type=str, choices=["asset", "order", "trade"], help="查询账户的资产、挂单、成交")
16
+
17
+
18
+ def run(args=None):
19
+ logger.info(args)
20
+ args = parser.parse_args(args=args)
21
+ logger.info(args)
22
+ print(args.user, args.password)
23
+ parser.exit(0)
24
+
25
+
26
+ if __name__ == "__main__":
27
+ run()
@@ -0,0 +1,233 @@
1
+ import os
2
+ import re
3
+ from random import SystemRandom
4
+ from typing import Any
5
+ from typing import Optional
6
+
7
+ from ddddocr import DdddOcr
8
+ from requests import Response
9
+ from requests import Session
10
+ from requests import get
11
+
12
+ from .const import _base_headers
13
+ from .const import _urls
14
+ from .utils import emt_trade_encrypt
15
+ from .utils import get_float
16
+ from .utils import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+ ocr = DdddOcr(show_ad=False)
20
+ session = Session()
21
+ _em_validate_key = ""
22
+
23
+
24
+ def _query_snapshot(symbol_code: str, market: str) -> Optional[dict]:
25
+ url = "https://emhsmarketwg.eastmoneysec.com/api/SHSZQuoteSnapshot"
26
+ params = {"id": symbol_code.strip(), "market": market}
27
+ headers = _base_headers.copy()
28
+ resp = session.get(url, params=params, headers=headers)
29
+ _check_resp(resp)
30
+ return resp.json()
31
+
32
+
33
+ def get_last_price(symbol_code: str, market: str) -> float:
34
+ ret = _query_snapshot(symbol_code, market)
35
+ if ret is None or "status" not in ret or ret["status"] != 0:
36
+ return float("nan")
37
+
38
+ return get_float(ret["realtimequote"], "currentPrice")
39
+
40
+
41
+ def _check_resp(resp: Response):
42
+ if resp.status_code != 200:
43
+ logger.error(f"request {resp.url} fail, code={resp.status_code}, response={resp.text}")
44
+ raise
45
+
46
+
47
+ def _query_something(tag: str, req_data: Optional[dict] = None) -> Optional[Response]:
48
+ """通用查询函数
49
+
50
+ :param tag: 请求类型
51
+ :param req_data: 请求提交数据,可选
52
+ :return:
53
+ """
54
+ if not _em_validate_key:
55
+ validate_key = login()
56
+ else:
57
+ validate_key = _em_validate_key
58
+ logger.info(validate_key)
59
+ assert tag in _urls, f"{tag} not in url list"
60
+ url = _urls[tag] + validate_key
61
+ if req_data is None:
62
+ req_data = {
63
+ "qqhs": 100,
64
+ "dwc": "",
65
+ }
66
+ headers = _base_headers.copy()
67
+ headers["X-Requested-With"] = "XMLHttpRequest"
68
+ logger.debug(f"(tag={tag}), (data={req_data}), (url={url})")
69
+ resp = session.post(url, headers=headers, data=req_data)
70
+ _check_resp(resp)
71
+ return resp
72
+
73
+
74
+ def _get_captcha_code() -> tuple[float, Any]:
75
+ """get random number and captcha code."""
76
+ cryptogen = SystemRandom()
77
+ random_num = cryptogen.random()
78
+ resp = get(f"{_urls['yzm']}{random_num}", headers=_base_headers, timeout=60)
79
+ _check_resp(resp)
80
+ code = ocr.classification(resp.content)
81
+ if code:
82
+ try:
83
+ code = int(code)
84
+ except Exception as e:
85
+ logger.error(f"get_captcha_code found exception: {e}, ocr result={code}")
86
+ return _get_captcha_code()
87
+
88
+ return random_num, code
89
+
90
+
91
+ def _get_em_validate_key():
92
+ """获取 em_validatekey"""
93
+ url = "https://jywg.18.cn/Trade/Buy"
94
+ resp = session.get(url, headers=_base_headers)
95
+ _check_resp(resp)
96
+ match_result = re.findall(r'id="em_validatekey" type="hidden" value="(.*?)"', resp.text)
97
+ if match_result:
98
+ _em_validatekey = match_result[0].strip()
99
+ global _em_validate_key
100
+ _em_validate_key = _em_validatekey
101
+ return _em_validatekey
102
+
103
+
104
+ def login(username: str = "", password: str = "", duration: int = 30) -> Optional[str]:
105
+ """登录接口.
106
+
107
+ :param str username: 用户名
108
+ :param str password: 密码(明文)
109
+ :param duration: 在线时长(分钟), defaults to 30
110
+ :type duration: int, optional
111
+ :return:
112
+ """
113
+ if not username:
114
+ username = os.getenv("EM_USERNAME", "")
115
+ if not password:
116
+ password = os.getenv("EM_PASSWORD", "")
117
+ random_num, code = _get_captcha_code()
118
+ headers = _base_headers.copy()
119
+ headers["X-Requested-With"] = "XMLHttpRequest"
120
+ headers["Referer"] = "https://jywg.18.cn/Login?el=1&clear=&returl=%2fTrade%2fBuy"
121
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
122
+ url = _urls["login"]
123
+ data = {
124
+ "userId": username.strip(),
125
+ "password": emt_trade_encrypt(password.strip()),
126
+ "randNumber": random_num,
127
+ "identifyCode": code,
128
+ "duration": duration,
129
+ "authCode": "",
130
+ "type": "Z",
131
+ "secInfo": "",
132
+ }
133
+ resp = session.post(url, headers=headers, data=data)
134
+ _check_resp(resp)
135
+ try:
136
+ logger.info(f"login success for {resp.json()}")
137
+ return _get_em_validate_key()
138
+ except KeyError as e:
139
+ logger.error(f"param data found exception:[{e}], [data={resp}]")
140
+
141
+
142
+ def query_asset_and_position():
143
+ """Get asset and position."""
144
+ resp = _query_something("query_asset_and_pos")
145
+ if resp:
146
+ return resp.json()
147
+
148
+
149
+ def query_orders():
150
+ """查询订单."""
151
+ resp = _query_something("query_orders")
152
+ if resp:
153
+ return resp.json()
154
+
155
+
156
+ def query_trades():
157
+ """查询成交."""
158
+ resp = _query_something("query_trades")
159
+ if resp:
160
+ return resp.json()
161
+
162
+
163
+ def query_history_orders(size, start_time, end_time):
164
+ """查询历史订单.
165
+
166
+ :param int size: 请求的数据的条目数
167
+ :param str start_time: 起始时间, 格式"%Y-%m-%d"
168
+ :param str end_time: 起始时间, 格式"%Y-%m-%d"
169
+ :return dict:
170
+ """
171
+ resp = _query_something("query_his_orders", {"qqhs": size, "dwc": "", "st": start_time, "et": end_time})
172
+ if resp:
173
+ return resp.json()
174
+
175
+
176
+ def query_history_trades(size, start_time, end_time):
177
+ """查询历史成交.
178
+
179
+ :param int size: 请求的数据的条目数
180
+ :param str start_time: 起始时间, 格式"%Y-%m-%d"
181
+ :param str end_time: 起始时间, 格式"%Y-%m-%d"
182
+ :return dict:
183
+ """
184
+ resp = _query_something("query_his_trades", {"qqhs": size, "dwc": "", "st": start_time, "et": end_time})
185
+ if resp:
186
+ return resp.json()
187
+
188
+
189
+ def query_funds_flow(size, start_time, end_time):
190
+ """查询资金.
191
+
192
+ :param int size: 请求的数据的条目数
193
+ :param str start_time: 起始时间, 格式"%Y-%m-%d"
194
+ :param str end_time: 起始时间, 格式"%Y-%m-%d"
195
+ :return dict:
196
+ """
197
+ resp = _query_something("query_funds_flow", {"qqhs": size, "dwc": "", "st": start_time, "et": end_time})
198
+ if resp:
199
+ return resp.json()
200
+
201
+
202
+ def insert_order(stock_code, trade_type, market: str, price: float, amount: int):
203
+ """交易接口, 买入或卖出.
204
+
205
+ :param str stock_code: 股票代码
206
+ :param str trade_type: 交易方向,B for buy, S for sell
207
+ :param str market: 股票市场,HA 上海, SA
208
+ :param float price: 股票价格
209
+ :param int amount: 买入/卖出数量
210
+ """
211
+ req_data = {
212
+ "stockCode": stock_code,
213
+ "tradeType": trade_type,
214
+ "zqmc": "",
215
+ "market": market,
216
+ "price": price,
217
+ "amount": amount,
218
+ }
219
+ resp = _query_something("insert_order", req_data=req_data)
220
+ logger.info(resp)
221
+ if resp:
222
+ return resp.json()
223
+
224
+
225
+ def cancel_order(order_str: str):
226
+ """取消交易接口.
227
+
228
+ :param str order_str: 订单字符串, 由成交日期+成交编号组成. 在insert_order和query_order接口, Wtrq的值是成交日期, Wtbh的值是成交编号, 格式为: 20240520_130662
229
+ """
230
+ data = {"revokes": order_str.strip()}
231
+ resp = _query_something("cancel_order", req_data=data)
232
+ if resp:
233
+ return resp.text.strip()
@@ -2,4 +2,4 @@ import subprocess
2
2
 
3
3
 
4
4
  def test_cli():
5
- assert subprocess.check_output(["emt", "-u=foo", "-p=bar"], text=True) == "('foo', 'bar')\n"
5
+ assert subprocess.check_output(["emt", "-u=foo", "-p=bar"], text=True) == "foo bar\n"
@@ -0,0 +1,88 @@
1
+ import datetime
2
+ import math
3
+ import os
4
+
5
+ from emtl.core import cancel_order
6
+ from emtl.core import get_last_price
7
+ from emtl.core import insert_order
8
+ from emtl.core import query_funds_flow
9
+ from emtl.core import query_history_orders
10
+ from emtl.core import query_history_trades
11
+ from emtl.core import query_orders
12
+ from emtl.core import query_trades
13
+
14
+ from .. import login
15
+ from .. import query_asset_and_position
16
+
17
+
18
+ def test_login():
19
+ validate_key = login(os.getenv("EM_USERNAME", ""), os.getenv("EM_PASSWORD", ""))
20
+ assert validate_key
21
+ assert len(validate_key) == len("b91d8012-b70b-4265-bb5d-f79442531017")
22
+
23
+
24
+ def test_query_asset_position():
25
+ resp = query_asset_and_position()
26
+ assert resp
27
+ assert resp["Status"] == 0
28
+
29
+
30
+ def test_query_orders():
31
+ resp = query_orders()
32
+ assert resp
33
+ assert resp["Status"] == 0
34
+
35
+
36
+ def test_query_trades():
37
+ resp = query_trades()
38
+ assert resp
39
+ assert resp["Status"] == 0
40
+
41
+
42
+ def test_query_history_orders():
43
+ end_date = datetime.datetime.now(datetime.timezone.utc)
44
+ start_date = end_date - datetime.timedelta(30)
45
+ st = start_date.strftime("%Y-%m-%d")
46
+ et = end_date.strftime("%Y-%m-%d")
47
+
48
+ resp = query_history_orders(100, st, et)
49
+ assert resp
50
+ assert resp["Status"] == 0
51
+
52
+
53
+ def test_query_history_trades():
54
+ end_date = datetime.datetime.now(datetime.timezone.utc)
55
+ start_date = end_date - datetime.timedelta(30)
56
+ st = start_date.strftime("%Y-%m-%d")
57
+ et = end_date.strftime("%Y-%m-%d")
58
+
59
+ resp = query_history_trades(100, st, et)
60
+ assert resp
61
+ assert resp["Status"] == 0
62
+
63
+
64
+ def test_query_funds_flow():
65
+ end_date = datetime.datetime.now(datetime.timezone.utc)
66
+ start_date = end_date - datetime.timedelta(30)
67
+ st = start_date.strftime("%Y-%m-%d")
68
+ et = end_date.strftime("%Y-%m-%d")
69
+
70
+ resp = query_funds_flow(100, st, et)
71
+ assert resp
72
+ assert resp["Status"] == 0
73
+
74
+
75
+ def test_insert_order():
76
+ resp = insert_order("000002", "B", "SA", 5.01, 100)
77
+ assert resp
78
+ assert resp["Status"] in (0, -1)
79
+
80
+
81
+ def test_cancel_order():
82
+ resp = cancel_order("20240520_130662")
83
+ assert resp
84
+ assert resp.startswith("130662")
85
+
86
+
87
+ def test_last_price():
88
+ assert math.isnan(get_last_price("000001", "SA"))
@@ -1,8 +1,6 @@
1
1
  import base64
2
2
  import logging
3
- from dataclasses import dataclass
4
- from typing import Any
5
- from typing import Optional
3
+ import math
6
4
 
7
5
  from cryptography.hazmat.primitives import serialization
8
6
  from cryptography.hazmat.primitives.asymmetric import padding
@@ -18,17 +16,6 @@ c0AT4qIJ/xtbBcHkFPK77upnsfDTJiVEuQDH+MiMeb+XhCLNKZGp0yaUU6GlxZdp
18
16
  """
19
17
 
20
18
 
21
- @dataclass
22
- class Response:
23
- message: str
24
- status: int
25
- error_code: str
26
- data: Any
27
-
28
- def is_ok(self) -> bool:
29
- return not self.message and self.status == 0
30
-
31
-
32
19
  def get_logger(name):
33
20
  """Logger."""
34
21
  formater = "%(asctime)s %(name)-20s %(funcName)s %(lineno)d: %(levelname)-8s: %(message)s"
@@ -37,15 +24,23 @@ def get_logger(name):
37
24
  return logger
38
25
 
39
26
 
40
- def response_deserialize(data: dict) -> Optional[Response]:
41
- if data is None:
42
- return None
43
- return Response(
44
- data["Message"] if "Message" in data else "", data["Status"], data["Errcode"] if "Errcode" in data else "", data["Data"]
45
- )
46
-
47
-
48
27
  def emt_trade_encrypt(content: str) -> str:
49
28
  _pub_key: rsa.RSAPublicKey = serialization.load_pem_public_key(rsa_public_key.encode("utf-8")) # type:ignore
50
29
  encrypt_text = _pub_key.encrypt(content.encode(), padding.PKCS1v15())
51
30
  return base64.b64encode(encrypt_text).decode("utf-8")
31
+
32
+
33
+ def double_equal(a, b) -> bool:
34
+ return math.fabs(a - b) < 1e-6
35
+
36
+
37
+ def get_float(data: dict, key: str) -> float:
38
+ if v := data[key].strip():
39
+ return float(v)
40
+ return 0.0
41
+
42
+
43
+ def get_int(data: dict, key: str) -> int:
44
+ if v := data[key].strip():
45
+ return int(v)
46
+ return 0
@@ -1,17 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emtl
3
- Version: 0.1.5.dev1
4
- Summary: 东方财富自动交易系统
3
+ Version: 0.2.1
4
+ Summary: 东方财富自动交易接口
5
5
  Home-page: https://github.com/riiy/emtl
6
- Author: Eastmoney Trade Library
6
+ Author: Riiy
7
7
  Author-email: riiyzhou@gmail.com
8
8
  License: MIT
9
9
  Project-URL: Documentation, https://emtl.readthedocs.io/
10
10
  Project-URL: Changelog, https://emtl.readthedocs.io/en/latest/changelog.html
11
11
  Project-URL: Issue Tracker, https://github.com/riiy/emtl/issues
12
12
  Keywords: autotrade,trade,东方财富,股票,交易,交易机器人
13
- Platform: UNKNOWN
14
- Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Development Status :: 5 - Production/Stable
15
14
  Classifier: Intended Audience :: Developers
16
15
  Classifier: License :: OSI Approved :: MIT License
17
16
  Classifier: Operating System :: Unix
@@ -33,7 +32,7 @@ Overview
33
32
 
34
33
 
35
34
 
36
- 东方财富自动交易系统
35
+ 东方财富自动交易接口
37
36
 
38
37
  * Free software: MIT license
39
38
 
@@ -81,6 +80,13 @@ Note, to combine the coverage data from all the tox environments run:
81
80
  PYTEST_ADDOPTS=--cov-append tox
82
81
 
83
82
 
83
+ Getting help
84
+ ============
85
+
86
+
87
+ Join `Telegram group <https://t.me/em_trade_lib>`_. Asking a question here is often the quickest way to get a pointer in the right direction.
88
+
89
+
84
90
  Changelog
85
91
  =========
86
92
 
@@ -105,4 +111,24 @@ Changelog
105
111
 
106
112
  * update docs theme
107
113
 
114
+ 0.1.5 (2024-04-24)
115
+ ------------------
116
+
117
+ * update login refrence
118
+
119
+ 0.1.7 (2024-04-27)
120
+ ------------------
121
+
122
+ * add insert order api
123
+
124
+
125
+ 0.1.8 (2024-04-30)
126
+ ------------------
127
+
128
+ * delete macos actions
129
+
130
+
131
+ 0.2.0 (2024-05-21)
132
+ ------------------
108
133
 
134
+ * complete all method
@@ -16,6 +16,7 @@ setup.py
16
16
  tox.ini
17
17
  .github/workflows/document.yml
18
18
  .github/workflows/github-actions.yml
19
+ .github/workflows/release.yml
19
20
  ci/bootstrap.py
20
21
  ci/requirements.txt
21
22
  ci/templates/.github/workflows/github-actions.yml
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  emt = emtl.cli:run
3
-
@@ -14,7 +14,7 @@ envlist =
14
14
  clean,
15
15
  check,
16
16
  docs,
17
- {py310,py311}-{cover,nocov},
17
+ {py310,py311}-{cover},
18
18
  report
19
19
  ignore_basepython_conflict = true
20
20
 
@@ -30,12 +30,10 @@ passenv =
30
30
  *
31
31
  usedevelop =
32
32
  cover: true
33
- nocov: false
34
33
  deps =
35
34
  pytest
36
35
  cover: pytest-cov
37
36
  commands =
38
- nocov: {posargs:pytest -vv --ignore=src}
39
37
  cover: {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv}
40
38
 
41
39
  [testenv:check]
@@ -1,12 +0,0 @@
1
- =====
2
- Usage
3
- =====
4
-
5
- To use the project:
6
-
7
- .. code-block:: python
8
-
9
- import emtl
10
- emtl.emt(...)
11
- emtl.login(...)
12
- emtl.query_asset_and_position(...)
@@ -1,11 +0,0 @@
1
- __version__ = "0.1.4"
2
-
3
- from .core import emt
4
- from .core import login
5
- from .core import query_asset_and_position
6
-
7
- __all__ = [
8
- "emt",
9
- "login",
10
- "query_asset_and_position",
11
- ]
@@ -1,30 +0,0 @@
1
- """
2
- Module that contains the command line app.
3
-
4
- Why does this file exist, and why not put this in __main__?
5
-
6
- You might be tempted to import things from __main__ later, but that will cause
7
- problems: the code will get executed twice:
8
-
9
- - When you run `python -memtl` python will execute
10
- ``__main__.py`` as a script. That means there will not be any
11
- ``emtl.__main__`` in ``sys.modules``.
12
- - When you import __main__ it will get executed again (as a module) because
13
- there"s no ``emtl.__main__`` in ``sys.modules``.
14
-
15
- Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration
16
- """
17
-
18
- import argparse
19
-
20
- from .core import emt
21
-
22
- parser = argparse.ArgumentParser(prog="emt", description="东方财富交易系统", epilog="东方财富交易系统")
23
- parser.add_argument("-u", "--user", required=True)
24
- parser.add_argument("-p", "--password", required=True)
25
-
26
-
27
- def run(args=None):
28
- args = parser.parse_args(args=args)
29
- print(emt(args.user, args.password))
30
- parser.exit(0)
@@ -1,141 +0,0 @@
1
- import re
2
- from random import SystemRandom
3
- from typing import Any
4
- from typing import Optional
5
-
6
- from ddddocr import DdddOcr
7
- from requests import Response
8
- from requests import Session
9
- from requests import get
10
-
11
- from .const import _base_headers
12
- from .const import _urls
13
- from .utils import Response as Resp
14
- from .utils import emt_trade_encrypt
15
- from .utils import get_logger
16
- from .utils import response_deserialize
17
-
18
- logger = get_logger(__name__)
19
- ocr = DdddOcr(show_ad=False)
20
- session = Session()
21
- _em_validate_key = ""
22
-
23
-
24
- def emt(user, password):
25
- return user, password
26
-
27
-
28
- def _check_resp(resp: Response):
29
- if resp.status_code != 200:
30
- logger.error(f"request {resp.url} fail, code={resp.status_code}, response={resp.text}")
31
- return False
32
- return True
33
-
34
-
35
- def _query_something(tag: str, count: int = 100, data: Optional[dict] = None) -> Optional[Response]:
36
- """通用查询函数
37
-
38
- :param tag: 请求类型
39
- :param count: 查询数量,可选
40
- :param data: 请求提交数据,可选
41
- :return:
42
- """
43
- assert _em_validate_key, "em_validatekey is empty"
44
- assert tag in _urls, f"{tag} not in url list"
45
- url = _urls[tag] + _em_validate_key
46
- if data is None:
47
- if count <= 0:
48
- count = 100
49
- elif count > 1000:
50
- count = 1000
51
- data = {
52
- "qqhs": count,
53
- "dwc": "",
54
- }
55
- headers = _base_headers.copy()
56
- headers["X-Requested-With"] = "XMLHttpRequest"
57
- logger.debug(f"(tag={tag}), (data={data}), (url={url})")
58
- resp = session.post(url, headers=headers, data=data)
59
- if not _check_resp(resp):
60
- return None
61
- logger.debug(resp.text)
62
- return resp
63
-
64
-
65
- def _get_captcha_code() -> Optional[tuple[float, Any]]:
66
- """get random number and captcha code."""
67
- cryptogen = SystemRandom()
68
- random_num = cryptogen.random()
69
- resp = get(f"{_urls['yzm']}{random_num}", headers=_base_headers, timeout=60)
70
- if not _check_resp(resp):
71
- return None
72
- code = ocr.classification(resp.content)
73
- logger.debug(f"random_num={random_num}, code={code}")
74
- if code:
75
- try:
76
- return random_num, int(code)
77
- except Exception as e:
78
- logger.error(f"get_captcha_code found exception: {e}, ocr result={code}")
79
- return _get_captcha_code()
80
-
81
-
82
- def _get_em_validate_key():
83
- """获取 em_validatekey"""
84
- url = "https://jywg.18.cn/Trade/Buy"
85
- resp = session.get(url, headers=_base_headers)
86
- if not _check_resp(resp):
87
- return None
88
- match_result = re.findall(r'id="em_validatekey" type="hidden" value="(.*?)"', resp.text)
89
- if match_result:
90
- _em_validatekey = match_result[0].strip()
91
- global _em_validate_key
92
- _em_validate_key = _em_validatekey
93
- return _em_validatekey
94
-
95
-
96
- def login(username: str, password: str, duration: int = 30) -> Optional[str]:
97
- """登录接口.
98
-
99
- :param str username: 用户名
100
- :param str password: 密码(明文)
101
- :param duration: 在线时长(分钟), defaults to 30
102
- :type duration: int, optional
103
- :return:
104
- """
105
- if (ret := _get_captcha_code()) is None:
106
- return ""
107
- random_num, code = ret
108
- logger.error(code)
109
- headers = _base_headers.copy()
110
- headers["X-Requested-With"] = "XMLHttpRequest"
111
- headers["Referer"] = "https://jywg.18.cn/Login?el=1&clear=&returl=%2fTrade%2fBuy"
112
- headers["Content-Type"] = "application/x-www-form-urlencoded"
113
- url = _urls["login"]
114
- data = {
115
- "userId": username.strip(),
116
- "password": emt_trade_encrypt(password.strip()),
117
- "randNumber": random_num,
118
- "identifyCode": code,
119
- "duration": duration,
120
- "authCode": "",
121
- "type": "Z",
122
- "secInfo": "",
123
- }
124
- resp = session.post(url, headers=headers, data=data)
125
- if not _check_resp(resp):
126
- return None
127
- data = resp.json()
128
- try:
129
- resp = response_deserialize(data)
130
- if resp and resp.status == 0 and resp.message.strip() == "":
131
- logger.info(f"login success for {resp.data[0]['khmc']}({username})")
132
- return _get_em_validate_key()
133
- except KeyError as e:
134
- logger.error(f"param data found exception:[{e}], [data={data}]")
135
-
136
-
137
- def query_asset_and_position() -> Optional[Resp]:
138
- """Get asset and position."""
139
- resp = _query_something("query_asset_and_pos")
140
- if resp:
141
- return response_deserialize(resp.json())
@@ -1,27 +0,0 @@
1
- import os
2
- from typing import Optional
3
-
4
- from .. import emt
5
- from .. import login
6
- from .. import query_asset_and_position
7
- from ..utils import Response
8
-
9
-
10
- def test_emt():
11
- assert emt("user", "pass") == ("user", "pass")
12
-
13
-
14
- def test_emt_empty():
15
- assert emt("", "") == ("", "")
16
-
17
-
18
- def test_login():
19
- validate_key = login(os.getenv("EM_USERNAME", ""), os.getenv("EM_PASSWORD", ""))
20
- if validate_key:
21
- assert len(validate_key) == len("b91d8012-b70b-4265-bb5d-f79442531017")
22
-
23
-
24
- def test_query_asset_position():
25
- resp: Optional[Response] = query_asset_and_position()
26
- if resp:
27
- assert resp.error_code == 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,4 +1,4 @@
1
- cryptography==42.0.5
2
- ddddocr==1.4.11
3
- requests==2.31.0
4
1
  rsa==4.9
2
+ requests==2.31.0
3
+ ddddocr==1.4.11
4
+ cryptography==42.0.5