Update tests and docs across core, lib, and contrib modules

This commit is contained in:
Ichinga Samuel
2026-02-16 10:20:49 +01:00
parent 9ecc2bb7f7
commit 844e39194b
493 changed files with 48196 additions and 22486 deletions
View File
+70
View File
@@ -0,0 +1,70 @@
from aiomql.core.sync.meta_trader import MetaTrader
import pytest
@pytest.fixture(scope="class")
def sync_mt():
"""Provides a synchronous MetaTrader instance for the test class."""
mt = MetaTrader()
mt.initialize()
mt.login()
yield mt
mt.shutdown()
@pytest.fixture(scope="function")
def buy_order_sync(btc_usd):
"""Creates a buy order request for BTCUSD."""
mt = MetaTrader()
sym_info = mt.symbol_info(btc_usd.name)
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
sl = sym_info.ask - dsl
tp = sym_info.ask + dsl
return {
"action": mt.TRADE_ACTION_DEAL,
"symbol": btc_usd.name,
"volume": sym_info.volume_min,
"type": mt.ORDER_TYPE_BUY,
"price": sym_info.ask,
"sl": sl,
"tp": tp,
}
@pytest.fixture(scope="function")
def sell_order_sync(eth_usd):
"""Creates a sell order request for ETHUSD."""
mt = MetaTrader()
sym_info = mt.symbol_info(eth_usd.name)
return {
"action": mt.TRADE_ACTION_DEAL,
"symbol": eth_usd.name,
"volume": sym_info.volume_min,
"type": mt.ORDER_TYPE_SELL,
"price": sym_info.bid,
}
@pytest.fixture(scope="class")
def make_buy_sell_orders_sync():
"""Creates buy and sell orders for BTCUSD to ensure open positions exist."""
mt = MetaTrader()
sym_info = mt.symbol_info("BTCUSD")
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
sl = sym_info.ask - dsl
tp = sym_info.ask + dsl
req = {
"action": mt.TRADE_ACTION_DEAL,
"symbol": "BTCUSD",
"volume": sym_info.volume_min,
"type": mt.ORDER_TYPE_BUY,
"price": sym_info.ask,
"sl": sl,
"tp": tp,
}
mt.order_send(req)
req["type"] = mt.ORDER_TYPE_SELL
req["price"] = sym_info.bid
req["sl"] = sym_info.bid + dsl
req["tp"] = sym_info.bid - dsl
mt.order_send(req)
+668
View File
@@ -0,0 +1,668 @@
"""Comprehensive tests for the synchronous history module.
Tests cover:
- History class initialization with various parameter combinations
- Class variable sharing (BaseMeta metaclass behavior)
- Synchronous initialization and data fetching
- Deal retrieval and filtering methods
- Order retrieval and filtering methods
- Edge cases and error handling
- UTC timezone handling
"""
from datetime import datetime, UTC, timedelta
import pytest
from aiomql.lib.sync.history import History
from aiomql.core.models import TradeDeal, TradeOrder
from aiomql.core.sync.meta_trader import MetaTrader
@pytest.fixture(scope="module")
def make_buy_sell_orders_sync():
"""Create buy and sell orders synchronously for testing history."""
mt = MetaTrader()
sym_info = mt.symbol_info("BTCUSD")
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
sl = sym_info.ask - dsl
tp = sym_info.ask + dsl
req = {
"action": mt.TRADE_ACTION_DEAL,
"symbol": "BTCUSD",
"volume": sym_info.volume_min,
"type": mt.ORDER_TYPE_BUY,
"price": sym_info.ask,
"sl": sl,
"tp": tp,
}
mt.order_send(req)
req["type"] = mt.ORDER_TYPE_SELL
req["price"] = sym_info.bid
req["sl"] = sym_info.bid + dsl
req["tp"] = sym_info.bid - dsl
mt.order_send(req)
class TestHistoryInitialization:
"""Test History class initialization and configuration."""
@classmethod
def setup_class(cls):
"""Set up test fixtures for initialization tests."""
cls.now = datetime.now()
cls.start = cls.now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = cls.now.replace(hour=23, minute=59, second=59, microsecond=0)
def test_init_with_datetime_objects(self):
"""Test initialization with datetime objects."""
history = History(date_from=self.start, date_to=self.end)
assert history.date_from == self.start
assert history.date_to == self.end
def test_init_with_timestamps(self):
"""Test initialization with Unix timestamp floats."""
start_ts = self.start.timestamp()
end_ts = self.end.timestamp()
history = History(date_from=start_ts, date_to=end_ts)
assert history.date_from.timestamp() == start_ts
assert history.date_to.timestamp() == end_ts
def test_init_with_mixed_types(self):
"""Test initialization with mixed datetime and timestamp."""
start_ts = self.start.timestamp()
history = History(date_from=start_ts, date_to=self.end)
assert history.date_from.timestamp() == start_ts
assert history.date_to == self.end
def test_init_with_group_filter(self):
"""Test initialization with symbol group filter."""
history = History(date_from=self.start, date_to=self.end, group="*USD*")
assert history.group == "*USD*"
def test_init_with_empty_group(self):
"""Test initialization with empty group (default)."""
history = History(date_from=self.start, date_to=self.end)
assert history.group == ""
def test_init_with_use_utc_true(self):
"""Test initialization with use_utc=True converts to UTC."""
local_time = datetime.now()
history = History(date_from=local_time, date_to=local_time, use_utc=True)
assert history.date_from.tzinfo == UTC
assert history.date_to.tzinfo == UTC
def test_init_with_use_utc_false(self):
"""Test initialization with use_utc=False keeps original timezone."""
local_time = datetime.now()
history = History(date_from=local_time, date_to=local_time, use_utc=False)
# When use_utc is False, timezone is not modified
assert history.date_from == local_time
assert history.date_to == local_time
def test_init_default_attributes(self):
"""Test default attribute values after initialization."""
history = History(date_from=self.start, date_to=self.end)
assert history.deals == ()
assert history.orders == ()
assert history.total_deals == 0
assert history.total_orders == 0
def test_init_class_variables_set(self):
"""Test that class variables mt5 and config are set."""
history = History(date_from=self.start, date_to=self.end)
assert hasattr(History, 'mt5')
assert hasattr(History, 'config')
assert hasattr(history, 'mt5')
assert hasattr(history, 'config')
def test_multiple_instances_share_mt5(self):
"""Test that multiple History instances share the same mt5 object."""
history1 = History(date_from=self.start, date_to=self.end)
history2 = History(date_from=self.start, date_to=self.end)
assert history1.mt5 is history2.mt5
def test_multiple_instances_share_config(self):
"""Test that multiple History instances share the same config object."""
history1 = History(date_from=self.start, date_to=self.end)
history2 = History(date_from=self.start, date_to=self.end)
assert history1.config is history2.config
class TestHistoryLive:
"""Live tests for synchronous History class with actual MT5 connection."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize history with live trades."""
self.history.initialize()
@classmethod
def setup_class(cls):
"""Set up test fixtures with today's date range."""
now = datetime.now()
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
cls.history = History(date_from=cls.start, date_to=cls.end)
def test_initialize_populates_deals(self):
"""Test that initialize() populates deals attribute."""
assert self.history.deals is not None
assert isinstance(self.history.deals, tuple)
def test_initialize_populates_orders(self):
"""Test that initialize() populates orders attribute."""
assert self.history.orders is not None
assert isinstance(self.history.orders, tuple)
def test_initialize_sets_total_deals(self):
"""Test that initialize() sets correct total_deals count."""
assert self.history.total_deals >= 0
assert self.history.total_deals == len(self.history.deals)
def test_initialize_sets_total_orders(self):
"""Test that initialize() sets correct total_orders count."""
assert self.history.total_orders >= 0
assert self.history.total_orders == len(self.history.orders)
def test_get_deals_returns_trade_deal_objects(self):
"""Test that get_deals returns TradeDeal objects."""
deals = self.history.get_deals()
assert isinstance(deals, tuple)
if deals:
assert all(isinstance(deal, TradeDeal) for deal in deals)
def test_get_orders_returns_trade_order_objects(self):
"""Test that get_orders returns TradeOrder objects."""
orders = self.history.get_orders()
assert isinstance(orders, tuple)
if orders:
assert all(isinstance(order, TradeOrder) for order in orders)
def test_deals_have_required_attributes(self):
"""Test that deals have expected TradeDeal attributes."""
if self.history.deals:
deal = self.history.deals[0]
assert hasattr(deal, 'ticket')
assert hasattr(deal, 'order')
assert hasattr(deal, 'time')
assert hasattr(deal, 'time_msc')
assert hasattr(deal, 'type')
assert hasattr(deal, 'position_id')
assert hasattr(deal, 'profit')
assert hasattr(deal, 'symbol')
def test_orders_have_required_attributes(self):
"""Test that orders have expected TradeOrder attributes."""
if self.history.orders:
order = self.history.orders[0]
assert hasattr(order, 'ticket')
assert hasattr(order, 'time_setup')
assert hasattr(order, 'time_done')
assert hasattr(order, 'time_done_msc')
assert hasattr(order, 'type')
assert hasattr(order, 'position_id')
assert hasattr(order, 'symbol')
class TestHistoryDealsFiltering:
"""Test deal filtering methods with live data."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize history with live trades."""
self.history.initialize()
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
now = datetime.now()
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
cls.history = History(date_from=cls.start, date_to=cls.end)
def test_filter_deals_by_ticket_returns_tuple(self):
"""Test filter_deals_by_ticket returns a tuple."""
if self.history.deals:
ticket = self.history.deals[0].order
deals = self.history.filter_deals_by_ticket(ticket=ticket)
assert isinstance(deals, tuple)
def test_filter_deals_by_ticket_finds_matching_deals(self):
"""Test filter_deals_by_ticket finds deals with matching order ticket."""
if self.history.deals:
ticket = self.history.deals[0].order
deals = self.history.filter_deals_by_ticket(ticket=ticket)
if deals:
assert all(deal.order == ticket for deal in deals)
def test_filter_deals_by_ticket_nonexistent_returns_empty(self):
"""Test filter_deals_by_ticket returns empty tuple for nonexistent ticket."""
nonexistent_ticket = 999999999999
deals = self.history.filter_deals_by_ticket(ticket=nonexistent_ticket)
assert deals == ()
def test_filter_deals_by_position_returns_tuple(self):
"""Test filter_deals_by_position returns a tuple."""
if self.history.deals:
position = self.history.deals[0].position_id
deals = self.history.filter_deals_by_position(position=position)
assert isinstance(deals, tuple)
def test_filter_deals_by_position_finds_matching_deals(self):
"""Test filter_deals_by_position finds deals with matching position_id."""
if self.history.deals:
position = self.history.deals[0].position_id
deals = self.history.filter_deals_by_position(position=position)
if deals:
assert all(deal.position_id == position for deal in deals)
def test_filter_deals_by_position_nonexistent_returns_empty(self):
"""Test filter_deals_by_position returns empty tuple for nonexistent position."""
nonexistent_position = 999999999999
deals = self.history.filter_deals_by_position(position=nonexistent_position)
assert deals == ()
class TestHistoryOrdersFiltering:
"""Test order filtering methods with live data."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize history with live trades."""
self.history.initialize()
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
now = datetime.now()
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
cls.history = History(date_from=cls.start, date_to=cls.end)
def test_filter_orders_by_ticket_returns_tuple(self):
"""Test filter_orders_by_ticket returns a tuple."""
if self.history.orders:
ticket = self.history.orders[0].ticket
orders = self.history.filter_orders_by_ticket(ticket=ticket)
assert isinstance(orders, tuple)
def test_filter_orders_by_ticket_finds_matching_orders(self):
"""Test filter_orders_by_ticket finds orders with matching ticket."""
if self.history.orders:
ticket = self.history.orders[0].ticket
orders = self.history.filter_orders_by_ticket(ticket=ticket)
if orders:
assert all(order.ticket == ticket for order in orders)
def test_filter_orders_by_ticket_nonexistent_returns_empty(self):
"""Test filter_orders_by_ticket returns empty tuple for nonexistent ticket."""
nonexistent_ticket = 999999999999
orders = self.history.filter_orders_by_ticket(ticket=nonexistent_ticket)
assert orders == ()
def test_filter_orders_by_position_returns_tuple(self):
"""Test filter_orders_by_position returns a tuple."""
if self.history.orders:
position = self.history.orders[0].position_id
orders = self.history.filter_orders_by_position(position=position)
assert isinstance(orders, tuple)
def test_filter_orders_by_position_finds_matching_orders(self):
"""Test filter_orders_by_position finds orders with matching position_id."""
if self.history.orders:
position = self.history.orders[0].position_id
orders = self.history.filter_orders_by_position(position=position)
if orders:
assert all(order.position_id == position for order in orders)
def test_filter_orders_by_position_nonexistent_returns_empty(self):
"""Test filter_orders_by_position returns empty tuple for nonexistent position."""
nonexistent_position = 999999999999
orders = self.history.filter_orders_by_position(position=nonexistent_position)
assert orders == ()
class TestHistoryWithGroupFilter:
"""Test History with group filter for specific symbols."""
@classmethod
def setup_class(cls):
"""Set up test fixtures with group filter."""
now = datetime.now()
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Ensure trades are created for group filter tests."""
pass
def test_group_filter_btcusd(self):
"""Test filtering history by BTCUSD symbol group."""
history = History(date_from=self.start, date_to=self.end, group="*BTCUSD*")
history.initialize()
for deal in history.deals:
assert "BTCUSD" in deal.symbol
def test_group_filter_usd(self):
"""Test filtering history by USD symbol group."""
history = History(date_from=self.start, date_to=self.end, group="*USD*")
history.initialize()
for deal in history.deals:
assert "USD" in deal.symbol
def test_group_filter_nonexistent_symbol(self):
"""Test filtering with nonexistent symbol group returns empty."""
history = History(date_from=self.start, date_to=self.end, group="NONEXISTENT12345")
history.initialize()
assert history.total_deals == 0
assert history.total_orders == 0
class TestHistoryDateRanges:
"""Test History with various date ranges."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Ensure trades are made for date range tests."""
pass
def test_today_date_range(self):
"""Test history retrieval for today's date range."""
now = datetime.now()
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
end = now.replace(hour=23, minute=59, second=59, microsecond=0)
history = History(date_from=start, date_to=end)
history.initialize()
# Should have at least the test trades
assert history.total_deals >= 0
def test_past_date_range(self):
"""Test history retrieval for a past date range."""
now = datetime.now()
end = now - timedelta(days=7)
start = end - timedelta(days=7)
history = History(date_from=start, date_to=end)
history.initialize()
assert isinstance(history.deals, tuple)
assert isinstance(history.orders, tuple)
def test_wide_date_range(self):
"""Test history retrieval for a wide date range (30 days)."""
now = datetime.now()
start = now - timedelta(days=30)
end = now
history = History(date_from=start, date_to=end)
history.initialize()
assert isinstance(history.deals, tuple)
assert isinstance(history.orders, tuple)
def test_narrow_date_range(self):
"""Test history retrieval for a narrow date range (1 hour)."""
now = datetime.now()
start = now - timedelta(hours=1)
end = now
history = History(date_from=start, date_to=end)
history.initialize()
assert isinstance(history.deals, tuple)
assert isinstance(history.orders, tuple)
class TestHistoryEdgeCases:
"""Test edge cases and error handling."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.now = datetime.now()
cls.start = cls.now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = cls.now.replace(hour=23, minute=59, second=59, microsecond=0)
def test_empty_history_no_trades(self):
"""Test handling of date range with no trades."""
# Use a future date range where no trades exist
future_start = datetime.now() + timedelta(days=365)
future_end = future_start + timedelta(days=1)
history = History(date_from=future_start, date_to=future_end)
history.initialize()
assert history.deals == ()
assert history.orders == ()
assert history.total_deals == 0
assert history.total_orders == 0
def test_filter_deals_by_ticket_with_no_deals(self):
"""Test filtering by ticket when deals is empty."""
future_start = datetime.now() + timedelta(days=365)
future_end = future_start + timedelta(days=1)
history = History(date_from=future_start, date_to=future_end)
history.initialize()
deals = history.filter_deals_by_ticket(ticket=12345)
assert deals == ()
def test_filter_deals_by_position_with_no_deals(self):
"""Test filtering by position when deals is empty."""
future_start = datetime.now() + timedelta(days=365)
future_end = future_start + timedelta(days=1)
history = History(date_from=future_start, date_to=future_end)
history.initialize()
deals = history.filter_deals_by_position(position=12345)
assert deals == ()
def test_filter_orders_by_ticket_with_no_orders(self):
"""Test filtering by ticket when orders is empty."""
future_start = datetime.now() + timedelta(days=365)
future_end = future_start + timedelta(days=1)
history = History(date_from=future_start, date_to=future_end)
history.initialize()
orders = history.filter_orders_by_ticket(ticket=12345)
assert orders == ()
def test_filter_orders_by_position_with_no_orders(self):
"""Test filtering by position when orders is empty."""
future_start = datetime.now() + timedelta(days=365)
future_end = future_start + timedelta(days=1)
history = History(date_from=future_start, date_to=future_end)
history.initialize()
orders = history.filter_orders_by_position(position=12345)
assert orders == ()
def test_initialize_can_be_called_multiple_times(self):
"""Test that initialize() can be safely called multiple times."""
history = History(date_from=self.start, date_to=self.end)
history.initialize()
first_deals = history.deals
first_orders = history.orders
history.initialize()
# Should still have data after reinitialization
assert isinstance(history.deals, tuple)
assert isinstance(history.orders, tuple)
def test_filtering_before_initialize(self):
"""Test filtering methods work on uninitialized history (empty tuples)."""
history = History(date_from=self.start, date_to=self.end)
# Don't call initialize
deals = history.filter_deals_by_ticket(ticket=12345)
assert deals == ()
deals = history.filter_deals_by_position(position=12345)
assert deals == ()
orders = history.filter_orders_by_ticket(ticket=12345)
assert orders == ()
orders = history.filter_orders_by_position(position=12345)
assert orders == ()
class TestHistoryUtcConversion:
"""Test UTC timezone conversion functionality."""
def test_utc_conversion_with_naive_datetime(self):
"""Test UTC conversion with naive datetime objects."""
now = datetime.now()
history = History(date_from=now, date_to=now, use_utc=True)
assert history.date_from.tzinfo == UTC
assert history.date_to.tzinfo == UTC
def test_utc_conversion_with_timestamps(self):
"""Test UTC conversion when dates are provided as timestamps."""
now = datetime.now()
ts = now.timestamp()
history = History(date_from=ts, date_to=ts, use_utc=True)
assert history.date_from.tzinfo == UTC
assert history.date_to.tzinfo == UTC
def test_no_utc_conversion_preserves_datetime(self):
"""Test that use_utc=False preserves the original datetime."""
now = datetime.now()
history = History(date_from=now, date_to=now, use_utc=False)
# Without UTC conversion, dates should equal original
assert history.date_from == now
assert history.date_to == now
class TestHistoryConsistency:
"""Test data consistency between different retrieval methods."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize history fixture."""
self.history.initialize()
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
now = datetime.now()
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
cls.history = History(date_from=cls.start, date_to=cls.end)
def test_get_deals_matches_deals_attribute(self):
"""Test that get_deals() returns same data as deals attribute."""
deals = self.history.get_deals()
# After initialization, deals attribute should have same count
# Note: Fresh call may have different data if trades occurred between calls
assert isinstance(deals, tuple)
if deals:
assert all(isinstance(d, TradeDeal) for d in deals)
def test_get_orders_matches_orders_attribute(self):
"""Test that get_orders() returns same data as orders attribute."""
orders = self.history.get_orders()
assert isinstance(orders, tuple)
if orders:
assert all(isinstance(o, TradeOrder) for o in orders)
def test_total_counts_match_tuple_lengths(self):
"""Test that total_deals and total_orders match tuple lengths."""
assert self.history.total_deals == len(self.history.deals)
assert self.history.total_orders == len(self.history.orders)
def test_filtered_deals_subset_of_all_deals(self):
"""Test that filtered deals are a subset of all deals."""
if self.history.deals:
ticket = self.history.deals[0].order
filtered = self.history.filter_deals_by_ticket(ticket=ticket)
for deal in filtered:
assert deal in self.history.deals
def test_filtered_orders_subset_of_all_orders(self):
"""Test that filtered orders are a subset of all orders."""
if self.history.orders:
position = self.history.orders[0].position_id
filtered = self.history.filter_orders_by_position(position=position)
for order in filtered:
assert order in self.history.orders
class TestHistorySyncVsAsync:
"""Test that sync History behaves correctly as a synchronous implementation."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
now = datetime.now()
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
def test_initialize_is_synchronous(self):
"""Test that initialize() is a synchronous method (not a coroutine)."""
history = History(date_from=self.start, date_to=self.end)
import inspect
assert not inspect.iscoroutinefunction(history.initialize)
def test_get_deals_is_synchronous(self):
"""Test that get_deals() is a synchronous method (not a coroutine)."""
history = History(date_from=self.start, date_to=self.end)
import inspect
assert not inspect.iscoroutinefunction(history.get_deals)
def test_get_orders_is_synchronous(self):
"""Test that get_orders() is a synchronous method (not a coroutine)."""
history = History(date_from=self.start, date_to=self.end)
import inspect
assert not inspect.iscoroutinefunction(history.get_orders)
def test_initialize_returns_none(self):
"""Test that initialize() returns None (not a coroutine object)."""
history = History(date_from=self.start, date_to=self.end)
result = history.initialize()
assert result is None
def test_get_deals_returns_tuple_directly(self):
"""Test that get_deals() returns a tuple directly (not a coroutine)."""
history = History(date_from=self.start, date_to=self.end)
result = history.get_deals()
assert isinstance(result, tuple)
def test_get_orders_returns_tuple_directly(self):
"""Test that get_orders() returns a tuple directly (not a coroutine)."""
history = History(date_from=self.start, date_to=self.end)
result = history.get_orders()
assert isinstance(result, tuple)
class TestHistoryClassMethods:
"""Test History class methods."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
now = datetime.now()
cls.start = now.replace(hour=0, minute=0, second=0, microsecond=0)
cls.end = now.replace(hour=23, minute=59, second=59, microsecond=0)
cls.history = History(date_from=cls.start, date_to=cls.end)
cls.history.initialize()
def test_get_deal_by_ticket_exists(self):
"""Test get_deal_by_ticket returns a TradeDeal."""
if self.history.deals:
ticket = self.history.deals[0].ticket
deal = History.get_deal_by_ticket(ticket=ticket)
assert isinstance(deal, TradeDeal)
assert deal.ticket == ticket
def test_get_deals_by_position_exists(self):
"""Test get_deals_by_position returns a tuple of TradeDeals."""
if self.history.deals:
position = self.history.deals[0].position_id
deals = History.get_deals_by_position(position=position)
assert isinstance(deals, tuple)
assert all(deal.position_id == position for deal in deals)
def test_get_order_by_ticket_exists(self):
"""Test get_order_by_ticket returns a TradeOrder."""
if self.history.orders:
ticket = self.history.orders[0].ticket
order = History.get_order_by_ticket(ticket=ticket)
assert isinstance(order, TradeOrder)
assert order.ticket == ticket
def test_get_orders_by_position_exists(self):
"""Test get_orders_by_position returns a tuple of TradeOrders."""
if self.history.orders:
position = self.history.orders[0].position_id
orders = History.get_orders_by_position(position=position)
assert isinstance(orders, tuple)
assert all(order.position_id == position for order in orders)
+190
View File
@@ -0,0 +1,190 @@
from datetime import datetime, timedelta
import pytz
import MetaTrader5
from aiomql.core.sync.meta_trader import MetaTrader
class TestMetaTraderSync:
@classmethod
def setup_class(cls):
cls.mt = MetaTrader()
cls.mt5 = MetaTrader5
cls.symbol = "BTCUSD"
now = datetime.now(tz=pytz.UTC)
cls.start = now - timedelta(hours=10)
cls.end = now + timedelta(hours=1)
cls.tf = cls.mt.TIMEFRAME_H1
@classmethod
def teardown_class(cls):
cls.mt.shutdown()
def test_initialize(self):
res = self.mt.initialize()
assert res == True
def test_login(self):
res = self.mt.login()
assert res == True
def test_last_error(self):
res = self.mt.last_error()
assert isinstance(res, tuple)
assert res[0] == 1
assert res[1] == "Success"
def test_version(self):
res = self.mt.version()
res2 = self.mt5.version()
assert res is not None
assert res == res2
def test_account_info(self):
res = self.mt.account_info()
res2 = self.mt5.account_info()
assert res is not None
assert res == res2
def test_terminal_info(self):
res = self.mt.terminal_info()
res2 = self.mt5.terminal_info()
assert res is not None
assert res == res2
def test_symbols_total(self):
res = self.mt.symbols_total()
res2 = self.mt5.symbols_total()
assert isinstance(res, int)
assert res == res2
def test_symbols_get(self):
res = self.mt.symbols_get()
res2 = self.mt5.symbols_get()
assert res is not None
assert len(res) == len(res2)
def test_symbol_info(self):
res = self.mt.symbol_info(self.symbol)
res2 = self.mt5.symbol_info(self.symbol)
assert res is not None
assert res == res2
def test_symbol_info_tick(self):
res = self.mt.symbol_info_tick(self.symbol)
res2 = self.mt5.symbol_info_tick(self.symbol)
assert res is not None
assert res == res2
def test_symbol_select(self):
res = self.mt.symbol_select(self.symbol, True)
assert res == True
def test_market_book_add(self):
res = self.mt.market_book_add(self.symbol)
assert res == True
def test_market_book_get(self):
res = self.mt.market_book_get(self.symbol)
res2 = self.mt5.market_book_get(self.symbol)
assert res is not None
assert res == res2
def test_market_book_release(self):
res = self.mt.market_book_release(self.symbol)
assert res == True
def test_copy_rates_from(self):
res = self.mt.copy_rates_from(self.symbol, self.tf, self.start, 10)
assert res is not None
assert res.shape[0] == 10
def test_copy_rates_from_pos(self):
res = self.mt.copy_rates_from_pos(self.symbol, self.tf, 0, 10)
assert res is not None
assert res.shape[0] == 10
def test_copy_rates_range(self):
res = self.mt.copy_rates_range(self.symbol, self.tf, self.start, self.end)
assert res is not None
assert res.shape[0] == 10
def test_copy_ticks_from(self):
res = self.mt.copy_ticks_from(self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL)
assert res is not None
assert res.shape[0] == 10
def test_copy_ticks_range(self):
res = self.mt.copy_ticks_range(self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL)
res2 = self.mt5.copy_ticks_range(self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL)
assert res is not None
assert res.shape[0] == res2.shape[0]
def test_orders_total(self):
res = self.mt.orders_total()
assert isinstance(res, int)
def test_orders_get(self):
res = self.mt.orders_get()
assert res is not None
assert isinstance(res, tuple)
assert len(res) == 0
def test_order_calc_margin(self, sell_order_sync):
price = sell_order_sync["price"]
volume = sell_order_sync["volume"]
type_ = sell_order_sync["type"]
res = self.mt.order_calc_margin(type_, self.symbol, volume, price)
assert isinstance(res, float)
def test_order_calc_profit(self, buy_order_sync):
volume = buy_order_sync["volume"]
price_open = buy_order_sync["price"]
price_close = buy_order_sync["tp"]
type_ = buy_order_sync["type"]
res = self.mt.order_calc_profit(type_, self.symbol, volume, price_open, price_close)
assert isinstance(res, float)
def test_order_check(self, buy_order_sync):
res = self.mt.order_check(buy_order_sync)
assert res is not None
assert res.retcode == 0
def test_order_send(self, sell_order_sync):
res = self.mt.order_send(sell_order_sync)
assert res is not None
assert res.retcode == 10009
def test_positions_total(self):
res = self.mt.positions_total()
assert isinstance(res, int)
assert res >= 0
def test_positions_get(self):
res = self.mt.positions_get()
assert res is not None
assert isinstance(res, tuple)
assert len(res) >= 0
def test_history_orders_total(self):
res = self.mt.history_orders_total(self.start, self.end)
assert isinstance(res, int)
assert res >= 0
def test_history_orders_get(self):
res = self.mt.history_orders_get(self.start, self.end)
assert res is not None
assert isinstance(res, tuple)
assert len(res) >= 0
def test_history_deals_total(self):
res = self.mt.history_deals_total(self.start, self.end)
assert isinstance(res, int)
assert res >= 0
def test_history_deals_get(self):
res = self.mt.history_deals_get(self.start, self.end)
assert res is not None
assert isinstance(res, tuple)
assert len(res) >= 0
+696
View File
@@ -0,0 +1,696 @@
"""Comprehensive tests for the synchronous Order module.
Tests cover:
- Order initialization and default values
- Order modification
- Order checking (margin sufficiency)
- Order sending (market orders)
- Margin calculations
- Profit/loss calculations
- Pending order management
- Request property and filtering
- Class methods for order operations
- cancel_order and send_order retry logic
- Error handling and __getstate__
- Verification that methods are synchronous
"""
import inspect
import pytest
from unittest.mock import MagicMock
from aiomql.lib.sync.order import Order
from aiomql.core.constants import TradeAction, OrderTime, OrderFilling, OrderType
from aiomql.core.models import OrderCheckResult, OrderSendResult, TradeOrder
from aiomql.core.exceptions import OrderError
class TestOrderInitialization:
"""Test Order class initialization and default values."""
def test_init_with_minimal_args(self):
"""Test Order can be initialized with minimal arguments."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert order.symbol == "BTCUSD"
assert order.type == OrderType.BUY
assert order.volume == 0.01
assert order.price == 50000.0
def test_init_default_action(self):
"""Test Order has default action of DEAL."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert order.action == TradeAction.DEAL
def test_init_default_type_time(self):
"""Test Order has default type_time of DAY."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert order.type_time == OrderTime.DAY
def test_init_default_type_filling(self):
"""Test Order has default type_filling of FOK."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert order.type_filling == OrderFilling.FOK
def test_init_override_defaults(self):
"""Test Order defaults can be overridden."""
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.01,
price=50000.0,
action=TradeAction.PENDING,
type_time=OrderTime.GTC,
type_filling=OrderFilling.IOC,
)
assert order.action == TradeAction.PENDING
assert order.type_time == OrderTime.GTC
assert order.type_filling == OrderFilling.IOC
def test_init_with_sl_tp(self):
"""Test Order can be initialized with stop loss and take profit."""
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.01,
price=50000.0,
sl=49000.0,
tp=51000.0,
)
assert order.sl == 49000.0
assert order.tp == 51000.0
def test_init_with_magic(self):
"""Test Order can be initialized with magic number."""
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.01,
price=50000.0,
magic=12345,
)
assert order.magic == 12345
def test_init_with_comment(self):
"""Test Order can be initialized with comment."""
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.01,
price=50000.0,
comment="Test order",
)
assert order.comment == "Test order"
class TestOrderModification:
"""Test Order modification method."""
def test_modify_single_attribute(self):
"""Test modifying a single attribute."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order.modify(volume=0.02)
assert order.volume == 0.02
def test_modify_multiple_attributes(self):
"""Test modifying multiple attributes at once."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order.modify(volume=0.02, price=51000.0, sl=49000.0)
assert order.volume == 0.02
assert order.price == 51000.0
assert order.sl == 49000.0
def test_modify_type(self):
"""Test modifying order type."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order.modify(type=OrderType.SELL)
assert order.type == OrderType.SELL
def test_modify_preserves_other_attributes(self):
"""Test modifying doesn't affect other attributes."""
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.01,
price=50000.0,
comment="Original",
)
order.modify(volume=0.02)
assert order.comment == "Original"
assert order.symbol == "BTCUSD"
def test_modify_returns_none(self):
"""Test modify returns None (modifies in place)."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
result = order.modify(volume=0.02)
assert result is None
def test_modify_action_to_pending(self):
"""Test modifying action to PENDING."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order.modify(action=TradeAction.PENDING)
assert order.action == TradeAction.PENDING
class TestOrderRequest:
"""Test Order request property."""
def test_request_is_dict(self):
"""Test request property returns a dictionary."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert isinstance(order.request, dict)
def test_request_contains_required_fields(self):
"""Test request contains required trade fields."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
request = order.request
assert "symbol" in request
assert "type" in request
assert "volume" in request
assert "price" in request
assert "action" in request
def test_request_filters_invalid_fields(self):
"""Test request only contains valid TradeRequest fields."""
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.01,
price=50000.0,
)
request = order.request
# Should not contain fields that aren't part of TradeRequest
for key in request:
assert key in order.mt5.TradeRequest.__match_args__
def test_request_includes_sl_tp_when_set(self):
"""Test request includes sl and tp when they are set."""
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.01,
price=50000.0,
sl=49000.0,
tp=51000.0,
)
request = order.request
assert request["sl"] == 49000.0
assert request["tp"] == 51000.0
def test_request_reflects_modify(self):
"""Test request reflects changes after modify."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order.modify(price=51000.0)
assert order.request["price"] == 51000.0
class TestOrderCheckLive:
"""Live tests for Order check method."""
def test_check_returns_order_check_result(self, buy_order_sync):
"""Test check returns OrderCheckResult."""
order = Order(**buy_order_sync)
result = order.check()
assert isinstance(result, OrderCheckResult)
def test_check_success_retcode(self, buy_order_sync):
"""Test successful check has retcode 0."""
order = Order(**buy_order_sync)
result = order.check()
assert result.retcode == 0
def test_check_has_margin_info(self, buy_order_sync):
"""Test check result contains margin information."""
order = Order(**buy_order_sync)
result = order.check()
assert hasattr(result, 'margin')
assert hasattr(result, 'margin_free')
def test_check_has_balance_info(self, buy_order_sync):
"""Test check result contains balance information."""
order = Order(**buy_order_sync)
result = order.check()
assert hasattr(result, 'balance')
assert hasattr(result, 'equity')
def test_check_with_kwargs_override(self, buy_order_sync):
"""Test check can use kwargs to override order params."""
order = Order(**buy_order_sync)
result = order.check(volume=buy_order_sync["volume"] * 2)
assert isinstance(result, OrderCheckResult)
def test_check_sell_order(self, sell_order_sync):
"""Test check works for sell orders."""
order = Order(**sell_order_sync)
result = order.check()
assert result.retcode == 0
def test_check_raises_order_error_when_none(self):
"""Test check raises OrderError when mt5.order_check returns None."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order.mt5.order_check = MagicMock(return_value=None)
with pytest.raises(OrderError):
order.check()
class TestOrderSendLive:
"""Live tests for Order send method."""
def test_send_returns_order_send_result(self, buy_order_sync):
"""Test send returns OrderSendResult."""
order = Order(**buy_order_sync)
result = order.send()
assert isinstance(result, OrderSendResult)
def test_send_success_retcode(self, buy_order_sync):
"""Test successful send has retcode 10009."""
order = Order(**buy_order_sync)
result = order.send()
assert result.retcode == 10009
def test_send_has_deal_ticket(self, buy_order_sync):
"""Test send result contains deal ticket."""
order = Order(**buy_order_sync)
result = order.send()
assert hasattr(result, 'deal')
assert result.deal > 0
def test_send_has_order_ticket(self, buy_order_sync):
"""Test send result contains order ticket."""
order = Order(**buy_order_sync)
result = order.send()
assert hasattr(result, 'order')
assert result.order > 0
def test_send_sell_order(self, sell_order_sync):
"""Test send works for sell orders."""
order = Order(**sell_order_sync)
result = order.send()
assert result.retcode == 10009
class TestCancelOrderSync:
"""Tests for cancel_order class method."""
def test_cancel_order_raises_for_none_result(self):
"""Test cancel_order raises OrderError when send_order returns None."""
original = Order.mt5.order_send
Order.mt5.order_send = MagicMock(return_value=None)
try:
with pytest.raises(OrderError):
Order.cancel_order(order=999999999)
finally:
Order.mt5.order_send = original
def test_cancel_order_sends_remove_action(self):
"""Test cancel_order sends REMOVE action."""
mock_result = MagicMock()
mock_result.retcode = 10009
mock_result._asdict = MagicMock(return_value={
"retcode": 10009, "deal": 0, "order": 12345, "volume": 0.0,
"price": 0.0, "bid": 0.0, "ask": 0.0, "comment": "",
"request_id": 1, "retcode_external": 0,
"request": {"action": 8, "order": 12345, "symbol": "BTCUSD"},
})
original = Order.mt5.order_send
Order.mt5.order_send = MagicMock(return_value=mock_result)
try:
result = Order.cancel_order(order=12345, symbol="BTCUSD")
assert isinstance(result, OrderSendResult)
call_args = Order.mt5.order_send.call_args[0][0]
assert call_args["action"] == TradeAction.REMOVE
finally:
Order.mt5.order_send = original
class TestSendOrderRetrySync:
"""Test send_order retry logic."""
def test_send_order_retries_on_10031(self):
"""Test send_order retries when retcode is 10031 (no connection)."""
mock_result_fail = MagicMock()
mock_result_fail.retcode = 10031
mock_result_fail._asdict = MagicMock(return_value={
"retcode": 10031, "deal": 0, "order": 0, "volume": 0.0,
"price": 0.0, "bid": 0.0, "ask": 0.0, "comment": "No connection",
"request_id": 1, "retcode_external": 0,
"request": {"action": 1, "symbol": "BTCUSD"},
})
mock_result_ok = MagicMock()
mock_result_ok.retcode = 10009
mock_result_ok._asdict = MagicMock(return_value={
"retcode": 10009, "deal": 12345, "order": 67890, "volume": 0.01,
"price": 50000.0, "bid": 49999.0, "ask": 50001.0, "comment": "",
"request_id": 2, "retcode_external": 0,
"request": {"action": 1, "symbol": "BTCUSD"},
})
original = Order.mt5.order_send
Order.mt5.order_send = MagicMock(side_effect=[mock_result_fail, mock_result_ok])
try:
result = Order.send_order(request={"symbol": "BTCUSD", "action": 1})
assert result.retcode == 10009
assert Order.mt5.order_send.call_count == 2
finally:
Order.mt5.order_send = original
def test_send_order_raises_when_none(self):
"""Test send_order raises OrderError when result is None."""
original = Order.mt5.order_send
Order.mt5.order_send = MagicMock(return_value=None)
try:
with pytest.raises(OrderError):
Order.send_order(request={"symbol": "BTCUSD", "action": 1})
finally:
Order.mt5.order_send = original
class TestOrderMarginCalculationLive:
"""Live tests for Order margin calculation."""
def test_calc_margin_returns_float(self, buy_order_sync):
"""Test calc_margin returns a float."""
order = Order(**buy_order_sync)
margin = order.calc_margin()
assert isinstance(margin, float)
def test_calc_margin_positive(self, buy_order_sync):
"""Test calc_margin returns positive value."""
order = Order(**buy_order_sync)
margin = order.calc_margin()
assert margin > 0
def test_calc_margin_buy_order(self, buy_order_sync):
"""Test calc_margin works for buy orders."""
order = Order(**buy_order_sync)
margin = order.calc_margin()
assert margin is not None
assert margin > 0
def test_calc_margin_sell_order(self, sell_order_sync):
"""Test calc_margin works for sell orders."""
order = Order(**sell_order_sync)
margin = order.calc_margin()
assert margin is not None
assert margin > 0
class TestOrderProfitCalculationLive:
"""Live tests for Order profit/loss calculations."""
def test_calc_profit_returns_float(self, buy_order_sync):
"""Test calc_profit returns a float."""
order = Order(**buy_order_sync)
profit = order.calc_profit()
assert isinstance(profit, float)
def test_calc_profit_is_positive_for_tp(self, buy_order_sync):
"""Test calc_profit is positive when price reaches TP."""
order = Order(**buy_order_sync)
profit = order.calc_profit()
assert profit > 0
def test_calc_loss_returns_float(self, buy_order_sync):
"""Test calc_loss returns a float."""
order = Order(**buy_order_sync)
loss = order.calc_loss()
assert isinstance(loss, float)
def test_calc_loss_is_negative_for_sl(self, buy_order_sync):
"""Test calc_loss is negative when price reaches SL."""
order = Order(**buy_order_sync)
loss = order.calc_loss()
assert loss < 0
def test_calc_profit_sell_order(self, sell_order_sync):
"""Test calc_profit works for sell orders (may be None if no TP)."""
order = Order(**sell_order_sync)
# sell_order may not have tp set
profit = order.calc_profit()
# May be None if tp is not set
assert profit is None or isinstance(profit, float)
class TestOrdersTotalLive:
"""Live tests for orders_total class method."""
def test_orders_total_returns_int(self):
"""Test orders_total returns an integer."""
total = Order.orders_total()
assert isinstance(total, int)
def test_orders_total_non_negative(self):
"""Test orders_total returns non-negative value."""
total = Order.orders_total()
assert total >= 0
class TestGetPendingOrdersLive:
"""Live tests for pending order retrieval."""
def test_get_pending_orders_returns_tuple(self):
"""Test get_pending_orders returns a tuple."""
orders = Order.get_pending_orders()
assert isinstance(orders, tuple)
def test_get_pending_orders_contains_trade_orders(self):
"""Test get_pending_orders contains TradeOrder objects."""
orders = Order.get_pending_orders()
for order in orders:
assert isinstance(order, TradeOrder)
def test_get_pending_orders_by_symbol(self):
"""Test get_pending_orders can filter by symbol."""
orders = Order.get_pending_orders(symbol="BTCUSD")
for order in orders:
assert order.symbol == "BTCUSD"
def test_get_pending_orders_by_group(self):
"""Test get_pending_orders can filter by group."""
orders = Order.get_pending_orders(group="*USD*")
for order in orders:
assert "USD" in order.symbol
def test_get_pending_order_nonexistent(self):
"""Test get_pending_order returns None for nonexistent ticket."""
order = Order.get_pending_order(ticket=999999999999)
assert order is None
class TestGetHistoryOrderByTicketLive:
"""Live tests for get_history_order_by_ticket class method."""
def test_get_history_order_by_ticket_nonexistent(self):
"""Test get_history_order_by_ticket returns None for nonexistent ticket."""
order = Order.get_history_order_by_ticket(ticket=999999999999)
assert order is None
def test_get_history_order_by_ticket_returns_trade_order_or_none(self):
"""Test get_history_order_by_ticket returns TradeOrder or None."""
# Get list of pending orders first
orders = Order.get_pending_orders()
if orders:
# If there are pending orders, test with a real ticket
ticket = orders[0].ticket
order = Order.get_history_order_by_ticket(ticket=ticket)
assert order is None or isinstance(order, TradeOrder)
else:
# If no pending orders, just verify nonexistent returns None
order = Order.get_history_order_by_ticket(ticket=999999999999)
assert order is None
class TestProfitToPriceLive:
"""Live tests for profit_to_price class method."""
def test_profit_to_price_buy_order(self, sync_mt):
"""Test profit_to_price calculates correct price for buy order."""
sym_info = sync_mt.symbol_info("BTCUSD")
price_open = sym_info.ask
volume = sym_info.volume_min
profit = 10.0 # $10 profit target
price = Order.profit_to_price(
profit=profit,
order_type=OrderType.BUY,
volume=volume,
symbol="BTCUSD",
price_open=price_open,
)
assert isinstance(price, float)
assert price > price_open # For buy, profit price should be higher
def test_profit_to_price_sell_order(self, sync_mt):
"""Test profit_to_price calculates correct price for sell order."""
sym_info = sync_mt.symbol_info("BTCUSD")
price_open = sym_info.bid
volume = sym_info.volume_min
profit = 10.0 # $10 profit target
price = Order.profit_to_price(
profit=profit,
order_type=OrderType.SELL,
volume=volume,
symbol="BTCUSD",
price_open=price_open,
)
assert isinstance(price, float)
assert price < price_open # For sell, profit price should be lower
class TestOrderClassAttributes:
"""Test Order class attributes and inheritance."""
def test_order_has_mt5_attribute(self):
"""Test Order class has mt5 attribute."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert hasattr(order, 'mt5')
def test_order_has_config_attribute(self):
"""Test Order class has config attribute."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert hasattr(order, 'config')
def test_order_inherits_trade_request(self):
"""Test Order inherits from TradeRequest."""
from aiomql.core.models import TradeRequest
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert isinstance(order, TradeRequest)
def test_order_getstate_excludes_mt5(self):
"""Test __getstate__ excludes mt5 attribute for pickling."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
state = order.__getstate__()
assert "mt5" not in state
def test_order_getstate_preserves_other_attrs(self):
"""Test __getstate__ preserves trade attributes."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
state = order.__getstate__()
assert state["symbol"] == "BTCUSD"
def test_order_mode_is_sync(self):
"""Test sync Order has mode set to 'sync'."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert order.mode == "sync"
class TestOrderEdgeCases:
"""Test edge cases and error handling."""
def test_check_with_zero_volume(self, sync_mt):
"""Test check with zero volume."""
sym_info = sync_mt.symbol_info("BTCUSD")
order = Order(
symbol="BTCUSD",
type=OrderType.BUY,
volume=0.0,
price=sym_info.ask,
)
# Should either raise error or return failed check
try:
result = order.check()
assert result.retcode != 0
except OrderError:
pass # Also acceptable
def test_multiple_orders_share_mt5(self):
"""Test multiple Order instances share the same mt5 object."""
order1 = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order2 = Order(symbol="ETHUSD", type=OrderType.SELL, volume=0.01, price=3000.0)
assert order1.mt5 is order2.mt5
def test_multiple_orders_share_config(self):
"""Test multiple Order instances share the same config object."""
order1 = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
order2 = Order(symbol="ETHUSD", type=OrderType.SELL, volume=0.01, price=3000.0)
assert order1.config is order2.config
def test_calc_margin_returns_none_on_error(self):
"""Test calc_margin returns None when an error occurs."""
order = Order(symbol="INVALIDSYMBOL", type=OrderType.BUY, volume=0.01, price=50000.0)
result = order.calc_margin()
assert result is None
def test_calc_profit_returns_none_when_no_tp(self):
"""Test calc_profit returns None when tp is not set."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
result = order.calc_profit()
assert result is None or isinstance(result, float)
def test_calc_loss_returns_none_when_no_sl(self):
"""Test calc_loss returns None when sl is not set."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
result = order.calc_loss()
assert result is None or isinstance(result, float)
def test_get_pending_orders_returns_empty_for_nonexistent_symbol(self):
"""Test get_pending_orders returns empty tuple for nonexistent symbol."""
orders = Order.get_pending_orders(symbol="NONEXISTENT")
assert orders == ()
class TestOrderSyncMethods:
"""Test that Order methods are truly synchronous."""
def test_orders_total_is_synchronous(self):
"""Test orders_total is a synchronous method."""
assert not inspect.iscoroutinefunction(Order.orders_total)
def test_get_pending_order_is_synchronous(self):
"""Test get_pending_order is a synchronous method."""
assert not inspect.iscoroutinefunction(Order.get_pending_order)
def test_get_pending_orders_is_synchronous(self):
"""Test get_pending_orders is a synchronous method."""
assert not inspect.iscoroutinefunction(Order.get_pending_orders)
def test_cancel_order_is_synchronous(self):
"""Test cancel_order is a synchronous method."""
assert not inspect.iscoroutinefunction(Order.cancel_order)
def test_check_is_synchronous(self):
"""Test check is a synchronous method."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert not inspect.iscoroutinefunction(order.check)
def test_send_is_synchronous(self):
"""Test send is a synchronous method."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert not inspect.iscoroutinefunction(order.send)
def test_calc_margin_is_synchronous(self):
"""Test calc_margin is a synchronous method."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert not inspect.iscoroutinefunction(order.calc_margin)
def test_calc_profit_is_synchronous(self):
"""Test calc_profit is a synchronous method."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert not inspect.iscoroutinefunction(order.calc_profit)
def test_calc_loss_is_synchronous(self):
"""Test calc_loss is a synchronous method."""
order = Order(symbol="BTCUSD", type=OrderType.BUY, volume=0.01, price=50000.0)
assert not inspect.iscoroutinefunction(order.calc_loss)
def test_profit_to_price_is_synchronous(self):
"""Test profit_to_price is a synchronous method."""
assert not inspect.iscoroutinefunction(Order.profit_to_price)
def test_get_history_order_by_ticket_is_synchronous(self):
"""Test get_history_order_by_ticket is a synchronous method."""
assert not inspect.iscoroutinefunction(Order.get_history_order_by_ticket)
def test_send_order_is_synchronous(self):
"""Test send_order is a synchronous method."""
assert not inspect.iscoroutinefunction(Order.send_order)
def test_orders_total_returns_directly(self):
"""Test orders_total returns a value directly (not a coroutine)."""
result = Order.orders_total()
assert isinstance(result, int)
def test_get_pending_orders_returns_directly(self):
"""Test get_pending_orders returns a value directly (not a coroutine)."""
result = Order.get_pending_orders()
assert isinstance(result, tuple)
+378
View File
@@ -0,0 +1,378 @@
"""Comprehensive tests for the synchronous Positions module.
Tests cover:
- Positions class initialization (BaseMeta metaclass behavior)
- Getting positions with various filters
- Getting positions by ticket and symbol
- Closing positions (individual and all)
- Class methods for position operations
- Edge cases and error handling
"""
import pytest
from aiomql.lib.sync.positions import Positions
from aiomql.core.models import TradePosition, OrderSendResult
from aiomql.core.exceptions import InvalidRequest
class TestPositionsInitialization:
"""Test Positions class initialization."""
def test_has_mt5_attribute(self):
"""Test Positions has mt5 class attribute."""
assert hasattr(Positions, 'mt5')
def test_has_config_attribute(self):
"""Test Positions has config class attribute."""
assert hasattr(Positions, 'config')
def test_class_attributes_are_shared(self):
"""Test that class attributes are shared across access points."""
assert Positions.mt5 is Positions.mt5
assert Positions.config is Positions.config
class TestGetPositionsLive:
"""Live tests for getting positions."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize with live trades."""
cls = type(self)
cls.positions = Positions.get_positions()
def test_get_positions_returns_tuple(self):
"""Test get_positions returns a tuple."""
positions = Positions.get_positions()
assert isinstance(positions, tuple)
def test_get_positions_contains_trade_positions(self):
"""Test get_positions contains TradePosition objects."""
positions = Positions.get_positions()
for position in positions:
assert isinstance(position, TradePosition)
def test_get_positions_by_symbol(self):
"""Test get_positions can filter by symbol."""
if self.positions:
symbol = self.positions[0].symbol
positions = Positions.get_positions(symbol=symbol)
for position in positions:
assert position.symbol == symbol
def test_get_positions_by_ticket(self):
"""Test get_positions can filter by ticket."""
if self.positions:
ticket = self.positions[0].ticket
positions = Positions.get_positions(ticket=ticket)
assert len(positions) <= 1
if positions:
assert positions[0].ticket == ticket
def test_get_positions_by_group(self):
"""Test get_positions can filter by group."""
positions = Positions.get_positions(group="*USD*")
for position in positions:
assert "USD" in position.symbol
def test_get_positions_symbol_overrides_ticket(self):
"""Test that symbol filter takes precedence over ticket."""
if self.positions:
symbol = self.positions[0].symbol
# Pass both symbol and ticket, symbol should take precedence
positions = Positions.get_positions(symbol=symbol, ticket=99999999)
# Should still return positions for symbol, not error on ticket
for position in positions:
assert position.symbol == symbol
class TestGetPositionByTicketLive:
"""Live tests for get_position_by_ticket class method."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize with live trades."""
cls = type(self)
cls.positions = Positions.get_positions()
def test_get_position_by_ticket_returns_trade_position(self):
"""Test get_position_by_ticket returns TradePosition."""
if self.positions:
ticket = self.positions[0].ticket
position = Positions.get_position_by_ticket(ticket=ticket)
assert isinstance(position, TradePosition)
def test_get_position_by_ticket_correct_ticket(self):
"""Test get_position_by_ticket returns position with matching ticket."""
if self.positions:
ticket = self.positions[0].ticket
position = Positions.get_position_by_ticket(ticket=ticket)
assert position.ticket == ticket
def test_get_position_by_ticket_nonexistent_returns_none(self):
"""Test get_position_by_ticket returns None for nonexistent ticket."""
position = Positions.get_position_by_ticket(ticket=999999999999)
assert position is None
def test_get_position_by_ticket_has_required_attributes(self):
"""Test returned position has required attributes."""
if self.positions:
ticket = self.positions[0].ticket
position = Positions.get_position_by_ticket(ticket=ticket)
assert hasattr(position, 'ticket')
assert hasattr(position, 'symbol')
assert hasattr(position, 'volume')
assert hasattr(position, 'type')
assert hasattr(position, 'price_open')
assert hasattr(position, 'price_current')
assert hasattr(position, 'profit')
class TestGetPositionsBySymbolLive:
"""Live tests for get_positions_by_symbol class method."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Ensure trades exist."""
pass
def test_get_positions_by_symbol_returns_tuple(self):
"""Test get_positions_by_symbol returns a tuple."""
positions = Positions.get_positions_by_symbol(symbol="BTCUSD")
assert isinstance(positions, tuple)
def test_get_positions_by_symbol_contains_trade_positions(self):
"""Test get_positions_by_symbol contains TradePosition objects."""
positions = Positions.get_positions_by_symbol(symbol="BTCUSD")
for position in positions:
assert isinstance(position, TradePosition)
def test_get_positions_by_symbol_correct_symbol(self):
"""Test all returned positions have the requested symbol."""
positions = Positions.get_positions_by_symbol(symbol="BTCUSD")
for position in positions:
assert position.symbol == "BTCUSD"
def test_get_positions_by_symbol_nonexistent_returns_empty(self):
"""Test get_positions_by_symbol returns empty tuple for nonexistent symbol."""
positions = Positions.get_positions_by_symbol(symbol="NONEXISTENT123")
assert positions == ()
class TestGetTotalPositionsLive:
"""Live tests for get_total_positions class method."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Ensure trades exist."""
pass
def test_get_total_positions_returns_int(self):
"""Test get_total_positions returns an integer."""
total = Positions.get_total_positions()
assert isinstance(total, int)
def test_get_total_positions_non_negative(self):
"""Test get_total_positions returns non-negative value."""
total = Positions.get_total_positions()
assert total >= 0
def test_get_total_positions_matches_get_positions(self):
"""Test get_total_positions matches length of get_positions."""
total = Positions.get_total_positions()
positions = Positions.get_positions()
assert total == len(positions)
class TestClosePositionLive:
"""Live tests for closing positions."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize with live trades."""
cls = type(self)
cls.positions = Positions.get_positions()
def test_close_position_returns_tuple(self):
"""Test close_position returns a tuple of (bool, OrderSendResult)."""
if self.positions:
position = self.positions[0]
result = Positions.close_position(position=position)
assert isinstance(result, tuple)
assert len(result) == 2
def test_close_position_success(self):
"""Test close_position successfully closes a position."""
# Refresh positions
positions = Positions.get_positions()
if positions:
position = positions[0]
success, result = Positions.close_position(position=position)
if success:
assert isinstance(result, OrderSendResult)
assert result.retcode == 10009
def test_close_position_by_ticket_returns_tuple(self):
"""Test close_position_by_ticket returns a tuple."""
# Refresh positions
positions = Positions.get_positions()
if positions:
ticket = positions[0].ticket
result = Positions.close_position_by_ticket(ticket=ticket)
assert isinstance(result, tuple)
assert len(result) == 2
def test_close_position_by_ticket_nonexistent_raises(self):
"""Test close_position_by_ticket raises InvalidRequest for nonexistent ticket."""
with pytest.raises(InvalidRequest):
Positions.close_position_by_ticket(ticket=999999999999)
class TestCloseStaticMethodLive:
"""Live tests for the static close method."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize with live trades."""
cls = type(self)
cls.positions = Positions.get_positions()
def test_close_returns_tuple(self):
"""Test close static method returns tuple of (bool, OrderSendResult)."""
# Refresh positions
positions = Positions.get_positions()
if positions:
position = positions[0]
result = Positions.close(
ticket=position.ticket,
symbol=position.symbol,
price=position.price_current,
volume=position.volume,
order_type=position.type,
)
assert isinstance(result, tuple)
assert len(result) == 2
class TestClosePositionsLive:
"""Live tests for close_positions class method."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize with live trades."""
cls = type(self)
cls.positions = Positions.get_positions()
def test_close_positions_returns_tuple(self):
"""Test close_positions returns a tuple."""
positions = Positions.get_positions()
result = Positions.close_positions(positions=positions)
assert isinstance(result, tuple)
def test_close_positions_empty_positions(self):
"""Test close_positions with empty positions returns empty tuple."""
result = Positions.close_positions(positions=())
assert result == ()
class TestCloseAllPositionsLive:
"""Live tests for closing all positions."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize with live trades."""
pass
def test_close_all_positions_returns_tuple(self):
"""Test close_all_positions class method returns a tuple."""
result = Positions.close_all_positions()
assert isinstance(result, tuple)
def test_close_all_positions_contains_order_send_results(self):
"""Test close_all_positions returns OrderSendResult objects."""
result = Positions.close_all_positions()
for res in result:
assert isinstance(res, OrderSendResult)
class TestPositionAttributes:
"""Test TradePosition attributes from positions."""
@pytest.fixture(scope="class", autouse=True)
def init(self, make_buy_sell_orders_sync):
"""Initialize with live trades."""
cls = type(self)
cls.positions = Positions.get_positions()
def test_position_has_ticket(self):
"""Test position has ticket attribute."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'ticket')
assert isinstance(position.ticket, int)
def test_position_has_symbol(self):
"""Test position has symbol attribute."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'symbol')
assert isinstance(position.symbol, str)
def test_position_has_volume(self):
"""Test position has volume attribute."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'volume')
assert isinstance(position.volume, float)
def test_position_has_type(self):
"""Test position has type attribute."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'type')
def test_position_has_price_open(self):
"""Test position has price_open attribute."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'price_open')
assert isinstance(position.price_open, float)
def test_position_has_price_current(self):
"""Test position has price_current attribute."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'price_current')
assert isinstance(position.price_current, float)
def test_position_has_profit(self):
"""Test position has profit attribute."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'profit')
assert isinstance(position.profit, float)
def test_position_has_sl_tp(self):
"""Test position has sl and tp attributes."""
if self.positions:
position = self.positions[0]
assert hasattr(position, 'sl')
assert hasattr(position, 'tp')
class TestPositionsEdgeCases:
"""Test edge cases and error handling."""
def test_get_positions_empty_when_no_positions(self):
"""Test get_positions returns empty tuple when no positions exist."""
# Close all positions first
Positions.close_all_positions()
result = Positions.get_positions()
# Result should be a tuple (possibly empty)
assert isinstance(result, tuple)
def test_get_positions_with_invalid_group(self):
"""Test get_positions with nonexistent group returns empty."""
result = Positions.get_positions(group="NONEXISTENT_GROUP_12345")
assert result == ()
+620
View File
@@ -0,0 +1,620 @@
"""Comprehensive tests for the synchronous Sessions module.
Tests cover:
- Duration NamedTuple
- delta helper function
- Session initialization and attributes
- Session __contains__, __str__, __repr__, __len__
- Session in_session method
- Session begin and close methods
- Session duration method
- Session close_positions, close_all, close_win, close_loss methods
- Session action method
- Session until method
- Sessions initialization
- Sessions find and find_next methods
- Sessions __contains__
- Sessions context manager
- Sessions check method
- Integration tests
"""
from datetime import time, datetime, timedelta, UTC
from unittest.mock import MagicMock, patch
import pytest
from aiomql.lib.sync.sessions import Session, Sessions, Duration, delta, backtest_sleep
from aiomql.core.config import Config
from aiomql.core.models import TradePosition, OrderSendResult
class TestDuration:
"""Test Duration NamedTuple."""
def test_duration_creation(self):
"""Test creating Duration with values."""
d = Duration(hours=2, minutes=30, seconds=45)
assert d.hours == 2
assert d.minutes == 30
assert d.seconds == 45
def test_duration_unpacking(self):
"""Test Duration can be unpacked."""
d = Duration(hours=1, minutes=15, seconds=30)
hours, minutes, seconds = d
assert hours == 1
assert minutes == 15
assert seconds == 30
def test_duration_is_tuple(self):
"""Test Duration is a tuple subclass."""
d = Duration(hours=1, minutes=0, seconds=0)
assert isinstance(d, tuple)
class TestDeltaFunction:
"""Test delta helper function."""
def test_delta_basic_time(self):
"""Test delta with basic time."""
t = time(hour=2, minute=30, second=45)
result = delta(t)
expected = timedelta(hours=2, minutes=30, seconds=45)
assert result == expected
def test_delta_midnight(self):
"""Test delta with midnight."""
t = time(hour=0, minute=0, second=0)
result = delta(t)
assert result == timedelta(0)
def test_delta_with_microseconds(self):
"""Test delta includes microseconds."""
t = time(hour=1, minute=2, second=3, microsecond=456789)
result = delta(t)
expected = timedelta(hours=1, minutes=2, seconds=3, microseconds=456789)
assert result == expected
def test_delta_end_of_day(self):
"""Test delta with end of day time."""
t = time(hour=23, minute=59, second=59)
result = delta(t)
expected = timedelta(hours=23, minutes=59, seconds=59)
assert result == expected
class TestSessionInitialization:
"""Test Session class initialization."""
def test_init_with_time_objects(self):
"""Test Session init with datetime.time objects."""
start = time(8, 0)
end = time(16, 0)
session = Session(start=start, end=end)
assert session.start.hour == 8
assert session.end.hour == 16
assert session.start.tzinfo == UTC
def test_init_with_integers(self):
"""Test Session init with integer hours."""
session = Session(start=9, end=17)
assert session.start.hour == 9
assert session.end.hour == 17
assert session.start.tzinfo == UTC
def test_init_with_on_start(self):
"""Test Session init with on_start action."""
session = Session(start=8, end=16, on_start="close_all")
assert session.on_start == "close_all"
def test_init_with_on_end(self):
"""Test Session init with on_end action."""
session = Session(start=8, end=16, on_end="close_loss")
assert session.on_end == "close_loss"
def test_init_with_custom_functions(self):
"""Test Session init with custom start/end functions."""
def my_start():
pass
def my_end():
pass
session = Session(start=8, end=16, custom_start=my_start, custom_end=my_end)
assert session.custom_start == my_start
assert session.custom_end == my_end
def test_init_with_name(self):
"""Test Session init with custom name."""
session = Session(start=8, end=16, name="Morning Session")
assert session.name == "Morning Session"
def test_init_default_name(self):
"""Test Session generates default name."""
session = Session(start=8, end=16)
assert "<-->" in session.name
def test_init_creates_positions_manager(self):
"""Test Session creates positions manager."""
session = Session(start=8, end=16)
assert session.positions_manager is not None
def test_init_creates_config(self):
"""Test Session creates config."""
session = Session(start=8, end=16)
assert isinstance(session.config, Config)
class TestSessionContains:
"""Test Session __contains__ method."""
def test_contains_time_in_session(self):
"""Test time within session returns True."""
session = Session(start=8, end=16)
test_time = time(12, 0)
assert test_time in session
def test_contains_time_at_start(self):
"""Test time at start of session."""
session = Session(start=8, end=16)
test_time = time(8, 0)
assert test_time in session
def test_contains_time_at_end(self):
"""Test time at end of session."""
session = Session(start=8, end=16)
test_time = time(16, 0)
assert test_time in session
def test_contains_time_before_session(self):
"""Test time before session returns False."""
session = Session(start=8, end=16)
test_time = time(7, 0)
assert test_time not in session
def test_contains_time_after_session(self):
"""Test time after session returns False."""
session = Session(start=8, end=16)
test_time = time(17, 0)
assert test_time not in session
class TestSessionStringMethods:
"""Test Session string representation methods."""
def test_str(self):
"""Test __str__ returns formatted string."""
session = Session(start=8, end=16)
result = str(session)
assert "<-->" in result
def test_repr(self):
"""Test __repr__ returns formatted string."""
session = Session(start=8, end=16)
result = repr(session)
assert "<-->" in result
class TestSessionLen:
"""Test Session __len__ method."""
def test_len_full_hours(self):
"""Test __len__ returns duration in seconds."""
session = Session(start=8, end=16)
expected = 8 * 3600 # 8 hours in seconds
assert len(session) == expected
def test_len_partial_hours(self):
"""Test __len__ with partial hours."""
session = Session(start=time(8, 30), end=time(16, 45))
expected = 8 * 3600 + 15 * 60 # 8 hours 15 minutes
assert len(session) == expected
class TestSessionDuration:
"""Test Session duration method."""
def test_duration_returns_duration_tuple(self):
"""Test duration returns Duration NamedTuple."""
session = Session(start=8, end=16)
result = session.duration()
assert isinstance(result, Duration)
def test_duration_values(self):
"""Test duration returns correct values."""
session = Session(start=8, end=16)
result = session.duration()
assert result.hours == 8
assert result.minutes == 0
assert result.seconds == 0
def test_duration_with_partial_hours(self):
"""Test duration with non-full hours."""
session = Session(start=time(8, 0), end=time(10, 30, 45))
result = session.duration()
assert result.hours == 2
assert result.minutes == 30
assert result.seconds == 45
class TestSessionInSession:
"""Test Session in_session method."""
@patch.object(Config, '__new__')
def test_in_session_live_mode(self, mock_config):
"""Test in_session in live mode."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
# Test depends on current time, just verify it runs
session = Session(start=0, end=23)
result = session.in_session()
assert isinstance(result, bool)
class TestSessionActions:
"""Test Session action methods."""
@pytest.fixture
def session(self):
"""Create a session for testing."""
return Session(start=8, end=16)
def test_begin_calls_action(self, session):
"""Test begin calls action with on_start."""
session.on_start = "close_all"
session.close_all = MagicMock()
session.begin()
session.close_all.assert_called_once()
def test_close_calls_action(self, session):
"""Test close calls action with on_end."""
session.on_end = "close_loss"
session.close_loss = MagicMock()
session.close()
session.close_loss.assert_called_once()
def test_action_close_all(self, session):
"""Test action dispatches to close_all."""
session.close_all = MagicMock()
session.action(action="close_all")
session.close_all.assert_called_once()
def test_action_close_win(self, session):
"""Test action dispatches to close_win."""
session.close_win = MagicMock()
session.action(action="close_win")
session.close_win.assert_called_once()
def test_action_close_loss(self, session):
"""Test action dispatches to close_loss."""
session.close_loss = MagicMock()
session.action(action="close_loss")
session.close_loss.assert_called_once()
def test_action_custom_start(self, session):
"""Test action calls custom_start."""
session.custom_start = MagicMock()
session.action(action="custom_start")
session.custom_start.assert_called_once()
def test_action_custom_end(self, session):
"""Test action calls custom_end."""
session.custom_end = MagicMock()
session.action(action="custom_end")
session.custom_end.assert_called_once()
def test_action_none_does_nothing(self, session):
"""Test action with None does nothing."""
# Should not raise
session.action(action=None)
def test_action_handles_exception(self, session):
"""Test action handles exceptions gracefully."""
session.close_all = MagicMock(side_effect=Exception("Test error"))
# Should not raise, just log warning
session.action(action="close_all")
class TestSessionClosePositions:
"""Test Session position closing methods."""
@pytest.fixture
def session(self):
"""Create a session for testing."""
return Session(start=8, end=16)
def test_close_positions(self, session):
"""Test close_positions calls positions manager."""
position = MagicMock(spec=TradePosition)
result = MagicMock(spec=OrderSendResult)
result.retcode = 10009
session.positions_manager.close_position = MagicMock(return_value=result)
session.close_positions(positions=(position,))
session.positions_manager.close_position.assert_called_once_with(position=position)
def test_close_all(self, session):
"""Test close_all gets and closes all positions."""
positions = (MagicMock(spec=TradePosition),)
session.positions_manager.get_positions = MagicMock(return_value=positions)
session.close_positions = MagicMock()
session.close_all()
session.positions_manager.get_positions.assert_called_once()
session.close_positions.assert_called_once_with(positions=positions)
def test_close_win_filters_profit(self, session):
"""Test close_win only closes profitable positions."""
win_pos = MagicMock(spec=TradePosition)
win_pos.profit = 100
loss_pos = MagicMock(spec=TradePosition)
loss_pos.profit = -50
session.positions_manager.get_positions = MagicMock(return_value=(win_pos, loss_pos))
session.close_positions = MagicMock()
session.close_win()
session.close_positions.assert_called_once()
closed_positions = session.close_positions.call_args[1]["positions"]
assert win_pos in closed_positions
assert loss_pos not in closed_positions
def test_close_loss_filters_loss(self, session):
"""Test close_loss only closes losing positions."""
win_pos = MagicMock(spec=TradePosition)
win_pos.profit = 100
loss_pos = MagicMock(spec=TradePosition)
loss_pos.profit = -50
session.positions_manager.get_positions = MagicMock(return_value=(win_pos, loss_pos))
session.close_positions = MagicMock()
session.close_loss()
session.close_positions.assert_called_once()
closed_positions = session.close_positions.call_args[1]["positions"]
assert loss_pos in closed_positions
assert win_pos not in closed_positions
class TestSessionUntil:
"""Test Session until method."""
@patch.object(Config, '__new__')
def test_until_returns_seconds(self, mock_config):
"""Test until returns seconds until session start."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
session = Session(start=23, end=0) # Future session
result = session.until()
assert isinstance(result, int)
assert result >= 0
class TestSessionsInitialization:
"""Test Sessions class initialization."""
def test_init_with_sessions(self):
"""Test Sessions init with list of Session objects."""
s1 = Session(start=8, end=12)
s2 = Session(start=13, end=17)
sessions = Sessions(sessions=[s1, s2])
assert len(sessions.sessions) == 2
assert sessions.current_session is None
def test_init_sorts_sessions(self):
"""Test Sessions sorts by start time."""
s1 = Session(start=13, end=17)
s2 = Session(start=8, end=12)
sessions = Sessions(sessions=[s1, s2])
assert sessions.sessions[0].start.hour == 8
assert sessions.sessions[1].start.hour == 13
def test_init_creates_config(self):
"""Test Sessions creates config."""
s1 = Session(start=8, end=12)
sessions = Sessions(sessions=[s1])
assert isinstance(sessions.config, Config)
class TestSessionsFind:
"""Test Sessions find method."""
@pytest.fixture
def sessions(self):
"""Create Sessions for testing."""
s1 = Session(start=8, end=12)
s2 = Session(start=13, end=17)
return Sessions(sessions=[s1, s2])
def test_find_returns_session(self, sessions):
"""Test find returns matching session."""
result = sessions.find(moment=time(10, 0))
assert result is not None
assert result.start.hour == 8
def test_find_returns_none_when_not_found(self, sessions):
"""Test find returns None when no match."""
result = sessions.find(moment=time(12, 30))
assert result is None
def test_find_second_session(self, sessions):
"""Test find can find second session."""
result = sessions.find(moment=time(15, 0))
assert result is not None
assert result.start.hour == 13
class TestSessionsFindNext:
"""Test Sessions find_next method."""
@pytest.fixture
def sessions(self):
"""Create Sessions for testing."""
s1 = Session(start=8, end=12)
s2 = Session(start=13, end=17)
return Sessions(sessions=[s1, s2])
def test_find_next_returns_next_session(self, sessions):
"""Test find_next returns next session."""
result = sessions.find_next(moment=time(7, 0))
assert result.start.hour == 8
def test_find_next_between_sessions(self, sessions):
"""Test find_next when between sessions."""
result = sessions.find_next(moment=time(12, 30))
assert result.start.hour == 13
def test_find_next_wraps_to_first(self, sessions):
"""Test find_next wraps to first session at end of day."""
result = sessions.find_next(moment=time(18, 0))
assert result.start.hour == 8
class TestSessionsContains:
"""Test Sessions __contains__ method."""
@pytest.fixture
def sessions(self):
"""Create Sessions for testing."""
s1 = Session(start=8, end=12)
s2 = Session(start=13, end=17)
return Sessions(sessions=[s1, s2])
def test_contains_time_in_session(self, sessions):
"""Test time within any session returns True."""
assert time(10, 0) in sessions
def test_contains_time_between_sessions(self, sessions):
"""Test time between sessions returns False."""
assert time(12, 30) not in sessions
def test_contains_time_outside_sessions(self, sessions):
"""Test time outside all sessions returns False."""
assert time(18, 0) not in sessions
class TestSessionsContextManager:
"""Test Sessions sync context manager."""
@pytest.fixture
def sessions(self):
"""Create Sessions for testing."""
s1 = Session(start=0, end=23) # All day session
return Sessions(sessions=[s1])
def test_enter_calls_check(self, sessions):
"""Test __enter__ calls check."""
sessions.check = MagicMock()
with sessions:
sessions.check.assert_called_once()
def test_exit_closes_session(self, sessions):
"""Test __exit__ closes current session."""
sessions.check = MagicMock()
mock_session = MagicMock()
with sessions:
sessions.current_session = mock_session
mock_session.close.assert_called_once()
class TestSessionsCheck:
"""Test Sessions check method."""
@pytest.fixture
def sessions(self):
"""Create Sessions for testing."""
s1 = Session(start=8, end=12)
s2 = Session(start=13, end=17)
return Sessions(sessions=[s1, s2])
def test_check_returns_if_in_session(self, sessions):
"""Test check returns early if already in session."""
mock_session = MagicMock()
mock_session.in_session.return_value = True
sessions.current_session = mock_session
sessions.check()
# Should return without changing current_session
assert sessions.current_session == mock_session
def test_check_starts_new_session(self, sessions):
"""Test check starts new session when found."""
sessions.find = MagicMock(return_value=sessions.sessions[0])
sessions.sessions[0].begin = MagicMock()
sessions.check()
assert sessions.current_session == sessions.sessions[0]
sessions.sessions[0].begin.assert_called_once()
def test_check_transitions_session(self, sessions):
"""Test check handles session transition."""
old_session = MagicMock()
old_session.in_session.return_value = False
old_session.close = MagicMock()
sessions.current_session = old_session
new_session = sessions.sessions[0]
new_session.begin = MagicMock()
sessions.find = MagicMock(return_value=new_session)
sessions.check()
old_session.close.assert_called_once()
assert sessions.current_session == new_session
class TestIntegration:
"""Integration tests for Sessions."""
def test_create_multiple_sessions(self):
"""Test creating multiple sessions."""
morning = Session(start=8, end=12, name="Morning", on_end="close_loss")
afternoon = Session(start=13, end=17, name="Afternoon", on_end="close_all")
evening = Session(start=18, end=22, name="Evening")
sessions = Sessions(sessions=[morning, afternoon, evening])
assert len(sessions.sessions) == 3
assert sessions.sessions[0].name == "Morning"
assert sessions.sessions[1].name == "Afternoon"
assert sessions.sessions[2].name == "Evening"
def test_session_duration_calculations(self):
"""Test session duration calculations are correct."""
session = Session(start=time(9, 30), end=time(16, 45))
duration = session.duration()
assert duration.hours == 7
assert duration.minutes == 15
assert duration.seconds == 0
def test_custom_action_functions(self):
"""Test custom action functions work."""
called = {"start": False, "end": False}
def on_start():
called["start"] = True
def on_end():
called["end"] = True
session = Session(
start=8, end=16,
on_start="custom_start", on_end="custom_end",
custom_start=on_start, custom_end=on_end
)
session.begin()
session.close()
assert called["start"] is True
assert called["end"] is True
+779
View File
@@ -0,0 +1,779 @@
"""Comprehensive tests for the sync Strategy module.
Tests cover (excluding backtest-related methods):
- Strategy initialization and attributes
- Strategy __repr__ method
- Strategy __getattr__ and __setattr__ for parameter access
- Strategy __enter__ and __exit__ context manager
- Strategy initialize method
- Strategy live_sleep static method
- Strategy sleep method in live mode
- Strategy delay method in live mode
- Strategy live_strategy method
- Strategy trade method (abstract)
- Integration tests
"""
import time
from datetime import time as dtime
from unittest.mock import MagicMock, patch
import pytest
from aiomql.lib.sync.strategy import Strategy
from aiomql.lib.sync.sessions import Session, Sessions
from aiomql.lib.sync.symbol import Symbol
from aiomql.core.config import Config
from aiomql.core.meta_trader import MetaTrader
from aiomql.core.exceptions import StopTrading
class ConcreteStrategy(Strategy):
"""Concrete implementation of Strategy for testing."""
name = "TestStrategy"
def trade(self):
"""Implement abstract trade method."""
pass
class CountingStrategy(Strategy):
"""Strategy that counts trade calls for testing."""
def __init__(self, *args, max_trades: int = 3, **kwargs):
super().__init__(*args, **kwargs)
self.trade_count = 0
self.max_trades = max_trades
def trade(self):
self.trade_count += 1
if self.trade_count >= self.max_trades:
self.running = False
class ErrorStrategy(Strategy):
"""Strategy that raises an error in trade."""
def trade(self):
raise Exception("Test error in trade")
class StopTradingStrategy(Strategy):
"""Strategy that raises StopTrading exception."""
def trade(self):
raise StopTrading("Stop trading requested")
class TestStrategyInitialization:
"""Test Strategy class initialization."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_init_with_symbol_only(self, mock_config, mock_symbol):
"""Test Strategy init with only symbol."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
assert strategy.symbol == mock_symbol
assert strategy.name == "ConcreteStrategy" # Class name used
assert strategy.running is True
assert "symbol" in strategy.parameters
assert strategy.parameters["symbol"] == "EURUSD"
assert "name" in strategy.parameters
@patch.object(Config, '__new__')
def test_init_with_custom_name(self, mock_config, mock_symbol):
"""Test Strategy init with custom name."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol, name="MyCustomStrategy")
assert strategy.name == "MyCustomStrategy"
assert strategy.parameters["name"] == "MyCustomStrategy"
@patch.object(Config, '__new__')
def test_init_with_params(self, mock_config, mock_symbol):
"""Test Strategy init with custom parameters."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
params = {"risk_percent": 0.02, "take_profit_pips": 50}
strategy = ConcreteStrategy(symbol=mock_symbol, params=params)
assert strategy.parameters["risk_percent"] == 0.02
assert strategy.parameters["take_profit_pips"] == 50
@patch.object(Config, '__new__')
def test_init_with_sessions(self, mock_config, mock_symbol):
"""Test Strategy init with custom sessions."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
sessions = Sessions(sessions=[Session(start=8, end=16)])
strategy = ConcreteStrategy(symbol=mock_symbol, sessions=sessions)
assert strategy.sessions == sessions
@patch.object(Config, '__new__')
def test_init_default_sessions(self, mock_config, mock_symbol):
"""Test Strategy init creates default sessions."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
assert strategy.sessions is not None
assert isinstance(strategy.sessions, Sessions)
@patch.object(Config, '__new__')
def test_init_creates_config(self, mock_config, mock_symbol):
"""Test Strategy init creates config."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
assert strategy.config is not None
@patch.object(Config, '__new__')
def test_init_creates_meta_trader_in_live_mode(self, mock_config, mock_symbol):
"""Test Strategy init creates MetaTrader in live mode."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
assert isinstance(strategy.mt5, MetaTrader)
@patch.object(Config, '__new__')
def test_init_class_parameters_merged(self, mock_config, mock_symbol):
"""Test class-level parameters are merged with instance params."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
class StrategyWithDefaults(Strategy):
parameters = {"default_sl": 50, "default_tp": 100}
def trade(self):
pass
strategy = StrategyWithDefaults(
symbol=mock_symbol, params={"custom_param": "value"}
)
assert strategy.parameters["default_sl"] == 50
assert strategy.parameters["default_tp"] == 100
assert strategy.parameters["custom_param"] == "value"
class TestStrategyRepr:
"""Test Strategy __repr__ method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
symbol.__repr__ = MagicMock(return_value="Symbol(EURUSD)")
return symbol
@patch.object(Config, '__new__')
def test_repr(self, mock_config, mock_symbol):
"""Test __repr__ returns formatted string."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
result = repr(strategy)
assert "ConcreteStrategy" in result
assert "Symbol(EURUSD)" in result
@patch.object(Config, '__new__')
def test_repr_with_custom_name(self, mock_config, mock_symbol):
"""Test __repr__ with custom strategy name."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol, name="MyStrategy")
result = repr(strategy)
assert "MyStrategy" in result
class TestStrategyGetSetAttr:
"""Test Strategy __getattr__ and __setattr__ methods."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_getattr_returns_parameter(self, mock_config, mock_symbol):
"""Test __getattr__ returns parameter value."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(
symbol=mock_symbol, params={"risk_percent": 0.02}
)
assert strategy.risk_percent == 0.02
@patch.object(Config, '__new__')
def test_getattr_raises_for_missing(self, mock_config, mock_symbol):
"""Test __getattr__ raises AttributeError for missing attribute."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
with pytest.raises(AttributeError) as exc_info:
_ = strategy.nonexistent_attribute
assert "nonexistent_attribute" in str(exc_info.value)
@patch.object(Config, '__new__')
def test_setattr_updates_parameter(self, mock_config, mock_symbol):
"""Test __setattr__ updates parameter value."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(
symbol=mock_symbol, params={"risk_percent": 0.02}
)
strategy.risk_percent = 0.05
assert strategy.parameters["risk_percent"] == 0.05
@patch.object(Config, '__new__')
def test_setattr_regular_attribute(self, mock_config, mock_symbol):
"""Test __setattr__ works for regular attributes."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.running = False
assert strategy.running is False
class TestStrategyContextManager:
"""Test Strategy sync context manager."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_enter_checks_session(self, mock_config, mock_symbol):
"""Test __enter__ calls sessions.check."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
strategy.sessions.current_session = MagicMock()
strategy.__enter__()
strategy.sessions.check.assert_called_once()
@patch.object(Config, '__new__')
def test_enter_sets_running_true(self, mock_config, mock_symbol):
"""Test __enter__ sets running to True."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.running = False
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
strategy.sessions.current_session = MagicMock()
strategy.__enter__()
assert strategy.running is True
@patch.object(Config, '__new__')
def test_enter_sets_current_session(self, mock_config, mock_symbol):
"""Test __enter__ sets current_session."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
mock_session = MagicMock()
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
strategy.sessions.current_session = mock_session
strategy.__enter__()
assert strategy.current_session == mock_session
@patch.object(Config, '__new__')
def test_exit_closes_session(self, mock_config, mock_symbol):
"""Test __exit__ closes current session."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
mock_session = MagicMock()
mock_session.close = MagicMock()
strategy.current_session = mock_session
strategy.__exit__(None, None, None)
mock_session.close.assert_called_once()
@patch.object(Config, '__new__')
def test_exit_sets_running_false(self, mock_config, mock_symbol):
"""Test __exit__ sets running to False."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.running = True
strategy.current_session = MagicMock()
strategy.current_session.close = MagicMock()
strategy.__exit__(None, None, None)
assert strategy.running is False
@patch.object(Config, '__new__')
def test_exit_handles_no_session(self, mock_config, mock_symbol):
"""Test __exit__ handles no current session."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.current_session = None
# Should not raise
strategy.__exit__(None, None, None)
assert strategy.running is False
@patch.object(Config, '__new__')
def test_exit_handles_exception(self, mock_config, mock_symbol):
"""Test __exit__ handles exception in close."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
mock_session = MagicMock()
mock_session.close = MagicMock(side_effect=Exception("Close error"))
strategy.current_session = mock_session
# Should not raise, just log
strategy.__exit__(None, None, None)
class TestStrategyInitialize:
"""Test Strategy initialize method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
symbol.initialize_sync = MagicMock(return_value=True)
return symbol
@patch.object(Config, '__new__')
def test_initialize_calls_symbol_initialize_sync(self, mock_config, mock_symbol):
"""Test initialize calls symbol.initialize_sync."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
result = strategy.initialize()
mock_symbol.initialize_sync.assert_called_once()
assert result is True
@patch.object(Config, '__new__')
def test_initialize_returns_symbol_result(self, mock_config, mock_symbol):
"""Test initialize returns symbol.initialize_sync result."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
mock_symbol.initialize_sync = MagicMock(return_value=False)
strategy = ConcreteStrategy(symbol=mock_symbol)
result = strategy.initialize()
assert result is False
class TestStrategyLiveSleep:
"""Test Strategy live_sleep static method."""
def test_live_sleep_sleeps_remaining_time(self):
"""Test live_sleep calculates correct sleep time."""
with patch('time.sleep') as mock_sleep:
Strategy.live_sleep(secs=60)
# Should have been called once
mock_sleep.assert_called_once()
# Sleep time should be between 0.1 and 60.1
call_args = mock_sleep.call_args[0][0]
assert 0.1 <= call_args <= 60.1
def test_live_sleep_short_duration(self):
"""Test live_sleep with short duration."""
with patch('time.sleep') as mock_sleep:
Strategy.live_sleep(secs=1)
mock_sleep.assert_called_once()
call_args = mock_sleep.call_args[0][0]
assert call_args >= 0.1
class TestStrategySleep:
"""Test Strategy sleep method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_sleep_live_mode(self, mock_config, mock_symbol):
"""Test sleep calls live_sleep in live mode."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
with patch.object(Strategy, 'live_sleep') as mock_live_sleep:
strategy.sleep(secs=60)
mock_live_sleep.assert_called_once_with(secs=60)
class TestStrategyDelay:
"""Test Strategy delay method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_delay_live_mode(self, mock_config, mock_symbol):
"""Test delay calls time.sleep in live mode."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
with patch('time.sleep') as mock_sleep:
strategy.delay(secs=5)
mock_sleep.assert_called_once_with(5)
class TestStrategyRunStrategy:
"""Test Strategy run_strategy method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_run_strategy_live_mode(self, mock_config, mock_symbol):
"""Test run_strategy calls live_strategy in live mode."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.live_strategy = MagicMock()
strategy.run_strategy()
strategy.live_strategy.assert_called_once()
class TestStrategyLiveStrategy:
"""Test Strategy live_strategy method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_live_strategy_runs_trade_loop(self, mock_config, mock_symbol):
"""Test live_strategy runs trade in a loop."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = CountingStrategy(symbol=mock_symbol, max_trades=3)
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
strategy.sessions.current_session = MagicMock()
strategy.sessions.current_session.close = MagicMock()
strategy.live_strategy()
assert strategy.trade_count == 3
assert strategy.running is False
@patch.object(Config, '__new__')
def test_live_strategy_handles_stop_trading(self, mock_config, mock_symbol):
"""Test live_strategy handles StopTrading exception."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = StopTradingStrategy(symbol=mock_symbol)
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
strategy.sessions.current_session = MagicMock()
strategy.sessions.current_session.close = MagicMock()
strategy.live_strategy()
assert strategy.running is False
@patch.object(Config, '__new__')
def test_live_strategy_handles_general_exception(self, mock_config, mock_symbol):
"""Test live_strategy handles and logs general exceptions."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ErrorStrategy(symbol=mock_symbol)
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
strategy.sessions.current_session = MagicMock()
strategy.sessions.current_session.close = MagicMock()
strategy.live_strategy()
assert strategy.running is False
class TestStrategyTrade:
"""Test Strategy trade abstract method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_trade_not_implemented(self, mock_config, mock_symbol):
"""Test trade raises NotImplementedError in base class."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
# Need to bypass ABC
strategy = Strategy.__new__(Strategy)
strategy.parameters = {}
strategy.symbol = mock_symbol
strategy.name = "TestStrategy"
strategy.running = True
strategy.config = config
strategy.mt5 = MagicMock()
with pytest.raises(NotImplementedError) as exc_info:
strategy.trade()
assert "Implement this method" in str(exc_info.value)
class TestStrategyTest:
"""Test Strategy test method."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
return symbol
@patch.object(Config, '__new__')
def test_test_calls_trade(self, mock_config, mock_symbol):
"""Test test method calls trade."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.trade = MagicMock()
strategy.test()
strategy.trade.assert_called_once()
class TestIntegration:
"""Integration tests for Strategy."""
@pytest.fixture
def mock_symbol(self):
"""Create a mock Symbol for testing."""
symbol = MagicMock(spec=Symbol)
symbol.name = "EURUSD"
symbol.initialize_sync = MagicMock(return_value=True)
return symbol
@patch.object(Config, '__new__')
def test_strategy_with_complete_setup(self, mock_config, mock_symbol):
"""Test strategy with complete configuration."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
sessions = Sessions(
sessions=[
Session(start=8, end=12, name="Morning"),
Session(start=13, end=17, name="Afternoon"),
]
)
params = {
"risk_percent": 0.02,
"max_trades": 5,
"stop_loss_pips": 30,
"take_profit_pips": 60,
}
strategy = ConcreteStrategy(
symbol=mock_symbol,
params=params,
sessions=sessions,
name="CompleteStrategy",
)
assert strategy.name == "CompleteStrategy"
assert strategy.symbol == mock_symbol
assert strategy.risk_percent == 0.02
assert strategy.max_trades == 5
assert len(strategy.sessions.sessions) == 2
@patch.object(Config, '__new__')
def test_strategy_full_lifecycle(self, mock_config, mock_symbol):
"""Test strategy through full lifecycle."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = CountingStrategy(symbol=mock_symbol, max_trades=2)
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
mock_session = MagicMock()
mock_session.close = MagicMock()
strategy.sessions.current_session = mock_session
# Enter context
strategy.__enter__()
assert strategy.running is True
# Run trades
while strategy.running:
strategy.trade()
# Exit context
strategy.__exit__(None, None, None)
assert strategy.running is False
assert strategy.trade_count == 2
@patch.object(Config, '__new__')
def test_parameter_inheritance(self, mock_config, mock_symbol):
"""Test parameter inheritance from class to instance."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
class BaseStrategy(Strategy):
parameters = {"base_param": "base_value"}
def trade(self):
pass
class DerivedStrategy(BaseStrategy):
parameters = {**BaseStrategy.parameters, "derived_param": "derived_value"}
strategy = DerivedStrategy(
symbol=mock_symbol, params={"instance_param": "instance_value"}
)
assert strategy.parameters["base_param"] == "base_value"
assert strategy.parameters["derived_param"] == "derived_value"
assert strategy.parameters["instance_param"] == "instance_value"
@patch.object(Config, '__new__')
def test_context_manager_with_statement(self, mock_config, mock_symbol):
"""Test strategy works with 'with' statement."""
config = MagicMock()
config.mode = "live"
mock_config.return_value = config
strategy = ConcreteStrategy(symbol=mock_symbol)
strategy.sessions = MagicMock()
strategy.sessions.check = MagicMock()
mock_session = MagicMock()
mock_session.close = MagicMock()
strategy.sessions.current_session = mock_session
with strategy as _:
assert strategy.running is True
assert strategy.current_session == mock_session
assert strategy.running is False
mock_session.close.assert_called_once()
+815
View File
@@ -0,0 +1,815 @@
"""Comprehensive tests for the synchronous Trader module.
Tests cover:
- Trader initialization with default and custom values
- set_trade_stop_levels_pips method
- set_trade_stop_levels_points method
- create_order_with_stops method
- create_order_with_sl method
- create_order_with_points method
- create_order_no_stops method
- check_order method
- send_order method
- record_trade method
- Integration tests with various order types
- Edge cases and boundary conditions
"""
from math import floor
import pytest
from aiomql.lib.ram import RAM
from aiomql.lib.sync.trader import Trader
from aiomql.contrib.traders.sync import SimpleTrader
from aiomql.contrib.symbols.sync import ForexSymbol
from aiomql.lib.sync.symbol import Symbol
from aiomql.core.constants import OrderType
from aiomql.lib.sync.account import Account
from aiomql.lib.sync.order import Order
from aiomql.core.config import Config
from aiomql.core.models import OrderSendResult, OrderCheckResult
class TestTraderInitialization:
"""Test Trader class initialization."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol before tests."""
self.symbol.initialize()
def test_init_with_symbol_only(self):
"""Test Trader can be initialized with just a symbol."""
trader = SimpleTrader(symbol=self.symbol)
assert trader.symbol == self.symbol
assert isinstance(trader.ram, RAM)
assert isinstance(trader.order, Order)
def test_init_with_symbol_and_ram(self):
"""Test Trader initialized with symbol and custom RAM."""
trader = SimpleTrader(symbol=self.symbol, ram=self.ram)
assert trader.symbol == self.symbol
assert trader.ram == self.ram
def test_init_creates_order_with_symbol_name(self):
"""Test Trader creates order with correct symbol name."""
trader = SimpleTrader(symbol=self.symbol)
assert trader.order.symbol == self.symbol.name
def test_init_has_config_attribute(self):
"""Test Trader has config attribute."""
trader = SimpleTrader(symbol=self.symbol)
assert hasattr(trader, 'config')
assert isinstance(trader.config, Config)
def test_init_has_parameters_attribute(self):
"""Test Trader has empty parameters dict."""
trader = SimpleTrader(symbol=self.symbol)
assert hasattr(trader, 'parameters')
assert isinstance(trader.parameters, dict)
assert trader.parameters == {}
def test_init_with_default_ram_values(self):
"""Test Trader uses default RAM if not provided."""
trader = SimpleTrader(symbol=self.symbol)
assert trader.ram.risk_to_reward == 2
assert trader.ram.risk == 1
class TestSetTradeStopLevelsPips:
"""Test set_trade_stop_levels_pips method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="EURUSD")
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_set_stop_levels_pips_buy_order(self):
"""Test setting stop levels for buy order using pips."""
tick = self.symbol.info_tick()
self.trader.order.price = tick.ask
self.trader.order.type = OrderType.BUY
pips = 50
self.trader.set_trade_stop_levels_pips(pips=pips)
expected_sl = round(tick.ask - (pips * self.symbol.pip), self.symbol.digits)
expected_tp = round(tick.ask + (pips * self.ram.risk_to_reward * self.symbol.pip), self.symbol.digits)
assert self.trader.order.sl == expected_sl
assert self.trader.order.tp == expected_tp
def test_set_stop_levels_pips_sell_order(self):
"""Test setting stop levels for sell order using pips."""
tick = self.symbol.info_tick()
self.trader.order.price = tick.bid
self.trader.order.type = OrderType.SELL
pips = 50
self.trader.set_trade_stop_levels_pips(pips=pips)
expected_sl = round(tick.bid + (pips * self.symbol.pip), self.symbol.digits)
expected_tp = round(tick.bid - (pips * self.ram.risk_to_reward * self.symbol.pip), self.symbol.digits)
assert self.trader.order.sl == expected_sl
assert self.trader.order.tp == expected_tp
def test_set_stop_levels_pips_custom_risk_to_reward(self):
"""Test setting stop levels with custom risk to reward ratio."""
tick = self.symbol.info_tick()
self.trader.order.price = tick.ask
self.trader.order.type = OrderType.BUY
pips = 30
custom_rr = 3
self.trader.set_trade_stop_levels_pips(pips=pips, risk_to_reward=custom_rr)
expected_sl = round(tick.ask - (pips * self.symbol.pip), self.symbol.digits)
expected_tp = round(tick.ask + (pips * custom_rr * self.symbol.pip), self.symbol.digits)
assert self.trader.order.sl == expected_sl
assert self.trader.order.tp == expected_tp
class TestSetTradeStopLevelsPoints:
"""Test set_trade_stop_levels_points method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="EURUSD")
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_set_stop_levels_points_buy_order(self):
"""Test setting stop levels for buy order using points."""
tick = self.symbol.info_tick()
self.trader.order.price = tick.ask
self.trader.order.type = OrderType.BUY
points = 500
self.trader.set_trade_stop_levels_points(points=points)
expected_sl = round(tick.ask - (points * self.symbol.point), self.symbol.digits)
expected_tp = round(tick.ask + (points * self.ram.risk_to_reward * self.symbol.point), self.symbol.digits)
assert self.trader.order.sl == expected_sl
assert self.trader.order.tp == expected_tp
def test_set_stop_levels_points_custom_risk_to_reward(self):
"""Test setting stop levels with custom risk to reward."""
tick = self.symbol.info_tick()
self.trader.order.price = tick.ask
self.trader.order.type = OrderType.BUY
points = 500
custom_rr = 4
self.trader.set_trade_stop_levels_points(points=points, risk_to_reward=custom_rr)
expected_sl = round(tick.ask - (points * self.symbol.point), self.symbol.digits)
expected_tp = round(tick.ask + (points * custom_rr * self.symbol.point), self.symbol.digits)
assert self.trader.order.sl == expected_sl
assert self.trader.order.tp == expected_tp
class TestCreateOrderNoStops:
"""Test create_order_no_stops method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_create_order_no_stops_buy(self):
"""Test creating buy order without stops."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
assert self.trader.order.type == OrderType.BUY
assert self.trader.order.volume == self.symbol.volume_min
assert self.trader.order.price is not None
def test_create_order_no_stops_sell(self):
"""Test creating sell order without stops."""
self.trader.create_order_no_stops(order_type=OrderType.SELL)
assert self.trader.order.type == OrderType.SELL
assert self.trader.order.volume == self.symbol.volume_min
assert self.trader.order.price is not None
def test_create_order_no_stops_with_custom_volume(self):
"""Test creating order with custom volume."""
custom_volume = self.symbol.volume_min * 2
self.trader.create_order_no_stops(order_type=OrderType.BUY, volume=custom_volume)
assert self.trader.order.volume == custom_volume
def test_create_order_no_stops_uses_correct_price(self):
"""Test order uses ask for buy and bid for sell."""
tick = self.symbol.info_tick()
self.trader.create_order_no_stops(order_type=OrderType.BUY)
# Price should be close to ask (may differ slightly due to timing)
assert abs(self.trader.order.price - tick.ask) < tick.ask * 0.01
def test_create_order_no_stops_send_success(self):
"""Test sending order without stops succeeds."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.order.send()
assert result is not None
assert result.retcode == 10009
class TestCreateOrderWithSl:
"""Test create_order_with_sl method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
cls.account = Account()
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol and account."""
self.symbol.initialize()
self.account.refresh()
def test_create_order_with_sl_sell(self):
"""Test creating sell order with stop loss."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = self.symbol.info_tick()
sl = tick.bid + dsl
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
assert self.trader.order.type == OrderType.SELL
assert self.trader.order.sl == sl
assert self.trader.order.tp is not None
assert self.trader.order.volume > 0
def test_create_order_with_sl_buy(self):
"""Test creating buy order with stop loss."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = self.symbol.info_tick()
sl = tick.ask - dsl
self.trader.create_order_with_sl(order_type=OrderType.BUY, sl=sl)
assert self.trader.order.type == OrderType.BUY
assert self.trader.order.sl == sl
assert self.trader.order.tp is not None
def test_create_order_with_sl_respects_risk_to_reward(self):
"""Test TP is set according to risk to reward ratio."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = self.symbol.info_tick()
sl = tick.bid + dsl
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
# TP should be approximately at dsl * risk_to_reward distance from price
expected_dtp = dsl * self.ram.risk_to_reward
actual_dtp = abs(self.trader.order.price - self.trader.order.tp)
assert abs(actual_dtp - expected_dtp) < self.symbol.point * 10
def test_create_order_with_sl_custom_amount_to_risk(self):
"""Test creating order with custom amount to risk."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = self.symbol.info_tick()
sl = tick.bid + dsl
custom_amount = 20
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl, amount_to_risk=custom_amount)
assert self.trader.order.volume > 0
def test_create_order_with_sl_send_success(self):
"""Test order with SL can be sent successfully."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = self.symbol.info_tick()
sl = tick.bid + dsl
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
result = self.trader.order.send()
assert result is not None
assert result.retcode == 10009
class TestCreateOrderWithStops:
"""Test create_order_with_stops method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
cls.account = Account()
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol and account."""
self.symbol.initialize()
self.account.refresh()
def test_create_order_with_stops_buy(self):
"""Test creating buy order with SL and TP."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
dtp = dsl * self.ram.risk_to_reward
tick = self.symbol.info_tick()
sl = tick.ask - dsl
tp = tick.ask + dtp
self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp)
assert self.trader.order.type == OrderType.BUY
assert self.trader.order.sl == sl
assert self.trader.order.tp == tp
assert self.trader.order.volume > 0
def test_create_order_with_stops_sell(self):
"""Test creating sell order with SL and TP."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
dtp = dsl * self.ram.risk_to_reward
tick = self.symbol.info_tick()
sl = tick.bid + dsl
tp = tick.bid - dtp
self.trader.create_order_with_stops(order_type=OrderType.SELL, sl=sl, tp=tp)
assert self.trader.order.type == OrderType.SELL
assert self.trader.order.sl == sl
assert self.trader.order.tp == tp
def test_create_order_with_stops_send_success(self):
"""Test order with stops can be sent successfully."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
dtp = dsl * self.ram.risk_to_reward
tick = self.symbol.info_tick()
sl = tick.ask - dsl
tp = tick.ask + dtp
self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp)
result = self.trader.order.send()
assert result is not None
assert result.retcode == 10009
def test_create_order_with_stops_custom_amount(self):
"""Test creating order with custom amount to risk."""
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
dtp = dsl * 2
tick = self.symbol.info_tick()
sl = tick.ask - dsl
tp = tick.ask + dtp
custom_amount = 25
self.trader.create_order_with_stops(
order_type=OrderType.BUY, sl=sl, tp=tp, amount_to_risk=custom_amount
)
assert self.trader.order.volume > 0
class TestCreateOrderWithPoints:
"""Test create_order_with_points method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
cls.account = Account()
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
self.account.refresh()
def test_create_order_with_points_buy(self):
"""Test creating buy order with points."""
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
assert self.trader.order.type == OrderType.BUY
assert self.trader.order.volume > 0
assert self.trader.order.sl is not None
assert self.trader.order.tp is not None
def test_create_order_with_points_sell(self):
"""Test creating sell order with points."""
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
self.trader.create_order_with_points(order_type=OrderType.SELL, points=points)
assert self.trader.order.type == OrderType.SELL
assert self.trader.order.volume > 0
def test_create_order_with_points_send_success(self):
"""Test order with points can be sent successfully."""
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
result = self.trader.order.send()
assert result is not None
assert result.retcode == 10009
def test_create_order_with_points_custom_risk_to_reward(self):
"""Test order with custom risk to reward."""
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
custom_rr = 3
self.trader.create_order_with_points(
order_type=OrderType.BUY, points=points, risk_to_reward=custom_rr
)
# TP should be at points * custom_rr distance from price
expected_tp_distance = points * custom_rr * self.symbol.point
actual_tp_distance = abs(self.trader.order.tp - self.trader.order.price)
assert abs(actual_tp_distance - expected_tp_distance) < self.symbol.point * 10
class TestCheckOrder:
"""Test check_order method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_check_order_returns_order_check_result(self):
"""Test check_order returns OrderCheckResult."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.check_order()
assert result is None or isinstance(result, OrderCheckResult)
def test_check_order_success(self):
"""Test check_order succeeds for valid order."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.check_order()
assert result is not None
assert result.retcode == 0
def test_check_order_has_margin_info(self):
"""Test check result contains margin information."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.check_order()
assert result is not None
assert hasattr(result, 'margin')
def test_check_order_sell(self):
"""Test check_order works for sell orders."""
self.trader.create_order_no_stops(order_type=OrderType.SELL)
result = self.trader.check_order()
assert result is not None
assert result.retcode == 0
class TestSendOrder:
"""Test send_order method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_send_order_returns_order_send_result(self):
"""Test send_order returns OrderSendResult."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.send_order()
assert result is None or isinstance(result, OrderSendResult)
def test_send_order_success(self):
"""Test send_order succeeds for valid order."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.send_order()
assert result is not None
assert result.retcode == 10009
def test_send_order_has_deal_ticket(self):
"""Test send result contains deal ticket."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.send_order()
assert result is not None
assert hasattr(result, 'deal')
assert result.deal > 0
def test_send_order_has_order_ticket(self):
"""Test send result contains order ticket."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.send_order()
assert result is not None
assert hasattr(result, 'order')
assert result.order > 0
class TestRecordTrade:
"""Test record_trade method."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_record_trade_with_successful_order(self):
"""Test recording a successful trade."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.send_order()
# Should not raise an error
self.trader.record_trade(result=result, parameters={"test": "value"}, name="TestStrategy", use_task_queue=False)
def test_record_trade_with_parameters(self):
"""Test recording trade with custom parameters."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.send_order()
params = {"strategy": "test", "risk": 1, "timeframe": "H1"}
self.trader.record_trade(result=result, parameters=params, name="MyStrategy", use_task_queue=False)
def test_record_trade_without_parameters(self):
"""Test recording trade without parameters."""
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result = self.trader.send_order()
# Should not raise an error
self.trader.record_trade(result=result, name="SimpleStrategy", use_task_queue=False)
class TestTraderWithDifferentSymbols:
"""Test Trader with different symbols."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.btc_usd = ForexSymbol(name="BTCUSD")
cls.eur_jpy = ForexSymbol(name="EURJPY")
cls.ram = RAM(fixed_amount=10)
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbols."""
self.btc_usd.initialize()
self.eur_jpy.initialize()
def test_trader_btc_usd(self):
"""Test trader with BTCUSD symbol."""
trader = SimpleTrader(symbol=self.btc_usd, ram=self.ram)
trader.create_order_no_stops(order_type=OrderType.BUY)
result = trader.send_order()
assert result is not None
assert result.retcode == 10009
def test_trader_eur_jpy(self):
"""Test trader with EURJPY symbol."""
trader = SimpleTrader(symbol=self.eur_jpy, ram=self.ram)
trader.create_order_no_stops(order_type=OrderType.SELL)
result = trader.send_order()
assert result is not None
assert result.retcode == 10009
class TestTraderIntegration:
"""Integration tests for Trader."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
cls.ram = RAM(fixed_amount=10, risk_to_reward=2)
cls.trader = SimpleTrader(symbol=cls.symbol, ram=cls.ram)
cls.account = Account()
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol and account."""
self.symbol.initialize()
self.account.refresh()
def test_full_trade_flow_buy(self):
"""Test complete trade flow for buy order."""
# Create order
points = self.symbol.trade_stops_level * 2 + self.symbol.spread
self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
# Check order
check_result = self.trader.check_order()
assert check_result is not None
assert check_result.retcode == 0
# Send order
send_result = self.trader.send_order()
assert send_result is not None
assert send_result.retcode == 10009
# Record trade
self.trader.record_trade(result=send_result, parameters={"test": True}, name="IntegrationTest", use_task_queue=False)
def test_full_trade_flow_sell(self):
"""Test complete trade flow for sell order."""
# Create order
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = self.symbol.info_tick()
sl = tick.bid + dsl
self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
# Check order
check_result = self.trader.check_order()
assert check_result is not None
assert check_result.retcode == 0
# Send order
send_result = self.trader.send_order()
assert send_result is not None
assert send_result.retcode == 10009
def test_multiple_orders_same_trader(self):
"""Test creating multiple orders with same trader."""
# First order
self.trader.create_order_no_stops(order_type=OrderType.BUY)
result1 = self.trader.send_order()
assert result1 is not None
assert result1.retcode == 10009
# Second order (different type)
self.trader.create_order_no_stops(order_type=OrderType.SELL)
result2 = self.trader.send_order()
assert result2 is not None
assert result2.retcode == 10009
class TestTraderEdgeCases:
"""Test edge cases and boundary conditions."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_trader_with_zero_fixed_amount(self):
"""Test trader with zero fixed amount RAM."""
ram = RAM(fixed_amount=0)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
assert trader.ram.fixed_amount == 0
def test_trader_with_high_risk_to_reward(self):
"""Test trader with high risk to reward ratio."""
ram = RAM(fixed_amount=10, risk_to_reward=10)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
assert trader.ram.risk_to_reward == 10
def test_trader_with_low_risk_to_reward(self):
"""Test trader with low risk to reward ratio."""
ram = RAM(fixed_amount=10, risk_to_reward=0.5)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
assert trader.ram.risk_to_reward == 0.5
def test_trader_order_modification_after_creation(self):
"""Test modifying order attributes after creation."""
ram = RAM(fixed_amount=10)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
trader.create_order_no_stops(order_type=OrderType.BUY)
original_volume = trader.order.volume
trader.order.volume = original_volume * 2
assert trader.order.volume == original_volume * 2
def test_trader_parameters_modification(self):
"""Test modifying trader parameters."""
ram = RAM(fixed_amount=10)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
trader.parameters["custom_param"] = "value"
trader.parameters["risk"] = 5
assert trader.parameters["custom_param"] == "value"
assert trader.parameters["risk"] == 5
def test_trader_with_minimum_volume(self):
"""Test creating order with minimum volume."""
ram = RAM(fixed_amount=1) # Very small amount
trader = SimpleTrader(symbol=self.symbol, ram=ram)
trader.create_order_no_stops(order_type=OrderType.BUY)
assert trader.order.volume >= self.symbol.volume_min
class TestTraderRAMIntegration:
"""Test Trader integration with RAM."""
@classmethod
def setup_class(cls):
"""Set up test fixtures."""
cls.symbol = ForexSymbol(name="BTCUSD")
@pytest.fixture(scope="class", autouse=True)
def initialize(self):
"""Initialize symbol."""
self.symbol.initialize()
def test_trader_uses_ram_get_amount(self):
"""Test trader uses RAM get_amount for volume calculation."""
ram = RAM(fixed_amount=20)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = trader.symbol.info_tick()
sl = tick.ask - dsl
trader.create_order_with_sl(order_type=OrderType.BUY, sl=sl)
# Volume should be calculated based on RAM fixed_amount (20)
assert trader.order.volume > 0
def test_trader_uses_ram_risk_to_reward(self):
"""Test trader uses RAM risk_to_reward for TP calculation."""
ram = RAM(fixed_amount=10, risk_to_reward=3)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
dsl = (self.symbol.trade_stops_level * 2 + self.symbol.spread) * self.symbol.point
tick = trader.symbol.info_tick()
sl = tick.ask - dsl
trader.create_order_with_sl(order_type=OrderType.BUY, sl=sl)
# TP distance should be 3x the SL distance
sl_distance = abs(trader.order.price - trader.order.sl)
tp_distance = abs(trader.order.tp - trader.order.price)
assert abs(tp_distance - (sl_distance * 3)) < self.symbol.point * 10
def test_trader_modifying_ram_after_init(self):
"""Test modifying RAM after trader initialization."""
ram = RAM(fixed_amount=10)
trader = SimpleTrader(symbol=self.symbol, ram=ram)
trader.ram.modify_ram(fixed_amount=25, risk_to_reward=4)
assert trader.ram.fixed_amount == 25
assert trader.ram.risk_to_reward == 4