удалить лимиты

This commit is contained in:
Azimkin
2026-09-22 11:43:32 +02:00
parent 316eefe58a
commit 599dbb0455
13 changed files with 59 additions and 242 deletions

View File

@@ -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