Files
momentum-system/tests/unit/test_alpaca_client.py
T
momentum devandClaude Fable 5 119e4c0ad1 money-path: reconcile a lost order-submit by client_order_id before journaling cancel
Plan 003 step 4. A transport error AFTER the POST landed leaves a live order
resting at the broker; journaling it 'cancelled' breeds the duplicate/naked
order (2026-07-06 incident). Add client.get_order_by_client_order_id + an
after= passthrough on get_orders; on the generic-exception path, look up the
order by the client_order_id we already send and, if live, emit the normal
submitted sequence (ok=True) instead of cancelled. AlpacaApiError (real
rejection) is NOT reconciled. Fills the empty test_alpaca_client.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:24:14 -04:00

64 lines
2.4 KiB
Python

"""AlpacaPaperClient request-shaping tests (plan 003 step 4 + client hardening).
The client's trickiest logic — by-client-order-id lookup, the `after=` passthrough,
and get_orders scoping — only bites in live trading, so pin the request shapes here
with a stubbed transport (no network).
"""
import pytest
from alpaca_paper.client import AlpacaApiError, AlpacaPaperClient
class _RecordingClient(AlpacaPaperClient):
"""Bypass __init__/network: capture (method, url, params) from _request and
return a scripted response, so we can assert the exact request shape."""
def __init__(self, response=None, raise_status=None):
self.calls = []
self._response = response if response is not None else []
self._raise_status = raise_status
class _S:
trading_base_url = "https://paper-api.alpaca.markets"
data_base_url = "https://data.alpaca.markets"
self.settings = _S()
def _request(self, method, url, params=None, **kw):
self.calls.append({"method": method, "url": url, "params": params or {}})
if self._raise_status:
raise AlpacaApiError(self._raise_status, "err")
return self._response
def test_get_order_by_client_order_id_request_shape():
c = _RecordingClient(response={"id": "b1", "status": "accepted"})
out = c.get_order_by_client_order_id("my-coid-123")
assert out == {"id": "b1", "status": "accepted"}
call = c.calls[-1]
assert call["method"] == "GET"
assert call["url"].endswith("/v2/orders:by_client_order_id")
assert call["params"] == {"client_order_id": "my-coid-123"}
def test_get_order_by_client_order_id_404_raises():
c = _RecordingClient(raise_status=404)
with pytest.raises(AlpacaApiError):
c.get_order_by_client_order_id("missing")
def test_get_orders_after_passthrough():
c = _RecordingClient(response=[])
c.get_orders(status="all", symbols=["AAA"], after="2026-07-13T00:00:00-04:00")
params = c.calls[-1]["params"]
assert params["after"] == "2026-07-13T00:00:00-04:00"
assert params["symbols"] == "AAA" # scoped query, single page
assert params["status"] == "all"
def test_get_orders_scoped_is_single_page():
# a symbols= query is scoped under the cap: exactly one request, no pagination
c = _RecordingClient(response=[{"id": "1"}, {"id": "2"}])
out = c.get_orders(symbols=["AAA", "BBB"])
assert len(c.calls) == 1
assert out == [{"id": "1"}, {"id": "2"}]