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,135 @@
1
+ {
2
+ "_meta": {
3
+ "updated": "2026-06-05T16:07:35",
4
+ "source": "eastmoney_push2",
5
+ "total_sectors": 14,
6
+ "total_stocks": 99
7
+ },
8
+ "金融": [
9
+ "sh601398",
10
+ "sh601288",
11
+ "sh601939",
12
+ "sh601988",
13
+ "sh600036",
14
+ "sh601166",
15
+ "sh601328",
16
+ "sh601318",
17
+ "sh600016",
18
+ "sh600000",
19
+ "sh601169",
20
+ "sh601688",
21
+ "sh600030",
22
+ "sh601881",
23
+ "sh601601"
24
+ ],
25
+ "消费": [
26
+ "sh600519",
27
+ "sz000858",
28
+ "sh600809",
29
+ "sz000568",
30
+ "sz002304",
31
+ "sh600887",
32
+ "sz002714",
33
+ "sh603288",
34
+ "sz000661"
35
+ ],
36
+ "医药": [
37
+ "sz300760",
38
+ "sh600436",
39
+ "sz300015",
40
+ "sh600276",
41
+ "sh603259",
42
+ "sz300122",
43
+ "sz000538",
44
+ "sh600196",
45
+ "sz002007",
46
+ "sh688185"
47
+ ],
48
+ "资源": [
49
+ "sz000807",
50
+ "sh603993",
51
+ "sh603799",
52
+ "sh600989",
53
+ "sh601088",
54
+ "sh601899",
55
+ "sh600489"
56
+ ],
57
+ "电力": [
58
+ "sh600900",
59
+ "sh600025",
60
+ "sh600886",
61
+ "sh601985",
62
+ "sh600795",
63
+ "sh600011",
64
+ "sh600027"
65
+ ],
66
+ "石化": [
67
+ "sh600028",
68
+ "sh601857",
69
+ "sh600346"
70
+ ],
71
+ "高股息": [
72
+ "sh601398",
73
+ "sh600900",
74
+ "sh600028",
75
+ "sh601857",
76
+ "sh601088",
77
+ "sh601328",
78
+ "sz000807",
79
+ "sh600886"
80
+ ],
81
+ "科技": [
82
+ "sz002415",
83
+ "sh603986",
84
+ "sh688981",
85
+ "sh600588",
86
+ "sz300496",
87
+ "sz002236"
88
+ ],
89
+ "机器人": [
90
+ "sh688017",
91
+ "sz300124",
92
+ "sz002747",
93
+ "sz300503",
94
+ "sz002472",
95
+ "sh688698",
96
+ "sz002527",
97
+ "sz300024"
98
+ ],
99
+ "PCB/AI算力": [
100
+ "sz300476",
101
+ "sz002463",
102
+ "sz002916",
103
+ "sh603228",
104
+ "sz002436"
105
+ ],
106
+ "军工": [
107
+ "sh600760",
108
+ "sz000768",
109
+ "sh600893",
110
+ "sh600150",
111
+ "sh601989",
112
+ "sh600862"
113
+ ],
114
+ "半导体": [
115
+ "sz002371",
116
+ "sh603501",
117
+ "sh688012",
118
+ "sz300661",
119
+ "sh688008",
120
+ "sz002049"
121
+ ],
122
+ "新能源": [
123
+ "sz300750",
124
+ "sz300014",
125
+ "sz300438",
126
+ "sz300274",
127
+ "sz002459"
128
+ ],
129
+ "光伏": [
130
+ "sh601012",
131
+ "sh600438",
132
+ "sz002459",
133
+ "sh688599"
134
+ ]
135
+ }
@@ -0,0 +1,66 @@
1
+ """统一数据类型定义。"""
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class Quote:
7
+ """统一行情数据结构。"""
8
+ code: str = ""
9
+ name: str = ""
10
+ price: float = 0.0
11
+ prev_close: float = 0.0
12
+ open: float = 0.0
13
+ high: float = 0.0
14
+ low: float = 0.0
15
+ change_pct: float = 0.0
16
+ change_amt: float = 0.0
17
+ volume: int = 0 # 手(腾讯源)或 股(新浪源),data 层不归一化
18
+ amount: float = 0.0 # 万元
19
+ turnover: float = 0.0 # %
20
+ pe: float = 0.0
21
+ pb: float = 0.0
22
+ total_cap: float = 0.0 # 亿
23
+ circulating_cap: float = 0.0
24
+ source: str = ""
25
+
26
+ def has_basic_data(self) -> bool:
27
+ return self.price > 0
28
+
29
+ def to_dict(self) -> dict:
30
+ return self.__dict__.copy()
31
+
32
+
33
+ @dataclass
34
+ class KlineBar:
35
+ """统一 K 线数据结构。"""
36
+ day: str = ""
37
+ open: float = 0.0
38
+ high: float = 0.0
39
+ low: float = 0.0
40
+ close: float = 0.0
41
+ volume: int = 0 # 手
42
+ amount: float = 0.0
43
+ pct_chg: float = 0.0
44
+ source: str = ""
45
+
46
+ def to_dict(self) -> dict:
47
+ return self.__dict__.copy()
48
+
49
+
50
+ @dataclass
51
+ class FinanceRecord:
52
+ """统一财务数据结构。"""
53
+ report_date: str = ""
54
+ eps: float = 0.0 # 每股收益
55
+ roe: float = 0.0 # ROE(%)
56
+ revenue_yoy: float = 0.0 # 营收同比(%)
57
+ net_profit_yoy: float = 0.0 # 净利同比(%)
58
+ gross_margin: float = 0.0 # 毛利率(%)
59
+ net_margin: float = 0.0 # 净利率(%)
60
+ debt_ratio: float = 0.0 # 负债率(%)
61
+ bps: float = 0.0 # 每股净资产
62
+ ocf_per_share: float = 0.0 # 每股经营现金流
63
+ source: str = ""
64
+
65
+ def to_dict(self) -> dict:
66
+ return self.__dict__.copy()
@@ -0,0 +1,130 @@
1
+ """
2
+ 数据源集合:自动发现并加载可用的数据源。
3
+ 依赖包未安装时自动跳过对应数据源。
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ # 添加 scripts 目录到 path
9
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
10
+
11
+ from common import BaseFetcher, DataFetcherManager
12
+
13
+
14
+ def _try_import(module_name, class_name):
15
+ """尝试导入模块,失败返回 None。"""
16
+ try:
17
+ import importlib
18
+ mod = importlib.import_module(f".{module_name}", package=__name__)
19
+ return getattr(mod, class_name, None)
20
+ except Exception:
21
+ return None
22
+
23
+
24
+ def get_quote_fetchers() -> list:
25
+ """获取所有可用的行情数据源。"""
26
+ fetchers = []
27
+
28
+ # 直接 HTTP 数据源(无依赖)
29
+ from .tencent_quote import TencentQuoteFetcher
30
+ from .eastmoney_quote import EastmoneyQuoteFetcher
31
+ from .sina_quote import SinaQuoteFetcher
32
+ fetchers.extend([TencentQuoteFetcher(), EastmoneyQuoteFetcher(), SinaQuoteFetcher()])
33
+
34
+ # 可选依赖数据源
35
+ cls = _try_import("efinance_quote", "EfinanceQuoteFetcher")
36
+ if cls:
37
+ fetchers.append(cls())
38
+
39
+ cls = _try_import("akshare_quote", "AkshareQuoteFetcher")
40
+ if cls:
41
+ fetchers.append(cls())
42
+
43
+ cls = _try_import("tushare_quote", "TushareQuoteFetcher")
44
+ if cls:
45
+ fetchers.append(cls())
46
+
47
+ cls = _try_import("pytdx_quote", "PytdxQuoteFetcher")
48
+ if cls:
49
+ fetchers.append(cls())
50
+
51
+ return fetchers
52
+
53
+
54
+ def get_kline_fetchers() -> list:
55
+ """获取所有可用的 K 线数据源。"""
56
+ fetchers = []
57
+
58
+ # 直接 HTTP 数据源(无依赖)
59
+ from .sina_kline import SinaKlineFetcher
60
+ from .eastmoney_kline import EastmoneyKlineFetcher
61
+ from .tencent_kline import TencentKlineFetcher
62
+ fetchers.extend([SinaKlineFetcher(), EastmoneyKlineFetcher(), TencentKlineFetcher()])
63
+
64
+ # 可选依赖数据源
65
+ cls = _try_import("efinance_kline", "EfinanceKlineFetcher")
66
+ if cls:
67
+ fetchers.append(cls())
68
+
69
+ cls = _try_import("akshare_kline", "AkshareKlineFetcher")
70
+ if cls:
71
+ fetchers.append(cls())
72
+
73
+ cls = _try_import("tushare_kline", "TushareKlineFetcher")
74
+ if cls:
75
+ fetchers.append(cls())
76
+
77
+ cls = _try_import("baostock_kline", "BaostockKlineFetcher")
78
+ if cls:
79
+ fetchers.append(cls())
80
+
81
+ cls = _try_import("yfinance_kline", "YfinanceKlineFetcher")
82
+ if cls:
83
+ fetchers.append(cls())
84
+
85
+ return fetchers
86
+
87
+
88
+ def get_finance_fetchers() -> list:
89
+ """获取所有可用的财务数据源。"""
90
+ fetchers = []
91
+
92
+ from .eastmoney_finance import EastmoneyFinanceFetcher
93
+ fetchers.append(EastmoneyFinanceFetcher())
94
+
95
+ cls = _try_import("efinance_finance", "EfinanceFinanceFetcher")
96
+ if cls:
97
+ fetchers.append(cls())
98
+
99
+ cls = _try_import("akshare_finance", "AkshareFinanceFetcher")
100
+ if cls:
101
+ fetchers.append(cls())
102
+
103
+ return fetchers
104
+
105
+
106
+ # 全局管理器(延迟初始化)
107
+ _quote_manager = None
108
+ _kline_manager = None
109
+ _finance_manager = None
110
+
111
+
112
+ def get_quote_manager() -> DataFetcherManager:
113
+ global _quote_manager
114
+ if _quote_manager is None:
115
+ _quote_manager = DataFetcherManager(get_quote_fetchers())
116
+ return _quote_manager
117
+
118
+
119
+ def get_kline_manager() -> DataFetcherManager:
120
+ global _kline_manager
121
+ if _kline_manager is None:
122
+ _kline_manager = DataFetcherManager(get_kline_fetchers())
123
+ return _kline_manager
124
+
125
+
126
+ def get_finance_manager() -> DataFetcherManager:
127
+ global _finance_manager
128
+ if _finance_manager is None:
129
+ _finance_manager = DataFetcherManager(get_finance_fetchers())
130
+ return _finance_manager
@@ -0,0 +1,35 @@
1
+ """akshare 财务数据源(需要 akshare 包)。"""
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 akshare as ak
10
+ HAS_AKSHARE = True
11
+ except ImportError:
12
+ HAS_AKSHARE = False
13
+
14
+
15
+ class AkshareFinanceFetcher(BaseFetcher):
16
+ """akshare 财务数据源 (优先级 3) - 需要安装 akshare 包。"""
17
+
18
+ def __init__(self):
19
+ super().__init__("akshare_finance", priority=3)
20
+
21
+ def fetch(self, code: str, **kwargs) -> list | None:
22
+ if not HAS_AKSHARE:
23
+ return None
24
+ try:
25
+ plain = code.lstrip("shszSHSZbjBJ")
26
+ df = ak.stock_financial_abstract(symbol=plain)
27
+ if df is None or df.empty:
28
+ return None
29
+ # 返回最近 4 季
30
+ result = []
31
+ for _, row in df.head(4).iterrows():
32
+ result.append(row.to_dict())
33
+ return result if result else None
34
+ except Exception:
35
+ return None
@@ -0,0 +1,59 @@
1
+ """akshare K 线数据源(需要 akshare 包)。"""
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 akshare as ak
10
+ HAS_AKSHARE = True
11
+ except ImportError:
12
+ HAS_AKSHARE = False
13
+
14
+
15
+ class AkshareKlineFetcher(BaseFetcher):
16
+ """akshare K 线数据源 (优先级 1) - 需要安装 akshare 包。"""
17
+
18
+ def __init__(self):
19
+ super().__init__("akshare_kline", priority=1)
20
+
21
+ def fetch(self, code: str, **kwargs) -> list | None:
22
+ if not HAS_AKSHARE:
23
+ return None
24
+ try:
25
+ scale = kwargs.get("scale", 240)
26
+ datalen = kwargs.get("datalen", 30)
27
+ plain = code.lstrip("shszSHSZbjBJ")
28
+
29
+ if scale == 240:
30
+ # 日 K
31
+ df = ak.stock_zh_a_hist(symbol=plain, period="daily", adjust="qfq")
32
+ elif scale == 60:
33
+ df = ak.stock_zh_a_hist(symbol=plain, period="60", adjust="qfq")
34
+ elif scale == 30:
35
+ df = ak.stock_zh_a_hist(symbol=plain, period="30", adjust="qfq")
36
+ elif scale == 15:
37
+ df = ak.stock_zh_a_hist(symbol=plain, period="15", adjust="qfq")
38
+ elif scale == 5:
39
+ df = ak.stock_zh_a_hist(symbol=plain, period="5", adjust="qfq")
40
+ else:
41
+ df = ak.stock_zh_a_hist(symbol=plain, period="daily", adjust="qfq")
42
+
43
+ if df is None or df.empty:
44
+ return None
45
+
46
+ df = df.tail(datalen)
47
+ result = []
48
+ for _, row in df.iterrows():
49
+ result.append({
50
+ "day": str(row.get("日期", ""))[:10],
51
+ "open": str(row.get("开盘", 0)),
52
+ "close": str(row.get("收盘", 0)),
53
+ "high": str(row.get("最高", 0)),
54
+ "low": str(row.get("最低", 0)),
55
+ "volume": str(row.get("成交量", 0)),
56
+ })
57
+ return result if result else None
58
+ except Exception:
59
+ return None
@@ -0,0 +1,52 @@
1
+ """akshare 行情数据源(需要 akshare 包)。"""
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 akshare as ak
10
+ HAS_AKSHARE = True
11
+ except ImportError:
12
+ HAS_AKSHARE = False
13
+
14
+
15
+ class AkshareQuoteFetcher(BaseFetcher):
16
+ """akshare 行情数据源 (优先级 1) - 需要安装 akshare 包。"""
17
+
18
+ def __init__(self):
19
+ super().__init__("akshare_quote", priority=1)
20
+
21
+ def fetch(self, code: str, **kwargs) -> dict | None:
22
+ if not HAS_AKSHARE:
23
+ return None
24
+ try:
25
+ plain = code.lstrip("shszSHSZbjBJ")
26
+ df = ak.stock_zh_a_spot_em()
27
+ if df is None or df.empty:
28
+ return None
29
+ row = df[df["代码"] == plain]
30
+ if row.empty:
31
+ return None
32
+ r = row.iloc[0]
33
+ return {
34
+ "code": str(r.get("代码", "")),
35
+ "name": str(r.get("名称", "")),
36
+ "price": str(r.get("最新价", 0)),
37
+ "prev_close": str(r.get("昨收", 0)),
38
+ "open": str(r.get("今开", 0)),
39
+ "change_pct": str(r.get("涨跌幅", 0)),
40
+ "change_amt": str(r.get("涨跌额", 0)),
41
+ "high": str(r.get("最高", 0)),
42
+ "low": str(r.get("最低", 0)),
43
+ "volume": str(r.get("成交量", 0)),
44
+ "amount": str(r.get("成交额", 0)),
45
+ "turnover": str(r.get("换手率", 0)),
46
+ "pe": str(r.get("市盈率-动态", 0)),
47
+ "pb": str(r.get("市净率", 0)),
48
+ "total_cap": str(r.get("总市值", 0)),
49
+ "circulating_cap": str(r.get("流通市值", 0)),
50
+ }
51
+ except Exception:
52
+ return None
@@ -0,0 +1,64 @@
1
+ """Baostock K 线数据源(需要 baostock 包)。"""
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 baostock as bs
10
+ HAS_BAOSTOCK = True
11
+ except ImportError:
12
+ HAS_BAOSTOCK = False
13
+
14
+
15
+ class BaostockKlineFetcher(BaseFetcher):
16
+ """Baostock K 线数据源 (优先级 3) - 需要安装 baostock 包。"""
17
+
18
+ def __init__(self):
19
+ super().__init__("baostock_kline", priority=3)
20
+
21
+ def fetch(self, code: str, **kwargs) -> list | None:
22
+ if not HAS_BAOSTOCK:
23
+ return None
24
+ scale = kwargs.get("scale", 240)
25
+ datalen = kwargs.get("datalen", 30)
26
+
27
+ # baostock 格式: sh.600989
28
+ plain = code.lstrip("shszSHSZbjBJ").zfill(6)
29
+ if plain.startswith(("60", "68", "51", "56", "58")):
30
+ bs_code = f"sh.{plain}"
31
+ else:
32
+ bs_code = f"sz.{plain}"
33
+
34
+ if scale != 240:
35
+ return None # baostock 只支持日线
36
+
37
+ try:
38
+ lg = bs.login()
39
+ rs = bs.query_history_k_data_plus(
40
+ bs_code,
41
+ "date,open,high,low,close,volume",
42
+ count=datalen,
43
+ frequency="d",
44
+ adjustflag="2",
45
+ )
46
+ if rs.error_code != "0":
47
+ bs.logout()
48
+ return None
49
+ result = []
50
+ while rs.next():
51
+ row = rs.get_row_data()
52
+ if len(row) >= 6:
53
+ result.append({
54
+ "day": row[0],
55
+ "open": row[1],
56
+ "high": row[2],
57
+ "low": row[3],
58
+ "close": row[4],
59
+ "volume": row[5],
60
+ })
61
+ bs.logout()
62
+ return result if result else None
63
+ except Exception:
64
+ return None
@@ -0,0 +1,29 @@
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
8
+
9
+ URL = "https://emweb.securities.eastmoney.com/PC_HSF10/NewFinanceAnalysis/ZYZBAjaxNew?type=0&code={code}"
10
+
11
+
12
+ class EastmoneyFinanceFetcher(BaseFetcher):
13
+ """东方财富财务数据源 (优先级 10)。缓存由 data 层统一管理。"""
14
+
15
+ def __init__(self):
16
+ super().__init__("eastmoney_finance", priority=10)
17
+
18
+ def fetch(self, code: str, **kwargs) -> list | None:
19
+ raw = http_get(URL.format(code=code))
20
+ try:
21
+ data = json.loads(raw)
22
+ except json.JSONDecodeError:
23
+ return None
24
+ if not data or "data" not in data or not data["data"]:
25
+ return None
26
+ result = data["data"][:4]
27
+ for r in result:
28
+ r["source"] = "eastmoney"
29
+ return result
@@ -0,0 +1,48 @@
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, to_secid
8
+
9
+ EASTMONEY_KLINE_URL = "https://push2his.eastmoney.com/api/qt/stock/kline/get?cb=&secid={secid}&ut=fa5fd1943c7b386f172d6893dbbd4dc1&fields1=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61&klt={klt}&fqt=1&end=20500101&lmt={lmt}"
10
+ KLT_MAP = {5: 5, 15: 15, 30: 30, 60: 60, 240: 101}
11
+
12
+
13
+ class EastmoneyKlineFetcher(BaseFetcher):
14
+ """东方财富 K 线数据源 (优先级 8)。"""
15
+
16
+ def __init__(self):
17
+ super().__init__("eastmoney_kline", priority=8)
18
+
19
+ def fetch(self, code: str, **kwargs) -> list | None:
20
+ scale = kwargs.get("scale", 240)
21
+ datalen = kwargs.get("datalen", 30)
22
+ secid = to_secid(code)
23
+ klt = KLT_MAP.get(scale, 101)
24
+ url = EASTMONEY_KLINE_URL.format(secid=secid, klt=klt, lmt=datalen)
25
+ raw = http_get(url, timeout=10)
26
+ try:
27
+ data = json.loads(raw)
28
+ except json.JSONDecodeError:
29
+ return None
30
+ if not data or data.get("rc") != 0 or "data" not in data:
31
+ return None
32
+ klines = data["data"].get("klines", [])
33
+ if not klines:
34
+ return None
35
+ result = []
36
+ for line in klines:
37
+ parts = line.split(",")
38
+ if len(parts) >= 6:
39
+ result.append({
40
+ "day": parts[0],
41
+ "open": parts[1],
42
+ "close": parts[2],
43
+ "high": parts[3],
44
+ "low": parts[4],
45
+ "volume": parts[5],
46
+ "source": "eastmoney",
47
+ })
48
+ return result if result else None