"""Trade execution services.""" from __future__ import annotations from dataclasses import asdict, dataclass from typing import Any from ..audit import audit_event from .market_service import normalize_symbol @dataclass class TradeIntent: market_type: str symbol: str side: str order_type: str qty: float | None quote_amount: float | None price: float | None reduce_only: bool dry_run: bool @dataclass class TradeResult: market_type: str symbol: str side: str order_type: str status: str dry_run: bool request_payload: dict[str, Any] response_payload: dict[str, Any] def _default_dry_run(config: dict[str, Any], dry_run: bool | None) -> bool: if dry_run is not None: return dry_run return bool(config.get("trading", {}).get("dry_run_default", False)) def _trade_log_payload( intent: TradeIntent, payload: dict[str, Any], *, status: str, error: str | None = None ) -> dict[str, Any]: return { "market_type": intent.market_type, "symbol": intent.symbol, "side": intent.side, "qty": intent.qty, "quote_amount": intent.quote_amount, "order_type": intent.order_type, "dry_run": intent.dry_run, "request_payload": payload, "response_payload": {} if error else payload, "status": status, "error": error, } def execute_spot_trade( config: dict[str, Any], *, side: str, symbol: str, qty: float | None, quote: float | None, order_type: str, price: float | None, dry_run: bool | None, spot_client: Any, ) -> dict[str, Any]: normalized_symbol = normalize_symbol(symbol) order_type = order_type.upper() side = side.upper() is_dry_run = _default_dry_run(config, dry_run) if side == "BUY" and order_type == "MARKET": if quote is None: raise RuntimeError("Spot market buy requires --quote") if qty is not None: raise RuntimeError("Spot market buy accepts --quote only; do not pass --qty") if side == "SELL": if qty is None: raise RuntimeError("Spot sell requires --qty") if quote is not None: raise RuntimeError("Spot sell accepts --qty only; do not pass --quote") if order_type == "LIMIT" and (qty is None or price is None): raise RuntimeError("Limit orders require both --qty and --price") payload: dict[str, Any] = { "symbol": normalized_symbol, "side": side, "type": order_type, } if qty is not None: payload["quantity"] = qty if quote is not None: payload["quoteOrderQty"] = quote if price is not None: payload["price"] = price payload["timeInForce"] = "GTC" intent = TradeIntent( market_type="spot", symbol=normalized_symbol, side=side, order_type=order_type, qty=qty, quote_amount=quote, price=price, reduce_only=False, dry_run=is_dry_run, ) audit_event("trade_submitted", _trade_log_payload(intent, payload, status="submitted"), dry_run=intent.dry_run) if is_dry_run: response = {"dry_run": True, "status": "DRY_RUN", "request": payload} result = asdict( TradeResult( market_type="spot", symbol=normalized_symbol, side=side, order_type=order_type, status="DRY_RUN", dry_run=True, request_payload=payload, response_payload=response, ) ) audit_event( "trade_filled", {**_trade_log_payload(intent, payload, status="DRY_RUN"), "response_payload": response}, dry_run=intent.dry_run ) return {"trade": result} try: response = spot_client.new_order(**payload) except Exception as exc: audit_event("trade_failed", _trade_log_payload(intent, payload, status="failed", error=str(exc)), dry_run=intent.dry_run) raise RuntimeError(f"Spot order failed: {exc}") from exc result = asdict( TradeResult( market_type="spot", symbol=normalized_symbol, side=side, order_type=order_type, status=str(response.get("status", "UNKNOWN")), dry_run=False, request_payload=payload, response_payload=response, ) ) audit_event( "trade_filled", {**_trade_log_payload(intent, payload, status=result["status"]), "response_payload": response}, dry_run=intent.dry_run, ) return {"trade": result}