mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-07-30 13:47:44 +00:00
887 lines
30 KiB
Python
887 lines
30 KiB
Python
"""Comprehensive tests for the synchronous Trader module.
|
|
|
|
Tests cover:
|
|
- Trader initialization (__init__)
|
|
- set_trade_stop_levels_pips (long/short orders)
|
|
- set_trade_stop_levels_points (long/short orders)
|
|
- create_order_with_stops
|
|
- create_order_with_sl
|
|
- create_order_with_points
|
|
- create_order_no_stops
|
|
- check_order
|
|
- send_order
|
|
- record_trade
|
|
- place_trade (abstract method enforcement)
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
import pytest
|
|
|
|
from aiomql.lib.sync.trader import Trader
|
|
from aiomql.lib.sync.order import Order
|
|
from aiomql.lib.ram import RAM
|
|
from aiomql.lib.sync.symbol import Symbol
|
|
from aiomql.core.models import OrderType, OrderSendResult, OrderCheckResult
|
|
from aiomql.core.config import Config
|
|
from aiomql.core.task_queue import QueueItem
|
|
|
|
|
|
# --- Concrete subclass for testing abstract Trader ---
|
|
|
|
class ConcreteTrader(Trader):
|
|
"""Non-abstract subclass of Trader for testing purposes."""
|
|
|
|
def place_trade(self, *args, **kwargs):
|
|
pass
|
|
|
|
|
|
# --- Fixtures ---
|
|
|
|
@pytest.fixture
|
|
def mock_symbol():
|
|
"""Create a mock Symbol with standard forex attributes."""
|
|
symbol = MagicMock(spec=Symbol)
|
|
symbol.name = "EURUSD"
|
|
symbol.pip = 0.0001
|
|
symbol.point = 0.00001
|
|
symbol.digits = 5
|
|
symbol.volume_min = 0.01
|
|
symbol.compute_volume_sl = MagicMock(return_value=0.1)
|
|
symbol.compute_volume_points = MagicMock(return_value=0.1)
|
|
symbol.amount_in_quote_currency = MagicMock(return_value=100.0)
|
|
symbol.info_tick = MagicMock()
|
|
return symbol
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ram():
|
|
"""Create a mock RAM instance."""
|
|
ram = MagicMock(spec=RAM)
|
|
ram.risk_to_reward = 2.0
|
|
ram.get_amount = MagicMock(return_value=100.0)
|
|
return ram
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_tick():
|
|
"""Create a mock price tick."""
|
|
tick = MagicMock()
|
|
tick.ask = 1.10000
|
|
tick.bid = 1.09990
|
|
return tick
|
|
|
|
|
|
@pytest.fixture
|
|
def trader(mock_symbol, mock_ram, mock_tick):
|
|
"""Create a ConcreteTrader for testing."""
|
|
mock_symbol.info_tick.return_value = mock_tick
|
|
t = ConcreteTrader(symbol=mock_symbol, ram=mock_ram)
|
|
return t
|
|
|
|
|
|
# --- Tests ---
|
|
|
|
class TestTraderInit:
|
|
"""Test Trader initialization."""
|
|
|
|
def test_init_sets_symbol(self, mock_symbol, mock_ram):
|
|
"""Test __init__ sets symbol."""
|
|
t = ConcreteTrader(symbol=mock_symbol, ram=mock_ram)
|
|
assert t.symbol is mock_symbol
|
|
|
|
def test_init_sets_ram(self, mock_symbol, mock_ram):
|
|
"""Test __init__ sets provided RAM."""
|
|
t = ConcreteTrader(symbol=mock_symbol, ram=mock_ram)
|
|
assert t.ram is mock_ram
|
|
|
|
def test_init_creates_default_ram(self, mock_symbol):
|
|
"""Test __init__ creates default RAM when none provided."""
|
|
t = ConcreteTrader(symbol=mock_symbol)
|
|
assert isinstance(t.ram, RAM)
|
|
|
|
def test_init_creates_order(self, mock_symbol, mock_ram):
|
|
"""Test __init__ creates Order with symbol name."""
|
|
t = ConcreteTrader(symbol=mock_symbol, ram=mock_ram)
|
|
assert isinstance(t.order, Order)
|
|
assert t.order.symbol == "EURUSD"
|
|
|
|
def test_init_creates_config(self, mock_symbol, mock_ram):
|
|
"""Test __init__ creates Config instance."""
|
|
t = ConcreteTrader(symbol=mock_symbol, ram=mock_ram)
|
|
assert isinstance(t.config, Config)
|
|
|
|
def test_init_creates_empty_parameters(self, mock_symbol, mock_ram):
|
|
"""Test __init__ creates empty parameters dict."""
|
|
t = ConcreteTrader(symbol=mock_symbol, ram=mock_ram)
|
|
assert t.parameters == {}
|
|
|
|
|
|
class TestSetTradeStopLevelsPips:
|
|
"""Test set_trade_stop_levels_pips method."""
|
|
|
|
def test_long_order_sl_below_price(self, trader):
|
|
"""Test long order sets SL below price."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
|
|
trader.set_trade_stop_levels_pips(pips=50)
|
|
|
|
assert trader.order.sl < trader.order.price
|
|
|
|
def test_long_order_tp_above_price(self, trader):
|
|
"""Test long order sets TP above price."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
|
|
trader.set_trade_stop_levels_pips(pips=50)
|
|
|
|
assert trader.order.tp > trader.order.price
|
|
|
|
def test_short_order_sl_above_price(self, trader):
|
|
"""Test short order sets SL above price."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = False
|
|
trader.order.type.is_short = True
|
|
|
|
trader.set_trade_stop_levels_pips(pips=50)
|
|
|
|
assert trader.order.sl > trader.order.price
|
|
|
|
def test_short_order_tp_below_price(self, trader):
|
|
"""Test short order sets TP below price."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = False
|
|
trader.order.type.is_short = True
|
|
|
|
trader.set_trade_stop_levels_pips(pips=50)
|
|
|
|
assert trader.order.tp < trader.order.price
|
|
|
|
def test_custom_risk_to_reward(self, trader):
|
|
"""Test custom risk_to_reward overrides RAM default."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
|
|
trader.set_trade_stop_levels_pips(pips=50, risk_to_reward=3.0)
|
|
|
|
sl_distance = abs(trader.order.price - trader.order.sl)
|
|
tp_distance = abs(trader.order.tp - trader.order.price)
|
|
assert round(tp_distance / sl_distance, 1) == 3.0
|
|
|
|
def test_uses_ram_risk_to_reward_by_default(self, trader):
|
|
"""Test uses RAM risk_to_reward when not specified."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
trader.ram.risk_to_reward = 2.0
|
|
|
|
trader.set_trade_stop_levels_pips(pips=50)
|
|
|
|
sl_distance = abs(trader.order.price - trader.order.sl)
|
|
tp_distance = abs(trader.order.tp - trader.order.price)
|
|
assert round(tp_distance / sl_distance, 1) == 2.0
|
|
|
|
def test_rounds_to_symbol_digits(self, trader):
|
|
"""Test SL and TP are rounded to symbol.digits."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
|
|
trader.set_trade_stop_levels_pips(pips=50)
|
|
|
|
sl_str = f"{trader.order.sl:.10f}".rstrip("0")
|
|
tp_str = f"{trader.order.tp:.10f}".rstrip("0")
|
|
sl_decimals = len(sl_str.split(".")[1]) if "." in sl_str else 0
|
|
tp_decimals = len(tp_str.split(".")[1]) if "." in tp_str else 0
|
|
assert sl_decimals <= 5
|
|
assert tp_decimals <= 5
|
|
|
|
|
|
class TestSetTradeStopLevelsPoints:
|
|
"""Test set_trade_stop_levels_points method."""
|
|
|
|
def test_long_order_sl_below_price(self, trader):
|
|
"""Test long order sets SL below price."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
|
|
trader.set_trade_stop_levels_points(points=500)
|
|
|
|
assert trader.order.sl < trader.order.price
|
|
|
|
def test_long_order_tp_above_price(self, trader):
|
|
"""Test long order sets TP above price."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
|
|
trader.set_trade_stop_levels_points(points=500)
|
|
|
|
assert trader.order.tp > trader.order.price
|
|
|
|
def test_custom_risk_to_reward(self, trader):
|
|
"""Test custom risk_to_reward for points."""
|
|
trader.order.price = 1.10000
|
|
trader.order.type = MagicMock()
|
|
trader.order.type.is_long = True
|
|
trader.order.type.is_short = False
|
|
|
|
trader.set_trade_stop_levels_points(points=500, risk_to_reward=4.0)
|
|
|
|
sl_distance = abs(trader.order.price - trader.order.sl)
|
|
tp_distance = abs(trader.order.tp - trader.order.price)
|
|
assert round(tp_distance / sl_distance, 1) == 4.0
|
|
|
|
|
|
class TestCreateOrderWithStops:
|
|
"""Test create_order_with_stops method."""
|
|
|
|
def test_sets_order_attributes(self, trader, mock_tick):
|
|
"""Test sets sl, tp, volume, price, and type on order."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
trader.order.set_attributes.assert_called_once()
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
assert call_kwargs["sl"] == 1.09500
|
|
assert call_kwargs["tp"] == 1.11000
|
|
assert call_kwargs["type"] is order_type
|
|
|
|
def test_uses_ask_for_long(self, trader, mock_tick):
|
|
"""Test uses ask price for long orders."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
assert call_kwargs["price"] == mock_tick.ask
|
|
|
|
def test_uses_bid_for_short(self, trader, mock_tick):
|
|
"""Test uses bid price for short orders."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = False
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.10500, tp=1.09000
|
|
)
|
|
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
assert call_kwargs["price"] == mock_tick.bid
|
|
|
|
def test_calculates_volume(self, trader):
|
|
"""Test computes volume using symbol.compute_volume_sl."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
trader.symbol.compute_volume_sl.assert_called_once()
|
|
|
|
def test_custom_amount_to_risk(self, trader):
|
|
"""Test custom amount_to_risk overrides RAM.get_amount."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000, amount_to_risk=500.0
|
|
)
|
|
|
|
trader.ram.get_amount.assert_not_called()
|
|
|
|
def test_default_amount_from_ram(self, trader):
|
|
"""Test uses RAM.get_amount when amount_to_risk not specified."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
trader.ram.get_amount.assert_called_once()
|
|
|
|
def test_converts_amount_to_quote_currency(self, trader):
|
|
"""Test converts amount using symbol.amount_in_quote_currency."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
trader.symbol.amount_in_quote_currency.assert_called_once()
|
|
|
|
|
|
class TestCreateOrderWithSl:
|
|
"""Test create_order_with_sl method."""
|
|
|
|
def test_calculates_tp_from_sl(self, trader, mock_tick):
|
|
"""Test calculates TP based on SL distance and risk_to_reward."""
|
|
order_type = OrderType.BUY
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_sl(
|
|
order_type=order_type, sl=1.09500
|
|
)
|
|
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
assert call_kwargs["tp"] > call_kwargs["price"]
|
|
|
|
def test_buy_uses_ask_price(self, trader, mock_tick):
|
|
"""Test BUY order uses ask price."""
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_sl(
|
|
order_type=OrderType.BUY, sl=1.09500
|
|
)
|
|
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
assert call_kwargs["price"] == mock_tick.ask
|
|
|
|
def test_sell_uses_bid_price(self, trader, mock_tick):
|
|
"""Test SELL order uses bid price."""
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_sl(
|
|
order_type=OrderType.SELL, sl=1.10500
|
|
)
|
|
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
assert call_kwargs["price"] == mock_tick.bid
|
|
|
|
def test_custom_risk_to_reward(self, trader, mock_tick):
|
|
"""Test custom risk_to_reward affects TP calculation."""
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_sl(
|
|
order_type=OrderType.BUY, sl=1.09500, risk_to_reward=3.0
|
|
)
|
|
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
price = call_kwargs["price"]
|
|
sl_dist = abs(price - 1.09500)
|
|
tp_dist = abs(call_kwargs["tp"] - price)
|
|
assert round(tp_dist / sl_dist, 1) == 3.0
|
|
|
|
def test_sell_tp_below_price(self, trader, mock_tick):
|
|
"""Test SELL order sets TP below price."""
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_sl(
|
|
order_type=OrderType.SELL, sl=1.10500
|
|
)
|
|
|
|
call_kwargs = trader.order.set_attributes.call_args[1]
|
|
assert call_kwargs["tp"] < call_kwargs["price"]
|
|
|
|
def test_custom_amount_to_risk(self, trader):
|
|
"""Test custom amount_to_risk skips RAM.get_amount."""
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_sl(
|
|
order_type=OrderType.BUY, sl=1.09500, amount_to_risk=200.0
|
|
)
|
|
|
|
trader.ram.get_amount.assert_not_called()
|
|
|
|
|
|
class TestCreateOrderWithPoints:
|
|
"""Test create_order_with_points method."""
|
|
|
|
def test_sets_order_type(self, trader):
|
|
"""Test sets order type on order."""
|
|
trader.create_order_with_points(
|
|
order_type=OrderType.BUY, points=500
|
|
)
|
|
|
|
assert trader.order.type == OrderType.BUY
|
|
|
|
def test_sets_price_from_tick(self, trader, mock_tick):
|
|
"""Test sets price from tick."""
|
|
trader.create_order_with_points(
|
|
order_type=OrderType.BUY, points=500
|
|
)
|
|
|
|
assert trader.order.price == mock_tick.ask
|
|
|
|
def test_sell_uses_bid(self, trader, mock_tick):
|
|
"""Test SELL uses bid price."""
|
|
trader.create_order_with_points(
|
|
order_type=OrderType.SELL, points=500
|
|
)
|
|
|
|
assert trader.order.price == mock_tick.bid
|
|
|
|
def test_computes_volume_with_points(self, trader):
|
|
"""Test uses symbol.compute_volume_points."""
|
|
trader.create_order_with_points(
|
|
order_type=OrderType.BUY, points=500
|
|
)
|
|
|
|
trader.symbol.compute_volume_points.assert_called_once()
|
|
|
|
def test_sets_stop_levels(self, trader):
|
|
"""Test calls set_trade_stop_levels_points."""
|
|
with patch.object(trader, 'set_trade_stop_levels_points') as mock_set_stops:
|
|
trader.create_order_with_points(
|
|
order_type=OrderType.BUY, points=500
|
|
)
|
|
|
|
mock_set_stops.assert_called_once_with(points=500, risk_to_reward=None)
|
|
|
|
def test_custom_risk_to_reward(self, trader):
|
|
"""Test passes custom risk_to_reward to set_trade_stop_levels_points."""
|
|
with patch.object(trader, 'set_trade_stop_levels_points') as mock_set_stops:
|
|
trader.create_order_with_points(
|
|
order_type=OrderType.BUY, points=500, risk_to_reward=3.0
|
|
)
|
|
|
|
mock_set_stops.assert_called_once_with(points=500, risk_to_reward=3.0)
|
|
|
|
|
|
class TestCreateOrderNoStops:
|
|
"""Test create_order_no_stops method."""
|
|
|
|
def test_sets_order_type(self, trader):
|
|
"""Test sets order type."""
|
|
trader.create_order_no_stops(order_type=OrderType.BUY)
|
|
|
|
assert trader.order.type == OrderType.BUY
|
|
|
|
def test_uses_min_volume_default(self, trader):
|
|
"""Test uses symbol.volume_min when volume not specified."""
|
|
trader.symbol.volume_min = 0.01
|
|
|
|
trader.create_order_no_stops(order_type=OrderType.BUY)
|
|
|
|
assert trader.order.volume == 0.01
|
|
|
|
def test_custom_volume(self, trader):
|
|
"""Test uses custom volume when provided."""
|
|
trader.create_order_no_stops(order_type=OrderType.BUY, volume=1.5)
|
|
|
|
assert trader.order.volume == 1.5
|
|
|
|
def test_buy_uses_ask_price(self, trader, mock_tick):
|
|
"""Test BUY uses ask price."""
|
|
trader.create_order_no_stops(order_type=OrderType.BUY)
|
|
|
|
assert trader.order.price == mock_tick.ask
|
|
|
|
def test_sell_uses_bid_price(self, trader, mock_tick):
|
|
"""Test SELL uses bid price."""
|
|
trader.create_order_no_stops(order_type=OrderType.SELL)
|
|
|
|
assert trader.order.price == mock_tick.bid
|
|
|
|
|
|
class TestCheckOrder:
|
|
"""Test check_order method."""
|
|
|
|
def test_check_order_success(self, trader):
|
|
"""Test check_order returns OrderCheckResult on success."""
|
|
mock_result = MagicMock(spec=OrderCheckResult)
|
|
mock_result.retcode = 0
|
|
trader.order.check = MagicMock(return_value=mock_result)
|
|
|
|
result = trader.check_order()
|
|
|
|
assert result is mock_result
|
|
|
|
def test_check_order_none_result(self, trader):
|
|
"""Test check_order handles None result."""
|
|
trader.order.check = MagicMock(return_value=None)
|
|
trader.order.mt5 = MagicMock()
|
|
trader.order.mt5.error = "Connection failed"
|
|
|
|
result = trader.check_order()
|
|
|
|
assert result is None
|
|
|
|
def test_check_order_nonzero_retcode(self, trader):
|
|
"""Test check_order logs warning for non-zero retcode."""
|
|
mock_result = MagicMock(spec=OrderCheckResult)
|
|
mock_result.retcode = 10015
|
|
mock_result.comment = "Invalid price"
|
|
trader.order.check = MagicMock(return_value=mock_result)
|
|
|
|
result = trader.check_order()
|
|
|
|
assert result is mock_result
|
|
assert result.retcode != 0
|
|
|
|
def test_check_order_calls_order_check(self, trader):
|
|
"""Test check_order delegates to order.check."""
|
|
mock_result = MagicMock(spec=OrderCheckResult)
|
|
mock_result.retcode = 0
|
|
trader.order.check = MagicMock(return_value=mock_result)
|
|
|
|
trader.check_order()
|
|
|
|
trader.order.check.assert_called_once()
|
|
|
|
|
|
class TestSendOrder:
|
|
"""Test send_order method."""
|
|
|
|
def test_send_order_success(self, trader):
|
|
"""Test send_order returns result on success (retcode 10009)."""
|
|
mock_result = MagicMock(spec=OrderSendResult)
|
|
mock_result.retcode = 10009
|
|
trader.order.send = MagicMock(return_value=mock_result)
|
|
|
|
result = trader.send_order()
|
|
|
|
assert result is mock_result
|
|
assert result.retcode == 10009
|
|
|
|
def test_send_order_none_result(self, trader):
|
|
"""Test send_order handles None result."""
|
|
trader.order.send = MagicMock(return_value=None)
|
|
trader.order.mt5 = MagicMock()
|
|
trader.order.mt5.error = "No connection"
|
|
|
|
result = trader.send_order()
|
|
|
|
assert result is None
|
|
|
|
def test_send_order_failure_retcode(self, trader):
|
|
"""Test send_order returns result for non-10009 retcode."""
|
|
mock_result = MagicMock(spec=OrderSendResult)
|
|
mock_result.retcode = 10006
|
|
mock_result.comment = "Requote"
|
|
trader.order.send = MagicMock(return_value=mock_result)
|
|
|
|
result = trader.send_order()
|
|
|
|
assert result is mock_result
|
|
assert result.retcode == 10006
|
|
|
|
def test_send_order_calls_order_send(self, trader):
|
|
"""Test send_order delegates to order.send."""
|
|
mock_result = MagicMock(spec=OrderSendResult)
|
|
mock_result.retcode = 10009
|
|
trader.order.send = MagicMock(return_value=mock_result)
|
|
|
|
trader.send_order()
|
|
|
|
trader.order.send.assert_called_once()
|
|
|
|
|
|
class TestRecordTrade:
|
|
"""Test record_trade method."""
|
|
|
|
@pytest.fixture
|
|
def successful_result(self):
|
|
"""Create a successful OrderSendResult."""
|
|
result = MagicMock(spec=OrderSendResult)
|
|
result.retcode = 10009
|
|
result.order = 12345
|
|
result.request = MagicMock()
|
|
return result
|
|
|
|
def test_record_trade_skips_when_disabled(self, trader, successful_result):
|
|
"""Test record_trade does nothing when record_trades is False."""
|
|
trader.config.record_trades = False
|
|
|
|
trader.record_trade(result=successful_result)
|
|
|
|
def test_record_trade_skips_failed_orders(self, trader):
|
|
"""Test record_trade skips when retcode is not 10009."""
|
|
trader.config.record_trades = True
|
|
result = MagicMock(spec=OrderSendResult)
|
|
result.retcode = 10006
|
|
|
|
trader.record_trade(result=result)
|
|
|
|
def test_record_trade_uses_task_queue(self, trader, successful_result):
|
|
"""Test record_trade adds to task queue by default."""
|
|
trader.config.record_trades = True
|
|
trader.config.task_queue = MagicMock()
|
|
trader.config.task_queue.add = MagicMock()
|
|
|
|
mock_order = MagicMock()
|
|
mock_order.sl = 1.09500
|
|
mock_order.tp = 1.11000
|
|
mock_order.time_setup_msc = 1705312800000
|
|
trader.order.get_history_order_by_ticket = MagicMock(return_value=mock_order)
|
|
trader.order.calc_profit = MagicMock(return_value=50.0)
|
|
|
|
with patch('aiomql.lib.sync.trader.Result') as MockResult:
|
|
mock_res = MagicMock()
|
|
mock_res.save_sync = MagicMock()
|
|
MockResult.return_value = mock_res
|
|
|
|
trader.record_trade(result=successful_result, parameters={"key": "value"})
|
|
|
|
trader.config.task_queue.add.assert_called_once()
|
|
|
|
def test_record_trade_direct_save(self, trader, successful_result):
|
|
"""Test record_trade saves directly when use_task_queue=False."""
|
|
trader.config.record_trades = True
|
|
|
|
mock_order = MagicMock()
|
|
mock_order.sl = 1.09500
|
|
mock_order.tp = 1.11000
|
|
mock_order.time_setup_msc = 1705312800000
|
|
trader.order.get_history_order_by_ticket = MagicMock(return_value=mock_order)
|
|
trader.order.calc_profit = MagicMock(return_value=50.0)
|
|
|
|
with patch('aiomql.lib.sync.trader.Result') as MockResult:
|
|
mock_res = MagicMock()
|
|
mock_res.save_sync = MagicMock()
|
|
MockResult.return_value = mock_res
|
|
|
|
trader.record_trade(
|
|
result=successful_result,
|
|
use_task_queue=False
|
|
)
|
|
|
|
mock_res.save_sync.assert_called_once()
|
|
|
|
def test_record_trade_with_parameters(self, trader, successful_result):
|
|
"""Test record_trade passes parameters to Result."""
|
|
trader.config.record_trades = True
|
|
trader.config.task_queue = MagicMock()
|
|
trader.config.task_queue.add = MagicMock()
|
|
|
|
mock_order = MagicMock()
|
|
mock_order.sl = 1.09500
|
|
mock_order.tp = 1.11000
|
|
mock_order.time_setup_msc = 1705312800000
|
|
trader.order.get_history_order_by_ticket = MagicMock(return_value=mock_order)
|
|
trader.order.calc_profit = MagicMock(return_value=50.0)
|
|
|
|
params = {"strategy": "scalping", "timeframe": "M5"}
|
|
|
|
with patch('aiomql.lib.sync.trader.Result') as MockResult:
|
|
mock_res = MagicMock()
|
|
mock_res.save_sync = MagicMock()
|
|
MockResult.return_value = mock_res
|
|
|
|
trader.record_trade(
|
|
result=successful_result, parameters=params, name="TestStrategy"
|
|
)
|
|
|
|
call_kwargs = MockResult.call_args[1]
|
|
assert call_kwargs["parameters"] == params
|
|
assert call_kwargs["name"] == "TestStrategy"
|
|
|
|
def test_record_trade_with_expected_profit(self, trader, successful_result):
|
|
"""Test record_trade uses provided expected_profit."""
|
|
trader.config.record_trades = True
|
|
trader.config.task_queue = MagicMock()
|
|
trader.config.task_queue.add = MagicMock()
|
|
|
|
mock_order = MagicMock()
|
|
mock_order.sl = 1.09500
|
|
mock_order.tp = 1.11000
|
|
mock_order.time_setup_msc = 1705312800000
|
|
trader.order.get_history_order_by_ticket = MagicMock(return_value=mock_order)
|
|
|
|
with patch('aiomql.lib.sync.trader.Result') as MockResult:
|
|
mock_res = MagicMock()
|
|
mock_res.save_sync = MagicMock()
|
|
MockResult.return_value = mock_res
|
|
|
|
trader.record_trade(
|
|
result=successful_result, expected_profit=75.0
|
|
)
|
|
|
|
call_kwargs = MockResult.call_args[1]
|
|
assert call_kwargs["expected_profit"] == 75.0
|
|
|
|
def test_record_trade_non_dict_parameters(self, trader, successful_result):
|
|
"""Test record_trade handles non-dict parameters."""
|
|
trader.config.record_trades = True
|
|
trader.config.task_queue = MagicMock()
|
|
trader.config.task_queue.add = MagicMock()
|
|
|
|
mock_order = MagicMock()
|
|
mock_order.sl = 1.09500
|
|
mock_order.tp = 1.11000
|
|
mock_order.time_setup_msc = 1705312800000
|
|
trader.order.get_history_order_by_ticket = MagicMock(return_value=mock_order)
|
|
trader.order.calc_profit = MagicMock(return_value=10.0)
|
|
|
|
with patch('aiomql.lib.sync.trader.Result') as MockResult:
|
|
mock_res = MagicMock()
|
|
mock_res.save_sync = MagicMock()
|
|
MockResult.return_value = mock_res
|
|
|
|
trader.record_trade(
|
|
result=successful_result, parameters="not_a_dict"
|
|
)
|
|
|
|
call_kwargs = MockResult.call_args[1]
|
|
assert call_kwargs["parameters"] == {}
|
|
|
|
def test_record_trade_updates_sl_tp_from_history(self, trader, successful_result):
|
|
"""Test record_trade sets sl and tp on result.request from history order."""
|
|
trader.config.record_trades = True
|
|
trader.config.task_queue = MagicMock()
|
|
trader.config.task_queue.add = MagicMock()
|
|
|
|
mock_order = MagicMock()
|
|
mock_order.sl = 1.08000
|
|
mock_order.tp = 1.12000
|
|
mock_order.time_setup_msc = 1705312800000
|
|
trader.order.get_history_order_by_ticket = MagicMock(return_value=mock_order)
|
|
trader.order.calc_profit = MagicMock(return_value=10.0)
|
|
|
|
with patch('aiomql.lib.sync.trader.Result') as MockResult:
|
|
mock_res = MagicMock()
|
|
mock_res.save_sync = MagicMock()
|
|
MockResult.return_value = mock_res
|
|
|
|
trader.record_trade(result=successful_result)
|
|
|
|
assert successful_result.request.sl == 1.08000
|
|
assert successful_result.request.tp == 1.12000
|
|
|
|
|
|
class TestPlaceTrade:
|
|
"""Test abstract place_trade method."""
|
|
|
|
def test_cannot_instantiate_trader_directly(self, mock_symbol, mock_ram):
|
|
"""Test Trader cannot be instantiated due to abstract method."""
|
|
with pytest.raises(TypeError):
|
|
Trader(symbol=mock_symbol, ram=mock_ram)
|
|
|
|
def test_subclass_must_implement_place_trade(self):
|
|
"""Test subclass without place_trade raises TypeError."""
|
|
with pytest.raises(TypeError):
|
|
class IncompleteTrader(Trader):
|
|
pass
|
|
|
|
IncompleteTrader(symbol=MagicMock())
|
|
|
|
def test_concrete_place_trade_callable(self, trader):
|
|
"""Test ConcreteTrader.place_trade is callable."""
|
|
trader.place_trade()
|
|
|
|
|
|
class TestIntegration:
|
|
"""Integration tests for sync Trader."""
|
|
|
|
def test_create_and_check_order(self, trader, mock_tick):
|
|
"""Test creating order then checking it."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
mock_check_result = MagicMock(spec=OrderCheckResult)
|
|
mock_check_result.retcode = 0
|
|
trader.order.check = MagicMock(return_value=mock_check_result)
|
|
|
|
check = trader.check_order()
|
|
assert check.retcode == 0
|
|
|
|
def test_create_and_send_order(self, trader, mock_tick):
|
|
"""Test creating order then sending it."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
mock_send_result = MagicMock(spec=OrderSendResult)
|
|
mock_send_result.retcode = 10009
|
|
trader.order.send = MagicMock(return_value=mock_send_result)
|
|
|
|
result = trader.send_order()
|
|
assert result.retcode == 10009
|
|
|
|
def test_full_trade_lifecycle(self, trader, mock_tick):
|
|
"""Test full lifecycle: create, check, send, record."""
|
|
order_type = MagicMock()
|
|
order_type.is_long = True
|
|
|
|
trader.order.set_attributes = MagicMock()
|
|
|
|
# Create
|
|
trader.create_order_with_stops(
|
|
order_type=order_type, sl=1.09500, tp=1.11000
|
|
)
|
|
|
|
# Check
|
|
mock_check = MagicMock(spec=OrderCheckResult)
|
|
mock_check.retcode = 0
|
|
trader.order.check = MagicMock(return_value=mock_check)
|
|
check = trader.check_order()
|
|
assert check.retcode == 0
|
|
|
|
# Send
|
|
mock_result = MagicMock(spec=OrderSendResult)
|
|
mock_result.retcode = 10009
|
|
mock_result.order = 99999
|
|
mock_result.request = MagicMock()
|
|
trader.order.send = MagicMock(return_value=mock_result)
|
|
result = trader.send_order()
|
|
assert result.retcode == 10009
|
|
|
|
# Record
|
|
trader.config.record_trades = True
|
|
trader.config.task_queue = MagicMock()
|
|
trader.config.task_queue.add = MagicMock()
|
|
|
|
mock_history_order = MagicMock()
|
|
mock_history_order.sl = 1.09500
|
|
mock_history_order.tp = 1.11000
|
|
mock_history_order.time_setup_msc = 1705312800000
|
|
trader.order.get_history_order_by_ticket = MagicMock(return_value=mock_history_order)
|
|
trader.order.calc_profit = MagicMock(return_value=50.0)
|
|
|
|
with patch('aiomql.lib.sync.trader.Result') as MockResult:
|
|
mock_res = MagicMock()
|
|
mock_res.save_sync = MagicMock()
|
|
MockResult.return_value = mock_res
|
|
|
|
trader.record_trade(result=result, name="TestStrategy")
|
|
|
|
trader.config.task_queue.add.assert_called_once()
|