test: add explicit unit tests for calculate_positions_margin_by_symbol and calculate_positions_margin_safe (#50)

Covers all acceptance criteria: partial failure with warning log, all-fail,
empty symbol list with no-broker-call assertion, duplicate deduplication,
successful aggregation with first-seen key order, suppress_errors=False
propagation, and three calculate_positions_margin_safe cases (partial skip,
all-fail → 0.0, empty list → 0.0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
agent
2026-06-23 10:26:05 +00:00
co-authored by Claude Sonnet 4.6
parent 823cb5b0a4
commit 7a17b8ca3e
+48 -9
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, cast from typing import Any, cast
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -3135,7 +3136,7 @@ class TestCalculatePositionsMarginBySymbol:
"""Tests for calculate_positions_margin_by_symbol (#50).""" """Tests for calculate_positions_margin_by_symbol (#50)."""
def test_all_symbols_succeed(self, mocker: MockerFixture) -> None: def test_all_symbols_succeed(self, mocker: MockerFixture) -> None:
"""Returns one entry per symbol when all margin calls succeed.""" """Returns one entry per symbol in first-seen order when all calls succeed."""
client = _mock_trade_client() client = _mock_trade_client()
mocker.patch( mocker.patch(
"mt5cli.trading.calculate_positions_margin", "mt5cli.trading.calculate_positions_margin",
@@ -3146,21 +3147,27 @@ class TestCalculatePositionsMarginBySymbol:
client, symbols=["EURUSD", "GBPUSD"] client, symbols=["EURUSD", "GBPUSD"]
) )
assert result == {"EURUSD": 12.5, "GBPUSD": 30.0} assert list(result.keys()) == ["EURUSD", "GBPUSD"]
_assert_close(result["EURUSD"], 12.5)
_assert_close(result["GBPUSD"], 30.0)
def test_one_symbol_fails_suppress_errors_true(self, mocker: MockerFixture) -> None: def test_one_symbol_fails_suppress_errors_true(
"""Skips the failing symbol and returns the successful one.""" self, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
) -> None:
"""Skips the failing symbol, emits a warning, and returns the successful one."""
client = _mock_trade_client() client = _mock_trade_client()
mocker.patch( mocker.patch(
"mt5cli.trading.calculate_positions_margin", "mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("tick unavailable"), 30.0], side_effect=[Mt5TradingError("tick unavailable"), 30.0],
) )
result = calculate_positions_margin_by_symbol( with caplog.at_level(logging.WARNING, logger="mt5cli.trading"):
client, symbols=["EURUSD", "GBPUSD"], suppress_errors=True result = calculate_positions_margin_by_symbol(
) client, symbols=["EURUSD", "GBPUSD"], suppress_errors=True
)
assert result == {"GBPUSD": 30.0} assert result == {"GBPUSD": 30.0}
assert any("EURUSD" in record.message for record in caplog.records)
def test_one_symbol_fails_suppress_errors_false( def test_one_symbol_fails_suppress_errors_false(
self, mocker: MockerFixture self, mocker: MockerFixture
@@ -3191,11 +3198,13 @@ class TestCalculatePositionsMarginBySymbol:
assert result == {} assert result == {}
def test_empty_symbol_list(self) -> None: def test_empty_symbol_list(self, mocker: MockerFixture) -> None:
"""Returns an empty dict for an empty input list.""" """Returns an empty dict for an empty input list without any broker calls."""
client = _mock_trade_client() client = _mock_trade_client()
mock_calc = mocker.patch("mt5cli.trading.calculate_positions_margin")
result = calculate_positions_margin_by_symbol(client, symbols=[]) result = calculate_positions_margin_by_symbol(client, symbols=[])
assert result == {} assert result == {}
mock_calc.assert_not_called()
def test_duplicate_symbols_preserve_first_seen_order( def test_duplicate_symbols_preserve_first_seen_order(
self, mocker: MockerFixture self, mocker: MockerFixture
@@ -3229,3 +3238,33 @@ class TestCalculatePositionsMarginSafe:
total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"]) total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"])
_assert_close(total, 42.5) _assert_close(total, 42.5)
def test_partial_failure_skips_and_sums(self, mocker: MockerFixture) -> None:
"""Sums only successful margins when one symbol raises."""
client = _mock_trade_client()
mocker.patch(
"mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("tick unavailable"), 30.0],
)
total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"])
_assert_close(total, 30.0)
def test_all_symbols_fail_returns_zero(self, mocker: MockerFixture) -> None:
"""Returns 0.0 when every symbol raises."""
client = _mock_trade_client()
mocker.patch(
"mt5cli.trading.calculate_positions_margin",
side_effect=[Mt5TradingError("err1"), Mt5RuntimeError("err2")],
)
total = calculate_positions_margin_safe(client, symbols=["EURUSD", "GBPUSD"])
_assert_close(total, 0.0)
def test_empty_symbols_returns_zero(self) -> None:
"""Returns 0.0 for an empty symbol list."""
client = _mock_trade_client()
total = calculate_positions_margin_safe(client, symbols=[])
_assert_close(total, 0.0)