feat: auto-clamp buy orders to fit within position limits instead of hard-rejecting

This commit is contained in:
Celes Renata
2026-04-28 14:20:44 +00:00
parent e360b66c3e
commit 226d799eb2
3 changed files with 123 additions and 0 deletions
+69
View File
@@ -12,6 +12,7 @@ Design: Section 4.8 - Risk Engine
"""
from __future__ import annotations
import math
import uuid
from datetime import datetime, timedelta, timezone
from enum import Enum
@@ -265,6 +266,74 @@ class ProposedOrder(BaseModel):
confidence: float = 0.0
# ---------------------------------------------------------------------------
# Order clamping — auto-scale to fit within position limits
# ---------------------------------------------------------------------------
def clamp_order_to_position_limits(
order: ProposedOrder,
config: PortfolioRiskConfig,
state: AccountRiskState,
) -> ProposedOrder:
"""Clamp a buy order's quantity/value to fit within position limits.
Instead of hard-rejecting orders that exceed max_position_pct or
max_position_value, this function computes the maximum allowed
order size and returns a new ProposedOrder scaled down to fit.
Sell orders are returned unchanged (they reduce exposure).
If the order already fits, it is returned unchanged.
If the clamped quantity rounds to zero, the order is returned with
quantity=0 and estimated_value=0 so the caller can reject it.
"""
if order.action == "sell" or order.quantity <= 0:
return order
limits = config.position_limits
existing_value = state.positions_by_symbol.get(order.ticker, 0.0)
# Compute per-share price from the order
price_per_share = (
order.estimated_value / order.quantity
if order.quantity > 0 and order.estimated_value > 0
else 0.0
)
if price_per_share <= 0:
return order # Can't clamp without a price; let risk checks handle it
# Compute the maximum additional value we can add to this position
max_allowed_value = limits.max_position_value - existing_value
# Also enforce max_position_pct if portfolio value is known
if state.portfolio_value > 0:
max_pct_value = (limits.max_position_pct * state.portfolio_value) - existing_value
max_allowed_value = min(max_allowed_value, max_pct_value)
# If already at or over the limit, clamp to zero
if max_allowed_value <= 0:
return order.model_copy(update={"quantity": 0.0, "estimated_value": 0.0})
# If the order already fits, return unchanged
if order.estimated_value <= max_allowed_value:
return order
# Clamp: compute the maximum whole shares that fit
clamped_shares = math.floor(max_allowed_value / price_per_share)
# Also respect max_shares_per_order
clamped_shares = min(clamped_shares, int(limits.max_shares_per_order))
if clamped_shares <= 0:
return order.model_copy(update={"quantity": 0.0, "estimated_value": 0.0})
clamped_value = clamped_shares * price_per_share
return order.model_copy(update={
"quantity": float(clamped_shares),
"estimated_value": clamped_value,
})
# ---------------------------------------------------------------------------
# Individual risk checks (Requirement 8.4)
# ---------------------------------------------------------------------------