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.
Files changed (154) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +21 -0
  3. package/CHANGELOG.md +93 -0
  4. package/CONTRIBUTING.md +331 -0
  5. package/README.md +259 -0
  6. package/experts/README.md +119 -0
  7. package/experts/buffett.md +91 -0
  8. package/experts/chaogu_yangjia.md +125 -0
  9. package/experts/decide.md +212 -0
  10. package/experts/duan_yongping.md +106 -0
  11. package/experts/lynch.md +127 -0
  12. package/experts/soros.md +89 -0
  13. package/experts/xu_xiang.md +107 -0
  14. package/experts/zhao_laoge.md +143 -0
  15. package/experts/zuoshou_xinyi.md +144 -0
  16. package/install-plugin.js +69 -0
  17. package/methodology.md +455 -0
  18. package/package.json +43 -0
  19. package/scripts/__pycache__/announcements.cpython-314.pyc +0 -0
  20. package/scripts/__pycache__/backtest.cpython-314.pyc +0 -0
  21. package/scripts/__pycache__/chan.cpython-314.pyc +0 -0
  22. package/scripts/__pycache__/classifier.cpython-314.pyc +0 -0
  23. package/scripts/__pycache__/common.cpython-314.pyc +0 -0
  24. package/scripts/__pycache__/finance.cpython-314.pyc +0 -0
  25. package/scripts/__pycache__/init_pool.cpython-314.pyc +0 -0
  26. package/scripts/__pycache__/kline.cpython-314.pyc +0 -0
  27. package/scripts/__pycache__/patterns_local.cpython-314.pyc +0 -0
  28. package/scripts/__pycache__/quote.cpython-314.pyc +0 -0
  29. package/scripts/__pycache__/refresh_pool.cpython-314.pyc +0 -0
  30. package/scripts/__pycache__/screener.cpython-314.pyc +0 -0
  31. package/scripts/__pycache__/technical.cpython-314.pyc +0 -0
  32. package/scripts/announcements.py +118 -0
  33. package/scripts/backtest.py +528 -0
  34. package/scripts/chan.py +591 -0
  35. package/scripts/classifier.py +302 -0
  36. package/scripts/common.py +507 -0
  37. package/scripts/data/__init__.py +208 -0
  38. package/scripts/data/__pycache__/__init__.cpython-314.pyc +0 -0
  39. package/scripts/data/__pycache__/cache.cpython-314.pyc +0 -0
  40. package/scripts/data/__pycache__/config.cpython-314.pyc +0 -0
  41. package/scripts/data/__pycache__/types.cpython-314.pyc +0 -0
  42. package/scripts/data/cache.py +99 -0
  43. package/scripts/data/config.py +49 -0
  44. package/scripts/data/industry_thresholds.json +199 -0
  45. package/scripts/data/portfolio_example.json +14 -0
  46. package/scripts/data/sector_etf.csv +14 -0
  47. package/scripts/data/sector_mapping.json +64 -0
  48. package/scripts/data/sector_stocks.json +135 -0
  49. package/scripts/data/types.py +66 -0
  50. package/scripts/fetchers/__init__.py +130 -0
  51. package/scripts/fetchers/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/scripts/fetchers/__pycache__/akshare_finance.cpython-314.pyc +0 -0
  53. package/scripts/fetchers/__pycache__/akshare_kline.cpython-314.pyc +0 -0
  54. package/scripts/fetchers/__pycache__/akshare_quote.cpython-314.pyc +0 -0
  55. package/scripts/fetchers/__pycache__/baostock_kline.cpython-314.pyc +0 -0
  56. package/scripts/fetchers/__pycache__/eastmoney_finance.cpython-314.pyc +0 -0
  57. package/scripts/fetchers/__pycache__/eastmoney_kline.cpython-314.pyc +0 -0
  58. package/scripts/fetchers/__pycache__/eastmoney_quote.cpython-314.pyc +0 -0
  59. package/scripts/fetchers/__pycache__/efinance_finance.cpython-314.pyc +0 -0
  60. package/scripts/fetchers/__pycache__/efinance_kline.cpython-314.pyc +0 -0
  61. package/scripts/fetchers/__pycache__/efinance_quote.cpython-314.pyc +0 -0
  62. package/scripts/fetchers/__pycache__/pytdx_quote.cpython-314.pyc +0 -0
  63. package/scripts/fetchers/__pycache__/sina_kline.cpython-314.pyc +0 -0
  64. package/scripts/fetchers/__pycache__/sina_quote.cpython-314.pyc +0 -0
  65. package/scripts/fetchers/__pycache__/tencent_kline.cpython-314.pyc +0 -0
  66. package/scripts/fetchers/__pycache__/tencent_quote.cpython-314.pyc +0 -0
  67. package/scripts/fetchers/__pycache__/tushare_kline.cpython-314.pyc +0 -0
  68. package/scripts/fetchers/__pycache__/tushare_quote.cpython-314.pyc +0 -0
  69. package/scripts/fetchers/__pycache__/yfinance_kline.cpython-314.pyc +0 -0
  70. package/scripts/fetchers/akshare_finance.py +35 -0
  71. package/scripts/fetchers/akshare_kline.py +59 -0
  72. package/scripts/fetchers/akshare_quote.py +52 -0
  73. package/scripts/fetchers/baostock_kline.py +64 -0
  74. package/scripts/fetchers/eastmoney_finance.py +29 -0
  75. package/scripts/fetchers/eastmoney_kline.py +48 -0
  76. package/scripts/fetchers/eastmoney_quote.py +68 -0
  77. package/scripts/fetchers/efinance_finance.py +32 -0
  78. package/scripts/fetchers/efinance_kline.py +46 -0
  79. package/scripts/fetchers/efinance_quote.py +53 -0
  80. package/scripts/fetchers/pytdx_kline.py +70 -0
  81. package/scripts/fetchers/pytdx_quote.py +78 -0
  82. package/scripts/fetchers/sina_kline.py +30 -0
  83. package/scripts/fetchers/sina_quote.py +35 -0
  84. package/scripts/fetchers/tencent_kline.py +52 -0
  85. package/scripts/fetchers/tencent_quote.py +29 -0
  86. package/scripts/fetchers/tushare_kline.py +62 -0
  87. package/scripts/fetchers/tushare_quote.py +62 -0
  88. package/scripts/fetchers/yfinance_kline.py +66 -0
  89. package/scripts/finance.py +92 -0
  90. package/scripts/init_pool.py +105 -0
  91. package/scripts/kline.py +62 -0
  92. package/scripts/monitor.py +107 -0
  93. package/scripts/patterns_local.py +599 -0
  94. package/scripts/quote.py +69 -0
  95. package/scripts/refresh_pool.py +328 -0
  96. package/scripts/screener.py +434 -0
  97. package/scripts/strategies/__init__.py +11 -0
  98. package/scripts/strategies/__pycache__/__init__.cpython-314.pyc +0 -0
  99. package/scripts/strategies/__pycache__/registry.cpython-314.pyc +0 -0
  100. package/scripts/strategies/__pycache__/thresholds.cpython-314.pyc +0 -0
  101. package/scripts/strategies/factors/__init__.py +8 -0
  102. package/scripts/strategies/factors/__pycache__/__init__.cpython-314.pyc +0 -0
  103. package/scripts/strategies/factors/__pycache__/liquidity.cpython-314.pyc +0 -0
  104. package/scripts/strategies/factors/__pycache__/momentum.cpython-314.pyc +0 -0
  105. package/scripts/strategies/factors/__pycache__/quality.cpython-314.pyc +0 -0
  106. package/scripts/strategies/factors/__pycache__/valuation.cpython-314.pyc +0 -0
  107. package/scripts/strategies/factors/__pycache__/volatility.cpython-314.pyc +0 -0
  108. package/scripts/strategies/factors/liquidity.py +49 -0
  109. package/scripts/strategies/factors/momentum.py +45 -0
  110. package/scripts/strategies/factors/quality.py +54 -0
  111. package/scripts/strategies/factors/valuation.py +76 -0
  112. package/scripts/strategies/factors/volatility.py +89 -0
  113. package/scripts/strategies/registry.py +87 -0
  114. package/scripts/strategies/thresholds.py +28 -0
  115. package/scripts/technical/__init__.py +116 -0
  116. package/scripts/technical/__pycache__/__init__.cpython-314.pyc +0 -0
  117. package/scripts/technical/__pycache__/astock.cpython-314.pyc +0 -0
  118. package/scripts/technical/__pycache__/boll.cpython-314.pyc +0 -0
  119. package/scripts/technical/__pycache__/candlestick.cpython-314.pyc +0 -0
  120. package/scripts/technical/__pycache__/core.cpython-314.pyc +0 -0
  121. package/scripts/technical/__pycache__/kdj.cpython-314.pyc +0 -0
  122. package/scripts/technical/__pycache__/macd.cpython-314.pyc +0 -0
  123. package/scripts/technical/__pycache__/moving_average.cpython-314.pyc +0 -0
  124. package/scripts/technical/__pycache__/report.cpython-314.pyc +0 -0
  125. package/scripts/technical/__pycache__/rsi.cpython-314.pyc +0 -0
  126. package/scripts/technical/__pycache__/scoring.cpython-314.pyc +0 -0
  127. package/scripts/technical/__pycache__/signals.cpython-314.pyc +0 -0
  128. package/scripts/technical/__pycache__/trend.cpython-314.pyc +0 -0
  129. package/scripts/technical/__pycache__/volume.cpython-314.pyc +0 -0
  130. package/scripts/technical/astock.py +98 -0
  131. package/scripts/technical/boll.py +49 -0
  132. package/scripts/technical/candlestick.py +151 -0
  133. package/scripts/technical/core.py +92 -0
  134. package/scripts/technical/kdj.py +68 -0
  135. package/scripts/technical/macd.py +97 -0
  136. package/scripts/technical/moving_average.py +59 -0
  137. package/scripts/technical/report.py +221 -0
  138. package/scripts/technical/rsi.py +37 -0
  139. package/scripts/technical/scoring.py +392 -0
  140. package/scripts/technical/signals.py +70 -0
  141. package/scripts/technical/trend.py +143 -0
  142. package/scripts/technical/volume.py +113 -0
  143. package/scripts/technical.py +215 -0
  144. package/skills/financial-analyst/SKILL.md +141 -0
  145. package/skills/help/SKILL.md +188 -0
  146. package/skills/init/SKILL.md +66 -0
  147. package/skills/investment-researcher/SKILL.md +152 -0
  148. package/skills/market/SKILL.md +99 -0
  149. package/skills/portfolio/SKILL.md +96 -0
  150. package/skills/screener/SKILL.md +128 -0
  151. package/skills/sector/SKILL.md +102 -0
  152. package/skills/stock/SKILL.md +148 -0
  153. package/skills/technical/SKILL.md +168 -0
  154. 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