93f4f62d42
- 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.
110 lines
2.7 KiB
Python
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
|