feat: Implement idempotency and recovery mechanisms for order execution
- Add IdempotencyKeyFactory for generating unique user references based on execution legs. - Introduce OrderReconciler to reconcile order statuses with historical data. - Implement PartialFillRecovery to handle partial fills by canceling orders and placing hedges. - Create TriangularExecutionSequencer for executing triangular arbitrage strategies. - Enhance storage with new tables for trades, orders, and PnL events. - Develop AsyncExecutionWriter for asynchronous writing of execution records to the database. - Add unit tests for execution persistence, sequencer behavior, fill monitoring, and idempotency checks. - Update KrakenRestClient to ensure proper payloads for order placement and querying.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from arbitrade.detection.engine import OpportunityEvent
|
||||
from arbitrade.execution.idempotency import IdempotencyKeyFactory, OrderReconciler
|
||||
from arbitrade.execution.sequencer import ExecutionLeg
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _FakeHistoryClient:
|
||||
response: dict[str, Any]
|
||||
|
||||
async def query_order(self, *, order_id: str, include_trades: bool = True) -> dict[str, Any]:
|
||||
return self.response
|
||||
|
||||
|
||||
def _sample_event() -> OpportunityEvent:
|
||||
return OpportunityEvent(
|
||||
detected_at=datetime.now(UTC),
|
||||
cycle="USD->BTC->ETH->USD",
|
||||
updated_pair="BTC/USD",
|
||||
gross_rate=1.02,
|
||||
net_rate=1.01,
|
||||
gross_pct=2.0,
|
||||
net_pct=1.0,
|
||||
est_profit=1.0,
|
||||
allocated_capital=10.0,
|
||||
)
|
||||
|
||||
|
||||
def test_idempotency_key_factory_is_deterministic() -> None:
|
||||
factory = IdempotencyKeyFactory()
|
||||
event = _sample_event()
|
||||
leg = ExecutionLeg(
|
||||
from_currency="USD",
|
||||
to_currency="BTC",
|
||||
pair="BTC/USD",
|
||||
side="buy",
|
||||
volume=10.0,
|
||||
)
|
||||
|
||||
first = factory.user_ref_for_leg(event, leg, 0)
|
||||
second = factory.user_ref_for_leg(event, leg, 0)
|
||||
|
||||
assert first == second
|
||||
assert first > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_order_reconciler_maps_query_response_to_report() -> None:
|
||||
client = _FakeHistoryClient(
|
||||
response={
|
||||
"order-1": {
|
||||
"status": "closed",
|
||||
"vol_exec": "5.0",
|
||||
"price": "100.0",
|
||||
"pair": "BTC/USD",
|
||||
"type": "buy",
|
||||
"userref": 12345,
|
||||
}
|
||||
}
|
||||
)
|
||||
reconciler = OrderReconciler(client)
|
||||
|
||||
report = await reconciler.reconcile_order(
|
||||
order_id="order-1",
|
||||
user_ref=12345,
|
||||
expected_pair="BTC/USD",
|
||||
expected_side="buy",
|
||||
expected_volume=10.0,
|
||||
)
|
||||
|
||||
assert report.is_terminal
|
||||
assert report.matches_request
|
||||
assert report.status == "closed"
|
||||
assert report.filled_volume == 5.0
|
||||
assert report.avg_price == 100.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_order_reconciler_marks_mismatch() -> None:
|
||||
client = _FakeHistoryClient(
|
||||
response={
|
||||
"order-1": {
|
||||
"status": "closed",
|
||||
"vol_exec": "5.0",
|
||||
"price": "100.0",
|
||||
"pair": "ETH/USD",
|
||||
"type": "sell",
|
||||
"userref": 999,
|
||||
}
|
||||
}
|
||||
)
|
||||
reconciler = OrderReconciler(client)
|
||||
|
||||
report = await reconciler.reconcile_order(
|
||||
order_id="order-1",
|
||||
user_ref=12345,
|
||||
expected_pair="BTC/USD",
|
||||
expected_side="buy",
|
||||
expected_volume=10.0,
|
||||
)
|
||||
|
||||
assert not report.matches_request
|
||||
Reference in New Issue
Block a user