| |
| """FIXED downloader for Binance futures funding rates + depth + equities. |
| |
| Fixes: |
| 1. Funding rates: use Binance REST API (works for any historical date) |
| 2. Depth: try both 'depth' and 'depthBookToTick' paths + monthly fallback |
| 3. Equities: auto-install yfinance if missing |
| |
| Usage: |
| python scripts/download_fixed.py --out data/more/ |
| """ |
| import argparse |
| import json |
| import logging |
| import subprocess |
| import sys |
| import urllib.request |
| from pathlib import Path |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
|
|
| def download_file(url: str, out_path: Path, timeout: int = 120) -> bool: |
| if out_path.exists() and out_path.stat().st_size > 0: |
| logger.info(" SKIP (exists): %s", out_path.name) |
| return True |
| try: |
| logger.info(" GET %s", url) |
| req = urllib.request.Request(url, headers={"User-Agent": "flash-crash-watchdog/0.4"}) |
| with urllib.request.urlopen(req, timeout=timeout) as response: |
| data = response.read() |
| out_path.parent.mkdir(parents=True, exist_ok=True) |
| out_path.write_bytes(data) |
| size_mb = len(data) / (1024 * 1024) |
| logger.info(" β %s (%.1f MB)", out_path.name, size_mb) |
| return True |
| except Exception as e: |
| logger.error(" β %s", e) |
| return False |
|
|
|
|
| def download_json(url: str, out_path: Path, timeout: int = 30) -> bool: |
| """Download JSON from an API endpoint.""" |
| if out_path.exists() and out_path.stat().st_size > 0: |
| logger.info(" SKIP (exists): %s", out_path.name) |
| return True |
| try: |
| logger.info(" GET %s", url) |
| req = urllib.request.Request(url, headers={"User-Agent": "flash-crash-watchdog/0.4"}) |
| with urllib.request.urlopen(req, timeout=timeout) as response: |
| data = json.loads(response.read()) |
| out_path.parent.mkdir(parents=True, exist_ok=True) |
| out_path.write_text(json.dumps(data, indent=2)) |
| logger.info(" β %s (%d records)", out_path.name, len(data) if isinstance(data, list) else 1) |
| return True |
| except Exception as e: |
| logger.error(" β %s", e) |
| return False |
|
|
|
|
| |
| |
| |
| def download_funding_rates_api(out_dir: Path) -> int: |
| """Download funding rates via Binance Futures REST API. |
| |
| API: GET /fapi/v1/fundingRate |
| Params: symbol, startTime, endTime, limit (max 1000) |
| """ |
| logger.info("=" * 70) |
| logger.info("1. BINANCE FUNDING RATES (via REST API)") |
| logger.info(" Inverted funding = extreme short pressure (crypto crash signal)") |
| logger.info("=" * 70) |
|
|
| |
| import datetime |
| crash_periods = { |
| "2021-05-19": ("2021-05-19", "2021-05-20", "May 2021 BTC crash"), |
| "2022-05-10": ("2022-05-09", "2022-05-13", "May 2022 LUNA crash"), |
| "2022-06-13": ("2022-06-13", "2022-06-14", "Celsius freeze"), |
| "2024-08-05": ("2024-08-05", "2024-08-06", "Carry trade unwind"), |
| } |
| symbols = ["BTCUSDT", "ETHUSDT"] |
|
|
| success = 0 |
| total = 0 |
| for symbol in symbols: |
| for date_key, (start, end, desc) in crash_periods.items(): |
| total += 1 |
| start_ts = int(datetime.datetime.strptime(start, "%Y-%m-%d").timestamp() * 1000) |
| end_ts = int(datetime.datetime.strptime(end, "%Y-%m-%d").timestamp() * 1000) |
| url = ( |
| f"https://fapi.binance.com/fapi/v1/fundingRate" |
| f"?symbol={symbol}&startTime={start_ts}&endTime={end_ts}&limit=1000" |
| ) |
| out = out_dir / f"{symbol}-funding-{date_key}.json" |
| logger.info(" %s %s β %s", symbol, date_key, desc) |
| if download_json(url, out): |
| success += 1 |
| logger.info(" Result: %d/%d files\n", success, total) |
| return success |
|
|
|
|
| |
| |
| |
| def download_futures_metrics_api(out_dir: Path) -> int: |
| """Download open interest history + long/short ratio via Binance API.""" |
| logger.info("=" * 70) |
| logger.info("2. BINANCE FUTURES METRICS (open interest + long/short ratio)") |
| logger.info("=" * 70) |
|
|
| import datetime |
| success = 0 |
| total = 0 |
|
|
| |
| |
| for symbol in ["BTCUSDT", "ETHUSDT"]: |
| total += 1 |
| url = f"https://fapi.binance.com/futures/data/openInterestHist?symbol={symbol}&period=15m&limit=1000" |
| out = out_dir / f"{symbol}-open-interest-recent.json" |
| if download_json(url, out): |
| success += 1 |
|
|
| |
| total += 1 |
| url = f"https://fapi.binance.com/futures/data/topLongShortAccountRatio?symbol={symbol}&period=15m&limit=1000" |
| out = out_dir / f"{symbol}-longshort-ratio-recent.json" |
| if download_json(url, out): |
| success += 1 |
|
|
| |
| total += 1 |
| url = f"https://fapi.binance.com/futures/data/takerlongshortRatio?symbol={symbol}&period=15m&limit=1000" |
| out = out_dir / f"{symbol}-taker-volume-recent.json" |
| if download_json(url, out): |
| success += 1 |
|
|
| logger.info(" Result: %d/%d files\n", success, total) |
| return success |
|
|
|
|
| |
| |
| |
| def download_depth_multi(out_dir: Path) -> int: |
| """Try multiple URL patterns for Binance depth data.""" |
| logger.info("=" * 70) |
| logger.info("3. BINANCE DEPTH SNAPSHOTS (trying multiple URL patterns)") |
| logger.info("=" * 70) |
|
|
| dates = ["2021-05-19", "2022-05-10", "2024-08-05"] |
| success = 0 |
| total = 0 |
|
|
| for date in dates: |
| total += 1 |
| year, month, day = date.split("-") |
| |
| urls = [ |
| |
| f"https://data.binance.vision/data/spot/daily/depthBookToTick/BTCUSDT/BTCUSDT-depthBookToTick-{date}.zip", |
| |
| f"https://data.binance.vision/data/spot/daily/depth/BTCUSDT/BTCUSDT-depth-{date}.zip", |
| |
| f"https://data.binance.vision/data/spot/monthly/depthBookToTick/BTCUSDT/BTCUSDT-depthBookToTick-{year}-{month}.zip", |
| |
| f"https://data.binance.vision/data/spot/monthly/depth/BTCUSDT/BTCUSDT-depth-{year}-{month}.zip", |
| ] |
| downloaded = False |
| for i, url in enumerate(urls): |
| out = out_dir / f"BTCUSDT-depth-{date}.zip" |
| if download_file(url, out): |
| success += 1 |
| downloaded = True |
| break |
| if not downloaded: |
| logger.warning(" β No depth data available for %s (tried 4 URL patterns)", date) |
| logger.info(" Result: %d/%d files\n", success, total) |
| return success |
|
|
|
|
| |
| |
| |
| def ensure_yfinance() -> bool: |
| """Ensure yfinance is installed. Returns True if available.""" |
| try: |
| import yfinance |
| return True |
| except ImportError: |
| logger.info(" yfinance not installed. Installing...") |
| try: |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "yfinance", "--quiet"]) |
| logger.info(" β yfinance installed") |
| return True |
| except Exception as e: |
| logger.error(" β Failed to install yfinance: %s", e) |
| logger.error(" Run manually: pip install yfinance") |
| return False |
|
|
|
|
| def download_equities(out_dir: Path) -> int: |
| """Download US equity + VIX data for flash-crash dates via yfinance.""" |
| logger.info("=" * 70) |
| logger.info("4. US EQUITY DATA (SPY, QQQ, VIX, XLF, XLE via yfinance)") |
| logger.info(" The May 6, 2010 US equities flash crash β the canonical case") |
| logger.info("=" * 70) |
|
|
| if not ensure_yfinance(): |
| return 0 |
|
|
| import yfinance as yf |
|
|
| tickers = ["SPY", "QQQ", "^VIX", "XLF", "XLE"] |
| periods = { |
| "2010-05-06": "May 6, 2010 US equities flash crash (Dow -9.2% in 36 min)", |
| "2015-08-24": "Aug 24, 2015 flash crash (China devaluation)", |
| "2020-03-12": "March 12, 2020 COVID crash", |
| "2024-08-05": "Aug 5, 2024 carry-trade unwind", |
| } |
|
|
| out_dir.mkdir(parents=True, exist_ok=True) |
| success = 0 |
| total = 0 |
| for ticker_symbol in tickers: |
| for date, desc in periods.items(): |
| total += 1 |
| logger.info(" %s %s β %s", ticker_symbol, date, desc) |
| |
| start = date |
| year, month, day = map(int, date.split("-")) |
| |
| from datetime import datetime, timedelta |
| end_dt = datetime(year, month, day) + timedelta(days=5) |
| end = end_dt.strftime("%Y-%m-%d") |
| try: |
| ticker = yf.Ticker(ticker_symbol) |
| |
| hist = ticker.history(start=start, end=end, interval="1m") |
| if hist.empty: |
| hist = ticker.history(start=start, end=end, interval="5m") |
| if hist.empty: |
| hist = ticker.history(start=start, end=end, interval="1h") |
| if hist.empty: |
| hist = ticker.history(start=start, end=end, interval="1d") |
| if not hist.empty: |
| out = out_dir / f"YFINANCE-{ticker_symbol.replace('^','')}-{date}.csv" |
| hist.to_csv(out) |
| size_kb = out.stat().st_size / 1024 |
| logger.info(" β %s (%.1f KB, %d bars)", out.name, size_kb, len(hist)) |
| success += 1 |
| else: |
| logger.warning(" β No data for %s on %s", ticker_symbol, date) |
| except Exception as e: |
| logger.error(" β %s: %s", ticker_symbol, e) |
| logger.info(" Result: %d/%d files\n", success, total) |
| return success |
|
|
|
|
| |
| |
| |
| def download_bybit(out_dir: Path) -> int: |
| """Download Bybit historical 1-min klines (cross-exchange).""" |
| logger.info("=" * 70) |
| logger.info("5. BYBIT HISTORICAL KLINES (cross-exchange β venue #2)") |
| logger.info("=" * 70) |
| dates = ["2021-05-19", "2022-05-10", "2024-08-05"] |
| symbols = ["BTCUSDT", "ETHUSDT"] |
| success = 0 |
| total = 0 |
| for symbol in symbols: |
| for date in dates: |
| total += 1 |
| url = f"https://public.bybit.com/kline/{symbol}/{date}/1min.csv.gz" |
| out = out_dir / f"BYBIT-{symbol}-1min-{date}.csv.gz" |
| if download_file(url, out): |
| success += 1 |
| logger.info(" Result: %d/%d files\n", success, total) |
| return success |
|
|
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser(description="Download fixed datasets (funding API + depth + equities)") |
| parser.add_argument("--out", default="data/more/", help="Output directory") |
| parser.add_argument("--only", default=None, |
| help="Comma-separated: funding,metrics,depth,equities,bybit") |
| args = parser.parse_args() |
|
|
| out_dir = Path(args.out) |
| only = set(args.only.split(",")) if args.only else None |
|
|
| total = 0 |
| if only is None or "funding" in only: |
| total += download_funding_rates_api(out_dir / "futures") |
| if only is None or "metrics" in only: |
| total += download_futures_metrics_api(out_dir / "metrics") |
| if only is None or "depth" in only: |
| total += download_depth_multi(out_dir / "depth") |
| if only is None or "equities" in only: |
| total += download_equities(out_dir / "equities") |
| if only is None or "bybit" in only: |
| total += download_bybit(out_dir / "bybit") |
|
|
| logger.info("=" * 70) |
| logger.info(" DOWNLOAD COMPLETE β %d files total", total) |
| logger.info(" Output: %s", out_dir.resolve()) |
| logger.info("=" * 70) |
| return 0 if total > 0 else 1 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|