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
+49
View File
@@ -52,6 +52,7 @@ from services.risk.engine import (
AccountRiskState,
PortfolioRiskConfig,
ProposedOrder,
clamp_order_to_position_limits,
evaluate_order,
)
from services.shared.audit import (
@@ -66,6 +67,7 @@ from services.shared.config import load_config
from services.shared.db import get_pg_pool, get_redis
from services.shared.logging import setup_logging
from services.shared.metrics import (
ORDERS_CLAMPED,
ORDERS_DUPLICATES_PREVENTED,
ORDERS_FILLED,
ORDERS_REJECTED,
@@ -288,6 +290,25 @@ async def load_risk_config(pool: asyncpg.Pool) -> PortfolioRiskConfig:
return PortfolioRiskConfig()
async def _estimate_share_price(
adapter: AlpacaBrokerAdapter,
ticker: str,
) -> float:
"""Estimate the current per-share price for a ticker.
Checks existing Alpaca positions first (free, no API call).
Returns 0.0 if no price can be determined.
"""
try:
positions = await adapter.get_positions()
for pos in positions:
if pos.ticker == ticker and pos.current_price > 0:
return pos.current_price
except Exception as e:
logger.debug("Could not fetch positions for price estimate: %s", e)
return 0.0
async def load_account_risk_state(
pool: asyncpg.Pool,
adapter: AlpacaBrokerAdapter,
@@ -534,6 +555,34 @@ async def process_order_job(
risk_config = await load_risk_config(pool)
risk_state = await load_account_risk_state(pool, adapter, account_uuid)
proposed = build_proposed_order(job)
# If estimated_value is missing, derive it from Alpaca positions or
# a fresh quote so that position-limit clamping can work.
if proposed.estimated_value <= 0 and proposed.quantity > 0:
price_per_share = await _estimate_share_price(adapter, proposed.ticker)
if price_per_share > 0:
proposed = proposed.model_copy(update={
"estimated_value": proposed.quantity * price_per_share,
})
job["estimated_value"] = proposed.estimated_value
# Auto-clamp buy orders to fit within position limits instead of
# hard-rejecting. If the clamped quantity is zero the normal risk
# evaluation will still reject the order with a clear reason.
original_qty = proposed.quantity
proposed = clamp_order_to_position_limits(proposed, risk_config, risk_state)
if proposed.quantity != original_qty:
logger.info(
"Order for %s clamped from %.0f to %.0f shares "
"(value %.2f%.2f) to fit position limits",
ticker, original_qty, proposed.quantity,
float(job.get("estimated_value", 0)), proposed.estimated_value,
)
ORDERS_CLAMPED.inc()
# Update the job dict so build_order_request picks up the clamped qty
job["quantity"] = proposed.quantity
job["estimated_value"] = proposed.estimated_value
evaluation = evaluate_order(proposed, risk_config, risk_state)
risk_eval_dict = {