Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion agent/backtest/loaders/ccxt_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions agent/tests/test_ccxt_loader_proxy.py
Original file line number Diff line number Diff line change
@@ -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