07.4.3.14 — Auto Trading UI. Realistic Pricing & Debug Live Tools

This commit is contained in:
2026-05-09 01:34:46 +03:00
parent ee78f9774a
commit df76490783
15 changed files with 2161 additions and 464 deletions

View File

@@ -60,12 +60,10 @@ async def open_auto(message: Message, state: FSMContext) -> None:
AutoTradeRunner.set_current_screen("auto")
current_state = AutoTradeService().get_state()
if current_state.status in {"RUNNING", "OBSERVING"}:
await AutoTradeRunner.delete_registered_screen(
bot=message.bot,
chat_id=message.chat.id,
)
await AutoTradeRunner.delete_registered_screen(
bot=message.bot,
chat_id=message.chat.id,
)
await render_auto_screen(message, edit_mode=False)

View File

@@ -24,30 +24,45 @@ class AutoRiskStates(StatesGroup):
waiting_max_loss = State()
def _format_number(value: float | int | None) -> str:
if value is None:
return ""
number = float(value)
if abs(number - round(number)) < 1e-9:
return f"{int(round(number))}"
return f"{number:.2f}".rstrip("0").rstrip(".")
def _format_percent(value: float | None) -> str:
if value is None:
return "off"
return f"🟢 {value:g}%"
return "off"
return f"{_format_number(value)}%"
def _format_usd(value: float | None) -> str:
if value is None:
return "off"
return f"🟢 {value:g} USD"
return "off"
return f"{_format_number(value)} USD"
def _rule_icon(value: float | None) -> str:
return "" if value is not None else "⚠️"
def _risk_keyboard() -> InlineKeyboardMarkup:
state = AutoTradeService().get_state()
builder = InlineKeyboardBuilder()
builder.button(text=f"🛑 Stop Loss", callback_data="auto:risk:set_sl")
builder.button(text=f"🎯 Take Profit", callback_data="auto:risk:set_tp")
builder.button(text=f"💸 Max Loss", callback_data="auto:risk:set_ml")
builder.button(text="♻️ Reset", callback_data="auto:risk:reset")
builder.button(text="⬅️ Назад", callback_data="settings:auto")
builder.button(text="🛑 SL", callback_data="auto:risk:set_sl")
builder.button(text="🎯 TP", callback_data="auto:risk:set_tp")
builder.button(text="💸 ML", callback_data="auto:risk:set_ml")
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
builder.button(text="⬅️ Назад", callback_data="settings:auto")
builder.button(text="♻️ Сбросить", callback_data="auto:risk:reset")
builder.adjust(2, 2, 2)
builder.adjust(3, 1, 2)
return builder.as_markup()
@@ -66,16 +81,13 @@ def _risk_text(status_message: str | None = None) -> str:
status = "🟢 Активна" if active_count else "⚪ Выключена"
text = (
"<b>⚠️ Risk Settings</b>\n\n"
"<b>🧯 Защита позиции</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
f"Статус защиты: {status}\n"
f"Активных правил: {active_count}/3\n\n"
f"🛑 Stop Loss: {_format_percent(state.stop_loss_percent)}\n"
f"🎯 Take Profit: {_format_percent(state.take_profit_percent)}\n"
f"💸 Max Loss: {_format_usd(state.max_loss_usd)}\n\n"
"<b>Подсказка:</b>\n"
"Пример: <code>0.5</code>, <code>1</code>\n"
"Введите <code>0</code>, чтобы отключить параметр."
f"{_rule_icon(state.stop_loss_percent)} Stop Loss · {_format_percent(state.stop_loss_percent)}\n"
f"{_rule_icon(state.take_profit_percent)} Take Profit · {_format_percent(state.take_profit_percent)}\n"
f"{_rule_icon(state.max_loss_usd)} Max Loss · {_format_usd(state.max_loss_usd)}\n"
)
if status_message:
@@ -155,7 +167,7 @@ async def _remember_risk_screen(callback: CallbackQuery, state: FSMContext) -> N
def _parse_positive_or_none(raw_text: str | None) -> float | None:
value_text = (raw_text or "").strip().replace(",", ".")
if value_text in {"0", "0.0", "off", "OFF", "-"}:
if value_text.lower() in {"0", "0.0", "off", "-"}:
return None
value = float(value_text)
@@ -222,11 +234,11 @@ async def ask_stop_loss(callback: CallbackQuery, state: FSMContext) -> None:
if callback.message is not None:
await callback.message.edit_text(
"<b>🛑 Stop Loss</b>\n\n"
"<b>Stop Loss</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Введите Stop Loss в процентах.\n"
"Например: <code>2</code>\n\n"
"Введите <code>0</code>, чтобы отключить."
"Например: <code>1</code>, <code>0.5</code>, <code>0,5</code>\n\n"
"отключить параметр - <code>0</code>"
)
await callback.answer()
@@ -240,11 +252,11 @@ async def ask_take_profit(callback: CallbackQuery, state: FSMContext) -> None:
if callback.message is not None:
await callback.message.edit_text(
"<b>🎯 Take Profit</b>\n\n"
"<b>Take Profit</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Введите Take Profit в процентах.\n"
"Например: <code>3</code>\n\n"
"Введите <code>0</code>, чтобы отключить."
"Например: <code>2</code>, <code>1.5</code>, <code>1,5</code>\n\n"
"отключить параметр - <code>0</code>"
)
await callback.answer()
@@ -258,11 +270,11 @@ async def ask_max_loss(callback: CallbackQuery, state: FSMContext) -> None:
if callback.message is not None:
await callback.message.edit_text(
"<b>💸 Max Loss</b>\n\n"
"<b>Maximum Loss</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Введите максимальный paper-убыток в USD.\n"
"Например: <code>10</code>\n\n"
"Введите <code>0</code>, чтобы отключить."
"Например: <code>100</code>, <code>50.5</code>, <code>50,5</code>\n\n"
"отключить параметр - <code>0</code>"
)
await callback.answer()
@@ -309,7 +321,7 @@ async def set_stop_loss(message: Message, state: FSMContext) -> None:
try:
value = _parse_positive_or_none(message.text)
except ValueError:
await message.answer("Введите число. Например: 2 или 0 для отключения.")
await message.answer("Введите число. Например: 1, 0.5 или 0 для отключения.")
return
if not _validate_percent(value):
@@ -333,7 +345,7 @@ async def set_take_profit(message: Message, state: FSMContext) -> None:
try:
value = _parse_positive_or_none(message.text)
except ValueError:
await message.answer("Введите число. Например: 3 или 0 для отключения.")
await message.answer("Введите число. Например: 2, 1.5 или 0 для отключения.")
return
if not _validate_percent(value):
@@ -357,7 +369,7 @@ async def set_max_loss(message: Message, state: FSMContext) -> None:
try:
value = _parse_positive_or_none(message.text)
except ValueError:
await message.answer("Введите число. Например: 10 или 0 для отключения.")
await message.answer("Введите число. Например: 100, 50.5 или 0 для отключения.")
return
if not _validate_max_loss(value):

