mt5cli 0.9.0__tar.gz → 0.9.1__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.0 → mt5cli-0.9.1}/PKG-INFO +1 -1
  2. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/trading.py +36 -8
  3. {mt5cli-0.9.0 → mt5cli-0.9.1}/pyproject.toml +1 -2
  4. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_trading.py +62 -1
  5. {mt5cli-0.9.0 → mt5cli-0.9.1}/uv.lock +1 -1
  6. {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/local-qa/SKILL.md +0 -0
  7. {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/local-qa/scripts/qa.sh +0 -0
  8. {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/mt5cli/SKILL.md +0 -0
  9. {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
  10. {mt5cli-0.9.0 → mt5cli-0.9.1}/.claude/agents/codex.md +0 -0
  11. {mt5cli-0.9.0 → mt5cli-0.9.1}/.claude/settings.json +0 -0
  12. {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/FUNDING.yml +0 -0
  13. {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/dependabot.yml +0 -0
  14. {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/renovate.json +0 -0
  15. {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/workflows/ci.yml +0 -0
  16. {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/workflows/claude.yml +0 -0
  17. {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/workflows/release.yml +0 -0
  18. {mt5cli-0.9.0 → mt5cli-0.9.1}/.gitignore +0 -0
  19. {mt5cli-0.9.0 → mt5cli-0.9.1}/AGENTS.md +0 -0
  20. {mt5cli-0.9.0 → mt5cli-0.9.1}/CLAUDE.md +0 -0
  21. {mt5cli-0.9.0 → mt5cli-0.9.1}/LICENSE +0 -0
  22. {mt5cli-0.9.0 → mt5cli-0.9.1}/README.md +0 -0
  23. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/cli.md +0 -0
  24. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/client.md +0 -0
  25. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/converters.md +0 -0
  26. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/exceptions.md +0 -0
  27. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/history.md +0 -0
  28. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/index.md +0 -0
  29. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/public-contract.md +0 -0
  30. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/schemas.md +0 -0
  31. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/sdk.md +0 -0
  32. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/storage.md +0 -0
  33. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/trading.md +0 -0
  34. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/utils.md +0 -0
  35. {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/index.md +0 -0
  36. {mt5cli-0.9.0 → mt5cli-0.9.1}/mkdocs.yml +0 -0
  37. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/__init__.py +0 -0
  38. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/__main__.py +0 -0
  39. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/cli.py +0 -0
  40. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/client.py +0 -0
  41. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/contract.py +0 -0
  42. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/converters.py +0 -0
  43. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/exceptions.py +0 -0
  44. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/history.py +0 -0
  45. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/retry.py +0 -0
  46. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/schemas.py +0 -0
  47. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/sdk.py +0 -0
  48. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/storage.py +0 -0
  49. {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/utils.py +0 -0
  50. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/__init__.py +0 -0
  51. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/conftest.py +0 -0
  52. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_cli.py +0 -0
  53. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_contracts.py +0 -0
  54. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_history.py +0 -0
  55. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_sdk.py +0 -0
  56. {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mt5cli
3
- Version: 0.9.0
3
+ Version: 0.9.1
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>
@@ -830,7 +830,9 @@ def calculate_volume_by_margin(
830
830
  """Calculate max normalized volume affordable for one side.
831
831
 
832
832
  Returns:
833
- Affordable volume rounded down to symbol volume constraints.
833
+ Largest stepped volume whose actual margin (from ``order_calc_margin``)
834
+ fits within ``available_margin``, rounded down to symbol volume
835
+ constraints; ``0.0`` when no affordable step exists.
834
836
 
835
837
  Raises:
836
838
  Mt5TradingError: If symbol volume constraints or tick data are invalid.
@@ -845,8 +847,7 @@ def calculate_volume_by_margin(
845
847
  msg = f"Invalid volume constraints for {symbol!r}."
846
848
  raise Mt5TradingError(msg)
847
849
  side = _normalize_order_side(order_side)
848
- tick = get_tick_snapshot(client, symbol)
849
- price = tick["ask"] if side == "BUY" else tick["bid"]
850
+ price = get_tick_snapshot(client, symbol)["ask" if side == "BUY" else "bid"]
850
851
  if not isinstance(price, int | float) or price <= 0:
851
852
  msg = f"Tick price is unavailable for {symbol!r}."
852
853
  raise Mt5TradingError(msg)
@@ -856,11 +857,38 @@ def calculate_volume_by_margin(
856
857
  min_margin = float(client.order_calc_margin(order_type, symbol, volume_min, price))
857
858
  if min_margin <= 0 or min_margin > available_margin:
858
859
  return 0.0
859
- raw_volume = available_margin / min_margin * volume_min
860
- capped = min(raw_volume, volume_max) if volume_max > 0 else raw_volume
861
- steps = floor(((capped - volume_min) / volume_step) + 1e-12)
862
- normalized = volume_min + max(0, steps) * volume_step
863
- return round(normalized, 10) if normalized >= volume_min else 0.0
860
+ lo = 0
861
+ hi = int(
862
+ max(
863
+ 0,
864
+ floor(
865
+ (
866
+ (
867
+ min(available_margin / min_margin * volume_min, volume_max)
868
+ if volume_max > 0
869
+ else available_margin / min_margin * volume_min
870
+ )
871
+ - volume_min
872
+ )
873
+ / volume_step
874
+ + 1e-12
875
+ ),
876
+ )
877
+ )
878
+ best = -1
879
+
880
+ while lo <= hi:
881
+ mid = (lo + hi) // 2
882
+ normalized = round(volume_min + mid * volume_step, 10)
883
+ actual = float(client.order_calc_margin(order_type, symbol, normalized, price))
884
+
885
+ if actual > 0 and actual <= available_margin:
886
+ best = mid
887
+ lo = mid + 1
888
+ else:
889
+ hi = mid - 1
890
+
891
+ return round(volume_min + best * volume_step, 10) if best >= 0 else 0.0
864
892
 
865
893
 
866
894
  def determine_order_limits(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mt5cli"
3
- version = "0.9.0"
3
+ version = "0.9.1"
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"}]
@@ -124,7 +124,6 @@ ignore = [
124
124
  ]
125
125
 
126
126
  [tool.ruff.lint.per-file-ignores]
127
- "mt5cli/history.py" = ["TC003"]
128
127
  "tests/**/*.py" = [
129
128
  "DOC201", # Missing return documentation
130
129
  "DOC501", # Raised exception missing from docstring
@@ -1375,6 +1375,67 @@ class TestVolumeAndExecution:
1375
1375
  with pytest.raises(Mt5TradingError):
1376
1376
  calculate_volume_by_margin(client, "EURUSD", 100.0, "SELL")
1377
1377
 
1378
+ def test_calculate_volume_by_margin_steps_down_when_margin_exceeds_budget(
1379
+ self,
1380
+ ) -> None:
1381
+ """Tiered margin: binary search returns the largest affordable step."""
1382
+ client = _mock_trade_client()
1383
+ client.symbol_info_as_dict.return_value = {
1384
+ "volume_min": 0.1,
1385
+ "volume_max": 1.0,
1386
+ "volume_step": 0.1,
1387
+ }
1388
+ client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
1389
+ # min_margin (0.1): 25.0 -> hi=4.
1390
+ # Search: mid=2 (0.3)->75<=130, mid=3 (0.4)->100<=130, mid=4 (0.5)->150>130.
1391
+ client.order_calc_margin.side_effect = [25.0, 75.0, 100.0, 150.0]
1392
+
1393
+ result = calculate_volume_by_margin(client, "EURUSD", 130.0, "BUY")
1394
+
1395
+ _assert_close(result, 0.4)
1396
+
1397
+ def test_calculate_volume_by_margin_returns_zero_when_all_steps_unaffordable(
1398
+ self,
1399
+ ) -> None:
1400
+ """If all binary-search probes exceed budget, returns zero."""
1401
+ client = _mock_trade_client()
1402
+ client.symbol_info_as_dict.return_value = {
1403
+ "volume_min": 0.1,
1404
+ "volume_max": 0.5,
1405
+ "volume_step": 0.1,
1406
+ }
1407
+ client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
1408
+ # min_margin (0.1): 10.0 -> hi=4.
1409
+ # Search: mid=2 (0.3)->150>130->hi=1, mid=0 (0.1)->150>130->hi=-1.
1410
+ client.order_calc_margin.side_effect = [10.0, 150.0, 150.0]
1411
+
1412
+ result = calculate_volume_by_margin(client, "EURUSD", 130.0, "BUY")
1413
+
1414
+ _assert_close(result, 0.0)
1415
+
1416
+ def test_calculate_volume_by_margin_binary_search_is_bounded(self) -> None:
1417
+ """Binary search finds the largest affordable volume in O(log n) MT5 calls."""
1418
+ client = _mock_trade_client()
1419
+ client.symbol_info_as_dict.return_value = {
1420
+ "volume_min": 0.01,
1421
+ "volume_max": 1000.0,
1422
+ "volume_step": 0.01,
1423
+ }
1424
+ client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
1425
+ # Steps 0-50000 cost 0.001 (affordable); steps 50001+ cost 2000.0 (not).
1426
+ # Total range: 99999 steps. Linear scan: ~50000 calls; binary search: ~17.
1427
+ affordable_step = 50000
1428
+
1429
+ def _margin(_ot: int, _sym: str, volume: float, _px: float) -> float:
1430
+ step = round((volume - 0.01) / 0.01)
1431
+ return 0.001 if step <= affordable_step else 2000.0
1432
+
1433
+ client.order_calc_margin.side_effect = _margin
1434
+ result = calculate_volume_by_margin(client, "EURUSD", 200.0, "BUY")
1435
+
1436
+ _assert_close(result, 500.01) # 0.01 + 50000 * 0.01
1437
+ assert client.order_calc_margin.call_count <= 25
1438
+
1378
1439
  def test_calculate_margin_and_volume_without_native_helper(self) -> None:
1379
1440
  """Test margin helper uses module volume calculation when needed."""
1380
1441
 
@@ -1401,7 +1462,7 @@ class TestVolumeAndExecution:
1401
1462
  ) -> float:
1402
1463
  assert order_type in {10, 11}
1403
1464
  assert symbol == "EURUSD"
1404
- _assert_close(volume, 0.1)
1465
+ assert 0.1 <= volume <= 1.0
1405
1466
  _assert_close(price, 100.0)
1406
1467
  return 10.0
1407
1468
 
@@ -487,7 +487,7 @@ wheels = [
487
487
 
488
488
  [[package]]
489
489
  name = "mt5cli"
490
- version = "0.9.0"
490
+ version = "0.9.1"
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
File without changes
File without changes
File without changes