fix(risk): allow incremental sizing with other positions

This commit is contained in:
Emerson Gomes
2026-07-16 18:28:36 -05:00
parent 12001fb6bc
commit 35c4bcc30b
3 changed files with 45 additions and 15 deletions
+6
View File
@@ -40,6 +40,12 @@ mtdata-cli trade_risk_analyze --symbol EURUSD --direction long \
| `stop_loss` | — | Proposed stop (alias `sl`). Required to compute risk-based volume. |
| `take_profit` | — | Optional target (alias `tp`) for reward/risk context. |
New-trade sizing uses an incremental candidate-risk policy: `desired_risk_pct`
limits the proposed trade's stop risk as a percentage of account equity, and
account-wide margin stress remains a hard safety gate. Existing positions on other
symbols do not prevent sizing, but the returned `sizing_risk_policy` states that the
suggestion is not an aggregate portfolio stop-risk cap.
### Kelly sizing
Set `sizing_method=kelly` and supply edge statistics:
+23 -13
View File
@@ -3535,9 +3535,8 @@ def run_trade_risk_analyze( # noqa: C901
"contract_price_product": "contract_size_times_price",
},
}
portfolio_sizing_blocked = False
other_positions_count: Optional[int] = None
if request.symbol:
other_positions_count = None
if portfolio_positions_total is not None:
other_positions_count = max(
0,
@@ -3564,7 +3563,6 @@ def run_trade_risk_analyze( # noqa: C901
"partial" if other_positions_count else "symbol_scope"
)
if other_positions_count:
portfolio_sizing_blocked = True
scoped_risk["overall_risk_status"] = "partial"
scoped_risk["quantified_risk_level"] = "unknown"
result["scope_warning"] = (
@@ -3576,6 +3574,21 @@ def run_trade_risk_analyze( # noqa: C901
f"{int(other_positions_count)} open position(s) exist on other symbols."
)
)
result["sizing_risk_policy"] = {
"mode": "incremental_candidate_risk",
"risk_target_basis": "percent_of_account_equity",
"candidate_symbol": str(request.symbol),
"account_margin_context_included": True,
"existing_portfolio_stop_risk_included": not bool(
other_positions_count
),
"portfolio_positions": int(portfolio_positions_total or 0),
"other_positions": int(other_positions_count or 0),
"note": (
"Suggested volume limits this candidate trade's stop risk; "
"it does not cap aggregate portfolio stop risk."
),
}
else:
result["risk_visibility"] = "portfolio"
result["scope"] = {
@@ -3784,15 +3797,8 @@ def run_trade_risk_analyze( # noqa: C901
)
)
)
if sizing_ready and (
portfolio_sizing_blocked
or margin_stress["status"] == "critical"
):
block_reason = (
"Symbol scope hides open positions on other symbols."
if portfolio_sizing_blocked
else "Account margin stress is critical."
)
if sizing_ready and margin_stress["status"] == "critical":
block_reason = "Account margin stress is critical."
result["position_sizing_error"] = _build_position_sizing_error(
code="portfolio_safety_block",
reason=block_reason,
@@ -3801,7 +3807,6 @@ def run_trade_risk_analyze( # noqa: C901
"before sizing a new trade."
),
details={
"risk_visibility": result.get("risk_visibility"),
"margin_stress": margin_stress,
},
)
@@ -4303,6 +4308,11 @@ def run_trade_risk_analyze( # noqa: C901
**({"margin_impact": margin_impact} if margin_impact else {}),
"sizing_notes": sizing_notes,
}
if other_positions_count:
result["position_sizing"]["sizing_notes"].append(
"Other-symbol positions are present; this is incremental "
"candidate sizing, not an aggregate portfolio risk cap."
)
if strict_risk_blocked:
result["position_sizing"].update(
{
@@ -639,8 +639,22 @@ def test_trade_risk_analyze_reports_symbol_scope_when_other_positions_exist() ->
assert out["scoped_risk"]["positions_count"] == 0
assert out["scoped_risk"]["overall_risk_status"] == "partial"
assert out["scoped_risk"]["quantified_risk_level"] == "unknown"
assert out["position_sizing_error"]["code"] == "portfolio_safety_block"
assert "position_sizing" not in out
assert out["position_sizing"]["suggested_volume"] == 1.0
assert out["position_sizing"]["risk_compliance"] == "within_requested_risk"
assert out["sizing_risk_policy"] == {
"mode": "incremental_candidate_risk",
"risk_target_basis": "percent_of_account_equity",
"candidate_symbol": "EURUSD",
"account_margin_context_included": True,
"existing_portfolio_stop_risk_included": False,
"portfolio_positions": 2,
"other_positions": 2,
"note": (
"Suggested volume limits this candidate trade's stop risk; it does not "
"cap aggregate portfolio stop risk."
),
}
assert "incremental candidate sizing" in out["position_sizing"]["sizing_notes"][-1]
def test_trade_risk_analyze_blocks_min_volume_risk_overshoot_by_default() -> None: