fix: risk engine blocking sell orders on over-concentrated positions
Two bugs: (1) trading engine omitted estimated_value from sell order jobs, causing risk engine to compute 0 reduction; (2) risk engine applied position size limits to sells, trapping users in positions they couldn't exit. Sells now always pass position value/pct checks.
This commit is contained in:
+29
-12
@@ -302,16 +302,24 @@ def _check_max_position_size(
|
||||
new_total_value = max(existing_value - order.estimated_value, 0.0)
|
||||
else:
|
||||
new_total_value = existing_value + order.estimated_value
|
||||
|
||||
# Sell orders always pass position value check — they reduce exposure
|
||||
if order.action == "sell":
|
||||
value_result = RiskCheckResult.PASS
|
||||
value_verb = "within (sell reduces exposure)"
|
||||
elif new_total_value <= limits.max_position_value:
|
||||
value_result = RiskCheckResult.PASS
|
||||
value_verb = "within"
|
||||
else:
|
||||
value_result = RiskCheckResult.FAIL
|
||||
value_verb = "exceeds"
|
||||
|
||||
checks.append(RiskCheckDetail(
|
||||
check_name="max_position_value",
|
||||
result=(
|
||||
RiskCheckResult.PASS
|
||||
if new_total_value <= limits.max_position_value
|
||||
else RiskCheckResult.FAIL
|
||||
),
|
||||
result=value_result,
|
||||
message=(
|
||||
f"Position value {new_total_value:.2f} "
|
||||
f"{'within' if new_total_value <= limits.max_position_value else 'exceeds'} "
|
||||
f"{value_verb} "
|
||||
f"limit {limits.max_position_value:.2f}"
|
||||
),
|
||||
threshold=limits.max_position_value,
|
||||
@@ -323,16 +331,25 @@ def _check_max_position_size(
|
||||
position_pct = new_total_value / state.portfolio_value
|
||||
else:
|
||||
position_pct = 1.0 if new_total_value > 0 else 0.0
|
||||
|
||||
# Sell orders that reduce concentration should always pass — blocking a
|
||||
# sell on an over-concentrated position prevents the user from fixing it.
|
||||
if order.action == "sell":
|
||||
pct_result = RiskCheckResult.PASS
|
||||
pct_verb = "within (sell reduces exposure)"
|
||||
elif position_pct <= limits.max_position_pct:
|
||||
pct_result = RiskCheckResult.PASS
|
||||
pct_verb = "within"
|
||||
else:
|
||||
pct_result = RiskCheckResult.FAIL
|
||||
pct_verb = "exceeds"
|
||||
|
||||
checks.append(RiskCheckDetail(
|
||||
check_name="max_position_pct",
|
||||
result=(
|
||||
RiskCheckResult.PASS
|
||||
if position_pct <= limits.max_position_pct
|
||||
else RiskCheckResult.FAIL
|
||||
),
|
||||
result=pct_result,
|
||||
message=(
|
||||
f"Position {position_pct:.4f} of portfolio "
|
||||
f"{'within' if position_pct <= limits.max_position_pct else 'exceeds'} "
|
||||
f"{pct_verb} "
|
||||
f"limit {limits.max_position_pct:.4f}"
|
||||
),
|
||||
threshold=limits.max_position_pct,
|
||||
|
||||
@@ -728,6 +728,7 @@ class TradingEngine:
|
||||
"side": "sell",
|
||||
"quantity": sell_qty,
|
||||
"order_type": "market",
|
||||
"estimated_value": estimated_proceeds,
|
||||
"source": "trading_engine",
|
||||
}
|
||||
if self.redis is not None:
|
||||
@@ -1813,14 +1814,17 @@ class TradingEngine:
|
||||
self.portfolio_state.active_pool += current * qty
|
||||
|
||||
async def _submit_sell_order(
|
||||
self, ticker: str, quantity: int, reason: str
|
||||
self, ticker: str, quantity: int, reason: str,
|
||||
estimated_value: float = 0.0,
|
||||
) -> None:
|
||||
"""Push a sell order to the broker queue via Redis."""
|
||||
order_job = {
|
||||
"ticker": ticker,
|
||||
"action": "sell",
|
||||
"side": "sell",
|
||||
"quantity": quantity,
|
||||
"order_type": "market",
|
||||
"estimated_value": estimated_value,
|
||||
"source": "trading_engine",
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user