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

621 lines
20 KiB
Python

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