stock-analyzer-skill 1.1.0 → 1.2.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 (102) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +110 -39
  3. package/data/reports/202506_Stock_Analysis_Summary.md +271 -0
  4. package/experts/README.md +23 -2
  5. package/experts/buffett.md +44 -1
  6. package/experts/chaogu_yangjia.md +50 -0
  7. package/experts/decide.md +54 -2
  8. package/experts/duan_yongping.md +45 -0
  9. package/experts/lynch.md +48 -1
  10. package/experts/soros.md +43 -0
  11. package/experts/xu_xiang.md +44 -0
  12. package/experts/zhao_laoge.md +53 -0
  13. package/experts/zuoshou_xinyi.md +66 -0
  14. package/methodology.md +313 -13
  15. package/package.json +1 -1
  16. package/scripts/__pycache__/screener.cpython-314.pyc +0 -0
  17. package/scripts/api/__init__.py +22 -0
  18. package/scripts/api/__pycache__/__init__.cpython-314.pyc +0 -0
  19. package/scripts/api/__pycache__/quote_cli.cpython-314.pyc +0 -0
  20. package/scripts/api/__pycache__/screener_cli.cpython-314.pyc +0 -0
  21. package/scripts/api/quote_cli.py +106 -0
  22. package/scripts/api/screener_cli.py +149 -0
  23. package/scripts/business/__init__.py +15 -0
  24. package/scripts/business/__pycache__/__init__.cpython-314.pyc +0 -0
  25. package/scripts/business/__pycache__/screening_service.cpython-314.pyc +0 -0
  26. package/scripts/business/__pycache__/stock_analysis.cpython-314.pyc +0 -0
  27. package/scripts/business/screening_service.py +267 -0
  28. package/scripts/business/stock_analysis.py +183 -0
  29. package/scripts/common/__init__.py +334 -0
  30. package/scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  31. package/scripts/common/__pycache__/http.cpython-314.pyc +0 -0
  32. package/scripts/common/__pycache__/parsers.cpython-314.pyc +0 -0
  33. package/scripts/common/__pycache__/utils.cpython-314.pyc +0 -0
  34. package/scripts/common/__pycache__/validators.cpython-314.pyc +0 -0
  35. package/scripts/common/exceptions/__init__.py +172 -0
  36. package/scripts/common/exceptions/__pycache__/__init__.cpython-314.pyc +0 -0
  37. package/scripts/common/http.py +79 -0
  38. package/scripts/common/metrics.py +92 -0
  39. package/scripts/common/parsers.py +125 -0
  40. package/scripts/common/utils.py +195 -0
  41. package/scripts/common/validators.py +219 -0
  42. package/scripts/config/__init__.py +24 -0
  43. package/scripts/config/__pycache__/__init__.cpython-314.pyc +0 -0
  44. package/scripts/config/__pycache__/loader.cpython-314.pyc +0 -0
  45. package/scripts/config/data_source.yaml +126 -0
  46. package/scripts/config/industry_thresholds.yaml +158 -0
  47. package/scripts/config/limits.yaml +48 -0
  48. package/scripts/config/loader.py +141 -0
  49. package/scripts/config/notification.yaml +57 -0
  50. package/scripts/config/scoring.yaml +159 -0
  51. package/scripts/data/__pycache__/config.cpython-314.pyc +0 -0
  52. package/scripts/data/__pycache__/types.cpython-314.pyc +0 -0
  53. package/scripts/data/config.py +56 -4
  54. package/scripts/data/portfolio.json +100 -0
  55. package/scripts/data/portfolio_example.json +66 -11
  56. package/scripts/data/sector_stocks.json +244 -80
  57. package/scripts/data/types.py +3 -3
  58. package/scripts/fetchers/__init__.py +54 -0
  59. package/scripts/fetchers/__pycache__/__init__.cpython-314.pyc +0 -0
  60. package/scripts/fetchers/__pycache__/akshare_quote.cpython-314.pyc +0 -0
  61. package/scripts/fetchers/__pycache__/eastmoney_event.cpython-314.pyc +0 -0
  62. package/scripts/fetchers/__pycache__/eastmoney_flow.cpython-314.pyc +0 -0
  63. package/scripts/fetchers/__pycache__/eastmoney_lhb.cpython-314.pyc +0 -0
  64. package/scripts/fetchers/__pycache__/eastmoney_quote.cpython-314.pyc +0 -0
  65. package/scripts/fetchers/__pycache__/efinance_quote.cpython-314.pyc +0 -0
  66. package/scripts/fetchers/__pycache__/sina_quote.cpython-314.pyc +0 -0
  67. package/scripts/fetchers/__pycache__/tencent_quote.cpython-314.pyc +0 -0
  68. package/scripts/fetchers/akshare_quote.py +17 -3
  69. package/scripts/fetchers/eastmoney_event.py +148 -0
  70. package/scripts/fetchers/eastmoney_flow.py +118 -0
  71. package/scripts/fetchers/eastmoney_lhb.py +134 -0
  72. package/scripts/fetchers/eastmoney_quote.py +3 -3
  73. package/scripts/fetchers/efinance_quote.py +17 -3
  74. package/scripts/fetchers/sina_quote.py +5 -9
  75. package/scripts/fetchers/tencent_quote.py +3 -1
  76. package/scripts/monitor/__init__.py +13 -0
  77. package/scripts/monitor/__pycache__/__init__.cpython-314.pyc +0 -0
  78. package/scripts/monitor/__pycache__/manager.cpython-314.pyc +0 -0
  79. package/scripts/monitor/channels/__init__.py +6 -0
  80. package/scripts/monitor/channels/__pycache__/__init__.cpython-314.pyc +0 -0
  81. package/scripts/monitor/channels/__pycache__/bark.cpython-314.pyc +0 -0
  82. package/scripts/monitor/channels/__pycache__/base.cpython-314.pyc +0 -0
  83. package/scripts/monitor/channels/bark.py +75 -0
  84. package/scripts/monitor/channels/base.py +36 -0
  85. package/scripts/monitor/health.py +148 -0
  86. package/scripts/monitor/manager.py +229 -0
  87. package/scripts/portfolio/__init__.py +13 -0
  88. package/scripts/portfolio/__pycache__/__init__.cpython-314.pyc +0 -0
  89. package/scripts/portfolio/__pycache__/manager.cpython-314.pyc +0 -0
  90. package/scripts/portfolio/manager.py +329 -0
  91. package/scripts/portfolio/performance.py +209 -0
  92. package/scripts/screener.py +78 -23
  93. package/scripts/strategies/factors/__pycache__/liquidity.cpython-314.pyc +0 -0
  94. package/scripts/strategies/factors/liquidity.py +1 -1
  95. package/skills/backtest/SKILL.md +57 -0
  96. package/skills/help/SKILL.md +69 -5
  97. package/skills/monitor/SKILL.md +98 -0
  98. package/skills/portfolio/SKILL.md +135 -40
  99. package/skills/stock/SKILL.md +99 -1
  100. package/skills/{init → stock-init}/SKILL.md +5 -5
  101. package/workflow.md +36 -2
  102. package/scripts/common.py +0 -507
