Files
arbitrade/tests/unit/test_idempotency.py
zwitschi 93f4f62d42 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.
2026-06-01 11:59:13 +02:00

110 lines
2.7 KiB
Python

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