fix(events): add optional side field to OrderFilledEvent
OrderFilledEvent had no side field, forcing api/main.py's chart() to backfill it from the sibling order_submitted row. Add an additive-only `side: str | None = None` field (existing payloads without it still parse) and populate it at the sole production emit site (alpaca_paper/execution.py _finalize) from the ExecutionRequest already in scope. The api/main.py backfill workaround is left in place as a harmless fallback for pre-existing events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
849bd476f9
commit
9772aca6bc
4 files changed
+61
No files matched your search
@@ -216,11 +216,13 @@ class AlpacaPaperExecutor:
|
||||
order_id=request.order_id,
|
||||
fill_price=fill_price or 0.0,
|
||||
fill_quantity=filled_qty,
|
||||
side=request.side,
|
||||
payload={
|
||||
"symbol": request.symbol,
|
||||
"order_id": request.order_id,
|
||||
"fill_price": fill_price,
|
||||
"fill_quantity": filled_qty,
|
||||
"side": request.side,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -195,6 +195,7 @@ class OrderFilledEvent(BaseEvent):
|
||||
fill_price: float = Field(description="Fill price")
|
||||
fill_quantity: int = Field(description="Filled quantity")
|
||||
slippage: float | None = Field(default=None, description="Slippage amount")
|
||||
side: str | None = Field(default=None, description="Buy or sell")
|
||||
|
||||
|
||||
class OrderCancelledEvent(BaseEvent):
|
||||
|
||||
@@ -72,6 +72,29 @@ def test_executor_submits_and_emits(store):
|
||||
assert len(submitted) == 1
|
||||
|
||||
|
||||
def test_executor_emits_fill_with_side(store):
|
||||
"""OrderFilledEvent must carry the request's side (schema gap fix) so
|
||||
consumers don't need the order_submitted backfill workaround."""
|
||||
class FilledClient(FakeClient):
|
||||
def submit_order(self, **kw):
|
||||
self.submitted.append(kw)
|
||||
return {"id": "broker-123", "status": "filled",
|
||||
"filled_qty": "10", "filled_avg_price": "14.05"}
|
||||
|
||||
client = FilledClient()
|
||||
executor = AlpacaPaperExecutor(store, client=client, session_id="t")
|
||||
request = ExecutionRequest(
|
||||
symbol="GOOD", side="buy", quantity=10,
|
||||
entry_price=14.0, stop_loss_price=13.45,
|
||||
)
|
||||
result = executor.execute(request)
|
||||
assert result.ok
|
||||
filled = store.query_events(event_type="order_filled")
|
||||
assert len(filled) == 1
|
||||
payload = json.loads(filled[0]["payload_json"])
|
||||
assert payload["side"] == "buy"
|
||||
|
||||
|
||||
def test_executor_rejects_zero_quantity(store):
|
||||
executor = AlpacaPaperExecutor(store, client=FakeClient(), session_id="t")
|
||||
result = executor.execute(ExecutionRequest(symbol="GOOD", side="buy", quantity=0))
|
||||
|
||||
@@ -10,6 +10,7 @@ from storage.event_schema import (
|
||||
EventMode,
|
||||
OrderApprovalRequestedEvent,
|
||||
OrderApprovedEvent,
|
||||
OrderFilledEvent,
|
||||
OrderRejectedEvent,
|
||||
OrderSubmittedEvent,
|
||||
SignalReadyEvent,
|
||||
@@ -58,6 +59,40 @@ def test_emit_and_query_round_trip(store):
|
||||
assert payload["signal_data"]["entry_price"] == 5.5
|
||||
|
||||
|
||||
def test_order_filled_event_side_round_trip(store):
|
||||
"""OrderFilledEvent carries an optional side; must round-trip when set,
|
||||
and old payloads without it must still parse (additive-only schema change)."""
|
||||
store.emit(
|
||||
OrderFilledEvent(
|
||||
timestamp=T0,
|
||||
mode=EventMode.PAPER,
|
||||
message="ABCD filled",
|
||||
symbol="ABCD",
|
||||
order_id="o1",
|
||||
fill_price=5.5,
|
||||
fill_quantity=10,
|
||||
side="buy",
|
||||
)
|
||||
)
|
||||
rows = store.query_events(event_type="order_filled")
|
||||
import json
|
||||
payload = json.loads(rows[0]["payload_json"])
|
||||
assert payload["side"] == "buy"
|
||||
|
||||
# additive-only: an event constructed without side still validates and
|
||||
# defaults to None, exactly like an older persisted record would.
|
||||
legacy = OrderFilledEvent(
|
||||
timestamp=T0,
|
||||
mode=EventMode.PAPER,
|
||||
message="ABCD filled",
|
||||
symbol="ABCD",
|
||||
order_id="o2",
|
||||
fill_price=5.5,
|
||||
fill_quantity=10,
|
||||
)
|
||||
assert legacy.side is None
|
||||
|
||||
|
||||
def test_query_orders_by_timestamp_ascending(store):
|
||||
for i in (3, 1, 2):
|
||||
store.emit(_signal(symbol=f"S{i}", ts=T0 + timedelta(minutes=i)))
|
||||
|
||||
Reference in new issue
Block a user