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.
Files changed (56) hide show
  1. {mt5cli-0.9.3 → mt5cli-0.9.4}/PKG-INFO +1 -1
  2. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/public-contract.md +19 -18
  3. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/__init__.py +2 -0
  4. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/contract.py +1 -0
  5. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/trading.py +54 -8
  6. {mt5cli-0.9.3 → mt5cli-0.9.4}/pyproject.toml +1 -1
  7. {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_contracts.py +2 -0
  8. {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_trading.py +137 -0
  9. {mt5cli-0.9.3 → mt5cli-0.9.4}/uv.lock +1 -1
  10. {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/local-qa/SKILL.md +0 -0
  11. {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  12. {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/mt5cli/SKILL.md +0 -0
  13. {mt5cli-0.9.3 → mt5cli-0.9.4}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
  14. {mt5cli-0.9.3 → mt5cli-0.9.4}/.claude/agents/codex.md +0 -0
  15. {mt5cli-0.9.3 → mt5cli-0.9.4}/.claude/settings.json +0 -0
  16. {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/FUNDING.yml +0 -0
  17. {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/dependabot.yml +0 -0
  18. {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/renovate.json +0 -0
  19. {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/workflows/ci.yml +0 -0
  20. {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/workflows/claude.yml +0 -0
  21. {mt5cli-0.9.3 → mt5cli-0.9.4}/.github/workflows/release.yml +0 -0
  22. {mt5cli-0.9.3 → mt5cli-0.9.4}/.gitignore +0 -0
  23. {mt5cli-0.9.3 → mt5cli-0.9.4}/AGENTS.md +0 -0
  24. {mt5cli-0.9.3 → mt5cli-0.9.4}/CLAUDE.md +0 -0
  25. {mt5cli-0.9.3 → mt5cli-0.9.4}/LICENSE +0 -0
  26. {mt5cli-0.9.3 → mt5cli-0.9.4}/README.md +0 -0
  27. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/cli.md +0 -0
  28. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/client.md +0 -0
  29. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/converters.md +0 -0
  30. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/exceptions.md +0 -0
  31. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/history.md +0 -0
  32. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/index.md +0 -0
  33. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/schemas.md +0 -0
  34. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/sdk.md +0 -0
  35. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/storage.md +0 -0
  36. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/trading.md +0 -0
  37. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/api/utils.md +0 -0
  38. {mt5cli-0.9.3 → mt5cli-0.9.4}/docs/index.md +0 -0
  39. {mt5cli-0.9.3 → mt5cli-0.9.4}/mkdocs.yml +0 -0
  40. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/__main__.py +0 -0
  41. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/cli.py +0 -0
  42. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/client.py +0 -0
  43. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/converters.py +0 -0
  44. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/exceptions.py +0 -0
  45. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/history.py +0 -0
  46. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/retry.py +0 -0
  47. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/schemas.py +0 -0
  48. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/sdk.py +0 -0
  49. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/storage.py +0 -0
  50. {mt5cli-0.9.3 → mt5cli-0.9.4}/mt5cli/utils.py +0 -0
  51. {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/__init__.py +0 -0
  52. {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/conftest.py +0 -0
  53. {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_cli.py +0 -0
  54. {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_history.py +0 -0
  55. {mt5cli-0.9.3 → mt5cli-0.9.4}/tests/test_sdk.py +0 -0
  56. {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
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
- | `calculate_symbol_group_margin_ratio` | Estimated symbol-group margin/equity with optional exposure |
105
- | `determine_order_limits` | SL/TP price levels from ratios |
106
- | `calculate_trailing_stop_updates` | Per-ticket generic trailing stop-loss update plan |
107
- | `ensure_symbol_selected` | Select/verify Market Watch visibility |
108
- | `place_market_order`, `close_open_positions`, `update_sltp_for_open_positions`, `update_trailing_stop_loss_for_open_positions` | Order execution helpers (`dry_run` supported) |
109
- | `MarginVolume`, `OrderLimits`, `OrderExecutionResult` | Typed return contracts for order helpers |
110
- | `OrderSide`, `OrderFillingMode`, `OrderTimeMode`, `PositionSide`, `ExecutionStatus` | Typed enums for order helpers |
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
- try:
838
- equity = float(account.get("equity") or 0.0)
839
- except (TypeError, ValueError) as exc:
840
- msg = "Account equity must be positive to calculate margin ratio."
841
- raise Mt5TradingError(msg) from exc
842
- if equity <= 0 or not isfinite(equity):
843
- msg = "Account equity must be positive to calculate margin ratio."
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
- return equity
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"
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,
@@ -487,7 +487,7 @@ wheels = [
487
487
 
488
488
  [[package]]
489
489
  name = "mt5cli"
490
- version = "0.9.3"
490
+ version = "0.9.4"
491
491
  source = { editable = "." }
492
492
  dependencies = [
493
493
  { name = "click" },
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