View File

@@ -2,6 +2,9 @@
from __future__ import annotations
import math
import time
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -11,15 +14,6 @@ from src.telegram.ui.currency_ui import format_usd_amount
from src.trading.auto.service import AutoTradeService
PAPER_BALANCE_USD = 1000.0
def _format_percent2(value: float | None) -> str:
if value is None:
return "off"
return f"{float(value):.2f}%"
def auto_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
@@ -27,18 +21,31 @@ def auto_keyboard() -> InlineKeyboardMarkup:
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.button(text="🧯 Защита", callback_data="auto:risk")
builder.adjust(3, 2)
return builder.as_markup()
def is_auto_configured(state) -> bool:
return bool(
state.symbol
and state.strategy
and state.risk_percent is not None
)
if not state.symbol:
return False
if not state.strategy:
return False
if state.risk_percent is None:
return False
strategy = state.strategy.upper()
if strategy == "TREND":
return (
state.stop_loss_percent is not None
and state.stop_loss_percent > 0
)
return True
def build_auto_text() -> str:
@@ -50,141 +57,258 @@ def build_auto_text() -> str:
if state.position_side != "NONE" and state.entry_price is not None:
return _build_active_position_text(state)
if state.status == "OFF":
return _build_stopped_without_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"
"🛠️ Открой настройки для запуска"
symbol_ready = state.symbol is not None
strategy_ready = state.strategy is not None
risk_ready = state.risk_percent is not None
symbol_icon = "" if symbol_ready else "⚠️"
strategy_icon = "" if strategy_ready else "⚠️"
risk_icon = "" if risk_ready else "⚠️"
parts = [
"🤖 Автоторговля ⚪ Не настроена",
_account_mode_line(),
"",
f"{symbol_icon} <b>Актив</b> · {_asset_symbol(state.symbol)}\n"
f"{strategy_icon} <b>Стратегия</b> · {_required_value(_strategy_short(state.strategy))}\n"
f"{risk_icon} <b>Риск на сделку</b> · {_required_value(_risk_percent_text(state))}\n"
]
strategy = (state.strategy or "").upper()
if strategy == "TREND":
sl_value = (
_format_percent(state.stop_loss_percent)
if state.stop_loss_percent is not None
else ""
)
sl_icon = "" if sl_value != "" else "⚠️"
parts.append(f"{sl_icon} <b>SL</b> · {sl_value}")
parts.extend([
"",
"⚠️ Требуется настройка параметров",
])
return "\n".join(parts)
def _build_stopped_without_position_text(state) -> str:
price = _current_price(state.symbol)
available = _allocated_balance(state) + _realized_pnl(state)
estimated_size = _estimated_size(state, price)
rr_line = _risk_reward_line(state)
risk_line = _risk_summary_line(
state,
estimated_size,
entry_price_override=price,
)
parts = [
f"🤖 Автоторговля {_status_text(state.status)}",
_account_mode_line(),
"",
f"<b>Доступно</b> · $ {_format_money_compact(available)}\n",
"🧾 <b>Подготовка ордера</b>",
"",
_order_header_line(state),
f"<b>Цена</b> · {_format_usd_or_dash(price)}",
_estimated_size_text(state, price),
_max_reserved_line(state, price),
f"<b>Риск</b> · {_risk_percent_text(state)} ($ {_format_money_compact(_target_risk_usd(state))})",
]
if rr_line or risk_line:
parts.append("")
if rr_line:
parts.append(rr_line)
if risk_line:
parts.append(risk_line)
return "\n".join(parts)
def _build_waiting_text(state) -> str:
price = _signal_entry_price(state)
available = _allocated_balance(state) + _realized_pnl(state)
estimated_size = _estimated_size(state, price)
rr_line = _risk_reward_line(state)
risk_line = _risk_summary_line(
state,
estimated_size,
entry_price_override=price,
)
parts = [
f"🤖 Автоторговля {_status_text(state.status)}",
_account_mode_line(),
"",
f"<b>Доступно</b> · $ {_format_money_compact(available)}",
"",
_signal_line(state),
*_signal_confidence_lines(state),
*_execution_block_lines(state),
"",
"🧾 <b>Подготовка ордера</b>",
"",
_order_header_line(state),
f"<b>{_price_label_for_signal(state)}</b> · {_format_usd_or_dash(price)}",
_estimated_size_text(state, price),
_max_reserved_line(state, price),
f"<b>Риск</b> · {_risk_percent_text(state)} ($ {_format_money_compact(_target_risk_usd(state))})",
]
if rr_line or risk_line:
parts.append("")
if rr_line:
parts.append(rr_line)
if risk_line:
parts.append(risk_line)
return "\n".join(parts)
def _build_active_position_text(state) -> str:
current_price = _current_price(state.symbol)
price_for_calc = current_price or state.entry_price or 0.0
size = state.position_size or 0.0
notional = size * price_for_calc
reserved = _position_reserved_usd(state, current_price)
available = _allocated_balance(state) + _realized_pnl(state) - reserved
pnl = state.unrealized_pnl_usd or 0.0
rr_line = _risk_reward_line(state)
risk_line = _risk_summary_line(state, size)
side_icon = "🟢" if state.position_side == "LONG" else "🔴"
parts = [
f"🤖 Автоторговля {_status_text(state.status)}",
_account_mode_line(),
"",
f"<b>Доступно</b> · $ {_format_money_compact(available)}",
f"<b>Зарезервировано</b> · $ {_format_money_compact(reserved)}",
f"<b>P&L</b> {_format_signed_usd_with_direction(pnl)}",
"",
(
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"<b>Количество</b> · {_format_crypto_size(size)} ⇢ $ {_format_money_compact(notional)}",
f"<b>Цена входа</b> · $ {_format_money(state.entry_price)}",
f"<b>Текущая цена</b> · {_format_usd_or_dash(current_price)}",
"",
"⚠️ Комиссии не учтены",
]
if rr_line or risk_line:
parts.append("")
if rr_line:
parts.append(rr_line)
if risk_line:
parts.append(risk_line)
return "\n".join(parts)
def _execution_block_lines(state) -> list[str]:
lines: list[str] = []
reason = getattr(state, "execution_block_reason", None)
if reason:
lines.append(f"🔴 Blocked: {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")
lines.append("Size adjusted by Max Reserved")
return lines
def _estimated_margin_text(state, price: float | None) -> str:
def _allocated_balance(state) -> float:
return float(getattr(state, "allocated_balance_usd", 1000.0) or 1000.0)
def _realized_pnl(state) -> float:
return float(getattr(state, "realized_pnl_usd", 0.0) or 0.0)
def _position_reserved_usd(state, current_price: float | None) -> float:
if (
state.position_side == "NONE"
or state.position_size is None
or state.position_size <= 0
):
return 0.0
price = current_price or state.entry_price or 0.0
leverage = state.leverage or 1.0
if price <= 0 or leverage <= 0:
return 0.0
return (state.position_size * price) / leverage
def _max_reserved_line(state, price: float | None = None) -> str:
size = _estimated_size(state, price)
if size is None or price is None:
return "Est. Margin:"
if size is None or price is None or price <= 0:
return "<b>Собственные средства</b> ·"
leverage = state.leverage or 1.0
if leverage <= 0:
return "Est. Margin:"
return "<b>Собственные средства</b> ·"
notional = size * price
reserved = notional / leverage
position_size_usd = size * price
own_funds_usd = position_size_usd / leverage
return f"Est. Margin: $ {_format_money0(reserved)}"
return (
f"<b>Собственные средства</b> · $ {_format_money_compact(own_funds_usd)}"
)
def _max_reserved_text(state) -> str:
max_percent = getattr(state, "max_reserved_balance_percent", None)
def _market_snapshot(symbol: str | None) -> dict[str, object] | None:
if not symbol:
return 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)
try:
return ExchangeService().get_market_snapshot(symbol)
except Exception:
return None
def _current_price(symbol: str | None) -> float | None:
snapshot = _market_snapshot(symbol)
if snapshot is not None:
price = snapshot.get("last_price")
if price is not None:
return float(price)
if not symbol:
return None
@@ -194,10 +318,32 @@ def _current_price(symbol: str | None) -> float | None:
return None
def _signal_entry_price(state) -> float | None:
snapshot = _market_snapshot(state.symbol)
if snapshot is None:
return _current_price(state.symbol)
signal = (state.last_signal or "HOLD").upper()
if signal == "BUY":
price = snapshot.get("ask_price")
elif signal == "SELL":
price = snapshot.get("bid_price")
else:
price = snapshot.get("last_price")
if price is None:
return None
return float(price)
def _target_risk_usd(state) -> float:
if state.risk_percent is None:
return 0.0
return PAPER_BALANCE_USD * (state.risk_percent / 100)
return _allocated_balance(state) * (state.risk_percent / 100)
def _estimated_size(state, price: float | None) -> float | None:
@@ -219,93 +365,113 @@ def _estimated_size(state, price: float | None) -> float | None:
max_percent = getattr(state, "max_reserved_balance_percent", None)
if max_percent is None or max_percent <= 0:
return risk_size
return _round_size(risk_size)
leverage = state.leverage or 1.0
if leverage <= 0:
return risk_size
return _round_size(risk_size)
max_reserved_usd = PAPER_BALANCE_USD * (max_percent / 100)
max_reserved_usd = _allocated_balance(state) * (max_percent / 100)
max_notional_usd = max_reserved_usd * leverage
max_size = max_notional_usd / price
return min(risk_size, max_size)
return _round_size(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:"
return "<b>Количество</b> ·"
notional = size * price
return (
f"Est. Size: {_format_crypto_size(size)} "
f"{_asset_symbol(state.symbol)} ($ {_format_money0(notional)})"
f"<b>Количество</b> · {_format_crypto_size(size)}\n"
f"<b>Размер позиции</b> · $ {_format_money_compact(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"
def _risk_summary_line(
state,
size: float | None,
*,
entry_price_override: float | None = None,
) -> str:
entry_price = entry_price_override or state.entry_price
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"<b>🛑 SL:</b> {_format_percent2(state.stop_loss_percent)} · "
f"-$ {_format_money0(loss)} ⇢ $ {_format_money0(price)}"
sl = _risk_loss_text(
percent=state.stop_loss_percent,
fixed_loss=None,
size=size,
entry_price=entry_price,
)
tp = _risk_profit_text(
percent=state.take_profit_percent,
size=size,
entry_price=entry_price,
)
ml = _risk_loss_text(
percent=None,
fixed_loss=state.max_loss_usd,
size=size,
entry_price=entry_price,
)
items = [
f"<b>SL</b> {sl}" if sl else "<b>SL</b> off",
f"<b>TP</b> {tp}" if tp else "<b>TP</b> off",
f"<b>ML</b> {ml}" if ml else "<b>ML</b> off",
]
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)}"
)
return " | ".join(items)
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"
def _risk_loss_text(
*,
percent: float | None,
fixed_loss: float | None,
size: float | None,
entry_price: float | None,
) -> str:
if fixed_loss is not None:
return f"-$ {_format_money_compact(abs(fixed_loss))}"
loss = abs(state.max_loss_usd)
price_move = loss / state.position_size
if percent is None:
return ""
if state.position_side == "SHORT":
price = state.entry_price + price_move
else:
price = state.entry_price - price_move
if size is None or size <= 0 or entry_price is None or entry_price <= 0:
loss = _target_loss_by_percent_stub(percent)
return f"-$ {_format_money_compact(loss)}" if loss is not None else ""
return f"<b>💣 ML:</b> -$ {_format_money0(loss)} ⇢ $ {_format_money0(price)}"
move = entry_price * (percent / 100)
loss = move * size
return f"-$ {_format_money_compact(loss)}"
def _risk_profit_text(
*,
percent: float | None,
size: float | None,
entry_price: float | None,
) -> str:
if percent is None:
return ""
if size is None or size <= 0 or entry_price is None or entry_price <= 0:
return ""
move = entry_price * (percent / 100)
profit = move * size
return f"+$ {_format_money_compact(profit)}"
def _target_loss_by_percent_stub(percent: float | None) -> float | None:
if percent is None:
return None
return None
def _risk_reward_line(state) -> str:
@@ -318,12 +484,82 @@ def _risk_reward_line(state) -> str:
return ""
ratio = state.take_profit_percent / state.stop_loss_percent
return f"<b>⚖️ R:R</b> = 1 : {_format_ratio_value(ratio)}"
return f"<b>R:R</b> = 1 : {_format_ratio_value(ratio)}"
def _order_header_line(state) -> str:
signal = (state.last_signal or "").upper()
if signal == "BUY":
return (
f"🟢 <b>{_asset_symbol(state.symbol)}</b> · "
f"{_strategy_short(state.strategy)} · "
f"<b>LONG</b> {_leverage_text(state.leverage)}"
)
if signal == "SELL":
return (
f"🔴 <b>{_asset_symbol(state.symbol)}</b> · "
f"{_strategy_short(state.strategy)} · "
f"<b>SHORT</b> {_leverage_text(state.leverage)}"
)
return (
f"<b>{_asset_symbol(state.symbol)}</b> · "
f"{_strategy_short(state.strategy)} · "
f"{_leverage_text(state.leverage)}"
)
def _price_label_for_signal(state) -> str:
signal = (state.last_signal or "").upper()
if signal in {"BUY", "SELL"}:
return "Цена входа"
return "Цена"
def _signal_line(state) -> str:
signal = (state.last_signal or "HOLD").upper()
if signal in {"BUY", "SELL"} and (
state.decision_status == "READY"
or getattr(state, "is_signal_ready", False)
):
return f"Сигнал {_signal_icon(signal)} {signal} · READY"
duration = _signal_duration_text(state)
return f"Сигнал {_signal_icon(signal)} {signal} · {duration}"
def _signal_duration_text(state) -> str:
started_at = getattr(state, "signal_started_at", None)
if started_at is not None:
total_seconds = max(0, int(time.monotonic() - float(started_at)))
else:
repeat_count = state.last_signal_repeat_count or 0
total_seconds = max(0, repeat_count * 5)
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours > 0:
return f"{hours}ч {minutes:02d}м"
if minutes > 0:
return f"{minutes}м {seconds:02d}с"
return f"{seconds}с"
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(".")
@@ -338,16 +574,16 @@ def _status_text(status: str) -> str:
def _decision_human_text(status: str) -> str:
mapping = {
"WAITING": "🟡 Ожидание сигнала",
"CONFIRMING": "🟠 Подтверждение сигнала",
"READY": "🟢 Сигнал готов",
"BLOCKED": "🔴 Сигнал заблокирован",
"WAITING": "Ожидание сигнала",
"CONFIRMING": "Подтверждение сигнала",
"READY": "Сигнал готов",
"BLOCKED": "Сигнал заблокирован",
}
return mapping.get(status, status)
def _account_mode_line() -> str:
return "🔸 DEMO аккаунт" if "DEMO" in mode_line().upper() else "🔸 LIVE аккаунт"
return "DEMO аккаунт" if "DEMO" in mode_line().upper() else "LIVE аккаунт"
def _asset_symbol(symbol: str | None) -> str:
@@ -387,51 +623,90 @@ def _leverage_text(value: float | None) -> str:
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)}"
return _format_percent(state.risk_percent)
def _required_value(value: str) -> str:
if not value or value == "":
return "required"
return ""
return value
def _signal_confidence_lines(state) -> list[str]:
signal = (state.last_signal or "HOLD").upper()
if signal == "HOLD":
return []
return [
f"Уверенность · {(state.last_signal_confidence or 0.0):.2f}"
]
def _signal_icon(signal: str | None) -> str:
mapping = {
"BUY": "🟢",
"SELL": "🔴",
"HOLD": "🟡",
}
return mapping.get(signal or "", "")
return mapping.get(signal or "", "")
def _round_size(value: float | int | None) -> float | None:
if value is None:
return None
precision = 5
factor = 10 ** precision
return math.floor(float(value) * factor) / factor
def _format_crypto_size(value: float | int | None) -> str:
rounded = _round_size(value)
if rounded is None:
return ""
return f"{rounded:.5f}".rstrip("0").rstrip(".")
def _format_percent(value: float | int | None) -> str:
if value is None:
return "off"
number = float(value)
if abs(number - round(number)) < 1e-9:
return f"{int(round(number))}%"
return f"{number:.2f}".rstrip("0").rstrip(".") + "%"
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:
def _format_money_compact(value: float | int | None) -> str:
if value is None:
return ""
return f"{float(value):,.0f}".replace(",", " ")
number = float(value)
if abs(number - round(number)) < 1e-9:
return f"{number:,.0f}".replace(",", " ")
return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".")
def _format_usd_or_dash(value: float | None) -> str:
if value is None:
return ""
return f"$ {_format_money(value)}"
@@ -442,16 +717,24 @@ def _format_signed_usd(value: float | int | None) -> str:
amount = float(value)
if amount > 0:
return f"+$ {_format_money(amount)}"
return f"+$ {_format_money_compact(amount)}"
if amount < 0:
return f"$ {_format_money(abs(amount))}"
return f"$ {_format_money_compact(abs(amount))}"
return "$ 0.00"
return "$ 0"
def _format_crypto_size(value: float | int | None) -> str:
def _format_signed_usd_with_direction(value: float | int | None) -> str:
if value is None:
return ""
return f"{float(value):.4f}".rstrip("0").rstrip(".")
amount = float(value)
if amount > 0:
return f"🟢 +$ {_format_money_compact(amount)}"
if amount < 0:
return f"🔴 $ {_format_money_compact(abs(amount))}"
return "$ 0"