Stage 05.2 - interactive draft builder
This commit is contained in:
@@ -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]
|
||||
@@ -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"
|
||||
8
app/src/trading/orders/states.py
Normal file
8
app/src/trading/orders/states.py
Normal 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()
|
||||
Reference in New Issue
Block a user