07.4.3.14 — Auto Trading UI. Realistic Pricing & Debug Live Tools
This commit is contained in:
@@ -60,12 +60,10 @@ async def open_auto(message: Message, state: FSMContext) -> None:
|
|||||||
|
|
||||||
AutoTradeRunner.set_current_screen("auto")
|
AutoTradeRunner.set_current_screen("auto")
|
||||||
|
|
||||||
current_state = AutoTradeService().get_state()
|
await AutoTradeRunner.delete_registered_screen(
|
||||||
if current_state.status in {"RUNNING", "OBSERVING"}:
|
bot=message.bot,
|
||||||
await AutoTradeRunner.delete_registered_screen(
|
chat_id=message.chat.id,
|
||||||
bot=message.bot,
|
)
|
||||||
chat_id=message.chat.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
await render_auto_screen(message, edit_mode=False)
|
await render_auto_screen(message, edit_mode=False)
|
||||||
|
|
||||||
|
|||||||
@@ -24,30 +24,45 @@ class AutoRiskStates(StatesGroup):
|
|||||||
waiting_max_loss = State()
|
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:
|
def _format_percent(value: float | None) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return "⚪ off"
|
return "off"
|
||||||
return f"🟢 {value:g}%"
|
return f"{_format_number(value)}%"
|
||||||
|
|
||||||
|
|
||||||
def _format_usd(value: float | None) -> str:
|
def _format_usd(value: float | None) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return "⚪ off"
|
return "off"
|
||||||
return f"🟢 {value:g} USD"
|
return f"{_format_number(value)} USD"
|
||||||
|
|
||||||
|
|
||||||
|
def _rule_icon(value: float | None) -> str:
|
||||||
|
return "✅" if value is not None else "⚠️"
|
||||||
|
|
||||||
|
|
||||||
def _risk_keyboard() -> InlineKeyboardMarkup:
|
def _risk_keyboard() -> InlineKeyboardMarkup:
|
||||||
state = AutoTradeService().get_state()
|
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
builder.button(text=f"🛑 Stop Loss", callback_data="auto:risk:set_sl")
|
builder.button(text="🛑 SL", callback_data="auto:risk:set_sl")
|
||||||
builder.button(text=f"🎯 Take Profit", callback_data="auto:risk:set_tp")
|
builder.button(text="🎯 TP", callback_data="auto:risk:set_tp")
|
||||||
builder.button(text=f"💸 Max Loss", callback_data="auto:risk:set_ml")
|
builder.button(text="💸 ML", 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="🤖 Автоторговля", callback_data="auto:home")
|
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()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
@@ -66,16 +81,13 @@ def _risk_text(status_message: str | None = None) -> str:
|
|||||||
status = "🟢 Активна" if active_count else "⚪ Выключена"
|
status = "🟢 Активна" if active_count else "⚪ Выключена"
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>⚠️ Risk Settings</b>\n\n"
|
"<b>🧯 Защита позиции</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
f"Статус защиты: {status}\n"
|
f"Статус защиты: {status}\n"
|
||||||
f"Активных правил: {active_count}/3\n\n"
|
f"Активных правил: {active_count}/3\n\n"
|
||||||
f"🛑 Stop Loss: {_format_percent(state.stop_loss_percent)}\n"
|
f"{_rule_icon(state.stop_loss_percent)} Stop Loss · {_format_percent(state.stop_loss_percent)}\n"
|
||||||
f"🎯 Take Profit: {_format_percent(state.take_profit_percent)}\n"
|
f"{_rule_icon(state.take_profit_percent)} Take Profit · {_format_percent(state.take_profit_percent)}\n"
|
||||||
f"💸 Max Loss: {_format_usd(state.max_loss_usd)}\n\n"
|
f"{_rule_icon(state.max_loss_usd)} Max Loss · {_format_usd(state.max_loss_usd)}\n"
|
||||||
"<b>Подсказка:</b>\n"
|
|
||||||
"Пример: <code>0.5</code>, <code>1</code>\n"
|
|
||||||
"Введите <code>0</code>, чтобы отключить параметр."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if status_message:
|
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:
|
def _parse_positive_or_none(raw_text: str | None) -> float | None:
|
||||||
value_text = (raw_text or "").strip().replace(",", ".")
|
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
|
return None
|
||||||
|
|
||||||
value = float(value_text)
|
value = float(value_text)
|
||||||
@@ -222,11 +234,11 @@ async def ask_stop_loss(callback: CallbackQuery, state: FSMContext) -> None:
|
|||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"<b>🛑 Stop Loss</b>\n\n"
|
"<b>Stop Loss</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
"Введите Stop Loss в процентах.\n"
|
"Введите Stop Loss в процентах.\n"
|
||||||
"Например: <code>2</code>\n\n"
|
"Например: <code>1</code>, <code>0.5</code>, <code>0,5</code>\n\n"
|
||||||
"Введите <code>0</code>, чтобы отключить."
|
"отключить параметр - <code>0</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -240,11 +252,11 @@ async def ask_take_profit(callback: CallbackQuery, state: FSMContext) -> None:
|
|||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"<b>🎯 Take Profit</b>\n\n"
|
"<b>Take Profit</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
"Введите Take Profit в процентах.\n"
|
"Введите Take Profit в процентах.\n"
|
||||||
"Например: <code>3</code>\n\n"
|
"Например: <code>2</code>, <code>1.5</code>, <code>1,5</code>\n\n"
|
||||||
"Введите <code>0</code>, чтобы отключить."
|
"отключить параметр - <code>0</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -258,11 +270,11 @@ async def ask_max_loss(callback: CallbackQuery, state: FSMContext) -> None:
|
|||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"<b>💸 Max Loss</b>\n\n"
|
"<b>Maximum Loss</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
"Введите максимальный paper-убыток в USD.\n"
|
"Введите максимальный paper-убыток в USD.\n"
|
||||||
"Например: <code>10</code>\n\n"
|
"Например: <code>100</code>, <code>50.5</code>, <code>50,5</code>\n\n"
|
||||||
"Введите <code>0</code>, чтобы отключить."
|
"отключить параметр - <code>0</code>"
|
||||||
)
|
)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -309,7 +321,7 @@ async def set_stop_loss(message: Message, state: FSMContext) -> None:
|
|||||||
try:
|
try:
|
||||||
value = _parse_positive_or_none(message.text)
|
value = _parse_positive_or_none(message.text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await message.answer("Введите число. Например: 2 или 0 для отключения.")
|
await message.answer("Введите число. Например: 1, 0.5 или 0 для отключения.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not _validate_percent(value):
|
if not _validate_percent(value):
|
||||||
@@ -333,7 +345,7 @@ async def set_take_profit(message: Message, state: FSMContext) -> None:
|
|||||||
try:
|
try:
|
||||||
value = _parse_positive_or_none(message.text)
|
value = _parse_positive_or_none(message.text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await message.answer("Введите число. Например: 3 или 0 для отключения.")
|
await message.answer("Введите число. Например: 2, 1.5 или 0 для отключения.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not _validate_percent(value):
|
if not _validate_percent(value):
|
||||||
@@ -357,7 +369,7 @@ async def set_max_loss(message: Message, state: FSMContext) -> None:
|
|||||||
try:
|
try:
|
||||||
value = _parse_positive_or_none(message.text)
|
value = _parse_positive_or_none(message.text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await message.answer("Введите число. Например: 10 или 0 для отключения.")
|
await message.answer("Введите число. Например: 100, 50.5 или 0 для отключения.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not _validate_max_loss(value):
|
if not _validate_max_loss(value):
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
from aiogram.types import InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
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
|
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:
|
def auto_keyboard() -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
@@ -27,18 +21,31 @@ def auto_keyboard() -> InlineKeyboardMarkup:
|
|||||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||||
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
|
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)
|
builder.adjust(3, 2)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
def is_auto_configured(state) -> bool:
|
def is_auto_configured(state) -> bool:
|
||||||
return bool(
|
if not state.symbol:
|
||||||
state.symbol
|
return False
|
||||||
and state.strategy
|
|
||||||
and state.risk_percent is not None
|
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:
|
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:
|
if state.position_side != "NONE" and state.entry_price is not None:
|
||||||
return _build_active_position_text(state)
|
return _build_active_position_text(state)
|
||||||
|
|
||||||
|
if state.status == "OFF":
|
||||||
|
return _build_stopped_without_position_text(state)
|
||||||
|
|
||||||
return _build_waiting_text(state)
|
return _build_waiting_text(state)
|
||||||
|
|
||||||
|
|
||||||
def _build_not_configured_text(state) -> str:
|
def _build_not_configured_text(state) -> str:
|
||||||
return (
|
symbol_ready = state.symbol is not None
|
||||||
"🤖 Автоторговля · ⚪ Не настроена\n"
|
strategy_ready = state.strategy is not None
|
||||||
f"{_account_mode_line()}\n\n"
|
risk_ready = state.risk_percent is not None
|
||||||
"Configuration required\n\n"
|
|
||||||
f"Pair: {_required_value(_asset_symbol(state.symbol))}\n"
|
symbol_icon = "" if symbol_ready else "⚠️"
|
||||||
f"Strategy: {_required_value(_strategy_short(state.strategy))}\n"
|
strategy_icon = "" if strategy_ready else "⚠️"
|
||||||
f"Position Risk: {_required_value(_risk_percent_text(state))}\n\n"
|
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]:
|
def _execution_block_lines(state) -> list[str]:
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
|
|
||||||
reason = getattr(state, "execution_block_reason", None)
|
reason = getattr(state, "execution_block_reason", None)
|
||||||
if reason:
|
if reason:
|
||||||
lines.append(f"🔴 Blocked: {reason}")
|
lines.append(f"Blocked · {reason}")
|
||||||
|
|
||||||
adjustment = getattr(state, "execution_size_adjustment_reason", None)
|
adjustment = getattr(state, "execution_size_adjustment_reason", None)
|
||||||
if adjustment == "MARGIN_LIMIT":
|
if adjustment == "MARGIN_LIMIT":
|
||||||
lines.append("🟠 Size adjusted by Max Reserved")
|
lines.append("Size adjusted by Max Reserved")
|
||||||
|
|
||||||
return lines
|
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)
|
size = _estimated_size(state, price)
|
||||||
|
|
||||||
if size is None or price is None:
|
if size is None or price is None or price <= 0:
|
||||||
return "Est. Margin: —"
|
return "<b>Собственные средства</b> · —"
|
||||||
|
|
||||||
leverage = state.leverage or 1.0
|
leverage = state.leverage or 1.0
|
||||||
if leverage <= 0:
|
if leverage <= 0:
|
||||||
return "Est. Margin: —"
|
return "<b>Собственные средства</b> · —"
|
||||||
|
|
||||||
notional = size * price
|
position_size_usd = size * price
|
||||||
reserved = notional / leverage
|
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:
|
def _market_snapshot(symbol: str | None) -> dict[str, object] | None:
|
||||||
max_percent = getattr(state, "max_reserved_balance_percent", None)
|
if not symbol:
|
||||||
|
return None
|
||||||
|
|
||||||
if max_percent is None or max_percent <= 0:
|
try:
|
||||||
return "Max Reserved: off"
|
return ExchangeService().get_market_snapshot(symbol)
|
||||||
|
except Exception:
|
||||||
max_reserved = PAPER_BALANCE_USD * (max_percent / 100)
|
return None
|
||||||
|
|
||||||
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:
|
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:
|
if not symbol:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -194,10 +318,32 @@ def _current_price(symbol: str | None) -> float | None:
|
|||||||
return 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:
|
def _target_risk_usd(state) -> float:
|
||||||
if state.risk_percent is None:
|
if state.risk_percent is None:
|
||||||
return 0.0
|
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:
|
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)
|
max_percent = getattr(state, "max_reserved_balance_percent", None)
|
||||||
if max_percent is None or max_percent <= 0:
|
if max_percent is None or max_percent <= 0:
|
||||||
return risk_size
|
return _round_size(risk_size)
|
||||||
|
|
||||||
leverage = state.leverage or 1.0
|
leverage = state.leverage or 1.0
|
||||||
if leverage <= 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_notional_usd = max_reserved_usd * leverage
|
||||||
max_size = max_notional_usd / price
|
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:
|
def _estimated_size_text(state, price: float | None) -> str:
|
||||||
size = _estimated_size(state, price)
|
size = _estimated_size(state, price)
|
||||||
if size is None or price is None:
|
if size is None or price is None:
|
||||||
return "Est. Size: —"
|
return "<b>Количество</b> · —"
|
||||||
|
|
||||||
notional = size * price
|
notional = size * price
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"Est. Size: {_format_crypto_size(size)} "
|
f"<b>Количество</b> · {_format_crypto_size(size)}\n"
|
||||||
f"{_asset_symbol(state.symbol)} ($ {_format_money0(notional)})"
|
f"<b>Размер позиции</b> · $ {_format_money_compact(notional)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _active_sl_line(state) -> str:
|
def _risk_summary_line(
|
||||||
if (
|
state,
|
||||||
state.stop_loss_percent is None
|
size: float | None,
|
||||||
or state.entry_price is None
|
*,
|
||||||
or state.position_size is None
|
entry_price_override: float | None = None,
|
||||||
):
|
) -> str:
|
||||||
return "<b>🛑 SL:</b> off"
|
entry_price = entry_price_override or state.entry_price
|
||||||
|
|
||||||
move = state.entry_price * (state.stop_loss_percent / 100)
|
sl = _risk_loss_text(
|
||||||
loss = move * state.position_size
|
percent=state.stop_loss_percent,
|
||||||
|
fixed_loss=None,
|
||||||
if state.position_side == "SHORT":
|
size=size,
|
||||||
price = state.entry_price + move
|
entry_price=entry_price,
|
||||||
else:
|
)
|
||||||
price = state.entry_price - move
|
tp = _risk_profit_text(
|
||||||
|
percent=state.take_profit_percent,
|
||||||
return (
|
size=size,
|
||||||
f"<b>🛑 SL:</b> {_format_percent2(state.stop_loss_percent)} · "
|
entry_price=entry_price,
|
||||||
f"-$ {_format_money0(loss)} ⇢ $ {_format_money0(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:
|
return " | ".join(items)
|
||||||
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:
|
def _risk_loss_text(
|
||||||
if (
|
*,
|
||||||
state.max_loss_usd is None
|
percent: float | None,
|
||||||
or state.entry_price is None
|
fixed_loss: float | None,
|
||||||
or state.position_size is None
|
size: float | None,
|
||||||
or state.position_size <= 0
|
entry_price: float | None,
|
||||||
):
|
) -> str:
|
||||||
return "<b>💣 ML:</b> off"
|
if fixed_loss is not None:
|
||||||
|
return f"-$ {_format_money_compact(abs(fixed_loss))}"
|
||||||
|
|
||||||
loss = abs(state.max_loss_usd)
|
if percent is None:
|
||||||
price_move = loss / state.position_size
|
return ""
|
||||||
|
|
||||||
if state.position_side == "SHORT":
|
if size is None or size <= 0 or entry_price is None or entry_price <= 0:
|
||||||
price = state.entry_price + price_move
|
loss = _target_loss_by_percent_stub(percent)
|
||||||
else:
|
return f"-$ {_format_money_compact(loss)}" if loss is not None else ""
|
||||||
price = state.entry_price - price_move
|
|
||||||
|
|
||||||
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:
|
def _risk_reward_line(state) -> str:
|
||||||
@@ -318,12 +484,82 @@ def _risk_reward_line(state) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
ratio = state.take_profit_percent / state.stop_loss_percent
|
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:
|
def _format_ratio_value(value: float) -> str:
|
||||||
if abs(value - round(value)) < 1e-9:
|
if abs(value - round(value)) < 1e-9:
|
||||||
return str(int(round(value)))
|
return str(int(round(value)))
|
||||||
|
|
||||||
return f"{value:.2f}".rstrip("0").rstrip(".")
|
return f"{value:.2f}".rstrip("0").rstrip(".")
|
||||||
|
|
||||||
|
|
||||||
@@ -338,16 +574,16 @@ def _status_text(status: str) -> str:
|
|||||||
|
|
||||||
def _decision_human_text(status: str) -> str:
|
def _decision_human_text(status: str) -> str:
|
||||||
mapping = {
|
mapping = {
|
||||||
"WAITING": "🟡 Ожидание сигнала",
|
"WAITING": "Ожидание сигнала",
|
||||||
"CONFIRMING": "🟠 Подтверждение сигнала",
|
"CONFIRMING": "Подтверждение сигнала",
|
||||||
"READY": "🟢 Сигнал готов",
|
"READY": "Сигнал готов",
|
||||||
"BLOCKED": "🔴 Сигнал заблокирован",
|
"BLOCKED": "Сигнал заблокирован",
|
||||||
}
|
}
|
||||||
return mapping.get(status, status)
|
return mapping.get(status, status)
|
||||||
|
|
||||||
|
|
||||||
def _account_mode_line() -> str:
|
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:
|
def _asset_symbol(symbol: str | None) -> str:
|
||||||
@@ -387,51 +623,90 @@ def _leverage_text(value: float | None) -> str:
|
|||||||
def _risk_percent_text(state) -> str:
|
def _risk_percent_text(state) -> str:
|
||||||
if state.risk_percent is None:
|
if state.risk_percent is None:
|
||||||
return "—"
|
return "—"
|
||||||
return f"{state.risk_percent:g}%"
|
|
||||||
|
|
||||||
|
return _format_percent(state.risk_percent)
|
||||||
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:
|
def _required_value(value: str) -> str:
|
||||||
if not value or value == "—":
|
if not value or value == "—":
|
||||||
return "required"
|
return "⏤"
|
||||||
|
|
||||||
return value
|
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:
|
def _signal_icon(signal: str | None) -> str:
|
||||||
mapping = {
|
mapping = {
|
||||||
"BUY": "🟢",
|
"BUY": "🟢",
|
||||||
"SELL": "🔴",
|
"SELL": "🔴",
|
||||||
"HOLD": "🟡",
|
"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:
|
def _format_money(value: float | int | None) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return "—"
|
return "—"
|
||||||
|
|
||||||
return format_usd_amount(float(value))
|
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:
|
if value is None:
|
||||||
return "—"
|
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:
|
def _format_usd_or_dash(value: float | None) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return "—"
|
return "—"
|
||||||
|
|
||||||
return f"$ {_format_money(value)}"
|
return f"$ {_format_money(value)}"
|
||||||
|
|
||||||
|
|
||||||
@@ -442,16 +717,24 @@ def _format_signed_usd(value: float | int | None) -> str:
|
|||||||
amount = float(value)
|
amount = float(value)
|
||||||
|
|
||||||
if amount > 0:
|
if amount > 0:
|
||||||
return f"+$ {_format_money(amount)}"
|
return f"+$ {_format_money_compact(amount)}"
|
||||||
|
|
||||||
if amount < 0:
|
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:
|
if value is None:
|
||||||
return "—"
|
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"
|
||||||
@@ -2,14 +2,20 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
from src.core.config import load_settings
|
from src.core.config import load_settings
|
||||||
|
from src.integrations.exchange.service import ExchangeService
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
from src.trading.auto.runner import AutoTradeRunner
|
||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.auto.service import AutoTradeService
|
||||||
from src.trading.execution.engine import ExecutionEngine
|
from src.trading.execution.engine import ExecutionEngine
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
from src.trading.position.state import PositionState
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="debug")
|
router = Router(name="debug")
|
||||||
@@ -19,6 +25,566 @@ def _debug_enabled() -> bool:
|
|||||||
return load_settings().debug_enabled
|
return load_settings().debug_enabled
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_help_text() -> str:
|
||||||
|
return (
|
||||||
|
"<b>🧪 Debug commands</b>\n\n"
|
||||||
|
"<b>Auto UI states:</b>\n"
|
||||||
|
"/debug_auto off\n"
|
||||||
|
"/debug_auto hold 335\n"
|
||||||
|
"/debug_auto buy 12 0.74\n"
|
||||||
|
"/debug_auto buy_ready 0.88\n"
|
||||||
|
"/debug_auto sell 9 0.71\n"
|
||||||
|
"/debug_auto sell_ready 0.91\n"
|
||||||
|
"/debug_auto long\n"
|
||||||
|
"/debug_auto short\n"
|
||||||
|
"/debug_auto reset\n"
|
||||||
|
"/debug_auto state\n\n"
|
||||||
|
"<b>Paper execution:</b>\n"
|
||||||
|
"/debug_exec buy — открыть LONG\n"
|
||||||
|
"/debug_exec sell — открыть SHORT\n"
|
||||||
|
"/debug_exec flip — перевернуть текущую позицию\n"
|
||||||
|
"/debug_exec flip_buy — перевернуть в LONG\n"
|
||||||
|
"/debug_exec flip_sell — перевернуть в SHORT\n"
|
||||||
|
"/debug_exec close — закрыть позицию\n"
|
||||||
|
"/debug_exec state — состояние позиции\n\n"
|
||||||
|
"<b>Live paper test:</b>\n"
|
||||||
|
"/debug_live buy — открыть LONG и запустить мониторинг\n"
|
||||||
|
"/debug_live sell — открыть SHORT и запустить мониторинг\n"
|
||||||
|
"/debug_live flip — перевернуть текущую позицию и продолжить мониторинг\n"
|
||||||
|
"/debug_live close — закрыть позицию\n"
|
||||||
|
"/debug_live stop — остановить мониторинг, позицию не закрывать\n"
|
||||||
|
"/debug_live state — состояние live paper test\n\n"
|
||||||
|
"<b>Legacy:</b>\n"
|
||||||
|
"/debug_signal BUY 0.95 3\n"
|
||||||
|
"/debug_signal SELL 0.70 2\n"
|
||||||
|
"/debug_signal HOLD 0.00 1\n"
|
||||||
|
"/debug_ready\n"
|
||||||
|
"/debug_state"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "/debug_help")
|
||||||
|
async def debug_help(message: Message) -> None:
|
||||||
|
if not _debug_enabled():
|
||||||
|
await message.answer("Debug mode выключен.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer(_debug_help_text())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text.startswith("/debug_auto"))
|
||||||
|
async def debug_auto(message: Message) -> None:
|
||||||
|
if not _debug_enabled():
|
||||||
|
await message.answer("Debug mode выключен.")
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = (message.text or "").split()
|
||||||
|
command = parts[1].lower() if len(parts) > 1 else "help"
|
||||||
|
|
||||||
|
service = AutoTradeService()
|
||||||
|
state = service.get_state()
|
||||||
|
|
||||||
|
if command in {"help", "-h", "--help"}:
|
||||||
|
await message.answer(_debug_help_text())
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "off":
|
||||||
|
_clear_debug_position(state)
|
||||||
|
state.status = "OFF"
|
||||||
|
state.decision_status = "WAITING"
|
||||||
|
state.last_signal = "HOLD"
|
||||||
|
state.last_signal_confidence = 0.0
|
||||||
|
state.last_signal_repeat_count = 1
|
||||||
|
state.is_signal_confirmed = False
|
||||||
|
state.is_signal_ready = False
|
||||||
|
_set_signal_started_at(state)
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer("✅ Debug Auto: OFF")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "reset":
|
||||||
|
_clear_debug_position(state)
|
||||||
|
state.status = "RUNNING"
|
||||||
|
state.decision_status = "WAITING"
|
||||||
|
state.decision_reason = None
|
||||||
|
state.last_signal = "HOLD"
|
||||||
|
state.last_signal_reason = "DEBUG RESET HOLD"
|
||||||
|
state.last_signal_confidence = 0.0
|
||||||
|
state.last_signal_repeat_count = 1
|
||||||
|
state.is_signal_confirmed = False
|
||||||
|
state.is_signal_ready = False
|
||||||
|
state.execution_block_reason = None
|
||||||
|
state.execution_size_adjustment_reason = None
|
||||||
|
_set_signal_started_at(state)
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer("✅ Debug Auto: reset to RUNNING HOLD")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "state":
|
||||||
|
_sync_state_from_position(state)
|
||||||
|
await message.answer(_debug_state_text(state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "hold":
|
||||||
|
seconds = _parse_int(parts, index=2, default=335)
|
||||||
|
_clear_debug_position(state)
|
||||||
|
_set_signal_state(
|
||||||
|
state=state,
|
||||||
|
signal="HOLD",
|
||||||
|
seconds=seconds,
|
||||||
|
confidence=0.0,
|
||||||
|
decision_status="WAITING",
|
||||||
|
ready=False,
|
||||||
|
)
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer(f"✅ Debug Auto: HOLD {seconds}s")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "buy":
|
||||||
|
seconds = _parse_int(parts, index=2, default=12)
|
||||||
|
confidence = _parse_float(parts, index=3, default=0.74)
|
||||||
|
_clear_debug_position(state)
|
||||||
|
_set_signal_state(
|
||||||
|
state=state,
|
||||||
|
signal="BUY",
|
||||||
|
seconds=seconds,
|
||||||
|
confidence=confidence,
|
||||||
|
decision_status="CONFIRMING",
|
||||||
|
ready=False,
|
||||||
|
)
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer(f"✅ Debug Auto: BUY {seconds}s confidence={confidence:.2f}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "buy_ready":
|
||||||
|
confidence = _parse_float(parts, index=2, default=0.88)
|
||||||
|
_clear_debug_position(state)
|
||||||
|
_set_signal_state(
|
||||||
|
state=state,
|
||||||
|
signal="BUY",
|
||||||
|
seconds=15,
|
||||||
|
confidence=confidence,
|
||||||
|
decision_status="READY",
|
||||||
|
ready=True,
|
||||||
|
)
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer(f"✅ Debug Auto: BUY READY confidence={confidence:.2f}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "sell":
|
||||||
|
seconds = _parse_int(parts, index=2, default=9)
|
||||||
|
confidence = _parse_float(parts, index=3, default=0.71)
|
||||||
|
_clear_debug_position(state)
|
||||||
|
_set_signal_state(
|
||||||
|
state=state,
|
||||||
|
signal="SELL",
|
||||||
|
seconds=seconds,
|
||||||
|
confidence=confidence,
|
||||||
|
decision_status="CONFIRMING",
|
||||||
|
ready=False,
|
||||||
|
)
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer(f"✅ Debug Auto: SELL {seconds}s confidence={confidence:.2f}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "sell_ready":
|
||||||
|
confidence = _parse_float(parts, index=2, default=0.91)
|
||||||
|
_clear_debug_position(state)
|
||||||
|
_set_signal_state(
|
||||||
|
state=state,
|
||||||
|
signal="SELL",
|
||||||
|
seconds=15,
|
||||||
|
confidence=confidence,
|
||||||
|
decision_status="READY",
|
||||||
|
ready=True,
|
||||||
|
)
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer(f"✅ Debug Auto: SELL READY confidence={confidence:.2f}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "long":
|
||||||
|
_set_debug_position(state=state, side="LONG")
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer("✅ Debug Auto: active LONG position")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "short":
|
||||||
|
_set_debug_position(state=state, side="SHORT")
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer("✅ Debug Auto: active SHORT position")
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text.startswith("/debug_exec"))
|
||||||
|
async def debug_exec(message: Message) -> None:
|
||||||
|
if not _debug_enabled():
|
||||||
|
await message.answer("Debug mode выключен.")
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = (message.text or "").split()
|
||||||
|
command = parts[1].lower() if len(parts) > 1 else "help"
|
||||||
|
|
||||||
|
service = AutoTradeService()
|
||||||
|
state = service.get_state()
|
||||||
|
engine = ExecutionEngine()
|
||||||
|
|
||||||
|
if command in {"help", "-h", "--help"}:
|
||||||
|
await message.answer(_debug_help_text())
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "state":
|
||||||
|
_sync_state_from_position(state)
|
||||||
|
await message.answer(_debug_state_text(state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "buy":
|
||||||
|
_prepare_ready_signal(state=state, signal="BUY", confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _after_debug_execution()
|
||||||
|
await message.answer(_execution_result_text("BUY execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "sell":
|
||||||
|
_prepare_ready_signal(state=state, signal="SELL", confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _after_debug_execution()
|
||||||
|
await message.answer(_execution_result_text("SELL execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "flip":
|
||||||
|
position = engine.get_position()
|
||||||
|
current_side = position.side or state.position_side or "NONE"
|
||||||
|
|
||||||
|
if current_side == "LONG":
|
||||||
|
target_signal = "SELL"
|
||||||
|
elif current_side == "SHORT":
|
||||||
|
target_signal = "BUY"
|
||||||
|
else:
|
||||||
|
await message.answer(
|
||||||
|
"⛔️ Flip невозможен: нет открытой позиции.\n\n"
|
||||||
|
"Сначала выполните /debug_exec buy или /debug_exec sell."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_prepare_ready_signal(state=state, signal=target_signal, confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _after_debug_execution()
|
||||||
|
await message.answer(_execution_result_text("AUTO FLIP execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "flip_buy":
|
||||||
|
_prepare_ready_signal(state=state, signal="BUY", confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _after_debug_execution()
|
||||||
|
await message.answer(_execution_result_text("FLIP to LONG execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "flip_sell":
|
||||||
|
_prepare_ready_signal(state=state, signal="SELL", confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _after_debug_execution()
|
||||||
|
await message.answer(_execution_result_text("FLIP to SHORT execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "close":
|
||||||
|
result = engine._close_position(state, forced_reason="DEBUG_CLOSE")
|
||||||
|
await _after_debug_execution()
|
||||||
|
await message.answer(_execution_result_text("CLOSE execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text.startswith("/debug_live"))
|
||||||
|
async def debug_live(message: Message) -> None:
|
||||||
|
if not _debug_enabled():
|
||||||
|
await message.answer("Debug mode выключен.")
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = (message.text or "").split()
|
||||||
|
command = parts[1].lower() if len(parts) > 1 else "help"
|
||||||
|
|
||||||
|
service = AutoTradeService()
|
||||||
|
state = service.get_state()
|
||||||
|
engine = ExecutionEngine()
|
||||||
|
|
||||||
|
if command in {"help", "-h", "--help"}:
|
||||||
|
await message.answer(_debug_help_text())
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "buy":
|
||||||
|
_prepare_ready_signal(state=state, signal="BUY", confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _start_live_monitoring()
|
||||||
|
await message.answer(_execution_result_text("LIVE BUY execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "sell":
|
||||||
|
_prepare_ready_signal(state=state, signal="SELL", confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _start_live_monitoring()
|
||||||
|
await message.answer(_execution_result_text("LIVE SELL execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "flip":
|
||||||
|
position = engine.get_position()
|
||||||
|
current_side = position.side or state.position_side or "NONE"
|
||||||
|
|
||||||
|
if current_side == "LONG":
|
||||||
|
target_signal = "SELL"
|
||||||
|
elif current_side == "SHORT":
|
||||||
|
target_signal = "BUY"
|
||||||
|
else:
|
||||||
|
await message.answer(
|
||||||
|
"⛔️ Live flip невозможен: нет открытой позиции.\n\n"
|
||||||
|
"Сначала выполните /debug_live buy или /debug_live sell."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_prepare_ready_signal(state=state, signal=target_signal, confidence=0.95)
|
||||||
|
result = engine.process(state)
|
||||||
|
await _start_live_monitoring()
|
||||||
|
await message.answer(_execution_result_text("LIVE AUTO FLIP execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "close":
|
||||||
|
result = engine._close_position(state, forced_reason="DEBUG_LIVE_CLOSE")
|
||||||
|
await _after_debug_execution()
|
||||||
|
await message.answer(_execution_result_text("LIVE CLOSE execution", result, state))
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "stop":
|
||||||
|
AutoTradeRunner.stop()
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
await message.answer("✅ Debug live stopped. Позиция не закрыта.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if command == "state":
|
||||||
|
_sync_state_from_position(state)
|
||||||
|
await message.answer(_debug_state_text(state))
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_ready_signal(*, state, signal: str, confidence: float) -> None:
|
||||||
|
state.status = "RUNNING"
|
||||||
|
state.last_signal = signal
|
||||||
|
state.last_signal_confidence = max(0.0, min(1.0, confidence))
|
||||||
|
state.last_signal_repeat_count = 3
|
||||||
|
state.last_signal_reason = f"DEBUG EXEC {signal}"
|
||||||
|
state.decision_status = "READY"
|
||||||
|
state.decision_reason = "DEBUG EXEC READY"
|
||||||
|
state.is_signal_confirmed = True
|
||||||
|
state.is_signal_ready = True
|
||||||
|
state.execution_block_reason = None
|
||||||
|
state.execution_size_adjustment_reason = None
|
||||||
|
_set_signal_started_at(state)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_signal_state(
|
||||||
|
*,
|
||||||
|
state,
|
||||||
|
signal: str,
|
||||||
|
seconds: int,
|
||||||
|
confidence: float,
|
||||||
|
decision_status: str,
|
||||||
|
ready: bool,
|
||||||
|
) -> None:
|
||||||
|
state.status = "RUNNING"
|
||||||
|
state.last_signal = signal
|
||||||
|
state.last_signal_confidence = max(0.0, min(1.0, confidence))
|
||||||
|
state.last_signal_repeat_count = _seconds_to_repeats(seconds)
|
||||||
|
state.last_signal_reason = f"DEBUG {signal} {seconds}s"
|
||||||
|
state.decision_status = decision_status
|
||||||
|
state.decision_reason = f"DEBUG {decision_status}"
|
||||||
|
state.is_signal_confirmed = ready
|
||||||
|
state.is_signal_ready = ready
|
||||||
|
state.execution_block_reason = None
|
||||||
|
state.execution_size_adjustment_reason = None
|
||||||
|
_set_signal_started_at(state, seconds_ago=seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_signal_started_at(state, *, seconds_ago: int = 0) -> None:
|
||||||
|
if hasattr(state, "signal_started_at"):
|
||||||
|
state.signal_started_at = time.monotonic() - max(0, seconds_ago)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_debug_position(*, state, side: str) -> None:
|
||||||
|
state.status = "RUNNING"
|
||||||
|
state.last_signal = "BUY" if side == "LONG" else "SELL"
|
||||||
|
state.last_signal_confidence = 0.90
|
||||||
|
state.last_signal_repeat_count = 3
|
||||||
|
state.decision_status = "READY"
|
||||||
|
state.is_signal_confirmed = True
|
||||||
|
state.is_signal_ready = True
|
||||||
|
_set_signal_started_at(state, seconds_ago=15)
|
||||||
|
|
||||||
|
entry_price = _debug_entry_price(state.symbol, side)
|
||||||
|
size = _debug_size_for_notional(entry_price, notional=1000.0)
|
||||||
|
now = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
position = PositionState(
|
||||||
|
side=side,
|
||||||
|
symbol=state.symbol,
|
||||||
|
entry_price=entry_price,
|
||||||
|
size=size,
|
||||||
|
leverage=state.leverage,
|
||||||
|
unrealized_pnl_usd=0.0,
|
||||||
|
opened_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
ExecutionEngine._position = position
|
||||||
|
_sync_state_from_position(state)
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_debug_position(state) -> None:
|
||||||
|
ExecutionEngine._position = PositionState()
|
||||||
|
|
||||||
|
state.position_side = "NONE"
|
||||||
|
state.entry_price = None
|
||||||
|
state.position_size = None
|
||||||
|
state.unrealized_pnl_usd = None
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_state_from_position(state) -> None:
|
||||||
|
position = ExecutionEngine().get_position()
|
||||||
|
|
||||||
|
state.position_side = position.side
|
||||||
|
state.entry_price = position.entry_price
|
||||||
|
state.position_size = position.size
|
||||||
|
state.unrealized_pnl_usd = position.unrealized_pnl_usd
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_entry_price(symbol: str, side: str) -> float:
|
||||||
|
try:
|
||||||
|
snapshot = ExchangeService().get_market_snapshot(symbol)
|
||||||
|
|
||||||
|
if side == "LONG":
|
||||||
|
return float(snapshot.get("ask_price") or snapshot.get("last_price"))
|
||||||
|
|
||||||
|
if side == "SHORT":
|
||||||
|
return float(snapshot.get("bid_price") or snapshot.get("last_price"))
|
||||||
|
|
||||||
|
return float(snapshot.get("last_price"))
|
||||||
|
except Exception:
|
||||||
|
return 100000.0
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_size_for_notional(entry_price: float, *, notional: float) -> float:
|
||||||
|
if entry_price <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
value = notional / entry_price
|
||||||
|
factor = 10**5
|
||||||
|
return math.floor(value * factor) / factor
|
||||||
|
|
||||||
|
|
||||||
|
def _seconds_to_repeats(seconds: int) -> int:
|
||||||
|
return max(1, math.ceil(max(0, seconds) / 5))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_int(parts: list[str], *, index: int, default: int) -> int:
|
||||||
|
try:
|
||||||
|
return int(parts[index])
|
||||||
|
except (IndexError, TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_float(parts: list[str], *, index: int, default: float) -> float:
|
||||||
|
try:
|
||||||
|
return float(parts[index])
|
||||||
|
except (IndexError, TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
async def _refresh_auto_screen() -> None:
|
||||||
|
AutoTradeRunner.set_current_screen("auto")
|
||||||
|
AutoTradeRunner._last_text = None
|
||||||
|
await AutoTradeRunner._refresh_screen(force=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def _start_live_monitoring() -> None:
|
||||||
|
state = AutoTradeService().get_state()
|
||||||
|
|
||||||
|
state.status = "RUNNING"
|
||||||
|
_sync_state_from_position(state)
|
||||||
|
|
||||||
|
AutoTradeRunner.set_current_screen("auto")
|
||||||
|
AutoTradeRunner._last_text = None
|
||||||
|
|
||||||
|
await AutoTradeRunner.process_last_event_now()
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
|
||||||
|
AutoTradeRunner.start()
|
||||||
|
|
||||||
|
|
||||||
|
async def _after_debug_execution() -> None:
|
||||||
|
state = AutoTradeService().get_state()
|
||||||
|
|
||||||
|
_sync_state_from_position(state)
|
||||||
|
|
||||||
|
AutoTradeRunner.set_current_screen("auto")
|
||||||
|
AutoTradeRunner._last_text = None
|
||||||
|
|
||||||
|
await AutoTradeRunner.process_last_event_now()
|
||||||
|
await _refresh_auto_screen()
|
||||||
|
|
||||||
|
|
||||||
|
def _execution_result_text(title: str, result, state) -> str:
|
||||||
|
_sync_state_from_position(state)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"✅ Debug {title}\n\n"
|
||||||
|
f"Action: {result.action}\n"
|
||||||
|
f"Can execute: {result.can_execute}\n"
|
||||||
|
f"Reason: {result.reason}\n\n"
|
||||||
|
f"Signal: {state.last_signal}\n"
|
||||||
|
f"Decision: {state.decision_status}\n\n"
|
||||||
|
f"Position: {state.position_side}\n"
|
||||||
|
f"Entry: {state.entry_price}\n"
|
||||||
|
f"Size: {state.position_size}\n"
|
||||||
|
f"PnL: {state.unrealized_pnl_usd}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_state_text(state) -> str:
|
||||||
|
runner_task_running = (
|
||||||
|
AutoTradeRunner._task is not None
|
||||||
|
and not AutoTradeRunner._task.done()
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
"<b>Debug Auto State</b>\n\n"
|
||||||
|
f"Status: {state.status}\n"
|
||||||
|
f"Symbol: {state.symbol}\n"
|
||||||
|
f"Strategy: {state.strategy}\n"
|
||||||
|
f"Risk: {state.risk_percent}\n"
|
||||||
|
f"Leverage: {state.leverage}\n\n"
|
||||||
|
f"Signal: {state.last_signal}\n"
|
||||||
|
f"Repeats: {state.last_signal_repeat_count}\n"
|
||||||
|
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
||||||
|
f"Decision: {state.decision_status}\n"
|
||||||
|
f"Ready: {state.is_signal_ready}\n"
|
||||||
|
f"Signal started at: {getattr(state, 'signal_started_at', None)}\n\n"
|
||||||
|
f"<b>Runner</b>\n"
|
||||||
|
f"Screen: {AutoTradeRunner._current_screen}\n"
|
||||||
|
f"Chat ID: {AutoTradeRunner._chat_id}\n"
|
||||||
|
f"Message ID: {AutoTradeRunner._message_id}\n"
|
||||||
|
f"Has bot: {AutoTradeRunner._bot is not None}\n"
|
||||||
|
f"Has render_text: {AutoTradeRunner._render_text is not None}\n"
|
||||||
|
f"Task running: {runner_task_running}\n\n"
|
||||||
|
f"<b>Position</b>\n"
|
||||||
|
f"Side: {state.position_side}\n"
|
||||||
|
f"Entry: {state.entry_price}\n"
|
||||||
|
f"Size: {state.position_size}\n"
|
||||||
|
f"PnL: {state.unrealized_pnl_usd}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str | None]:
|
def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str | None]:
|
||||||
parts = (raw_text or "").split()
|
parts = (raw_text or "").split()
|
||||||
|
|
||||||
@@ -45,34 +611,6 @@ def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str
|
|||||||
return signal, confidence, repeat_count, None
|
return signal, confidence, repeat_count, None
|
||||||
|
|
||||||
|
|
||||||
def _debug_help_text() -> str:
|
|
||||||
return (
|
|
||||||
"<b>🧪 Debug commands</b>\n\n"
|
|
||||||
"<b>Основная команда:</b>\n"
|
|
||||||
"/debug_signal BUY 0.95 3\n"
|
|
||||||
"/debug_signal SELL 0.70 2\n"
|
|
||||||
"/debug_signal HOLD 0.00 1\n\n"
|
|
||||||
"<b>Быстрые команды:</b>\n"
|
|
||||||
"/debug_signal — BUY 0.90 2\n"
|
|
||||||
"/debug_ready — READY BUY\n"
|
|
||||||
"/debug_state — текущее состояние\n"
|
|
||||||
"/debug_help — список команд\n\n"
|
|
||||||
"<b>Priority тест:</b>\n"
|
|
||||||
"HIGH: confidence >= 0.80 и repeats >= 3\n"
|
|
||||||
"MEDIUM: confidence >= 0.60 или repeats >= 2\n"
|
|
||||||
"LOW: всё остальное"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text == "/debug_help")
|
|
||||||
async def debug_help(message: Message) -> None:
|
|
||||||
if not _debug_enabled():
|
|
||||||
await message.answer("Debug mode выключен.")
|
|
||||||
return
|
|
||||||
|
|
||||||
await message.answer(_debug_help_text())
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text.startswith("/debug_signal"))
|
@router.message(F.text.startswith("/debug_signal"))
|
||||||
async def debug_signal(message: Message) -> None:
|
async def debug_signal(message: Message) -> None:
|
||||||
if not _debug_enabled():
|
if not _debug_enabled():
|
||||||
@@ -82,10 +620,7 @@ async def debug_signal(message: Message) -> None:
|
|||||||
signal, confidence, repeat_count, error = _parse_debug_signal_args(message.text)
|
signal, confidence, repeat_count, error = _parse_debug_signal_args(message.text)
|
||||||
|
|
||||||
if error is not None:
|
if error is not None:
|
||||||
await message.answer(
|
await message.answer(f"⛔️ {error}\n\n{_debug_help_text()}")
|
||||||
f"⛔️ {error}\n\n"
|
|
||||||
f"{_debug_help_text()}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
service = AutoTradeService()
|
service = AutoTradeService()
|
||||||
@@ -99,13 +634,8 @@ async def debug_signal(message: Message) -> None:
|
|||||||
if state.status == "OFF":
|
if state.status == "OFF":
|
||||||
state.status = "RUNNING"
|
state.status = "RUNNING"
|
||||||
|
|
||||||
await AutoTradeRunner._handle_important_event(state)
|
_set_signal_started_at(state)
|
||||||
|
await _refresh_auto_screen()
|
||||||
execution_result = ExecutionEngine().process(state)
|
|
||||||
|
|
||||||
await AutoTradeRunner.process_last_event_now()
|
|
||||||
|
|
||||||
AutoTradeRunner.start()
|
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
event_type="debug_signal_forced",
|
event_type="debug_signal_forced",
|
||||||
@@ -119,9 +649,6 @@ async def debug_signal(message: Message) -> None:
|
|||||||
"decision_status": state.decision_status,
|
"decision_status": state.decision_status,
|
||||||
"confidence": state.last_signal_confidence,
|
"confidence": state.last_signal_confidence,
|
||||||
"repeat_count": state.last_signal_repeat_count,
|
"repeat_count": state.last_signal_repeat_count,
|
||||||
"execution_action": execution_result.action,
|
|
||||||
"execution_can_execute": execution_result.can_execute,
|
|
||||||
"execution_reason": execution_result.reason,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -130,10 +657,7 @@ async def debug_signal(message: Message) -> None:
|
|||||||
f"Signal: {state.last_signal}\n"
|
f"Signal: {state.last_signal}\n"
|
||||||
f"Decision: {state.decision_status}\n"
|
f"Decision: {state.decision_status}\n"
|
||||||
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
||||||
f"Repeats: {state.last_signal_repeat_count}\n\n"
|
f"Repeats: {state.last_signal_repeat_count}"
|
||||||
f"Execution: {execution_result.action}\n"
|
|
||||||
f"Can execute: {execution_result.can_execute}\n"
|
|
||||||
f"Reason: {execution_result.reason}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -144,30 +668,25 @@ async def debug_ready(message: Message) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
service = AutoTradeService()
|
service = AutoTradeService()
|
||||||
state = service.debug_force_signal(
|
state = service.get_state()
|
||||||
|
|
||||||
|
_clear_debug_position(state)
|
||||||
|
_set_signal_state(
|
||||||
|
state=state,
|
||||||
signal="BUY",
|
signal="BUY",
|
||||||
|
seconds=15,
|
||||||
confidence=0.95,
|
confidence=0.95,
|
||||||
repeat_count=3,
|
decision_status="READY",
|
||||||
reason="DEBUG READY BUY 0.95 ×3",
|
ready=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if state.status == "OFF":
|
await _refresh_auto_screen()
|
||||||
state.status = "RUNNING"
|
|
||||||
|
|
||||||
await AutoTradeRunner._handle_important_event(state)
|
|
||||||
|
|
||||||
execution_result = ExecutionEngine().process(state)
|
|
||||||
|
|
||||||
await AutoTradeRunner.process_last_event_now()
|
|
||||||
|
|
||||||
AutoTradeRunner.start()
|
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"✅ Debug READY создан\n\n"
|
"✅ Debug READY создан\n\n"
|
||||||
f"Signal: {state.last_signal}\n"
|
f"Signal: {state.last_signal}\n"
|
||||||
f"Decision: {state.decision_status}\n"
|
f"Decision: {state.decision_status}\n"
|
||||||
f"Execution: {execution_result.action}\n"
|
f"Confidence: {state.last_signal_confidence:.2f}"
|
||||||
f"Can execute: {execution_result.can_execute}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,19 +697,5 @@ async def debug_state(message: Message) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
state = AutoTradeService().get_state()
|
state = AutoTradeService().get_state()
|
||||||
|
_sync_state_from_position(state)
|
||||||
await message.answer(
|
await message.answer(_debug_state_text(state))
|
||||||
"<b>Debug Auto State</b>\n\n"
|
|
||||||
f"Status: {state.status}\n"
|
|
||||||
f"Symbol: {state.symbol}\n"
|
|
||||||
f"Strategy: {state.strategy}\n"
|
|
||||||
f"Risk: {state.risk_percent}\n"
|
|
||||||
f"Leverage: {state.leverage}\n\n"
|
|
||||||
f"Signal: {state.last_signal}\n"
|
|
||||||
f"Repeats: {state.last_signal_repeat_count}\n"
|
|
||||||
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
|
||||||
f"Decision: {state.decision_status}\n\n"
|
|
||||||
f"Position: {state.position_side}\n"
|
|
||||||
f"Entry: {state.entry_price}\n"
|
|
||||||
f"PnL: {state.unrealized_pnl_usd}"
|
|
||||||
)
|
|
||||||
@@ -181,9 +181,9 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
state = AutoTradeService().get_state()
|
state = AutoTradeService().get_state()
|
||||||
|
|
||||||
strategy_map = {
|
strategy_map = {
|
||||||
"TREND": "📈 Trend Following",
|
"TREND": "TREND FOLLOWING",
|
||||||
"GRID": "🧩 Grid Trading",
|
"GRID": "GRID TRADING",
|
||||||
"SCALP": "⚡ Scalping",
|
"SCALP": "SCALPING",
|
||||||
}
|
}
|
||||||
|
|
||||||
strategy_ready = state.strategy is not None
|
strategy_ready = state.strategy is not None
|
||||||
@@ -191,10 +191,37 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
risk_ready = state.risk_percent is not None
|
risk_ready = state.risk_percent is not None
|
||||||
leverage_ready = state.leverage is not None
|
leverage_ready = state.leverage is not None
|
||||||
|
|
||||||
is_configured = strategy_ready and symbol_ready and risk_ready and leverage_ready
|
is_trend_strategy = (state.strategy or "").upper() == "TREND"
|
||||||
|
sl_ready = (
|
||||||
|
state.stop_loss_percent is not None
|
||||||
|
and state.stop_loss_percent > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
is_configured = (
|
||||||
|
strategy_ready
|
||||||
|
and symbol_ready
|
||||||
|
and risk_ready
|
||||||
|
and leverage_ready
|
||||||
|
and (not is_trend_strategy or sl_ready)
|
||||||
|
)
|
||||||
|
|
||||||
strategy = strategy_map.get(state.strategy or "", "—")
|
strategy = strategy_map.get(state.strategy or "", "—")
|
||||||
symbol = state.symbol or "—"
|
|
||||||
|
symbol = "—"
|
||||||
|
|
||||||
|
if state.symbol:
|
||||||
|
base = state.symbol.split("_", 1)[0].upper()
|
||||||
|
|
||||||
|
if "/" in base:
|
||||||
|
symbol = base.split("/", 1)[0]
|
||||||
|
else:
|
||||||
|
for suffix in ("USDT", "USD", "EUR", "BTC"):
|
||||||
|
if base.endswith(suffix) and len(base) > len(suffix):
|
||||||
|
base = base[: -len(suffix)]
|
||||||
|
break
|
||||||
|
|
||||||
|
symbol = base
|
||||||
|
|
||||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
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 "—"
|
leverage = f"x{state.leverage:g}" if state.leverage is not None else "—"
|
||||||
max_reserved = (
|
max_reserved = (
|
||||||
@@ -202,44 +229,70 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
if state.max_reserved_balance_percent is not None
|
if state.max_reserved_balance_percent is not None
|
||||||
else "off"
|
else "off"
|
||||||
)
|
)
|
||||||
sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
|
sl = (
|
||||||
tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
|
f"{state.stop_loss_percent:g}%"
|
||||||
ml = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
|
if state.stop_loss_percent is not None
|
||||||
risk_controls = f"SL {sl} · TP {tp} · ML {ml}"
|
else "off"
|
||||||
|
)
|
||||||
|
|
||||||
strategy_icon = "✅" if strategy_ready else "👉"
|
tp = (
|
||||||
symbol_icon = "✅" if symbol_ready else "👉"
|
f"{state.take_profit_percent:g}%"
|
||||||
risk_icon = "✅" if risk_ready else "👉"
|
if state.take_profit_percent is not None
|
||||||
leverage_icon = "✅" if leverage_ready else "👉"
|
else "off"
|
||||||
|
)
|
||||||
|
|
||||||
|
ml = (
|
||||||
|
f"{state.max_loss_usd:g} USD"
|
||||||
|
if state.max_loss_usd is not None
|
||||||
|
else "off"
|
||||||
|
)
|
||||||
|
|
||||||
|
strategy_icon = "✅" if strategy_ready else "⚠️"
|
||||||
|
symbol_icon = "✅" if symbol_ready else "⚠️"
|
||||||
|
risk_icon = "✅" if risk_ready else "⚠️"
|
||||||
|
leverage_icon = "✅" if leverage_ready else "⚠️"
|
||||||
|
sl_icon = "✅" if sl_ready else "⚠️"
|
||||||
|
|
||||||
|
if is_trend_strategy:
|
||||||
|
risk_controls_block = (
|
||||||
|
"<b>Защита позиции:</b>\n"
|
||||||
|
f"{sl_icon} Stop Loss · <b>{'required' if not sl_ready else sl}</b>\n"
|
||||||
|
f"✅ Take Profit · {tp}\n"
|
||||||
|
f"✅ Max Loss · {ml}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
risk_controls_block = (
|
||||||
|
"<b>Защита позиции:</b>\n"
|
||||||
|
f"✅ Stop Loss · {sl}\n"
|
||||||
|
f"✅ Take Profit · {tp}\n"
|
||||||
|
f"✅ Max Loss · {ml}"
|
||||||
|
)
|
||||||
|
|
||||||
config_status = (
|
config_status = (
|
||||||
"✅ Все параметры настроены"
|
"✅ Все параметры настроены"
|
||||||
if is_configured
|
if is_configured
|
||||||
else "⛔️ Настрой все параметры"
|
else "⚠️ Настрой все параметры"
|
||||||
)
|
)
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>🤖 Автоторговля</b>\n\n"
|
"<b>🤖 Автоторговля</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки\n\n"
|
"<b>СИСТЕМА</b> · Настройки\n\n"
|
||||||
f"{strategy_icon} Стратегия: {strategy}\n"
|
f"{strategy_icon} Стратегия: <b>{strategy}</b>\n"
|
||||||
f"{symbol_icon} Инструмент: {symbol}\n"
|
f"{symbol_icon} Актив: <b>{symbol}</b>\n"
|
||||||
f"{risk_icon} Риск на сделку: {risk}\n"
|
f"{risk_icon} Риск на сделку: <b>{risk}</b>\n"
|
||||||
f"{leverage_icon} Плечо: {leverage}\n\n"
|
f"{leverage_icon} Плечо: <b>{leverage}</b>\n\n"
|
||||||
f"✅ Max Reserved: {max_reserved}\n"
|
f"✅ Лимит на сделку: <b>{max_reserved}</b>\n\n"
|
||||||
f"✅ Risk Controls: {risk_controls}\n\n"
|
f"{risk_controls_block}\n\n"
|
||||||
f"{config_status}"
|
f"{config_status}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not is_configured:
|
|
||||||
text += "\n\nВыберите настройку:"
|
|
||||||
|
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="🧠 Стратегия", callback_data="settings:auto_strategy")
|
builder.button(text="🧠 Стратегия", callback_data="settings:auto_strategy")
|
||||||
builder.button(text="📈 Инструмент", callback_data="settings:auto_symbol")
|
builder.button(text="💱 Актив", callback_data="settings:auto_symbol")
|
||||||
builder.button(text="🛡️ Риск на сделку", callback_data="settings:auto_risk")
|
|
||||||
builder.button(text="⚙️ Плечо", callback_data="settings:auto_leverage")
|
builder.button(text="⚙️ Плечо", callback_data="settings:auto_leverage")
|
||||||
builder.button(text="⚠️ Risk Controls", callback_data="auto:risk")
|
builder.button(text="🏦 Лимит", callback_data="settings:auto_max_reserved")
|
||||||
builder.button(text="🏦 Max Reserved", callback_data="settings:auto_max_reserved")
|
builder.button(text="🛡️ Риск", callback_data="settings:auto_risk")
|
||||||
|
builder.button(text="🧯 Защита", callback_data="auto:risk")
|
||||||
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
|
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
|
||||||
builder.button(text="⬅️ Назад", callback_data="system:management")
|
builder.button(text="⬅️ Назад", callback_data="system:management")
|
||||||
builder.adjust(2, 2, 2, 2)
|
builder.adjust(2, 2, 2, 2)
|
||||||
@@ -294,17 +347,36 @@ async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
|
|||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>📈 Инструмент</b>\n\n"
|
"<b>💱 Актив</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
"Выберите инструмент:"
|
"Выберите актив:"
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text=settings.default_symbol, callback_data=f"settings:auto_symbol:{settings.default_symbol}")
|
|
||||||
builder.button(text="BTCUSDT", callback_data="settings:auto_symbol:BTCUSDT")
|
builder.button(
|
||||||
builder.button(text="ETHUSDT", callback_data="settings:auto_symbol:ETHUSDT")
|
text="BTC",
|
||||||
|
callback_data="settings:auto_symbol:BTC/USD_LEVERAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.button(
|
||||||
|
text="ETH",
|
||||||
|
callback_data="settings:auto_symbol:ETH/USD_LEVERAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.button(
|
||||||
|
text="LTC",
|
||||||
|
callback_data="settings:auto_symbol:LTC/USD_LEVERAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.button(
|
||||||
|
text="XRP",
|
||||||
|
callback_data="settings:auto_symbol:XRP/USD_LEVERAGE",
|
||||||
|
)
|
||||||
|
|
||||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||||
builder.adjust(1, 2, 1)
|
|
||||||
|
builder.adjust(2, 2, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -319,7 +391,7 @@ async def set_auto_symbol(callback: CallbackQuery) -> None:
|
|||||||
await open_auto_settings(callback)
|
await open_auto_settings(callback)
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
AutoTradeRunner.set_current_screen("settings_auto")
|
||||||
await callback.answer("Инструмент обновлён")
|
await callback.answer("Актив обновлён")
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto_risk")
|
@router.callback_query(F.data == "settings:auto_risk")
|
||||||
@@ -330,7 +402,7 @@ async def open_auto_risk_settings(callback: CallbackQuery) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>🛡️ Риск</b>\n\n"
|
"<b>🛡️ Риск на сделку</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
"Выберите риск на сделку:"
|
"Выберите риск на сделку:"
|
||||||
)
|
)
|
||||||
@@ -407,7 +479,7 @@ async def open_trade_settings(callback: CallbackQuery) -> None:
|
|||||||
text = (
|
text = (
|
||||||
"<b>💹 Торговля</b>\n\n"
|
"<b>💹 Торговля</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки\n\n"
|
"<b>СИСТЕМА</b> · Настройки\n\n"
|
||||||
"Инструмент: —\n"
|
"Актив: —\n"
|
||||||
"Тип ордера по умолчанию: —\n"
|
"Тип ордера по умолчанию: —\n"
|
||||||
"Пресеты количества: —\n\n"
|
"Пресеты количества: —\n\n"
|
||||||
"В разработке."
|
"В разработке."
|
||||||
@@ -618,7 +690,7 @@ async def open_system_about(callback: CallbackQuery) -> None:
|
|||||||
reply_markup=builder.as_markup(),
|
reply_markup=builder.as_markup(),
|
||||||
)
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto_max_reserved")
|
@router.callback_query(F.data == "settings:auto_max_reserved")
|
||||||
async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
||||||
@@ -629,7 +701,7 @@ async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>🏦 Max Reserved</b>\n\n"
|
"<b>🏦 Лимит на сделку</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
"Максимальная доля баланса, которую можно зарезервировать под позицию:"
|
"Максимальная доля баланса, которую можно зарезервировать под позицию:"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class AutoTradeRunner:
|
|||||||
_current_screen: str | None = None
|
_current_screen: str | None = None
|
||||||
|
|
||||||
_analysis_interval_seconds = 5
|
_analysis_interval_seconds = 5
|
||||||
_ui_interval_seconds = 60
|
_ui_interval_seconds = 5
|
||||||
|
|
||||||
_last_text: str | None = None
|
_last_text: str | None = None
|
||||||
_last_ui_refresh_at: float = 0.0
|
_last_ui_refresh_at: float = 0.0
|
||||||
@@ -550,17 +550,66 @@ class AutoTradeRunner:
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return "—"
|
return "—"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _log_refresh_skip(cls, reason: str, payload: dict | None = None) -> None:
|
||||||
|
try:
|
||||||
|
JournalService().log_ui_info(
|
||||||
|
event_type="auto_screen_refresh_skipped",
|
||||||
|
message=f"Auto screen refresh skipped: {reason}",
|
||||||
|
screen="auto",
|
||||||
|
action="refresh_screen",
|
||||||
|
payload=payload or {},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _log_refresh_success(cls, payload: dict | None = None) -> None:
|
||||||
|
try:
|
||||||
|
JournalService().log_ui_info(
|
||||||
|
event_type="auto_screen_refreshed",
|
||||||
|
message="Auto screen refreshed.",
|
||||||
|
screen="auto",
|
||||||
|
action="refresh_screen",
|
||||||
|
payload=payload or {},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _log_refresh_error(cls, reason: str, payload: dict | None = None) -> None:
|
||||||
|
try:
|
||||||
|
JournalService().log_error(
|
||||||
|
"auto_screen_refresh_error",
|
||||||
|
f"Auto screen refresh error: {reason}",
|
||||||
|
payload or {},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
||||||
if cls._current_screen != "auto":
|
if cls._current_screen != "auto":
|
||||||
|
cls._log_refresh_skip("current_screen_not_auto")
|
||||||
return
|
return
|
||||||
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
|
|
||||||
if now < cls._retry_after_until:
|
if now < cls._retry_after_until:
|
||||||
|
cls._log_refresh_skip(
|
||||||
|
"retry_after_active",
|
||||||
|
{"retry_after_until": cls._retry_after_until, "now": now},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not force and now - cls._last_ui_refresh_at < cls._ui_interval_seconds:
|
if not force and now - cls._last_ui_refresh_at < cls._ui_interval_seconds:
|
||||||
|
cls._log_refresh_skip(
|
||||||
|
"ui_interval_not_reached",
|
||||||
|
{
|
||||||
|
"elapsed": round(now - cls._last_ui_refresh_at, 2),
|
||||||
|
"interval": cls._ui_interval_seconds,
|
||||||
|
},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not all(
|
if not all(
|
||||||
@@ -572,11 +621,22 @@ class AutoTradeRunner:
|
|||||||
cls._render_markup,
|
cls._render_markup,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
|
cls._log_refresh_skip(
|
||||||
|
"screen_not_registered",
|
||||||
|
{
|
||||||
|
"has_bot": cls._bot is not None,
|
||||||
|
"chat_id": cls._chat_id,
|
||||||
|
"message_id": cls._message_id,
|
||||||
|
"has_render_text": cls._render_text is not None,
|
||||||
|
"has_render_markup": cls._render_markup is not None,
|
||||||
|
},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
text = cls._render_text()
|
text = cls._render_text()
|
||||||
|
|
||||||
if text == cls._last_text:
|
if text == cls._last_text:
|
||||||
|
cls._log_refresh_skip("text_not_changed")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -589,8 +649,23 @@ class AutoTradeRunner:
|
|||||||
cls._last_text = text
|
cls._last_text = text
|
||||||
cls._last_ui_refresh_at = now
|
cls._last_ui_refresh_at = now
|
||||||
|
|
||||||
|
cls._log_refresh_success(
|
||||||
|
{
|
||||||
|
"chat_id": cls._chat_id,
|
||||||
|
"message_id": cls._message_id,
|
||||||
|
"text_length": len(text),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
except TelegramRetryAfter as exc:
|
except TelegramRetryAfter as exc:
|
||||||
cls._retry_after_until = time.monotonic() + exc.retry_after + 5
|
cls._retry_after_until = time.monotonic() + exc.retry_after + 5
|
||||||
|
cls._log_refresh_error(
|
||||||
|
"telegram_retry_after",
|
||||||
|
{
|
||||||
|
"retry_after": exc.retry_after,
|
||||||
|
"retry_after_until": cls._retry_after_until,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
except TelegramBadRequest as exc:
|
except TelegramBadRequest as exc:
|
||||||
error_text = str(exc).lower()
|
error_text = str(exc).lower()
|
||||||
@@ -598,6 +673,7 @@ class AutoTradeRunner:
|
|||||||
if "message is not modified" in error_text:
|
if "message is not modified" in error_text:
|
||||||
cls._last_text = text
|
cls._last_text = text
|
||||||
cls._last_ui_refresh_at = now
|
cls._last_ui_refresh_at = now
|
||||||
|
cls._log_refresh_skip("telegram_message_not_modified")
|
||||||
return
|
return
|
||||||
|
|
||||||
if "message to edit not found" in error_text:
|
if "message to edit not found" in error_text:
|
||||||
@@ -605,7 +681,19 @@ class AutoTradeRunner:
|
|||||||
cls._render_text = None
|
cls._render_text = None
|
||||||
cls._render_markup = None
|
cls._render_markup = None
|
||||||
cls._last_text = None
|
cls._last_text = None
|
||||||
|
cls._log_refresh_error(
|
||||||
|
"telegram_message_to_edit_not_found",
|
||||||
|
{"error": str(exc)},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception:
|
cls._log_refresh_error(
|
||||||
pass
|
"telegram_bad_request",
|
||||||
|
{"error": str(exc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
cls._log_refresh_error(
|
||||||
|
"unexpected_refresh_error",
|
||||||
|
{"error": str(exc)},
|
||||||
|
)
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from src.core.config import load_settings
|
from src.core.config import load_settings
|
||||||
@@ -49,6 +50,9 @@ class AutoTradeService:
|
|||||||
|
|
||||||
previous_signal = state.last_signal
|
previous_signal = state.last_signal
|
||||||
previous_decision_status = state.decision_status
|
previous_decision_status = state.decision_status
|
||||||
|
|
||||||
|
if previous_signal != normalized_signal or state.signal_started_at is None:
|
||||||
|
state.signal_started_at = time.monotonic()
|
||||||
|
|
||||||
state.last_signal = normalized_signal
|
state.last_signal = normalized_signal
|
||||||
state.last_signal_repeat_count = repeat_count
|
state.last_signal_repeat_count = repeat_count
|
||||||
@@ -85,6 +89,18 @@ class AutoTradeService:
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
# установить капитал, выделенный под автоторговлю
|
||||||
|
def set_allocated_balance_usd(self, value: float) -> AutoTradeState:
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
if value <= 0:
|
||||||
|
value = 1000.0
|
||||||
|
|
||||||
|
state.allocated_balance_usd = value
|
||||||
|
state.execution_block_reason = None
|
||||||
|
state.execution_size_adjustment_reason = None
|
||||||
|
return state
|
||||||
|
|
||||||
# получить текущее состояние автоторговли
|
# получить текущее состояние автоторговли
|
||||||
def get_state(self) -> AutoTradeState:
|
def get_state(self) -> AutoTradeState:
|
||||||
if not self._state.symbol:
|
if not self._state.symbol:
|
||||||
@@ -264,6 +280,7 @@ class AutoTradeService:
|
|||||||
state.is_signal_confirmed = False
|
state.is_signal_confirmed = False
|
||||||
state.is_signal_ready = False
|
state.is_signal_ready = False
|
||||||
state.execution_block_reason = None
|
state.execution_block_reason = None
|
||||||
|
state.signal_started_at = None
|
||||||
|
|
||||||
# собрать контекст для стратегии
|
# собрать контекст для стратегии
|
||||||
def _build_strategy_context(self) -> StrategyContext:
|
def _build_strategy_context(self) -> StrategyContext:
|
||||||
@@ -397,6 +414,9 @@ class AutoTradeService:
|
|||||||
previous_signal = state.last_signal
|
previous_signal = state.last_signal
|
||||||
previous_decision_status = state.decision_status
|
previous_decision_status = state.decision_status
|
||||||
|
|
||||||
|
if previous_signal != signal or state.signal_started_at is None:
|
||||||
|
state.signal_started_at = time.monotonic()
|
||||||
|
|
||||||
state.last_signal = signal
|
state.last_signal = signal
|
||||||
state.last_signal_repeat_count = self._same_signal_count
|
state.last_signal_repeat_count = self._same_signal_count
|
||||||
state.last_signal_confidence = confidence
|
state.last_signal_confidence = confidence
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ class AutoTradeState:
|
|||||||
status: str = "OFF"
|
status: str = "OFF"
|
||||||
|
|
||||||
# выбранная стратегия: TREND / GRID / SCALP
|
# выбранная стратегия: TREND / GRID / SCALP
|
||||||
strategy: str | None = None
|
strategy: str | None = "TREND"
|
||||||
|
|
||||||
# торговый инструмент
|
# торговый инструмент
|
||||||
symbol: str = ""
|
symbol: str = "BTC/USD_LEVERAGE"
|
||||||
|
|
||||||
# риск на одну сделку в %
|
# риск на одну сделку в %
|
||||||
risk_percent: float | None = None
|
risk_percent: float | None = 1.0
|
||||||
|
|
||||||
# текущий PnL
|
# текущий PnL
|
||||||
pnl_usd: float = 0.0
|
pnl_usd: float = 0.0
|
||||||
@@ -37,6 +37,9 @@ class AutoTradeState:
|
|||||||
# причина последнего сигнала
|
# причина последнего сигнала
|
||||||
last_signal_reason: str | None = None
|
last_signal_reason: str | None = None
|
||||||
|
|
||||||
|
# время начала текущего сигнала, monotonic timestamp
|
||||||
|
signal_started_at: float | None = None
|
||||||
|
|
||||||
# статус торгового решения: WAITING / CONFIRMING / READY / BLOCKED
|
# статус торгового решения: WAITING / CONFIRMING / READY / BLOCKED
|
||||||
decision_status: str = "WAITING"
|
decision_status: str = "WAITING"
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ class AutoTradeState:
|
|||||||
leverage: float | None = 2.0
|
leverage: float | None = 2.0
|
||||||
|
|
||||||
# stop loss по движению цены в %
|
# stop loss по движению цены в %
|
||||||
stop_loss_percent: float | None = None
|
stop_loss_percent: float | None = 1.0
|
||||||
|
|
||||||
# take profit по движению цены в %
|
# take profit по движению цены в %
|
||||||
take_profit_percent: float | None = None
|
take_profit_percent: float | None = None
|
||||||
@@ -83,4 +86,10 @@ class AutoTradeState:
|
|||||||
execution_block_reason: str | None = None
|
execution_block_reason: str | None = None
|
||||||
|
|
||||||
# причина авто-уменьшения размера позиции
|
# причина авто-уменьшения размера позиции
|
||||||
execution_size_adjustment_reason: str | None = None
|
execution_size_adjustment_reason: str | None = None
|
||||||
|
|
||||||
|
# капитал, выделенный только под AutoTrade
|
||||||
|
allocated_balance_usd: float = 1000.0
|
||||||
|
|
||||||
|
# зафиксированный результат закрытых paper-сделок
|
||||||
|
realized_pnl_usd: float = 0.0
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from src.core.event_bus import EventBus
|
from src.core.event_bus import EventBus
|
||||||
@@ -14,6 +15,7 @@ from src.trading.position.state import PositionState
|
|||||||
|
|
||||||
class ExecutionEngine:
|
class ExecutionEngine:
|
||||||
_position = PositionState()
|
_position = PositionState()
|
||||||
|
_size_precision = 5
|
||||||
|
|
||||||
def get_position(self) -> PositionState:
|
def get_position(self) -> PositionState:
|
||||||
return type(self)._position
|
return type(self)._position
|
||||||
@@ -58,8 +60,7 @@ class ExecutionEngine:
|
|||||||
return ExecutionDecision("NONE", False, "Позиция уже открыта.")
|
return ExecutionDecision("NONE", False, "Позиция уже открыта.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(state.symbol)
|
entry_price = self._entry_price_for_side(state.symbol, side)
|
||||||
entry_price = ticker.price
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return ExecutionDecision("NONE", False, f"Не удалось получить цену для paper execution: {exc}")
|
return ExecutionDecision("NONE", False, f"Не удалось получить цену для paper execution: {exc}")
|
||||||
|
|
||||||
@@ -72,13 +73,22 @@ class ExecutionEngine:
|
|||||||
False,
|
False,
|
||||||
"Позиция не открыта: невозможно рассчитать size без Stop Loss.",
|
"Позиция не открыта: невозможно рассчитать size без Stop Loss.",
|
||||||
)
|
)
|
||||||
|
|
||||||
size = self._adjust_size_by_margin_limit(
|
size = self._adjust_size_by_margin_limit(
|
||||||
state=state,
|
state=state,
|
||||||
entry_price=entry_price,
|
entry_price=entry_price,
|
||||||
size=size,
|
size=size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
size = self._round_order_size(size)
|
||||||
|
|
||||||
|
if size <= 0:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"Позиция не открыта: итоговый size равен 0.",
|
||||||
|
)
|
||||||
|
|
||||||
type(self)._position = PositionState(
|
type(self)._position = PositionState(
|
||||||
side=side,
|
side=side,
|
||||||
symbol=state.symbol,
|
symbol=state.symbol,
|
||||||
@@ -105,6 +115,7 @@ class ExecutionEngine:
|
|||||||
"repeat_count": state.last_signal_repeat_count,
|
"repeat_count": state.last_signal_repeat_count,
|
||||||
"reason": state.last_signal_reason,
|
"reason": state.last_signal_reason,
|
||||||
"opened_at": now,
|
"opened_at": now,
|
||||||
|
"pricing": "ask_for_long_bid_for_short",
|
||||||
}
|
}
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
@@ -131,14 +142,14 @@ class ExecutionEngine:
|
|||||||
return ExecutionDecision("NONE", False, "Нет направления для flip.")
|
return ExecutionDecision("NONE", False, "Нет направления для flip.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(state.symbol)
|
exit_price = self._exit_price_for_side(position.symbol or state.symbol, position.side)
|
||||||
flip_price = ticker.price
|
new_entry_price = self._entry_price_for_side(state.symbol, new_side)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return ExecutionDecision("NONE", False, f"Ошибка получения цены для flip: {exc}")
|
return ExecutionDecision("NONE", False, f"Ошибка получения цены для flip: {exc}")
|
||||||
|
|
||||||
now = self._now_time()
|
now = self._now_time()
|
||||||
pnl = self._calculate_pnl(flip_price)
|
pnl = self._calculate_pnl(exit_price)
|
||||||
new_size = self._calculate_position_size(state, entry_price=flip_price)
|
new_size = self._calculate_position_size(state, entry_price=new_entry_price)
|
||||||
|
|
||||||
if new_size <= 0:
|
if new_size <= 0:
|
||||||
return ExecutionDecision(
|
return ExecutionDecision(
|
||||||
@@ -146,13 +157,24 @@ class ExecutionEngine:
|
|||||||
False,
|
False,
|
||||||
"Flip отменён: невозможно рассчитать size без Stop Loss.",
|
"Flip отменён: невозможно рассчитать size без Stop Loss.",
|
||||||
)
|
)
|
||||||
|
|
||||||
new_size = self._adjust_size_by_margin_limit(
|
new_size = self._adjust_size_by_margin_limit(
|
||||||
state=state,
|
state=state,
|
||||||
entry_price=flip_price,
|
entry_price=new_entry_price,
|
||||||
size=new_size,
|
size=new_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
new_size = self._round_order_size(new_size)
|
||||||
|
|
||||||
|
if new_size <= 0:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"Flip отменён: итоговый size равен 0.",
|
||||||
|
)
|
||||||
|
|
||||||
|
state.realized_pnl_usd += pnl
|
||||||
|
|
||||||
old_side = position.side
|
old_side = position.side
|
||||||
old_entry_price = position.entry_price
|
old_entry_price = position.entry_price
|
||||||
old_size = position.size
|
old_size = position.size
|
||||||
@@ -162,7 +184,7 @@ class ExecutionEngine:
|
|||||||
type(self)._position = PositionState(
|
type(self)._position = PositionState(
|
||||||
side=new_side,
|
side=new_side,
|
||||||
symbol=state.symbol,
|
symbol=state.symbol,
|
||||||
entry_price=flip_price,
|
entry_price=new_entry_price,
|
||||||
size=new_size,
|
size=new_size,
|
||||||
leverage=state.leverage,
|
leverage=state.leverage,
|
||||||
unrealized_pnl_usd=0.0,
|
unrealized_pnl_usd=0.0,
|
||||||
@@ -180,8 +202,8 @@ class ExecutionEngine:
|
|||||||
"new_side": new_side,
|
"new_side": new_side,
|
||||||
"side": new_side,
|
"side": new_side,
|
||||||
"entry_price": old_entry_price,
|
"entry_price": old_entry_price,
|
||||||
"exit_price": flip_price,
|
"exit_price": exit_price,
|
||||||
"new_entry_price": flip_price,
|
"new_entry_price": new_entry_price,
|
||||||
"old_size": old_size,
|
"old_size": old_size,
|
||||||
"new_size": new_size,
|
"new_size": new_size,
|
||||||
"size": new_size,
|
"size": new_size,
|
||||||
@@ -195,6 +217,7 @@ class ExecutionEngine:
|
|||||||
"opened_at": old_opened_at,
|
"opened_at": old_opened_at,
|
||||||
"closed_at": now,
|
"closed_at": now,
|
||||||
"new_opened_at": now,
|
"new_opened_at": now,
|
||||||
|
"pricing": "exit_by_side_then_entry_by_side",
|
||||||
}
|
}
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
@@ -231,13 +254,14 @@ class ExecutionEngine:
|
|||||||
exit_price = forced_exit_price
|
exit_price = forced_exit_price
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(state.symbol)
|
exit_price = self._exit_price_for_side(position.symbol or state.symbol, position.side)
|
||||||
exit_price = ticker.price
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return ExecutionDecision("NONE", False, f"Ошибка получения цены для закрытия: {exc}")
|
return ExecutionDecision("NONE", False, f"Ошибка получения цены для закрытия: {exc}")
|
||||||
|
|
||||||
pnl = forced_pnl if forced_pnl is not None else self._calculate_pnl(exit_price)
|
pnl = forced_pnl if forced_pnl is not None else self._calculate_pnl(exit_price)
|
||||||
|
|
||||||
|
state.realized_pnl_usd += pnl
|
||||||
|
|
||||||
now = self._now_time()
|
now = self._now_time()
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
@@ -258,6 +282,7 @@ class ExecutionEngine:
|
|||||||
"is_forced": forced_reason is not None,
|
"is_forced": forced_reason is not None,
|
||||||
"opened_at": position.opened_at,
|
"opened_at": position.opened_at,
|
||||||
"closed_at": now,
|
"closed_at": now,
|
||||||
|
"pricing": "bid_for_long_exit_ask_for_short_exit",
|
||||||
}
|
}
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
@@ -293,8 +318,7 @@ class ExecutionEngine:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(position.symbol or state.symbol)
|
current_price = self._exit_price_for_side(position.symbol or state.symbol, position.side)
|
||||||
current_price = ticker.price
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -327,34 +351,19 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_stop_loss_hit(
|
def _is_stop_loss_hit(self, state: AutoTradeState, price_move_percent: float) -> bool:
|
||||||
self,
|
|
||||||
state: AutoTradeState,
|
|
||||||
price_move_percent: float,
|
|
||||||
) -> bool:
|
|
||||||
if state.stop_loss_percent is None:
|
if state.stop_loss_percent is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return price_move_percent <= -abs(state.stop_loss_percent)
|
return price_move_percent <= -abs(state.stop_loss_percent)
|
||||||
|
|
||||||
def _is_take_profit_hit(
|
def _is_take_profit_hit(self, state: AutoTradeState, price_move_percent: float) -> bool:
|
||||||
self,
|
|
||||||
state: AutoTradeState,
|
|
||||||
price_move_percent: float,
|
|
||||||
) -> bool:
|
|
||||||
if state.take_profit_percent is None:
|
if state.take_profit_percent is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return price_move_percent >= abs(state.take_profit_percent)
|
return price_move_percent >= abs(state.take_profit_percent)
|
||||||
|
|
||||||
def _is_max_loss_hit(
|
def _is_max_loss_hit(self, state: AutoTradeState, unrealized_pnl: float) -> bool:
|
||||||
self,
|
|
||||||
state: AutoTradeState,
|
|
||||||
unrealized_pnl: float,
|
|
||||||
) -> bool:
|
|
||||||
if state.max_loss_usd is None:
|
if state.max_loss_usd is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return unrealized_pnl <= -abs(state.max_loss_usd)
|
return unrealized_pnl <= -abs(state.max_loss_usd)
|
||||||
|
|
||||||
def _calculate_price_move_percent(self, current_price: float) -> float:
|
def _calculate_price_move_percent(self, current_price: float) -> float:
|
||||||
@@ -371,7 +380,7 @@ class ExecutionEngine:
|
|||||||
return round(((entry - current_price) / entry) * 100, 4)
|
return round(((entry - current_price) / entry) * 100, 4)
|
||||||
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def _should_flip_position(self, state: AutoTradeState) -> bool:
|
def _should_flip_position(self, state: AutoTradeState) -> bool:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
@@ -403,8 +412,7 @@ class ExecutionEngine:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(position.symbol or state.symbol)
|
current_price = self._exit_price_for_side(position.symbol or state.symbol, position.side)
|
||||||
current_price = ticker.price
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
return
|
return
|
||||||
@@ -430,15 +438,14 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
if price is None:
|
if price is None:
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(state.symbol)
|
price = self._signal_entry_price(state)
|
||||||
price = ticker.price
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
if price <= 0:
|
if price <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
balance_usd = 1000.0
|
balance_usd = state.allocated_balance_usd
|
||||||
target_risk_usd = balance_usd * (state.risk_percent / 100)
|
target_risk_usd = balance_usd * (state.risk_percent / 100)
|
||||||
stop_loss_distance_usd = price * (state.stop_loss_percent / 100)
|
stop_loss_distance_usd = price * (state.stop_loss_percent / 100)
|
||||||
|
|
||||||
@@ -446,8 +453,7 @@ class ExecutionEngine:
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
size = target_risk_usd / stop_loss_distance_usd
|
size = target_risk_usd / stop_loss_distance_usd
|
||||||
|
return self._round_size(size)
|
||||||
return round(size, 8)
|
|
||||||
|
|
||||||
def _adjust_size_by_margin_limit(
|
def _adjust_size_by_margin_limit(
|
||||||
self,
|
self,
|
||||||
@@ -462,26 +468,85 @@ class ExecutionEngine:
|
|||||||
state.execution_size_adjustment_reason = None
|
state.execution_size_adjustment_reason = None
|
||||||
|
|
||||||
if max_percent is None or max_percent <= 0:
|
if max_percent is None or max_percent <= 0:
|
||||||
return round(size, 8)
|
return self._round_size(size)
|
||||||
|
|
||||||
leverage = state.leverage or 1.0
|
leverage = state.leverage or 1.0
|
||||||
if leverage <= 0 or entry_price <= 0:
|
if leverage <= 0 or entry_price <= 0:
|
||||||
state.execution_block_reason = "Invalid leverage or entry price."
|
state.execution_block_reason = "Invalid leverage or entry price."
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
balance_usd = 1000.0
|
balance_usd = state.allocated_balance_usd
|
||||||
max_reserved_usd = balance_usd * (max_percent / 100)
|
max_reserved_usd = balance_usd * (max_percent / 100)
|
||||||
|
|
||||||
max_notional_usd = max_reserved_usd * leverage
|
max_notional_usd = max_reserved_usd * leverage
|
||||||
max_size = max_notional_usd / entry_price
|
max_size = max_notional_usd / entry_price
|
||||||
|
|
||||||
if size <= max_size:
|
if size <= max_size:
|
||||||
return round(size, 8)
|
return self._round_size(size)
|
||||||
|
|
||||||
state.execution_size_adjustment_reason = "MARGIN_LIMIT"
|
state.execution_size_adjustment_reason = "MARGIN_LIMIT"
|
||||||
|
return self._round_size(max_size)
|
||||||
|
|
||||||
|
def _signal_entry_price(self, state: AutoTradeState) -> float:
|
||||||
|
if state.last_signal == "BUY":
|
||||||
|
return self._entry_price_for_side(state.symbol, "LONG")
|
||||||
|
|
||||||
|
if state.last_signal == "SELL":
|
||||||
|
return self._entry_price_for_side(state.symbol, "SHORT")
|
||||||
|
|
||||||
|
return self._market_last_price(state.symbol)
|
||||||
|
|
||||||
|
def _entry_price_for_side(self, symbol: str, side: str) -> float:
|
||||||
|
snapshot = ExchangeService().get_market_snapshot(symbol)
|
||||||
|
|
||||||
|
if side == "LONG":
|
||||||
|
return self._snapshot_price(snapshot, "ask_price", "last_price")
|
||||||
|
|
||||||
|
if side == "SHORT":
|
||||||
|
return self._snapshot_price(snapshot, "bid_price", "last_price")
|
||||||
|
|
||||||
|
return self._snapshot_price(snapshot, "last_price")
|
||||||
|
|
||||||
|
def _exit_price_for_side(self, symbol: str, side: str) -> float:
|
||||||
|
snapshot = ExchangeService().get_market_snapshot(symbol)
|
||||||
|
|
||||||
|
if side == "LONG":
|
||||||
|
return self._snapshot_price(snapshot, "bid_price", "last_price")
|
||||||
|
|
||||||
|
if side == "SHORT":
|
||||||
|
return self._snapshot_price(snapshot, "ask_price", "last_price")
|
||||||
|
|
||||||
|
return self._snapshot_price(snapshot, "last_price")
|
||||||
|
|
||||||
|
def _market_last_price(self, symbol: str) -> float:
|
||||||
|
snapshot = ExchangeService().get_market_snapshot(symbol)
|
||||||
|
return self._snapshot_price(snapshot, "last_price")
|
||||||
|
|
||||||
|
def _snapshot_price(
|
||||||
|
self,
|
||||||
|
snapshot: dict[str, object],
|
||||||
|
primary_key: str,
|
||||||
|
fallback_key: str | None = None,
|
||||||
|
) -> float:
|
||||||
|
raw_price = snapshot.get(primary_key)
|
||||||
|
|
||||||
|
if raw_price is None and fallback_key is not None:
|
||||||
|
raw_price = snapshot.get(fallback_key)
|
||||||
|
|
||||||
|
if raw_price is None:
|
||||||
|
raise ValueError(f"Market snapshot price '{primary_key}' is missing.")
|
||||||
|
|
||||||
|
price = float(raw_price)
|
||||||
|
|
||||||
|
if price <= 0:
|
||||||
|
raise ValueError(f"Market snapshot price '{primary_key}' is invalid: {price}")
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
||||||
|
def _round_size(self, size: float) -> float:
|
||||||
|
factor = 10 ** self._size_precision
|
||||||
|
return math.floor(float(size) * factor) / factor
|
||||||
|
|
||||||
return round(max_size, 8)
|
|
||||||
|
|
||||||
def _calculate_pnl(self, current_price: float) -> float:
|
def _calculate_pnl(self, current_price: float) -> float:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
@@ -504,5 +569,9 @@ class ExecutionEngine:
|
|||||||
state.position_size = position.size
|
state.position_size = position.size
|
||||||
state.unrealized_pnl_usd = position.unrealized_pnl_usd
|
state.unrealized_pnl_usd = position.unrealized_pnl_usd
|
||||||
|
|
||||||
|
def _round_order_size(self, value: float) -> float:
|
||||||
|
factor = 10 ** self._size_precision
|
||||||
|
return math.floor(float(value) * factor) / factor
|
||||||
|
|
||||||
def _now_time(self) -> str:
|
def _now_time(self) -> str:
|
||||||
return datetime.now().strftime("%H:%M:%S")
|
return datetime.now().strftime("%H:%M:%S")
|
||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from src.trading.strategies.base import BaseStrategy
|
from src.trading.strategies.base import BaseStrategy
|
||||||
from src.trading.strategies.hold import HoldStrategy
|
from src.trading.strategies.hold import HoldStrategy
|
||||||
|
from src.trading.strategies.scalp import ScalpStrategy
|
||||||
from src.trading.strategies.trend import TrendStrategy
|
from src.trading.strategies.trend import TrendStrategy
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ class StrategyRegistry:
|
|||||||
"HOLD": HoldStrategy(),
|
"HOLD": HoldStrategy(),
|
||||||
"TREND": TrendStrategy(),
|
"TREND": TrendStrategy(),
|
||||||
"GRID": HoldStrategy(),
|
"GRID": HoldStrategy(),
|
||||||
"SCALP": HoldStrategy(),
|
"SCALP": ScalpStrategy(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# получить стратегию по имени
|
# получить стратегию по имени
|
||||||
|
|||||||
156
app/src/trading/strategies/scalp.py
Normal file
156
app/src/trading/strategies/scalp.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# app/src/trading/strategies/scalp.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from src.integrations.exchange.service import ExchangeService
|
||||||
|
from src.trading.strategies.base import StrategyContext
|
||||||
|
from src.trading.strategies.signals import SignalResult, SignalType
|
||||||
|
|
||||||
|
|
||||||
|
class ScalpStrategy:
|
||||||
|
name = "SCALP"
|
||||||
|
|
||||||
|
_price_window: dict[str, list[float]] = {}
|
||||||
|
|
||||||
|
# короткое окно = быстрая реакция
|
||||||
|
_window_size = 4
|
||||||
|
|
||||||
|
# ниже порог = чувствительнее TREND
|
||||||
|
_threshold_percent = 0.02
|
||||||
|
|
||||||
|
# для scalp допускаем чуть больше шума
|
||||||
|
_min_direction_ratio = 0.55
|
||||||
|
|
||||||
|
def analyze(self, context: StrategyContext) -> SignalResult:
|
||||||
|
try:
|
||||||
|
ticker = ExchangeService().get_price(context.symbol)
|
||||||
|
except Exception as exc:
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.HOLD,
|
||||||
|
reason="Не удалось получить рыночную цену. Безопасный HOLD.",
|
||||||
|
confidence=0.0,
|
||||||
|
payload={
|
||||||
|
"strategy": self.name,
|
||||||
|
"symbol": context.symbol,
|
||||||
|
"error": str(exc),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
symbol = ticker.symbol
|
||||||
|
current_price = float(ticker.price)
|
||||||
|
|
||||||
|
prices = self._price_window.setdefault(symbol, [])
|
||||||
|
prices.append(current_price)
|
||||||
|
|
||||||
|
if len(prices) > self._window_size:
|
||||||
|
prices.pop(0)
|
||||||
|
|
||||||
|
if len(prices) < self._window_size:
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.HOLD,
|
||||||
|
reason="Недостаточно данных для SCALP.",
|
||||||
|
confidence=0.0,
|
||||||
|
payload={
|
||||||
|
"strategy": self.name,
|
||||||
|
"symbol": symbol,
|
||||||
|
"price": current_price,
|
||||||
|
"window_size": len(prices),
|
||||||
|
"required_window_size": self._window_size,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
first_price = prices[0]
|
||||||
|
last_price = prices[-1]
|
||||||
|
|
||||||
|
if first_price <= 0:
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.HOLD,
|
||||||
|
reason="Некорректная стартовая цена в окне SCALP.",
|
||||||
|
confidence=0.0,
|
||||||
|
payload={
|
||||||
|
"strategy": self.name,
|
||||||
|
"symbol": symbol,
|
||||||
|
"prices": prices,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
change_percent = ((last_price - first_price) / first_price) * 100
|
||||||
|
direction_ratio = self._direction_ratio(prices, change_percent)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"strategy": self.name,
|
||||||
|
"symbol": symbol,
|
||||||
|
"first_price": first_price,
|
||||||
|
"current_price": last_price,
|
||||||
|
"change_percent": round(change_percent, 5),
|
||||||
|
"direction_ratio": round(direction_ratio, 3),
|
||||||
|
"window_size": len(prices),
|
||||||
|
"threshold_percent": self._threshold_percent,
|
||||||
|
"min_direction_ratio": self._min_direction_ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
change_percent >= self._threshold_percent
|
||||||
|
and direction_ratio >= self._min_direction_ratio
|
||||||
|
):
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.BUY,
|
||||||
|
reason="Быстрый краткосрочный импульс вверх.",
|
||||||
|
confidence=self._calculate_confidence(change_percent, direction_ratio),
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
change_percent <= -self._threshold_percent
|
||||||
|
and direction_ratio >= self._min_direction_ratio
|
||||||
|
):
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.SELL,
|
||||||
|
reason="Быстрый краткосрочный импульс вниз.",
|
||||||
|
confidence=self._calculate_confidence(change_percent, direction_ratio),
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.HOLD,
|
||||||
|
reason="SCALP-импульс недостаточно сильный.",
|
||||||
|
confidence=0.0,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _direction_ratio(self, prices: list[float], change_percent: float) -> float:
|
||||||
|
if len(prices) < 2:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
up_moves = 0
|
||||||
|
down_moves = 0
|
||||||
|
|
||||||
|
for previous_price, current_price in zip(prices, prices[1:]):
|
||||||
|
if current_price > previous_price:
|
||||||
|
up_moves += 1
|
||||||
|
elif current_price < previous_price:
|
||||||
|
down_moves += 1
|
||||||
|
|
||||||
|
total_moves = max(1, len(prices) - 1)
|
||||||
|
|
||||||
|
if change_percent >= 0:
|
||||||
|
return up_moves / total_moves
|
||||||
|
|
||||||
|
return down_moves / total_moves
|
||||||
|
|
||||||
|
def _calculate_confidence(
|
||||||
|
self,
|
||||||
|
change_percent: float,
|
||||||
|
direction_ratio: float,
|
||||||
|
) -> float:
|
||||||
|
strength = abs(change_percent) / self._threshold_percent
|
||||||
|
|
||||||
|
if strength < 1:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
strength_score = min(1.0, strength / 2)
|
||||||
|
direction_score = min(1.0, direction_ratio)
|
||||||
|
|
||||||
|
confidence = 0.35 + (strength_score * 0.4) + (direction_score * 0.25)
|
||||||
|
|
||||||
|
return round(min(1.0, confidence), 2)
|
||||||
@@ -10,28 +10,24 @@ from src.trading.strategies.signals import SignalResult, SignalType
|
|||||||
class TrendStrategy:
|
class TrendStrategy:
|
||||||
name = "TREND"
|
name = "TREND"
|
||||||
|
|
||||||
_last_prices: dict[str, float] = {}
|
_price_window: dict[str, list[float]] = {}
|
||||||
_threshold_percent = 0.02
|
|
||||||
|
|
||||||
# рассчитать уверенность сигнала по силе движения цены
|
# длиннее окно = меньше шума
|
||||||
def _calculate_confidence(self, change_percent: float) -> float:
|
_window_size = 8
|
||||||
strength = abs(change_percent) / self._threshold_percent
|
|
||||||
|
|
||||||
if strength < 1:
|
# общий порог изменения за окно
|
||||||
return 0.0
|
_threshold_percent = 0.05
|
||||||
|
|
||||||
confidence = 0.35 + ((strength - 1) / 2) * 0.65
|
# сколько движений внутри окна должно быть в сторону сигнала
|
||||||
|
_min_direction_ratio = 0.6
|
||||||
|
|
||||||
return round(min(1.0, confidence), 2)
|
|
||||||
|
|
||||||
# анализ простого тренда по изменению цены
|
|
||||||
def analyze(self, context: StrategyContext) -> SignalResult:
|
def analyze(self, context: StrategyContext) -> SignalResult:
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(context.symbol)
|
snapshot = ExchangeService().get_market_snapshot(context.symbol)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return SignalResult(
|
return SignalResult(
|
||||||
signal=SignalType.HOLD,
|
signal=SignalType.HOLD,
|
||||||
reason="Не удалось получить рыночную цену. Безопасный HOLD.",
|
reason="Не удалось получить рыночный snapshot. Безопасный HOLD.",
|
||||||
confidence=0.0,
|
confidence=0.0,
|
||||||
payload={
|
payload={
|
||||||
"strategy": self.name,
|
"strategy": self.name,
|
||||||
@@ -40,63 +36,159 @@ class TrendStrategy:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
symbol = ticker.symbol
|
symbol = str(snapshot.get("symbol") or context.symbol)
|
||||||
current_price = ticker.price
|
current_price = self._analysis_price(snapshot)
|
||||||
previous_price = self._last_prices.get(symbol)
|
|
||||||
|
|
||||||
self._last_prices[symbol] = current_price
|
if current_price <= 0:
|
||||||
|
|
||||||
if previous_price is None or previous_price <= 0:
|
|
||||||
return SignalResult(
|
return SignalResult(
|
||||||
signal=SignalType.HOLD,
|
signal=SignalType.HOLD,
|
||||||
reason="Недостаточно данных для определения тренда.",
|
reason="Некорректная рыночная цена. Безопасный HOLD.",
|
||||||
|
confidence=0.0,
|
||||||
|
payload={
|
||||||
|
"strategy": self.name,
|
||||||
|
"symbol": symbol,
|
||||||
|
"snapshot": snapshot,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
prices = self._price_window.setdefault(symbol, [])
|
||||||
|
prices.append(current_price)
|
||||||
|
|
||||||
|
if len(prices) > self._window_size:
|
||||||
|
prices.pop(0)
|
||||||
|
|
||||||
|
if len(prices) < self._window_size:
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.HOLD,
|
||||||
|
reason="Недостаточно данных для анализа тренда.",
|
||||||
confidence=0.0,
|
confidence=0.0,
|
||||||
payload={
|
payload={
|
||||||
"strategy": self.name,
|
"strategy": self.name,
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
"price": current_price,
|
"price": current_price,
|
||||||
|
"window_size": len(prices),
|
||||||
|
"required_window_size": self._window_size,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
change_percent = ((current_price - previous_price) / previous_price) * 100
|
first_price = prices[0]
|
||||||
|
last_price = prices[-1]
|
||||||
|
|
||||||
if change_percent >= self._threshold_percent:
|
if first_price <= 0:
|
||||||
|
return SignalResult(
|
||||||
|
signal=SignalType.HOLD,
|
||||||
|
reason="Некорректная стартовая цена в окне.",
|
||||||
|
confidence=0.0,
|
||||||
|
payload={
|
||||||
|
"strategy": self.name,
|
||||||
|
"symbol": symbol,
|
||||||
|
"prices": prices,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
change_percent = ((last_price - first_price) / first_price) * 100
|
||||||
|
direction_ratio = self._direction_ratio(prices, change_percent)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"strategy": self.name,
|
||||||
|
"symbol": symbol,
|
||||||
|
"analysis_price": last_price,
|
||||||
|
"first_price": first_price,
|
||||||
|
"current_price": last_price,
|
||||||
|
"last_price": snapshot.get("last_price"),
|
||||||
|
"bid_price": snapshot.get("bid_price"),
|
||||||
|
"ask_price": snapshot.get("ask_price"),
|
||||||
|
"change_percent": round(change_percent, 5),
|
||||||
|
"direction_ratio": round(direction_ratio, 3),
|
||||||
|
"window_size": len(prices),
|
||||||
|
"threshold_percent": self._threshold_percent,
|
||||||
|
"min_direction_ratio": self._min_direction_ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
change_percent >= self._threshold_percent
|
||||||
|
and direction_ratio >= self._min_direction_ratio
|
||||||
|
):
|
||||||
return SignalResult(
|
return SignalResult(
|
||||||
signal=SignalType.BUY,
|
signal=SignalType.BUY,
|
||||||
reason="Цена растёт выше порога тренда.",
|
reason="Устойчивый рост цены в окне TREND.",
|
||||||
confidence=self._calculate_confidence(change_percent),
|
confidence=self._calculate_confidence(change_percent, direction_ratio),
|
||||||
payload={
|
payload=payload,
|
||||||
"strategy": self.name,
|
|
||||||
"symbol": symbol,
|
|
||||||
"previous_price": previous_price,
|
|
||||||
"current_price": current_price,
|
|
||||||
"change_percent": round(change_percent, 5),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if change_percent <= -self._threshold_percent:
|
if (
|
||||||
|
change_percent <= -self._threshold_percent
|
||||||
|
and direction_ratio >= self._min_direction_ratio
|
||||||
|
):
|
||||||
return SignalResult(
|
return SignalResult(
|
||||||
signal=SignalType.SELL,
|
signal=SignalType.SELL,
|
||||||
reason="Цена падает ниже порога тренда.",
|
reason="Устойчивое снижение цены в окне TREND.",
|
||||||
confidence=self._calculate_confidence(change_percent),
|
confidence=self._calculate_confidence(change_percent, direction_ratio),
|
||||||
payload={
|
payload=payload,
|
||||||
"strategy": self.name,
|
|
||||||
"symbol": symbol,
|
|
||||||
"previous_price": previous_price,
|
|
||||||
"current_price": current_price,
|
|
||||||
"change_percent": round(change_percent, 5),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return SignalResult(
|
return SignalResult(
|
||||||
signal=SignalType.HOLD,
|
signal=SignalType.HOLD,
|
||||||
reason="Изменение цены ниже порога тренда.",
|
reason="Тренд недостаточно устойчивый.",
|
||||||
confidence=0.0,
|
confidence=0.0,
|
||||||
payload={
|
payload=payload,
|
||||||
"strategy": self.name,
|
)
|
||||||
"symbol": symbol,
|
|
||||||
"previous_price": previous_price,
|
def _analysis_price(self, snapshot: dict[str, object]) -> float:
|
||||||
"current_price": current_price,
|
bid = self._safe_float(snapshot.get("bid_price"))
|
||||||
"change_percent": round(change_percent, 5),
|
ask = self._safe_float(snapshot.get("ask_price"))
|
||||||
},
|
|
||||||
)
|
if bid is not None and ask is not None and bid > 0 and ask > 0:
|
||||||
|
return (bid + ask) / 2
|
||||||
|
|
||||||
|
last = self._safe_float(snapshot.get("last_price"))
|
||||||
|
if last is not None:
|
||||||
|
return last
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _safe_float(self, value: object) -> float | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _direction_ratio(self, prices: list[float], change_percent: float) -> float:
|
||||||
|
if len(prices) < 2:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
up_moves = 0
|
||||||
|
down_moves = 0
|
||||||
|
|
||||||
|
for previous_price, current_price in zip(prices, prices[1:]):
|
||||||
|
if current_price > previous_price:
|
||||||
|
up_moves += 1
|
||||||
|
elif current_price < previous_price:
|
||||||
|
down_moves += 1
|
||||||
|
|
||||||
|
total_moves = max(1, len(prices) - 1)
|
||||||
|
|
||||||
|
if change_percent >= 0:
|
||||||
|
return up_moves / total_moves
|
||||||
|
|
||||||
|
return down_moves / total_moves
|
||||||
|
|
||||||
|
def _calculate_confidence(
|
||||||
|
self,
|
||||||
|
change_percent: float,
|
||||||
|
direction_ratio: float,
|
||||||
|
) -> float:
|
||||||
|
strength = abs(change_percent) / self._threshold_percent
|
||||||
|
|
||||||
|
if strength < 1:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
strength_score = min(1.0, strength / 3)
|
||||||
|
direction_score = min(1.0, direction_ratio)
|
||||||
|
|
||||||
|
confidence = 0.3 + (strength_score * 0.4) + (direction_score * 0.3)
|
||||||
|
|
||||||
|
return round(min(1.0, confidence), 2)
|
||||||
@@ -242,6 +242,30 @@
|
|||||||
- risk_percent теперь реально влияет на размер позиции
|
- risk_percent теперь реально влияет на размер позиции
|
||||||
- flip теперь проходит через margin protection
|
- flip теперь проходит через margin protection
|
||||||
|
|
||||||
|
#### 07.4.3.14 — Auto UI, Realistic Pricing & Debug Live Tools ✅
|
||||||
|
- redesigned RUNNING auto-trading UI
|
||||||
|
- HOLD / BUY / SELL / READY state separation
|
||||||
|
- compact signal rendering with real duration
|
||||||
|
- confidence hidden for HOLD state
|
||||||
|
- direction-aware LONG / SHORT UI blocks
|
||||||
|
- compact active position rendering
|
||||||
|
- removed zero-value UI noise without position
|
||||||
|
- realistic bid / ask pricing in auto UI
|
||||||
|
- realistic bid / ask execution pricing
|
||||||
|
- TREND strategy switched to mid-price analysis
|
||||||
|
- corrected own funds / margin calculations
|
||||||
|
- safer size rounding for margin protection
|
||||||
|
- signal_started_at support for real-time duration tracking
|
||||||
|
- improved auto screen refresh handling
|
||||||
|
- live UI refresh diagnostics in AutoTradeRunner
|
||||||
|
- new debug UI-state commands
|
||||||
|
- new paper execution debug commands
|
||||||
|
- automatic flip direction detection
|
||||||
|
- live paper execution monitoring commands
|
||||||
|
- integration testing flow for SL / TP / ML
|
||||||
|
- integration testing flow for execution alerts
|
||||||
|
- preparation for isolated debug runtime architecture
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,31 @@
|
|||||||
- risk_percent теперь реально влияет на размер позиции
|
- risk_percent теперь реально влияет на размер позиции
|
||||||
- flip теперь проходит через margin protection
|
- flip теперь проходит через margin protection
|
||||||
|
|
||||||
|
#### 07.4.3.14 — Auto UI, Realistic Pricing & Debug Live Tools ✅
|
||||||
|
|
||||||
|
- redesigned RUNNING auto-trading UI
|
||||||
|
- HOLD / BUY / SELL / READY state separation
|
||||||
|
- compact signal rendering with real duration
|
||||||
|
- confidence hidden for HOLD state
|
||||||
|
- direction-aware LONG / SHORT UI blocks
|
||||||
|
- compact active position rendering
|
||||||
|
- removed zero-value UI noise without position
|
||||||
|
- realistic bid / ask pricing in auto UI
|
||||||
|
- realistic bid / ask execution pricing
|
||||||
|
- TREND strategy switched to mid-price analysis
|
||||||
|
- corrected own funds / margin calculations
|
||||||
|
- safer size rounding for margin protection
|
||||||
|
- signal_started_at support for real-time duration tracking
|
||||||
|
- improved auto screen refresh handling
|
||||||
|
- live UI refresh diagnostics in AutoTradeRunner
|
||||||
|
- new debug UI-state commands
|
||||||
|
- new paper execution debug commands
|
||||||
|
- automatic flip direction detection
|
||||||
|
- live paper execution monitoring commands
|
||||||
|
- integration testing flow for SL / TP / ML
|
||||||
|
- integration testing flow for execution alerts
|
||||||
|
- preparation for isolated debug runtime architecture
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
# 07.4.3.14 — Auto Trading UI, Realistic Pricing & Debug Live Tools
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Привести экран автоторговли к более понятному trading-terminal формату и сделать расчёты paper execution ближе к реальности:
|
||||||
|
|
||||||
|
- обновить UI блока автоторговли
|
||||||
|
- разделить состояния HOLD / BUY / SELL / READY
|
||||||
|
- использовать bid / ask цены для входа и выхода
|
||||||
|
- улучшить отображение позиции
|
||||||
|
- добавить live debug-команды для проверки paper execution
|
||||||
|
- подготовить основу для последующей изоляции debug-режима
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что было раньше
|
||||||
|
|
||||||
|
Ранее экран `Автоторговля — Работает` показывал технические данные:
|
||||||
|
|
||||||
|
- `HOLD ×N`
|
||||||
|
- `Зарезервировано · $ 0`
|
||||||
|
- `P&L · $ 0`
|
||||||
|
- confidence даже для HOLD
|
||||||
|
- расчёты от одной цены `lastPrice`
|
||||||
|
|
||||||
|
Это создавало лишний шум и не отражало реальную механику исполнения.
|
||||||
|
|
||||||
|
Также debug-команды смешивали UI-проверку и execution-проверку.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что реализовано
|
||||||
|
|
||||||
|
## 1. Новый формат экрана RUNNING
|
||||||
|
|
||||||
|
Для состояния без позиции экран стал компактнее.
|
||||||
|
|
||||||
|
Было:
|
||||||
|
|
||||||
|
```text
|
||||||
|
📡 Ожидание сигнала
|
||||||
|
🟡 HOLD ×18
|
||||||
|
Уверенность · 0.00
|
||||||
|
```
|
||||||
|
|
||||||
|
Стало:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Сигнал 🟡 HOLD · 5м 35с
|
||||||
|
```
|
||||||
|
|
||||||
|
Для HOLD больше не отображается confidence, так как он не несёт торгового смысла.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. BUY / SELL signal UI
|
||||||
|
|
||||||
|
Для BUY:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Сигнал 🟢 BUY · 12с
|
||||||
|
Уверенность · 0.74
|
||||||
|
```
|
||||||
|
|
||||||
|
После подтверждения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Сигнал 🟢 BUY · READY
|
||||||
|
Уверенность · 0.88
|
||||||
|
```
|
||||||
|
|
||||||
|
Для SELL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Сигнал 🔴 SELL · 9с
|
||||||
|
Уверенность · 0.71
|
||||||
|
```
|
||||||
|
|
||||||
|
После подтверждения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Сигнал 🔴 SELL · READY
|
||||||
|
Уверенность · 0.91
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Direction-aware order block
|
||||||
|
|
||||||
|
Для HOLD:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BTC · Trend · x2
|
||||||
|
Цена · $ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Для BUY:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🟢 BTC · Trend · LONG x2
|
||||||
|
Цена входа · $ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Для SELL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🔴 BTC · Trend · SHORT x2
|
||||||
|
Цена входа · $ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Убран UI-шум без открытой позиции
|
||||||
|
|
||||||
|
Если позиция не открыта, экран больше не показывает:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Зарезервировано · $ 0
|
||||||
|
P&L · $ 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Эти строки остаются только для активной позиции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Realistic bid / ask pricing в UI
|
||||||
|
|
||||||
|
UI теперь использует market snapshot:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ExchangeService().get_market_snapshot(symbol)
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HOLD → last_price
|
||||||
|
BUY / LONG → ask_price
|
||||||
|
SELL / SHORT → bid_price
|
||||||
|
```
|
||||||
|
|
||||||
|
Если bid / ask недоступны, используется fallback на `last_price`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. TREND strategy переведена на mid price
|
||||||
|
|
||||||
|
Стратегия TREND теперь анализирует рынок по:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mid_price = (bid_price + ask_price) / 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Fallback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
last_price
|
||||||
|
```
|
||||||
|
|
||||||
|
Это делает сигнал более стабильным и не искажает анализ spread-ом.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Realistic paper execution pricing
|
||||||
|
|
||||||
|
ExecutionEngine теперь использует более реалистичные цены:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LONG entry → ask_price
|
||||||
|
SHORT entry → bid_price
|
||||||
|
LONG exit → bid_price
|
||||||
|
SHORT exit → ask_price
|
||||||
|
```
|
||||||
|
|
||||||
|
Flip теперь состоит из:
|
||||||
|
|
||||||
|
```text
|
||||||
|
exit текущей позиции по стороне
|
||||||
|
+
|
||||||
|
entry новой позиции по стороне
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Own funds / margin calculation fix
|
||||||
|
|
||||||
|
Исправлен расчёт собственных средств:
|
||||||
|
|
||||||
|
```text
|
||||||
|
own_funds = position_notional / leverage
|
||||||
|
```
|
||||||
|
|
||||||
|
А не через статичный процент от allocated balance.
|
||||||
|
|
||||||
|
Также size округляется вниз, чтобы reserved margin не превышал лимит.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Signal duration
|
||||||
|
|
||||||
|
Добавлена поддержка реального времени удержания сигнала через:
|
||||||
|
|
||||||
|
```python
|
||||||
|
signal_started_at
|
||||||
|
```
|
||||||
|
|
||||||
|
Если timestamp недоступен, используется fallback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
last_signal_repeat_count × 5 sec
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Debug UI states
|
||||||
|
|
||||||
|
Добавлены команды для проверки UI-состояний:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_auto hold 335
|
||||||
|
/debug_auto buy 12 0.74
|
||||||
|
/debug_auto buy_ready 0.88
|
||||||
|
/debug_auto sell 9 0.71
|
||||||
|
/debug_auto sell_ready 0.91
|
||||||
|
/debug_auto long
|
||||||
|
/debug_auto short
|
||||||
|
/debug_auto reset
|
||||||
|
/debug_auto state
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Debug paper execution
|
||||||
|
|
||||||
|
Добавлены команды разового paper execution:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_exec buy
|
||||||
|
/debug_exec sell
|
||||||
|
/debug_exec flip
|
||||||
|
/debug_exec flip_buy
|
||||||
|
/debug_exec flip_sell
|
||||||
|
/debug_exec close
|
||||||
|
/debug_exec state
|
||||||
|
```
|
||||||
|
|
||||||
|
`/debug_exec flip` автоматически определяет текущую позицию:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LONG → SELL / SHORT
|
||||||
|
SHORT → BUY / LONG
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Debug live paper test
|
||||||
|
|
||||||
|
Добавлен live debug режим:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_live buy
|
||||||
|
/debug_live sell
|
||||||
|
/debug_live flip
|
||||||
|
/debug_live close
|
||||||
|
/debug_live stop
|
||||||
|
/debug_live state
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот режим открывает paper-позицию и запускает обычный AutoTradeRunner, чтобы проверить:
|
||||||
|
|
||||||
|
- мониторинг рынка
|
||||||
|
- обновление UI
|
||||||
|
- P&L
|
||||||
|
- SL / TP / ML
|
||||||
|
- flip по стратегии
|
||||||
|
- execution alerts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Auto screen runner diagnostics
|
||||||
|
|
||||||
|
В debug state добавлена диагностика AutoTradeRunner:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Screen
|
||||||
|
Chat ID
|
||||||
|
Message ID
|
||||||
|
Has bot
|
||||||
|
Has render_text
|
||||||
|
Task running
|
||||||
|
```
|
||||||
|
|
||||||
|
Это помогает быстро понять, зарегистрирован ли экран автоторговли для live refresh.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектурный результат
|
||||||
|
|
||||||
|
Теперь:
|
||||||
|
|
||||||
|
- экран RUNNING стал компактнее и понятнее
|
||||||
|
- HOLD не перегружает UI
|
||||||
|
- BUY / SELL отображают направление LONG / SHORT
|
||||||
|
- расчёты preview используют bid / ask
|
||||||
|
- strategy анализирует mid price
|
||||||
|
- paper execution использует bid / ask для входа и выхода
|
||||||
|
- active position screen обновляется live
|
||||||
|
- debug-команды позволяют тестировать UI и execution быстрее
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ограничения текущей реализации
|
||||||
|
|
||||||
|
Текущий debug live режим пока не изолирован.
|
||||||
|
|
||||||
|
Он вмешивается в реальные runtime-компоненты:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AutoTradeState
|
||||||
|
ExecutionEngine._position
|
||||||
|
AutoTradeRunner
|
||||||
|
EventBus
|
||||||
|
Auto screen
|
||||||
|
```
|
||||||
|
|
||||||
|
Это удобно для integration testing, но не является полноценным sandbox.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Следующий этап
|
||||||
|
|
||||||
|
07.4.3.15 — Isolated Debug Runtime
|
||||||
|
|
||||||
|
Планируется:
|
||||||
|
|
||||||
|
- отделить debug runtime от обычной автоторговли
|
||||||
|
- создать отдельный DebugAutoState
|
||||||
|
- создать отдельный DebugRunner
|
||||||
|
- создать отдельный DebugExecutionEngine
|
||||||
|
- создать отдельный debug screen
|
||||||
|
- исключить влияние debug-команд на AutoTradeService
|
||||||
|
- маркировать debug-логи и уведомления как `[DEBUG]`
|
||||||
|
- оставить `/debug_live` только как временный legacy integration test
|
||||||
Reference in New Issue
Block a user