Files
2026-02-21 07:43:33 +01:00

909 lines
31 KiB
Python

"""Comprehensive tests for the 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, AsyncMock, patch, PropertyMock
import pytest
from aiomql.lib.trader import Trader
from aiomql.lib.order import Order
from aiomql.lib.ram import RAM
from aiomql.lib.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."""
async 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 = AsyncMock(return_value=100.0)
symbol.info_tick = AsyncMock()
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 = AsyncMock(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)
# With 5 digits, values should have at most 5 decimal places
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."""
async 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()
await 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
async 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()
await 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
async 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()
await 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
async 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()
await trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000
)
trader.symbol.compute_volume_sl.assert_called_once()
async 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()
await trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000, amount_to_risk=500.0
)
# RAM.get_amount should not be called
trader.ram.get_amount.assert_not_called()
async 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()
await trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000
)
trader.ram.get_amount.assert_called_once()
async 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()
await 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."""
async 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()
await trader.create_order_with_sl(
order_type=order_type, sl=1.09500
)
call_kwargs = trader.order.set_attributes.call_args[1]
# For BUY: tp should be above price
assert call_kwargs["tp"] > call_kwargs["price"]
async def test_buy_uses_ask_price(self, trader, mock_tick):
"""Test BUY order uses ask price."""
trader.order.set_attributes = MagicMock()
await 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
async def test_sell_uses_bid_price(self, trader, mock_tick):
"""Test SELL order uses bid price."""
trader.order.set_attributes = MagicMock()
await 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
async def test_custom_risk_to_reward(self, trader, mock_tick):
"""Test custom risk_to_reward affects TP calculation."""
trader.order.set_attributes = MagicMock()
await 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
async def test_sell_tp_below_price(self, trader, mock_tick):
"""Test SELL order sets TP below price."""
trader.order.set_attributes = MagicMock()
await 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"]
async def test_custom_amount_to_risk(self, trader):
"""Test custom amount_to_risk skips RAM.get_amount."""
trader.order.set_attributes = MagicMock()
await 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."""
async def test_sets_order_type(self, trader):
"""Test sets order type on order."""
await trader.create_order_with_points(
order_type=OrderType.BUY, points=500
)
assert trader.order.type == OrderType.BUY
async def test_sets_price_from_tick(self, trader, mock_tick):
"""Test sets price from tick."""
await trader.create_order_with_points(
order_type=OrderType.BUY, points=500
)
assert trader.order.price == mock_tick.ask
async def test_sell_uses_bid(self, trader, mock_tick):
"""Test SELL uses bid price."""
await trader.create_order_with_points(
order_type=OrderType.SELL, points=500
)
assert trader.order.price == mock_tick.bid
async def test_computes_volume_with_points(self, trader):
"""Test uses symbol.compute_volume_points."""
await trader.create_order_with_points(
order_type=OrderType.BUY, points=500
)
trader.symbol.compute_volume_points.assert_called_once()
async 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:
await trader.create_order_with_points(
order_type=OrderType.BUY, points=500
)
mock_set_stops.assert_called_once_with(points=500, risk_to_reward=None)
async 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:
await 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."""
async def test_sets_order_type(self, trader):
"""Test sets order type."""
await trader.create_order_no_stops(order_type=OrderType.BUY)
assert trader.order.type == OrderType.BUY
async def test_uses_min_volume_default(self, trader):
"""Test uses symbol.volume_min when volume not specified."""
trader.symbol.volume_min = 0.01
await trader.create_order_no_stops(order_type=OrderType.BUY)
assert trader.order.volume == 0.01
async def test_custom_volume(self, trader):
"""Test uses custom volume when provided."""
await trader.create_order_no_stops(order_type=OrderType.BUY, volume=1.5)
assert trader.order.volume == 1.5
async def test_buy_uses_ask_price(self, trader, mock_tick):
"""Test BUY uses ask price."""
await trader.create_order_no_stops(order_type=OrderType.BUY)
assert trader.order.price == mock_tick.ask
async def test_sell_uses_bid_price(self, trader, mock_tick):
"""Test SELL uses bid price."""
await trader.create_order_no_stops(order_type=OrderType.SELL)
assert trader.order.price == mock_tick.bid
# async def test_no_sl_set(self, trader):
# """Test no stop loss is set."""
# # SL should remain at its default (0 or unset)
# original_sl = trader.order.sl
# await trader.create_order_no_stops(order_type=OrderType.BUY)
# assert trader.order.sl == original_sl
# async def test_no_tp_set(self, trader):
# """Test no take profit is set."""
# original_tp = trader.order.tp
# await trader.create_order_no_stops(order_type=OrderType.BUY)
# assert trader.order.tp == original_tp
class TestCheckOrder:
"""Test check_order method."""
async 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 = AsyncMock(return_value=mock_result)
result = await trader.check_order()
assert result is mock_result
async def test_check_order_none_result(self, trader):
"""Test check_order handles None result."""
trader.order.check = AsyncMock(return_value=None)
trader.order.mt5 = MagicMock()
trader.order.mt5.error = "Connection failed"
result = await trader.check_order()
assert result is None
async 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 = AsyncMock(return_value=mock_result)
result = await trader.check_order()
assert result is mock_result
assert result.retcode != 0
async 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 = AsyncMock(return_value=mock_result)
await trader.check_order()
trader.order.check.assert_called_once()
class TestSendOrder:
"""Test send_order method."""
async 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 = AsyncMock(return_value=mock_result)
result = await trader.send_order()
assert result is mock_result
assert result.retcode == 10009
async def test_send_order_none_result(self, trader):
"""Test send_order handles None result."""
trader.order.send = AsyncMock(return_value=None)
trader.order.mt5 = MagicMock()
trader.order.mt5.error = "No connection"
result = await trader.send_order()
assert result is None
async 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 = AsyncMock(return_value=mock_result)
result = await trader.send_order()
assert result is mock_result
assert result.retcode == 10006
async 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 = AsyncMock(return_value=mock_result)
await 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
async 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
await trader.record_trade(result=successful_result)
# Should return early — no further calls
async 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
await trader.record_trade(result=result)
# Should return early
async 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 = AsyncMock(return_value=mock_order)
trader.order.calc_profit = AsyncMock(return_value=50.0)
with patch('aiomql.lib.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save = AsyncMock()
MockResult.return_value = mock_res
await trader.record_trade(result=successful_result, parameters={"key": "value"})
trader.config.task_queue.add.assert_called_once()
async 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 = AsyncMock(return_value=mock_order)
trader.order.calc_profit = AsyncMock(return_value=50.0)
with patch('aiomql.lib.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save = AsyncMock()
MockResult.return_value = mock_res
await trader.record_trade(
result=successful_result,
use_task_queue=False
)
mock_res.save.assert_called_once()
async 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 = AsyncMock(return_value=mock_order)
trader.order.calc_profit = AsyncMock(return_value=50.0)
params = {"strategy": "scalping", "timeframe": "M5"}
with patch('aiomql.lib.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save = AsyncMock()
MockResult.return_value = mock_res
await 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"
async 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 = AsyncMock(return_value=mock_order)
with patch('aiomql.lib.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save = AsyncMock()
MockResult.return_value = mock_res
await trader.record_trade(
result=successful_result, expected_profit=75.0
)
call_kwargs = MockResult.call_args[1]
assert call_kwargs["expected_profit"] == 75.0
async 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 = AsyncMock(return_value=mock_order)
trader.order.calc_profit = AsyncMock(return_value=10.0)
with patch('aiomql.lib.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save = AsyncMock()
MockResult.return_value = mock_res
await trader.record_trade(
result=successful_result, parameters="not_a_dict"
)
call_kwargs = MockResult.call_args[1]
assert call_kwargs["parameters"] == {}
async 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 = AsyncMock(return_value=mock_order)
trader.order.calc_profit = AsyncMock(return_value=10.0)
with patch('aiomql.lib.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save = AsyncMock()
MockResult.return_value = mock_res
await 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())
async def test_concrete_place_trade_callable(self, trader):
"""Test ConcreteTrader.place_trade is callable."""
# Should not raise
await trader.place_trade()
class TestIntegration:
"""Integration tests for Trader."""
async 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()
await trader.create_order_with_stops(
order_type=order_type, sl=1.09500, tp=1.11000
)
# Now check the order
mock_check_result = MagicMock(spec=OrderCheckResult)
mock_check_result.retcode = 0
trader.order.check = AsyncMock(return_value=mock_check_result)
check = await trader.check_order()
assert check.retcode == 0
async 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()
await 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 = AsyncMock(return_value=mock_send_result)
result = await trader.send_order()
assert result.retcode == 10009
async 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
await 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 = AsyncMock(return_value=mock_check)
check = await 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 = AsyncMock(return_value=mock_result)
result = await 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 = AsyncMock(return_value=mock_history_order)
trader.order.calc_profit = AsyncMock(return_value=50.0)
with patch('aiomql.lib.trader.Result') as MockResult:
mock_res = MagicMock()
mock_res.save = AsyncMock()
MockResult.return_value = mock_res
await trader.record_trade(result=result, name="TestStrategy")
trader.config.task_queue.add.assert_called_once()