feat(V2): URL exchange configurabili da .env (DERIBIT_URL_*, BYBIT_URL_*, ecc.)

This commit is contained in:
AdrianoDev
2026-04-30 20:36:31 +02:00
parent b71c66917c
commit 436dfd6f5a
6 changed files with 141 additions and 13 deletions
+8
View File
@@ -14,35 +14,43 @@ async def build_client(
if exchange == "deribit":
from cerbero_mcp.exchanges.deribit.client import DeribitClient
url = settings.deribit.url_testnet if env == "testnet" else settings.deribit.url_live
return DeribitClient(
client_id=settings.deribit.client_id,
client_secret=settings.deribit.client_secret.get_secret_value(),
testnet=(env == "testnet"),
base_url_override=url,
)
if exchange == "bybit":
from cerbero_mcp.exchanges.bybit.client import BybitClient
url = settings.bybit.url_testnet if env == "testnet" else settings.bybit.url_live
return BybitClient(
api_key=settings.bybit.api_key,
api_secret=settings.bybit.api_secret.get_secret_value(),
testnet=(env == "testnet"),
base_url=url,
)
if exchange == "hyperliquid":
from cerbero_mcp.exchanges.hyperliquid.client import HyperliquidClient
url = settings.hyperliquid.url_testnet if env == "testnet" else settings.hyperliquid.url_live
return HyperliquidClient(
wallet_address=settings.hyperliquid.wallet_address,
private_key=settings.hyperliquid.private_key.get_secret_value(),
testnet=(env == "testnet"),
api_wallet_address=settings.hyperliquid.api_wallet_address,
base_url=url,
)
if exchange == "alpaca":
from cerbero_mcp.exchanges.alpaca.client import AlpacaClient
url = settings.alpaca.url_testnet if env == "testnet" else settings.alpaca.url_live
return AlpacaClient(
api_key=settings.alpaca.api_key_id,
secret_key=settings.alpaca.secret_key.get_secret_value(),
paper=(env == "testnet"),
base_url=url,
)
if exchange == "macro":
# Read-only data provider — env ignored. Il registry
+13 -3
View File
@@ -92,6 +92,7 @@ class AlpacaClient:
api_key: str,
secret_key: str,
paper: bool = True,
base_url: str | None = None,
trading: Any | None = None,
stock_data: Any | None = None,
crypto_data: Any | None = None,
@@ -100,9 +101,18 @@ class AlpacaClient:
self.api_key = api_key
self.secret_key = secret_key
self.paper = paper
self._trading = trading or TradingClient(
api_key=api_key, secret_key=secret_key, paper=paper
)
self.base_url = base_url
# alpaca-py TradingClient accetta `url_override` per override URL trading.
# Data clients (Stock/Crypto/Option) non supportano url_override sul costruttore;
# usano endpoint dati separati (data.alpaca.markets) — `base_url` è ignorato per essi.
if trading is None:
trading_kwargs: dict[str, Any] = {
"api_key": api_key, "secret_key": secret_key, "paper": paper,
}
if base_url:
trading_kwargs["url_override"] = base_url
trading = TradingClient(**trading_kwargs)
self._trading = trading
self._stock = stock_data or StockHistoricalDataClient(
api_key=api_key, secret_key=secret_key
)
+10 -1
View File
@@ -30,15 +30,24 @@ class BybitClient:
api_secret: str,
testnet: bool = True,
http: Any | None = None,
base_url: str | None = None,
) -> None:
self.api_key = api_key
self.api_secret = api_secret
self.testnet = testnet
self._http = http or HTTP(
# pybit HTTP non accetta `endpoint` come kwarg (vedi _V5HTTPManager.__init__:
# solo `domain`/`tld`/`testnet`). Override URL applicato post-init
# sovrascrivendo l'attributo `endpoint` dell'istanza HTTP.
self.base_url = base_url
if http is None:
http = HTTP(
api_key=api_key,
api_secret=api_secret,
testnet=testnet,
)
if base_url:
http.endpoint = base_url
self._http = http
async def _run(self, fn, /, **kwargs):
return await asyncio.to_thread(fn, **kwargs)
@@ -28,11 +28,14 @@ class DeribitClient:
client_id: str
client_secret: str
testnet: bool = True
base_url_override: str | None = None
_token: str | None = field(default=None, init=False, repr=False)
_token_expires_at: float = field(default=0.0, init=False, repr=False)
@property
def base_url(self) -> str:
if self.base_url_override:
return self.base_url_override
return BASE_TESTNET if self.testnet else BASE_LIVE
# ── Auth ─────────────────────────────────────────────────────
@@ -52,11 +52,16 @@ class HyperliquidClient:
private_key: str,
testnet: bool = True,
api_wallet_address: str | None = None,
base_url: str | None = None,
):
self.wallet_address = wallet_address
self.private_key = private_key
self.testnet = testnet
self.api_wallet_address = api_wallet_address or wallet_address
self._base_url_override = base_url
if base_url:
self.base_url = base_url
else:
self.base_url = BASE_TESTNET if testnet else BASE_LIVE
self._exchange: Any | None = None
@@ -70,13 +75,16 @@ class HyperliquidClient:
)
if self._exchange is None:
account = Account.from_key(self.private_key)
base_url = (
if self._base_url_override:
sdk_base_url = self._base_url_override
else:
sdk_base_url = (
hl_constants.TESTNET_API_URL if self.testnet else hl_constants.MAINNET_API_URL
)
empty_spot_meta: dict[str, Any] = {"universe": [], "tokens": []}
self._exchange = Exchange(
account,
base_url,
sdk_base_url,
account_address=self.wallet_address,
spot_meta=empty_spot_meta,
)
+90
View File
@@ -148,6 +148,96 @@ async def test_build_client_sentiment_no_env_distinction(monkeypatch):
assert c_test.lunarcrush_key == c_live.lunarcrush_key
@pytest.mark.asyncio
async def test_deribit_url_from_env_overrides_default(monkeypatch):
"""DERIBIT_URL_TESTNET custom nel .env → builder lo passa al client."""
from tests.unit.test_settings import _minimal_env
env = _minimal_env(DERIBIT_URL_TESTNET="https://custom-test.example.com/api/v2")
for k, v in env.items():
monkeypatch.setenv(k, v)
from cerbero_mcp.exchanges import build_client
from cerbero_mcp.settings import Settings
s = Settings()
c = await build_client(s, "deribit", "testnet")
assert c.base_url == "https://custom-test.example.com/api/v2"
assert "custom-test.example.com" in c.base_url
@pytest.mark.asyncio
async def test_deribit_url_live_from_env_overrides_default(monkeypatch):
from tests.unit.test_settings import _minimal_env
env = _minimal_env(DERIBIT_URL_LIVE="https://custom-live.example.com/api/v2")
for k, v in env.items():
monkeypatch.setenv(k, v)
from cerbero_mcp.exchanges import build_client
from cerbero_mcp.settings import Settings
s = Settings()
c = await build_client(s, "deribit", "mainnet")
assert c.base_url == "https://custom-live.example.com/api/v2"
@pytest.mark.asyncio
async def test_hyperliquid_url_from_env_overrides_default(monkeypatch):
from tests.unit.test_settings import _minimal_env
env = _minimal_env(HYPERLIQUID_URL_TESTNET="https://hl-custom.example.com")
for k, v in env.items():
monkeypatch.setenv(k, v)
from cerbero_mcp.exchanges import build_client
from cerbero_mcp.settings import Settings
s = Settings()
c = await build_client(s, "hyperliquid", "testnet")
assert c.base_url == "https://hl-custom.example.com"
# SDK override deve essere disponibile per init lazy
assert c._base_url_override == "https://hl-custom.example.com"
@pytest.mark.asyncio
async def test_bybit_url_from_env_overrides_default(monkeypatch):
"""Bybit: pybit non accetta `endpoint` come kwarg, ma setting di
`_http.endpoint` post-init rispecchia l'override."""
from tests.unit.test_settings import _minimal_env
env = _minimal_env(BYBIT_URL_TESTNET="https://bybit-custom.example.com")
for k, v in env.items():
monkeypatch.setenv(k, v)
from cerbero_mcp.exchanges import build_client
from cerbero_mcp.settings import Settings
s = Settings()
c = await build_client(s, "bybit", "testnet")
assert c.base_url == "https://bybit-custom.example.com"
# override applicato all'istanza pybit HTTP via attributo `endpoint`
assert getattr(c._http, "endpoint", None) == "https://bybit-custom.example.com"
@pytest.mark.asyncio
async def test_alpaca_url_from_env_overrides_default(monkeypatch):
"""Alpaca: TradingClient supporta url_override per trading API.
Data clients (Stock/Crypto/Option) non supportano override sul costruttore."""
from tests.unit.test_settings import _minimal_env
env = _minimal_env(ALPACA_URL_TESTNET="https://alpaca-custom.example.com")
for k, v in env.items():
monkeypatch.setenv(k, v)
from cerbero_mcp.exchanges import build_client
from cerbero_mcp.settings import Settings
s = Settings()
c = await build_client(s, "alpaca", "testnet")
assert c.base_url == "https://alpaca-custom.example.com"
@pytest.mark.asyncio
async def test_build_client_unknown_exchange_raises(monkeypatch):
from tests.unit.test_settings import _minimal_env