mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
feat(screen): v1.15.0 — 强势股排名 + 修复证券类型识别与名称分批查询
新增:强势股排名(screen strength) - 全市场按 5/20/60 日涨幅加权合成强势分,纯离线扫描 - 三种预设:steady(稳健)/breakout(妖股)/balanced(均衡) - CLI: easy-tdx screen strength --preset steady --top 50 --table - Web API: GET /api/v1/market/strength - 支持自定义权重、成交额过滤、并发扫描 修复: - _detect_security_type 代码段不全,ETF/基金/科创板/逆回购被误判为 A 股 - screen strength/rank 名称补齐超 80 只时末尾被丢弃(分批查询) 详见 CHANGELOG.md
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# 23. 强势股排名(screen strength)
|
||||
|
||||
按 **5 / 20 / 60 日涨幅加权**合成强势分,从全市场选出"最近最强"的股票。
|
||||
|
||||
## 三种预设模式
|
||||
|
||||
| 模式 | 性格 | 适合 |
|
||||
|------|------|------|
|
||||
| `steady` | 中长期稳健(60日主导 + 波动率惩罚) | 选"稳着涨"的票 |
|
||||
| `breakout` | 近期妖股爆发(5日主导,纯涨幅) | 选"短期最猛"的票 |
|
||||
| `balanced` | 三周期均衡 + 波动率调整 | 不确定时的安全默认 |
|
||||
|
||||
## 前提条件
|
||||
|
||||
需要本地通达信 `.day` 日线数据(扫描纯离线,无网络请求):
|
||||
|
||||
```bash
|
||||
# 同步最新日线数据
|
||||
easy-tdx offline sync
|
||||
|
||||
# 或用通达信客户端下载日线数据到 vipdoc/{sh,sz}/lday/
|
||||
```
|
||||
|
||||
## 示例文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `strength_api.py` | Python API 调用(`StrengthRanker` 类) |
|
||||
| `strength_cli.sh` | CLI 命令示例(`easy-tdx screen strength`) |
|
||||
| `strength_web_api.py` | Web API 调用(`GET /api/v1/market/strength`) |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from easy_tdx.screen.strength import StrengthRanker
|
||||
|
||||
ranker = StrengthRanker(preset="steady")
|
||||
results = ranker.rank(top_n=20)
|
||||
for r in results[:5]:
|
||||
print(f"#{r.rank} {r.market}{r.code} 强势分={r.strength:.2f}")
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# 表格输出
|
||||
easy-tdx screen strength --preset steady --top 50 --table
|
||||
|
||||
# 近期妖股 + 补齐名称
|
||||
easy-tdx screen strength --preset breakout --top 20 --names --table
|
||||
|
||||
# 自定义权重(自动归一化)
|
||||
easy-tdx screen strength --w5 0.5 --w20 0.3 --w60 0.2 --top 30 --table
|
||||
```
|
||||
|
||||
### Web API
|
||||
|
||||
```bash
|
||||
# 启动服务
|
||||
easy-tdx serve
|
||||
|
||||
# 调用接口
|
||||
curl "http://localhost:8000/api/v1/market/strength?preset=breakout&top_n=20"
|
||||
```
|
||||
|
||||
## 输出字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `rank` | int | 排名 |
|
||||
| `code` | str | 6 位股票代码 |
|
||||
| `market` | str | 市场(SZ/SH) |
|
||||
| `name` | str | 股票名称(需 `--names` 开启) |
|
||||
| `last_close` | float | 最新收盘价 |
|
||||
| `last_date` | int | 数据截止日(YYYYMMDD) |
|
||||
| `ret_5` | float | 5 日涨幅 |
|
||||
| `ret_20` | float | 20 日涨幅 |
|
||||
| `ret_60` | float | 60 日涨幅 |
|
||||
| `vol_20` | float | 20 日波动率(对数收益率标准差) |
|
||||
| `strength` | float | 强势综合分(排序依据) |
|
||||
|
||||
## 公式
|
||||
|
||||
```
|
||||
ret_5 = close[-1] / close[-6] - 1
|
||||
ret_20 = close[-1] / close[-21] - 1
|
||||
ret_60 = close[-1] / close[-61] - 1
|
||||
vol_20 = std(log_return, 20)[-1]
|
||||
|
||||
strength = (w5·ret_5 + w20·ret_20 + w60·ret_60) / vol_20 # vol_adjusted=True
|
||||
strength = w5·ret_5 + w20·ret_20 + w60·ret_60 # vol_adjusted=False
|
||||
```
|
||||
|
||||
权重自动归一化:`w = w / (w5 + w20 + w60)`。
|
||||
@@ -0,0 +1,104 @@
|
||||
"""强势股排名 — Python API 示例。
|
||||
|
||||
本示例演示如何用 StrengthRanker 扫描全市场,按 5/20/60 日涨幅加权选出强势股。
|
||||
|
||||
运行前提:
|
||||
1. 本地安装通达信,且 vipdoc/{sh,sz}/lday/*.day 数据已同步(含最新交易日)。
|
||||
2. 可通过 easy-tdx offline sync 命令同步数据。
|
||||
3. pip install easy-tdx
|
||||
|
||||
运行方式:
|
||||
python examples/23_screen_strength/strength_api.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from easy_tdx.screen.strength import STRENGTH_PRESETS, StrengthRanker
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# ── 1. 查看所有预设模式 ──────────────────────────────────────────────
|
||||
print("=" * 60)
|
||||
print("可用预设模式:")
|
||||
print("=" * 60)
|
||||
for name, cfg in STRENGTH_PRESETS.items():
|
||||
print(f" {name:10} w5={cfg['w5']:.2f} w20={cfg['w20']:.2f} "
|
||||
f"w60={cfg['w60']:.2f} vol_adjusted={cfg['vol_adjusted']}")
|
||||
print(f" {cfg['desc']}")
|
||||
print()
|
||||
|
||||
# ── 2. steady 模式:中长期稳健强势 Top 20 ────────────────────────────
|
||||
print("=" * 60)
|
||||
print("[steady] 中长期稳健强势 Top 20")
|
||||
print("=" * 60)
|
||||
ranker = StrengthRanker(preset="steady")
|
||||
|
||||
# 进度回调(扫描 ~5000 只约 30-60 秒)
|
||||
def on_progress(current: int, total: int, name: str) -> None:
|
||||
if name == "done":
|
||||
print(f"\r扫描完成: {total} 只")
|
||||
else:
|
||||
pct = current * 100 // total if total > 0 else 0
|
||||
print(f"\r[{current}/{total}] {pct}% scanning {name}", end="")
|
||||
|
||||
results = ranker.rank(top_n=20, progress_callback=on_progress)
|
||||
data_date = results[0].last_date if results else 0
|
||||
print()
|
||||
print(ranker.to_table(results, "steady", data_date))
|
||||
print()
|
||||
|
||||
# ── 3. breakout 模式:近期妖股爆发 Top 10 ───────────────────────────
|
||||
print("=" * 60)
|
||||
print("[breakout] 近期妖股爆发 Top 10")
|
||||
print("=" * 60)
|
||||
breakout_ranker = StrengthRanker(preset="breakout")
|
||||
results = breakout_ranker.rank(top_n=10)
|
||||
data_date = results[0].last_date if results else 0
|
||||
print(breakout_ranker.to_table(results, "breakout", data_date))
|
||||
print()
|
||||
|
||||
# ── 4. 自定义权重 + 成交额过滤 ──────────────────────────────────────
|
||||
print("=" * 60)
|
||||
print("[自定义] 5:3:2 权重 + 日均成交额 ≥ 5000 万")
|
||||
print("=" * 60)
|
||||
custom_ranker = StrengthRanker(
|
||||
w5=0.5, w20=0.3, w60=0.2,
|
||||
vol_adjusted=False, # 纯加权涨幅
|
||||
min_amount=50_000_000, # 最近 5 日日均成交额 ≥ 5000 万
|
||||
)
|
||||
results = custom_ranker.rank(top_n=15)
|
||||
data_date = results[0].last_date if results else 0
|
||||
print(custom_ranker.to_table(results, "custom", data_date))
|
||||
print()
|
||||
|
||||
# ── 5. 并发扫描 + JSON 输出到文件 ───────────────────────────────────
|
||||
print("=" * 60)
|
||||
print("[并发] balanced 模式 + 4 进程 + 输出 JSON")
|
||||
print("=" * 60)
|
||||
parallel_ranker = StrengthRanker(preset="balanced")
|
||||
results = parallel_ranker.rank(
|
||||
top_n=50,
|
||||
workers=4, # 4 进程并发,速度提升约 4 倍
|
||||
progress_callback=on_progress,
|
||||
)
|
||||
data_date = results[0].last_date if results else 0
|
||||
json_str = parallel_ranker.to_json(results, "balanced", data_date)
|
||||
|
||||
output_file = "strength_balanced.json"
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(json_str)
|
||||
print(f"\n排名: {len(results)} 只 → {output_file}")
|
||||
|
||||
# ── 6. 编程式访问排名数据 ───────────────────────────────────────────
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("[编程式访问] 遍历前 5 名")
|
||||
print("=" * 60)
|
||||
for r in results[:5]:
|
||||
print(f" #{r.rank} {r.market}{r.code} 现价={r.last_close:.2f} "
|
||||
f"5日={r.ret_5:+.2%} 20日={r.ret_20:+.2%} 60日={r.ret_60:+.2%} "
|
||||
f"强势分={r.strength:.2f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# easy-tdx 强势股排名 — CLI 使用示例
|
||||
#
|
||||
# 前提:本地通达信 vipdoc/{sh,sz}/lday/*.day 已同步最新数据
|
||||
# 可用 `easy-tdx offline sync` 同步
|
||||
#
|
||||
# 三种预设模式:
|
||||
# steady — 中长期稳健(60日主导 + 波动率惩罚),选稳着涨的票
|
||||
# breakout — 近期妖股爆发(5日主导,纯涨幅),选最猛的票
|
||||
# balanced — 三周期均衡 + 波动率调整
|
||||
#
|
||||
# 用法:去掉命令前的 # 即可实际执行。
|
||||
|
||||
echo "================================================================"
|
||||
echo "1. steady 模式 — 中长期稳健强势 Top 50(表格输出)"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --preset steady --top 50 --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "2. breakout 模式 — 近期妖股爆发 Top 20(补齐股票名称)"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --preset breakout --top 20 --names --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "3. balanced 模式 — 三周期均衡 Top 30"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --preset balanced --top 30 --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "4. 自定义权重(自动归一化,5:3:2 = 0.5:0.3:0.2)"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --w5 0.5 --w20 0.3 --w60 0.2 --top 30 --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "5. 自定义权重 + 关闭波动率惩罚(纯加权涨幅)"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --w5 0.6 --w20 0.3 --w60 0.1 --no-vol-adjusted --top 20 --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "6. 并发扫描(4 进程,速度提升约 4 倍)"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --preset steady --top 100 --workers 4 --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "7. 过滤低流动性(最近 5 日日均成交额 ≥ 5000 万)"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --preset breakout --top 30 --min-amount 50000000 --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "8. 缩小范围(仅深圳)+ 输出到 JSON 文件"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --universe sz --top 30 --output sz_strength.json
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "9. 仅上海 + 最小上市天数 120 日(过滤次新股)"
|
||||
echo "================================================================"
|
||||
# easy-tdx screen strength --universe sh --min-listed-days 120 --top 30 --table
|
||||
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "10. 对比三种预设(同一批股票,不同视角)"
|
||||
echo "================================================================"
|
||||
echo "--- steady(稳健)---"
|
||||
# easy-tdx screen strength --preset steady --top 10 --table
|
||||
echo ""
|
||||
echo "--- breakout(妖股)---"
|
||||
# easy-tdx screen strength --preset breakout --top 10 --table
|
||||
echo ""
|
||||
echo "--- balanced(均衡)---"
|
||||
# easy-tdx screen strength --preset balanced --top 10 --table
|
||||
@@ -0,0 +1,112 @@
|
||||
"""强势股排名 — Web API 调用示例。
|
||||
|
||||
演示如何通过 HTTP 调用 easy-tdx 的 REST API 获取强势股排名。
|
||||
|
||||
前提:
|
||||
1. 启动 Web API 服务:easy-tdx serve --port 8000
|
||||
2. 本地 vipdoc 数据已同步(扫描依赖本地 .day 文件)
|
||||
3. pip install requests
|
||||
|
||||
运行方式:
|
||||
python examples/23_screen_strength/strength_web_api.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000/api/v1"
|
||||
|
||||
|
||||
def fetch_strength(
|
||||
preset: str = "steady",
|
||||
top_n: int = 20,
|
||||
universe: str = "all",
|
||||
min_amount: float = 0.0,
|
||||
) -> dict:
|
||||
"""调用 GET /market/strength 获取强势股排名。
|
||||
|
||||
Args:
|
||||
preset: 预设模式 steady / breakout / balanced
|
||||
top_n: 返回前 N 名
|
||||
universe: 范围 all / sh / sz
|
||||
min_amount: 日均成交额下限(元)
|
||||
|
||||
Returns:
|
||||
{"data": [...], "count": N}
|
||||
"""
|
||||
resp = requests.get(
|
||||
f"{BASE_URL}/market/strength",
|
||||
params={
|
||||
"preset": preset,
|
||||
"top_n": top_n,
|
||||
"universe": universe,
|
||||
"min_amount": min_amount,
|
||||
},
|
||||
timeout=120, # 扫描全市场可能需要 30-60 秒
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def fetch_strength_custom_weights(
|
||||
w5: float = 0.5,
|
||||
w20: float = 0.3,
|
||||
w60: float = 0.2,
|
||||
top_n: int = 30,
|
||||
) -> dict:
|
||||
"""自定义权重调用(覆盖预设)。"""
|
||||
resp = requests.get(
|
||||
f"{BASE_URL}/market/strength",
|
||||
params={
|
||||
"w5": w5, "w20": w20, "w60": w60,
|
||||
"top_n": top_n,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def print_ranking(result: dict, title: str) -> None:
|
||||
"""格式化打印排名结果。"""
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f" {title}")
|
||||
print(f"{'=' * 70}")
|
||||
data = result.get("data", [])
|
||||
if not data:
|
||||
print(" 无数据")
|
||||
return
|
||||
|
||||
print(f" {'排名':>4} {'代码':<10} {'现价':>10} "
|
||||
f"{'5日':>8} {'20日':>8} {'60日':>8} {'强势分':>8}")
|
||||
print(f" {'-' * 66}")
|
||||
for row in data:
|
||||
print(f" {row['rank']:>4} {row['market']}{row['code']:<9} "
|
||||
f"{row['last_close']:>9.2f} "
|
||||
f"{row['ret_5']:>7.2%} {row['ret_20']:>7.2%} "
|
||||
f"{row['ret_60']:>7.2%} {row['strength']:>8.2f}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# ── 1. steady 模式:中长期稳健 Top 20 ───────────────────────────────
|
||||
result = fetch_strength(preset="steady", top_n=20)
|
||||
print_ranking(result, "[steady] 中长期稳健强势 Top 20")
|
||||
|
||||
# ── 2. breakout 模式:近期妖股 Top 10 ───────────────────────────────
|
||||
result = fetch_strength(preset="breakout", top_n=10)
|
||||
print_ranking(result, "[breakout] 近期妖股爆发 Top 10")
|
||||
|
||||
# ── 3. 自定义权重 + 成交额过滤 ──────────────────────────────────────
|
||||
result = fetch_strength_custom_weights(w5=0.5, w20=0.3, w60=0.2, top_n=15)
|
||||
print_ranking(result, "[自定义 5:3:2] Top 15")
|
||||
|
||||
# ── 4. 过滤低流动性(日均成交额 ≥ 5000 万)─────────────────────────
|
||||
result = fetch_strength(
|
||||
preset="breakout", top_n=20, min_amount=50_000_000
|
||||
)
|
||||
print_ranking(result, "[breakout + 流动性过滤] Top 20")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user