Files
Chan/research/download_oos.py
T
jackyu66gitandCursor 66061f79a1 research: 低滞后信号口径定稿与实盘前偏差审计
fast_bsp3 改用 tol=-1 + require_touch=False,信号滞后从 5.8 根降到 2.2 根。
滞后与收益严格单调(年化 370% -> 906%,同一份数据同一套成本),
这是本轮提升的主因,也意味着实盘延迟会直接侵蚀收益。

新增 step31~39 验证策略能否落地:
- 跨品种样本外——8 个未参与调参的币,PF 2.73 / t 28.5,无一为负
- 时点重建——只喂到信号那一根重算,同根命中 100%,确认无未来函数;
  1m 在 2000 根窗口即饱和,计算耗时 0.20s
- 偏差审计——多空对称、中枢生效时刻零回退、滑点稳健至 30bp、持仓几乎不重叠
- 消融——alpha 来自缠论中枢的上下文定位,而非「收盘转强」这个触发动作

补 research/HANDOFF.md:记录确切口径与参数、已排除的偏差、
已验证无效因而不必重做的方向,以及下一步用影子交易器实测执行滑点的方案。

清理 step1~20 的输出:早期方法论已被推翻(存在未来函数偏差),
其结论不再被引用;脚本保留,需要时可重跑。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 17:47:41 +08:00

95 lines
3.7 KiB
Python

"""下载样本外验证用的新币种数据,存成与 freqtrade 一致的 feather。
现有结论全部建立在 BTC/ETH/SOL 三个币上,参数(级别对、tol、SL/TP)也是
在这三个币上挑的,存在选择偏差。本脚本补齐一批完全没参与过调参的品种。
只拉 30m/2h:那是实测最强的一对,用它做样本外足够,且请求量只有全级别的四成。
限速要点(踩过的坑):
klines(limit=1500) 权重 30,上限 2400/分钟 = 80 次/分钟 = 0.75s/次,
0.8s 间隔正好卡在边缘,一旦触发 429,短退避跨不过计数窗口就会连续失败。
故间隔放到 1.3s、权重阈值压到 1400、429 时等满一个窗口。
另外分页失败不再丢弃整只币,已抓到的部分照样落盘。
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import pandas as pd
import requests
OUT = Path(__file__).resolve().parents[1] / "data" / "binance" / "futures"
BASE = "https://fapi.binance.com/fapi/v1/klines"
PROXY = {"http": "http://127.0.0.1:7897", "https": "http://127.0.0.1:7897"}
SYMBOLS = ["BNB", "XRP", "DOGE", "ADA", "AVAX", "LINK", "LTC", "TRX"]
TFS = ["2h", "30m"]
START_MS = int(pd.Timestamp("2019-01-01", tz="UTC").timestamp() * 1000)
COLS = ["date", "open", "high", "low", "close", "volume"]
GAP = 1.3
WEIGHT_CAP = 1400
def fetch(sess: requests.Session, symbol: str, tf: str) -> pd.DataFrame | None:
rows: list[list] = []
cur = START_MS
while True:
batch = None
for attempt in range(6):
try:
r = sess.get(BASE, params={"symbol": f"{symbol}USDT", "interval": tf,
"startTime": cur, "limit": 1500}, timeout=40)
if r.status_code in (418, 429):
time.sleep(65)
continue
if r.status_code == 400:
return None
r.raise_for_status()
batch = r.json()
if int(r.headers.get("X-MBX-USED-WEIGHT-1M", 0) or 0) > WEIGHT_CAP:
time.sleep(40)
break
except Exception:
time.sleep(5 * (attempt + 1))
if not batch:
break # 抓不动或抓完了,保留已有部分
rows.extend(batch)
nxt = int(batch[-1][0]) + 1
if nxt <= cur or len(batch) < 1500:
break
cur = nxt
time.sleep(GAP)
if len(rows) < 1000:
return None
df = pd.DataFrame(rows).iloc[:, :6]
df.columns = ["ts", "open", "high", "low", "close", "volume"]
df["date"] = pd.to_datetime(pd.to_numeric(df["ts"]), unit="ms", utc=True)
for c in ("open", "high", "low", "close", "volume"):
df[c] = pd.to_numeric(df[c])
return df[COLS].drop_duplicates("date").sort_values("date").reset_index(drop=True)
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
sess = requests.Session()
sess.proxies.update(PROXY)
jobs = [(s, tf) for tf in TFS for s in SYMBOLS]
print(f"[下载] {len(jobs)} 个任务,单线程 {GAP}s 间隔", flush=True)
for i, (sym, tf) in enumerate(jobs, 1):
path = OUT / f"{sym}_USDT_USDT-{tf}-futures.feather"
if path.exists():
print(f" [{i}/{len(jobs)}] {sym} {tf} 已存在", flush=True)
continue
df = fetch(sess, sym, tf)
if df is None:
print(f" [{i}/{len(jobs)}] {sym} {tf} 失败", flush=True)
continue
df.to_feather(path)
print(f" [{i}/{len(jobs)}] {sym} {tf} {len(df)} 根 "
f"{df['date'].min():%Y-%m-%d}~{df['date'].max():%Y-%m-%d}", flush=True)
if __name__ == "__main__":
sys.exit(main())