dragon-quant 0.2.2__tar.gz → 0.2.3__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 (69) hide show
  1. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/PKG-INFO +20 -1
  2. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/README.md +19 -0
  3. dragon_quant-0.2.3/dragon_quant/_version.py +1 -0
  4. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/cli.py +16 -0
  5. dragon_quant-0.2.3/dragon_quant/config/__init__.py +4 -0
  6. dragon_quant-0.2.3/dragon_quant/config/api_config.py +173 -0
  7. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/data.py +12 -2
  8. dragon_quant-0.2.3/dragon_quant/fix_api.py +411 -0
  9. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/logging/reporter.py +42 -14
  10. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/providers/base.py +3 -1
  11. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/providers/tencent.py +3 -1
  12. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/providers/xueqiu.py +3 -1
  13. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/rate_limit.py +12 -7
  14. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/scorers/absorption.py +182 -43
  15. dragon_quant-0.2.3/dragon_quant/scorers/anti_drop.py +408 -0
  16. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/scorers/drive.py +58 -18
  17. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/storage/db.py +307 -20
  18. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant.egg-info/PKG-INFO +20 -1
  19. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant.egg-info/SOURCES.txt +4 -0
  20. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/pyproject.toml +4 -1
  21. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_logging.py +70 -0
  22. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_providers_parsing.py +26 -1
  23. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_rate_limit.py +20 -1
  24. dragon_quant-0.2.3/tests/test_scorers_absorption.py +201 -0
  25. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_scorers_anti_drop.py +102 -3
  26. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_scorers_drive.py +70 -2
  27. dragon_quant-0.2.3/tests/test_storage.py +286 -0
  28. dragon_quant-0.2.3/web_ui/index.html +426 -0
  29. dragon_quant-0.2.2/dragon_quant/_version.py +0 -1
  30. dragon_quant-0.2.2/dragon_quant/scorers/anti_drop.py +0 -206
  31. dragon_quant-0.2.2/tests/test_scorers_absorption.py +0 -50
  32. dragon_quant-0.2.2/tests/test_storage.py +0 -125
  33. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/LICENSE +0 -0
  34. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/__init__.py +0 -0
  35. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/__main__.py +0 -0
  36. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/analyze.py +0 -0
  37. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/cache/__init__.py +0 -0
  38. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/cache/data_cache.py +0 -0
  39. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/logging/__init__.py +0 -0
  40. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/logging/logger.py +0 -0
  41. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/logging/query.py +0 -0
  42. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/models/__init__.py +0 -0
  43. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/models/types.py +0 -0
  44. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/orchestrator.py +0 -0
  45. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/providers/__init__.py +0 -0
  46. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/providers/browser.py +0 -0
  47. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/providers/cookie.py +0 -0
  48. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/providers/eastmoney.py +0 -0
  49. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/review.py +0 -0
  50. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/scorers/__init__.py +0 -0
  51. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/scorers/leadership.py +0 -0
  52. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/storage/__init__.py +0 -0
  53. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/storage/manager.py +0 -0
  54. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/storage/paths.py +0 -0
  55. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/utils/__init__.py +0 -0
  56. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant/utils/trading.py +0 -0
  57. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant.egg-info/dependency_links.txt +0 -0
  58. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant.egg-info/entry_points.txt +0 -0
  59. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant.egg-info/requires.txt +0 -0
  60. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/dragon_quant.egg-info/top_level.txt +0 -0
  61. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/setup.cfg +0 -0
  62. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_cache.py +0 -0
  63. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_models.py +0 -0
  64. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_orchestrator_helpers.py +0 -0
  65. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_providers_cookie.py +0 -0
  66. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_review.py +0 -0
  67. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/tests/test_scorers_leadership.py +0 -0
  68. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/web_ui/__init__.py +0 -0
  69. {dragon_quant-0.2.2 → dragon_quant-0.2.3}/web_ui/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dragon-quant
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: 龙头战法四维量化筛选系统 — A股涨停板龙头识别工具
5
5
  Author: gitBingxu
6
6
  License-Expression: MIT
@@ -138,6 +138,25 @@ dragon-quant data batch-quote --codes 600172,000001,002409
138
138
  dragon-quant data cookie-status # 查看 Cookie 状态
139
139
  dragon-quant data cookie-fetch # 刷新全部 Cookie
140
140
  dragon-quant data cookie-fetch --source xueqiu # 只刷新雪球
