stock-analyzer-skill 1.1.0
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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +21 -0
- package/CHANGELOG.md +93 -0
- package/CONTRIBUTING.md +331 -0
- package/README.md +259 -0
- package/experts/README.md +119 -0
- package/experts/buffett.md +91 -0
- package/experts/chaogu_yangjia.md +125 -0
- package/experts/decide.md +212 -0
- package/experts/duan_yongping.md +106 -0
- package/experts/lynch.md +127 -0
- package/experts/soros.md +89 -0
- package/experts/xu_xiang.md +107 -0
- package/experts/zhao_laoge.md +143 -0
- package/experts/zuoshou_xinyi.md +144 -0
- package/install-plugin.js +69 -0
- package/methodology.md +455 -0
- package/package.json +43 -0
- package/scripts/__pycache__/announcements.cpython-314.pyc +0 -0
- package/scripts/__pycache__/backtest.cpython-314.pyc +0 -0
- package/scripts/__pycache__/chan.cpython-314.pyc +0 -0
- package/scripts/__pycache__/classifier.cpython-314.pyc +0 -0
- package/scripts/__pycache__/common.cpython-314.pyc +0 -0
- package/scripts/__pycache__/finance.cpython-314.pyc +0 -0
- package/scripts/__pycache__/init_pool.cpython-314.pyc +0 -0
- package/scripts/__pycache__/kline.cpython-314.pyc +0 -0
- package/scripts/__pycache__/patterns_local.cpython-314.pyc +0 -0
- package/scripts/__pycache__/quote.cpython-314.pyc +0 -0
- package/scripts/__pycache__/refresh_pool.cpython-314.pyc +0 -0
- package/scripts/__pycache__/screener.cpython-314.pyc +0 -0
- package/scripts/__pycache__/technical.cpython-314.pyc +0 -0
- package/scripts/announcements.py +118 -0
- package/scripts/backtest.py +528 -0
- package/scripts/chan.py +591 -0
- package/scripts/classifier.py +302 -0
- package/scripts/common.py +507 -0
- package/scripts/data/__init__.py +208 -0
- package/scripts/data/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/data/__pycache__/cache.cpython-314.pyc +0 -0
- package/scripts/data/__pycache__/config.cpython-314.pyc +0 -0
- package/scripts/data/__pycache__/types.cpython-314.pyc +0 -0
- package/scripts/data/cache.py +99 -0
- package/scripts/data/config.py +49 -0
- package/scripts/data/industry_thresholds.json +199 -0
- package/scripts/data/portfolio_example.json +14 -0
- package/scripts/data/sector_etf.csv +14 -0
- package/scripts/data/sector_mapping.json +64 -0
- package/scripts/data/sector_stocks.json +135 -0
- package/scripts/data/types.py +66 -0
- package/scripts/fetchers/__init__.py +130 -0
- package/scripts/fetchers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/akshare_finance.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/akshare_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/akshare_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/baostock_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_finance.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/efinance_finance.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/efinance_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/efinance_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/pytdx_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/sina_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/sina_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tencent_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tencent_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tushare_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tushare_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/yfinance_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/akshare_finance.py +35 -0
- package/scripts/fetchers/akshare_kline.py +59 -0
- package/scripts/fetchers/akshare_quote.py +52 -0
- package/scripts/fetchers/baostock_kline.py +64 -0
- package/scripts/fetchers/eastmoney_finance.py +29 -0
- package/scripts/fetchers/eastmoney_kline.py +48 -0
- package/scripts/fetchers/eastmoney_quote.py +68 -0
- package/scripts/fetchers/efinance_finance.py +32 -0
- package/scripts/fetchers/efinance_kline.py +46 -0
- package/scripts/fetchers/efinance_quote.py +53 -0
- package/scripts/fetchers/pytdx_kline.py +70 -0
- package/scripts/fetchers/pytdx_quote.py +78 -0
- package/scripts/fetchers/sina_kline.py +30 -0
- package/scripts/fetchers/sina_quote.py +35 -0
- package/scripts/fetchers/tencent_kline.py +52 -0
- package/scripts/fetchers/tencent_quote.py +29 -0
- package/scripts/fetchers/tushare_kline.py +62 -0
- package/scripts/fetchers/tushare_quote.py +62 -0
- package/scripts/fetchers/yfinance_kline.py +66 -0
- package/scripts/finance.py +92 -0
- package/scripts/init_pool.py +105 -0
- package/scripts/kline.py +62 -0
- package/scripts/monitor.py +107 -0
- package/scripts/patterns_local.py +599 -0
- package/scripts/quote.py +69 -0
- package/scripts/refresh_pool.py +328 -0
- package/scripts/screener.py +434 -0
- package/scripts/strategies/__init__.py +11 -0
- package/scripts/strategies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/strategies/__pycache__/registry.cpython-314.pyc +0 -0
- package/scripts/strategies/__pycache__/thresholds.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__init__.py +8 -0
- package/scripts/strategies/factors/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/liquidity.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/momentum.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/quality.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/valuation.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/volatility.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/liquidity.py +49 -0
- package/scripts/strategies/factors/momentum.py +45 -0
- package/scripts/strategies/factors/quality.py +54 -0
- package/scripts/strategies/factors/valuation.py +76 -0
- package/scripts/strategies/factors/volatility.py +89 -0
- package/scripts/strategies/registry.py +87 -0
- package/scripts/strategies/thresholds.py +28 -0
- package/scripts/technical/__init__.py +116 -0
- package/scripts/technical/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/astock.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/boll.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/candlestick.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/core.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/kdj.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/macd.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/moving_average.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/report.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/rsi.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/scoring.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/signals.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/trend.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/volume.cpython-314.pyc +0 -0
- package/scripts/technical/astock.py +98 -0
- package/scripts/technical/boll.py +49 -0
- package/scripts/technical/candlestick.py +151 -0
- package/scripts/technical/core.py +92 -0
- package/scripts/technical/kdj.py +68 -0
- package/scripts/technical/macd.py +97 -0
- package/scripts/technical/moving_average.py +59 -0
- package/scripts/technical/report.py +221 -0
- package/scripts/technical/rsi.py +37 -0
- package/scripts/technical/scoring.py +392 -0
- package/scripts/technical/signals.py +70 -0
- package/scripts/technical/trend.py +143 -0
- package/scripts/technical/volume.py +113 -0
- package/scripts/technical.py +215 -0
- package/skills/financial-analyst/SKILL.md +141 -0
- package/skills/help/SKILL.md +188 -0
- package/skills/init/SKILL.md +66 -0
- package/skills/investment-researcher/SKILL.md +152 -0
- package/skills/market/SKILL.md +99 -0
- package/skills/portfolio/SKILL.md +96 -0
- package/skills/screener/SKILL.md +128 -0
- package/skills/sector/SKILL.md +102 -0
- package/skills/stock/SKILL.md +148 -0
- package/skills/technical/SKILL.md +168 -0
- package/workflow.md +91 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一数据获取 API。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
from data import get_quote, get_kline, get_finance
|
|
6
|
+
|
|
7
|
+
quote = get_quote("sh600989")
|
|
8
|
+
quotes = get_quotes(["sh600989", "sz000807"])
|
|
9
|
+
bars = get_kline("sh600989", scale=240, datalen=30)
|
|
10
|
+
records = get_finance("SH600989")
|
|
11
|
+
"""
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .types import Quote, KlineBar, FinanceRecord
|
|
16
|
+
from .config import get_config
|
|
17
|
+
from . import cache
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_common_helpers():
|
|
24
|
+
"""延迟导入 common,避免 common.py ↔ data/__init__.py 循环导入。"""
|
|
25
|
+
from common import to_float, to_int
|
|
26
|
+
return to_float, to_int
|
|
27
|
+
|
|
28
|
+
# 延迟导入 fetchers(避免循环导入),线程安全
|
|
29
|
+
_fetchers_lock = threading.Lock()
|
|
30
|
+
_fetchers_loaded = False
|
|
31
|
+
_quote_manager = None
|
|
32
|
+
_kline_manager = None
|
|
33
|
+
_finance_manager = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_fetchers():
|
|
37
|
+
global _fetchers_loaded, _quote_manager, _kline_manager, _finance_manager
|
|
38
|
+
if _fetchers_loaded:
|
|
39
|
+
return
|
|
40
|
+
with _fetchers_lock:
|
|
41
|
+
if _fetchers_loaded:
|
|
42
|
+
return
|
|
43
|
+
from fetchers import get_quote_manager, get_kline_manager, get_finance_manager
|
|
44
|
+
_quote_manager = get_quote_manager()
|
|
45
|
+
_kline_manager = get_kline_manager()
|
|
46
|
+
_finance_manager = get_finance_manager()
|
|
47
|
+
_fetchers_loaded = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_quote(code: str, use_cache: bool = True) -> Optional[Quote]:
|
|
51
|
+
"""获取单只股票行情。"""
|
|
52
|
+
_load_fetchers()
|
|
53
|
+
cfg = get_config()
|
|
54
|
+
key = f"quote_{code}"
|
|
55
|
+
|
|
56
|
+
if use_cache:
|
|
57
|
+
cached = cache.get_json(key, cfg.quote_cache_ttl)
|
|
58
|
+
if cached:
|
|
59
|
+
return _dict_to_quote(cached)
|
|
60
|
+
|
|
61
|
+
result = _quote_manager.fetch(code)
|
|
62
|
+
if result is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
quote = _dict_to_quote(result)
|
|
66
|
+
|
|
67
|
+
if use_cache and quote.has_basic_data():
|
|
68
|
+
cache.set_json(key, quote.to_dict())
|
|
69
|
+
|
|
70
|
+
return quote
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_quotes(codes: list, use_cache: bool = True) -> list:
|
|
74
|
+
"""批量获取行情。"""
|
|
75
|
+
from common import parallel_map
|
|
76
|
+
cfg = get_config()
|
|
77
|
+
results = parallel_map(lambda c: get_quote(c, use_cache), codes,
|
|
78
|
+
max_workers=cfg.max_workers, timeout=30)
|
|
79
|
+
return [q for q in results.values() if q is not None]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_kline(code: str, scale: int = 240, datalen: int = 30,
|
|
83
|
+
use_cache: bool = True) -> list:
|
|
84
|
+
"""获取 K 线数据。"""
|
|
85
|
+
_load_fetchers()
|
|
86
|
+
cfg = get_config()
|
|
87
|
+
key = f"kline_{code}_{scale}_{datalen}"
|
|
88
|
+
|
|
89
|
+
if use_cache:
|
|
90
|
+
cached = cache.get_json(key, cfg.kline_cache_ttl)
|
|
91
|
+
if cached:
|
|
92
|
+
return [_dict_to_kline_bar(bar) for bar in cached]
|
|
93
|
+
|
|
94
|
+
records = _kline_manager.fetch(code, scale=scale, datalen=datalen)
|
|
95
|
+
if not records:
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
bars = [_dict_to_kline_bar(r) for r in records]
|
|
99
|
+
|
|
100
|
+
if use_cache and bars:
|
|
101
|
+
cache.set_json(key, [b.to_dict() for b in bars])
|
|
102
|
+
|
|
103
|
+
return bars
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_finance(code: str, use_cache: bool = True) -> list:
|
|
107
|
+
"""获取财务数据。"""
|
|
108
|
+
_load_fetchers()
|
|
109
|
+
cfg = get_config()
|
|
110
|
+
key = f"finance_{code}"
|
|
111
|
+
|
|
112
|
+
if use_cache:
|
|
113
|
+
cached = cache.get_json(key, cfg.finance_cache_ttl)
|
|
114
|
+
if cached:
|
|
115
|
+
records = [_dict_to_finance(r) for r in cached]
|
|
116
|
+
# 校验缓存有效性:至少有一个非零数据点
|
|
117
|
+
if any(r.eps != 0 or r.roe != 0 for r in records):
|
|
118
|
+
return records
|
|
119
|
+
# 零值缓存视为无效,忽略并重新拉取
|
|
120
|
+
|
|
121
|
+
result = _finance_manager.fetch(code)
|
|
122
|
+
if not result:
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
records = [_dict_to_finance(r) for r in result]
|
|
126
|
+
|
|
127
|
+
if use_cache and records:
|
|
128
|
+
cache.set_json(key, [r.to_dict() for r in records])
|
|
129
|
+
|
|
130
|
+
return records
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------- 内部转换函数(使用 common.to_float / common.to_int) ----------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _dict_to_quote(d: dict) -> Quote:
|
|
137
|
+
to_float, to_int = _get_common_helpers()
|
|
138
|
+
return Quote(
|
|
139
|
+
code=d.get("code", ""),
|
|
140
|
+
name=d.get("name", ""),
|
|
141
|
+
price=to_float(d.get("price")),
|
|
142
|
+
prev_close=to_float(d.get("prev_close")),
|
|
143
|
+
open=to_float(d.get("open")),
|
|
144
|
+
high=to_float(d.get("high")),
|
|
145
|
+
low=to_float(d.get("low")),
|
|
146
|
+
change_pct=to_float(d.get("change_pct")),
|
|
147
|
+
change_amt=to_float(d.get("change_amt")),
|
|
148
|
+
volume=to_int(d.get("volume")),
|
|
149
|
+
amount=to_float(d.get("amount")),
|
|
150
|
+
turnover=to_float(d.get("turnover")),
|
|
151
|
+
pe=to_float(d.get("pe")),
|
|
152
|
+
pb=to_float(d.get("pb")),
|
|
153
|
+
total_cap=to_float(d.get("total_cap")),
|
|
154
|
+
circulating_cap=to_float(d.get("circulating_cap")),
|
|
155
|
+
source=d.get("source", ""),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _dict_to_kline_bar(d: dict) -> KlineBar:
|
|
160
|
+
to_float, to_int = _get_common_helpers()
|
|
161
|
+
return KlineBar(
|
|
162
|
+
day=d.get("day", ""),
|
|
163
|
+
open=to_float(d.get("open")),
|
|
164
|
+
high=to_float(d.get("high")),
|
|
165
|
+
low=to_float(d.get("low")),
|
|
166
|
+
close=to_float(d.get("close")),
|
|
167
|
+
volume=to_int(d.get("volume")),
|
|
168
|
+
amount=to_float(d.get("amount")),
|
|
169
|
+
pct_chg=to_float(d.get("pct_chg")),
|
|
170
|
+
source=d.get("source", ""),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _dict_to_finance(d: dict) -> FinanceRecord:
|
|
175
|
+
"""将 fetcher 返回的 dict 转为 FinanceRecord,支持东财原始字段名映射。"""
|
|
176
|
+
to_float, _ = _get_common_helpers()
|
|
177
|
+
FIELD_MAP = {
|
|
178
|
+
"report_date": ["REPORT_DATE", "REPORTDATETIME", "NOTICE_DATE"],
|
|
179
|
+
"eps": ["EPSJB"],
|
|
180
|
+
"roe": ["ROEJQ"],
|
|
181
|
+
"revenue_yoy": ["TOTALOPERATEREVETZ"],
|
|
182
|
+
"net_profit_yoy": ["PARENTNETPROFITTZ"],
|
|
183
|
+
"gross_margin": ["XSMLL"],
|
|
184
|
+
"net_margin": ["XSJLL"],
|
|
185
|
+
"debt_ratio": ["ZCFZL"],
|
|
186
|
+
"bps": ["BPS"],
|
|
187
|
+
"ocf_per_share": ["MGJYXJJE"],
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
def _find(candidates, default=""):
|
|
191
|
+
for k in candidates:
|
|
192
|
+
if k in d and d[k] not in (None, "", "-"):
|
|
193
|
+
return d[k]
|
|
194
|
+
return default
|
|
195
|
+
|
|
196
|
+
return FinanceRecord(
|
|
197
|
+
report_date=str(_find(FIELD_MAP["report_date"]))[:10],
|
|
198
|
+
eps=to_float(_find(FIELD_MAP["eps"])),
|
|
199
|
+
roe=to_float(_find(FIELD_MAP["roe"])),
|
|
200
|
+
revenue_yoy=to_float(_find(FIELD_MAP["revenue_yoy"])),
|
|
201
|
+
net_profit_yoy=to_float(_find(FIELD_MAP["net_profit_yoy"])),
|
|
202
|
+
gross_margin=to_float(_find(FIELD_MAP["gross_margin"])),
|
|
203
|
+
net_margin=to_float(_find(FIELD_MAP["net_margin"])),
|
|
204
|
+
debt_ratio=to_float(_find(FIELD_MAP["debt_ratio"])),
|
|
205
|
+
bps=to_float(_find(FIELD_MAP["bps"])),
|
|
206
|
+
ocf_per_share=to_float(_find(FIELD_MAP["ocf_per_share"])),
|
|
207
|
+
source=d.get("source", ""),
|
|
208
|
+
)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""统一缓存管理。"""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
_DEFAULT_CACHE_DIR = Path(__file__).resolve().parent.parent.parent / ".cache"
|
|
11
|
+
CACHE_DIR = Path(os.getenv("STOCK_CACHE_DIR", str(_DEFAULT_CACHE_DIR)))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_dir():
|
|
15
|
+
CACHE_DIR.mkdir(exist_ok=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get(key: str, ttl_seconds: int) -> Optional[bytes]:
|
|
19
|
+
"""读取缓存,TTL 超时返回 None。"""
|
|
20
|
+
_ensure_dir()
|
|
21
|
+
f = CACHE_DIR / f"{key}.cache"
|
|
22
|
+
if not f.exists():
|
|
23
|
+
return None
|
|
24
|
+
if time.time() - f.stat().st_mtime > ttl_seconds:
|
|
25
|
+
f.unlink(missing_ok=True)
|
|
26
|
+
return None
|
|
27
|
+
return f.read_bytes()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set(key: str, data: bytes):
|
|
31
|
+
"""写入缓存(原子写入:先写临时文件,再 rename)。"""
|
|
32
|
+
_ensure_dir()
|
|
33
|
+
f = CACHE_DIR / f"{key}.cache"
|
|
34
|
+
fd, tmp_path = tempfile.mkstemp(dir=CACHE_DIR, suffix=".tmp")
|
|
35
|
+
try:
|
|
36
|
+
os.write(fd, data)
|
|
37
|
+
os.close(fd)
|
|
38
|
+
fd = -1
|
|
39
|
+
os.replace(tmp_path, f)
|
|
40
|
+
except Exception:
|
|
41
|
+
if fd >= 0:
|
|
42
|
+
try:
|
|
43
|
+
os.close(fd)
|
|
44
|
+
except OSError:
|
|
45
|
+
pass
|
|
46
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_json(key: str, ttl_seconds: int):
|
|
51
|
+
"""读取 JSON 缓存。"""
|
|
52
|
+
raw = get(key, ttl_seconds)
|
|
53
|
+
if raw is None:
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
return json.loads(raw)
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def set_json(key: str, data):
|
|
62
|
+
"""写入 JSON 缓存。"""
|
|
63
|
+
set(key, json.dumps(data, ensure_ascii=False).encode())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def clear(prefix: str = ""):
|
|
67
|
+
"""清除指定前缀或全部缓存。"""
|
|
68
|
+
if not CACHE_DIR.exists():
|
|
69
|
+
return
|
|
70
|
+
for f in CACHE_DIR.glob("*.cache"):
|
|
71
|
+
if not prefix or f.stem.startswith(prefix):
|
|
72
|
+
f.unlink()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cleanup(prefix: str = None, max_age_seconds: int = 86400):
|
|
76
|
+
"""清理过期缓存。prefix 为空时清理所有过期文件。返回清理数量。"""
|
|
77
|
+
_ensure_dir()
|
|
78
|
+
cleaned = 0
|
|
79
|
+
for f in CACHE_DIR.glob("*.cache"):
|
|
80
|
+
if prefix and not f.name.startswith(prefix):
|
|
81
|
+
continue
|
|
82
|
+
if time.time() - f.stat().st_mtime > max_age_seconds:
|
|
83
|
+
f.unlink(missing_ok=True)
|
|
84
|
+
cleaned += 1
|
|
85
|
+
return cleaned
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cache_key(url: str) -> str:
|
|
89
|
+
"""用 URL 的 SHA256 生成缓存键。"""
|
|
90
|
+
return hashlib.sha256(url.encode()).hexdigest()[:32]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cache_key_for_stock(prefix: str, code: str, **params) -> str:
|
|
94
|
+
"""生成股票相关的缓存键,支持按代码清除。
|
|
95
|
+
格式: {prefix}_{code}_{param_hash}
|
|
96
|
+
"""
|
|
97
|
+
param_str = "_".join(f"{k}={v}" for k, v in sorted(params.items()))
|
|
98
|
+
param_hash = hashlib.md5(param_str.encode()).hexdigest()[:8] if param_str else ""
|
|
99
|
+
return f"{prefix}_{code}_{param_hash}".rstrip("_")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""统一配置管理。"""
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class DataConfig:
|
|
8
|
+
"""数据层配置。"""
|
|
9
|
+
# 缓存 TTL (秒)
|
|
10
|
+
quote_cache_ttl: int = 900 # 15 分钟
|
|
11
|
+
kline_cache_ttl: int = 21600 # 6 小时
|
|
12
|
+
finance_cache_ttl: int = 21600 # 6 小时
|
|
13
|
+
ann_cache_ttl: int = 1800 # 30 分钟
|
|
14
|
+
|
|
15
|
+
# 熔断器
|
|
16
|
+
circuit_failure_threshold: int = 5
|
|
17
|
+
circuit_recovery_timeout: int = 60
|
|
18
|
+
|
|
19
|
+
# 并发
|
|
20
|
+
max_workers: int = 8
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_env(cls) -> "DataConfig":
|
|
24
|
+
"""从环境变量加载配置。"""
|
|
25
|
+
cfg = cls()
|
|
26
|
+
cfg.quote_cache_ttl = int(os.getenv("DATA_QUOTE_TTL", cfg.quote_cache_ttl))
|
|
27
|
+
cfg.kline_cache_ttl = int(os.getenv("DATA_KLINE_TTL", cfg.kline_cache_ttl))
|
|
28
|
+
cfg.finance_cache_ttl = int(os.getenv("DATA_FINANCE_TTL", cfg.finance_cache_ttl))
|
|
29
|
+
cfg.circuit_failure_threshold = int(os.getenv("DATA_CIRCUIT_THRESHOLD", cfg.circuit_failure_threshold))
|
|
30
|
+
cfg.circuit_recovery_timeout = int(os.getenv("DATA_CIRCUIT_TIMEOUT", cfg.circuit_recovery_timeout))
|
|
31
|
+
cfg.max_workers = int(os.getenv("DATA_MAX_WORKERS", cfg.max_workers))
|
|
32
|
+
return cfg
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_config = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_config() -> DataConfig:
|
|
39
|
+
"""获取全局配置单例。"""
|
|
40
|
+
global _config
|
|
41
|
+
if _config is None:
|
|
42
|
+
_config = DataConfig.from_env()
|
|
43
|
+
return _config
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def reset_config():
|
|
47
|
+
"""重置配置单例(用于测试或动态重载)。"""
|
|
48
|
+
global _config
|
|
49
|
+
_config = None
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_description": "行业差异化阈值表。按申万一级行业分组,定义各行业的基本面和估值阈值。",
|
|
3
|
+
"_usage": "screener.py 和 technical/scoring.py 按行业选择阈值,避免一刀切误判。",
|
|
4
|
+
"金融": {
|
|
5
|
+
"roe_min": 10,
|
|
6
|
+
"roe_excellent": 15,
|
|
7
|
+
"gross_margin_min": 0,
|
|
8
|
+
"debt_ratio_max": 90,
|
|
9
|
+
"pe_undervalued": 8,
|
|
10
|
+
"pe_reasonable": 12,
|
|
11
|
+
"pe_expensive": 18,
|
|
12
|
+
"peg_undervalued": 0.8,
|
|
13
|
+
"peg_reasonable": 1.5,
|
|
14
|
+
"vol_threshold": 0.015,
|
|
15
|
+
"notes": "银行/保险 ROE 天然 10-15%,负债率 90%+ 属正常,用资本充足率替代负债率",
|
|
16
|
+
"profit_growth_excellent": 15,
|
|
17
|
+
"revenue_growth_excellent": 10
|
|
18
|
+
},
|
|
19
|
+
"银行": {
|
|
20
|
+
"roe_min": 8,
|
|
21
|
+
"roe_excellent": 12,
|
|
22
|
+
"gross_margin_min": 0,
|
|
23
|
+
"debt_ratio_max": 95,
|
|
24
|
+
"pe_undervalued": 5,
|
|
25
|
+
"pe_reasonable": 8,
|
|
26
|
+
"pe_expensive": 12,
|
|
27
|
+
"peg_undervalued": 0.5,
|
|
28
|
+
"peg_reasonable": 1.0,
|
|
29
|
+
"vol_threshold": 0.012,
|
|
30
|
+
"notes": "银行 ROE 天然偏低(8-12%),PE 极低(5-8倍),高负债率属正常",
|
|
31
|
+
"profit_growth_excellent": 12,
|
|
32
|
+
"revenue_growth_excellent": 8
|
|
33
|
+
},
|
|
34
|
+
"券商": {
|
|
35
|
+
"roe_min": 8,
|
|
36
|
+
"roe_excellent": 18,
|
|
37
|
+
"gross_margin_min": 0,
|
|
38
|
+
"debt_ratio_max": 85,
|
|
39
|
+
"pe_undervalued": 10,
|
|
40
|
+
"pe_reasonable": 18,
|
|
41
|
+
"pe_expensive": 28,
|
|
42
|
+
"peg_undervalued": 0.8,
|
|
43
|
+
"peg_reasonable": 1.5,
|
|
44
|
+
"vol_threshold": 0.025,
|
|
45
|
+
"notes": "券商 ROE 波动大(牛市高熊市低),PE 中等,周期性强",
|
|
46
|
+
"profit_growth_excellent": 25,
|
|
47
|
+
"revenue_growth_excellent": 20
|
|
48
|
+
},
|
|
49
|
+
"消费": {
|
|
50
|
+
"roe_min": 15,
|
|
51
|
+
"roe_excellent": 25,
|
|
52
|
+
"gross_margin_min": 30,
|
|
53
|
+
"debt_ratio_max": 50,
|
|
54
|
+
"pe_undervalued": 15,
|
|
55
|
+
"pe_reasonable": 25,
|
|
56
|
+
"pe_expensive": 40,
|
|
57
|
+
"peg_undervalued": 0.8,
|
|
58
|
+
"peg_reasonable": 1.5,
|
|
59
|
+
"vol_threshold": 0.02,
|
|
60
|
+
"notes": "白酒/食品 ROE 天然高,毛利率 60%+ 常见,估值享受溢价",
|
|
61
|
+
"profit_growth_excellent": 25,
|
|
62
|
+
"revenue_growth_excellent": 20
|
|
63
|
+
},
|
|
64
|
+
"科技": {
|
|
65
|
+
"roe_min": 10,
|
|
66
|
+
"roe_excellent": 20,
|
|
67
|
+
"gross_margin_min": 20,
|
|
68
|
+
"debt_ratio_max": 60,
|
|
69
|
+
"pe_undervalued": 20,
|
|
70
|
+
"pe_reasonable": 40,
|
|
71
|
+
"pe_expensive": 60,
|
|
72
|
+
"peg_undervalued": 0.8,
|
|
73
|
+
"peg_reasonable": 2.0,
|
|
74
|
+
"vol_threshold": 0.03,
|
|
75
|
+
"notes": "科技股 PE 30-50 常态,高研发投入导致短期 ROE 偏低,不能用传统标准",
|
|
76
|
+
"profit_growth_excellent": 50,
|
|
77
|
+
"revenue_growth_excellent": 35
|
|
78
|
+
},
|
|
79
|
+
"半导体": {
|
|
80
|
+
"roe_min": 8,
|
|
81
|
+
"roe_excellent": 15,
|
|
82
|
+
"gross_margin_min": 25,
|
|
83
|
+
"debt_ratio_max": 55,
|
|
84
|
+
"pe_undervalued": 25,
|
|
85
|
+
"pe_reasonable": 50,
|
|
86
|
+
"pe_expensive": 80,
|
|
87
|
+
"peg_undervalued": 0.8,
|
|
88
|
+
"peg_reasonable": 2.0,
|
|
89
|
+
"vol_threshold": 0.035,
|
|
90
|
+
"notes": "半导体重资产,ROE 天然偏低(8-15%),PE 容忍度高(25-80),研发投入大",
|
|
91
|
+
"profit_growth_excellent": 60,
|
|
92
|
+
"revenue_growth_excellent": 40
|
|
93
|
+
},
|
|
94
|
+
"软件": {
|
|
95
|
+
"roe_min": 12,
|
|
96
|
+
"roe_excellent": 22,
|
|
97
|
+
"gross_margin_min": 50,
|
|
98
|
+
"debt_ratio_max": 45,
|
|
99
|
+
"pe_undervalued": 30,
|
|
100
|
+
"pe_reasonable": 60,
|
|
101
|
+
"pe_expensive": 100,
|
|
102
|
+
"peg_undervalued": 1.0,
|
|
103
|
+
"peg_reasonable": 2.5,
|
|
104
|
+
"vol_threshold": 0.032,
|
|
105
|
+
"notes": "软件轻资产高毛利(50%+),ROE 偏高,PE 估值更激进(30-100)",
|
|
106
|
+
"profit_growth_excellent": 45,
|
|
107
|
+
"revenue_growth_excellent": 30
|
|
108
|
+
},
|
|
109
|
+
"周期": {
|
|
110
|
+
"roe_min": 8,
|
|
111
|
+
"roe_excellent": 15,
|
|
112
|
+
"gross_margin_min": 10,
|
|
113
|
+
"debt_ratio_max": 65,
|
|
114
|
+
"pe_undervalued": 8,
|
|
115
|
+
"pe_reasonable": 15,
|
|
116
|
+
"pe_expensive": 25,
|
|
117
|
+
"peg_undervalued": 0.5,
|
|
118
|
+
"peg_reasonable": 1.0,
|
|
119
|
+
"vol_threshold": 0.025,
|
|
120
|
+
"notes": "周期股用 3 年均值 ROE,单年高增速往往是周期顶部信号",
|
|
121
|
+
"profit_growth_excellent": 30,
|
|
122
|
+
"revenue_growth_excellent": 25
|
|
123
|
+
},
|
|
124
|
+
"医药": {
|
|
125
|
+
"roe_min": 12,
|
|
126
|
+
"roe_excellent": 20,
|
|
127
|
+
"gross_margin_min": 40,
|
|
128
|
+
"debt_ratio_max": 45,
|
|
129
|
+
"pe_undervalued": 15,
|
|
130
|
+
"pe_reasonable": 30,
|
|
131
|
+
"pe_expensive": 50,
|
|
132
|
+
"peg_undervalued": 0.8,
|
|
133
|
+
"peg_reasonable": 1.8,
|
|
134
|
+
"vol_threshold": 0.028,
|
|
135
|
+
"notes": "创新药高 PE 合理,仿制药/中药估值较低,需区分细分赛道",
|
|
136
|
+
"profit_growth_excellent": 35,
|
|
137
|
+
"revenue_growth_excellent": 25
|
|
138
|
+
},
|
|
139
|
+
"制造": {
|
|
140
|
+
"roe_min": 12,
|
|
141
|
+
"roe_excellent": 18,
|
|
142
|
+
"gross_margin_min": 15,
|
|
143
|
+
"debt_ratio_max": 60,
|
|
144
|
+
"pe_undervalued": 12,
|
|
145
|
+
"pe_reasonable": 22,
|
|
146
|
+
"pe_expensive": 35,
|
|
147
|
+
"peg_undervalued": 0.8,
|
|
148
|
+
"peg_reasonable": 1.5,
|
|
149
|
+
"vol_threshold": 0.025,
|
|
150
|
+
"notes": "制造业分化大,高端制造(新能源/半导体)估值高于传统制造",
|
|
151
|
+
"profit_growth_excellent": 35,
|
|
152
|
+
"revenue_growth_excellent": 25
|
|
153
|
+
},
|
|
154
|
+
"地产": {
|
|
155
|
+
"roe_min": 8,
|
|
156
|
+
"roe_excellent": 15,
|
|
157
|
+
"gross_margin_min": 15,
|
|
158
|
+
"debt_ratio_max": 80,
|
|
159
|
+
"pe_undervalued": 5,
|
|
160
|
+
"pe_reasonable": 10,
|
|
161
|
+
"pe_expensive": 15,
|
|
162
|
+
"peg_undervalued": 0.5,
|
|
163
|
+
"peg_reasonable": 1.0,
|
|
164
|
+
"vol_threshold": 0.028,
|
|
165
|
+
"notes": "地产用净负债率<100% 替代资产负债率,PE 天然低",
|
|
166
|
+
"profit_growth_excellent": 15,
|
|
167
|
+
"revenue_growth_excellent": 10
|
|
168
|
+
},
|
|
169
|
+
"能源": {
|
|
170
|
+
"roe_min": 10,
|
|
171
|
+
"roe_excellent": 18,
|
|
172
|
+
"gross_margin_min": 15,
|
|
173
|
+
"debt_ratio_max": 55,
|
|
174
|
+
"pe_undervalued": 6,
|
|
175
|
+
"pe_reasonable": 12,
|
|
176
|
+
"pe_expensive": 20,
|
|
177
|
+
"peg_undervalued": 0.5,
|
|
178
|
+
"peg_reasonable": 1.0,
|
|
179
|
+
"vol_threshold": 0.022,
|
|
180
|
+
"notes": "煤炭/石油周期性强,高分红低估值是常态",
|
|
181
|
+
"profit_growth_excellent": 20,
|
|
182
|
+
"revenue_growth_excellent": 15
|
|
183
|
+
},
|
|
184
|
+
"默认": {
|
|
185
|
+
"roe_min": 12,
|
|
186
|
+
"roe_excellent": 18,
|
|
187
|
+
"gross_margin_min": 20,
|
|
188
|
+
"debt_ratio_max": 60,
|
|
189
|
+
"pe_undervalued": 15,
|
|
190
|
+
"pe_reasonable": 25,
|
|
191
|
+
"pe_expensive": 40,
|
|
192
|
+
"peg_undervalued": 0.8,
|
|
193
|
+
"peg_reasonable": 1.5,
|
|
194
|
+
"vol_threshold": 0.025,
|
|
195
|
+
"notes": "未匹配行业的默认阈值",
|
|
196
|
+
"profit_growth_excellent": 40,
|
|
197
|
+
"revenue_growth_excellent": 30
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
code,name,category,bk_code
|
|
2
|
+
sh510050,上证50ETF,宽基,
|
|
3
|
+
sh510300,沪深300ETF,宽基,
|
|
4
|
+
sh510500,中证500ETF,宽基,
|
|
5
|
+
sh512010,医药ETF,行业,BK0465
|
|
6
|
+
sh512480,半导体ETF,行业,BK0917
|
|
7
|
+
sh512690,白酒ETF,行业,BK0896
|
|
8
|
+
sh512800,银行ETF,行业,BK0475
|
|
9
|
+
sh513120,港股创新药ETF,跨境,
|
|
10
|
+
sh518880,黄金ETF,商品,
|
|
11
|
+
sh515030,新能源车ETF,行业,BK0900
|
|
12
|
+
sh512660,军工ETF,行业,BK0490
|
|
13
|
+
sh515790,光伏ETF,行业,BK0588
|
|
14
|
+
sh516160,新能源ETF,行业,BK0493
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"description": "板块名 → 东财BK代码映射,供 refresh_pool.py 使用",
|
|
4
|
+
"updated": "2026-06-05",
|
|
5
|
+
"api": "https://push2.eastmoney.com/api/qt/clist/get?fs=b:{BK_CODE}"
|
|
6
|
+
},
|
|
7
|
+
"金融": {
|
|
8
|
+
"bk_codes": ["BK0475", "BK0449"],
|
|
9
|
+
"note": "银行+证券"
|
|
10
|
+
},
|
|
11
|
+
"消费": {
|
|
12
|
+
"bk_codes": ["BK0896", "BK0438"],
|
|
13
|
+
"note": "白酒+食品饮料"
|
|
14
|
+
},
|
|
15
|
+
"医药": {
|
|
16
|
+
"bk_codes": ["BK0465", "BK0899"],
|
|
17
|
+
"note": "医药生物+创新药"
|
|
18
|
+
},
|
|
19
|
+
"资源": {
|
|
20
|
+
"bk_codes": ["BK0519", "BK1649"],
|
|
21
|
+
"note": "稀缺资源+油气"
|
|
22
|
+
},
|
|
23
|
+
"电力": {
|
|
24
|
+
"bk_codes": ["BK1024", "BK0432"],
|
|
25
|
+
"note": "绿色电力+电力"
|
|
26
|
+
},
|
|
27
|
+
"石化": {
|
|
28
|
+
"bk_codes": ["BK0431"],
|
|
29
|
+
"note": "石油石化"
|
|
30
|
+
},
|
|
31
|
+
"高股息": {
|
|
32
|
+
"bk_codes": [],
|
|
33
|
+
"note": "跨板块主题,从其他板块筛选PE<20且ROE>8%的标的",
|
|
34
|
+
"filter": "dividend"
|
|
35
|
+
},
|
|
36
|
+
"科技": {
|
|
37
|
+
"bk_codes": ["BK0448", "BK0482"],
|
|
38
|
+
"note": "消费电子+计算机"
|
|
39
|
+
},
|
|
40
|
+
"机器人": {
|
|
41
|
+
"bk_codes": ["BK1184", "BK1090"],
|
|
42
|
+
"note": "人形机器人+机器人概念"
|
|
43
|
+
},
|
|
44
|
+
"PCB/AI算力": {
|
|
45
|
+
"bk_codes": ["BK0877", "BK1134"],
|
|
46
|
+
"note": "PCB+算力"
|
|
47
|
+
},
|
|
48
|
+
"军工": {
|
|
49
|
+
"bk_codes": ["BK0490"],
|
|
50
|
+
"note": "军工"
|
|
51
|
+
},
|
|
52
|
+
"半导体": {
|
|
53
|
+
"bk_codes": ["BK0917"],
|
|
54
|
+
"note": "半导体概念"
|
|
55
|
+
},
|
|
56
|
+
"新能源": {
|
|
57
|
+
"bk_codes": ["BK0900"],
|
|
58
|
+
"note": "新能源车"
|
|
59
|
+
},
|
|
60
|
+
"光伏": {
|
|
61
|
+
"bk_codes": ["BK0588"],
|
|
62
|
+
"note": "光伏概念"
|
|
63
|
+
}
|
|
64
|
+
}
|