"""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)