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
package/methodology.md
CHANGED
|
@@ -200,25 +200,26 @@ p = 胜率, b = 赔率(期望收益/最大风险)
|
|
|
200
200
|
|------|--------|------|------|
|
|
201
201
|
| 质量 | quality | ROE、净利增速、营收增速、毛利率、负债率、经营现金流/EPS | 好公司与盈利质量 |
|
|
202
202
|
| 估值 | valuation | PE、PB、PEG、PE/ROE | 安全边际和估值消化能力 |
|
|
203
|
-
| 动量 | momentum | 20
|
|
203
|
+
| 动量 | momentum | 20日收益、MA10/MA20、量能比、换手率 | 市场是否开始认可 |
|
|
204
204
|
| 流动性 | liquidity | 成交额、总市值、换手适中程度 | 能否交易、能否退出 |
|
|
205
|
+
| 波动率 | volatility | 历史收益率标准差(低波动得高分) | A股低波动异象 |
|
|
205
206
|
|
|
206
|
-
### 5.
|
|
207
|
+
### 5. 策略权重(五因子模型)
|
|
207
208
|
|
|
208
|
-
| 策略 | 市场环境 | 质量 | 估值 | 动量 | 流动性 |
|
|
209
|
-
|
|
210
|
-
| balanced | 震荡/方向不明 |
|
|
211
|
-
| quality_value | 价值修复/防守 |
|
|
212
|
-
| growth_momentum | 进攻行情/主线题材 |
|
|
213
|
-
| defensive | 缩量弱市/避险 |
|
|
214
|
-
| turning_point | 超跌修复/拐点 |
|
|
209
|
+
| 策略 | 市场环境 | 质量 | 估值 | 动量 | 流动性 | 波动率 |
|
|
210
|
+
|------|----------|------|------|------|--------|--------|
|
|
211
|
+
| balanced | 震荡/方向不明 | 25% | 20% | 20% | 15% | 20% |
|
|
212
|
+
| quality_value | 价值修复/防守 | 35% | 30% | 5% | 12% | 18% |
|
|
213
|
+
| growth_momentum | 进攻行情/主线题材 | 18% | 15% | 35% | 12% | 20% |
|
|
214
|
+
| defensive | 缩量弱市/避险 | 25% | 22% | 8% | 12% | 33% |
|
|
215
|
+
| turning_point | 超跌修复/拐点 | 18% | 18% | 32% | 14% | 18% |
|
|
215
216
|
|
|
216
217
|
### 6. 输出标准
|
|
217
218
|
|
|
218
219
|
选股结果必须同时给出:
|
|
219
220
|
|
|
220
|
-
- 候选排名:总分 +
|
|
221
|
-
-
|
|
221
|
+
- 候选排名:总分 + 五因子分。
|
|
222
|
+
- 剔除原因:让”为什么没选”可审计。
|
|
222
223
|
- 市场适配:当前更适合进攻、均衡还是防守。
|
|
223
224
|
- 交易计划:买入触发、失效条件、止损/降仓、仓位上限。
|
|
224
225
|
- 后续跟踪:需要复核的财报、公告、板块 ETF、关键均线或支撑位。
|
|
@@ -232,9 +233,153 @@ python3 scripts/screener.py --codes sh600989,sz000807,300476 --strategy growth_m
|
|
|
232
233
|
python3 scripts/screener.py --strategy defensive --exclude-loss --json
|
|
233
234
|
```
|
|
234
235
|
|
|
235
|
-
##
|
|
236
|
+
## 七、数据获取工具详解(v1.1.0)
|
|
236
237
|
|
|
237
|
-
### 1.
|
|
238
|
+
### 1. 代码结构总览
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
scripts/
|
|
242
|
+
├── __init__.py # 入口
|
|
243
|
+
├── common/ # 公共工具包(v1.1.0 新增)
|
|
244
|
+
│ ├── __init__.py # 主模块(缓存、HTTP、编码转换)
|
|
245
|
+
│ ├── validators.py # 输入验证器
|
|
246
|
+
│ └── exceptions/ # 统一异常类
|
|
247
|
+
│ └── __init__.py
|
|
248
|
+
├── config/ # 配置加载器(v1.1.0 新增)
|
|
249
|
+
│ ├── __init__.py
|
|
250
|
+
│ ├── loader.py
|
|
251
|
+
│ ├── data_source.yaml
|
|
252
|
+
│ ├── industry_thresholds.yaml
|
|
253
|
+
│ ├── limits.yaml
|
|
254
|
+
│ └── scoring.yaml
|
|
255
|
+
├── data/ # 数据层
|
|
256
|
+
├── strategies/ # 选股策略
|
|
257
|
+
└── technical/ # 技术分析
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 2. Common 模块详解
|
|
261
|
+
|
|
262
|
+
#### 2.1 缓存与 HTTP
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from common import (
|
|
266
|
+
cache_get, cache_set, cache_cleanup,
|
|
267
|
+
http_get, http_get_cached,
|
|
268
|
+
CACHE_DIR,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# 带缓存的 HTTP 请求(默认 6 小时 TTL)
|
|
272
|
+
data = http_get_cached("https://qt.gtimg.cn/q=sh600989")
|
|
273
|
+
|
|
274
|
+
# 直接 HTTP 请求
|
|
275
|
+
data = http_get(url, timeout=10)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### 2.2 股票代码标准化
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from common import normalize_quote_code, normalize_finance_code, plain_code
|
|
282
|
+
|
|
283
|
+
# 标准化为 sh/sz 前缀格式
|
|
284
|
+
quote_code = normalize_quote_code("600989") # → "sh600989"
|
|
285
|
+
finance_code = normalize_finance_code("600989") # → "sh600989"
|
|
286
|
+
plain = plain_code("sh600989") # → "600989"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### 2.3 输入验证器
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
from common.validators import (
|
|
293
|
+
validate_code, normalize_code, validate_codes,
|
|
294
|
+
validate_date, validate_date_range,
|
|
295
|
+
validate_positive, validate_in_range,
|
|
296
|
+
ValidationError,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# 验证单个代码
|
|
300
|
+
if validate_code("sh600989"):
|
|
301
|
+
code = normalize_code("600989") # → "sh600989"
|
|
302
|
+
|
|
303
|
+
# 批量验证
|
|
304
|
+
codes = validate_codes(["600989", "000807", "300476"])
|
|
305
|
+
|
|
306
|
+
# 日期验证
|
|
307
|
+
validate_date_range("2024-01-01", "2024-12-31")
|
|
308
|
+
|
|
309
|
+
# 数值验证
|
|
310
|
+
validate_positive(10.5, "price", min_value=0)
|
|
311
|
+
validate_in_range(15.0, "pe", 0, 1000)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### 2.4 统一异常类
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
from common.exceptions import (
|
|
318
|
+
StockAnalyzerError, DataError, NetworkError,
|
|
319
|
+
RateLimitError, ParseError, DataUnavailableError,
|
|
320
|
+
BusinessError, ValidationError, StrategyError,
|
|
321
|
+
InsufficientDataError, ConfigurationError,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
result = http_get(url)
|
|
326
|
+
except RateLimitError as e:
|
|
327
|
+
print(f"触发速率限制: {e.retry_after}秒后重试")
|
|
328
|
+
except NetworkError as e:
|
|
329
|
+
print(f"网络错误: {e.url}, {e.message}")
|
|
330
|
+
except ValidationError as e:
|
|
331
|
+
print(f"校验失败: {e.field} = {e.value_str}, {e.message}")
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 3. Config 模块详解
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from config import get_limit_config, get_scoring_config, load_industry_thresholds
|
|
338
|
+
|
|
339
|
+
# 获取限制配置
|
|
340
|
+
st_prefixes = get_limit_config("st_prefixes", ["ST", "*ST"])
|
|
341
|
+
min_amount = get_limit_config("min_amount.创业板", 3000)
|
|
342
|
+
|
|
343
|
+
# 获取评分配置
|
|
344
|
+
quality_weights = get_scoring_config("weights.quality")
|
|
345
|
+
valuation_weights = get_scoring_config("weights.valuation")
|
|
346
|
+
|
|
347
|
+
# 获取行业阈值
|
|
348
|
+
thresholds = load_industry_thresholds()
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**配置文件说明:**
|
|
352
|
+
|
|
353
|
+
| 文件 | 用途 |
|
|
354
|
+
|------|------|
|
|
355
|
+
| `limits.yaml` | ST 前缀、最低成交额、最低市值、涨跌停限制 |
|
|
356
|
+
| `scoring.yaml` | 多因子评分权重、因子阈值 |
|
|
357
|
+
| `industry_thresholds.yaml` | 各行业 ROE/PE/增速阈值 |
|
|
358
|
+
| `data_source.yaml` | API 端点配置、缓存 TTL |
|
|
359
|
+
|
|
360
|
+
### 4. 数据层使用
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
from data import get_quote, get_quotes, get_kline, get_finance
|
|
364
|
+
|
|
365
|
+
# 单只行情
|
|
366
|
+
quote = get_quote("sh600989")
|
|
367
|
+
print(quote.price, quote.pe, quote.change_pct)
|
|
368
|
+
|
|
369
|
+
# 批量行情
|
|
370
|
+
quotes = get_quotes(["sh600989", "sz000807"])
|
|
371
|
+
|
|
372
|
+
# K线数据
|
|
373
|
+
bars = get_kline("sh600989", scale=240, datalen=30) # 日K
|
|
374
|
+
bars = get_kline("sh600989", scale=5, datalen=100) # 5分钟
|
|
375
|
+
|
|
376
|
+
# 财务数据
|
|
377
|
+
records = get_finance("sh600989")
|
|
378
|
+
fin = records[0] # 最新一期
|
|
379
|
+
print(fin.ROEJQ, fin.EPSJB, fin.PARENTNETPROFITTZ)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### 5. 腾讯实时行情 — 批量查询
|
|
238
383
|
|
|
239
384
|
**单只查询:**
|
|
240
385
|
```bash
|
|
@@ -453,3 +598,158 @@ done
|
|
|
453
598
|
6. 高赔率≠无风险:仍需止损纪律
|
|
454
599
|
7. 防御仓位(黄金/低估值金融)是组合压舱石
|
|
455
600
|
8. 科技仓位不能为零,至少5-8%
|
|
601
|
+
|
|
602
|
+
## 十、近期案例复盘总结(2026年6月)
|
|
603
|
+
|
|
604
|
+
> 完整案例汇总见 `data/reports/202506_Stock_Analysis_Summary.md`
|
|
605
|
+
|
|
606
|
+
### 10.1 本次分析股票概览
|
|
607
|
+
|
|
608
|
+
| 股票 | 代码 | 评级 | 置信度 | PE | Q1净利增速 | 今日涨跌 |
|
|
609
|
+
|------|------|------|--------|-----|-----------|----------|
|
|
610
|
+
| 云铝股份 | 000807 | Buy | 80% | 10.7 | +269% | -3.04% |
|
|
611
|
+
| 宝丰能源 | 600989 | Buy | 78% | 13.8 | +50% | +0.73% |
|
|
612
|
+
| 洛阳钼业 | 603993 | Buy | 75% | 15.0 | +97% | -7.15% |
|
|
613
|
+
| 华友钴业 | 603799 | Hold | 65% | 12.1 | +99% | -6.49% |
|
|
614
|
+
| 海尔智家 | 600690 | Hold | 65% | 10.1 | -15% | -0.69% |
|
|
615
|
+
| 贵研铂业 | 600459 | Hold | 55% | 29.8 | +9% | -3.64% |
|
|
616
|
+
| 中兴通讯 | 000063 | **Sell** | 55% | 39.4 | **-47%** | -5.93% |
|
|
617
|
+
|
|
618
|
+
### 10.2 成功案例特征
|
|
619
|
+
|
|
620
|
+
**Buy 评级的共同特征**:
|
|
621
|
+
1. **高ROE**:ROE > 15%(云铝19.7%、宝丰24.8%、洛阳26.6%)
|
|
622
|
+
2. **高增长**:Q1净利增速 > 50%(云铝269%、洛阳97%、华友99%)
|
|
623
|
+
3. **低估值**:PE < 15x,PE/ROE < 3
|
|
624
|
+
4. **技术面偏多**:技术评分 > 60,买入信号共振
|
|
625
|
+
|
|
626
|
+
**案例1:云铝股份**
|
|
627
|
+
- 核心逻辑:Q1净利暴增269%,PE仅10.7x,负债率17.7%极安全
|
|
628
|
+
- 技术面:双针探底+老鸭头形态
|
|
629
|
+
- 结论:Buy,置信度80%,目标价32元
|
|
630
|
+
|
|
631
|
+
**案例2:宝丰能源**
|
|
632
|
+
- 核心逻辑:ROE 25%顶级,PEG仅0.17严重低估
|
|
633
|
+
- 技术面:三阴一阳+缠论底背驰
|
|
634
|
+
- 结论:Buy,置信度78%,目标价26元
|
|
635
|
+
|
|
636
|
+
**案例3:洛阳钼业**
|
|
637
|
+
- 核心逻辑:铜金双极战略,Q1净利+97%
|
|
638
|
+
- 技术面:MACD底背离+老鸭头
|
|
639
|
+
- 结论:Buy,置信度75%,目标价20元
|
|
640
|
+
|
|
641
|
+
### 10.3 失败案例警示
|
|
642
|
+
|
|
643
|
+
**Sell/Hold 评级的共同特征**:
|
|
644
|
+
1. **业绩下滑**:Q1净利负增长(中兴-47%、海尔-15%)
|
|
645
|
+
2. **高估值陷阱**:PE > 30 且无高增长支撑(中兴PE 39x)
|
|
646
|
+
3. **技术面极弱**:MACD死叉、均线空头排列
|
|
647
|
+
4. **负债率过高**: > 60%(华友63.6%、贵研64.9%)
|
|
648
|
+
|
|
649
|
+
**案例:中兴通讯**
|
|
650
|
+
- 问题:PE 39x严重高估,Q1净利-47%下滑,ROE仅7.6%
|
|
651
|
+
- 技术面:今日放量大跌-5.93%,MACD死叉
|
|
652
|
+
- 结论:Sell,置信度55%,建议回避
|
|
653
|
+
|
|
654
|
+
### 10.4 选股核心逻辑总结
|
|
655
|
+
|
|
656
|
+
| 优先级 | 指标 | 阈值 | 权重 |
|
|
657
|
+
|--------|------|------|------|
|
|
658
|
+
| 1 | ROE | >15%优秀,>20%顶级 | 25% |
|
|
659
|
+
| 2 | 净利增速 | >20%成长,>50%高速 | 25% |
|
|
660
|
+
| 3 | PE/ROE | <3低估,<1极度低估 | 20% |
|
|
661
|
+
| 4 | 技术面 | 评分>60,买入信号共振 | 15% |
|
|
662
|
+
| 5 | 负债率 | <60%健康,<50%优秀 | 10% |
|
|
663
|
+
| 6 | 毛利率 | >30%有壁垒 | 5% |
|
|
664
|
+
|
|
665
|
+
### 10.5 风险识别模式库
|
|
666
|
+
|
|
667
|
+
| 风险类型 | 识别信号 | 典型案例 | 应对策略 |
|
|
668
|
+
|----------|----------|----------|----------|
|
|
669
|
+
| 业绩暴降 | Q1/Q2增速转负 | 中兴(-47%) | 立即卖出 |
|
|
670
|
+
| 估值泡沫 | PE>30 且 增速<20% | 中兴(PE 39x) | 回避 |
|
|
671
|
+
| 技术破位 | MACD死叉+放量下跌 | 华友钴业 | 止损 |
|
|
672
|
+
| 负债过高 | 负债率>60% | 华友(63.6%) | 降低仓位 |
|
|
673
|
+
| 周期陷阱 | 低价但无成长 | 贵研铂业 | 观察 |
|
|
674
|
+
| 趋势向下 | 均线空头排列 | 中兴通讯 | 观望 |
|
|
675
|
+
|
|
676
|
+
### 10.6 今日市场特征(2026-06-08)
|
|
677
|
+
|
|
678
|
+
- **大盘**:沪深300ETF -2.15%,偏弱震荡
|
|
679
|
+
- **板块**:有色/能源/AI板块领跌
|
|
680
|
+
- **情绪**:7只分析股6只下跌,仅宝丰能源微涨
|
|
681
|
+
- **结论**:市场处于调整期,控制仓位,防守为主
|
|
682
|
+
|
|
683
|
+
### 10.7 输出格式标准化
|
|
684
|
+
|
|
685
|
+
深度分析报告必须包含以下核心章节(详见 `skills/stock/SKILL.md` Step 3.1):
|
|
686
|
+
|
|
687
|
+
1. **一句话结论**:核心判断 + 置信度
|
|
688
|
+
2. **五层分析**:基本面/估值/技术面/板块/风险收益比
|
|
689
|
+
3. **同业对比**:与行业竞争对手关键指标对比
|
|
690
|
+
4. **8人专家圆桌**:8位专家评分 + 分组汇总 + 信心指数
|
|
691
|
+
5. **投资建议**:评级 + 仓位 + 止损止盈 + 跟踪条件
|
|
692
|
+
6. **核心矛盾**:主要风险点和转折信号
|
|
693
|
+
|
|
694
|
+
## 十一、测试框架(v1.1.0)
|
|
695
|
+
|
|
696
|
+
### 1. Pytest 配置
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
# 运行所有测试(跳过网络测试)
|
|
700
|
+
pytest
|
|
701
|
+
|
|
702
|
+
# 运行包含网络测试
|
|
703
|
+
pytest --run-network
|
|
704
|
+
|
|
705
|
+
# 运行特定标记的测试
|
|
706
|
+
pytest -m unit
|
|
707
|
+
pytest -m "not slow"
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**可用标记:**
|
|
711
|
+
|
|
712
|
+
| 标记 | 说明 |
|
|
713
|
+
|------|------|
|
|
714
|
+
| `unit` | 单元测试 |
|
|
715
|
+
| `integration` | 集成测试 |
|
|
716
|
+
| `e2e` | 端到端测试 |
|
|
717
|
+
| `network` | 需要网络的测试(默认跳过) |
|
|
718
|
+
| `slow` | 慢速测试 |
|
|
719
|
+
|
|
720
|
+
### 2. 测试数据 fixtures
|
|
721
|
+
|
|
722
|
+
```python
|
|
723
|
+
import pytest
|
|
724
|
+
from conftest import (
|
|
725
|
+
SAMPLE_KLINE,
|
|
726
|
+
SAMPLE_QUOTE,
|
|
727
|
+
sample_finance_data,
|
|
728
|
+
mock_http_get,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# 使用标准K线数据
|
|
732
|
+
def test_ema_trend(SAMPLE_KLINE):
|
|
733
|
+
closes = [bar["close"] for bar in SAMPLE_KLINE]
|
|
734
|
+
...
|
|
735
|
+
|
|
736
|
+
# 使用标准行情数据
|
|
737
|
+
def test_quota_fields(SAMPLE_QUOTE):
|
|
738
|
+
assert SAMPLE_QUOTE["pe"] > 0
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### 3. Mock 网络请求
|
|
742
|
+
|
|
743
|
+
```python
|
|
744
|
+
from unittest.mock import patch
|
|
745
|
+
|
|
746
|
+
@patch("common.http_get")
|
|
747
|
+
def test_with_mock(mock_get):
|
|
748
|
+
mock_get.return_value = b'...'
|
|
749
|
+
result = get_quote("sh600989")
|
|
750
|
+
...
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
> **版本说明**:v1.1.0 新增 common 包、config 包、增强测试框架。
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API 层 - 面向用户/CLI 的统一入口。
|
|
3
|
+
|
|
4
|
+
模块:
|
|
5
|
+
- quote_cli: 行情查询 CLI
|
|
6
|
+
- kline_cli: K线查询 CLI
|
|
7
|
+
- screener_cli: 选股 CLI
|
|
8
|
+
- backtest_cli: 回测 CLI
|
|
9
|
+
|
|
10
|
+
使用方法:
|
|
11
|
+
from api import quote_cli, screener_cli
|
|
12
|
+
|
|
13
|
+
# 命令行调用
|
|
14
|
+
python -m api.quote_cli sh600989
|
|
15
|
+
"""
|
|
16
|
+
from .quote_cli import main as quote_main
|
|
17
|
+
from .screener_cli import main as screener_main
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"quote_main",
|
|
21
|
+
"screener_main",
|
|
22
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
行情查询 CLI (API 层)。
|
|
4
|
+
|
|
5
|
+
重构自 scripts/quote.py,将用户交互逻辑与业务逻辑分离。
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
12
|
+
|
|
13
|
+
from common import (
|
|
14
|
+
split_codes,
|
|
15
|
+
batchify,
|
|
16
|
+
normalize_quote_code,
|
|
17
|
+
parallel_map,
|
|
18
|
+
err,
|
|
19
|
+
DataError,
|
|
20
|
+
format_error,
|
|
21
|
+
)
|
|
22
|
+
from common.validators import validate_code
|
|
23
|
+
from data import get_quote, get_quotes
|
|
24
|
+
from common.exceptions import ValidationError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fetch_batch(codes: list, use_cache: bool = True) -> list:
|
|
28
|
+
"""批量获取行情,返回 dict 列表。"""
|
|
29
|
+
quotes = get_quotes(codes, use_cache=use_cache)
|
|
30
|
+
return [q.to_dict() for q in quotes]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_table(records: list):
|
|
34
|
+
"""渲染表格输出。"""
|
|
35
|
+
if not records:
|
|
36
|
+
print("(无数据)")
|
|
37
|
+
return
|
|
38
|
+
print(f"{'代码':<10} {'名称':<10} {'现价':>8} {'涨跌%':>7} {'PE':>7} {'换手%':>6} {'市值亿':>8}")
|
|
39
|
+
print("-" * 60)
|
|
40
|
+
for r in records:
|
|
41
|
+
print(f"{r['code']:<10} {r['name']:<10} {r['price']:>8} {r['change_pct']:>7} {r['pe']:>7} {r['turnover']:>6} {r['total_cap']:>8}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
"""主入口。"""
|
|
46
|
+
if len(sys.argv) < 2:
|
|
47
|
+
err("用法: python -m api.quote_cli <代码|@文件> [-j] [--sources]")
|
|
48
|
+
|
|
49
|
+
args = sys.argv[1:]
|
|
50
|
+
|
|
51
|
+
if "--sources" in args:
|
|
52
|
+
from fetchers import get_quote_fetchers
|
|
53
|
+
fetchers = get_quote_fetchers()
|
|
54
|
+
print("可用行情数据源:")
|
|
55
|
+
for f in fetchers:
|
|
56
|
+
print(f" - {f.name} (优先级 {f.priority})")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
json_mode = "-j" in args
|
|
60
|
+
args = [a for a in args if a not in ("-j", "--sources")]
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# 验证输入
|
|
64
|
+
raw_codes = split_codes(args[0])
|
|
65
|
+
codes = []
|
|
66
|
+
for c in raw_codes:
|
|
67
|
+
if not validate_code(c):
|
|
68
|
+
raise ValidationError("code", c, "格式无效")
|
|
69
|
+
codes.append(normalize_quote_code(c))
|
|
70
|
+
|
|
71
|
+
if not codes:
|
|
72
|
+
err("未提供代码")
|
|
73
|
+
|
|
74
|
+
# 分批查询 (腾讯单次 ≤15)
|
|
75
|
+
batches = list(batchify(codes, 15))
|
|
76
|
+
if len(batches) > 1:
|
|
77
|
+
results = parallel_map(
|
|
78
|
+
lambda b: fetch_batch(b, use_cache=True),
|
|
79
|
+
batches,
|
|
80
|
+
max_workers=4,
|
|
81
|
+
timeout=30
|
|
82
|
+
)
|
|
83
|
+
all_records = []
|
|
84
|
+
for batch in batches:
|
|
85
|
+
all_records.extend(results.get(batch, []))
|
|
86
|
+
else:
|
|
87
|
+
all_records = fetch_batch(batches[0])
|
|
88
|
+
|
|
89
|
+
if json_mode:
|
|
90
|
+
print(json.dumps(all_records, ensure_ascii=False, indent=2))
|
|
91
|
+
else:
|
|
92
|
+
render_table(all_records)
|
|
93
|
+
|
|
94
|
+
except ValidationError as e:
|
|
95
|
+
print(f"❌ 输入错误: {e.message}", file=sys.stderr)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
except DataError as e:
|
|
98
|
+
print(f"❌ 数据错误: {format_error(e)}", file=sys.stderr)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"❌ 系统错误: {format_error(e)}", file=sys.stderr)
|
|
102
|
+
sys.exit(2)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
选股 CLI (API 层)。
|
|
4
|
+
|
|
5
|
+
重构自 scripts/screener.py,将用户交互逻辑与业务逻辑分离。
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import argparse
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
13
|
+
|
|
14
|
+
from common import DATA_DIR, to_float
|
|
15
|
+
from data import get_quote, get_quotes
|
|
16
|
+
from common.exceptions import ValidationError, format_error
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_universe(sector=None, codes=None):
|
|
20
|
+
"""加载股票池。"""
|
|
21
|
+
import json
|
|
22
|
+
|
|
23
|
+
if codes:
|
|
24
|
+
return sorted({normalize_quote_code(c) for c in codes})
|
|
25
|
+
|
|
26
|
+
path = DATA_DIR / "sector_stocks.json"
|
|
27
|
+
sectors = json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
|
|
29
|
+
if sector:
|
|
30
|
+
matched = []
|
|
31
|
+
for name, items in sectors.items():
|
|
32
|
+
if sector.lower() in name.lower():
|
|
33
|
+
matched.extend(items)
|
|
34
|
+
if not matched:
|
|
35
|
+
raise SystemExit(f"未在内置标的库找到板块: {sector}")
|
|
36
|
+
return sorted({normalize_quote_code(c) for c in matched})
|
|
37
|
+
|
|
38
|
+
all_codes = []
|
|
39
|
+
for items in sectors.values():
|
|
40
|
+
all_codes.extend(items)
|
|
41
|
+
return sorted({normalize_quote_code(c) for c in all_codes})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def normalize_quote_code(code: str) -> str:
|
|
45
|
+
"""标准化股票代码。"""
|
|
46
|
+
c = code.strip().lower()
|
|
47
|
+
if c.startswith(("sh", "sz", "bj")):
|
|
48
|
+
return c
|
|
49
|
+
|
|
50
|
+
# 纯数字
|
|
51
|
+
digits = "".join(filter(str.isdigit, c))
|
|
52
|
+
if len(digits) != 6:
|
|
53
|
+
raise ValidationError("code", code, "必须是6位数字")
|
|
54
|
+
|
|
55
|
+
if digits.startswith(("60", "68", "51", "56", "58")):
|
|
56
|
+
return f"sh{digits}"
|
|
57
|
+
return f"sz{digits}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_finance_code(code: str) -> str:
|
|
61
|
+
"""标准化财务代码。"""
|
|
62
|
+
q = normalize_quote_code(code)
|
|
63
|
+
return q[:2].upper() + q[2:] if len(q) >= 8 else q.upper()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def render(rows, strategy, top):
|
|
67
|
+
"""渲染结果。"""
|
|
68
|
+
from strategies import STRATEGIES
|
|
69
|
+
|
|
70
|
+
accepted = [r for r in rows if not r.get("rejected")]
|
|
71
|
+
accepted.sort(key=lambda r: r.get("score", 0), reverse=True)
|
|
72
|
+
|
|
73
|
+
print(f"策略: {STRATEGIES[strategy]['label']} ({strategy})")
|
|
74
|
+
print(f"入选: {len(accepted)} | 剔除: {len(rows) - len(accepted)}")
|
|
75
|
+
print()
|
|
76
|
+
|
|
77
|
+
header = "排名 | 代码 | 名称 | 行业 | 板块 | 总分 | 质量 | 估值 | 动量 | 流动性 | PE | ROE | RSI | 20日% | 趋势"
|
|
78
|
+
print(header)
|
|
79
|
+
print("-" * len(header))
|
|
80
|
+
|
|
81
|
+
for idx, r in enumerate(accepted[:top], 1):
|
|
82
|
+
print(
|
|
83
|
+
f"{idx:>2} | {r['code']:<8} | {r['name']:<8} | {r.get('industry', '默认'):<4} | "
|
|
84
|
+
f"{r.get('board', '主板'):<4} | {r.get('score', 0):>5} | {r.get('quality', 0):>5} | "
|
|
85
|
+
f"{r.get('valuation', 0):>5} | {r.get('momentum', 0):>5} | {r.get('liquidity', 0):>6} | "
|
|
86
|
+
f"{r.get('pe', '-'):>6} | {r.get('roe', '-'):>6} | {r.get('rsi', 50):>4} | "
|
|
87
|
+
f"{r.get('ret20', 0):>5} | {r.get('trend', '震荡')}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
"""主入口。"""
|
|
93
|
+
parser = argparse.ArgumentParser(description="A 股多因子选股器")
|
|
94
|
+
parser.add_argument("--strategy", default="balanced")
|
|
95
|
+
parser.add_argument("--sector", help="板块名称")
|
|
96
|
+
parser.add_argument("--codes", help="逗号分隔代码列表")
|
|
97
|
+
parser.add_argument("--top", type=int, default=10)
|
|
98
|
+
parser.add_argument("--min-amount", type=float, default=5000)
|
|
99
|
+
parser.add_argument("--min-cap", type=float, default=40)
|
|
100
|
+
parser.add_argument("--exclude-loss", action="store_true")
|
|
101
|
+
parser.add_argument("-j", "--json", action="store_true")
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# 加载股票池
|
|
106
|
+
codes = load_universe(args.sector, args.codes.split(",") if args.codes else None)
|
|
107
|
+
|
|
108
|
+
# 获取行情
|
|
109
|
+
quotes = get_quotes(codes)
|
|
110
|
+
quote_dict = {q.code: q.to_dict() for q in quotes}
|
|
111
|
+
|
|
112
|
+
# TODO: 调用业务层筛选
|
|
113
|
+
# 暂时简单返回行情数据
|
|
114
|
+
rows = []
|
|
115
|
+
for code in codes[:args.top]:
|
|
116
|
+
if code in quote_dict:
|
|
117
|
+
q = quote_dict[code]
|
|
118
|
+
rows.append({
|
|
119
|
+
"code": code,
|
|
120
|
+
"name": q.get("name", ""),
|
|
121
|
+
"score": 0,
|
|
122
|
+
"quality": 0,
|
|
123
|
+
"valuation": 0,
|
|
124
|
+
"momentum": 0,
|
|
125
|
+
"liquidity": 0,
|
|
126
|
+
"pe": q.get("pe", "-"),
|
|
127
|
+
"roe": "-",
|
|
128
|
+
"rsi": 50,
|
|
129
|
+
"ret20": 0,
|
|
130
|
+
"trend": "震荡",
|
|
131
|
+
"industry": "默认",
|
|
132
|
+
"board": "主板",
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if args.json:
|
|
136
|
+
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
|
137
|
+
else:
|
|
138
|
+
render(rows, args.strategy, args.top)
|
|
139
|
+
|
|
140
|
+
except ValidationError as e:
|
|
141
|
+
print(f"❌ 输入错误: {e.message}", file=sys.stderr)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"❌ 系统错误: {format_error(e)}", file=sys.stderr)
|
|
145
|
+
sys.exit(2)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Business 层 - 业务逻辑聚合。
|
|
3
|
+
|
|
4
|
+
模块:
|
|
5
|
+
- stock_analysis: 个股分析业务流程
|
|
6
|
+
- screening_service: 选股服务
|
|
7
|
+
- backtest_service: 回测服务
|
|
8
|
+
"""
|
|
9
|
+
from .stock_analysis import StockAnalysisService
|
|
10
|
+
from .screening_service import ScreeningService
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"StockAnalysisService",
|
|
14
|
+
"ScreeningService",
|
|
15
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|