Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b878a61c07 |
+36
-8
@@ -830,7 +830,9 @@ def calculate_volume_by_margin(
|
||||
"""Calculate max normalized volume affordable for one side.
|
||||
|
||||
Returns:
|
||||
Affordable volume rounded down to symbol volume constraints.
|
||||
Largest stepped volume whose actual margin (from ``order_calc_margin``)
|
||||
fits within ``available_margin``, rounded down to symbol volume
|
||||
constraints; ``0.0`` when no affordable step exists.
|
||||
|
||||
Raises:
|
||||
Mt5TradingError: If symbol volume constraints or tick data are invalid.
|
||||
@@ -845,8 +847,7 @@ def calculate_volume_by_margin(
|
||||
msg = f"Invalid volume constraints for {symbol!r}."
|
||||
raise Mt5TradingError(msg)
|
||||
side = _normalize_order_side(order_side)
|
||||
tick = get_tick_snapshot(client, symbol)
|
||||
price = tick["ask"] if side == "BUY" else tick["bid"]
|
||||
price = get_tick_snapshot(client, symbol)["ask" if side == "BUY" else "bid"]
|
||||
if not isinstance(price, int | float) or price <= 0:
|
||||
msg = f"Tick price is unavailable for {symbol!r}."
|
||||
raise Mt5TradingError(msg)
|
||||
@@ -856,11 +857,38 @@ def calculate_volume_by_margin(
|
||||
min_margin = float(client.order_calc_margin(order_type, symbol, volume_min, price))
|
||||
if min_margin <= 0 or min_margin > available_margin:
|
||||
return 0.0
|
||||
raw_volume = available_margin / min_margin * volume_min
|
||||
capped = min(raw_volume, volume_max) if volume_max > 0 else raw_volume
|
||||
steps = floor(((capped - volume_min) / volume_step) + 1e-12)
|
||||
normalized = volume_min + max(0, steps) * volume_step
|
||||
return round(normalized, 10) if normalized >= volume_min else 0.0
|
||||
lo = 0
|
||||
hi = int(
|
||||
max(
|
||||
0,
|
||||
floor(
|
||||
(
|
||||
(
|
||||
min(available_margin / min_margin * volume_min, volume_max)
|
||||
if volume_max > 0
|
||||
else available_margin / min_margin * volume_min
|
||||
)
|
||||
- volume_min
|
||||
)
|
||||
/ volume_step
|
||||
+ 1e-12
|
||||
),
|
||||
)
|
||||
)
|
||||
best = -1
|
||||
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
normalized = round(volume_min + mid * volume_step, 10)
|
||||
actual = float(client.order_calc_margin(order_type, symbol, normalized, price))
|
||||
|
||||
if actual > 0 and actual <= available_margin:
|
||||
best = mid
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
|
||||
return round(volume_min + best * volume_step, 10) if best >= 0 else 0.0
|
||||
|
||||
|
||||
def determine_order_limits(
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mt5cli"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
description = "Generic MT5 data and execution infrastructure for Python applications"
|
||||
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
||||
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
|
||||
@@ -124,7 +124,6 @@ ignore = [
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"mt5cli/history.py" = ["TC003"]
|
||||
"tests/**/*.py" = [
|
||||
"DOC201", # Missing return documentation
|
||||
"DOC501", # Raised exception missing from docstring
|
||||
|
||||
+62
-1
@@ -1375,6 +1375,67 @@ class TestVolumeAndExecution:
|
||||
with pytest.raises(Mt5TradingError):
|
||||
calculate_volume_by_margin(client, "EURUSD", 100.0, "SELL")
|
||||
|
||||
def test_calculate_volume_by_margin_steps_down_when_margin_exceeds_budget(
|
||||
self,
|
||||
) -> None:
|
||||
"""Tiered margin: binary search returns the largest affordable step."""
|
||||
client = _mock_trade_client()
|
||||
client.symbol_info_as_dict.return_value = {
|
||||
"volume_min": 0.1,
|
||||
"volume_max": 1.0,
|
||||
"volume_step": 0.1,
|
||||
}
|
||||
client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
|
||||
# min_margin (0.1): 25.0 -> hi=4.
|
||||
# Search: mid=2 (0.3)->75<=130, mid=3 (0.4)->100<=130, mid=4 (0.5)->150>130.
|
||||
client.order_calc_margin.side_effect = [25.0, 75.0, 100.0, 150.0]
|
||||
|
||||
result = calculate_volume_by_margin(client, "EURUSD", 130.0, "BUY")
|
||||
|
||||
_assert_close(result, 0.4)
|
||||
|
||||
def test_calculate_volume_by_margin_returns_zero_when_all_steps_unaffordable(
|
||||
self,
|
||||
) -> None:
|
||||
"""If all binary-search probes exceed budget, returns zero."""
|
||||
client = _mock_trade_client()
|
||||
client.symbol_info_as_dict.return_value = {
|
||||
"volume_min": 0.1,
|
||||
"volume_max": 0.5,
|
||||
"volume_step": 0.1,
|
||||
}
|
||||
client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
|
||||
# min_margin (0.1): 10.0 -> hi=4.
|
||||
# Search: mid=2 (0.3)->150>130->hi=1, mid=0 (0.1)->150>130->hi=-1.
|
||||
client.order_calc_margin.side_effect = [10.0, 150.0, 150.0]
|
||||
|
||||
result = calculate_volume_by_margin(client, "EURUSD", 130.0, "BUY")
|
||||
|
||||
_assert_close(result, 0.0)
|
||||
|
||||
def test_calculate_volume_by_margin_binary_search_is_bounded(self) -> None:
|
||||
"""Binary search finds the largest affordable volume in O(log n) MT5 calls."""
|
||||
client = _mock_trade_client()
|
||||
client.symbol_info_as_dict.return_value = {
|
||||
"volume_min": 0.01,
|
||||
"volume_max": 1000.0,
|
||||
"volume_step": 0.01,
|
||||
}
|
||||
client.symbol_info_tick_as_dict.return_value = {"ask": 100.0, "bid": 99.0}
|
||||
# Steps 0-50000 cost 0.001 (affordable); steps 50001+ cost 2000.0 (not).
|
||||
# Total range: 99999 steps. Linear scan: ~50000 calls; binary search: ~17.
|
||||
affordable_step = 50000
|
||||
|
||||
def _margin(_ot: int, _sym: str, volume: float, _px: float) -> float:
|
||||
step = round((volume - 0.01) / 0.01)
|
||||
return 0.001 if step <= affordable_step else 2000.0
|
||||
|
||||
client.order_calc_margin.side_effect = _margin
|
||||
result = calculate_volume_by_margin(client, "EURUSD", 200.0, "BUY")
|
||||
|
||||
_assert_close(result, 500.01) # 0.01 + 50000 * 0.01
|
||||
assert client.order_calc_margin.call_count <= 25
|
||||
|
||||
def test_calculate_margin_and_volume_without_native_helper(self) -> None:
|
||||
"""Test margin helper uses module volume calculation when needed."""
|
||||
|
||||
@@ -1401,7 +1462,7 @@ class TestVolumeAndExecution:
|
||||
) -> float:
|
||||
assert order_type in {10, 11}
|
||||
assert symbol == "EURUSD"
|
||||
_assert_close(volume, 0.1)
|
||||
assert 0.1 <= volume <= 1.0
|
||||
_assert_close(price, 100.0)
|
||||
return 10.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user