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.
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Protocol
|
|
|
|
from arbitrade.execution.fill_monitor import FillMonitorResult, OrderFillState
|
|
|
|
|
|
class SupportsOrderLifecycle(Protocol):
|
|
async def cancel_order(self, *, order_id: str) -> dict[str, Any]: ...
|
|
|
|
async def place_market_order(
|
|
self, *, pair: str, side: str, volume: float
|
|
) -> dict[str, Any]: ...
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RecoveryAction:
|
|
order_id: str
|
|
canceled: bool
|
|
hedged: bool
|
|
hedge_pair: str | None = None
|
|
hedge_side: str | None = None
|
|
hedge_volume: float | None = None
|
|
cancel_response: dict[str, Any] | None = None
|
|
hedge_response: dict[str, Any] | None = None
|
|
reason: str | None = None
|
|
|
|
|
|
class PartialFillRecovery:
|
|
def __init__(self, rest_client: SupportsOrderLifecycle) -> None:
|
|
self._rest_client = rest_client
|
|
|
|
@staticmethod
|
|
def _counter_side(side: str) -> str:
|
|
normalized = side.lower()
|
|
if normalized == "buy":
|
|
return "sell"
|
|
if normalized == "sell":
|
|
return "buy"
|
|
raise ValueError("side must be 'buy' or 'sell'")
|
|
|
|
@staticmethod
|
|
def _residual_volume(terminal_state: OrderFillState | None, requested_volume: float) -> float:
|
|
if requested_volume <= 0.0:
|
|
raise ValueError("requested_volume must be > 0.0")
|
|
if terminal_state is None or terminal_state.filled_volume is None:
|
|
return requested_volume
|
|
residual = requested_volume - terminal_state.filled_volume
|
|
return residual if residual > 0.0 else 0.0
|
|
|
|
async def recover_partial_fill(
|
|
self,
|
|
*,
|
|
order_id: str,
|
|
pair: str,
|
|
side: str,
|
|
requested_volume: float,
|
|
fill_result: FillMonitorResult,
|
|
) -> RecoveryAction:
|
|
if not order_id.strip():
|
|
raise ValueError("order_id must be non-empty")
|
|
|
|
cancel_response: dict[str, Any] | None = None
|
|
hedge_response: dict[str, Any] | None = None
|
|
hedged = False
|
|
canceled = False
|
|
reason = None
|
|
|
|
state = fill_result.terminal_state or fill_result.last_state
|
|
residual_volume = self._residual_volume(state, requested_volume)
|
|
|
|
if state is not None and state.status in {"open", "partial"}:
|
|
cancel_response = await self._rest_client.cancel_order(order_id=order_id)
|
|
canceled = True
|
|
reason = f"canceled_{state.status}_order"
|
|
|
|
if residual_volume > 0.0 and fill_result.timed_out:
|
|
hedge_response = await self._rest_client.place_market_order(
|
|
pair=pair,
|
|
side=self._counter_side(side),
|
|
volume=residual_volume,
|
|
)
|
|
hedged = True
|
|
if reason is None:
|
|
reason = "hedged_timed_out_order"
|
|
|
|
return RecoveryAction(
|
|
order_id=order_id,
|
|
canceled=canceled,
|
|
hedged=hedged,
|
|
hedge_pair=pair if hedged else None,
|
|
hedge_side=self._counter_side(side) if hedged else None,
|
|
hedge_volume=residual_volume if hedged else None,
|
|
cancel_response=cancel_response,
|
|
hedge_response=hedge_response,
|
|
reason=reason,
|
|
)
|