frytrader 0.1.0__py3-none-any.whl

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 (53) hide show
  1. easytrader/__init__.py +11 -0
  2. easytrader/api.py +128 -0
  3. easytrader/background_focus.py +147 -0
  4. easytrader/background_runtime.py +133 -0
  5. easytrader/background_trader.py +542 -0
  6. easytrader/background_windows.py +1115 -0
  7. easytrader/clienttrader.py +684 -0
  8. easytrader/config/__init__.py +0 -0
  9. easytrader/config/background_controls.ahk +52 -0
  10. easytrader/config/client.py +193 -0
  11. easytrader/config/global.json +52 -0
  12. easytrader/config/xq.json +11 -0
  13. easytrader/exceptions.py +11 -0
  14. easytrader/follower.py +408 -0
  15. easytrader/gf_clienttrader.py +84 -0
  16. easytrader/gj_clienttrader.py +80 -0
  17. easytrader/grid_strategies.py +845 -0
  18. easytrader/ht_clienttrader.py +76 -0
  19. easytrader/htzq_clienttrader.py +61 -0
  20. easytrader/joinquant_follower.py +140 -0
  21. easytrader/log.py +14 -0
  22. easytrader/miniqmt/__init__.py +1 -0
  23. easytrader/miniqmt/miniqmt_trader.py +653 -0
  24. easytrader/native/bin/focus_guard32.dll +0 -0
  25. easytrader/native/bin/focus_host32.exe +0 -0
  26. easytrader/native/build.ps1 +38 -0
  27. easytrader/native/focus_guard.c +23 -0
  28. easytrader/native/focus_host.c +92 -0
  29. easytrader/native/kernel32.def +18 -0
  30. easytrader/native/user32.def +9 -0
  31. easytrader/pop_dialog_handler.py +112 -0
  32. easytrader/refresh_strategies.py +59 -0
  33. easytrader/remoteclient.py +135 -0
  34. easytrader/ricequant_follower.py +119 -0
  35. easytrader/screenshot.py +409 -0
  36. easytrader/server.py +135 -0
  37. easytrader/universal_clienttrader.py +60 -0
  38. easytrader/utils/__init__.py +1 -0
  39. easytrader/utils/captcha.py +250 -0
  40. easytrader/utils/misc.py +31 -0
  41. easytrader/utils/perf.py +48 -0
  42. easytrader/utils/stock.py +86 -0
  43. easytrader/utils/win_gui.py +3 -0
  44. easytrader/webtrader.py +245 -0
  45. easytrader/wk_clienttrader.py +56 -0
  46. easytrader/xq_follower.py +249 -0
  47. easytrader/xqtrader.py +644 -0
  48. easytrader/yh_clienttrader.py +129 -0
  49. frytrader-0.1.0.dist-info/METADATA +237 -0
  50. frytrader-0.1.0.dist-info/RECORD +53 -0
  51. frytrader-0.1.0.dist-info/WHEEL +5 -0
  52. frytrader-0.1.0.dist-info/licenses/LICENSE +22 -0
  53. frytrader-0.1.0.dist-info/top_level.txt +1 -0
