feat etf
This commit is contained in:
88
labs/analysis/etf/verify_picks.py
Normal file
88
labs/analysis/etf/verify_picks.py
Normal 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}")
|
||||
Reference in New Issue
Block a user