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>
95 lines
3.7 KiB
Python
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())
|