"""Tests for trade execution dry-run paths.""" import pytest from coinhunter.services import trade_execution as te from coinhunter.services.trade_common import set_dry_run class TestMarketSellDryRun: def test_returns_dry_run_order(self, monkeypatch): set_dry_run(True) monkeypatch.setattr( te, "prepare_sell_quantity", lambda ex, sym, qty: ("BTC/USDT", 0.5, 60000.0, 30000.0) ) order = te.market_sell(None, "BTCUSDT", 0.5, "dec-123") assert order["id"] == "dry-sell-dec-123" assert order["symbol"] == "BTC/USDT" assert order["amount"] == 0.5 assert order["status"] == "closed" set_dry_run(False) class TestMarketBuyDryRun: def test_returns_dry_run_order(self, monkeypatch): set_dry_run(True) monkeypatch.setattr( te, "prepare_buy_quantity", lambda ex, sym, amt: ("ETH/USDT", 1.0, 3000.0, 3000.0) ) order = te.market_buy(None, "ETHUSDT", 100, "dec-456") assert order["id"] == "dry-buy-dec-456" assert order["symbol"] == "ETH/USDT" assert order["amount"] == 1.0 assert order["status"] == "closed" set_dry_run(False) class TestActionSellAll: def test_raises_when_balance_zero(self, monkeypatch): monkeypatch.setattr(te, "fetch_balances", lambda ex: {"BTC": 0}) with pytest.raises(RuntimeError, match="balance is zero"): te.action_sell_all(None, "BTCUSDT", "dec-789", {}) def test_dry_run_does_not_reconcile(self, monkeypatch): set_dry_run(True) monkeypatch.setattr(te, "fetch_balances", lambda ex: {"BTC": 0.5}) monkeypatch.setattr( te, "market_sell", lambda ex, sym, qty, did: {"id": "dry-1", "amount": qty, "price": 60000.0, "cost": 30000.0, "status": "closed"} ) monkeypatch.setattr(te, "reconcile_positions_with_exchange", lambda ex, hint=None: ([], {})) monkeypatch.setattr(te, "log_trade", lambda *a, **k: None) monkeypatch.setattr(te, "log_decision", lambda *a, **k: None) result = te.action_sell_all(None, "BTCUSDT", "dec-789", {"analysis": "test"}) assert result["id"] == "dry-1" set_dry_run(False) class TestActionBuy: def test_raises_when_insufficient_usdt(self, monkeypatch): monkeypatch.setattr(te, "fetch_balances", lambda ex: {"USDT": 10.0}) with pytest.raises(RuntimeError, match="Insufficient USDT"): te.action_buy(None, "BTCUSDT", 50, "dec-abc", {}) def test_dry_run_skips_save(self, monkeypatch): set_dry_run(True) monkeypatch.setattr(te, "fetch_balances", lambda ex: {"USDT": 100.0}) monkeypatch.setattr( te, "market_buy", lambda ex, sym, amt, did: {"id": "dry-2", "amount": 0.01, "price": 50000.0, "cost": 500.0, "status": "closed"} ) monkeypatch.setattr(te, "load_positions", lambda: []) monkeypatch.setattr(te, "upsert_position", lambda positions, pos: positions.append(pos)) monkeypatch.setattr(te, "reconcile_positions_with_exchange", lambda ex, hint=None: ([], {})) monkeypatch.setattr(te, "log_trade", lambda *a, **k: None) monkeypatch.setattr(te, "log_decision", lambda *a, **k: None) result = te.action_buy(None, "BTCUSDT", 50, "dec-abc", {}) assert result["id"] == "dry-2" set_dry_run(False) class TestCommandBalances: def test_returns_balances(self, monkeypatch, capsys): monkeypatch.setattr(te, "fetch_balances", lambda ex: {"USDT": 100.0, "BTC": 0.5}) result = te.command_balances(None) assert result == {"USDT": 100.0, "BTC": 0.5} captured = capsys.readouterr() assert '"USDT": 100.0' in captured.out class TestCommandStatus: def test_returns_snapshot(self, monkeypatch, capsys): monkeypatch.setattr(te, "fetch_balances", lambda ex: {"USDT": 100.0}) monkeypatch.setattr(te, "load_positions", lambda: []) monkeypatch.setattr(te, "build_market_snapshot", lambda ex: {"BTC/USDT": 50000.0}) result = te.command_status(None) assert result["balances"]["USDT"] == 100.0 captured = capsys.readouterr() assert '"balances"' in captured.out class TestCommandOrders: def test_returns_orders(self, monkeypatch, capsys): orders = [{"id": "1", "symbol": "BTC/USDT"}] monkeypatch.setattr(te, "print_json", lambda d: None) ex = type("Ex", (), {"fetch_open_orders": lambda self: orders})() result = te.command_orders(ex) assert result == orders class TestCommandOrderStatus: def test_returns_order(self, monkeypatch, capsys): order = {"id": "123", "status": "open"} monkeypatch.setattr(te, "print_json", lambda d: None) ex = type("Ex", (), {"fetch_order": lambda self, oid, sym: order})() result = te.command_order_status(ex, "BTCUSDT", "123") assert result == order class TestCommandCancel: def test_dry_run_returns_early(self, monkeypatch): set_dry_run(True) monkeypatch.setattr(te, "log", lambda msg: None) result = te.command_cancel(None, "BTCUSDT", "123") assert result["dry_run"] is True set_dry_run(False) def test_cancel_by_order_id(self, monkeypatch): set_dry_run(False) monkeypatch.setattr(te, "print_json", lambda d: None) ex = type("Ex", (), {"cancel_order": lambda self, oid, sym: {"id": oid, "status": "canceled"}})() result = te.command_cancel(ex, "BTCUSDT", "123") assert result["status"] == "canceled" def test_cancel_latest_when_no_order_id(self, monkeypatch): set_dry_run(False) monkeypatch.setattr(te, "print_json", lambda d: None) ex = type("Ex", (), { "fetch_open_orders": lambda self, sym: [{"id": "999"}, {"id": "888"}], "cancel_order": lambda self, oid, sym: {"id": oid, "status": "canceled"}, })() result = te.command_cancel(ex, "BTCUSDT", None) assert result["id"] == "888" def test_cancel_raises_when_no_open_orders(self, monkeypatch): set_dry_run(False) ex = type("Ex", (), {"fetch_open_orders": lambda self, sym: []})() with pytest.raises(RuntimeError, match="No open orders"): te.command_cancel(ex, "BTCUSDT", None) class TestPrintJson: def test_outputs_sorted_json(self, capsys): te.print_json({"b": 2, "a": 1}) captured = capsys.readouterr() assert '"a": 1' in captured.out assert '"b": 2' in captured.out