"""Test AlpacaClient httpx-based (V2.0.0). Mockano gli endpoint REST tramite pytest-httpx. Coprono account/positions, ordini (place/cancel/limit-error), close position, clock, asset class invalida → ValueError. """ from __future__ import annotations import re import pytest from cerbero_mcp.exchanges.alpaca.client import AlpacaClient from pytest_httpx import HTTPXMock PAPER = "https://paper-api.alpaca.markets" DATA = "https://data.alpaca.markets" @pytest.mark.asyncio async def test_init_paper_mode(client: AlpacaClient): assert client.paper is True assert client.base_url is None assert client._trading_base == PAPER @pytest.mark.asyncio async def test_init_live_mode(client_live: AlpacaClient): assert client_live.paper is False assert client_live._trading_base == "https://api.alpaca.markets" @pytest.mark.asyncio async def test_init_base_url_override(): c = AlpacaClient( api_key="k", secret_key="s", paper=True, base_url="https://alpaca-custom.example.com", ) try: assert c.base_url == "https://alpaca-custom.example.com" assert c._trading_base == "https://alpaca-custom.example.com" # Data endpoint NON viene overridato assert c._data_base == DATA finally: await c.aclose() @pytest.mark.asyncio async def test_get_account(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=f"{PAPER}/v2/account", json={"id": "abc", "equity": "100000.00", "buying_power": "200000.00"}, ) result = await client.get_account() assert result["id"] == "abc" assert result["equity"] == "100000.00" @pytest.mark.asyncio async def test_get_account_sends_auth_headers( httpx_mock: HTTPXMock, client: AlpacaClient ): httpx_mock.add_response(url=f"{PAPER}/v2/account", json={"id": "x"}) await client.get_account() req = httpx_mock.get_requests()[0] assert req.headers["APCA-API-KEY-ID"] == "test_key" assert req.headers["APCA-API-SECRET-KEY"] == "test_secret" assert req.headers["Accept"] == "application/json" @pytest.mark.asyncio async def test_get_positions_returns_list( httpx_mock: HTTPXMock, client: AlpacaClient ): httpx_mock.add_response( url=f"{PAPER}/v2/positions", json=[{"symbol": "AAPL", "qty": "10", "side": "long"}], ) result = await client.get_positions() assert len(result) == 1 assert result[0]["symbol"] == "AAPL" @pytest.mark.asyncio async def test_place_market_order_stocks( httpx_mock: HTTPXMock, client: AlpacaClient ): httpx_mock.add_response( method="POST", url=f"{PAPER}/v2/orders", json={"id": "o123", "symbol": "AAPL", "status": "accepted"}, ) result = await client.place_order( symbol="AAPL", side="buy", qty=1, order_type="market", asset_class="stocks" ) assert result["id"] == "o123" # body POST corretto req = httpx_mock.get_requests()[0] import json as _j body = _j.loads(req.content) assert body["symbol"] == "AAPL" assert body["side"] == "buy" assert body["type"] == "market" assert body["qty"] == "1" assert body["time_in_force"] == "day" @pytest.mark.asyncio async def test_place_limit_order_requires_price(client: AlpacaClient): with pytest.raises(ValueError, match="limit_price"): await client.place_order( symbol="AAPL", side="buy", qty=1, order_type="limit" ) @pytest.mark.asyncio async def test_place_stop_order_requires_price(client: AlpacaClient): with pytest.raises(ValueError, match="stop_price"): await client.place_order( symbol="AAPL", side="buy", qty=1, order_type="stop" ) @pytest.mark.asyncio async def test_place_unsupported_order_type(client: AlpacaClient): with pytest.raises(ValueError, match="unsupported order_type"): await client.place_order( symbol="AAPL", side="buy", qty=1, order_type="trailing_stop" ) @pytest.mark.asyncio async def test_cancel_order(httpx_mock: HTTPXMock, client: AlpacaClient): # 204 No Content su success httpx_mock.add_response( method="DELETE", url=f"{PAPER}/v2/orders/o1", status_code=204, ) result = await client.cancel_order("o1") assert result == {"order_id": "o1", "canceled": True} @pytest.mark.asyncio async def test_cancel_all_orders(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( method="DELETE", url=f"{PAPER}/v2/orders", json=[ {"id": "a", "status": 200}, {"id": "b", "status": 200}, ], ) result = await client.cancel_all_orders() assert len(result) == 2 @pytest.mark.asyncio async def test_close_position_no_options( httpx_mock: HTTPXMock, client: AlpacaClient ): httpx_mock.add_response( method="DELETE", url=f"{PAPER}/v2/positions/AAPL", json={"id": "close-1", "symbol": "AAPL"}, ) result = await client.close_position("AAPL") assert result["id"] == "close-1" @pytest.mark.asyncio async def test_close_position_with_qty( httpx_mock: HTTPXMock, client: AlpacaClient ): httpx_mock.add_response( method="DELETE", url=f"{PAPER}/v2/positions/AAPL?qty=5.0", json={"id": "close-2"}, ) result = await client.close_position("AAPL", qty=5.0) assert result["id"] == "close-2" @pytest.mark.asyncio async def test_close_all_positions( httpx_mock: HTTPXMock, client: AlpacaClient ): httpx_mock.add_response( method="DELETE", url=f"{PAPER}/v2/positions?cancel_orders=true", json=[{"symbol": "AAPL", "status": 200}], ) result = await client.close_all_positions(cancel_orders=True) assert len(result) == 1 assert result[0]["symbol"] == "AAPL" @pytest.mark.asyncio async def test_get_clock(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=f"{PAPER}/v2/clock", json={"is_open": True, "next_close": "2026-04-21T20:00:00Z"}, ) result = await client.get_clock() assert result["is_open"] is True @pytest.mark.asyncio async def test_invalid_asset_class(client: AlpacaClient): with pytest.raises(ValueError, match="invalid asset_class"): await client.get_ticker("AAPL", asset_class="forex") @pytest.mark.asyncio async def test_get_ticker_stocks(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=f"{DATA}/v2/stocks/AAPL/trades/latest", json={ "symbol": "AAPL", "trade": {"p": 175.50, "s": 100, "t": "2026-04-18T15:30:00Z"}, }, ) httpx_mock.add_response( url=f"{DATA}/v2/stocks/AAPL/quotes/latest", json={ "symbol": "AAPL", "quote": { "bp": 175.40, "ap": 175.55, "bs": 50, "as": 25, "t": "2026-04-18T15:30:00Z", }, }, ) result = await client.get_ticker("AAPL", asset_class="stocks") assert result["asset_class"] == "stocks" assert result["last_price"] == 175.50 assert result["bid"] == 175.40 assert result["ask"] == 175.55 @pytest.mark.asyncio async def test_get_bars_stocks(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=re.compile(rf"^{DATA}/v2/stocks/bars\?.*"), json={ "bars": { "AAPL": [ { "t": "2026-04-17T00:00:00Z", "o": 170.0, "h": 176.0, "l": 169.5, "c": 175.0, "v": 1000000, } ] } }, ) result = await client.get_bars( symbol="AAPL", asset_class="stocks", interval="1d", start="2026-04-17T00:00:00", end="2026-04-18T00:00:00", limit=10, ) assert result["symbol"] == "AAPL" assert result["interval"] == "1d" assert len(result["bars"]) == 1 assert result["bars"][0]["close"] == 175.0 @pytest.mark.asyncio async def test_get_bars_unsupported_timeframe(client: AlpacaClient): with pytest.raises(ValueError, match="unsupported timeframe"): await client.get_bars( symbol="AAPL", asset_class="stocks", interval="3min", ) @pytest.mark.asyncio async def test_get_bars_invalid_asset_class(client: AlpacaClient): with pytest.raises(ValueError, match="invalid asset_class"): await client.get_bars(symbol="AAPL", asset_class="forex") @pytest.mark.asyncio async def test_get_assets(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=re.compile(rf"^{PAPER}/v2/assets\?.*"), json=[ {"symbol": "AAPL", "tradable": True, "class": "us_equity"}, {"symbol": "GOOG", "tradable": True, "class": "us_equity"}, ], ) result = await client.get_assets(asset_class="stocks", status="active") assert len(result) == 2 assert result[0]["symbol"] == "AAPL" @pytest.mark.asyncio async def test_get_assets_invalid_class(client: AlpacaClient): with pytest.raises(ValueError, match="invalid asset_class"): await client.get_assets(asset_class="forex") @pytest.mark.asyncio async def test_get_open_orders(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=re.compile(rf"^{PAPER}/v2/orders\?.*"), json=[{"id": "o1", "status": "open", "symbol": "AAPL"}], ) result = await client.get_open_orders(limit=10) assert len(result) == 1 assert result[0]["id"] == "o1" @pytest.mark.asyncio async def test_amend_order(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( method="PATCH", url=f"{PAPER}/v2/orders/o1", json={"id": "o1", "qty": "5", "limit_price": "180.0"}, ) result = await client.amend_order( "o1", qty=5, limit_price=180.0, tif="gtc" ) assert result["id"] == "o1" req = httpx_mock.get_requests()[0] import json as _j body = _j.loads(req.content) assert body["qty"] == "5" assert body["limit_price"] == "180.0" assert body["time_in_force"] == "gtc" @pytest.mark.asyncio async def test_get_calendar(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=re.compile(rf"^{PAPER}/v2/calendar.*"), json=[{"date": "2026-04-20", "open": "09:30", "close": "16:00"}], ) result = await client.get_calendar(start="2026-04-20", end="2026-04-20") assert len(result) == 1 assert result[0]["date"] == "2026-04-20" @pytest.mark.asyncio async def test_get_calendar_no_filters( httpx_mock: HTTPXMock, client: AlpacaClient ): httpx_mock.add_response( url=f"{PAPER}/v2/calendar", json=[{"date": "2026-04-20"}], ) result = await client.get_calendar() assert len(result) == 1 @pytest.mark.asyncio async def test_get_snapshot(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=re.compile(rf"^{DATA}/v2/stocks/snapshots\?.*"), json={ "AAPL": { "latestTrade": {"p": 175.0}, "latestQuote": {"bp": 174.9, "ap": 175.1}, } }, ) result = await client.get_snapshot("AAPL") assert result["latestTrade"]["p"] == 175.0 @pytest.mark.asyncio async def test_get_option_chain(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=re.compile(rf"^{DATA}/v1beta1/options/snapshots/AAPL.*"), json={ "snapshots": { "AAPL250620C00200000": { "latestQuote": {"bp": 1.20, "ap": 1.30} } } }, ) result = await client.get_option_chain("AAPL", expiry="2026-06-20") assert result["underlying"] == "AAPL" assert result["expiry"] == "2026-06-20" assert "AAPL250620C00200000" in result["contracts"] @pytest.mark.asyncio async def test_get_activities(httpx_mock: HTTPXMock, client: AlpacaClient): httpx_mock.add_response( url=re.compile(rf"^{PAPER}/v2/account/activities.*"), json=[ {"id": "1", "activity_type": "FILL"}, {"id": "2", "activity_type": "TRANS"}, ], ) result = await client.get_activities(limit=10) assert len(result) == 2 @pytest.mark.asyncio async def test_aclose_idempotent(client: AlpacaClient): await client.aclose() await client.aclose() # nessun raise