This commit is contained in:
2026-09-21 12:38:13 +08:00
parent bcacbed427
commit ed6d4ee7a5
14 changed files with 2458 additions and 1073 deletions

View File

@@ -0,0 +1,88 @@
"""最终挑选校验:对每个板块的候选做"策略实际窗口(最近 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}")