Files

887 lines
30 KiB
Python
Raw Permalink Normal View History

"""Comprehensive tests for the synchronous Trader module.
Tests cover:
2026-02-21 07:43:33 +01:00
- 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)
"""
2026-02-21 07:43:33 +01:00
from unittest.mock import MagicMock, patch
import pytest
from aiomql.lib.sync.trader import Trader
from aiomql.lib.sync.order import Order
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
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."""
2026-02-21 07:43:33 +01:00
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."""
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
trader.set_trade_stop_levels_points(points=500)
2026-02-21 07:43:33 +01:00
assert trader.order.sl < trader.order.price
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
trader.set_trade_stop_levels_points(points=500)
2026-02-21 07:43:33 +01:00
assert trader.order.tp > trader.order.price
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
trader.set_trade_stop_levels_points(points=500, risk_to_reward=4.0)
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
class TestCreateOrderWithStops:
"""Test create_order_with_stops method."""
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
trader.order.set_attributes = MagicMock()
2026-02-21 07:43:33 +01:00
trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000
)
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
trader.order.set_attributes = MagicMock()
2026-02-21 07:43:33 +01:00
trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000
)
2026-02-21 07:43:33 +01:00
call_kwargs = trader.order.set_attributes.call_args[1]
assert call_kwargs["price"] == mock_tick.ask
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
trader.order.set_attributes = MagicMock()
2026-02-21 07:43:33 +01:00
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."""
2026-02-21 07:43:33 +01:00
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
2026-02-21 07:43:33 +01:00
trader.order.set_attributes = MagicMock()
2026-02-21 07:43:33 +01:00
trader.create_order_with_sl(
order_type=order_type, sl=1.09500
)
2026-02-21 07:43:33 +01:00
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()
2026-02-21 07:43:33 +01:00
trader.create_order_with_sl(
order_type=OrderType.BUY, sl=1.09500
)
2026-02-21 07:43:33 +01:00
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."""
2026-02-21 07:43:33 +01:00
def test_sets_order_type(self, trader):
"""Test sets order type on order."""
trader.create_order_with_points(
order_type=OrderType.BUY, points=500
)
2026-02-21 07:43:33 +01:00
assert trader.order.type == OrderType.BUY
2026-02-21 07:43:33 +01:00
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
)
2026-02-21 07:43:33 +01:00
assert trader.order.price == mock_tick.ask
2026-02-21 07:43:33 +01:00
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
)
2026-02-21 07:43:33 +01:00
assert trader.order.price == mock_tick.bid
2026-02-21 07:43:33 +01:00
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
)
2026-02-21 07:43:33 +01:00
trader.symbol.compute_volume_points.assert_called_once()
2026-02-21 07:43:33 +01:00
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
)
2026-02-21 07:43:33 +01:00
mock_set_stops.assert_called_once_with(points=500, risk_to_reward=None)
2026-02-21 07:43:33 +01:00
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
)
2026-02-21 07:43:33 +01:00
mock_set_stops.assert_called_once_with(points=500, risk_to_reward=3.0)
2026-02-21 07:43:33 +01:00
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)
2026-02-21 07:43:33 +01:00
assert trader.order.type == OrderType.BUY
2026-02-21 07:43:33 +01:00
def test_uses_min_volume_default(self, trader):
"""Test uses symbol.volume_min when volume not specified."""
trader.symbol.volume_min = 0.01
2026-02-21 07:43:33 +01:00
trader.create_order_no_stops(order_type=OrderType.BUY)
2026-02-21 07:43:33 +01:00
assert trader.order.volume == 0.01
2026-02-21 07:43:33 +01:00
def test_custom_volume(self, trader):
"""Test uses custom volume when provided."""
trader.create_order_no_stops(order_type=OrderType.BUY, volume=1.5)
2026-02-21 07:43:33 +01:00
assert trader.order.volume == 1.5
2026-02-21 07:43:33 +01:00
def test_buy_uses_ask_price(self, trader, mock_tick):
"""Test BUY uses ask price."""
trader.create_order_no_stops(order_type=OrderType.BUY)
2026-02-21 07:43:33 +01:00
assert trader.order.price == mock_tick.ask
2026-02-21 07:43:33 +01:00
def test_sell_uses_bid_price(self, trader, mock_tick):
"""Test SELL uses bid price."""
trader.create_order_no_stops(order_type=OrderType.SELL)
2026-02-21 07:43:33 +01:00
assert trader.order.price == mock_tick.bid
2026-02-21 07:43:33 +01:00
class TestCheckOrder:
"""Test check_order method."""
2026-02-21 07:43:33 +01:00
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)
2026-02-21 07:43:33 +01:00
result = trader.check_order()
2026-02-21 07:43:33 +01:00
assert result is mock_result
2026-02-21 07:43:33 +01:00
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"
2026-02-21 07:43:33 +01:00
result = trader.check_order()
2026-02-21 07:43:33 +01:00
assert result is None
2026-02-21 07:43:33 +01:00
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)
2026-02-21 07:43:33 +01:00
result = trader.check_order()
2026-02-21 07:43:33 +01:00
assert result is mock_result
assert result.retcode != 0
2026-02-21 07:43:33 +01:00
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)
2026-02-21 07:43:33 +01:00
trader.check_order()
2026-02-21 07:43:33 +01:00
trader.order.check.assert_called_once()
2026-02-21 07:43:33 +01:00
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()
2026-02-21 07:43:33 +01:00
assert result is mock_result
assert result.retcode == 10009
2026-02-21 07:43:33 +01:00
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()
2026-02-21 07:43:33 +01:00
assert result is None
2026-02-21 07:43:33 +01:00
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)
2026-02-21 07:43:33 +01:00
result = trader.send_order()
2026-02-21 07:43:33 +01:00
assert result is mock_result
assert result.retcode == 10006
2026-02-21 07:43:33 +01:00
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)
2026-02-21 07:43:33 +01:00
trader.send_order()
2026-02-21 07:43:33 +01:00
trader.order.send.assert_called_once()
2026-02-21 07:43:33 +01:00
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
)
2026-02-21 07:43:33 +01:00
mock_check_result = MagicMock(spec=OrderCheckResult)
mock_check_result.retcode = 0
trader.order.check = MagicMock(return_value=mock_check_result)
2026-02-21 07:43:33 +01:00
check = trader.check_order()
assert check.retcode == 0
2026-02-21 07:43:33 +01:00
def test_create_and_send_order(self, trader, mock_tick):
"""Test creating order then sending it."""
order_type = MagicMock()
order_type.is_long = True
2026-02-21 07:43:33 +01:00
trader.order.set_attributes = MagicMock()
2026-02-21 07:43:33 +01:00
trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000
)
2026-02-21 07:43:33 +01:00
mock_send_result = MagicMock(spec=OrderSendResult)
mock_send_result.retcode = 10009
trader.order.send = MagicMock(return_value=mock_send_result)
2026-02-21 07:43:33 +01:00
result = trader.send_order()
assert result.retcode == 10009
2026-02-21 07:43:33 +01:00
def test_full_trade_lifecycle(self, trader, mock_tick):
"""Test full lifecycle: create, check, send, record."""
order_type = MagicMock()
order_type.is_long = True
2026-02-21 07:43:33 +01:00
trader.order.set_attributes = MagicMock()
2026-02-21 07:43:33 +01:00
# Create
trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000
)
2026-02-21 07:43:33 +01:00
# 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
2026-02-21 07:43:33 +01:00
# Record
trader.config.record_trades = True
trader.config.task_queue = MagicMock()
trader.config.task_queue.add = MagicMock()
2026-02-21 07:43:33 +01:00
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)
2026-02-21 07:43:33 +01:00
with patch('aiomql.lib.sync.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save_sync = MagicMock()
MockResult.return_value = mock_res
2026-02-21 07:43:33 +01:00
trader.record_trade(result=result, name="TestStrategy")
2026-02-21 07:43:33 +01:00
trader.config.task_queue.add.assert_called_once()