07.4.3.13 - Risk-based Sizing and Margin Protection
This commit is contained in:
@@ -64,7 +64,20 @@ class ExecutionEngine:
|
||||
return ExecutionDecision("NONE", False, f"Не удалось получить цену для paper execution: {exc}")
|
||||
|
||||
now = self._now_time()
|
||||
size = self._calculate_position_size(state)
|
||||
size = self._calculate_position_size(state, entry_price=entry_price)
|
||||
|
||||
if size <= 0:
|
||||
return ExecutionDecision(
|
||||
"NONE",
|
||||
False,
|
||||
"Позиция не открыта: невозможно рассчитать size без Stop Loss.",
|
||||
)
|
||||
|
||||
size = self._adjust_size_by_margin_limit(
|
||||
state=state,
|
||||
entry_price=entry_price,
|
||||
size=size,
|
||||
)
|
||||
|
||||
type(self)._position = PositionState(
|
||||
side=side,
|
||||
@@ -125,7 +138,20 @@ class ExecutionEngine:
|
||||
|
||||
now = self._now_time()
|
||||
pnl = self._calculate_pnl(flip_price)
|
||||
new_size = self._calculate_position_size(state)
|
||||
new_size = self._calculate_position_size(state, entry_price=flip_price)
|
||||
|
||||
if new_size <= 0:
|
||||
return ExecutionDecision(
|
||||
"NONE",
|
||||
False,
|
||||
"Flip отменён: невозможно рассчитать size без Stop Loss.",
|
||||
)
|
||||
|
||||
new_size = self._adjust_size_by_margin_limit(
|
||||
state=state,
|
||||
entry_price=flip_price,
|
||||
size=new_size,
|
||||
)
|
||||
|
||||
old_side = position.side
|
||||
old_entry_price = position.entry_price
|
||||
@@ -388,11 +414,74 @@ class ExecutionEngine:
|
||||
|
||||
self._sync_state_from_position(state)
|
||||
|
||||
def _calculate_position_size(self, state: AutoTradeState) -> float:
|
||||
risk_percent = state.risk_percent or 0.0
|
||||
leverage = state.leverage or 1.0
|
||||
return round((risk_percent * leverage) / 100, 8)
|
||||
def _calculate_position_size(
|
||||
self,
|
||||
state: AutoTradeState,
|
||||
*,
|
||||
entry_price: float | None = None,
|
||||
) -> float:
|
||||
if state.risk_percent is None or state.risk_percent <= 0:
|
||||
return 0.0
|
||||
|
||||
if state.stop_loss_percent is None or state.stop_loss_percent <= 0:
|
||||
return 0.0
|
||||
|
||||
price = entry_price
|
||||
|
||||
if price is None:
|
||||
try:
|
||||
ticker = ExchangeService().get_price(state.symbol)
|
||||
price = ticker.price
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
if price <= 0:
|
||||
return 0.0
|
||||
|
||||
balance_usd = 1000.0
|
||||
target_risk_usd = balance_usd * (state.risk_percent / 100)
|
||||
stop_loss_distance_usd = price * (state.stop_loss_percent / 100)
|
||||
|
||||
if stop_loss_distance_usd <= 0:
|
||||
return 0.0
|
||||
|
||||
size = target_risk_usd / stop_loss_distance_usd
|
||||
|
||||
return round(size, 8)
|
||||
|
||||
def _adjust_size_by_margin_limit(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
entry_price: float,
|
||||
size: float,
|
||||
) -> float:
|
||||
max_percent = state.max_reserved_balance_percent
|
||||
|
||||
state.execution_block_reason = None
|
||||
state.execution_size_adjustment_reason = None
|
||||
|
||||
if max_percent is None or max_percent <= 0:
|
||||
return round(size, 8)
|
||||
|
||||
leverage = state.leverage or 1.0
|
||||
if leverage <= 0 or entry_price <= 0:
|
||||
state.execution_block_reason = "Invalid leverage or entry price."
|
||||
return 0.0
|
||||
|
||||
balance_usd = 1000.0
|
||||
max_reserved_usd = balance_usd * (max_percent / 100)
|
||||
|
||||
max_notional_usd = max_reserved_usd * leverage
|
||||
max_size = max_notional_usd / entry_price
|
||||
|
||||
if size <= max_size:
|
||||
return round(size, 8)
|
||||
|
||||
state.execution_size_adjustment_reason = "MARGIN_LIMIT"
|
||||
|
||||
return round(max_size, 8)
|
||||
|
||||
def _calculate_pnl(self, current_price: float) -> float:
|
||||
position = type(self)._position
|
||||
|
||||
|
||||
Reference in New Issue
Block a user