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