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>
64 lines
2.4 KiB
Python
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"}]
|