Stage 05.2 - interactive draft builder

This commit is contained in:
2026-04-17 09:05:31 +03:00
parent f662ff1901
commit f48effd9b5
8 changed files with 761 additions and 41 deletions

View File

@@ -1,12 +0,0 @@
from aiogram import F, Router
from aiogram.types import Message
from src.telegram.menus import TRADE_TEXT
router = Router(name="trade")
@router.message(F.text == "⚡ Торговля")
async def open_trade(message: Message) -> None:
await message.answer(TRADE_TEXT)

View File

@@ -1,29 +1,391 @@
from __future__ import annotations
from aiogram import Router
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.trading.orders.service import OrderDraftsService
from src.trading.orders.states import NewOrderDraftStates
router = Router(name="trade_new_order")
@router.message(Command("new_order"))
async def create_new_order_draft(message: Message) -> None:
service = OrderDraftsService()
draft = service.create_default_draft()
def _side_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="🟢 BUY", callback_data="order_side:BUY")
builder.button(text="🔴 SELL", callback_data="order_side:SELL")
builder.button(text="✖️ Отмена", callback_data="order_cancel")
builder.adjust(2, 1)
return builder.as_markup()
text = (
"<b>📝 Черновик ордера создан</b>\n\n"
f"• инструмент: {draft.symbol}\n"
f"• сторона: {draft.side}\n"
f"• тип: {draft.order_type}\n"
f"• количество: {draft.quantity}\n"
f"• статус: {draft.status}\n\n"
"Это тестовый draft flow. Реальный ордер не отправлялся."
def _type_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="⚡ MARKET", callback_data="order_type:MARKET")
builder.button(text="🎯 LIMIT", callback_data="order_type:LIMIT")
builder.button(text="✖️ Отмена", callback_data="order_cancel")
builder.adjust(2, 1)
return builder.as_markup()
def _cancel_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="✖️ Отмена", callback_data="order_cancel")
return builder.as_markup()
def _quantity_keyboard(presets: list[str]) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
labels = ["25%", "50%", "75%", "100%"]
for label, value in zip(labels, presets):
builder.button(text=label, callback_data=f"order_qty:{value}")
builder.button(text="✍️ Ввести вручную", callback_data="order_qty:manual")
builder.button(text="✖️ Отмена", callback_data="order_cancel")
builder.adjust(2, 2, 1)
return builder.as_markup()
def _price_keyboard(bid: float, ask: float, last: float) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text=f"Bid {bid:.2f}", callback_data=f"order_price:{bid}")
builder.button(text=f"Ask {ask:.2f}", callback_data=f"order_price:{ask}")
builder.button(text=f"Last {last:.2f}", callback_data=f"order_price:{last}")
builder.button(text="✍️ Ввести вручную", callback_data="order_price:manual")
builder.button(text="✖️ Отмена", callback_data="order_cancel")
builder.adjust(2, 1, 1, 1)
return builder.as_markup()
def _render_draft_summary(
symbol: str,
side: str,
order_type: str,
quantity: str,
price: str | None,
) -> str:
lines = [
"<b>📝 Черновик ордера создан</b>",
"",
f"• инструмент: {symbol}",
f"• сторона: {side}",
f"• тип: {order_type}",
f"• количество: {quantity}",
]
if price:
lines.append(f"• цена: {price}")
lines.extend(
[
"• статус: draft",
"",
"Это тестовый draft flow. Реальный ордер не отправлялся.",
]
)
return "\n".join(lines)
@router.message(Command("cancel_order"))
async def cancel_order_builder(message: Message, state: FSMContext) -> None:
await state.clear()
await message.answer(
"<b>⚡ Торговля</b>\n\n"
"Создание черновика отменено."
)
@router.callback_query(F.data == "order_cancel")
async def cancel_order_builder_callback(
callback: CallbackQuery,
state: FSMContext,
) -> None:
await state.clear()
await callback.message.edit_text(
"<b>⚡ Торговля</b>\n\n"
"Создание черновика отменено."
)
await callback.answer()
@router.message(Command("new_order"))
async def start_new_order_draft(message: Message, state: FSMContext) -> None:
await state.clear()
await state.set_state(NewOrderDraftStates.waiting_side)
await message.answer(
"<b>⚡ Новый черновик ордера</b>\n\n"
"Шаг 1/4\n"
"Выберите сторону:",
reply_markup=_side_keyboard(),
)
@router.callback_query(
NewOrderDraftStates.waiting_side,
F.data.startswith("order_side:"),
)
async def process_order_side_callback(
callback: CallbackQuery,
state: FSMContext,
) -> None:
side = callback.data.split(":", 1)[1]
await state.update_data(side=side)
await state.set_state(NewOrderDraftStates.waiting_type)
await callback.message.edit_text(
"<b>⚡ Новый черновик ордера</b>\n\n"
"Шаг 2/4\n"
"Выберите тип ордера:",
reply_markup=_type_keyboard(),
)
await callback.answer()
@router.message(NewOrderDraftStates.waiting_side)
async def process_order_side_text(message: Message) -> None:
await message.answer(
"Пожалуйста, используйте кнопки для выбора стороны.",
reply_markup=_side_keyboard(),
)
@router.callback_query(
NewOrderDraftStates.waiting_type,
F.data.startswith("order_type:"),
)
async def process_order_type_callback(
callback: CallbackQuery,
state: FSMContext,
) -> None:
order_type = callback.data.split(":", 1)[1]
service = OrderDraftsService()
data = await state.get_data()
side = data.get("side", "BUY")
await state.update_data(order_type=order_type)
await state.set_state(NewOrderDraftStates.waiting_quantity)
context = service.get_entry_context(side=side, order_type=order_type)
await callback.message.edit_text(
"<b>⚡ Новый черновик ордера</b>\n\n"
"Шаг 3/4\n"
f"Инструмент: <b>{context.symbol}</b>\n"
f"Доступно: <b>{context.available_balance:.8f} {context.balance_currency}</b>\n"
f"Ориентир цены: <b>{context.reference_price:.2f}</b>\n\n"
"Выберите количество или введите его вручную:",
reply_markup=_quantity_keyboard(context.quantity_presets),
)
await callback.answer()
@router.message(NewOrderDraftStates.waiting_type)
async def process_order_type_text(message: Message) -> None:
await message.answer(
"Пожалуйста, используйте кнопки для выбора типа ордера.",
reply_markup=_type_keyboard(),
)
@router.callback_query(
NewOrderDraftStates.waiting_quantity,
F.data.startswith("order_qty:"),
)
async def process_quantity_callback(
callback: CallbackQuery,
state: FSMContext,
) -> None:
value = callback.data.split(":", 1)[1]
if value == "manual":
await callback.message.edit_text(
"<b>⚡ Новый черновик ордера</b>\n\n"
"Шаг 3/4\n"
"Введите количество вручную, например: <b>0.001</b>",
reply_markup=_cancel_keyboard(),
)
await callback.answer()
return
service = OrderDraftsService()
quantity = service.normalize_quantity(value)
if quantity is None:
await callback.answer("Некорректное значение количества.", show_alert=True)
return
data = await state.get_data()
order_type = data.get("order_type", "MARKET")
await state.update_data(quantity=quantity)
if order_type == "LIMIT":
context = service.get_entry_context(side=data["side"], order_type=order_type)
await state.set_state(NewOrderDraftStates.waiting_price)
await callback.message.edit_text(
"<b>⚡ Новый черновик ордера</b>\n\n"
"Шаг 4/4\n"
f"Bid: <b>{context.bid_price:.2f}</b>\n"
f"Ask: <b>{context.ask_price:.2f}</b>\n"
f"Last: <b>{context.last_price:.2f}</b>\n\n"
"Выберите цену или введите её вручную:",
reply_markup=_price_keyboard(
bid=context.bid_price,
ask=context.ask_price,
last=context.last_price,
),
)
await callback.answer()
return
draft = service.build_draft(
side=data["side"],
order_type=order_type,
quantity=quantity,
)
service.save_draft(draft)
await state.clear()
await callback.message.edit_text(
_render_draft_summary(
symbol=draft.symbol,
side=draft.side,
order_type=draft.order_type,
quantity=draft.quantity,
price=draft.price,
)
)
await callback.answer()
@router.message(NewOrderDraftStates.waiting_quantity)
async def process_order_quantity(message: Message, state: FSMContext) -> None:
service = OrderDraftsService()
quantity = service.normalize_quantity(message.text or "")
if quantity is None:
await message.answer("Введите корректное количество, например: 0.001")
return
data = await state.get_data()
order_type = data.get("order_type", "MARKET")
await state.update_data(quantity=quantity)
if order_type == "LIMIT":
context = service.get_entry_context(side=data["side"], order_type=order_type)
await state.set_state(NewOrderDraftStates.waiting_price)
await message.answer(
"<b>⚡ Новый черновик ордера</b>\n\n"
"Шаг 4/4\n"
f"Bid: <b>{context.bid_price:.2f}</b>\n"
f"Ask: <b>{context.ask_price:.2f}</b>\n"
f"Last: <b>{context.last_price:.2f}</b>\n\n"
"Выберите цену или введите её вручную:",
reply_markup=_price_keyboard(
bid=context.bid_price,
ask=context.ask_price,
last=context.last_price,
),
)
return
draft = service.build_draft(
side=data["side"],
order_type=order_type,
quantity=quantity,
)
service.save_draft(draft)
await state.clear()
await message.answer(
_render_draft_summary(
symbol=draft.symbol,
side=draft.side,
order_type=draft.order_type,
quantity=draft.quantity,
price=draft.price,
)
)
@router.callback_query(
NewOrderDraftStates.waiting_price,
F.data.startswith("order_price:"),
)
async def process_price_callback(
callback: CallbackQuery,
state: FSMContext,
) -> None:
value = callback.data.split(":", 1)[1]
if value == "manual":
await callback.message.edit_text(
"<b>⚡ Новый черновик ордера</b>\n\n"
"Шаг 4/4\n"
"Введите цену вручную, например: <b>73000</b>",
reply_markup=_cancel_keyboard(),
)
await callback.answer()
return
service = OrderDraftsService()
price = service.normalize_price(value)
if price is None:
await callback.answer("Некорректная цена.", show_alert=True)
return
data = await state.get_data()
draft = service.build_draft(
side=data["side"],
order_type=data["order_type"],
quantity=data["quantity"],
price=price,
)
service.save_draft(draft)
await state.clear()
await callback.message.edit_text(
_render_draft_summary(
symbol=draft.symbol,
side=draft.side,
order_type=draft.order_type,
quantity=draft.quantity,
price=draft.price,
)
)
await callback.answer()
@router.message(NewOrderDraftStates.waiting_price)
async def process_order_price(message: Message, state: FSMContext) -> None:
service = OrderDraftsService()
price = service.normalize_price(message.text or "")
if price is None:
await message.answer("Введите корректную цену, например: 73000")
return
data = await state.get_data()
draft = service.build_draft(
side=data["side"],
order_type=data["order_type"],
quantity=data["quantity"],
price=price,
)
service.save_draft(draft)
await state.clear()
await message.answer(
_render_draft_summary(
symbol=draft.symbol,
side=draft.side,
order_type=draft.order_type,
quantity=draft.quantity,
price=draft.price,
)
)
await message.answer(text)
@router.message(Command("drafts"))
@@ -50,4 +412,4 @@ async def show_recent_drafts(message: Message) -> None:
]
)
await message.answer("\n".join(lines).rstrip())
await message.answer("\n".join(lines).rstrip())