удалить лимиты
This commit is contained in:
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from decimal import ROUND_DOWN, Decimal
|
||||
|
||||
from llm_trade_bot.config import RiskConfig
|
||||
from llm_trade_bot.models import (
|
||||
AccountSnapshot,
|
||||
Category,
|
||||
@@ -14,14 +13,11 @@ from llm_trade_bot.models import (
|
||||
)
|
||||
|
||||
|
||||
class RiskRejected(ValueError):
|
||||
"""Raised when a requested order violates a hard safety rule."""
|
||||
class OrderRejected(ValueError):
|
||||
"""Raised when an order cannot be represented by the selected exchange."""
|
||||
|
||||
|
||||
class RiskEngine:
|
||||
def __init__(self, config: RiskConfig):
|
||||
self._config = config
|
||||
|
||||
class OrderValidator:
|
||||
def validate_open(
|
||||
self,
|
||||
request: OpenTradeRequest,
|
||||
@@ -29,52 +25,27 @@ class RiskEngine:
|
||||
instrument: InstrumentRules,
|
||||
) -> ValidatedOrder:
|
||||
if account.equity <= 0:
|
||||
raise RiskRejected("Account equity must be positive")
|
||||
if account.active_ideas >= self._config.max_positions:
|
||||
raise RiskRejected("Maximum number of active ideas reached")
|
||||
if request.category is Category.SPOT and request.side is Side.SELL:
|
||||
raise RiskRejected("Opening spot shorts is not allowed")
|
||||
if request.category is Category.SPOT and request.order_type is OrderType.MARKET:
|
||||
raise RiskRejected("Spot entries must be Limit orders with attached TP/SL")
|
||||
raise OrderRejected("Account equity must be positive")
|
||||
if request.order_type is OrderType.LIMIT and request.limit_price is None:
|
||||
raise RiskRejected("Limit order requires limit_price")
|
||||
raise OrderRejected("Limit order requires limit_price")
|
||||
|
||||
entry = request.limit_price if request.order_type is OrderType.LIMIT else request.reference_price
|
||||
if entry is None or entry <= 0 or request.stop_loss <= 0:
|
||||
raise RiskRejected("Entry and stop-loss prices must be positive")
|
||||
raise OrderRejected("Entry and stop-loss prices must be positive")
|
||||
self._validate_protection(request, entry)
|
||||
|
||||
max_risk = Decimal(str(self._config.max_risk_per_trade_pct))
|
||||
if request.risk_pct <= 0 or request.risk_pct > max_risk:
|
||||
raise RiskRejected(f"risk_pct must be in (0, {max_risk}]")
|
||||
|
||||
configured_leverage = Decimal(str(self._config.max_leverage))
|
||||
allowed_leverage = min(configured_leverage, instrument.max_leverage)
|
||||
leverage = Decimal(1) if request.category is Category.SPOT else request.leverage
|
||||
if leverage < 1 or leverage > allowed_leverage:
|
||||
raise RiskRejected(f"Leverage must be between 1 and {allowed_leverage}")
|
||||
|
||||
risk_amount = account.equity * request.risk_pct / Decimal(100)
|
||||
per_unit_loss = abs(entry - request.stop_loss)
|
||||
if per_unit_loss <= 0:
|
||||
raise RiskRejected("Stop-loss must differ from entry")
|
||||
risk_quantity = risk_amount / per_unit_loss
|
||||
|
||||
exposure_limit = account.equity * Decimal(str(self._config.max_total_exposure_pct)) / Decimal(100)
|
||||
remaining_exposure = exposure_limit - account.gross_exposure
|
||||
if remaining_exposure <= 0:
|
||||
raise RiskRejected("Total exposure limit reached")
|
||||
exposure_quantity = remaining_exposure / entry
|
||||
if leverage < 1 or leverage > instrument.max_leverage:
|
||||
raise OrderRejected(f"Leverage must be between 1 and {instrument.max_leverage}")
|
||||
|
||||
buying_power = account.available_balance * leverage
|
||||
balance_quantity = buying_power / entry
|
||||
quantity = min(risk_quantity, exposure_quantity, balance_quantity)
|
||||
quantity = buying_power / entry
|
||||
quantity = _round_down(quantity, instrument.qty_step)
|
||||
notional = quantity * entry
|
||||
if quantity < instrument.min_qty:
|
||||
raise RiskRejected("Calculated quantity is below instrument minimum")
|
||||
raise OrderRejected("Calculated quantity is below instrument minimum")
|
||||
if notional < instrument.min_notional:
|
||||
raise RiskRejected("Calculated notional is below instrument minimum")
|
||||
raise OrderRejected("Calculated notional is below instrument minimum")
|
||||
|
||||
rounded_entry = _round_price(entry, instrument.tick_size)
|
||||
stop = _round_price(request.stop_loss, instrument.tick_size)
|
||||
@@ -96,7 +67,6 @@ class RiskEngine:
|
||||
stop,
|
||||
take_profit,
|
||||
trailing,
|
||||
request.risk_pct,
|
||||
leverage,
|
||||
request.rationale,
|
||||
)
|
||||
@@ -115,29 +85,29 @@ class RiskEngine:
|
||||
def _validate_protection(request: OpenTradeRequest, entry: Decimal) -> None:
|
||||
if request.side is Side.BUY:
|
||||
if request.stop_loss >= entry:
|
||||
raise RiskRejected("Buy stop-loss must be below entry")
|
||||
raise OrderRejected("Buy stop-loss must be below entry")
|
||||
if request.take_profit is not None and request.take_profit <= entry:
|
||||
raise RiskRejected("Buy take-profit must be above entry")
|
||||
raise OrderRejected("Buy take-profit must be above entry")
|
||||
else:
|
||||
if request.stop_loss <= entry:
|
||||
raise RiskRejected("Sell stop-loss must be above entry")
|
||||
raise OrderRejected("Sell stop-loss must be above entry")
|
||||
if request.take_profit is not None and request.take_profit >= entry:
|
||||
raise RiskRejected("Sell take-profit must be below entry")
|
||||
raise OrderRejected("Sell take-profit must be below entry")
|
||||
|
||||
if request.category is Category.SPOT:
|
||||
if request.take_profit is None:
|
||||
raise RiskRejected("Spot entry requires take-profit")
|
||||
raise OrderRejected("Spot entry requires take-profit")
|
||||
if request.trailing_distance is not None:
|
||||
raise RiskRejected("Trailing stop is not supported for Spot")
|
||||
raise OrderRejected("Trailing stop is not supported for Spot")
|
||||
elif request.take_profit is None and request.trailing_distance is None:
|
||||
raise RiskRejected("Linear entry requires take-profit or trailing stop")
|
||||
raise OrderRejected("Linear entry requires take-profit or trailing stop")
|
||||
if request.trailing_distance is not None and request.trailing_distance <= 0:
|
||||
raise RiskRejected("Trailing distance must be positive")
|
||||
raise OrderRejected("Trailing distance must be positive")
|
||||
|
||||
|
||||
def _round_down(value: Decimal, step: Decimal) -> Decimal:
|
||||
if step <= 0:
|
||||
raise RiskRejected("Instrument step must be positive")
|
||||
raise OrderRejected("Instrument step must be positive")
|
||||
return (value / step).to_integral_value(rounding=ROUND_DOWN) * step
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user