37 lines
1.4 KiB
Python
37 lines
1.4 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict
|
|
|
|
# Simple risk rules for MVP
|
|
MAX_POSITION_FRACTION = 0.6 # max 60% of equity in a single position
|
|
|
|
|
|
def _equity(sim_state: Dict[str, Any], price: float) -> float:
|
|
cash = float(sim_state.get("cash", 0.0))
|
|
pos = sim_state.get("position")
|
|
qty = float(pos["quantity"]) if pos else 0.0
|
|
return cash + qty * price
|
|
|
|
|
|
def validate_order(sim_state: Dict[str, Any], action: str, quantity: float, price: float) -> None:
|
|
action = str(action).upper()
|
|
if quantity <= 0 or price <= 0:
|
|
raise ValueError("Quantity and price must be positive")
|
|
|
|
if action == "BUY":
|
|
# Anti-stacking: only one symbol supported in MVP, allow averaging up to cap
|
|
pos = sim_state.get("position")
|
|
current_qty = float(pos["quantity"]) if pos else 0.0
|
|
new_qty = current_qty + float(quantity)
|
|
resulting_position_value = new_qty * float(price)
|
|
eq_now = _equity(sim_state, price)
|
|
if eq_now <= 0:
|
|
raise ValueError("Equity must be positive")
|
|
if resulting_position_value > MAX_POSITION_FRACTION * eq_now:
|
|
raise ValueError("Position exceeds max allowed exposure fraction")
|
|
elif action == "SELL":
|
|
pos = sim_state.get("position")
|
|
if not pos or float(quantity) > float(pos.get("quantity", 0.0)):
|
|
raise ValueError("Insufficient position to sell")
|
|
else:
|
|
raise ValueError("Unsupported action") |