easytrader/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ import urllib3
3
+
4
+ from easytrader import exceptions
5
+ from easytrader.api import use, follower
6
+ from easytrader.log import logger
7
+
8
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
9
+
10
+ __version__ = "0.1.0"
11
+ __author__ = "shidenggui, Fryt1"
easytrader/api.py ADDED
@@ -0,0 +1,128 @@
1
+ # -*- coding: utf-8 -*-
2
+ import logging
3
+ import sys
4
+
5
+ import six
6
+
7
+ from easytrader.joinquant_follower import JoinQuantFollower
8
+ from easytrader.log import logger
9
+ from easytrader.ricequant_follower import RiceQuantFollower
10
+ from easytrader.xq_follower import XueQiuFollower
11
+ from easytrader.xqtrader import XueQiuTrader
12
+
13
+ if sys.version_info <= (3, 5):
14
+ raise TypeError("不支持 Python3.5 及以下版本,请升级")
15
+
16
+
17
+ def use(broker, debug=False, **kwargs):
18
+ """用于生成特定的券商对象
19
+ :param broker: 券商名支持
20
+ 例如 ['miniqmt', 'xq', '雪球', 'gj_client', '国金客户端', "universal_client", "通用同花顺客户端", "ths", "同花顺客户端"] 等
21
+ :param debug: 控制 debug 日志的显示, 默认为 False
22
+ :param initial_assets: [雪球参数] 控制雪球初始资金,默认为一百万
23
+ :return the class of trader
24
+
25
+ Usage::
26
+
27
+ >>> import easytrader
28
+ >>> user = easytrader.use('xq')
29
+ >>> user.prepare('xq.json')
30
+ """
31
+ if "background" in kwargs:
32
+ if not isinstance(kwargs["background"], bool):
33
+ raise TypeError("background 必须是布尔值")
34
+ if kwargs["background"] and broker.lower() not in (
35
+ "ths", "同花顺客户端"
36
+ ):
37
+ raise NotImplementedError("后台只读模式仅支持 ths 远航版")
38
+
39
+ if debug:
40
+ logger.setLevel(logging.DEBUG)
41
+
42
+ if broker.lower() in ["xq", "雪球"]:
43
+ return XueQiuTrader(**kwargs)
44
+
45
+ if broker.lower() in ["yh_client", "银河客户端"]:
46
+ from .yh_clienttrader import YHClientTrader
47
+
48
+ return YHClientTrader()
49
+
50
+ if broker.lower() in ["ht_client", "华泰客户端"]:
51
+ from .ht_clienttrader import HTClientTrader
52
+
53
+ return HTClientTrader()
54
+
55
+ if broker.lower() in ["wk_client", "五矿客户端"]:
56
+ from easytrader.wk_clienttrader import WKClientTrader
57
+
58
+ return WKClientTrader()
59
+
60
+ if broker.lower() in ["htzq_client", "海通证券客户端"]:
61
+ from easytrader.htzq_clienttrader import HTZQClientTrader
62
+
63
+ return HTZQClientTrader()
64
+
65
+ if broker.lower() in ["gj_client", "国金客户端"]:
66
+ from .gj_clienttrader import GJClientTrader
67
+
68
+ return GJClientTrader()
69
+
70
+ if broker.lower() in ["gf_client", "广发客户端"]:
71
+ from .gf_clienttrader import GFClientTrader
72
+
73
+ return GFClientTrader()
74
+
75
+ if broker.lower() in ["universal_client", "通用同花顺客户端"]:
76
+ from easytrader.universal_clienttrader import UniversalClientTrader
77
+
78
+ return UniversalClientTrader()
79
+
80
+ if broker.lower() in ["ths", "同花顺客户端"]:
81
+ background = kwargs.pop("background", False)
82
+ if not isinstance(background, bool):
83
+ raise TypeError("background 必须是布尔值")
84
+ if background:
85
+ from .background_trader import BackgroundThsTrader
86
+
87
+ return BackgroundThsTrader(**kwargs)
88
+ from .clienttrader import ClientTrader
89
+
90
+ return ClientTrader()
91
+
92
+ if broker.lower() in ["miniqmt"]:
93
+ try:
94
+ import xtquant
95
+ except:
96
+ logger.error("miniqmt 相关组件 xtqimt 未安装, 请执行 pip install easytrader[xtquant]安装")
97
+ from easytrader.miniqmt.miniqmt_trader import MiniqmtTrader
98
+
99
+ return MiniqmtTrader()
100
+
101
+ raise NotImplementedError
102
+
103
+
104
+ def follower(platform, **kwargs):
105
+ """用于生成特定的券商对象
106
+ :param platform:平台支持 ['jq', 'joinquant', '聚宽’]
107
+ :param initial_assets: [雪球参数] 控制雪球初始资金,默认为一万,
108
+ 总资金由 initial_assets * 组合当前净值 得出
109
+ :param total_assets: [雪球参数] 控制雪球总资金,无默认值,
110
+ 若设置则覆盖 initial_assets
111
+ :return the class of follower
112
+
113
+ Usage::
114
+
115
+ >>> import easytrader
116
+ >>> user = easytrader.use('xq')
117
+ >>> user.prepare('xq.json')
118
+ >>> jq = easytrader.follower('jq')
119
+ >>> jq.login(user='username', password='password')
120
+ >>> jq.follow(users=user, strategies=['strategies_link'])
121
+ """
122
+ if platform.lower() in ["rq", "ricequant", "米筐"]:
123
+ return RiceQuantFollower()
124
+ if platform.lower() in ["jq", "joinquant", "聚宽"]:
125
+ return JoinQuantFollower()
126
+ if platform.lower() in ["xq", "xueqiu", "雪球"]:
127
+ return XueQiuFollower(**kwargs)
128
+ raise NotImplementedError
@@ -0,0 +1,147 @@
1
+ """Bounded thread-specific activation veto for the x86 Voyager UI thread.
2
+
3
+ The helper and hook are built from the included native sources. They never
4
+ patch executable files or send input; Windows removes the hook if the host
5
+ exits. Each query owns its helper and explicitly stops it in finally.
6
+ """
7
+
8
+ import hashlib
9
+ import os
10
+ import struct
11
+ import subprocess
12
+ import tempfile
13
+ import time
14
+ from pathlib import Path
15
+
16
+ from easytrader.background_runtime import BackgroundError
17
+
18
+ _HASHES = {
19
+ "focus_guard32.dll": (
20
+ "6f9143a13d75c0e2b874a6854bb1bede94f0408bae7e4dff086941e88481236b"
21
+ ),
22
+ "focus_host32.exe": (
23
+ "5753bae4146a96b394f4981858e2d425330595c23b236b42bc0c90e19eb49f24"
24
+ ),
25
+ }
26
+
27
+
28
+ def pe_machine(path):
29
+ """Reject a bitness mismatch before a window is moved or hooked."""
30
+ with Path(path).open("rb") as stream:
31
+ if stream.read(2) != b"MZ":
32
+ raise BackgroundError("原生保护文件不是 PE 可执行文件")
33
+ stream.seek(0x3C)
34
+ offset_bytes = stream.read(4)
35
+ if len(offset_bytes) != 4:
36
+ raise BackgroundError("PE 文件头不完整")
37
+ offset = struct.unpack("<I", offset_bytes)[0]
38
+ if not 64 <= offset <= 16 * 1024 * 1024:
39
+ raise BackgroundError("PE 文件头位置无效")
40
+ stream.seek(offset)
41
+ if stream.read(4) != b"PE\0\0":
42
+ raise BackgroundError("PE 文件签名无效")
43
+ machine = stream.read(2)
44
+ if len(machine) != 2:
45
+ raise BackgroundError("PE 架构信息缺失")
46
+ return struct.unpack("<H", machine)[0]
47
+
48
+
49
+ class ThreadActivationGuard:
50
+ def __init__(self, directory=None):
51
+ self.directory = (
52
+ Path(directory)
53
+ if directory
54
+ else Path(__file__).resolve().parent / "native" / "bin"
55
+ )
56
+ for name, expected in _HASHES.items():
57
+ path = self.directory / name
58
+ if not path.is_file():
59
+ raise BackgroundError("缺少原生防激活保护,请重新安装完整包")
60
+ if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
61
+ raise BackgroundError("原生防激活保护校验失败,请重新安装")
62
+ if pe_machine(path) != 0x14C:
63
+ raise BackgroundError("当前防激活保护仅支持 x86 下单窗口")
64
+ self.host = None
65
+ self.temp = None
66
+ self.log = None
67
+ self.folder = None
68
+
69
+ def start(self, backend, guard):
70
+ if self.host is not None:
71
+ raise BackgroundError("原生防激活保护已在运行")
72
+ guard.check()
73
+ hwnd, pid, thread = backend.hook_target()
74
+ self.temp = tempfile.TemporaryDirectory(prefix="frytrader-focus-")
75
+ self.folder = Path(self.temp.name)
76
+ self.log = (self.folder / "host.log").open("wb")
77
+ (self.folder / "target.txt").write_text(
78
+ "%d,0,%d,%d,%d" % (hwnd, pid, thread, os.getpid()),
79
+ encoding="ascii",
80
+ )
81
+ environment = os.environ.copy()
82
+ environment["FRYTRADER_FOCUS_DIRECTORY"] = str(self.folder)
83
+ environment["FRYTRADER_FOCUS_DLL"] = str(
84
+ self.directory / "focus_guard32.dll"
85
+ )
86
+ try:
87
+ self.host = subprocess.Popen(
88
+ [str(self.directory / "focus_host32.exe")],
89
+ env=environment,
90
+ stdin=subprocess.DEVNULL,
91
+ stdout=self.log,
92
+ stderr=subprocess.STDOUT,
93
+ shell=False,
94
+ )
95
+ deadline = min(time.monotonic() + 2, guard.deadline)
96
+ ready = self.folder / "ready.txt"
97
+ while not ready.exists():
98
+ guard.check()
99
+ if self.host.poll() is not None:
100
+ raise BackgroundError(
101
+ "防激活宿主未就绪(exit=%s)" % self.host.returncode
102
+ )
103
+ if time.monotonic() >= deadline:
104
+ raise BackgroundError("防激活宿主启动超时")
105
+ time.sleep(0.01)
106
+ if ready.read_text(encoding="ascii") != "ok":
107
+ raise BackgroundError("防激活宿主就绪信号无效")
108
+ self.check()
109
+ guard.check()
110
+ except BaseException:
111
+ self.close()
112
+ raise
113
+
114
+ def check(self):
115
+ if self.host is None or self.host.poll() is not None:
116
+ raise BackgroundError("防激活保护已退出,已停止后台操作")
117
+
118
+ def close(self):
119
+ failed = None
120
+ try:
121
+ if self.host is not None:
122
+ if self.host.poll() is None:
123
+ (self.folder / "stop.txt").write_text(
124
+ "stop", encoding="ascii"
125
+ )
126
+ try:
127
+ self.host.wait(timeout=2)
128
+ except subprocess.TimeoutExpired:
129
+ # Only our own helper is terminated.
130
+ self.host.terminate()
131
+ self.host.wait(timeout=2)
132
+ failed = "防激活宿主未能正常卸载,已终止自己的宿主"
133
+ if self.host.returncode != 0 and failed is None:
134
+ failed = (
135
+ "防激活宿主退出异常(exit=%s)" % self.host.returncode
136
+ )
137
+ finally:
138
+ self.host = None
139
+ if self.log is not None:
140
+ self.log.close()
141
+ self.log = None
142
+ if self.temp is not None:
143
+ self.temp.cleanup()
144
+ self.temp = None
145
+ self.folder = None
146
+ if failed:
147
+ raise BackgroundError(failed)
@@ -0,0 +1,133 @@
1
+ """Nonactivating AutoHotkey bridge, restricted to captcha controls."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+ import time
8
+ from pathlib import Path
9
+
10
+
11
+ class BackgroundError(RuntimeError):
12
+ """A background read could not be confirmed safely."""
13
+
14
+
15
+ class BackgroundInterference(BackgroundError):
16
+ """The user desktop or foreground was unexpectedly taken by Voyager."""
17
+
18
+
19
+ def resolve_ahk_path(configured=None):
20
+ """Use a configured/runtime executable; never download or install here."""
21
+ candidates = []
22
+ if configured:
23
+ candidates.append(Path(configured).expanduser())
24
+ elif os.environ.get("FRYTRADER_AHK"):
25
+ candidates.append(Path(os.environ["FRYTRADER_AHK"]).expanduser())
26
+ else:
27
+ local = os.environ.get("LOCALAPPDATA")
28
+ if local:
29
+ candidates.append(
30
+ Path(local) / "Frytrader" / "runtime" / "AutoHotkey64.exe"
31
+ )
32
+ discovered = shutil.which("AutoHotkey64.exe")
33
+ if discovered:
34
+ candidates.append(Path(discovered))
35
+ program_files = os.environ.get("ProgramFiles")
36
+ if program_files:
37
+ candidates.append(
38
+ Path(program_files) / "AutoHotkey" / "v2" / "AutoHotkey64.exe"
39
+ )
40
+ for candidate in candidates:
41
+ if candidate.is_file() and candidate.suffix.lower() == ".exe":
42
+ return candidate.resolve()
43
+ raise BackgroundError(
44
+ "未找到 AutoHotkey v2。请配置 ahk_path 或 FRYTRADER_AHK,"
45
+ "后台模式不会自动安装软件或退回真实键鼠输入。"
46
+ )
47
+
48
+
49
+ class AutoHotkeyControls:
50
+ """Only ControlSendText and ControlClick NA; no foreground fallback."""
51
+
52
+ def __init__(self, ahk_path=None):
53
+ self.executable = resolve_ahk_path(ahk_path)
54
+ self.script = (
55
+ Path(__file__).resolve().parent
56
+ / "config"
57
+ / "background_controls.ahk"
58
+ )
59
+ if not self.script.is_file():
60
+ raise BackgroundError("后台控件脚本未随安装包提供")
61
+
62
+ @staticmethod
63
+ def validate_code(code):
64
+ if (
65
+ not isinstance(code, str)
66
+ or len(code) != 4
67
+ or not code.isascii()
68
+ or not code.isalnum()
69
+ ):
70
+ raise BackgroundError("验证码必须是四位 ASCII 字母或数字")
71
+
72
+ def _run(self, action, controls, control, pid, guard, code=None):
73
+ guard.check()
74
+ with tempfile.TemporaryDirectory(prefix="frytrader-na-") as folder:
75
+ directory = Path(folder)
76
+ output = directory / "result.txt"
77
+ command = [
78
+ str(self.executable),
79
+ "/ErrorStdOut",
80
+ str(self.script),
81
+ action,
82
+ str(controls.dialog),
83
+ str(control),
84
+ str(pid),
85
+ str(output),
86
+ ]
87
+ if code is not None:
88
+ command.append(code)
89
+ # Regular files/DEVNULL, not named-pipe stdio. No shell quoting.
90
+ with (directory / "helper.log").open("wb") as log:
91
+ process = subprocess.Popen(
92
+ command,
93
+ stdin=subprocess.DEVNULL,
94
+ stdout=log,
95
+ stderr=subprocess.STDOUT,
96
+ shell=False,
97
+ )
98
+ try:
99
+ deadline = min(time.monotonic() + 5, guard.deadline)
100
+ while process.poll() is None:
101
+ guard.check()
102
+ if time.monotonic() >= deadline:
103
+ raise BackgroundError("NA 控件操作超时")
104
+ time.sleep(0.01)
105
+ guard.check()
106
+ result = (
107
+ output.read_text(encoding="utf-8-sig")
108
+ if output.exists()
109
+ else "missing result"
110
+ )
111
+ if process.returncode or result != "ok":
112
+ raise BackgroundError(
113
+ "NA 控件操作未确认(%s,exit=%s)"
114
+ % (action, process.returncode)
115
+ )
116
+ finally:
117
+ # This is our short-lived bridge, never the trader.
118
+ if process.poll() is None:
119
+ process.terminate()
120
+ try:
121
+ process.wait(timeout=1)
122
+ except subprocess.TimeoutExpired:
123
+ process.kill()
124
+ process.wait(timeout=1)
125
+
126
+ def fill(self, controls, code, pid, guard):
127
+ self.validate_code(code)
128
+ self._run("fill", controls, controls.editor, pid, guard, code)
129
+
130
+ def click(self, controls, role, pid, guard):
131
+ if role not in ("ok", "cancel", "image"):
132
+ raise BackgroundError("后台桥接只允许验证码对话框操作")
133
+ self._run("click", controls, getattr(controls, role), pid, guard)