diff --git a/agent/backtest/loaders/ccxt_loader.py b/agent/backtest/loaders/ccxt_loader.py index 7b9dc816..bd38acc4 100644 --- a/agent/backtest/loaders/ccxt_loader.py +++ b/agent/backtest/loaders/ccxt_loader.py @@ -37,6 +37,28 @@ _CCXT_FETCH_BUDGET_S = float(os.getenv("CCXT_FETCH_BUDGET_S", "60")) +def _first_proxy_env(*names: str) -> str: + for name in names: + value = os.getenv(name, "").strip() + if value: + return value + return "" + + +def _ccxt_proxy_config() -> dict[str, str]: + """Build CCXT proxy settings from conventional proxy environment variables.""" + all_proxy = _first_proxy_env("ALL_PROXY", "all_proxy") + http_proxy = _first_proxy_env("HTTP_PROXY", "http_proxy") or all_proxy + https_proxy = _first_proxy_env("HTTPS_PROXY", "https_proxy") or all_proxy or http_proxy + + proxies: dict[str, str] = {} + if http_proxy: + proxies["http"] = http_proxy + if https_proxy: + proxies["https"] = https_proxy + return proxies + + @register class DataLoader: """CCXT-backed crypto OHLCV loader (100+ exchanges).""" @@ -64,7 +86,12 @@ def _get_exchange(self): if exchange_cls is None: logger.warning("Unknown CCXT exchange %s, falling back to binance", exchange_id) exchange_cls = ccxt.binance - return exchange_cls({"enableRateLimit": True, "timeout": _CCXT_TIMEOUT_MS}) + + config = {"enableRateLimit": True, "timeout": _CCXT_TIMEOUT_MS} + proxies = _ccxt_proxy_config() + if proxies: + config["proxies"] = proxies + return exchange_cls(config) def fetch( self, diff --git a/agent/tests/test_ccxt_loader_proxy.py b/agent/tests/test_ccxt_loader_proxy.py new file mode 100644 index 00000000..ffa9b724 --- /dev/null +++ b/agent/tests/test_ccxt_loader_proxy.py @@ -0,0 +1,48 @@ +"""Regression tests for CCXT proxy environment handling.""" + +from __future__ import annotations + +import ccxt + +from backtest.loaders.ccxt_loader import DataLoader + + +class _FakeExchange: + def __init__(self, config): + self.config = config + + +def _clear_proxy_env(monkeypatch): + for name in ( + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + ): + monkeypatch.delenv(name, raising=False) + + +def test_get_exchange_passes_all_proxy_to_ccxt(monkeypatch): + _clear_proxy_env(monkeypatch) + monkeypatch.setenv("CCXT_EXCHANGE", "binance") + monkeypatch.setenv("ALL_PROXY", "socks5h://127.0.0.1:1088") + monkeypatch.setattr(ccxt, "binance", _FakeExchange) + + exchange = DataLoader()._get_exchange() + + assert exchange.config["proxies"] == { + "http": "socks5h://127.0.0.1:1088", + "https": "socks5h://127.0.0.1:1088", + } + + +def test_get_exchange_omits_proxy_config_when_env_absent(monkeypatch): + _clear_proxy_env(monkeypatch) + monkeypatch.setenv("CCXT_EXCHANGE", "binance") + monkeypatch.setattr(ccxt, "binance", _FakeExchange) + + exchange = DataLoader()._get_exchange() + + assert "proxies" not in exchange.config