07.4.3.13 - Risk-based Sizing and Margin Protection
This commit is contained in:
280
app/src/telegram/handlers/auto/_ui_old_working.py
Normal file
280
app/src/telegram/handlers/auto/_ui_old_working.py
Normal file
@@ -0,0 +1,280 @@
|
||||
# app/src/telegram/handlers/auto/ui.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from src.integrations.exchange.service import ExchangeService
|
||||
from src.telegram.ui.common import mode_line
|
||||
from src.telegram.ui.currency_ui import format_usd_amount
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
|
||||
|
||||
PAPER_BALANCE_USD = 1000.0
|
||||
|
||||
def strategy_label(strategy: str | None) -> str:
|
||||
mapping = {
|
||||
"TREND": "📈 Trend Following",
|
||||
"GRID": "🧩 Grid Trading",
|
||||
"SCALP": "⚡ Scalping",
|
||||
}
|
||||
return mapping.get(strategy or "", "—")
|
||||
|
||||
|
||||
def status_label(status: str) -> str:
|
||||
mapping = {
|
||||
"OFF": "⚪ Выключена",
|
||||
"OBSERVING": "👀 Наблюдение",
|
||||
"RUNNING": "🟢 Активна",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
def signal_label(signal: str | None) -> str:
|
||||
mapping = {
|
||||
"BUY": "🟢 BUY",
|
||||
"SELL": "🔴 SELL",
|
||||
"HOLD": "🟡 HOLD",
|
||||
}
|
||||
return mapping.get(signal or "", "—")
|
||||
|
||||
|
||||
def decision_label(status: str) -> str:
|
||||
mapping = {
|
||||
"WAITING": "🟡 Ожидание",
|
||||
"CONFIRMING": "🟠 Подтверждение",
|
||||
"READY": "🟢 Готово к входу",
|
||||
"BLOCKED": "🔴 Заблокировано",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
def value_or_dash(value: object) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return str(value)
|
||||
|
||||
|
||||
def price_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.2f}"
|
||||
|
||||
|
||||
def market_price_or_dash(symbol: str | None) -> str:
|
||||
if not symbol:
|
||||
return "—"
|
||||
|
||||
try:
|
||||
ticker = ExchangeService().get_price(symbol)
|
||||
return f"$ {format_usd_amount(ticker.price)}"
|
||||
except Exception:
|
||||
return "—"
|
||||
|
||||
|
||||
def usd_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.2f} USD"
|
||||
|
||||
|
||||
def size_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.8f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def leverage_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.1f}x"
|
||||
|
||||
|
||||
def format_symbol(symbol: str | None) -> str:
|
||||
if not symbol:
|
||||
return "—"
|
||||
|
||||
base_symbol = symbol.split("_", 1)[0]
|
||||
parts = base_symbol.split("/", 1)
|
||||
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]} / {parts[1]}"
|
||||
|
||||
return base_symbol
|
||||
|
||||
|
||||
def compact_strategy(strategy: str | None) -> str:
|
||||
if not strategy:
|
||||
return "—"
|
||||
return strategy.upper()
|
||||
|
||||
|
||||
def compact_leverage(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"x{value:g}"
|
||||
|
||||
|
||||
def is_auto_configured(state) -> bool:
|
||||
return bool(
|
||||
state.symbol
|
||||
and state.strategy
|
||||
and state.risk_percent is not None
|
||||
)
|
||||
|
||||
|
||||
def context_line(state) -> str:
|
||||
symbol = format_symbol(state.symbol)
|
||||
strategy = compact_strategy(state.strategy)
|
||||
leverage = compact_leverage(state.leverage)
|
||||
|
||||
if leverage == "—":
|
||||
return f"{symbol} · {strategy}"
|
||||
|
||||
return f"{symbol} · {strategy} · {leverage}"
|
||||
|
||||
|
||||
def auto_keyboard() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
|
||||
builder.button(text="⚠️ Risk", callback_data="auto:risk")
|
||||
|
||||
builder.adjust(3, 2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def risk_settings_line(state) -> str:
|
||||
sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
|
||||
tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
|
||||
max_loss = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
|
||||
|
||||
return f"Controls: SL {sl} · TP {tp} · ML {max_loss}"
|
||||
|
||||
|
||||
def target_risk_usd_line(state) -> str:
|
||||
if state.risk_percent is None:
|
||||
return "Target Risk: —"
|
||||
|
||||
risk_usd = PAPER_BALANCE_USD * (state.risk_percent / 100)
|
||||
return f"Target Risk: {risk_usd:.2f} USD"
|
||||
|
||||
|
||||
def estimated_size_line(state) -> str:
|
||||
if (
|
||||
state.risk_percent is None
|
||||
or state.risk_percent <= 0
|
||||
or state.stop_loss_percent is None
|
||||
or state.stop_loss_percent <= 0
|
||||
):
|
||||
return "Est. Size: —"
|
||||
|
||||
try:
|
||||
ticker = ExchangeService().get_price(state.symbol)
|
||||
price = ticker.price
|
||||
except Exception:
|
||||
return "Est. Size: —"
|
||||
|
||||
if price <= 0:
|
||||
return "Est. Size: —"
|
||||
|
||||
target_risk_usd = PAPER_BALANCE_USD * (state.risk_percent / 100)
|
||||
stop_loss_distance_usd = price * (state.stop_loss_percent / 100)
|
||||
|
||||
if stop_loss_distance_usd <= 0:
|
||||
return "Est. Size: —"
|
||||
|
||||
size = target_risk_usd / stop_loss_distance_usd
|
||||
return f"Est. Size: {size:.8f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def position_size_line(state) -> str:
|
||||
if state.position_size is None or state.position_size == 0:
|
||||
return "Position Size: —"
|
||||
|
||||
return f"Position Size: {state.position_size:.8f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def actual_risk_usd_line(state) -> str:
|
||||
if (
|
||||
state.position_side == "NONE"
|
||||
or state.entry_price is None
|
||||
or state.position_size is None
|
||||
or state.stop_loss_percent is None
|
||||
or state.stop_loss_percent <= 0
|
||||
):
|
||||
return "Actual Risk: —"
|
||||
|
||||
stop_loss_distance_usd = state.entry_price * (state.stop_loss_percent / 100)
|
||||
actual_risk_usd = stop_loss_distance_usd * state.position_size
|
||||
|
||||
return f"Actual Risk: {actual_risk_usd:.2f} USD"
|
||||
|
||||
|
||||
def build_auto_text() -> str:
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
|
||||
account_mode = "DEMO" if "DEMO" in mode_line().upper() else "LIVE"
|
||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
||||
configured = is_auto_configured(state)
|
||||
price = market_price_or_dash(state.symbol)
|
||||
|
||||
status_line = {
|
||||
"OFF": "⚪ Off",
|
||||
"OBSERVING": "👀 Watch",
|
||||
"RUNNING": "🟢 On",
|
||||
}.get(state.status, state.status)
|
||||
|
||||
header = (
|
||||
f"<b>🤖 Автоторговля · {status_line}</b>\n"
|
||||
f"🔸 {account_mode} аккаунт\n\n"
|
||||
)
|
||||
|
||||
if state.status == "OFF":
|
||||
if not configured:
|
||||
return (
|
||||
f"{header}"
|
||||
"⚠️ Не настроена\n"
|
||||
"Настрой параметры"
|
||||
)
|
||||
|
||||
return (
|
||||
f"{header}"
|
||||
f"{context_line(state)}\n"
|
||||
f"Price: {price}\n"
|
||||
f"Position Risk: {risk}\n"
|
||||
f"{target_risk_usd_line(state)}\n"
|
||||
f"{estimated_size_line(state)}\n"
|
||||
f"{risk_settings_line(state)}"
|
||||
)
|
||||
|
||||
position_line = (
|
||||
f"Pos: {value_or_dash(state.position_side)} | "
|
||||
f"PnL: {usd_or_dash(state.unrealized_pnl_usd)}"
|
||||
)
|
||||
|
||||
if state.position_side != "NONE" and state.entry_price is not None:
|
||||
position_line = (
|
||||
f"Pos: {value_or_dash(state.position_side)} | "
|
||||
f"Entry: $ {price_or_dash(state.entry_price)} | "
|
||||
f"PnL: {usd_or_dash(state.unrealized_pnl_usd)}"
|
||||
)
|
||||
|
||||
return (
|
||||
f"{header}"
|
||||
f"{context_line(state)}\n"
|
||||
f"Price: {price}\n\n"
|
||||
f"{signal_label(state.last_signal)} ×{state.last_signal_repeat_count} "
|
||||
f"· {state.decision_status}\n\n"
|
||||
f"{position_line}\n"
|
||||
f"Position Risk: {risk}\n"
|
||||
f"{target_risk_usd_line(state)}\n"
|
||||
f"{position_size_line(state)}\n"
|
||||
f"{actual_risk_usd_line(state)}\n"
|
||||
f"{risk_settings_line(state)}"
|
||||
)
|
||||
@@ -11,126 +11,13 @@ from src.telegram.ui.currency_ui import format_usd_amount
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
|
||||
|
||||
def strategy_label(strategy: str | None) -> str:
|
||||
mapping = {
|
||||
"TREND": "📈 Trend Following",
|
||||
"GRID": "🧩 Grid Trading",
|
||||
"SCALP": "⚡ Scalping",
|
||||
}
|
||||
return mapping.get(strategy or "", "—")
|
||||
PAPER_BALANCE_USD = 1000.0
|
||||
|
||||
|
||||
def status_label(status: str) -> str:
|
||||
mapping = {
|
||||
"OFF": "⚪ Выключена",
|
||||
"OBSERVING": "👀 Наблюдение",
|
||||
"RUNNING": "🟢 Активна",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
def signal_label(signal: str | None) -> str:
|
||||
mapping = {
|
||||
"BUY": "🟢 BUY",
|
||||
"SELL": "🔴 SELL",
|
||||
"HOLD": "🟡 HOLD",
|
||||
}
|
||||
return mapping.get(signal or "", "—")
|
||||
|
||||
|
||||
def decision_label(status: str) -> str:
|
||||
mapping = {
|
||||
"WAITING": "🟡 Ожидание",
|
||||
"CONFIRMING": "🟠 Подтверждение",
|
||||
"READY": "🟢 Готово к входу",
|
||||
"BLOCKED": "🔴 Заблокировано",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
def value_or_dash(value: object) -> str:
|
||||
def _format_percent2(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return str(value)
|
||||
|
||||
|
||||
def price_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.2f}"
|
||||
|
||||
|
||||
def market_price_or_dash(symbol: str | None) -> str:
|
||||
if not symbol:
|
||||
return "—"
|
||||
|
||||
try:
|
||||
ticker = ExchangeService().get_price(symbol)
|
||||
return f"$ {format_usd_amount(ticker.price)}"
|
||||
except Exception:
|
||||
return "—"
|
||||
|
||||
|
||||
def usd_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.2f} USD"
|
||||
|
||||
|
||||
def size_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.8f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def leverage_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.1f}x"
|
||||
|
||||
|
||||
def format_symbol(symbol: str | None) -> str:
|
||||
if not symbol:
|
||||
return "—"
|
||||
|
||||
base_symbol = symbol.split("_", 1)[0]
|
||||
parts = base_symbol.split("/", 1)
|
||||
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]} / {parts[1]}"
|
||||
|
||||
return base_symbol
|
||||
|
||||
|
||||
def compact_strategy(strategy: str | None) -> str:
|
||||
if not strategy:
|
||||
return "—"
|
||||
return strategy.upper()
|
||||
|
||||
|
||||
def compact_leverage(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"x{value:g}"
|
||||
|
||||
|
||||
def is_auto_configured(state) -> bool:
|
||||
return bool(
|
||||
state.symbol
|
||||
and state.strategy
|
||||
and state.risk_percent is not None
|
||||
)
|
||||
|
||||
|
||||
def context_line(state) -> str:
|
||||
symbol = format_symbol(state.symbol)
|
||||
strategy = compact_strategy(state.strategy)
|
||||
leverage = compact_leverage(state.leverage)
|
||||
|
||||
if leverage == "—":
|
||||
return f"{symbol} · {strategy}"
|
||||
|
||||
return f"{symbol} · {strategy} · {leverage}"
|
||||
return "off"
|
||||
return f"{float(value):.2f}%"
|
||||
|
||||
|
||||
def auto_keyboard() -> InlineKeyboardMarkup:
|
||||
@@ -146,79 +33,425 @@ def auto_keyboard() -> InlineKeyboardMarkup:
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def risk_settings_line(state) -> str:
|
||||
sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
|
||||
tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
|
||||
max_loss = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
|
||||
|
||||
return f"Controls: SL {sl} · TP {tp} · ML {max_loss}"
|
||||
|
||||
|
||||
def estimated_size_line(state) -> str:
|
||||
if state.risk_percent is None or state.leverage is None:
|
||||
return ""
|
||||
|
||||
size = round((state.risk_percent * state.leverage) / 100, 8)
|
||||
return f"Est. Size: {size}"
|
||||
def is_auto_configured(state) -> bool:
|
||||
return bool(
|
||||
state.symbol
|
||||
and state.strategy
|
||||
and state.risk_percent is not None
|
||||
)
|
||||
|
||||
|
||||
def build_auto_text() -> str:
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
state = AutoTradeService().get_state()
|
||||
|
||||
account_mode = "DEMO" if "DEMO" in mode_line().upper() else "LIVE"
|
||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
||||
configured = is_auto_configured(state)
|
||||
price = market_price_or_dash(state.symbol)
|
||||
|
||||
status_line = {
|
||||
"OFF": "⚪ Off",
|
||||
"OBSERVING": "👀 Watch",
|
||||
"RUNNING": "🟢 On",
|
||||
}.get(state.status, state.status)
|
||||
|
||||
header = (
|
||||
f"<b>🤖 Автоторговля · {status_line}</b>\n"
|
||||
f"🔸 {account_mode} аккаунт\n\n"
|
||||
)
|
||||
|
||||
if state.status == "OFF":
|
||||
if not configured:
|
||||
return (
|
||||
f"{header}"
|
||||
"⚠️ Не настроена\n"
|
||||
"Настрой параметры"
|
||||
)
|
||||
|
||||
return (
|
||||
f"{header}"
|
||||
f"{context_line(state)}\n"
|
||||
f"Price: {price}\n"
|
||||
f"Position Risk: {risk}\n"
|
||||
f"{estimated_size_line(state)}\n"
|
||||
f"{risk_settings_line(state)}"
|
||||
)
|
||||
|
||||
position_line = (
|
||||
f"Pos: {value_or_dash(state.position_side)} | "
|
||||
f"PnL: {usd_or_dash(state.unrealized_pnl_usd)}"
|
||||
)
|
||||
if not is_auto_configured(state):
|
||||
return _build_not_configured_text(state)
|
||||
|
||||
if state.position_side != "NONE" and state.entry_price is not None:
|
||||
position_line = (
|
||||
f"Pos: {value_or_dash(state.position_side)} | "
|
||||
f"Entry: $ {price_or_dash(state.entry_price)} | "
|
||||
f"PnL: {usd_or_dash(state.unrealized_pnl_usd)}"
|
||||
)
|
||||
return _build_active_position_text(state)
|
||||
|
||||
return _build_waiting_text(state)
|
||||
|
||||
|
||||
def _build_not_configured_text(state) -> str:
|
||||
return (
|
||||
"🤖 Автоторговля · ⚪ Не настроена\n"
|
||||
f"{_account_mode_line()}\n\n"
|
||||
"Configuration required\n\n"
|
||||
f"Pair: {_required_value(_asset_symbol(state.symbol))}\n"
|
||||
f"Strategy: {_required_value(_strategy_short(state.strategy))}\n"
|
||||
f"Position Risk: {_required_value(_risk_percent_text(state))}\n\n"
|
||||
"🛠️ Открой настройки для запуска"
|
||||
)
|
||||
|
||||
|
||||
def _execution_block_lines(state) -> list[str]:
|
||||
lines: list[str] = []
|
||||
|
||||
reason = getattr(state, "execution_block_reason", None)
|
||||
if reason:
|
||||
lines.append(f"🔴 Blocked: {reason}")
|
||||
|
||||
adjustment = getattr(state, "execution_size_adjustment_reason", None)
|
||||
if adjustment == "MARGIN_LIMIT":
|
||||
lines.append("🟠 Size adjusted by Max Reserved")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _estimated_margin_text(state, price: float | None) -> str:
|
||||
size = _estimated_size(state, price)
|
||||
|
||||
if size is None or price is None:
|
||||
return "Est. Margin: —"
|
||||
|
||||
leverage = state.leverage or 1.0
|
||||
if leverage <= 0:
|
||||
return "Est. Margin: —"
|
||||
|
||||
notional = size * price
|
||||
reserved = notional / leverage
|
||||
|
||||
return f"Est. Margin: $ {_format_money0(reserved)}"
|
||||
|
||||
|
||||
def _max_reserved_text(state) -> str:
|
||||
max_percent = getattr(state, "max_reserved_balance_percent", None)
|
||||
|
||||
if max_percent is None or max_percent <= 0:
|
||||
return "Max Reserved: off"
|
||||
|
||||
max_reserved = PAPER_BALANCE_USD * (max_percent / 100)
|
||||
|
||||
return f"Max Reserved: {max_percent:g}% · $ {_format_money0(max_reserved)}"
|
||||
|
||||
|
||||
def _build_waiting_text(state) -> str:
|
||||
price = _current_price(state.symbol)
|
||||
signal = state.last_signal or "—"
|
||||
repeats = state.last_signal_repeat_count or 0
|
||||
confidence = state.last_signal_confidence or 0.0
|
||||
rr_line = _risk_reward_line(state)
|
||||
|
||||
parts = [
|
||||
f"🤖 Автоторговля · {_status_text(state.status)}",
|
||||
_account_mode_line(),
|
||||
f"🏦 Balance: $ {_format_money(PAPER_BALANCE_USD)}",
|
||||
"",
|
||||
f"<b>{_asset_symbol(state.symbol)}</b> · {_strategy_short(state.strategy)} · {_leverage_text(state.leverage)}",
|
||||
f"💲 Price: {_format_usd_or_dash(price)}",
|
||||
"",
|
||||
_decision_human_text(state.decision_status),
|
||||
f"{_signal_icon(signal)} {signal} ×{repeats}",
|
||||
f"Confidence: {confidence:.2f}",
|
||||
*(_execution_block_lines(state)),
|
||||
"",
|
||||
f"Position Risk: {_risk_percent_text(state)} · $ {_format_money0(_target_risk_usd(state))}",
|
||||
_estimated_size_text(state, price),
|
||||
_estimated_margin_text(state, price),
|
||||
_max_reserved_text(state),
|
||||
"",
|
||||
f"<b>🛑 SL:</b> {_format_percent2(state.stop_loss_percent)}",
|
||||
f"<b>🎯 TP:</b> {_format_percent2(state.take_profit_percent)}",
|
||||
f"<b>💣 ML:</b> {_max_loss_or_off(state.max_loss_usd)}",
|
||||
]
|
||||
|
||||
if rr_line:
|
||||
parts.extend(["", rr_line])
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _build_active_position_text(state) -> str:
|
||||
current_price = _current_price(state.symbol)
|
||||
current_price_for_calc = current_price or state.entry_price or 0.0
|
||||
|
||||
size = state.position_size or 0.0
|
||||
leverage = state.leverage or 1.0
|
||||
|
||||
notional = size * current_price_for_calc
|
||||
reserved = notional / leverage if leverage > 0 else 0.0
|
||||
pnl = state.unrealized_pnl_usd or 0.0
|
||||
|
||||
rr_line = _risk_reward_line(state)
|
||||
side_icon = "🟢" if state.position_side == "LONG" else "🔴"
|
||||
|
||||
parts = [
|
||||
f"🤖 Автоторговля · {_status_text(state.status)}",
|
||||
_account_mode_line(),
|
||||
f"🏦 Balance: $ {_format_money(PAPER_BALANCE_USD)}",
|
||||
f"💵 Reserved: $ {_format_money(reserved)}",
|
||||
"",
|
||||
(
|
||||
f"{side_icon} <b>{_asset_symbol(state.symbol)}</b> · "
|
||||
f"{_strategy_short(state.strategy)} · "
|
||||
f"<b>{state.position_side}</b> {_leverage_text(state.leverage)}"
|
||||
),
|
||||
f"📦 Size: {_format_crypto_size(size)} ($ {_format_money0(notional)})",
|
||||
f"💲 Entry: $ {_format_money(state.entry_price)}",
|
||||
f"💲 Current: {_format_usd_or_dash(current_price)}",
|
||||
f"⚖️ P&L: {_format_signed_usd(pnl)}",
|
||||
"Fees: not included",
|
||||
"",
|
||||
_active_sl_line(state),
|
||||
_active_tp_line(state),
|
||||
_active_ml_line(state),
|
||||
]
|
||||
|
||||
if rr_line:
|
||||
parts.append(rr_line)
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _current_price(symbol: str | None) -> float | None:
|
||||
if not symbol:
|
||||
return None
|
||||
|
||||
try:
|
||||
return float(ExchangeService().get_price(symbol).price)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _target_risk_usd(state) -> float:
|
||||
if state.risk_percent is None:
|
||||
return 0.0
|
||||
return PAPER_BALANCE_USD * (state.risk_percent / 100)
|
||||
|
||||
|
||||
def _estimated_size(state, price: float | None) -> float | None:
|
||||
if (
|
||||
price is None
|
||||
or price <= 0
|
||||
or state.risk_percent is None
|
||||
or state.risk_percent <= 0
|
||||
or state.stop_loss_percent is None
|
||||
or state.stop_loss_percent <= 0
|
||||
):
|
||||
return None
|
||||
|
||||
stop_loss_distance_usd = price * (state.stop_loss_percent / 100)
|
||||
if stop_loss_distance_usd <= 0:
|
||||
return None
|
||||
|
||||
risk_size = _target_risk_usd(state) / stop_loss_distance_usd
|
||||
|
||||
max_percent = getattr(state, "max_reserved_balance_percent", None)
|
||||
if max_percent is None or max_percent <= 0:
|
||||
return risk_size
|
||||
|
||||
leverage = state.leverage or 1.0
|
||||
if leverage <= 0:
|
||||
return risk_size
|
||||
|
||||
max_reserved_usd = PAPER_BALANCE_USD * (max_percent / 100)
|
||||
max_notional_usd = max_reserved_usd * leverage
|
||||
max_size = max_notional_usd / price
|
||||
|
||||
return min(risk_size, max_size)
|
||||
|
||||
|
||||
def _estimated_size_text(state, price: float | None) -> str:
|
||||
size = _estimated_size(state, price)
|
||||
if size is None or price is None:
|
||||
return "Est. Size: —"
|
||||
|
||||
notional = size * price
|
||||
return (
|
||||
f"Est. Size: {_format_crypto_size(size)} "
|
||||
f"{_asset_symbol(state.symbol)} ($ {_format_money0(notional)})"
|
||||
)
|
||||
|
||||
|
||||
def _active_sl_line(state) -> str:
|
||||
if (
|
||||
state.stop_loss_percent is None
|
||||
or state.entry_price is None
|
||||
or state.position_size is None
|
||||
):
|
||||
return "<b>🛑 SL:</b> off"
|
||||
|
||||
move = state.entry_price * (state.stop_loss_percent / 100)
|
||||
loss = move * state.position_size
|
||||
|
||||
if state.position_side == "SHORT":
|
||||
price = state.entry_price + move
|
||||
else:
|
||||
price = state.entry_price - move
|
||||
|
||||
return (
|
||||
f"{header}"
|
||||
f"{context_line(state)}\n"
|
||||
f"Price: {price}\n\n"
|
||||
f"{signal_label(state.last_signal)} ×{state.last_signal_repeat_count} "
|
||||
f"· {state.decision_status}\n\n"
|
||||
f"{position_line}\n"
|
||||
f"Position Risk: {risk}\n"
|
||||
f"{estimated_size_line(state)}\n"
|
||||
f"{risk_settings_line(state)}"
|
||||
f"<b>🛑 SL:</b> {_format_percent2(state.stop_loss_percent)} · "
|
||||
f"-$ {_format_money0(loss)} ⇢ $ {_format_money0(price)}"
|
||||
)
|
||||
|
||||
|
||||
def _active_tp_line(state) -> str:
|
||||
if (
|
||||
state.take_profit_percent is None
|
||||
or state.entry_price is None
|
||||
or state.position_size is None
|
||||
):
|
||||
return "<b>🎯 TP:</b> off"
|
||||
|
||||
move = state.entry_price * (state.take_profit_percent / 100)
|
||||
profit = move * state.position_size
|
||||
|
||||
if state.position_side == "SHORT":
|
||||
price = state.entry_price - move
|
||||
else:
|
||||
price = state.entry_price + move
|
||||
|
||||
return (
|
||||
f"<b>🎯 TP:</b> {_format_percent2(state.take_profit_percent)} · "
|
||||
f"+$ {_format_money0(profit)} ⇢ $ {_format_money0(price)}"
|
||||
)
|
||||
|
||||
|
||||
def _active_ml_line(state) -> str:
|
||||
if (
|
||||
state.max_loss_usd is None
|
||||
or state.entry_price is None
|
||||
or state.position_size is None
|
||||
or state.position_size <= 0
|
||||
):
|
||||
return "<b>💣 ML:</b> off"
|
||||
|
||||
loss = abs(state.max_loss_usd)
|
||||
price_move = loss / state.position_size
|
||||
|
||||
if state.position_side == "SHORT":
|
||||
price = state.entry_price + price_move
|
||||
else:
|
||||
price = state.entry_price - price_move
|
||||
|
||||
return f"<b>💣 ML:</b> -$ {_format_money0(loss)} ⇢ $ {_format_money0(price)}"
|
||||
|
||||
|
||||
def _risk_reward_line(state) -> str:
|
||||
if (
|
||||
state.stop_loss_percent is None
|
||||
or state.stop_loss_percent <= 0
|
||||
or state.take_profit_percent is None
|
||||
or state.take_profit_percent <= 0
|
||||
):
|
||||
return ""
|
||||
|
||||
ratio = state.take_profit_percent / state.stop_loss_percent
|
||||
return f"<b>⚖️ R:R</b> = 1 : {_format_ratio_value(ratio)}"
|
||||
|
||||
|
||||
def _format_ratio_value(value: float) -> str:
|
||||
if abs(value - round(value)) < 1e-9:
|
||||
return str(int(round(value)))
|
||||
return f"{value:.2f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def _status_text(status: str) -> str:
|
||||
mapping = {
|
||||
"OFF": "⚪ Остановлена",
|
||||
"OBSERVING": "👀 Наблюдение",
|
||||
"RUNNING": "🟢 Работает",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
def _decision_human_text(status: str) -> str:
|
||||
mapping = {
|
||||
"WAITING": "🟡 Ожидание сигнала",
|
||||
"CONFIRMING": "🟠 Подтверждение сигнала",
|
||||
"READY": "🟢 Сигнал готов",
|
||||
"BLOCKED": "🔴 Сигнал заблокирован",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
def _account_mode_line() -> str:
|
||||
return "🔸 DEMO аккаунт" if "DEMO" in mode_line().upper() else "🔸 LIVE аккаунт"
|
||||
|
||||
|
||||
def _asset_symbol(symbol: str | None) -> str:
|
||||
if not symbol:
|
||||
return "—"
|
||||
|
||||
base = symbol.split("_", 1)[0].upper()
|
||||
|
||||
if "/" in base:
|
||||
return base.split("/", 1)[0]
|
||||
|
||||
for suffix in ("USDT", "USD", "EUR", "BTC"):
|
||||
if base.endswith(suffix) and len(base) > len(suffix):
|
||||
return base[: -len(suffix)]
|
||||
|
||||
return base
|
||||
|
||||
|
||||
def _strategy_short(strategy: str | None) -> str:
|
||||
if not strategy:
|
||||
return "—"
|
||||
|
||||
mapping = {
|
||||
"TREND": "Trend",
|
||||
"GRID": "Grid",
|
||||
"SCALP": "Scalp",
|
||||
}
|
||||
return mapping.get(strategy.upper(), strategy.title())
|
||||
|
||||
|
||||
def _leverage_text(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "x—"
|
||||
return f"x{value:g}"
|
||||
|
||||
|
||||
def _risk_percent_text(state) -> str:
|
||||
if state.risk_percent is None:
|
||||
return "—"
|
||||
return f"{state.risk_percent:g}%"
|
||||
|
||||
|
||||
def _percent_or_off(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "off"
|
||||
return f"{value:g}%"
|
||||
|
||||
|
||||
def _max_loss_or_off(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "off"
|
||||
return f"$ {_format_money0(value)}"
|
||||
|
||||
|
||||
def _required_value(value: str) -> str:
|
||||
if not value or value == "—":
|
||||
return "required"
|
||||
return value
|
||||
|
||||
|
||||
def _signal_icon(signal: str | None) -> str:
|
||||
mapping = {
|
||||
"BUY": "🟢",
|
||||
"SELL": "🔴",
|
||||
"HOLD": "🟡",
|
||||
}
|
||||
return mapping.get(signal or "", "⚪")
|
||||
|
||||
|
||||
def _format_money(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return format_usd_amount(float(value))
|
||||
|
||||
|
||||
def _format_money0(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{float(value):,.0f}".replace(",", " ")
|
||||
|
||||
|
||||
def _format_usd_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"$ {_format_money(value)}"
|
||||
|
||||
|
||||
def _format_signed_usd(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
amount = float(value)
|
||||
|
||||
if amount > 0:
|
||||
return f"+$ {_format_money(amount)}"
|
||||
|
||||
if amount < 0:
|
||||
return f"−$ {_format_money(abs(amount))}"
|
||||
|
||||
return "$ 0.00"
|
||||
|
||||
|
||||
def _format_crypto_size(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
return f"{float(value):.4f}".rstrip("0").rstrip(".")
|
||||
@@ -197,6 +197,11 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
symbol = state.symbol or "—"
|
||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
||||
leverage = f"x{state.leverage:g}" if state.leverage is not None else "—"
|
||||
max_reserved = (
|
||||
f"{state.max_reserved_balance_percent:g}%"
|
||||
if state.max_reserved_balance_percent is not None
|
||||
else "off"
|
||||
)
|
||||
sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
|
||||
tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
|
||||
ml = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
|
||||
@@ -220,6 +225,7 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
f"{symbol_icon} Инструмент: {symbol}\n"
|
||||
f"{risk_icon} Риск на сделку: {risk}\n"
|
||||
f"{leverage_icon} Плечо: {leverage}\n\n"
|
||||
f"✅ Max Reserved: {max_reserved}\n"
|
||||
f"✅ Risk Controls: {risk_controls}\n\n"
|
||||
f"{config_status}"
|
||||
)
|
||||
@@ -233,9 +239,10 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="🛡️ Риск на сделку", callback_data="settings:auto_risk")
|
||||
builder.button(text="⚙️ Плечо", callback_data="settings:auto_leverage")
|
||||
builder.button(text="⚠️ Risk Controls", callback_data="auto:risk")
|
||||
builder.button(text="🏦 Max Reserved", callback_data="settings:auto_max_reserved")
|
||||
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
|
||||
builder.button(text="⬅️ Назад", callback_data="system:management")
|
||||
builder.adjust(2, 2, 1, 2)
|
||||
builder.adjust(2, 2, 2, 2)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
await callback.answer()
|
||||
@@ -611,3 +618,44 @@ async def open_system_about(callback: CallbackQuery) -> None:
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "settings:auto_max_reserved")
|
||||
async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>🏦 Max Reserved</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
"Максимальная доля баланса, которую можно зарезервировать под позицию:"
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="25%", callback_data="settings:auto_max_reserved:25")
|
||||
builder.button(text="50%", callback_data="settings:auto_max_reserved:50")
|
||||
builder.button(text="75%", callback_data="settings:auto_max_reserved:75")
|
||||
builder.button(text="100%", callback_data="settings:auto_max_reserved:100")
|
||||
builder.button(text="off", callback_data="settings:auto_max_reserved:off")
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||
builder.adjust(2, 2, 1, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("settings:auto_max_reserved:"))
|
||||
async def set_auto_max_reserved(callback: CallbackQuery) -> None:
|
||||
raw_value = callback.data.split(":", 2)[2]
|
||||
|
||||
value = None if raw_value == "off" else float(raw_value)
|
||||
AutoTradeService().set_max_reserved_balance_percent(value)
|
||||
|
||||
if callback.message is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
await callback.answer("Max Reserved обновлён")
|
||||
@@ -239,6 +239,13 @@ class AutoTradeService:
|
||||
state.max_loss_usd = value
|
||||
return state
|
||||
|
||||
# установить максимальное использование баланса под маржу
|
||||
def set_max_reserved_balance_percent(self, value: float | None) -> AutoTradeState:
|
||||
state = self.get_state()
|
||||
state.max_reserved_balance_percent = value
|
||||
state.execution_block_reason = None
|
||||
return state
|
||||
|
||||
# сбросить внутренний трекинг сигналов
|
||||
def _reset_signal_tracking(self) -> None:
|
||||
self._last_signal_key = None
|
||||
@@ -256,6 +263,7 @@ class AutoTradeService:
|
||||
state.decision_reason = None
|
||||
state.is_signal_confirmed = False
|
||||
state.is_signal_ready = False
|
||||
state.execution_block_reason = None
|
||||
|
||||
# собрать контекст для стратегии
|
||||
def _build_strategy_context(self) -> StrategyContext:
|
||||
|
||||
@@ -75,3 +75,12 @@ class AutoTradeState:
|
||||
|
||||
# максимальный допустимый paper-убыток в USD
|
||||
max_loss_usd: float | None = None
|
||||
|
||||
# максимальная доля баланса, которую можно зарезервировать под позицию
|
||||
max_reserved_balance_percent: float | None = 50.0
|
||||
|
||||
# последняя причина блокировки execution
|
||||
execution_block_reason: str | None = None
|
||||
|
||||
# причина авто-уменьшения размера позиции
|
||||
execution_size_adjustment_reason: str | None = None
|
||||
@@ -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,10 +414,73 @@ class ExecutionEngine:
|
||||
|
||||
self._sync_state_from_position(state)
|
||||
|
||||
def _calculate_position_size(self, state: AutoTradeState) -> float:
|
||||
risk_percent = state.risk_percent or 0.0
|
||||
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
|
||||
return round((risk_percent * leverage) / 100, 8)
|
||||
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
|
||||
|
||||
@@ -223,6 +223,25 @@
|
||||
- Telegram execution alerts с причиной риска
|
||||
- единая точка принятия решений (execution layer)
|
||||
|
||||
#### 07.4.3.13 — Risk-Based Position Sizing & Margin Protection ✅
|
||||
- risk-based position sizing через SL distance
|
||||
- размер позиции теперь рассчитывается от Risk %
|
||||
- execution-level margin validation
|
||||
- защита от oversized positions
|
||||
- max reserved balance limit
|
||||
- execution block reason state
|
||||
- блокировка ENTRY / FLIP при превышении margin limit
|
||||
- compact mobile UI redesign
|
||||
- новый формат SL / TP / ML
|
||||
- compact position rendering
|
||||
- estimated margin preview
|
||||
- max reserved preview
|
||||
- execution blocked status в UI
|
||||
- улучшенный mobile formatting
|
||||
- SL стал обязательным для risk-engine sizing
|
||||
- risk_percent теперь реально влияет на размер позиции
|
||||
- flip теперь проходит через margin protection
|
||||
|
||||
### 07.4.4
|
||||
⏳ Grid Strategy
|
||||
|
||||
|
||||
@@ -208,6 +208,26 @@
|
||||
- Telegram execution alerts с причиной риска
|
||||
- единая точка принятия решений (execution layer)
|
||||
|
||||
#### 07.4.3.13 — Risk-Based Position Sizing & Margin Protection ✅
|
||||
|
||||
- risk-based position sizing через SL distance
|
||||
- размер позиции теперь рассчитывается от Risk %
|
||||
- execution-level margin validation
|
||||
- защита от oversized positions
|
||||
- max reserved balance limit
|
||||
- execution block reason state
|
||||
- блокировка ENTRY / FLIP при превышении margin limit
|
||||
- compact mobile UI redesign
|
||||
- новый формат SL / TP / ML
|
||||
- compact position rendering
|
||||
- estimated margin preview
|
||||
- max reserved preview
|
||||
- execution blocked status в UI
|
||||
- улучшенный mobile formatting
|
||||
- SL стал обязательным для risk-engine sizing
|
||||
- risk_percent теперь реально влияет на размер позиции
|
||||
- flip теперь проходит через margin protection
|
||||
|
||||
---
|
||||
|
||||
### 07.4.4
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
# 07.4.3.13 — Risk-Based Position Sizing & Margin Protection
|
||||
|
||||
## Цель
|
||||
|
||||
Перевести paper execution с “фиксированного размера позиции” на полноценную risk-based модель, где:
|
||||
|
||||
- размер позиции рассчитывается через Risk %
|
||||
- Stop Loss участвует в sizing
|
||||
- execution защищён от oversized positions
|
||||
- margin usage контролируется до открытия сделки
|
||||
- UI показывает реальную нагрузку на баланс
|
||||
|
||||
---
|
||||
|
||||
# Что было раньше
|
||||
|
||||
Ранее:
|
||||
|
||||
- risk_percent существовал только как UI-параметр
|
||||
- размер позиции не ограничивался margin usage
|
||||
- leverage мог создавать oversized exposure
|
||||
- позиция могла резервировать 100%+ баланса
|
||||
- SL / TP использовались только для close logic
|
||||
|
||||
Пример проблемы:
|
||||
|
||||
- balance = 1000 USD
|
||||
- leverage = x5
|
||||
- BTC = 80 000
|
||||
- risk = 2%
|
||||
- SL = 1%
|
||||
|
||||
execution открывал позицию:
|
||||
|
||||
- notional ≈ 10 000 USD
|
||||
- reserved ≈ 2 000 USD
|
||||
|
||||
что превышало весь paper balance.
|
||||
|
||||
---
|
||||
|
||||
# Что реализовано
|
||||
|
||||
## 1. Risk-Based Position Sizing
|
||||
|
||||
ExecutionEngine теперь рассчитывает size через риск.
|
||||
|
||||
Добавлена формула:
|
||||
|
||||
```
|
||||
target_risk_usd = balance * (risk_percent / 100)
|
||||
stop_loss_distance_usd = price * (stop_loss_percent / 100)
|
||||
size = target_risk_usd / stop_loss_distance_usd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. SL стал обязательным для execution
|
||||
|
||||
Теперь позиция не открывается без:
|
||||
|
||||
* risk_percent
|
||||
* stop_loss_percent
|
||||
|
||||
Причина:
|
||||
|
||||
без SL невозможно определить:
|
||||
|
||||
* допустимый убыток
|
||||
* размер позиции
|
||||
* риск сделки
|
||||
|
||||
---
|
||||
|
||||
## 3. Margin Protection
|
||||
|
||||
Добавлено новое поле state:
|
||||
|
||||
`max_reserved_balance_percent`
|
||||
|
||||
По умолчанию: 50.0
|
||||
|
||||
---
|
||||
|
||||
## 4. Margin Validation
|
||||
|
||||
Перед:
|
||||
|
||||
* ENTRY
|
||||
* FLIP
|
||||
|
||||
выполняется:
|
||||
`_validate_margin_usage()`
|
||||
|
||||
---
|
||||
|
||||
## 5. Проверка reserved margin
|
||||
|
||||
Execution рассчитывает:
|
||||
```
|
||||
notional_usd = entry_price * size
|
||||
reserved_usd = notional_usd / leverage
|
||||
```
|
||||
|
||||
и сравнивает с лимитом:
|
||||
```
|
||||
max_reserved_usd =
|
||||
balance * (max_reserved_balance_percent / 100)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Execution Blocking
|
||||
|
||||
Если margin превышает лимит:
|
||||
|
||||
* позиция НЕ открывается
|
||||
* flip НЕ выполняется
|
||||
* execution возвращает BLOCK reason
|
||||
|
||||
---
|
||||
|
||||
## 7. Execution Block State
|
||||
|
||||
Добавлено новое поле:
|
||||
`execution_block_reason`
|
||||
|
||||
Используется для UI и debugging.
|
||||
|
||||
---
|
||||
|
||||
## UI Improvements
|
||||
|
||||
Compact Mobile Layout
|
||||
|
||||
Полностью переработан mobile rendering.
|
||||
|
||||
### Новый формат позиции
|
||||
|
||||
Было:
|
||||
`📦 Size: 0.02447179 · $ 2 000.41`
|
||||
|
||||
Стало:
|
||||
`📦 Size: 0.0245 ($ 2 000)`
|
||||
|
||||
### Новый формат SL / TP / ML
|
||||
|
||||
Было:
|
||||
`🛑 SL: −$ 20.00 · $ 82 544.02`
|
||||
|
||||
Стало:
|
||||
```
|
||||
🛑 SL: 1.00% · -$ 20 ⇢ $ 82 544
|
||||
🎯 TP: 2.00% · +$ 40 ⇢ $ 80 092
|
||||
💣 ML: -$ 100 ⇢ $ 85 813
|
||||
```
|
||||
|
||||
### Добавлено отображение:
|
||||
|
||||
* Est. Margin
|
||||
* Max Reserved
|
||||
* Blocked reason
|
||||
|
||||
### Новые UI элементы
|
||||
|
||||
Estimated Margin
|
||||
`Est. Margin: $ 400`
|
||||
|
||||
Max Reserved
|
||||
`Max Reserved: 50% · $ 500`
|
||||
|
||||
Execution Blocked
|
||||
`🔴 Blocked: Margin $ 1000 > limit $ 500`
|
||||
|
||||
---
|
||||
|
||||
## Изменения Execution Flow
|
||||
|
||||
### ENTRY
|
||||
|
||||
Теперь flow:
|
||||
signal
|
||||
→ size calculation
|
||||
→ margin validation
|
||||
→ open position
|
||||
|
||||
### FLIP
|
||||
|
||||
Теперь flip:
|
||||
|
||||
* закрывает текущую позицию
|
||||
* рассчитывает новый size
|
||||
* валидирует margin
|
||||
* только потом открывает reverse position
|
||||
|
||||
---
|
||||
|
||||
## Архитектурный результат
|
||||
|
||||
Теперь:
|
||||
|
||||
* risk_percent реально влияет на trade size
|
||||
* leverage влияет на reserved balance
|
||||
* SL участвует в sizing engine
|
||||
* execution защищён от oversized exposure
|
||||
* UI показывает реальную margin load
|
||||
|
||||
---
|
||||
|
||||
## Ограничения текущей реализации
|
||||
|
||||
Пока ещё нет:
|
||||
|
||||
* dynamic balance updates
|
||||
* equity tracking
|
||||
* realized pnl balance updates
|
||||
* liquidation simulation
|
||||
* fees engine
|
||||
* funding
|
||||
* tiered leverage
|
||||
* maintenance margin
|
||||
* partial close
|
||||
|
||||
---
|
||||
|
||||
## Следующий этап
|
||||
|
||||
07.4.3.14 — Strategy Engine Upgrade
|
||||
|
||||
Планируется:
|
||||
|
||||
* улучшение TREND strategy
|
||||
* rolling candles / history window
|
||||
* EMA-based trend detection
|
||||
* ATR volatility filter
|
||||
* noise reduction
|
||||
* smarter confidence model
|
||||
* configurable thresholds
|
||||
* полноценная SCALP strategy
|
||||
* anti-whipsaw protection
|
||||
* multi-confirmation signals
|
||||
|
||||
Reference in New Issue
Block a user