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.
- {mt5cli-0.9.0 → mt5cli-0.9.1}/PKG-INFO +1 -1
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/trading.py +36 -8
- {mt5cli-0.9.0 → mt5cli-0.9.1}/pyproject.toml +1 -2
- {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_trading.py +62 -1
- {mt5cli-0.9.0 → mt5cli-0.9.1}/uv.lock +1 -1
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/local-qa/SKILL.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/local-qa/scripts/qa.sh +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/mt5cli/SKILL.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.agents/skills/pr-feedback-triage/SKILL.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.claude/agents/codex.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.claude/settings.json +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/FUNDING.yml +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/dependabot.yml +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/renovate.json +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/workflows/ci.yml +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/workflows/claude.yml +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.github/workflows/release.yml +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/.gitignore +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/AGENTS.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/CLAUDE.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/LICENSE +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/README.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/cli.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/client.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/converters.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/exceptions.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/history.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/index.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/public-contract.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/schemas.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/sdk.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/storage.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/trading.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/api/utils.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/docs/index.md +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mkdocs.yml +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/__init__.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/__main__.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/cli.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/client.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/contract.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/converters.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/exceptions.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/history.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/retry.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/schemas.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/sdk.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/storage.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/mt5cli/utils.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/__init__.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/conftest.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_cli.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_contracts.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_history.py +0 -0
- {mt5cli-0.9.0 → mt5cli-0.9.1}/tests/test_sdk.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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.
|
|
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
|
-
|
|
1465
|
+
assert 0.1 <= volume <= 1.0
|
|
1405
1466
|
_assert_close(price, 100.0)
|
|
1406
1467
|
return 10.0
|
|
1407
1468
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|