refactor: rewrite to CoinHunter V2 flat architecture
Replace the V1 commands/services split with a flat, direct architecture: - cli.py dispatches directly to service functions - New services: account, market, trade, opportunity - Thin Binance wrappers: spot_client, um_futures_client - Add audit logging, runtime paths, and TOML config - Remove legacy V1 code: commands/, precheck, review engine, smart executor - Add ruff + mypy toolchain and fix edge cases in trade params Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
124
tests/test_trade_service.py
Normal file
124
tests/test_trade_service.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""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(),
|
||||
)
|
||||
Reference in New Issue
Block a user