141
+
142
+ # 手动设置 Cookie(推荐兜底方案)
143
+ # 适用场景:Playwright 自动获取失败/被风控;或你已经在浏览器里抓到了可用 Cookie。
144
+ #
145
+ # 1) 从浏览器开发者工具(Network)里复制请求头中的 Cookie(整段)
146
+ # 2) 写入本地(注意:Cookie 属于敏感信息,请勿提交到仓库/群聊)
147
+
148
+ # 设置东财 Cookie
149
+ python3 -m dragon_quant.providers.cookie set --source em --cookie 'qgqp_b_id=...; st_nvi=...; nid18=...'
150
+
151
+ # 设置雪球 Cookie
152
+ python3 -m dragon_quant.providers.cookie set --source xq --cookie 'xq_a_token=...; xq_is_login=1; u=...'
153
+
154
+ # 查看是否生效
155
+ python3 -m dragon_quant.providers.cookie status --show
156
+
157
+ # Cookie 文件默认位置(macOS)
158
+ # ~/Library/Application Support/dragon-quant/cookies/eastmoney
159
+ # ~/Library/Application Support/dragon-quant/cookies/xueqiu
141
160
  ```
142
161
 
143
162
  ### `review` — 龙头回测
@@ -119,6 +119,25 @@ dragon-quant data batch-quote --codes 600172,000001,002409
119
119
  dragon-quant data cookie-status # 查看 Cookie 状态
120
120
  dragon-quant data cookie-fetch # 刷新全部 Cookie
121
121
  dragon-quant data cookie-fetch --source xueqiu # 只刷新雪球
122
+
123
+ # 手动设置 Cookie(推荐兜底方案)
124
+ # 适用场景:Playwright 自动获取失败/被风控;或你已经在浏览器里抓到了可用 Cookie。
125
+ #
126
+ # 1) 从浏览器开发者工具(Network)里复制请求头中的 Cookie(整段)
127
+ # 2) 写入本地(注意:Cookie 属于敏感信息,请勿提交到仓库/群聊)
128
+
129
+ # 设置东财 Cookie
130
+ python3 -m dragon_quant.providers.cookie set --source em --cookie 'qgqp_b_id=...; st_nvi=...; nid18=...'
131
+
132
+ # 设置雪球 Cookie
133
+ python3 -m dragon_quant.providers.cookie set --source xq --cookie 'xq_a_token=...; xq_is_login=1; u=...'
134
+
135
+ # 查看是否生效
136
+ python3 -m dragon_quant.providers.cookie status --show
137
+
138
+ # Cookie 文件默认位置(macOS)
139
+ # ~/Library/Application Support/dragon-quant/cookies/eastmoney
140
+ # ~/Library/Application Support/dragon-quant/cookies/xueqiu
122
141
  ```
123
142
 
124
143
  ### `review` — 龙头回测
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -7,6 +7,7 @@ CLI 入口 — dragon-quant 命令行工具
7
7
  dragon-quant data {sector,components,kline,minute,quote,batch-quote} [options]
8
8
  dragon-quant review [--date DATE] [--top N] [--force]
9
9
  dragon-quant storage {status,size,clear} [options]
