"""OANDA v20 historical candle downloader. The access token is read from configuration (the ``OANDA_ACCESS_TOKEN`` environment variable) rather than being hard-coded. """ import time from datetime import datetime, timedelta import pandas as pd import oandapyV20 import oandapyV20.endpoints.instruments as instruments from oandapyV20.exceptions import V20Error from tradingbot.config import OANDA_ACCESS_TOKEN class OandaDataConnector: def __init__(self, access_token): self.client = oandapyV20.API(access_token=access_token) self.last_request_time = 0 self.request_limit_delay = 0.01 # 100 requests per second max def _respect_rate_limit(self): current_time = time.time() time_passed = current_time - self.last_request_time if time_passed < self.request_limit_delay: time.sleep(self.request_limit_delay - time_passed) self.last_request_time = time.time() def get_historical_data_chunked(self, instrument, timeframe, start_date, end_date=None, chunk_days=5): if end_date is None: end_date = datetime.now() all_candles = [] current_date = start_date while current_date < end_date: self._respect_rate_limit() chunk_end = min(current_date + timedelta(days=chunk_days), end_date) params = { "from": current_date.strftime('%Y-%m-%dT%H:%M:%SZ'), "to": chunk_end.strftime('%Y-%m-%dT%H:%M:%SZ'), "granularity": timeframe, "price": "MBA" # Mid, Bid, Ask prices } try: r = instruments.InstrumentsCandles(instrument=instrument, params=params) self.client.request(r) for candle in r.response['candles']: if candle['complete']: all_candles.append({ 'timestamp': candle['time'], 'open': float(candle['mid']['o']), 'high': float(candle['mid']['h']), 'low': float(candle['mid']['l']), 'close': float(candle['mid']['c']), 'volume': float(candle['volume']) }) print(f"Downloaded data from {current_date.date()} to {chunk_end.date()}") current_date = chunk_end except V20Error as e: print(f"Error fetching data: {e}") return None df = pd.DataFrame(all_candles) if not df.empty: df['timestamp'] = pd.to_datetime(df['timestamp']) df.set_index('timestamp', inplace=True) df = df.sort_index() return df def download(instrument="EUR_USD", timeframe="M5", lookback_days=1825, access_token=None, out_dir="."): """Download ``lookback_days`` of candles and save them to a CSV. Returns the downloaded DataFrame (or ``None`` on failure). """ token = access_token or OANDA_ACCESS_TOKEN if not token: raise ValueError( "No OANDA access token. Set the OANDA_ACCESS_TOKEN environment " "variable or pass access_token=..." ) connector = OandaDataConnector(token) start_date = datetime.now() - timedelta(days=lookback_days) data = connector.get_historical_data_chunked( instrument=instrument, timeframe=timeframe, start_date=start_date, ) if data is not None: name = instrument.replace("_", "") filename = ( f"{out_dir}/{name}_{timeframe}_" f"{start_date.strftime('%Y%m%d')}_to_{datetime.now().strftime('%Y%m%d')}.csv" ) data.to_csv(filename) print(f"Data saved to {filename}") print(f"Total candles downloaded: {len(data)}") return data if __name__ == "__main__": download()