Stage 05.2 - interactive draft builder
This commit is contained in:
@@ -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)
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user