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

@@ -9,4 +9,19 @@ class OrderDraft:
side: str
order_type: str
quantity: str
price: str | None = None
status: str = "draft"
@dataclass(slots=True)
class OrderEntryContext:
symbol: str
side: str
order_type: str
balance_currency: str
available_balance: float
reference_price: float
last_price: float
bid_price: float
ask_price: float
quantity_presets: list[str]

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from src.core.config import load_settings
from src.integrations.exchange.service import ExchangeService
from src.storage.repositories.order_drafts import OrderDraftRepository
from src.trading.journal.service import JournalService
from src.trading.orders.models import OrderDraft
from src.trading.orders.models import OrderDraft, OrderEntryContext
class OrderDraftsService:
@@ -11,29 +12,39 @@ class OrderDraftsService:
self.settings = load_settings()
self.repository = OrderDraftRepository()
self.journal = JournalService()
self.exchange = ExchangeService()
def create_default_draft(self) -> OrderDraft:
draft = OrderDraft(
def build_draft(
self,
*,
side: str,
order_type: str,
quantity: str,
price: str | None = None,
) -> OrderDraft:
return OrderDraft(
symbol=self.settings.default_symbol,
side="BUY",
order_type="MARKET",
quantity="0.001",
side=side.upper(),
order_type=order_type.upper(),
quantity=quantity,
price=price,
status="draft",
)
self._save_draft(draft)
return draft
def _save_draft(self, draft: OrderDraft) -> None:
def save_draft(self, draft: OrderDraft) -> None:
payload = {
"source": "trade_screen",
"mode": "draft_only",
"price": draft.price,
}
self.repository.add_draft(
symbol=draft.symbol,
side=draft.side,
order_type=draft.order_type,
quantity=draft.quantity,
status=draft.status,
payload={
"source": "trade_screen",
"mode": "draft_only",
},
payload=payload,
)
try:
@@ -45,6 +56,7 @@ class OrderDraftsService:
"side": draft.side,
"order_type": draft.order_type,
"quantity": draft.quantity,
"price": draft.price,
"status": draft.status,
},
)
@@ -53,3 +65,99 @@ class OrderDraftsService:
def list_recent_drafts(self, limit: int = 5) -> list[dict[str, str]]:
return self.repository.list_recent_drafts(limit=limit)
def get_entry_context(self, *, side: str, order_type: str) -> OrderEntryContext:
validation = self.exchange.validate_symbol(self.settings.default_symbol)
if not validation.is_valid or validation.symbol_info is None:
raise ValueError(validation.message)
balances = self.exchange.get_balance_summary()
market = self.exchange.get_market_snapshot(self.settings.default_symbol)
base_asset = validation.symbol_info.base_asset or "BASE"
quote_asset = validation.symbol_info.quote_asset or "QUOTE"
available_by_currency = {
item.currency.upper(): float(item.available)
for item in balances
}
side_upper = side.upper()
order_type_upper = order_type.upper()
if side_upper == "BUY":
balance_currency = quote_asset.upper()
available_balance = available_by_currency.get(balance_currency, 0.0)
reference_price = float(market["ask_price"])
max_qty = (available_balance / reference_price) if reference_price > 0 else 0.0
else:
balance_currency = base_asset.upper()
available_balance = available_by_currency.get(balance_currency, 0.0)
reference_price = float(market["bid_price"])
max_qty = available_balance
quantity_presets = [
self._format_number(max_qty * 0.25),
self._format_number(max_qty * 0.50),
self._format_number(max_qty * 0.75),
self._format_number(max_qty),
]
return OrderEntryContext(
symbol=self.settings.default_symbol,
side=side_upper,
order_type=order_type_upper,
balance_currency=balance_currency,
available_balance=available_balance,
reference_price=reference_price,
last_price=float(market["last_price"]),
bid_price=float(market["bid_price"]),
ask_price=float(market["ask_price"]),
quantity_presets=quantity_presets,
)
@staticmethod
def normalize_side(raw: str) -> str | None:
value = (raw or "").strip().upper()
if value in {"BUY", "SELL"}:
return value
return None
@staticmethod
def normalize_order_type(raw: str) -> str | None:
value = (raw or "").strip().upper()
if value in {"MARKET", "LIMIT"}:
return value
return None
@staticmethod
def normalize_quantity(raw: str) -> str | None:
value = (raw or "").strip().replace(",", ".")
if not value:
return None
try:
quantity = float(value)
except ValueError:
return None
if quantity <= 0:
return None
return value
@staticmethod
def normalize_price(raw: str) -> str | None:
value = (raw or "").strip().replace(",", ".")
if not value:
return None
try:
price = float(value)
except ValueError:
return None
if price <= 0:
return None
return value
@staticmethod
def _format_number(value: float) -> str:
text = f"{value:.8f}"
text = text.rstrip("0").rstrip(".")
return text or "0"

View File

@@ -0,0 +1,8 @@
from aiogram.fsm.state import State, StatesGroup
class NewOrderDraftStates(StatesGroup):
waiting_side = State()
waiting_type = State()
waiting_quantity = State()
waiting_price = State()