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,68 @@
|
|
|
1
|
+
"""东方财富行情数据源。"""
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
6
|
+
|
|
7
|
+
from common import BaseFetcher, http_get, to_secid
|
|
8
|
+
|
|
9
|
+
EASTMONEY_QUOTE_URL = "https://push2.eastmoney.com/api/qt/stock/get?secid={secid}&fields=f43,f44,f45,f46,f47,f48,f50,f51,f52,f55,f57,f58,f60,f116,f117,f162,f167,f168,f169,f170"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _div100(v):
|
|
13
|
+
try:
|
|
14
|
+
return str(round(float(v) / 100, 2))
|
|
15
|
+
except (TypeError, ValueError):
|
|
16
|
+
return "0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _div10000(v):
|
|
20
|
+
try:
|
|
21
|
+
return str(round(float(v) / 100000000, 2))
|
|
22
|
+
except (TypeError, ValueError):
|
|
23
|
+
return "0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EastmoneyQuoteFetcher(BaseFetcher):
|
|
27
|
+
"""东方财富行情数据源 (优先级 8)。"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__("eastmoney_quote", priority=8)
|
|
31
|
+
|
|
32
|
+
def fetch(self, code: str, **kwargs) -> dict | None:
|
|
33
|
+
secid = to_secid(code)
|
|
34
|
+
url = EASTMONEY_QUOTE_URL.format(secid=secid)
|
|
35
|
+
raw = http_get(url)
|
|
36
|
+
try:
|
|
37
|
+
data = json.loads(raw)
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
return None
|
|
40
|
+
if not data or data.get("rc") != 0 or "data" not in data:
|
|
41
|
+
return None
|
|
42
|
+
d = data["data"]
|
|
43
|
+
if not d:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
code_str = str(d.get("f57", ""))
|
|
47
|
+
if code_str and len(code_str) < 6:
|
|
48
|
+
code_str = code_str.zfill(6)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"code": code_str,
|
|
52
|
+
"name": d.get("f58", ""),
|
|
53
|
+
"price": _div100(d.get("f43", 0)),
|
|
54
|
+
"prev_close": _div100(d.get("f60", 0)),
|
|
55
|
+
"open": _div100(d.get("f46", 0)),
|
|
56
|
+
"change_pct": _div100(d.get("f170", 0)),
|
|
57
|
+
"change_amt": _div100(d.get("f169", 0)),
|
|
58
|
+
"high": _div100(d.get("f44", 0)),
|
|
59
|
+
"low": _div100(d.get("f45", 0)),
|
|
60
|
+
"volume": str(d.get("f47", 0)),
|
|
61
|
+
"amount": str(round(float(d.get("f48", 0)) / 10000, 2)),
|
|
62
|
+
"turnover": _div100(d.get("f168", 0)),
|
|
63
|
+
"pe": _div100(d.get("f162", 0)),
|
|
64
|
+
"pb": _div100(d.get("f167", 0)),
|
|
65
|
+
"total_cap": _div10000(d.get("f116", 0)),
|
|
66
|
+
"circulating_cap": _div10000(d.get("f117", 0)),
|
|
67
|
+
"source": "eastmoney",
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""efinance 财务数据源(需要 efinance 包)。"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
5
|
+
|
|
6
|
+
from common import BaseFetcher
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import efinance as ef
|
|
10
|
+
HAS_EFINANCE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_EFINANCE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EfinanceFinanceFetcher(BaseFetcher):
|
|
16
|
+
"""efinance 财务数据源 (优先级 5) - 需要安装 efinance 包。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__("efinance_finance", priority=5)
|
|
20
|
+
|
|
21
|
+
def fetch(self, code: str, **kwargs) -> list | None:
|
|
22
|
+
if not HAS_EFINANCE:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
plain = code.lstrip("shszSHSZbjBJ")
|
|
26
|
+
df = ef.stock.get_base_info(plain)
|
|
27
|
+
if df is None or df.empty:
|
|
28
|
+
return None
|
|
29
|
+
# 返回最近 4 季数据
|
|
30
|
+
return [df.to_dict()]
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""efinance K 线数据源(需要 efinance 包)。"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
5
|
+
|
|
6
|
+
from common import BaseFetcher
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import efinance as ef
|
|
10
|
+
HAS_EFINANCE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_EFINANCE = False
|
|
13
|
+
|
|
14
|
+
KLT_MAP = {5: 5, 15: 15, 30: 30, 60: 60, 240: 101}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EfinanceKlineFetcher(BaseFetcher):
|
|
18
|
+
"""efinance K 线数据源 (优先级 0) - 需要安装 efinance 包。"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__("efinance_kline", priority=0)
|
|
22
|
+
|
|
23
|
+
def fetch(self, code: str, **kwargs) -> list | None:
|
|
24
|
+
if not HAS_EFINANCE:
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
scale = kwargs.get("scale", 240)
|
|
28
|
+
datalen = kwargs.get("datalen", 30)
|
|
29
|
+
plain = code.lstrip("shszSHSZbjBJ")
|
|
30
|
+
klt = KLT_MAP.get(scale, 101)
|
|
31
|
+
df = ef.stock.get_quote_history(plain, klt=klt, count=datalen)
|
|
32
|
+
if df is None or df.empty:
|
|
33
|
+
return None
|
|
34
|
+
result = []
|
|
35
|
+
for _, row in df.iterrows():
|
|
36
|
+
result.append({
|
|
37
|
+
"day": str(row.get("日期", ""))[:10],
|
|
38
|
+
"open": str(row.get("开盘", 0)),
|
|
39
|
+
"close": str(row.get("收盘", 0)),
|
|
40
|
+
"high": str(row.get("最高", 0)),
|
|
41
|
+
"low": str(row.get("最低", 0)),
|
|
42
|
+
"volume": str(row.get("成交量", 0)),
|
|
43
|
+
})
|
|
44
|
+
return result if result else None
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""efinance 行情数据源(需要 efinance 包)。"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
5
|
+
|
|
6
|
+
from common import BaseFetcher
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import efinance as ef
|
|
10
|
+
HAS_EFINANCE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_EFINANCE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EfinanceQuoteFetcher(BaseFetcher):
|
|
16
|
+
"""efinance 行情数据源 (优先级 0) - 需要安装 efinance 包。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__("efinance_quote", priority=0)
|
|
20
|
+
|
|
21
|
+
def fetch(self, code: str, **kwargs) -> dict | None:
|
|
22
|
+
if not HAS_EFINANCE:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
# efinance 接受纯代码如 "600989"
|
|
26
|
+
plain = code.lstrip("shszSHSZbjBJ")
|
|
27
|
+
df = ef.stock.get_realtime_quotes()
|
|
28
|
+
if df is None or df.empty:
|
|
29
|
+
return None
|
|
30
|
+
row = df[df["股票代码"] == plain]
|
|
31
|
+
if row.empty:
|
|
32
|
+
return None
|
|
33
|
+
r = row.iloc[0]
|
|
34
|
+
return {
|
|
35
|
+
"code": str(r.get("股票代码", "")),
|
|
36
|
+
"name": str(r.get("股票名称", "")),
|
|
37
|
+
"price": str(r.get("最新价", 0)),
|
|
38
|
+
"prev_close": str(r.get("昨收", 0)),
|
|
39
|
+
"open": str(r.get("今开", 0)),
|
|
40
|
+
"change_pct": str(r.get("涨跌幅", 0)),
|
|
41
|
+
"change_amt": str(r.get("涨跌额", 0)),
|
|
42
|
+
"high": str(r.get("最高", 0)),
|
|
43
|
+
"low": str(r.get("最低", 0)),
|
|
44
|
+
"volume": str(r.get("成交量", 0)),
|
|
45
|
+
"amount": str(r.get("成交额", 0)),
|
|
46
|
+
"turnover": str(r.get("换手率", 0)),
|
|
47
|
+
"pe": str(r.get("市盈率-动态", 0)),
|
|
48
|
+
"pb": str(r.get("市净率", 0)),
|
|
49
|
+
"total_cap": str(r.get("总市值", 0)),
|
|
50
|
+
"circulating_cap": str(r.get("流通市值", 0)),
|
|
51
|
+
}
|
|
52
|
+
except Exception:
|
|
53
|
+
return None
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""通达信 K 线数据源(需要 pytdx 包)。"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
5
|
+
|
|
6
|
+
from common import BaseFetcher
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from pytdx.hq import TdxHq_API
|
|
10
|
+
HAS_PYTDX = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_PYTDX = False
|
|
13
|
+
|
|
14
|
+
DEFAULT_SERVERS = [
|
|
15
|
+
("119.147.212.81", 7709),
|
|
16
|
+
("112.74.214.43", 7709),
|
|
17
|
+
("221.231.141.60", 7709),
|
|
18
|
+
("101.227.73.20", 7709),
|
|
19
|
+
("101.227.77.254", 7709),
|
|
20
|
+
("14.215.128.18", 7709),
|
|
21
|
+
("59.173.18.140", 7709),
|
|
22
|
+
("218.75.126.9", 7709),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
CATEGORY_MAP = {5: 0, 15: 1, 30: 2, 60: 3, 240: 9}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_market(code: str) -> int:
|
|
29
|
+
plain = code.lstrip("shszSHSZbjBJ").zfill(6)
|
|
30
|
+
if plain.startswith(("60", "68", "51", "56", "58")):
|
|
31
|
+
return 1
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PytdxKlineFetcher(BaseFetcher):
|
|
36
|
+
"""通达信 K 线数据源 (优先级 2) - 需要安装 pytdx 包。"""
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
super().__init__("pytdx_kline", priority=2)
|
|
40
|
+
|
|
41
|
+
def fetch(self, code: str, **kwargs) -> list | None:
|
|
42
|
+
if not HAS_PYTDX:
|
|
43
|
+
return None
|
|
44
|
+
scale = kwargs.get("scale", 240)
|
|
45
|
+
datalen = kwargs.get("datalen", 30)
|
|
46
|
+
plain = code.lstrip("shszSHSZbjBJ").zfill(6)
|
|
47
|
+
market = _get_market(code)
|
|
48
|
+
category = CATEGORY_MAP.get(scale, 9)
|
|
49
|
+
|
|
50
|
+
api = TdxHq_API()
|
|
51
|
+
for host, port in DEFAULT_SERVERS:
|
|
52
|
+
try:
|
|
53
|
+
with api.connect(host, port, time_out=5):
|
|
54
|
+
data = api.get_security_bars(category, market, plain, 0, datalen)
|
|
55
|
+
if not data:
|
|
56
|
+
continue
|
|
57
|
+
result = []
|
|
58
|
+
for d in data:
|
|
59
|
+
result.append({
|
|
60
|
+
"day": str(d.get("datetime", ""))[:10],
|
|
61
|
+
"open": str(d.get("open", 0)),
|
|
62
|
+
"close": str(d.get("close", 0)),
|
|
63
|
+
"high": str(d.get("high", 0)),
|
|
64
|
+
"low": str(d.get("low", 0)),
|
|
65
|
+
"volume": str(d.get("vol", 0)),
|
|
66
|
+
})
|
|
67
|
+
return result if result else None
|
|
68
|
+
except Exception:
|
|
69
|
+
continue
|
|
70
|
+
return None
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""通达信行情数据源(需要 pytdx 包)。"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
5
|
+
|
|
6
|
+
from common import BaseFetcher
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from pytdx.hq import TdxHq_API
|
|
10
|
+
HAS_PYTDX = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_PYTDX = False
|
|
13
|
+
|
|
14
|
+
# 默认服务器列表
|
|
15
|
+
DEFAULT_SERVERS = [
|
|
16
|
+
("119.147.212.81", 7709),
|
|
17
|
+
("112.74.214.43", 7709),
|
|
18
|
+
("221.231.141.60", 7709),
|
|
19
|
+
("101.227.73.20", 7709),
|
|
20
|
+
("101.227.77.254", 7709),
|
|
21
|
+
("14.215.128.18", 7709),
|
|
22
|
+
("59.173.18.140", 7709),
|
|
23
|
+
("218.75.126.9", 7709),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_market(code: str) -> int:
|
|
28
|
+
"""0=深圳, 1=上海。"""
|
|
29
|
+
plain = code.lstrip("shszSHSZbjBJ").zfill(6)
|
|
30
|
+
if plain.startswith(("60", "68", "51", "56", "58")):
|
|
31
|
+
return 1
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PytdxQuoteFetcher(BaseFetcher):
|
|
36
|
+
"""通达信行情数据源 (优先级 2) - 需要安装 pytdx 包。"""
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
super().__init__("pytdx_quote", priority=2)
|
|
40
|
+
|
|
41
|
+
def fetch(self, code: str, **kwargs) -> dict | None:
|
|
42
|
+
if not HAS_PYTDX:
|
|
43
|
+
return None
|
|
44
|
+
api = TdxHq_API()
|
|
45
|
+
plain = code.lstrip("shszSHSZbjBJ").zfill(6)
|
|
46
|
+
market = _get_market(code)
|
|
47
|
+
|
|
48
|
+
for host, port in DEFAULT_SERVERS:
|
|
49
|
+
try:
|
|
50
|
+
with api.connect(host, port, time_out=5):
|
|
51
|
+
data = api.get_security_quotes([(market, plain)])
|
|
52
|
+
if not data:
|
|
53
|
+
continue
|
|
54
|
+
d = data[0]
|
|
55
|
+
price = d.get("price", 0)
|
|
56
|
+
prev_close = d.get("last_close", 0)
|
|
57
|
+
change_pct = round((price / prev_close - 1) * 100, 2) if prev_close > 0 else 0
|
|
58
|
+
return {
|
|
59
|
+
"code": plain,
|
|
60
|
+
"name": d.get("name", ""),
|
|
61
|
+
"price": str(price),
|
|
62
|
+
"prev_close": str(prev_close),
|
|
63
|
+
"open": str(d.get("open", 0)),
|
|
64
|
+
"change_pct": str(change_pct),
|
|
65
|
+
"change_amt": str(round(price - prev_close, 2)),
|
|
66
|
+
"high": str(d.get("high", 0)),
|
|
67
|
+
"low": str(d.get("low", 0)),
|
|
68
|
+
"volume": str(d.get("vol", 0)),
|
|
69
|
+
"amount": str(d.get("amount", 0)),
|
|
70
|
+
"turnover": "",
|
|
71
|
+
"pe": "",
|
|
72
|
+
"pb": "",
|
|
73
|
+
"total_cap": "",
|
|
74
|
+
"circulating_cap": "",
|
|
75
|
+
}
|
|
76
|
+
except Exception:
|
|
77
|
+
continue
|
|
78
|
+
return None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""新浪 K 线数据源。"""
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
6
|
+
|
|
7
|
+
from common import BaseFetcher, http_get
|
|
8
|
+
|
|
9
|
+
SINA_URL = "https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol={symbol}&scale={scale}&ma=no&datalen={datalen}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SinaKlineFetcher(BaseFetcher):
|
|
13
|
+
"""新浪 K 线数据源 (优先级 10)。"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__("sina_kline", priority=10)
|
|
17
|
+
|
|
18
|
+
def fetch(self, code: str, **kwargs) -> list | None:
|
|
19
|
+
scale = kwargs.get("scale", 240)
|
|
20
|
+
datalen = kwargs.get("datalen", 30)
|
|
21
|
+
raw = http_get(SINA_URL.format(symbol=code, scale=scale, datalen=datalen))
|
|
22
|
+
try:
|
|
23
|
+
records = json.loads(raw)
|
|
24
|
+
if records:
|
|
25
|
+
for r in records:
|
|
26
|
+
r["source"] = "sina"
|
|
27
|
+
return records
|
|
28
|
+
return None
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""新浪行情数据源。"""
|
|
2
|
+
import sys
|
|
3
|
+
import urllib.request
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
6
|
+
|
|
7
|
+
from common import BaseFetcher, parse_sina_quote_line
|
|
8
|
+
|
|
9
|
+
SINA_URL = "https://hq.sinajs.cn/list={codes}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SinaQuoteFetcher(BaseFetcher):
|
|
13
|
+
"""新浪行情数据源 (优先级 5)。"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__("sina_quote", priority=5)
|
|
17
|
+
|
|
18
|
+
def fetch(self, code: str, **kwargs) -> dict | None:
|
|
19
|
+
url = SINA_URL.format(codes=code)
|
|
20
|
+
req = urllib.request.Request(url, headers={
|
|
21
|
+
"Referer": "https://finance.sina.com.cn",
|
|
22
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
23
|
+
})
|
|
24
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
25
|
+
raw = resp.read()
|
|
26
|
+
text = raw.decode("gbk", errors="replace")
|
|
27
|
+
for line in text.strip().split("\n"):
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if not line:
|
|
30
|
+
continue
|
|
31
|
+
rec = parse_sina_quote_line(line)
|
|
32
|
+
if rec:
|
|
33
|
+
rec["source"] = "sina"
|
|
34
|
+
return rec
|
|
35
|
+
return None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""腾讯 K 线数据源。"""
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
6
|
+
|
|
7
|
+
from common import BaseFetcher, http_get
|
|
8
|
+
|
|
9
|
+
TENCENT_URL = "http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={stockCode},{period},,,{count},qfq"
|
|
10
|
+
SCALE_MAP = {5: "m5", 15: "m15", 30: "m30", 60: "m60", 240: "day"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TencentKlineFetcher(BaseFetcher):
|
|
14
|
+
"""腾讯 K 线数据源 (优先级 5)。"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__("tencent_kline", priority=5)
|
|
18
|
+
|
|
19
|
+
def fetch(self, code: str, **kwargs) -> list | None:
|
|
20
|
+
scale = kwargs.get("scale", 240)
|
|
21
|
+
datalen = kwargs.get("datalen", 30)
|
|
22
|
+
period = SCALE_MAP.get(scale, "day")
|
|
23
|
+
url = TENCENT_URL.format(stockCode=code, period=period, count=datalen)
|
|
24
|
+
raw = http_get(url)
|
|
25
|
+
try:
|
|
26
|
+
resp = json.loads(raw)
|
|
27
|
+
except json.JSONDecodeError:
|
|
28
|
+
return None
|
|
29
|
+
if resp.get("code") != 0 or "data" not in resp:
|
|
30
|
+
return None
|
|
31
|
+
stock_data = resp["data"].get(code, {})
|
|
32
|
+
key_candidates = [f"qfq{period}", period]
|
|
33
|
+
records = []
|
|
34
|
+
for key in key_candidates:
|
|
35
|
+
if key in stock_data:
|
|
36
|
+
records = stock_data[key]
|
|
37
|
+
break
|
|
38
|
+
if not records:
|
|
39
|
+
return None
|
|
40
|
+
result = []
|
|
41
|
+
for row in records:
|
|
42
|
+
if len(row) >= 6:
|
|
43
|
+
result.append({
|
|
44
|
+
"day": row[0],
|
|
45
|
+
"open": row[1],
|
|
46
|
+
"high": row[3],
|
|
47
|
+
"low": row[4],
|
|
48
|
+
"close": row[2],
|
|
49
|
+
"volume": row[5],
|
|
50
|
+
"source": "tencent",
|
|
51
|
+
})
|
|
52
|
+
return result if result else None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""腾讯行情数据源。"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
5
|
+
|
|
6
|
+
from common import BaseFetcher, http_get, decode_gbk, parse_tencent_line
|
|
7
|
+
|
|
8
|
+
TENCENT_URL = "https://qt.gtimg.cn/q={codes}"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TencentQuoteFetcher(BaseFetcher):
|
|
12
|
+
"""腾讯行情数据源 (优先级 10)。"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
super().__init__("tencent_quote", priority=10)
|
|
16
|
+
|
|
17
|
+
def fetch(self, code: str, **kwargs) -> dict | None:
|
|
18
|
+
url = TENCENT_URL.format(codes=code)
|
|
19
|
+
raw = http_get(url)
|
|
20
|
+
text = decode_gbk(raw)
|
|
21
|
+
for line in text.strip().split(";"):
|
|
22
|
+
line = line.strip()
|
|
23
|
+
if not line:
|
|
24
|
+
continue
|
|
25
|
+
rec = parse_tencent_line(line)
|
|
26
|
+
if rec:
|
|
27
|
+
rec["source"] = "tencent"
|
|
28
|
+
return rec
|
|
29
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tushare K 线数据源(需要 tushare 包 + token)。"""
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
6
|
+
|
|
7
|
+
from common import BaseFetcher
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import tushare as ts
|
|
11
|
+
token = os.environ.get("TUSHARE_TOKEN", "")
|
|
12
|
+
if token:
|
|
13
|
+
ts.set_token(token)
|
|
14
|
+
HAS_TUSHARE = True
|
|
15
|
+
else:
|
|
16
|
+
HAS_TUSHARE = False
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_TUSHARE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TushareKlineFetcher(BaseFetcher):
|
|
22
|
+
"""Tushare K 线数据源 (优先级 2) - 需要安装 tushare 包并设置 TUSHARE_TOKEN。"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__("tushare_kline", priority=2)
|
|
26
|
+
|
|
27
|
+
def fetch(self, code: str, **kwargs) -> list | None:
|
|
28
|
+
if not HAS_TUSHARE:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
scale = kwargs.get("scale", 240)
|
|
32
|
+
datalen = kwargs.get("datalen", 30)
|
|
33
|
+
plain = code.lstrip("shszSHSZbjBJ")
|
|
34
|
+
if code.startswith(("sh", "SH")):
|
|
35
|
+
ts_code = f"{plain}.SH"
|
|
36
|
+
else:
|
|
37
|
+
ts_code = f"{plain}.SZ"
|
|
38
|
+
|
|
39
|
+
pro = ts.pro_api()
|
|
40
|
+
if scale == 240:
|
|
41
|
+
df = pro.daily(ts_code=ts_code, limit=datalen)
|
|
42
|
+
else:
|
|
43
|
+
# 分钟线需要额外权限
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if df is None or df.empty:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
result = []
|
|
50
|
+
for _, row in df.iterrows():
|
|
51
|
+
result.append({
|
|
52
|
+
"day": str(row.get("trade_date", "")),
|
|
53
|
+
"open": str(row.get("open", 0)),
|
|
54
|
+
"close": str(row.get("close", 0)),
|
|
55
|
+
"high": str(row.get("high", 0)),
|
|
56
|
+
"low": str(row.get("low", 0)),
|
|
57
|
+
"volume": str(row.get("vol", 0)),
|
|
58
|
+
})
|
|
59
|
+
result.reverse() # tushare 返回倒序
|
|
60
|
+
return result if result else None
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tushare 行情数据源(需要 tushare 包 + token)。"""
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
6
|
+
|
|
7
|
+
from common import BaseFetcher
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import tushare as ts
|
|
11
|
+
token = os.environ.get("TUSHARE_TOKEN", "")
|
|
12
|
+
if token:
|
|
13
|
+
ts.set_token(token)
|
|
14
|
+
HAS_TUSHARE = True
|
|
15
|
+
else:
|
|
16
|
+
HAS_TUSHARE = False
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_TUSHARE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TushareQuoteFetcher(BaseFetcher):
|
|
22
|
+
"""Tushare 行情数据源 (优先级 -1) - 需要安装 tushare 包并设置 TUSHARE_TOKEN。"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__("tushare_quote", priority=-1 if HAS_TUSHARE else 2)
|
|
26
|
+
|
|
27
|
+
def fetch(self, code: str, **kwargs) -> dict | None:
|
|
28
|
+
if not HAS_TUSHARE:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
plain = code.lstrip("shszSHSZbjBJ")
|
|
32
|
+
# 转换为 tushare 格式: 600989.SH
|
|
33
|
+
if code.startswith(("sh", "SH")):
|
|
34
|
+
ts_code = f"{plain}.SH"
|
|
35
|
+
else:
|
|
36
|
+
ts_code = f"{plain}.SZ"
|
|
37
|
+
|
|
38
|
+
pro = ts.pro_api()
|
|
39
|
+
df = pro.daily(ts_code=ts_code, limit=1)
|
|
40
|
+
if df is None or df.empty:
|
|
41
|
+
return None
|
|
42
|
+
r = df.iloc[0]
|
|
43
|
+
return {
|
|
44
|
+
"code": plain,
|
|
45
|
+
"name": "",
|
|
46
|
+
"price": str(r.get("close", 0)),
|
|
47
|
+
"prev_close": str(r.get("pre_close", 0)),
|
|
48
|
+
"open": str(r.get("open", 0)),
|
|
49
|
+
"change_pct": str(r.get("pct_chg", 0)),
|
|
50
|
+
"change_amt": str(r.get("change", 0)),
|
|
51
|
+
"high": str(r.get("high", 0)),
|
|
52
|
+
"low": str(r.get("low", 0)),
|
|
53
|
+
"volume": str(r.get("vol", 0)),
|
|
54
|
+
"amount": str(r.get("amount", 0)),
|
|
55
|
+
"turnover": "",
|
|
56
|
+
"pe": "",
|
|
57
|
+
"pb": "",
|
|
58
|
+
"total_cap": "",
|
|
59
|
+
"circulating_cap": "",
|
|
60
|
+
}
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|