10
+ dragon-quant fix-api [--provider eastmoney] [--show-browser]
10
11
  """
11
12
 
12
13
  import argparse
@@ -222,6 +223,12 @@ def _cmd_review_ui(args):
222
223
  start_server(port=args.port, open_browser=not args.no_browser)
223
224
 
224
225
 
226
+ def _cmd_fix_api(args):
227
+ """修复 API 配置命令"""
228
+ from dragon_quant.fix_api import fix_api
229
+ fix_api(provider=args.fix_provider, headless=not args.show_browser)
230
+
231
+
225
232
  def _cmd_storage(args):
226
233
  """存储管理命令"""
227
234
  mgr = StorageManager()
@@ -388,6 +395,13 @@ def main():
388
395
  clear_p.add_argument("--logs", action="store_true", help="清理日志")
389
396
  clear_p.add_argument("--days", type=int, default=None, help="保留最近N天")
390
397
 
398
+ # fix-api 子命令
399
+ fix_p = sub.add_parser("fix-api", help="从目标网站自动捕获并修复 API 配置")
400
+ fix_p.add_argument("--provider", dest="fix_provider", default="eastmoney",
401
+ choices=["eastmoney"], help="要修复的 provider (默认: eastmoney)")
402
+ fix_p.add_argument("--show-browser", action="store_true",
403
+ help="显示浏览器窗口 (默认无头运行)")
404
+
391
405
  args = parser.parse_args()
392
406
 
393
407
  if args.command == "scan":
@@ -400,6 +414,8 @@ def main():
400
414
  _cmd_storage(args)
401
415
  elif args.command == "review":
402
416
  _cmd_review(args)
417
+ elif args.command == "fix-api":
418
+ _cmd_fix_api(args)
403
419
 
404
420
  else:
405
421
  parser.print_help()
@@ -0,0 +1,4 @@
1
+ """
2
+ 配置管理 — API 请求模板持久化与回退
3
+ """
4
+
@@ -0,0 +1,173 @@
1
+ """
2
+ API 请求模板 — 持久化配置管理
3
+
4
+ 每个 provider 下的每个 endpoint 对应一个 RequestTemplate,
5
+ 封装请求 URL 拼接和 Header 构建。
6
+
7
+ 配置存储路径: {DATA_DIR}/config/api_config.json
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import threading
13
+ from dataclasses import dataclass, field
14
+ from typing import Optional
15
+
16
+ import urllib.parse
17
+ import time
18
+
19
+ from dragon_quant.storage.paths import DATA_DIR
20
+
21
+ CONFIG_DIR = DATA_DIR / "config"
22
+ CONFIG_PATH = CONFIG_DIR / "api_config.json"
23
+
24
+ CONFIG_VERSION = "1"
25
+
26
+
27
+ @dataclass
28
+ class RequestTemplate:
29
+ base: str
30
+ path: str
31
+ fixed_params: dict = field(default_factory=dict)
32
+ headers: dict = field(default_factory=dict)
33
+ referer: str = ""
34
+
35
+ def build_url(self, **dynamic_params) -> str:
36
+ params = {**self.fixed_params, **dynamic_params}
37
+ if "cb" not in params:
38
+ # 尽量对齐东财前端 JSONP callback 命名,降低风控概率
39
+ # 形如:jQuery112307663912278618489_1780992936600
40
+ # 注:callback 名称不会影响我们 _parse_jsonp 的解析。
41
+ ts = int(time.time() * 1000)
42
+ pseudo_rand = int(str(ts)[-6:]) * 123457 # 仅用于仿真格式,无安全意义
43
+ params["cb"] = f"jQuery{pseudo_rand}_{ts}"
44
+ if "_" not in params:
45
+ params["_"] = str(int(time.time() * 1000))
46
+ qs = urllib.parse.urlencode(params)
47
+ return f"{self.base}{self.path}?{qs}"
48
+
49
+ def build_headers(self, referer: str = "", cookie: str = "") -> dict:
50
+ headers = {}
51
+ if self.headers:
52
+ headers.update(self.headers)
53
+ if referer or self.referer:
54
+ headers["Referer"] = referer or self.referer
55
+ if cookie:
56
+ headers["Cookie"] = cookie
57
+ return headers
58
+
59
+
60
+ def _ensure_config_dir():
61
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
62
+
63
+
64
+ def load_templates(provider: str) -> Optional[dict[str, RequestTemplate]]:
65
+ if not CONFIG_PATH.exists():
66
+ return None
67
+ try:
68
+ with open(CONFIG_PATH) as f:
69
+ data = json.load(f)
70
+ provider_data = data.get(provider, {})
71
+ if not provider_data:
72
+ return None
73
+ templates = {}
74
+ for ep_name, ep_cfg in provider_data.items():
75
+ templates[ep_name] = RequestTemplate(
76
+ base=ep_cfg["base"],
77
+ path=ep_cfg["path"],
78
+ fixed_params=ep_cfg.get("fixed_params", {}),
79
+ headers=ep_cfg.get("headers", {}),
80
+ referer=ep_cfg.get("referer", ""),
81
+ )
82
+ return templates
83
+ except Exception:
84
+ return None
85
+
86
+
87
+ def save_templates(provider: str, templates: dict[str, RequestTemplate]):
88
+ _ensure_config_dir()
89
+ existing = {}
90
+ if CONFIG_PATH.exists():
91
+ try:
92
+ with open(CONFIG_PATH) as f:
93
+ existing = json.load(f)
94
+ except Exception:
95
+ existing = {}
96
+ existing.setdefault("version", CONFIG_VERSION)
97
+ provider_data = {}
98
+ for name, tpl in templates.items():
99
+ provider_data[name] = {
100
+ "base": tpl.base,
101
+ "path": tpl.path,
102
+ "fixed_params": tpl.fixed_params,
103
+ "headers": tpl.headers,
104
+ "referer": tpl.referer,
105
+ }
106
+ existing[provider] = provider_data
107
+ tmp_path = CONFIG_PATH.with_suffix(".tmp")
108
+ with open(tmp_path, "w") as f:
109
+ json.dump(existing, f, ensure_ascii=False, indent=2)
110
+ os.replace(tmp_path, CONFIG_PATH)
111
+
112
+
113
+ def save_single_template(provider: str, endpoint: str, tpl: RequestTemplate):
114
+ existing = {}
115
+ if CONFIG_PATH.exists():
116
+ try:
117
+ with open(CONFIG_PATH) as f:
118
+ existing = json.load(f)
119
+ except Exception:
120
+ existing = {}
121
+ existing.setdefault("version", CONFIG_VERSION)
122
+ existing.setdefault(provider, {})
123
+ existing[provider][endpoint] = {
124
+ "base": tpl.base,
125
+ "path": tpl.path,
126
+ "fixed_params": tpl.fixed_params,
127
+ "headers": tpl.headers,
128
+ "referer": tpl.referer,
129
+ }
130
+ _ensure_config_dir()
131
+ tmp_path = CONFIG_PATH.with_suffix(".tmp")
132
+ with open(tmp_path, "w") as f:
133
+ json.dump(existing, f, ensure_ascii=False, indent=2)
134
+ os.replace(tmp_path, CONFIG_PATH)
135
+
136
+
137
+ def hardcoded_templates(provider: str) -> dict[str, RequestTemplate]:
138
+ if provider == "eastmoney":
139
+ return {
140
+ "sector_ranking": RequestTemplate(
141
+ base="https://push2.eastmoney.com",
142
+ path="/webguest/api/qt/clist/get",
143
+ fixed_params={
144
+ "np": "1", "fltt": "1", "invt": "2",
145
+ "timil": "1", "dect": "1", "wbp2u": "|0|0|0|web",
146
+ },
147
+ headers={},
148
+ referer="https://quote.eastmoney.com/center/hsbk.html",
149
+ ),
150
+ "sector_components": RequestTemplate(
151
+ base="https://push2.eastmoney.com",
152
+ path="/api/qt/clist/get",
153
+ fixed_params={
154
+ "np": "1", "fltt": "2", "invt": "2",
155
+ "dect": "1",
156
+ },
157
+ headers={},
158
+ referer="https://quote.eastmoney.com/center/gridlist.html",
159
+ ),
160
+ "sector_5min_kline": RequestTemplate(
161
+ base="https://push2his.eastmoney.com",
162
+ path="/api/qt/stock/kline/get",
163
+ fixed_params={
164
+ "fqt": "1",
165
+ "beg": "0",
166
+ "end": "20500101",
167
+ "smplmt": "460",
168
+ },
169
+ headers={},
170
+ referer="https://quote.eastmoney.com/bk/90.{code}.html",
171
+ ),
172
+ }
173
+ return {}
@@ -81,17 +81,27 @@ def get_sector_ranking(asc: bool = False) -> list[SectorPerformance]:
81
81
  return em.get_sector_ranking(asc=asc)
82
82
 
83
83
 
84
- def get_sector_components(sector_code: str) -> list[StockInfo]:
84
+ def get_sector_components(sector_code: str, page: int = 1,
85
+ all_pages: bool = False,
86
+ page_size: int = 50) -> list[StockInfo]:
85
87
  """获取概念板块成分股列表(按涨跌幅降序)
86
88
 
87
89
  Args:
88
90
  sector_code: 板块代码,如 "BK0487"
91
+ page: 页码(默认第一页)
92
+ all_pages: 是否自动拉取全量分页
93
+ page_size: 每页大小
89
94
  Returns:
90
95
  list[StockInfo] 成分股列表
91
96
  """
92
97
  providers = _get_providers()
93
98
  em = providers["eastmoney"]
94
- return em.get_sector_components(sector_code)
99
+ return em.get_sector_components(
100
+ sector_code,
101
+ page=page,
102
+ all_pages=all_pages,
103
+ page_size=page_size,
104
+ )
95
105
 
96
106
 
97
107
  def get_sector_5min_kline(sector_code: str, bars: int = 100) -> list[KBar]: