Files
big-qmt/labs/analysis/etf/verify_picks.py
2026-09-21 12:38:13 +08:00

89 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""最终挑选校验:对每个板块的候选做"策略实际窗口(最近 120 根)内是否有价格断层"检查。
接口的 OHLC 不做拆分/份额折算复原(``pre_close`` 已折算、价格未折算),
若断层落在最近 120 根内MA60/ATR 会被污染,网格锚点会算错 → 必须避开。
用法: py -3.14 -B labs/analysis/etf/verify_picks.py
"""
import json
import sys
import urllib.request
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from backtest import CACHE, DAILY_URL, WARMUP # noqa: E402
# 板块 -> (首选, [替补...])
PLAN = {
"AI": ("159819.SZ", ["515070.SH", "159363.SZ"]),
"CPO/通信": ("515880.SH", ["515050.SH", "159994.SZ", "159583.SZ", "159695.SZ"]),
"PCB": ("159732.SZ", ["562950.SH", "159997.SZ", "561100.SH"]),
"人形机器人": ("562500.SH", ["159530.SZ", "159770.SZ", "159272.SZ"]),
"光/光伏": ("515790.SH", ["159755.SZ", "516160.SH", "561910.SH"]),
"创新药": ("159992.SZ", ["159570.SZ", "515120.SH", "512010.SH"]),
"半导体": ("588200.SH", ["159516.SZ", "588170.SH", "159995.SZ", "512480.SH"]),
"存储": ("588750.SH", ["588290.SH", "589130.SH", "588890.SH"]),
"半导体材料": ("588160.SH", ["588010.SH", "589510.SH", "159761.SZ"]),
"玻璃基板": ("159745.SZ", ["588010.SH", "159763.SZ"]),
"电力": ("159326.SZ", ["159611.SZ", "159625.SZ", "561560.SH"]),
"航空航天": ("159227.SZ", ["512660.SH", "159267.SZ", "512710.SH"]),
"金属": ("518880.SH", ["512400.SH", "159934.SZ"]),
"金融": ("512880.SH", ["512070.SH", "512800.SH", "159841.SZ"]),
"能源": ("515220.SH", ["561360.SH", "159518.SZ"]),
"宽基(保留)": ("510300.SH", []),
}
def load(code: str) -> list[dict]:
path = CACHE / f"{code}.json"
if path.exists():
rows = json.loads(path.read_text(encoding="utf-8"))
else:
request = urllib.request.Request(f"{DAILY_URL}?code={code}",
headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(request, timeout=30) as response:
rows = json.load(response)
CACHE.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(rows), encoding="utf-8")
return sorted(rows, key=lambda r: str(r["trade_date"]))
def report(code: str) -> tuple[bool, str]:
"""返回 (窗口内是否干净, 说明)。窗口 = 最近 120 根(策略指标只用这段)。"""
rows = load(code)
window = rows[-120:] if len(rows) >= 120 else rows
worst, worst_day = 0.0, ""
for prev, cur in zip(window, window[1:]):
a, b = float(prev["close"]), float(cur["close"])
if a > 0:
change = abs(b / a - 1) * 100
if change > worst:
worst, worst_day = change, str(cur["trade_date"])
clean = worst <= 15
note = "干净" if clean else f"窗口内{worst_day}跳空 {worst:.1f}%"
closes = [float(r["close"]) for r in window]
return clean, f"{note} 最近120根 {min(closes):.3f}~{max(closes):.3f} 最新 {closes[-1]:.3f}"
print(f"{'板块':12} {'首选':11} {'窗口干净':>8} 说明")
picks = {}
for sector, (first, alts) in PLAN.items():
for code in [first, *alts]:
clean, note = report(code)
tag = "★首选" if code == first else " 替补"
print(f"{sector:12} {code:11} {str(clean):>8} {tag} {note}")
if code == first:
picks[sector] = (code, clean)
print()
print("== 首选中有断层的板块 ==")
for sector, (code, clean) in picks.items():
if not clean:
print(f" {sector}: {code}")