mt5cli 0.9.3__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mt5cli-0.9.3 → mt5cli-0.9.4}/PKG-INFO +1 -1
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/public-contract.md +19 -18
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/__init__.py +2 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/contract.py +1 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/trading.py +54 -8
- {mt5cli-0.9.3 → mt5cli-0.9.4}/pyproject.toml +1 -1
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_contracts.py +2 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_trading.py +137 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/uv.lock +1 -1
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.claude/agents/codex.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.claude/settings.json +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/FUNDING.yml +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/dependabot.yml +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/renovate.json +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/workflows/ci.yml +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/workflows/claude.yml +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/workflows/release.yml +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/.gitignore +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/AGENTS.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/CLAUDE.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/LICENSE +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/README.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/cli.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/client.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/converters.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/exceptions.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/history.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/index.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/schemas.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/sdk.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/storage.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/trading.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/utils.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/index.md +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mkdocs.yml +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/__main__.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/cli.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/client.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/converters.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/exceptions.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/history.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/retry.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/schemas.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/sdk.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/storage.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/utils.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/__init__.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/conftest.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_cli.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_history.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_sdk.py +0 -0
- {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mt5cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Generic MT5 data and execution infrastructure for Python applications
|
|
5
5
|
Project-URL: Repository, https://github.com/dceoy/mt5cli.git
|
|
6
6
|
Author-email: dceoy <dceoy@users.noreply.github.com>
|
|
@@ -90,24 +90,25 @@ diagrams.
|
|
|
90
90
|
These helpers implement broker-facing calculations only. They do not encode
|
|
91
91
|
strategy entries, exits, Kelly sizing, or signal logic.
|
|
92
92
|
|
|
93
|
-
| Symbol | Role
|
|
94
|
-
| ------------------------------------------------------------------------------------------------------------------------------ |
|
|
95
|
-
| `get_account_snapshot`, `get_symbol_snapshot`, `get_tick_snapshot`, `get_positions_frame` | Normalized account/symbol/tick/position views
|
|
96
|
-
| `extract_tick_price` | Positive finite bid/ask extraction from tick mappings
|
|
97
|
-
| `detect_position_side` | Net long / short / flat from open positions
|
|
98
|
-
| `calculate_spread_ratio` | Relative bid-ask spread
|
|
99
|
-
| `calculate_margin_and_volume`, `calculate_volume_by_margin`, `calculate_new_position_margin_ratio` | Margin budget and volume sizing
|
|
100
|
-
| `normalize_order_volume`, `estimate_order_margin`, `calculate_positions_margin` | Broker volume normalization and margin totals
|
|
101
|
-
| `calculate_positions_margin_by_symbol` | Per-symbol margin map (resilient, first-seen order)
|
|
102
|
-
| `calculate_positions_margin_safe` | Summed total margin across symbols (failed symbols skipped)
|
|
103
|
-
| `calculate_projected_margin_ratio` | Estimated symbol margin/equity after optional new exposure
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
93
|
+
| Symbol | Role |
|
|
94
|
+
| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
|
95
|
+
| `get_account_snapshot`, `get_symbol_snapshot`, `get_tick_snapshot`, `get_positions_frame` | Normalized account/symbol/tick/position views |
|
|
96
|
+
| `extract_tick_price` | Positive finite bid/ask extraction from tick mappings |
|
|
97
|
+
| `detect_position_side` | Net long / short / flat from open positions |
|
|
98
|
+
| `calculate_spread_ratio` | Relative bid-ask spread |
|
|
99
|
+
| `calculate_margin_and_volume`, `calculate_volume_by_margin`, `calculate_new_position_margin_ratio` | Margin budget and volume sizing |
|
|
100
|
+
| `normalize_order_volume`, `estimate_order_margin`, `calculate_positions_margin` | Broker volume normalization and margin totals |
|
|
101
|
+
| `calculate_positions_margin_by_symbol` | Per-symbol margin map (resilient, first-seen order) |
|
|
102
|
+
| `calculate_positions_margin_safe` | Summed total margin across symbols (failed symbols skipped) |
|
|
103
|
+
| `calculate_projected_margin_ratio` | Estimated symbol-scoped margin/equity after optional new exposure |
|
|
104
|
+
| `calculate_account_projected_margin_ratio` | Account snapshot margin/equity after optional new exposure |
|
|
105
|
+
| `calculate_symbol_group_margin_ratio` | Estimated symbol-group margin/equity with optional exposure |
|
|
106
|
+
| `determine_order_limits` | SL/TP price levels from ratios |
|
|
107
|
+
| `calculate_trailing_stop_updates` | Per-ticket generic trailing stop-loss update plan |
|
|
108
|
+
| `ensure_symbol_selected` | Select/verify Market Watch visibility |
|
|
109
|
+
| `place_market_order`, `close_open_positions`, `update_sltp_for_open_positions`, `update_trailing_stop_loss_for_open_positions` | Order execution helpers (`dry_run` supported) |
|
|
110
|
+
| `MarginVolume`, `OrderLimits`, `OrderExecutionResult` | Typed return contracts for order helpers |
|
|
111
|
+
| `OrderSide`, `OrderFillingMode`, `OrderTimeMode`, `PositionSide`, `ExecutionStatus` | Typed enums for order helpers |
|
|
111
112
|
|
|
112
113
|
`MT5Client.order_send()` and CLI `order-send --yes` are live execution paths.
|
|
113
114
|
|
|
@@ -119,6 +119,7 @@ from .trading import (
|
|
|
119
119
|
OrderSide,
|
|
120
120
|
OrderTimeMode,
|
|
121
121
|
PositionSide,
|
|
122
|
+
calculate_account_projected_margin_ratio,
|
|
122
123
|
calculate_margin_and_volume,
|
|
123
124
|
calculate_new_position_margin_ratio,
|
|
124
125
|
calculate_positions_margin,
|
|
@@ -196,6 +197,7 @@ __all__ = [
|
|
|
196
197
|
"build_config",
|
|
197
198
|
"build_rate_targets",
|
|
198
199
|
"build_rate_view_name",
|
|
200
|
+
"calculate_account_projected_margin_ratio",
|
|
199
201
|
"calculate_margin_and_volume",
|
|
200
202
|
"calculate_new_position_margin_ratio",
|
|
201
203
|
"calculate_positions_margin",
|
|
@@ -26,6 +26,7 @@ STABLE_SDK_EXPORTS: frozenset[str] = frozenset({
|
|
|
26
26
|
"build_config",
|
|
27
27
|
"build_rate_targets",
|
|
28
28
|
"build_rate_view_name",
|
|
29
|
+
"calculate_account_projected_margin_ratio",
|
|
29
30
|
"calculate_margin_and_volume",
|
|
30
31
|
"calculate_new_position_margin_ratio",
|
|
31
32
|
"calculate_projected_margin_ratio",
|
|
@@ -127,6 +127,7 @@ __all__ = [
|
|
|
127
127
|
"OrderSide",
|
|
128
128
|
"OrderTimeMode",
|
|
129
129
|
"PositionSide",
|
|
130
|
+
"calculate_account_projected_margin_ratio",
|
|
130
131
|
"calculate_margin_and_volume",
|
|
131
132
|
"calculate_new_position_margin_ratio",
|
|
132
133
|
"calculate_positions_margin",
|
|
@@ -834,15 +835,60 @@ def calculate_new_position_margin_ratio(
|
|
|
834
835
|
|
|
835
836
|
def _account_equity(client: Mt5TradingClient) -> float:
|
|
836
837
|
account = get_account_snapshot(client)
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
838
|
+
return _required_account_number(account, "equity", allow_zero=False)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _required_account_number(
|
|
842
|
+
account: Mapping[str, object],
|
|
843
|
+
field: str,
|
|
844
|
+
*,
|
|
845
|
+
allow_zero: bool,
|
|
846
|
+
) -> float:
|
|
847
|
+
raw_value = account.get(field)
|
|
848
|
+
if isinstance(raw_value, bool) or not isinstance(raw_value, Real):
|
|
849
|
+
msg = f"Account {field} must be a finite number to calculate margin ratio."
|
|
844
850
|
raise Mt5TradingError(msg)
|
|
845
|
-
|
|
851
|
+
value = float(raw_value)
|
|
852
|
+
if (
|
|
853
|
+
not isfinite(value)
|
|
854
|
+
or (not allow_zero and value <= 0)
|
|
855
|
+
or (allow_zero and value < 0)
|
|
856
|
+
):
|
|
857
|
+
msg = (
|
|
858
|
+
f"Account {field} must be a non-negative finite number."
|
|
859
|
+
if allow_zero
|
|
860
|
+
else f"Account {field} must be a positive finite number."
|
|
861
|
+
)
|
|
862
|
+
raise Mt5TradingError(msg)
|
|
863
|
+
return value
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def calculate_account_projected_margin_ratio(
|
|
867
|
+
client: Mt5TradingClient,
|
|
868
|
+
*,
|
|
869
|
+
symbol: str | None = None,
|
|
870
|
+
new_position_side: OrderSide | None = None,
|
|
871
|
+
new_position_volume: float = 0.0,
|
|
872
|
+
) -> float:
|
|
873
|
+
"""Return account-wide current plus optional new-position margin over equity.
|
|
874
|
+
|
|
875
|
+
Current exposure comes from the broker account snapshot ``margin`` field so
|
|
876
|
+
unrelated open positions remain in the baseline. Optional projected
|
|
877
|
+
exposure is added via :func:`estimate_order_margin` only when a symbol, side,
|
|
878
|
+
and positive volume are all supplied.
|
|
879
|
+
|
|
880
|
+
"""
|
|
881
|
+
account = get_account_snapshot(client)
|
|
882
|
+
equity = _required_account_number(account, "equity", allow_zero=False)
|
|
883
|
+
margin = _required_account_number(account, "margin", allow_zero=True)
|
|
884
|
+
if symbol is not None and new_position_side is not None and new_position_volume > 0:
|
|
885
|
+
margin += estimate_order_margin(
|
|
886
|
+
client,
|
|
887
|
+
symbol,
|
|
888
|
+
new_position_side,
|
|
889
|
+
new_position_volume,
|
|
890
|
+
)
|
|
891
|
+
return margin / equity
|
|
846
892
|
|
|
847
893
|
|
|
848
894
|
def calculate_projected_margin_ratio(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mt5cli"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.4"
|
|
4
4
|
description = "Generic MT5 data and execution infrastructure for Python applications"
|
|
5
5
|
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
6
6
|
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
|
@@ -37,6 +37,7 @@ from mt5cli import (
|
|
|
37
37
|
RateTarget,
|
|
38
38
|
build_config,
|
|
39
39
|
build_rate_targets,
|
|
40
|
+
calculate_account_projected_margin_ratio,
|
|
40
41
|
calculate_margin_and_volume,
|
|
41
42
|
calculate_positions_margin,
|
|
42
43
|
calculate_projected_margin_ratio,
|
|
@@ -684,6 +685,7 @@ class TestStableSdkContract:
|
|
|
684
685
|
assert price is not None
|
|
685
686
|
assert abs(price - 1.2) < 1e-9
|
|
686
687
|
assert callable(calculate_trailing_stop_updates)
|
|
688
|
+
assert callable(calculate_account_projected_margin_ratio)
|
|
687
689
|
assert callable(calculate_projected_margin_ratio)
|
|
688
690
|
assert callable(calculate_symbol_group_margin_ratio)
|
|
689
691
|
|
|
@@ -19,6 +19,7 @@ from mt5cli.trading import (
|
|
|
19
19
|
MarginVolume,
|
|
20
20
|
OrderExecutionResult,
|
|
21
21
|
OrderLimits,
|
|
22
|
+
calculate_account_projected_margin_ratio,
|
|
22
23
|
calculate_margin_and_volume,
|
|
23
24
|
calculate_new_position_margin_ratio,
|
|
24
25
|
calculate_positions_margin,
|
|
@@ -1891,6 +1892,142 @@ class TestVolumeAndExecution:
|
|
|
1891
1892
|
_assert_close(result, 0.024)
|
|
1892
1893
|
client.order_calc_margin.assert_called_once_with(11, "EURUSD", 0.1, 1.1)
|
|
1893
1894
|
|
|
1895
|
+
@pytest.mark.parametrize(
|
|
1896
|
+
("account", "kwargs", "candidate_margin", "expected_ratio"),
|
|
1897
|
+
[
|
|
1898
|
+
({"equity": 10_000.0, "margin": 4500.0}, {}, None, 0.45),
|
|
1899
|
+
(
|
|
1900
|
+
{"equity": 10_000.0, "margin": 4500.0},
|
|
1901
|
+
{
|
|
1902
|
+
"symbol": "EURUSD",
|
|
1903
|
+
"new_position_side": "BUY",
|
|
1904
|
+
"new_position_volume": 0.1,
|
|
1905
|
+
},
|
|
1906
|
+
1000.0,
|
|
1907
|
+
0.55,
|
|
1908
|
+
),
|
|
1909
|
+
({"equity": 10_000.0, "margin": 55.0}, {}, None, 0.0055),
|
|
1910
|
+
],
|
|
1911
|
+
)
|
|
1912
|
+
def test_account_projected_margin_ratio_uses_account_margin_baseline(
|
|
1913
|
+
self,
|
|
1914
|
+
account: dict[str, object],
|
|
1915
|
+
kwargs: dict[str, object],
|
|
1916
|
+
candidate_margin: float | None,
|
|
1917
|
+
expected_ratio: float,
|
|
1918
|
+
mocker: MockerFixture,
|
|
1919
|
+
) -> None:
|
|
1920
|
+
"""Test account-wide exposure uses snapshot margin plus optional candidate."""
|
|
1921
|
+
client = _mock_trade_client()
|
|
1922
|
+
client.account_info_as_dict.return_value = account
|
|
1923
|
+
client.positions_get_as_df.return_value = pd.DataFrame(
|
|
1924
|
+
[{"symbol": "GBPUSD", "type": 0, "volume": 2.0}],
|
|
1925
|
+
)
|
|
1926
|
+
mock_margin = mocker.patch(
|
|
1927
|
+
"mt5cli.trading.estimate_order_margin",
|
|
1928
|
+
return_value=candidate_margin,
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
result = calculate_account_projected_margin_ratio(client, **cast("Any", kwargs))
|
|
1932
|
+
|
|
1933
|
+
_assert_close(result, expected_ratio)
|
|
1934
|
+
if candidate_margin is None:
|
|
1935
|
+
mock_margin.assert_not_called()
|
|
1936
|
+
else:
|
|
1937
|
+
mock_margin.assert_called_once_with(client, "EURUSD", "BUY", 0.1)
|
|
1938
|
+
client.positions_get_as_df.assert_not_called()
|
|
1939
|
+
|
|
1940
|
+
@pytest.mark.parametrize(
|
|
1941
|
+
("kwargs", "expected_ratio"),
|
|
1942
|
+
[
|
|
1943
|
+
({"new_position_side": "BUY", "new_position_volume": 0.1}, 0.45),
|
|
1944
|
+
({"symbol": "EURUSD", "new_position_volume": 0.1}, 0.45),
|
|
1945
|
+
({"symbol": "EURUSD", "new_position_side": "BUY"}, 0.45),
|
|
1946
|
+
(
|
|
1947
|
+
{
|
|
1948
|
+
"symbol": "EURUSD",
|
|
1949
|
+
"new_position_side": "BUY",
|
|
1950
|
+
"new_position_volume": -0.1,
|
|
1951
|
+
},
|
|
1952
|
+
0.45,
|
|
1953
|
+
),
|
|
1954
|
+
],
|
|
1955
|
+
)
|
|
1956
|
+
def test_account_projected_margin_ratio_skips_incomplete_candidate(
|
|
1957
|
+
self,
|
|
1958
|
+
kwargs: dict[str, object],
|
|
1959
|
+
expected_ratio: float,
|
|
1960
|
+
mocker: MockerFixture,
|
|
1961
|
+
) -> None:
|
|
1962
|
+
"""Test candidate margin is added only when symbol, side, and volume exist."""
|
|
1963
|
+
client = _mock_trade_client()
|
|
1964
|
+
client.account_info_as_dict.return_value = {
|
|
1965
|
+
"equity": 10_000.0,
|
|
1966
|
+
"margin": 4500.0,
|
|
1967
|
+
}
|
|
1968
|
+
mock_margin = mocker.patch("mt5cli.trading.estimate_order_margin")
|
|
1969
|
+
|
|
1970
|
+
result = calculate_account_projected_margin_ratio(client, **cast("Any", kwargs))
|
|
1971
|
+
|
|
1972
|
+
_assert_close(result, expected_ratio)
|
|
1973
|
+
mock_margin.assert_not_called()
|
|
1974
|
+
|
|
1975
|
+
@pytest.mark.parametrize(
|
|
1976
|
+
("account", "match"),
|
|
1977
|
+
[
|
|
1978
|
+
({"margin": 4500.0}, "Account equity"),
|
|
1979
|
+
({"equity": None, "margin": 4500.0}, "Account equity"),
|
|
1980
|
+
({"equity": "10000", "margin": 4500.0}, "Account equity"),
|
|
1981
|
+
({"equity": True, "margin": 4500.0}, "Account equity"),
|
|
1982
|
+
({"equity": float("nan"), "margin": 4500.0}, "Account equity"),
|
|
1983
|
+
({"equity": float("inf"), "margin": 4500.0}, "Account equity"),
|
|
1984
|
+
({"equity": 0.0, "margin": 4500.0}, "Account equity"),
|
|
1985
|
+
({"equity": -1.0, "margin": 4500.0}, "Account equity"),
|
|
1986
|
+
({"equity": 10_000.0}, "Account margin"),
|
|
1987
|
+
({"equity": 10_000.0, "margin": None}, "Account margin"),
|
|
1988
|
+
({"equity": 10_000.0, "margin": "4500"}, "Account margin"),
|
|
1989
|
+
({"equity": 10_000.0, "margin": True}, "Account margin"),
|
|
1990
|
+
({"equity": 10_000.0, "margin": False}, "Account margin"),
|
|
1991
|
+
({"equity": 10_000.0, "margin": float("nan")}, "Account margin"),
|
|
1992
|
+
({"equity": 10_000.0, "margin": float("inf")}, "Account margin"),
|
|
1993
|
+
({"equity": 10_000.0, "margin": -1.0}, "Account margin"),
|
|
1994
|
+
],
|
|
1995
|
+
)
|
|
1996
|
+
def test_account_projected_margin_ratio_rejects_invalid_snapshot_fields(
|
|
1997
|
+
self,
|
|
1998
|
+
account: dict[str, object],
|
|
1999
|
+
match: str,
|
|
2000
|
+
) -> None:
|
|
2001
|
+
"""Test invalid account equity and margin fields fail closed."""
|
|
2002
|
+
client = _mock_trade_client()
|
|
2003
|
+
client.account_info_as_dict.return_value = account
|
|
2004
|
+
|
|
2005
|
+
with pytest.raises(Mt5TradingError, match=match):
|
|
2006
|
+
calculate_account_projected_margin_ratio(client)
|
|
2007
|
+
|
|
2008
|
+
def test_account_projected_margin_ratio_propagates_candidate_margin_error(
|
|
2009
|
+
self,
|
|
2010
|
+
mocker: MockerFixture,
|
|
2011
|
+
) -> None:
|
|
2012
|
+
"""Test candidate margin errors are not suppressed."""
|
|
2013
|
+
client = _mock_trade_client()
|
|
2014
|
+
client.account_info_as_dict.return_value = {
|
|
2015
|
+
"equity": 10_000.0,
|
|
2016
|
+
"margin": 4500.0,
|
|
2017
|
+
}
|
|
2018
|
+
mocker.patch(
|
|
2019
|
+
"mt5cli.trading.estimate_order_margin",
|
|
2020
|
+
side_effect=Mt5TradingError("bad tick"),
|
|
2021
|
+
)
|
|
2022
|
+
|
|
2023
|
+
with pytest.raises(Mt5TradingError, match="bad tick"):
|
|
2024
|
+
calculate_account_projected_margin_ratio(
|
|
2025
|
+
client,
|
|
2026
|
+
symbol="EURUSD",
|
|
2027
|
+
new_position_side="BUY",
|
|
2028
|
+
new_position_volume=0.1,
|
|
2029
|
+
)
|
|
2030
|
+
|
|
1894
2031
|
def test_symbol_group_margin_ratio_sums_group_exposure(
|
|
1895
2032
|
self,
|
|
1896
2033
|
mocker: MockerFixture,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|