From c2eab3581140770897aad938968f2b20a014602b Mon Sep 17 00:00:00 2001 From: BizzleBot Date: Thu, 19 Mar 2026 22:15:40 +0000 Subject: [PATCH] fix: use binanceus for data fetch (binance geo-blocked), add multi-exchange fallback --- __pycache__/orchestrator.cpython-313.pyc | Bin 0 -> 26481 bytes data/btc_1h.csv | 17521 +++++++++++++++++++++ data/btc_4h.csv | 4381 ++++++ scripts/fetch_data.py | 115 +- 4 files changed, 21954 insertions(+), 63 deletions(-) create mode 100644 __pycache__/orchestrator.cpython-313.pyc create mode 100644 data/btc_1h.csv create mode 100644 data/btc_4h.csv diff --git a/__pycache__/orchestrator.cpython-313.pyc b/__pycache__/orchestrator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..129455faa308e0de1c0ffe8d6577741b6ef94a8c GIT binary patch literal 26481 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 pd.DataFrame: - """Fetch OHLCV data for a given timeframe.""" - exchange = ccxt.binance({"enableRateLimit": True}) - - # Calculate start time +def fetch_ohlcv(timeframe): + """Try multiple exchanges until we get sufficient history.""" + exchanges = [ + ("coinbasepro", "BTC/USD"), + ("binanceus", "BTC/USDT"), + ("kraken", "BTC/USD"), + ("bitfinex", "BTC/USD"), + ] now_ms = int(time.time() * 1000) - if timeframe == "1h": - ms_per_candle = 3600 * 1000 - elif timeframe == "4h": - ms_per_candle = 4 * 3600 * 1000 - else: - raise ValueError(f"Unsupported timeframe: {timeframe}") + ms_map = {"1h": 3600_000, "4h": 14400_000} + ms_per = ms_map[timeframe] + target_since = now_ms - (YEARS * 365 * 24 * 3600 * 1000) - since = now_ms - (YEARS_HISTORY * 365 * 24 * 3600 * 1000) - - all_candles = [] - print(f" Fetching {SYMBOL} {timeframe} from {datetime.fromtimestamp(since / 1000, tz=timezone.utc).strftime('%Y-%m-%d')}...") - - while since < now_ms: + for exch_id, symbol in exchanges: + print(f" Trying {exch_id} for {symbol} {timeframe}...") try: - candles = exchange.fetch_ohlcv(SYMBOL, timeframe, since=since, limit=LIMIT_PER_REQUEST) + exchange = getattr(ccxt, exch_id)({"enableRateLimit": True}) + since = target_since + all_c = [] + fails = 0 + while since < now_ms and fails < 5: + try: + candles = exchange.fetch_ohlcv(symbol, timeframe, since=since, limit=LIMIT) + except Exception as e: + fails += 1 + print(f" Error ({fails}/5): {e}") + time.sleep(3) + continue + if not candles: + break + all_c.extend(candles) + since = candles[-1][0] + ms_per + print(f"\r {len(all_c)} candles...", end="", flush=True) + time.sleep(exchange.rateLimit / 1000 + 0.1) + print(f"\r {len(all_c)} candles from {exch_id}") + if len(all_c) > 2000: # Good enough + df = pd.DataFrame(all_c, columns=["timestamp","open","high","low","close","volume"]) + df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + return df.drop_duplicates(subset=["timestamp"]).sort_values("timestamp").reset_index(drop=True) + else: + print(f" Only {len(all_c)} candles, trying next exchange...") except Exception as e: - print(f" Warning: fetch error, retrying in 5s — {e}") - time.sleep(5) + print(f" Exchange {exch_id} failed: {e}") continue - - if not candles: - break - - all_candles.extend(candles) - since = candles[-1][0] + ms_per_candle - sys.stdout.write(f"\r Downloaded {len(all_candles)} candles...") - sys.stdout.flush() - time.sleep(exchange.rateLimit / 1000) - - print(f"\r Downloaded {len(all_candles)} candles total.") - - df = pd.DataFrame(all_candles, columns=["timestamp", "open", "high", "low", "close", "volume"]) - df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True) - df = df.drop_duplicates(subset=["timestamp"]).sort_values("timestamp").reset_index(drop=True) - return df - - -def main(): - os.makedirs(DATA_DIR, exist_ok=True) - - for tf in ["1h", "4h"]: - print(f"\n[*] Fetching {tf} data...") - df = fetch_ohlcv(tf) - out_path = os.path.join(DATA_DIR, f"btc_{tf}.csv") - df.to_csv(out_path, index=False) - print(f" Saved {len(df)} rows to {out_path}") - print(f" Range: {df['timestamp'].iloc[0]} → {df['timestamp'].iloc[-1]}") - - print("\nData fetch complete!") - + raise RuntimeError("Could not fetch sufficient data from any exchange") if __name__ == "__main__": - main() + os.makedirs(DATA_DIR, exist_ok=True) + for tf in ["4h", "1h"]: + print(f"[*] Fetching {tf}...") + df = fetch_ohlcv(tf) + out = os.path.join(DATA_DIR, f"btc_{tf}.csv") + df.to_csv(out, index=False) + print(f" Saved {out} ({len(df)} rows, {df['timestamp'].min()} to {df['timestamp'].max()})") + print("[OK] Done!")