emtl 0.1.5.dev1__tar.gz → 0.2.1.dev3__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.
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.bumpversion.cfg +3 -3
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.cookiecutterrc +1 -1
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.github/workflows/github-actions.yml +0 -36
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.pre-commit-config.yaml +1 -1
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/CHANGELOG.rst +22 -0
- {emtl-0.1.5.dev1/src/emtl.egg-info → emtl-0.2.1.dev3}/PKG-INFO +34 -5
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/README.rst +10 -3
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/conf.py +1 -1
- emtl-0.2.1.dev3/docs/usage.rst +12 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/setup.py +4 -4
- emtl-0.2.1.dev3/src/emtl/__init__.py +23 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl/_version.py +2 -2
- emtl-0.2.1.dev3/src/emtl/cli.py +27 -0
- emtl-0.2.1.dev3/src/emtl/core.py +233 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl/tests/test_cli.py +1 -1
- emtl-0.2.1.dev3/src/emtl/tests/test_core.py +88 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl/utils.py +17 -22
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3/src/emtl.egg-info}/PKG-INFO +34 -5
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/tox.ini +1 -3
- emtl-0.1.5.dev1/docs/usage.rst +0 -12
- emtl-0.1.5.dev1/src/emtl/__init__.py +0 -11
- emtl-0.1.5.dev1/src/emtl/cli.py +0 -30
- emtl-0.1.5.dev1/src/emtl/core.py +0 -141
- emtl-0.1.5.dev1/src/emtl/tests/test_core.py +0 -27
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.coveragerc +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.editorconfig +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.github/workflows/document.yml +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.gitignore +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/.readthedocs.yml +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/AUTHORS.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/CONTRIBUTING.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/LICENSE +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/ci/bootstrap.py +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/ci/requirements.txt +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/ci/templates/.github/workflows/github-actions.yml +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/authors.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/changelog.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/contributing.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/index.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/installation.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/readme.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/reference/emtl.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/reference/index.rst +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/requirements.txt +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/docs/spelling_wordlist.txt +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/pyproject.toml +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/pytest.ini +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/setup.cfg +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl/__main__.py +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl/const.py +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl/tests/__init__.py +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl.egg-info/SOURCES.txt +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl.egg-info/dependency_links.txt +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl.egg-info/entry_points.txt +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl.egg-info/not-zip-safe +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl.egg-info/requires.txt +0 -0
- {emtl-0.1.5.dev1 → emtl-0.2.1.dev3}/src/emtl.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[bumpversion]
|
|
2
|
-
current_version = 0.
|
|
2
|
+
current_version = 0.2.0
|
|
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}"
|
|
@@ -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:
|
|
@@ -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.
|
|
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,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: emtl
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.1.dev3
|
|
4
|
+
Summary: 东方财富自动交易接口
|
|
5
5
|
Home-page: https://github.com/riiy/emtl
|
|
6
|
-
Author:
|
|
6
|
+
Author: Riiy
|
|
7
7
|
Author-email: riiyzhou@gmail.com
|
|
8
8
|
License: MIT
|
|
9
9
|
Project-URL: Documentation, https://emtl.readthedocs.io/
|
|
@@ -11,7 +11,7 @@ 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
13
|
Platform: UNKNOWN
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
17
|
Classifier: Operating System :: Unix
|
|
@@ -33,7 +33,7 @@ Overview
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
东方财富自动交易接口
|
|
37
37
|
|
|
38
38
|
* Free software: MIT license
|
|
39
39
|
|
|
@@ -81,6 +81,13 @@ Note, to combine the coverage data from all the tox environments run:
|
|
|
81
81
|
PYTEST_ADDOPTS=--cov-append tox
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
Getting help
|
|
85
|
+
============
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
|
|
84
91
|
Changelog
|
|
85
92
|
=========
|
|
86
93
|
|
|
@@ -105,4 +112,26 @@ Changelog
|
|
|
105
112
|
|
|
106
113
|
* update docs theme
|
|
107
114
|
|
|
115
|
+
0.1.5 (2024-04-24)
|
|
116
|
+
------------------
|
|
117
|
+
|
|
118
|
+
* update login refrence
|
|
119
|
+
|
|
120
|
+
0.1.7 (2024-04-27)
|
|
121
|
+
------------------
|
|
122
|
+
|
|
123
|
+
* add insert order api
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
0.1.8 (2024-04-30)
|
|
127
|
+
------------------
|
|
128
|
+
|
|
129
|
+
* delete macos actions
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
0.2.0 (2024-05-21)
|
|
133
|
+
------------------
|
|
134
|
+
|
|
135
|
+
* complete all method
|
|
136
|
+
|
|
108
137
|
|
|
@@ -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.
|
|
44
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/riiy/emtl/v0.2.0.svg
|
|
45
45
|
:alt: Commits since latest release
|
|
46
|
-
:target: https://github.com/riiy/emtl/compare/v0.
|
|
46
|
+
:target: https://github.com/riiy/emtl/compare/v0.2.0...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.
|
|
@@ -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.
|
|
19
|
+
"fallback_version": "0.2.0",
|
|
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="
|
|
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 ::
|
|
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.0"
|
|
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.
|
|
16
|
-
__version_tuple__ = version_tuple = (0,
|
|
15
|
+
__version__ = version = '0.2.1.dev3'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 2, 1, 'dev3')
|
|
@@ -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()
|
|
@@ -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
|
-
|
|
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,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: emtl
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.1.dev3
|
|
4
|
+
Summary: 东方财富自动交易接口
|
|
5
5
|
Home-page: https://github.com/riiy/emtl
|
|
6
|
-
Author:
|
|
6
|
+
Author: Riiy
|
|
7
7
|
Author-email: riiyzhou@gmail.com
|
|
8
8
|
License: MIT
|
|
9
9
|
Project-URL: Documentation, https://emtl.readthedocs.io/
|
|
@@ -11,7 +11,7 @@ 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
13
|
Platform: UNKNOWN
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
17
|
Classifier: Operating System :: Unix
|
|
@@ -33,7 +33,7 @@ Overview
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
东方财富自动交易接口
|
|
37
37
|
|
|
38
38
|
* Free software: MIT license
|
|
39
39
|
|
|
@@ -81,6 +81,13 @@ Note, to combine the coverage data from all the tox environments run:
|
|
|
81
81
|
PYTEST_ADDOPTS=--cov-append tox
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
Getting help
|
|
85
|
+
============
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
|
|
84
91
|
Changelog
|
|
85
92
|
=========
|
|
86
93
|
|
|
@@ -105,4 +112,26 @@ Changelog
|
|
|
105
112
|
|
|
106
113
|
* update docs theme
|
|
107
114
|
|
|
115
|
+
0.1.5 (2024-04-24)
|
|
116
|
+
------------------
|
|
117
|
+
|
|
118
|
+
* update login refrence
|
|
119
|
+
|
|
120
|
+
0.1.7 (2024-04-27)
|
|
121
|
+
------------------
|
|
122
|
+
|
|
123
|
+
* add insert order api
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
0.1.8 (2024-04-30)
|
|
127
|
+
------------------
|
|
128
|
+
|
|
129
|
+
* delete macos actions
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
0.2.0 (2024-05-21)
|
|
133
|
+
------------------
|
|
134
|
+
|
|
135
|
+
* complete all method
|
|
136
|
+
|
|
108
137
|
|
|
@@ -14,7 +14,7 @@ envlist =
|
|
|
14
14
|
clean,
|
|
15
15
|
check,
|
|
16
16
|
docs,
|
|
17
|
-
{py310,py311}-{cover
|
|
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]
|
emtl-0.1.5.dev1/docs/usage.rst
DELETED
emtl-0.1.5.dev1/src/emtl/cli.py
DELETED
|
@@ -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)
|
emtl-0.1.5.dev1/src/emtl/core.py
DELETED
|
@@ -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
|
|
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
|