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.
- package/CHANGELOG.md +36 -0
- package/README.md +110 -39
- package/data/reports/202506_Stock_Analysis_Summary.md +271 -0
- package/experts/README.md +23 -2
- package/experts/buffett.md +44 -1
- package/experts/chaogu_yangjia.md +50 -0
- package/experts/decide.md +54 -2
- package/experts/duan_yongping.md +45 -0
- package/experts/lynch.md +48 -1
- package/experts/soros.md +43 -0
- package/experts/xu_xiang.md +44 -0
- package/experts/zhao_laoge.md +53 -0
- package/experts/zuoshou_xinyi.md +66 -0
- package/methodology.md +313 -13
- package/package.json +1 -1
- package/scripts/__pycache__/screener.cpython-314.pyc +0 -0
- package/scripts/api/__init__.py +22 -0
- package/scripts/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/api/__pycache__/quote_cli.cpython-314.pyc +0 -0
- package/scripts/api/__pycache__/screener_cli.cpython-314.pyc +0 -0
- package/scripts/api/quote_cli.py +106 -0
- package/scripts/api/screener_cli.py +149 -0
- package/scripts/business/__init__.py +15 -0
- package/scripts/business/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/business/__pycache__/screening_service.cpython-314.pyc +0 -0
- package/scripts/business/__pycache__/stock_analysis.cpython-314.pyc +0 -0
- package/scripts/business/screening_service.py +267 -0
- package/scripts/business/stock_analysis.py +183 -0
- package/scripts/common/__init__.py +334 -0
- package/scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/common/__pycache__/http.cpython-314.pyc +0 -0
- package/scripts/common/__pycache__/parsers.cpython-314.pyc +0 -0
- package/scripts/common/__pycache__/utils.cpython-314.pyc +0 -0
- package/scripts/common/__pycache__/validators.cpython-314.pyc +0 -0
- package/scripts/common/exceptions/__init__.py +172 -0
- package/scripts/common/exceptions/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/common/http.py +79 -0
- package/scripts/common/metrics.py +92 -0
- package/scripts/common/parsers.py +125 -0
- package/scripts/common/utils.py +195 -0
- package/scripts/common/validators.py +219 -0
- package/scripts/config/__init__.py +24 -0
- package/scripts/config/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/config/__pycache__/loader.cpython-314.pyc +0 -0
- package/scripts/config/data_source.yaml +126 -0
- package/scripts/config/industry_thresholds.yaml +158 -0
- package/scripts/config/limits.yaml +48 -0
- package/scripts/config/loader.py +141 -0
- package/scripts/config/notification.yaml +57 -0
- package/scripts/config/scoring.yaml +159 -0
- package/scripts/data/__pycache__/config.cpython-314.pyc +0 -0
- package/scripts/data/__pycache__/types.cpython-314.pyc +0 -0
- package/scripts/data/config.py +56 -4
- package/scripts/data/portfolio.json +100 -0
- package/scripts/data/portfolio_example.json +66 -11
- package/scripts/data/sector_stocks.json +244 -80
- package/scripts/data/types.py +3 -3
- package/scripts/fetchers/__init__.py +54 -0
- package/scripts/fetchers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/akshare_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_event.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_flow.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_lhb.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/efinance_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/sina_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tencent_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/akshare_quote.py +17 -3
- package/scripts/fetchers/eastmoney_event.py +148 -0
- package/scripts/fetchers/eastmoney_flow.py +118 -0
- package/scripts/fetchers/eastmoney_lhb.py +134 -0
- package/scripts/fetchers/eastmoney_quote.py +3 -3
- package/scripts/fetchers/efinance_quote.py +17 -3
- package/scripts/fetchers/sina_quote.py +5 -9
- package/scripts/fetchers/tencent_quote.py +3 -1
- package/scripts/monitor/__init__.py +13 -0
- package/scripts/monitor/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/monitor/__pycache__/manager.cpython-314.pyc +0 -0
- package/scripts/monitor/channels/__init__.py +6 -0
- package/scripts/monitor/channels/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/monitor/channels/__pycache__/bark.cpython-314.pyc +0 -0
- package/scripts/monitor/channels/__pycache__/base.cpython-314.pyc +0 -0
- package/scripts/monitor/channels/bark.py +75 -0
- package/scripts/monitor/channels/base.py +36 -0
- package/scripts/monitor/health.py +148 -0
- package/scripts/monitor/manager.py +229 -0
- package/scripts/portfolio/__init__.py +13 -0
- package/scripts/portfolio/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/portfolio/__pycache__/manager.cpython-314.pyc +0 -0
- package/scripts/portfolio/manager.py +329 -0
- package/scripts/portfolio/performance.py +209 -0
- package/scripts/screener.py +78 -23
- package/scripts/strategies/factors/__pycache__/liquidity.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/liquidity.py +1 -1
- package/skills/backtest/SKILL.md +57 -0
- package/skills/help/SKILL.md +69 -5
- package/skills/monitor/SKILL.md +98 -0
- package/skills/portfolio/SKILL.md +135 -40
- package/skills/stock/SKILL.md +99 -1
- package/skills/{init → stock-init}/SKILL.md +5 -5
- package/workflow.md +36 -2
- 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"]
|