Skip to content
Open
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
30 changes: 30 additions & 0 deletions backend_api_python/app/services/live_trading/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,35 @@ def _get_requests_verify() -> Union[bool, str]:
return _requests_verify_value


# Cached proxy setting for all live-trading REST calls.
_proxies_value: Optional[Dict[str, str]] = None
_proxies_resolved = False


def _get_proxies() -> Optional[Dict[str, str]]:
"""
Resolve the proxy for live-trading REST calls.

``PROXY_URL`` is documented (see ``env.example``) to cover "market data,
exchange and broker API traffic": CCXT already honors it for market data,
but plain ``requests`` does not understand that variable, so without this
bridge live-trading REST calls bypass the configured proxy entirely.

- ``PROXY_URL`` set (http(s):// or socks5(h)://, PySocks is in requirements):
returned explicitly and takes precedence over environment proxies.
- ``PROXY_URL`` unset: returns ``None`` — nothing is forced and standard
``HTTPS_PROXY`` / ``HTTP_PROXY`` / ``ALL_PROXY`` environment variables
still apply through ``requests`` trust_env (with ``NO_PROXY`` bypasses).
"""
global _proxies_value, _proxies_resolved
if not _proxies_resolved:
proxy_url = (os.environ.get("PROXY_URL") or "").strip()
if proxy_url:
_proxies_value = {"http": proxy_url, "https": proxy_url}
_proxies_resolved = True
return _proxies_value


@dataclass
class LiveOrderResult:
exchange_id: str
Expand Down Expand Up @@ -143,6 +172,7 @@ def _request(
headers=request_headers or None,
timeout=self.timeout_sec,
verify=_get_requests_verify(),
proxies=_get_proxies(),
) as resp:
text = resp.text or ""
parsed: Dict[str, Any] = {}
Expand Down
92 changes: 92 additions & 0 deletions backend_api_python/tests/test_live_rest_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Live-trading REST proxy resolution.

Regression coverage for the live-trading funnel honoring ``PROXY_URL``:
market data (CCXT) already honored it while ``BaseRestClient._request``
sent exchange REST traffic directly, so deployments that rely on
``PROXY_URL`` for exchange reachability broke only on live trading.
"""

import pytest

from app.services.live_trading import base as rest_base
from app.services.live_trading.base import BaseRestClient


@pytest.fixture(autouse=True)
def _reset_proxy_cache():
rest_base._proxies_resolved = False
rest_base._proxies_value = None
yield
rest_base._proxies_resolved = False
rest_base._proxies_value = None


def _capture_request(monkeypatch):
calls = {}

def fake_request(method, url, **kwargs):
calls["method"] = method
calls["url"] = url
calls.update(kwargs)

class _Resp:
status_code = 200
text = "{}"

def json(self):
return {}

def __enter__(self):
return self

def __exit__(self, *args):
return False

return _Resp()

monkeypatch.setattr(rest_base.requests, "request", fake_request)
return calls


def test_explicit_proxy_url_is_applied(monkeypatch):
monkeypatch.setenv("PROXY_URL", "http://infra-01:20171")
calls = _capture_request(monkeypatch)
BaseRestClient("https://example.com")._request("GET", "/time")
assert calls["proxies"] == {
"http": "http://infra-01:20171",
"https": "http://infra-01:20171",
}


def test_socks5h_proxy_url_is_applied(monkeypatch):
monkeypatch.setenv("PROXY_URL", "socks5h://127.0.0.1:10808")
calls = _capture_request(monkeypatch)
BaseRestClient("https://example.com")._request("GET", "/time")
assert calls["proxies"] == {
"http": "socks5h://127.0.0.1:10808",
"https": "socks5h://127.0.0.1:10808",
}


def test_unset_proxy_url_forces_nothing(monkeypatch):
monkeypatch.delenv("PROXY_URL", raising=False)
calls = _capture_request(monkeypatch)
BaseRestClient("https://example.com")._request("GET", "/time")
assert calls["proxies"] is None # trust_env (standard vars) stays in charge


def test_resolution_is_cached_per_process(monkeypatch):
monkeypatch.setenv("PROXY_URL", "http://first:1")
assert rest_base._get_proxies()["http"] == "http://first:1"
monkeypatch.setenv("PROXY_URL", "http://second:2")
assert rest_base._get_proxies()["http"] == "http://first:1"


def test_funnel_is_single_entrypoint(monkeypatch):
"""Guard: _request must keep forwarding the resolved proxies kwarg."""
monkeypatch.setenv("PROXY_URL", "http://infra-01:20171")
calls = _capture_request(monkeypatch)
client = BaseRestClient("https://example.com")
client._request("POST", "/order", json_body={"x": 1})
assert calls["method"] == "POST"
assert calls["proxies"] is not None