"""Trade execution tests.""" from __future__ import annotations import unittest from unittest.mock import patch from coinhunter.services import trade_service class FakeSpotClient: def __init__(self): self.calls = [] def new_order(self, **kwargs): self.calls.append(kwargs) return {"symbol": kwargs["symbol"], "status": "FILLED", "orderId": 1} class FakeFuturesClient: def __init__(self): self.calls = [] def new_order(self, **kwargs): self.calls.append(kwargs) return {"symbol": kwargs["symbol"], "status": "FILLED", "orderId": 2} def position_risk(self, symbol=None): return [{"symbol": "BTCUSDT", "positionAmt": "-0.02", "notional": "-1200"}] class TradeServiceTestCase(unittest.TestCase): def test_spot_market_buy_dry_run_does_not_call_client(self): events = [] with patch.object(trade_service, "audit_event", side_effect=lambda event, payload: events.append((event, payload))): client = FakeSpotClient() payload = trade_service.execute_spot_trade( {"trading": {"dry_run_default": False}}, side="buy", symbol="btc/usdt", qty=None, quote=100, order_type="market", price=None, dry_run=True, spot_client=client, ) self.assertEqual(payload["trade"]["status"], "DRY_RUN") self.assertEqual(client.calls, []) self.assertEqual([event for event, _ in events], ["trade_submitted", "trade_filled"]) def test_spot_limit_sell_maps_payload(self): with patch.object(trade_service, "audit_event", return_value=None): client = FakeSpotClient() payload = trade_service.execute_spot_trade( {"trading": {"dry_run_default": False}}, side="sell", symbol="BTCUSDT", qty=0.1, quote=None, order_type="limit", price=90000, dry_run=False, spot_client=client, ) self.assertEqual(payload["trade"]["status"], "FILLED") self.assertEqual(client.calls[0]["timeInForce"], "GTC") def test_futures_close_uses_opposite_side(self): with patch.object(trade_service, "audit_event", return_value=None): client = FakeFuturesClient() payload = trade_service.close_futures_position( {"trading": {"dry_run_default": False}}, symbol="BTCUSDT", dry_run=False, futures_client=client, ) self.assertEqual(payload["trade"]["side"], "BUY") self.assertEqual(client.calls[0]["reduceOnly"], "true") def test_spot_market_buy_requires_quote(self): with patch.object(trade_service, "audit_event", return_value=None): with self.assertRaisesRegex(RuntimeError, "requires --quote"): trade_service.execute_spot_trade( {"trading": {"dry_run_default": False}}, side="buy", symbol="BTCUSDT", qty=None, quote=None, order_type="market", price=None, dry_run=False, spot_client=FakeSpotClient(), ) def test_spot_market_buy_rejects_qty(self): with patch.object(trade_service, "audit_event", return_value=None): with self.assertRaisesRegex(RuntimeError, "accepts --quote only"): trade_service.execute_spot_trade( {"trading": {"dry_run_default": False}}, side="buy", symbol="BTCUSDT", qty=0.1, quote=100, order_type="market", price=None, dry_run=False, spot_client=FakeSpotClient(), ) def test_spot_market_sell_rejects_quote(self): with patch.object(trade_service, "audit_event", return_value=None): with self.assertRaisesRegex(RuntimeError, "accepts --qty only"): trade_service.execute_spot_trade( {"trading": {"dry_run_default": False}}, side="sell", symbol="BTCUSDT", qty=0.1, quote=100, order_type="market", price=None, dry_run=False, spot_client=FakeSpotClient(), )