From 1f01990139340f8465318229f91412b92154a6e1 Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Wed, 8 Jul 2026 06:43:57 +0000 Subject: [PATCH] Add test for orders and positions --- tests/test_trading.py | 190 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 tests/test_trading.py diff --git a/tests/test_trading.py b/tests/test_trading.py new file mode 100644 index 0000000..2644cde --- /dev/null +++ b/tests/test_trading.py @@ -0,0 +1,190 @@ +"""Tests for account, positions, and orders.""" + +import responses +import pytest + +import mt5bridge_ccxt + + +class TestFetchBalance: + @responses.activate + def test_returns_ccxt_balance(self, exchange_config, mock_account): + responses.add( + responses.GET, + "http://mock-mt5bridge:8080/account", + json=mock_account, status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + bal = ex.fetch_balance() + + assert "info" in bal + assert bal["free"]["USD"] == 9600.0 + assert bal["used"]["USD"] == 500.0 + assert bal["total"]["USD"] == 10000.0 + assert "debt" in bal + assert bal["timestamp"] > 0 + + +class TestFetchPositions: + @responses.activate + def test_returns_long_position(self, exchange_config, mock_positions): + responses.add( + responses.GET, + "http://mock-mt5bridge:8080/positions", + json=mock_positions, status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + positions = ex.fetch_positions() + + assert len(positions) == 1 + p = positions[0] + assert p["symbol"] == "XAU/USD" + assert p["side"] == "long" + assert p["contracts"] == 0.01 + assert p["entryPrice"] == 4180.0 + assert p["markPrice"] == 4185.0 + assert p["unrealizedPnl"] == 0.5 + assert p["stopLossPrice"] == 4170.0 + assert p["takeProfitPrice"] == 4190.0 + + @responses.activate + def test_filter_by_symbol(self, exchange_config, mock_positions): + responses.add( + responses.GET, + "http://mock-mt5bridge:8080/positions", + json=mock_positions, status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + # Filter to a symbol we don't have + positions = ex.fetch_positions(["BTC/USD"]) + assert len(positions) == 0 + + @responses.activate + def test_short_position(self, exchange_config): + responses.add( + responses.GET, + "http://mock-mt5bridge:8080/positions", + json={"data": [{ + "ticket": 2002, "symbol": "XAUUSDc", "type": 1, + "volume": 0.02, "price_open": 4200.0, "sl": 0, "tp": 0, + "price_current": 4195.0, "swap": 0.0, "profit": 1.0, + "comment": "", "magic": 0, + }], "count": 1, "format": "json"}, + status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + positions = ex.fetch_positions() + assert positions[0]["side"] == "short" + + +class TestCreateOrder: + @responses.activate + def test_market_buy_succeeds(self, exchange_config, mock_order_result): + responses.add( + responses.POST, + "http://mock-mt5bridge:8080/order/send", + json=mock_order_result, status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + order = ex.create_order("XAU/USD", "market", "buy", 0.01) + + assert order["id"] == "2001" + assert order["symbol"] == "XAU/USD" + assert order["side"] == "buy" + assert order["type"] == "market" + assert order["amount"] == 0.01 + assert order["status"] == "open" + + # Verify request body + call = responses.calls[0] + import json + body = json.loads(call.request.body) + assert body["request"]["symbol"] == "XAUUSDc" + assert body["request"]["volume"] == 0.01 + assert body["request"]["order_type"] == 0 # market buy + assert body["request"]["action"] == 1 + + @responses.activate + def test_limit_order_requires_price(self, exchange_config): + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + from ccxt.base.errors import ArgumentsRequired + with pytest.raises(ArgumentsRequired): + ex.create_order("XAU/USD", "limit", "buy", 0.01) + + @responses.activate + def test_rejected_order_raises(self, exchange_config): + responses.add( + responses.POST, + "http://mock-mt5bridge:8080/order/send", + json={"data": {"retcode": 10019, "order": 0, "comment": "No money"}, + "count": 1, "format": "json"}, + status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + from ccxt.base.errors import InvalidOrder + with pytest.raises(InvalidOrder): + ex.create_order("XAU/USD", "market", "buy", 0.01) + + @responses.activate + def test_insufficient_funds_raises(self, exchange_config): + responses.add( + responses.POST, + "http://mock-mt5bridge:8080/order/send", + json={"data": {"retcode": 10019, "order": 0, "comment": "Not enough money"}, + "count": 1, "format": "json"}, + status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + from ccxt.base.errors import InsufficientFunds + with pytest.raises(InsufficientFunds): + ex.create_order("XAU/USD", "market", "buy", 0.01) + + @responses.activate + def test_invalid_order_type(self, exchange_config): + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + from ccxt.base.errors import InvalidOrder + with pytest.raises(InvalidOrder): + ex.create_order("XAU/USD", "weird_type", "buy", 0.01) + + @responses.activate + def test_check_first_option(self, exchange_config, mock_order_result): + responses.add( + responses.POST, + "http://mock-mt5bridge:8080/order/check", + json={"data": {"retcode": 0, "balance": 10000, "equity": 10100, + "profit": 0, "margin": 0, "margin_free": 10100, + "margin_level": 0, "comment": "OK"}, + "count": 1, "format": "json"}, + status=200, + ) + responses.add( + responses.POST, + "http://mock-mt5bridge:8080/order/send", + json=mock_order_result, status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + order = ex.create_order("XAU/USD", "market", "buy", 0.01, + params={"check_first": True}) + assert order["status"] == "open" + + +class TestCancelOrder: + @responses.activate + def test_cancel_sends_gvar(self, exchange_config): + responses.add( + responses.POST, + "http://mock-mt5bridge:8080/gvar/MT5BRIDGE_CMD_CANCEL_12345", + json={"name": "MT5BRIDGE_CMD_CANCEL_12345", "value": 0.0}, + status=200, + ) + responses.add( + responses.POST, + "http://mock-mt5bridge:8080/gvar/MT5BRIDGE_LAST_CANCEL", + json={"name": "MT5BRIDGE_LAST_CANCEL", "value": 12345.0}, + status=200, + ) + ex = mt5bridge_ccxt.mt5bridge(exchange_config) + result = ex.cancel_order("12345") + assert result["id"] == "12345" + assert result["status"] == "canceling" + assert result["info"]["method"] == "gvar_signal"