@@ -0,0 +1,267 @@
1
+ """
2
+ 选股服务 (Business 层)。
3
+
4
+ 提供选股筛选的业务逻辑,与 CLI 层解耦。
5
+ """
6
+ import logging
7
+ from typing import List, Dict, Any, Optional
8
+
9
+ from common import to_float, normalize_quote_code, board_type
10
+ from common.exceptions import InsufficientDataError, ValidationError
11
+ from common.validators import validate_code
12
+ from data import get_quote, get_quotes, get_kline, get_finance
13
+ from classifier import infer_industry
14
+ from strategies import (
15
+ STRATEGIES,
16
+ quality_score,
17
+ valuation_score,
18
+ momentum_score,
19
+ liquidity_score,
20
+ volatility_from_closes
21
+ )
22
+ from strategies.thresholds import get_industry_threshold
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ScreeningService:
28
+ """选股服务。"""
29
+
30
+ def __init__(self):
31
+ self.default_strategy = "balanced"
32
+ self.max_workers = 8
33
+
34
+ def screen(
35
+ self,
36
+ codes: List[str],
37
+ strategy: str = "balanced",
38
+ filters: Optional[Dict[str, Any]] = None
39
+ ) -> List[Dict[str, Any]]:
40
+ """
41
+ 选股筛选。
42
+
43
+ Args:
44
+ codes: 股票代码列表
45
+ strategy: 策略名称
46
+ filters: 筛选条件
47
+
48
+ Returns:
49
+ 筛选结果列表
50
+ """
51
+ filters = filters or {}
52
+
53
+ # 验证策略
54
+ if strategy not in STRATEGIES:
55
+ logger.warning(f"未知策略 {strategy},使用默认策略")
56
+ strategy = self.default_strategy
57
+
58
+ # 标准化代码
59
+ normalized_codes = []
60
+ for c in codes:
61
+ try:
62
+ if validate_code(c):
63
+ normalized_codes.append(normalize_quote_code(c))
64
+ except ValidationError:
65
+ logger.warning(f"跳过无效代码: {c}")
66
+
67
+ if not normalized_codes:
68
+ return []
69
+
70
+ # 获取行情数据
71
+ quotes = get_quotes(normalized_codes)
72
+ quote_map = {q.code: q for q in quotes}
73
+
74
+ # 预获取财务数据
75
+ fin_cache = self._prefetch_finance(normalized_codes)
76
+
77
+ # 分析每只股票
78
+ results = []
79
+ for code in normalized_codes:
80
+ quote = quote_map.get(code)
81
+ if not quote:
82
+ continue
83
+
84
+ try:
85
+ stock_result = self._analyze_stock(
86
+ code,
87
+ quote,
88
+ fin_cache.get(code, []),
89
+ strategy,
90
+ filters
91
+ )
92
+ if stock_result:
93
+ results.append(stock_result)
94
+ except Exception as e:
95
+ logger.warning(f"分析失败 {code}: {e}")
96
+ continue
97
+
98
+ # 排序
99
+ results.sort(key=lambda r: r.get("score", 0), reverse=True)
100
+
101
+ return results
102
+
103
+ def _prefetch_finance(self, codes: List[str]) -> Dict[str, List[dict]]:
104
+ """预获取财务数据。"""
105
+ from concurrent.futures import ThreadPoolExecutor, as_completed
106
+ from data import get_finance
107
+ from common import normalize_finance_code
108
+
109
+ results = {}
110
+
111
+ def fetch_one(code):
112
+ try:
113
+ records = get_finance(normalize_finance_code(code))
114
+ return code, [r.to_dict() for r in records]
115
+ except Exception:
116
+ return code, []
117
+
118
+ with ThreadPoolExecutor(max_workers=self.max_workers) as ex:
119
+ futures = {ex.submit(fetch_one, c): c for c in codes}
120
+ for future in as_completed(futures):
121
+ try:
122
+ code, data = future.result()
123
+ results[code] = data
124
+ except Exception:
125
+ pass
126
+
127
+ return results
128
+
129
+ def _analyze_stock(
130
+ self,
131
+ code: str,
132
+ quote,
133
+ fin_records: List[dict],
134
+ strategy: str,
135
+ filters: Dict[str, Any]
136
+ ) -> Optional[Dict[str, Any]]:
137
+ """分析单只股票。"""
138
+ quote_dict = quote.to_dict()
139
+ fin = fin_records[0] if fin_records else {}
140
+
141
+ # 行业分类
142
+ industry = infer_industry(quote_dict.get("name", ""), code)
143
+
144
+ # 硬过滤
145
+ rejected = self._hard_filter(quote_dict, fin, filters)
146
+ if rejected:
147
+ return {
148
+ "code": code,
149
+ "name": quote_dict.get("name", ""),
150
+ "score": 0,
151
+ "rejected": rejected,
152
+ }
153
+
154
+ # 计算因子得分
155
+ features = self._compute_features(code)
156
+
157
+ weights = STRATEGIES[strategy]
158
+ parts = {
159
+ "quality": quality_score(fin, industry),
160
+ "valuation": valuation_score(quote_dict, fin, industry),
161
+ "momentum": momentum_score(features, quote_dict),
162
+ "liquidity": liquidity_score(quote_dict),
163
+ "volatility": volatility_from_closes(features.get("closes", []), industry),
164
+ }
165
+
166
+ total = sum(
167
+ parts.get(k, 0) * weights.get(k, 0)
168
+ for k in set(parts) | set(weights)
169
+ if k != "label"
170
+ )
171
+
172
+ return {
173
+ "code": code,
174
+ "name": quote_dict.get("name", ""),
175
+ "board": board_type(code),
176
+ "industry": industry,
177
+ "score": round(total, 1),
178
+ "quality": round(parts["quality"], 1),
179
+ "valuation": round(parts["valuation"], 1),
180
+ "momentum": round(parts["momentum"], 1),
181
+ "liquidity": round(parts["liquidity"], 1),
182
+ "volatility": round(parts["volatility"], 1),
183
+ "price": quote_dict.get("price"),
184
+ "change_pct": quote_dict.get("change_pct"),
185
+ "pe": quote_dict.get("pe"),
186
+ "pb": quote_dict.get("pb"),
187
+ "roe": fin.get("roe", "-"),
188
+ "profit_growth": fin.get("net_profit_yoy", "-"),
189
+ "ret20": round(features.get("ret20", 0), 1),
190
+ "trend": "上升" if features.get("trend", 0) > 0 else "下降" if features.get("trend", 0) < 0 else "震荡",
191
+ "rsi": features.get("rsi", 50),
192
+ "macd_signal": features.get("macd_signal", 0),
193
+ "rejected": [],
194
+ }
195
+
196
+ def _compute_features(self, code: str) -> dict:
197
+ """计算技术指标特征。"""
198
+ import statistics
199
+ from technical import macd_full, rsi_features
200
+ from data import get_kline
201
+
202
+ bars = get_kline(code, scale=240, datalen=240)
203
+ closes = [b.close for b in bars if b.close > 0]
204
+ volumes = [b.volume for b in bars if b.volume > 0]
205
+
206
+ if len(closes) < 10:
207
+ return {"trend": 0, "ret20": 0, "rsi": 50, "macd_signal": 0}
208
+
209
+ # 趋势
210
+ ma10 = statistics.mean(closes[-10:])
211
+ ma20 = statistics.mean(closes[-20:]) if len(closes) >= 20 else statistics.mean(closes)
212
+ trend = 1 if closes[-1] > ma10 > ma20 else (-1 if closes[-1] < ma10 < ma20 else 0)
213
+
214
+ # 20日收益率
215
+ base = closes[-21] if len(closes) >= 21 else closes[0]
216
+ ret20 = (closes[-1] / base - 1) * 100 if base else 0
217
+
218
+ # RSI
219
+ rsi_data = rsi_features(closes)
220
+ rsi = rsi_data.get("rsi", 50)
221
+
222
+ # MACD
223
+ macd = macd_full(closes) or {}
224
+ macd_signal = macd.get("signal", 0)
225
+
226
+ return {
227
+ "trend": trend,
228
+ "ret20": ret20,
229
+ "rsi": rsi,
230
+ "macd_signal": macd_signal,
231
+ "closes": closes,
232
+ }
233
+
234
+ def _hard_filter(self, quote: dict, fin: dict, filters: dict) -> List[str]:
235
+ """硬过滤。"""
236
+ reasons = []
237
+ name = quote.get("name", "")
238
+ code = quote.get("code", "")
239
+ bd = board_type(code)
240
+
241
+ # ST 检测
242
+ if name.upper().startswith(("ST", "*ST")):
243
+ reasons.append("ST风险")
244
+
245
+ # 最低市值
246
+ min_cap = filters.get("min_cap", 40)
247
+ if bd == "创业板" or bd == "科创板":
248
+ min_cap = min_cap * 0.6
249
+ elif bd == "北交所":
250
+ min_cap = min_cap * 0.4
251
+
252
+ if to_float(quote.get("total_cap", 0)) < min_cap:
253
+ reasons.append(f"市值<{min_cap}亿")
254
+
255
+ # 最低成交额(配置值单位为万元,amount 已归一化为元)
256
+ min_amount = filters.get("min_amount", 5000)
257
+ if to_float(quote.get("amount", 0)) / 10000 < min_amount:
258
+ reasons.append(f"成交额<{min_amount}万")
259
+
260
+ # 排除亏损
261
+ if filters.get("exclude_loss") and to_float(fin.get("eps", 0)) <= 0:
262
+ reasons.append("EPS<=0")
263
+
264
+ return reasons
265
+
266
+
267
+ __all__ = ["ScreeningService"]
@@ -0,0 +1,183 @@
1
+ """
2
+ 个股分析业务流程 (Business 层)。
3
+
4
+ 聚合技术分析、财务分析、缠论分析等模块,提供统一的分析入口。
5
+ """
6
+ import logging
7
+ from typing import Optional, Dict, Any, List
8
+
9
+ from common.exceptions import InsufficientDataError, ValidationError
10
+ from common.validators import normalize_code, validate_code
11
+ from data import get_quote, get_kline, get_finance
12
+ from classifier import profile_stock
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class StockAnalysisService:
18
+ """个股分析服务。"""
19
+
20
+ def __init__(self):
21
+ self.min_kline_days = 30 # 缠论分析最少需要30根K线
22
+
23
+ def analyze(self, code: str, include_technical: bool = True,
24
+ include_finance: bool = True, include_chan: bool = True) -> Dict[str, Any]:
25
+ """
26
+ 完整分析一只股票。
27
+
28
+ Args:
29
+ code: 股票代码
30
+ include_technical: 是否包含技术分析
31
+ include_finance: 是否包含财务分析
32
+ include_chan: 是否包含缠论分析
33
+
34
+ Returns:
35
+ 分析结果字典
36
+ """
37
+ # 验证输入
38
+ code = self._normalize_code(code)
39
+
40
+ result = {
41
+ "code": code,
42
+ "name": "",
43
+ "price": 0,
44
+ "change_pct": 0,
45
+ }
46
+
47
+ # 1. 获取实时行情
48
+ quote = get_quote(code)
49
+ if quote:
50
+ result["name"] = quote.name
51
+ result["price"] = quote.price
52
+ result["change_pct"] = quote.change_pct
53
+
54
+ # 2. 行业和类型画像
55
+ if quote:
56
+ result["profile"] = profile_stock(quote.to_dict())
57
+
58
+ # 3. K线数据
59
+ kline = get_kline(code, scale=240, datalen=120)
60
+ if not kline or len(kline) < 10:
61
+ logger.warning(f"K线数据不足: {code}")
62
+ result["warning"] = "K线数据不足"
63
+ else:
64
+ kline_dicts = [b.to_dict() for b in kline]
65
+ result["kline_count"] = len(kline_dicts)
66
+
67
+ # 技术分析
68
+ if include_technical:
69
+ result["technical"] = self._analyze_technical(kline_dicts)
70
+
71
+ # 缠论分析
72
+ if include_chan and len(kline) >= self.min_kline_days:
73
+ result["chan"] = self._analyze_chan(kline_dicts)
74
+
75
+ # 4. 财务数据
76
+ if include_finance:
77
+ finance = get_finance(code)
78
+ if finance:
79
+ result["finance"] = self._extract_finance_summary(finance[0].to_dict())
80
+
81
+ # 5. 综合评分
82
+ if "technical" in result and "profile" in result:
83
+ result["score"] = self._calculate_composite_score(result)
84
+
85
+ return result
86
+
87
+ def _normalize_code(self, code: str) -> str:
88
+ """标准化股票代码。"""
89
+ if not validate_code(code):
90
+ raise ValidationError("code", code, "格式无效")
91
+ return normalize_code(code)
92
+
93
+ def _analyze_technical(self, kline: List[dict]) -> dict:
94
+ """技术分析。"""
95
+ from technical import (
96
+ ma_system, macd_full, kdj_full, bollinger,
97
+ rsi_features, volume_analysis, detect_candle_patterns
98
+ )
99
+
100
+ closes = [k["close"] for k in kline if k.get("close", 0) > 0]
101
+ highs = [k["high"] for k in kline if k.get("high", 0) > 0]
102
+ lows = [k["low"] for k in kline if k.get("low", 0) > 0]
103
+ volumes = [k["volume"] for k in kline if k.get("volume", 0) > 0]
104
+
105
+ result = {}
106
+
107
+ # 均线系统
108
+ ma = ma_system(closes)
109
+ result["ma"] = ma.get("alignment", "数据不足")
110
+
111
+ # MACD
112
+ macd = macd_full(closes) or {}
113
+ result["macd_signal"] = macd.get("signal", 0)
114
+ result["macd_divergence"] = macd.get("divergence", "")
115
+
116
+ # KDJ
117
+ kdj = kdj_full(closes)
118
+ result["kdj"] = kdj.get("signal", "")
119
+
120
+ # BOLL
121
+ boll = bollinger(closes)
122
+ result["boll_position"] = boll.get("position", 0.5)
123
+
124
+ # RSI
125
+ rsi = rsi_features(closes)
126
+ result["rsi"] = rsi.get("rsi", 50)
127
+
128
+ # 成交量
129
+ vol = volume_analysis(closes, volumes)
130
+ result["volume_signal"] = vol.get("volume_price_signal", 0)
131
+
132
+ # K线形态
133
+ patterns = detect_candle_patterns(kline)
134
+ result["patterns"] = patterns[:5] if patterns else []
135
+
136
+ return result
137
+
138
+ def _analyze_chan(self, kline: List[dict]) -> dict:
139
+ """缠论分析。"""
140
+ from chan import chan_full_analysis
141
+
142
+ try:
143
+ analysis = chan_full_analysis(kline)
144
+ return analysis
145
+ except Exception as e:
146
+ logger.warning(f"缠论分析失败: {e}")
147
+ return {"error": str(e)}
148
+
149
+ def _extract_finance_summary(self, fin: dict) -> dict:
150
+ """提取财务摘要。"""
151
+ return {
152
+ "eps": fin.get("eps", 0),
153
+ "roe": fin.get("roe", 0),
154
+ "net_profit_yoy": fin.get("net_profit_yoy", 0),
155
+ "revenue_yoy": fin.get("revenue_yoy", 0),
156
+ "gross_margin": fin.get("gross_margin", 0),
157
+ "debt_ratio": fin.get("debt_ratio", 0),
158
+ }
159
+
160
+ def _calculate_composite_score(self, result: dict) -> dict:
161
+ """计算综合评分。"""
162
+ from technical import composite_score, detect_market_environment
163
+
164
+ tech = result.get("technical", {})
165
+ profile = result.get("profile", {})
166
+
167
+ features = {
168
+ "ma_system": {"alignment": tech.get("ma", "数据不足")},
169
+ "macd": {"signal": tech.get("macd_signal", 0), "divergence": tech.get("macd_divergence", "")},
170
+ "kdj": {"signal": tech.get("kdj", "")},
171
+ "bollinger": {"position": tech.get("boll_position", 0.5)},
172
+ "rsi": {"rsi": tech.get("rsi", 50)},
173
+ "volume": {"volume_price_signal": tech.get("volume_signal", 0)},
174
+ "patterns": tech.get("patterns", []),
175
+ }
176
+
177
+ stock_type = profile.get("type", "普通股")
178
+ score_result = composite_score(features, stock_type=stock_type)
179
+
180
+ return score_result
181
+
182
+
183
+ __all__ = ["StockAnalysisService"]