dragon-quant 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.
- dragon_quant/__init__.py +17 -0
- dragon_quant/__main__.py +5 -0
- dragon_quant/analyze.py +113 -0
- dragon_quant/cache/__init__.py +0 -0
- dragon_quant/cache/data_cache.py +171 -0
- dragon_quant/cli.py +301 -0
- dragon_quant/data.py +222 -0
- dragon_quant/logging/__init__.py +2 -0
- dragon_quant/logging/logger.py +189 -0
- dragon_quant/logging/query.py +236 -0
- dragon_quant/logging/reporter.py +331 -0
- dragon_quant/models/__init__.py +0 -0
- dragon_quant/models/types.py +88 -0
- dragon_quant/orchestrator.py +476 -0
- dragon_quant/providers/__init__.py +23 -0
- dragon_quant/providers/base.py +62 -0
- dragon_quant/providers/cookie.py +92 -0
- dragon_quant/providers/eastmoney.py +283 -0
- dragon_quant/providers/tencent.py +201 -0
- dragon_quant/providers/xueqiu.py +179 -0
- dragon_quant/rate_limit.py +103 -0
- dragon_quant/scorers/__init__.py +11 -0
- dragon_quant/scorers/absorption.py +297 -0
- dragon_quant/scorers/anti_drop.py +206 -0
- dragon_quant/scorers/drive.py +406 -0
- dragon_quant/scorers/leadership.py +225 -0
- dragon_quant/storage/__init__.py +0 -0
- dragon_quant/storage/manager.py +109 -0
- dragon_quant/storage/paths.py +43 -0
- dragon_quant/utils/__init__.py +0 -0
- dragon_quant-0.1.0.dist-info/METADATA +520 -0
- dragon_quant-0.1.0.dist-info/RECORD +36 -0
- dragon_quant-0.1.0.dist-info/WHEEL +5 -0
- dragon_quant-0.1.0.dist-info/entry_points.txt +2 -0
- dragon_quant-0.1.0.dist-info/licenses/LICENSE +21 -0
- dragon_quant-0.1.0.dist-info/top_level.txt +1 -0
dragon_quant/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dragon_quant — 龙头战法四维量化筛选系统
|
|
3
|
+
|
|
4
|
+
公共 API:
|
|
5
|
+
from dragon_quant import scan # 编排器:完整扫描
|
|
6
|
+
from dragon_quant.data import ( # 原子数据查询
|
|
7
|
+
get_sector_ranking, get_sector_components, get_sector_5min_kline,
|
|
8
|
+
get_kline, get_minute_kline, get_quote, batch_get_quotes,
|
|
9
|
+
)
|
|
10
|
+
from dragon_quant.logging.query import ( # 日志查询
|
|
11
|
+
tail_logs, query_logs, clear_logs, list_logs, log_summary,
|
|
12
|
+
)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dragon_quant.orchestrator import scan
|
|
16
|
+
|
|
17
|
+
__all__ = ["scan"]
|
dragon_quant/__main__.py
ADDED
dragon_quant/analyze.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
analyze — 子进程入口:加载共享缓存,对全部候选股四维打分
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python -m dragon_quant.analyze --shared-cache <path> [--json]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import argparse
|
|
11
|
+
from dragon_quant.cache.data_cache import DataCache
|
|
12
|
+
from dragon_quant.models.types import Candidate
|
|
13
|
+
from dragon_quant.scorers.drive import score as score_drive
|
|
14
|
+
from dragon_quant.scorers.anti_drop import score as score_anti_drop
|
|
15
|
+
from dragon_quant.scorers.leadership import score as score_leadership
|
|
16
|
+
from dragon_quant.scorers.absorption import score as score_absorption
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SCORERS = [
|
|
20
|
+
("drive", score_drive, 0.35),
|
|
21
|
+
("anti_drop", score_anti_drop, 0.15),
|
|
22
|
+
("leadership", score_leadership, 0.25),
|
|
23
|
+
("absorption", score_absorption, 0.25),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
parser = argparse.ArgumentParser(description="龙头战法四维评分(子进程入口)")
|
|
29
|
+
parser.add_argument("--shared-cache", required=True, help="共享缓存文件路径")
|
|
30
|
+
parser.add_argument("--json", action="store_true", help="JSON 输出(默认开启)")
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
# 加载共享缓存
|
|
34
|
+
cache = DataCache()
|
|
35
|
+
with open(args.shared_cache) as f:
|
|
36
|
+
data = json.load(f)
|
|
37
|
+
cache.load_snapshot(data)
|
|
38
|
+
|
|
39
|
+
# 读取元数据
|
|
40
|
+
candidates_raw = cache.get("__meta__:candidates") or []
|
|
41
|
+
all_sector_codes = cache.get("__meta__:sector_codes") or []
|
|
42
|
+
sector_name_map_raw = cache.get("__meta__:sector_name_map") or {}
|
|
43
|
+
|
|
44
|
+
if not candidates_raw:
|
|
45
|
+
print("[]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# 重建候选股对象(用于 drive 的 peer_pool 构建)
|
|
49
|
+
candidate_pool = [
|
|
50
|
+
Candidate(
|
|
51
|
+
code=c["code"], name=c["name"],
|
|
52
|
+
concepts=c.get("concepts", []),
|
|
53
|
+
primary_sector=c.get("primary_sector", ""),
|
|
54
|
+
board_count=c.get("board_count", 0),
|
|
55
|
+
)
|
|
56
|
+
for c in candidates_raw
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# ─── 逐只打分 ───
|
|
60
|
+
results = []
|
|
61
|
+
for cand in candidates_raw:
|
|
62
|
+
code = cand["code"]
|
|
63
|
+
name = cand["name"]
|
|
64
|
+
primary_sector = cand.get("primary_sector", "")
|
|
65
|
+
|
|
66
|
+
dims = {}
|
|
67
|
+
composite = 0.0
|
|
68
|
+
|
|
69
|
+
for dim_name, score_fn, weight in SCORERS:
|
|
70
|
+
try:
|
|
71
|
+
kwargs = {"code": code, "cache": cache}
|
|
72
|
+
if dim_name == "drive":
|
|
73
|
+
kwargs["candidate_pool"] = candidate_pool
|
|
74
|
+
if dim_name in ("drive", "leadership", "absorption"):
|
|
75
|
+
kwargs["primary_sector"] = primary_sector
|
|
76
|
+
if dim_name == "absorption":
|
|
77
|
+
kwargs["all_sector_codes"] = all_sector_codes
|
|
78
|
+
kwargs["sector_name_map"] = sector_name_map_raw
|
|
79
|
+
|
|
80
|
+
sr = score_fn(**kwargs)
|
|
81
|
+
dims[dim_name] = {
|
|
82
|
+
"score": sr.score,
|
|
83
|
+
"weight": sr.weight,
|
|
84
|
+
"details": sr.details,
|
|
85
|
+
}
|
|
86
|
+
composite += sr.score * sr.weight
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f" ⚠️ {dim_name} 打分异常 {code}: {e}", file=sys.stderr)
|
|
89
|
+
dims[dim_name] = {
|
|
90
|
+
"score": 50.0,
|
|
91
|
+
"weight": weight,
|
|
92
|
+
"details": {"error": str(e)},
|
|
93
|
+
}
|
|
94
|
+
composite += 50.0 * weight
|
|
95
|
+
|
|
96
|
+
results.append({
|
|
97
|
+
"code": code,
|
|
98
|
+
"name": name,
|
|
99
|
+
"concepts": cand.get("concepts", []),
|
|
100
|
+
"board_count": cand.get("board_count", 0),
|
|
101
|
+
"primary_sector": primary_sector,
|
|
102
|
+
"primary_sector_name": sector_name_map_raw.get(primary_sector, ""),
|
|
103
|
+
"composite_score": round(composite, 2),
|
|
104
|
+
"dimensions": dims,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# 输出
|
|
108
|
+
output = json.dumps(results, ensure_ascii=False, indent=2)
|
|
109
|
+
print(output)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataCache — 内存 + 本地双重缓存
|
|
3
|
+
|
|
4
|
+
流程:调用方 → DataCache(先查缓存) → RateLimiter(并发控制) → Provider(HTTP)
|
|
5
|
+
|
|
6
|
+
缓存策略:
|
|
7
|
+
- 板块行情/个股K线 → 当天有效(过期=次日00:00)
|
|
8
|
+
- 涨停榜 → 当天有效
|
|
9
|
+
- 写入本地 JSON 做持久化(subprocess 共享)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _today_end() -> int:
|
|
22
|
+
"""当日 23:59:59 的时间戳(秒)"""
|
|
23
|
+
t = time.localtime()
|
|
24
|
+
return int(time.mktime((t.tm_year, t.tm_mon, t.tm_mday, 23, 59, 59, 0, 0, 0)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _to_json_safe(obj):
|
|
28
|
+
"""递归转换 dataclass / bytes 为 JSON 安全类型"""
|
|
29
|
+
if hasattr(obj, '__dataclass_fields__'):
|
|
30
|
+
return {
|
|
31
|
+
f.name: _to_json_safe(getattr(obj, f.name))
|
|
32
|
+
for f in obj.__dataclass_fields__.values()
|
|
33
|
+
}
|
|
34
|
+
if isinstance(obj, list):
|
|
35
|
+
return [_to_json_safe(i) for i in obj]
|
|
36
|
+
if isinstance(obj, dict):
|
|
37
|
+
return {k: _to_json_safe(v) for k, v in obj.items()}
|
|
38
|
+
if isinstance(obj, bytes):
|
|
39
|
+
return obj.decode('utf-8', errors='replace')
|
|
40
|
+
return obj
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CacheEntry:
|
|
44
|
+
__slots__ = ("data", "ttl")
|
|
45
|
+
|
|
46
|
+
def __init__(self, data: Any, ttl: int):
|
|
47
|
+
self.data = data
|
|
48
|
+
self.ttl = ttl
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def expired(self) -> bool:
|
|
52
|
+
return time.time() > self.ttl
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DataCache:
|
|
56
|
+
"""内存缓存 + 可选的本地持久化"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, cache_dir: Optional[Path] = None):
|
|
59
|
+
self._mem: dict[str, CacheEntry] = {}
|
|
60
|
+
self._lock = threading.Lock()
|
|
61
|
+
|
|
62
|
+
if cache_dir is None:
|
|
63
|
+
from dragon_quant.storage.paths import CACHE_DIR
|
|
64
|
+
cache_dir = CACHE_DIR
|
|
65
|
+
|
|
66
|
+
self._cache_dir = cache_dir
|
|
67
|
+
self._hit = 0
|
|
68
|
+
self._miss = 0
|
|
69
|
+
|
|
70
|
+
# ─── 内存操作 ───
|
|
71
|
+
|
|
72
|
+
def get(self, key: str) -> Optional[Any]:
|
|
73
|
+
with self._lock:
|
|
74
|
+
entry = self._mem.get(key)
|
|
75
|
+
if entry is None:
|
|
76
|
+
self._miss += 1
|
|
77
|
+
return None
|
|
78
|
+
if entry.expired:
|
|
79
|
+
with self._lock:
|
|
80
|
+
del self._mem[key]
|
|
81
|
+
self._miss += 1
|
|
82
|
+
return None
|
|
83
|
+
self._hit += 1
|
|
84
|
+
return entry.data
|
|
85
|
+
|
|
86
|
+
def set(self, key: str, data: Any, ttl: Optional[int] = None):
|
|
87
|
+
if ttl is None:
|
|
88
|
+
ttl = _today_end()
|
|
89
|
+
with self._lock:
|
|
90
|
+
self._mem[key] = CacheEntry(data, ttl)
|
|
91
|
+
# 持久化
|
|
92
|
+
if self._cache_dir:
|
|
93
|
+
self._persist(key, data)
|
|
94
|
+
|
|
95
|
+
def invalidate(self, key: str):
|
|
96
|
+
with self._lock:
|
|
97
|
+
self._mem.pop(key, None)
|
|
98
|
+
if self._cache_dir:
|
|
99
|
+
p = self._cache_dir / f"{key}.json"
|
|
100
|
+
if p.exists():
|
|
101
|
+
p.unlink()
|
|
102
|
+
|
|
103
|
+
def clear_expired(self):
|
|
104
|
+
now = time.time()
|
|
105
|
+
with self._lock:
|
|
106
|
+
expired = [k for k, v in self._mem.items() if v.expired]
|
|
107
|
+
for k in expired:
|
|
108
|
+
del self._mem[k]
|
|
109
|
+
|
|
110
|
+
def snapshot(self) -> dict[str, Any]:
|
|
111
|
+
"""导出全部未过期数据快照(用于共享缓存)"""
|
|
112
|
+
now = time.time()
|
|
113
|
+
result = {}
|
|
114
|
+
with self._lock:
|
|
115
|
+
for k, v in self._mem.items():
|
|
116
|
+
if v.ttl > now:
|
|
117
|
+
result[k] = v.data
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
def load_snapshot(self, data: dict[str, Any], ttl: Optional[int] = None):
|
|
121
|
+
"""从外部加载数据到缓存"""
|
|
122
|
+
if ttl is None:
|
|
123
|
+
ttl = _today_end()
|
|
124
|
+
with self._lock:
|
|
125
|
+
for k, v in data.items():
|
|
126
|
+
self._mem[k] = CacheEntry(v, ttl)
|
|
127
|
+
|
|
128
|
+
# ─── 本地持久化 ───
|
|
129
|
+
|
|
130
|
+
def _persist(self, key: str, data: Any):
|
|
131
|
+
if not self._cache_dir:
|
|
132
|
+
return
|
|
133
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
try:
|
|
135
|
+
safe = _to_json_safe(data)
|
|
136
|
+
with open(self._cache_dir / f"{key}.json", "w") as f:
|
|
137
|
+
json.dump({"data": safe, "ts": time.time()}, f, ensure_ascii=False)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f" ⚠️ 缓存持久化失败 {key}: {e}", file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
def load_persisted(self, key: str, max_age: int = 86400) -> Optional[Any]:
|
|
142
|
+
"""从本地文件恢复缓存"""
|
|
143
|
+
if not self._cache_dir:
|
|
144
|
+
return None
|
|
145
|
+
p = self._cache_dir / f"{key}.json"
|
|
146
|
+
if not p.exists():
|
|
147
|
+
return None
|
|
148
|
+
try:
|
|
149
|
+
with open(p) as f:
|
|
150
|
+
blob = json.load(f)
|
|
151
|
+
age = time.time() - blob.get("ts", 0)
|
|
152
|
+
if age > max_age:
|
|
153
|
+
p.unlink()
|
|
154
|
+
return None
|
|
155
|
+
data = blob["data"]
|
|
156
|
+
self.set(key, data, ttl=int(time.time()) + max_age - int(age))
|
|
157
|
+
return data
|
|
158
|
+
except Exception:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# ─── 统计 ───
|
|
162
|
+
|
|
163
|
+
def stats(self) -> dict:
|
|
164
|
+
return {"hit": self._hit, "miss": self._miss, "size": len(self._mem)}
|
|
165
|
+
|
|
166
|
+
# ─── 助手 ───
|
|
167
|
+
|
|
168
|
+
def cache_key(self, provider: str, endpoint: str, *args) -> str:
|
|
169
|
+
parts = [provider, endpoint]
|
|
170
|
+
parts.extend(str(a) for a in args)
|
|
171
|
+
return ":".join(parts)
|
dragon_quant/cli.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI 入口 — dragon-quant 命令行工具
|
|
3
|
+
|
|
4
|
+
命令:
|
|
5
|
+
dragon-quant scan [--top 5] [--candidates 5] [--workers 2]
|
|
6
|
+
dragon-quant logs {tail,query,clear,list} [options]
|
|
7
|
+
dragon-quant data {sector,components,kline,minute,quote,batch-quote} [options]
|
|
8
|
+
dragon-quant storage {status,size,clear} [options]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from dragon_quant.orchestrator import scan as orchestrate_scan
|
|
16
|
+
from dragon_quant.storage.manager import StorageManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _cmd_scan(args):
|
|
20
|
+
"""扫描命令"""
|
|
21
|
+
orchestrate_scan(
|
|
22
|
+
top_n=args.top,
|
|
23
|
+
candidates_n=args.candidates,
|
|
24
|
+
workers=args.workers,
|
|
25
|
+
verbose=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cmd_logs(args):
|
|
30
|
+
"""日志命令"""
|
|
31
|
+
from dragon_quant.logging.query import (
|
|
32
|
+
tail_logs, query_logs, clear_logs, list_logs, log_summary,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if args.logs_action == "tail":
|
|
36
|
+
entries = tail_logs(lines=args.lines)
|
|
37
|
+
for e in entries:
|
|
38
|
+
print(json.dumps(e, ensure_ascii=False))
|
|
39
|
+
|
|
40
|
+
elif args.logs_action == "query":
|
|
41
|
+
entries = query_logs(
|
|
42
|
+
date=args.date,
|
|
43
|
+
category=args.category,
|
|
44
|
+
level=args.level,
|
|
45
|
+
code=args.code,
|
|
46
|
+
tail=args.tail,
|
|
47
|
+
)
|
|
48
|
+
for e in entries:
|
|
49
|
+
print(json.dumps(e, ensure_ascii=False))
|
|
50
|
+
|
|
51
|
+
elif args.logs_action == "clear":
|
|
52
|
+
result = clear_logs(days=args.days)
|
|
53
|
+
print(f"清除日志: {result['cleared']} 个文件")
|
|
54
|
+
for f in result.get("files_removed", []):
|
|
55
|
+
print(f" ✓ {f}")
|
|
56
|
+
print(f"保留: {result['kept']} 个文件")
|
|
57
|
+
|
|
58
|
+
elif args.logs_action == "list":
|
|
59
|
+
files = list_logs()
|
|
60
|
+
if not files:
|
|
61
|
+
print("(无日志文件)")
|
|
62
|
+
else:
|
|
63
|
+
print(f"{'文件名':30s} {'大小':>8s} {'时间':20s} {'行数':>6s}")
|
|
64
|
+
print("-" * 74)
|
|
65
|
+
for f in files:
|
|
66
|
+
print(f"{f['name']:30s} {f['size']:>8s} {f['mtime']:20s} {f['lines']:6d}")
|
|
67
|
+
|
|
68
|
+
elif args.logs_action == "summary":
|
|
69
|
+
summary = log_summary(date=args.date)
|
|
70
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cmd_data(args):
|
|
74
|
+
"""数据查询命令"""
|
|
75
|
+
from dragon_quant.data import (
|
|
76
|
+
get_sector_ranking, get_sector_components, get_sector_5min_kline,
|
|
77
|
+
get_kline, get_minute_kline, get_quote, batch_get_quotes,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if args.data_action == "sector":
|
|
81
|
+
sectors = get_sector_ranking(asc=args.asc)
|
|
82
|
+
for s in sectors:
|
|
83
|
+
print(f"{s.code:10s} {s.name:12s} {s.pct:>+8.2f}%")
|
|
84
|
+
|
|
85
|
+
elif args.data_action == "components":
|
|
86
|
+
if not args.sector:
|
|
87
|
+
print("错误: 需要 --sector <板块代码>", file=sys.stderr)
|
|
88
|
+
return
|
|
89
|
+
stocks = get_sector_components(args.sector)
|
|
90
|
+
for s in stocks:
|
|
91
|
+
print(f"{s.code:8s} {s.name:8s} {s.pct:>+8.2f}%")
|
|
92
|
+
|
|
93
|
+
elif args.data_action == "kline":
|
|
94
|
+
if not args.code:
|
|
95
|
+
print("错误: 需要 --code <股票代码>", file=sys.stderr)
|
|
96
|
+
return
|
|
97
|
+
klines = get_kline(args.code, source=args.source, days=args.days)
|
|
98
|
+
for k in klines:
|
|
99
|
+
print(json.dumps(_kbar_to_dict(k), ensure_ascii=False))
|
|
100
|
+
|
|
101
|
+
elif args.data_action == "minute":
|
|
102
|
+
if not args.code:
|
|
103
|
+
print("错误: 需要 --code <股票代码>", file=sys.stderr)
|
|
104
|
+
return
|
|
105
|
+
klines = get_minute_kline(args.code, source=args.source)
|
|
106
|
+
for k in klines:
|
|
107
|
+
print(json.dumps(_kbar_to_dict(k), ensure_ascii=False))
|
|
108
|
+
|
|
109
|
+
elif args.data_action == "quote":
|
|
110
|
+
if not args.code:
|
|
111
|
+
print("错误: 需要 --code <股票代码>", file=sys.stderr)
|
|
112
|
+
return
|
|
113
|
+
q = get_quote(args.code, source=args.source)
|
|
114
|
+
if q:
|
|
115
|
+
print(json.dumps(_to_dict(q), ensure_ascii=False, indent=2))
|
|
116
|
+
else:
|
|
117
|
+
print(f"获取行情失败: {args.code}")
|
|
118
|
+
|
|
119
|
+
elif args.data_action == "batch_quote":
|
|
120
|
+
if not args.codes:
|
|
121
|
+
print("错误: 需要 --codes <代码列表,逗号分隔>", file=sys.stderr)
|
|
122
|
+
return
|
|
123
|
+
codes = [c.strip() for c in args.codes.split(",")]
|
|
124
|
+
quotes = batch_get_quotes(codes, source=args.source)
|
|
125
|
+
for q in quotes:
|
|
126
|
+
if q:
|
|
127
|
+
print(json.dumps(_to_dict(q), ensure_ascii=False))
|
|
128
|
+
|
|
129
|
+
elif args.data_action == "cookie_status":
|
|
130
|
+
from dragon_quant.data import cookie_status
|
|
131
|
+
print(json.dumps(cookie_status(), ensure_ascii=False, indent=2))
|
|
132
|
+
|
|
133
|
+
elif args.data_action == "cookie_fetch":
|
|
134
|
+
from dragon_quant.data import fetch_cookies
|
|
135
|
+
result = fetch_cookies(source=args.source)
|
|
136
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _cmd_storage(args):
|
|
140
|
+
"""存储管理命令"""
|
|
141
|
+
mgr = StorageManager()
|
|
142
|
+
|
|
143
|
+
if args.storage_action == "status":
|
|
144
|
+
s = mgr.status()
|
|
145
|
+
print(f"数据根目录: {s['data_dir']}")
|
|
146
|
+
print(f"{'目录':10s} {'文件数':>6s} {'大小':>8s}")
|
|
147
|
+
print("-" * 28)
|
|
148
|
+
for key in ("cookies", "cache", "logs", "results", "shared"):
|
|
149
|
+
d = s[key]
|
|
150
|
+
if d["exists"]:
|
|
151
|
+
print(f"{key:10s} {d['files']:6d} {d['size']:>8s}")
|
|
152
|
+
else:
|
|
153
|
+
print(f"{key:10s} (不存在)")
|
|
154
|
+
|
|
155
|
+
elif args.storage_action == "clear":
|
|
156
|
+
if args.all:
|
|
157
|
+
r = mgr.clear_all()
|
|
158
|
+
for k, v in r.items():
|
|
159
|
+
print(f" 清理 {k}: {v} 个文件")
|
|
160
|
+
else:
|
|
161
|
+
if args.cache:
|
|
162
|
+
n = mgr.clear_cache()
|
|
163
|
+
print(f" 清理 cache: {n} 个文件")
|
|
164
|
+
if args.results:
|
|
165
|
+
n = mgr.clear_results(days=args.days)
|
|
166
|
+
print(f" 清理 results: {n} 个文件")
|
|
167
|
+
if args.days:
|
|
168
|
+
print(f" (保留最近 {args.days} 天)")
|
|
169
|
+
if args.logs:
|
|
170
|
+
n = mgr.clear_logs(days=args.days)
|
|
171
|
+
print(f" 清理 logs: {n} 个文件")
|
|
172
|
+
if args.days:
|
|
173
|
+
print(f" (保留最近 {args.days} 天)")
|
|
174
|
+
|
|
175
|
+
elif args.storage_action == "size":
|
|
176
|
+
s = mgr.size()
|
|
177
|
+
print(f"总占用: {s['total']}")
|
|
178
|
+
from dragon_quant.storage.manager import _fmt_size
|
|
179
|
+
for k, b in s["by_dir"].items():
|
|
180
|
+
print(f" {k}: {_fmt_size(b)}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _kbar_to_dict(kbar) -> dict:
|
|
184
|
+
"""KBar → dict"""
|
|
185
|
+
d = {}
|
|
186
|
+
for f in ("time", "open", "close", "high", "low", "volume", "pct"):
|
|
187
|
+
v = getattr(kbar, f, None)
|
|
188
|
+
if isinstance(v, float):
|
|
189
|
+
v = round(v, 4)
|
|
190
|
+
d[f] = v
|
|
191
|
+
return d
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _to_dict(obj) -> dict:
|
|
195
|
+
"""dataclass → dict"""
|
|
196
|
+
if hasattr(obj, '__dataclass_fields__'):
|
|
197
|
+
return {f.name: _to_dict(getattr(obj, f.name)) for f in obj.__dataclass_fields__.values()}
|
|
198
|
+
if isinstance(obj, list):
|
|
199
|
+
return [_to_dict(i) for i in obj]
|
|
200
|
+
if isinstance(obj, dict):
|
|
201
|
+
return {k: _to_dict(v) for k, v in obj.items()}
|
|
202
|
+
return obj
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main():
|
|
206
|
+
shared = argparse.ArgumentParser(add_help=False)
|
|
207
|
+
shared.add_argument("--top", type=int, default=25, help="最终候选股数量 (默认25)")
|
|
208
|
+
shared.add_argument("--candidates", type=int, default=5, help="每板块取前N只 (默认5)")
|
|
209
|
+
shared.add_argument("--workers", type=int, default=2, help="并发线程数 (默认2)")
|
|
210
|
+
|
|
211
|
+
parser = argparse.ArgumentParser(
|
|
212
|
+
description="龙头战法四维量化筛选系统",
|
|
213
|
+
)
|
|
214
|
+
parser.set_defaults(command="scan")
|
|
215
|
+
sub = parser.add_subparsers(dest="command")
|
|
216
|
+
|
|
217
|
+
# scan 子命令
|
|
218
|
+
scan_p = sub.add_parser("scan", help="批量扫描龙头股", parents=[shared])
|
|
219
|
+
|
|
220
|
+
# logs 子命令
|
|
221
|
+
logs_p = sub.add_parser("logs", help="日志查询与管理")
|
|
222
|
+
logs_subs = logs_p.add_subparsers(dest="logs_action")
|
|
223
|
+
|
|
224
|
+
tail_p = logs_subs.add_parser("tail", help="查看最新日志")
|
|
225
|
+
tail_p.add_argument("-n", "--lines", type=int, default=20, help="返回行数 (默认20)")
|
|
226
|
+
|
|
227
|
+
query_p = logs_subs.add_parser("query", help="按条件查询日志")
|
|
228
|
+
query_p.add_argument("--date", help="日期 (YYYYMMDD)")
|
|
229
|
+
query_p.add_argument("--category", help="类别过滤,如 phase、api、scorer:drive")
|
|
230
|
+
query_p.add_argument("--level", help="级别过滤,如 info、warn、error")
|
|
231
|
+
query_p.add_argument("--code", help="股票代码过滤")
|
|
232
|
+
query_p.add_argument("--tail", type=int, default=200, help="最多返回条数 (默认200)")
|
|
233
|
+
|
|
234
|
+
clear_logs_p = logs_subs.add_parser("clear", help="清除旧日志")
|
|
235
|
+
clear_logs_p.add_argument("--days", type=int, default=7, help="保留最近N天 (默认7)")
|
|
236
|
+
|
|
237
|
+
logs_subs.add_parser("list", help="列出所有日志文件")
|
|
238
|
+
sum_p = logs_subs.add_parser("summary", help="最新扫描摘要")
|
|
239
|
+
sum_p.add_argument("--date", help="日期过滤")
|
|
240
|
+
|
|
241
|
+
# data 子命令
|
|
242
|
+
data_p = sub.add_parser("data", help="原子数据查询")
|
|
243
|
+
data_subs = data_p.add_subparsers(dest="data_action")
|
|
244
|
+
|
|
245
|
+
sector_p = data_subs.add_parser("sector", help="板块排行榜")
|
|
246
|
+
sector_p.add_argument("--asc", action="store_true", help="跌幅榜(默认涨幅榜)")
|
|
247
|
+
|
|
248
|
+
comp_p = data_subs.add_parser("components", help="板块成分股")
|
|
249
|
+
comp_p.add_argument("--sector", required=True, help="板块代码,如 BK0487")
|
|
250
|
+
|
|
251
|
+
kline_p = data_subs.add_parser("kline", help="个股日K线")
|
|
252
|
+
kline_p.add_argument("--code", required=True, help="股票代码")
|
|
253
|
+
kline_p.add_argument("--source", default="xueqiu", choices=["xueqiu", "tencent"])
|
|
254
|
+
kline_p.add_argument("--days", type=int, default=20)
|
|
255
|
+
|
|
256
|
+
min_p = data_subs.add_parser("minute", help="个股1分钟K线(分时)")
|
|
257
|
+
min_p.add_argument("--code", required=True, help="股票代码")
|
|
258
|
+
min_p.add_argument("--source", default="xueqiu", choices=["xueqiu", "tencent"])
|
|
259
|
+
|
|
260
|
+
quote_p = data_subs.add_parser("quote", help="个股实时行情")
|
|
261
|
+
quote_p.add_argument("--code", required=True, help="股票代码")
|
|
262
|
+
quote_p.add_argument("--source", default="tencent", choices=["tencent", "xueqiu"])
|
|
263
|
+
|
|
264
|
+
bq_p = data_subs.add_parser("batch-quote", help="批量实时行情")
|
|
265
|
+
bq_p.add_argument("--codes", required=True, help="股票代码,逗号分隔")
|
|
266
|
+
bq_p.add_argument("--source", default="tencent", choices=["tencent", "xueqiu"])
|
|
267
|
+
|
|
268
|
+
data_subs.add_parser("cookie-status", help="查看 Cookie 状态")
|
|
269
|
+
cf_p = data_subs.add_parser("cookie-fetch", help="刷新 Cookie")
|
|
270
|
+
cf_p.add_argument("--source", default="all", choices=["all", "eastmoney", "xueqiu"])
|
|
271
|
+
|
|
272
|
+
# storage 子命令
|
|
273
|
+
st_p = sub.add_parser("storage", help="持久化数据管理")
|
|
274
|
+
st_subs = st_p.add_subparsers(dest="storage_action")
|
|
275
|
+
|
|
276
|
+
st_subs.add_parser("status", help="查看存储状态")
|
|
277
|
+
st_subs.add_parser("size", help="查看磁盘占用")
|
|
278
|
+
|
|
279
|
+
clear_p = st_subs.add_parser("clear", help="清理数据")
|
|
280
|
+
clear_p.add_argument("--all", action="store_true", help="清理全部(cache+results+logs)")
|
|
281
|
+
clear_p.add_argument("--cache", action="store_true", help="清理缓存")
|
|
282
|
+
clear_p.add_argument("--results", action="store_true", help="清理结果")
|
|
283
|
+
clear_p.add_argument("--logs", action="store_true", help="清理日志")
|
|
284
|
+
clear_p.add_argument("--days", type=int, default=None, help="保留最近N天")
|
|
285
|
+
|
|
286
|
+
args = parser.parse_args()
|
|
287
|
+
|
|
288
|
+
if args.command == "scan":
|
|
289
|
+
_cmd_scan(args)
|
|
290
|
+
elif args.command == "logs":
|
|
291
|
+
_cmd_logs(args)
|
|
292
|
+
elif args.command == "data":
|
|
293
|
+
_cmd_data(args)
|
|
294
|
+
elif args.command == "storage":
|
|
295
|
+
_cmd_storage(args)
|
|
296
|
+
else:
|
|
297
|
+
parser.print_help()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
main()
|