Stage 05.8 - quantity normalization by exchange rules
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, InvalidOperation, ROUND_DOWN
|
||||
from decimal import Decimal, InvalidOperation, ROUND_DOWN, ROUND_UP
|
||||
|
||||
from src.core.config import load_settings
|
||||
from src.integrations.exchange.models import ExchangeSymbol
|
||||
@@ -181,8 +181,6 @@ class OrderDraftsService:
|
||||
return self.repository.get_draft_by_id(draft_id)
|
||||
|
||||
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)
|
||||
@@ -307,6 +305,103 @@ class OrderDraftsService:
|
||||
|
||||
return errors
|
||||
|
||||
def normalize_preset_quantity(
|
||||
self,
|
||||
*,
|
||||
side: str,
|
||||
order_type: str,
|
||||
raw_quantity: str,
|
||||
price: str | None = None,
|
||||
) -> str | None:
|
||||
return self._normalize_entry_quantity_with_rules(
|
||||
side=side,
|
||||
order_type=order_type,
|
||||
raw_quantity=raw_quantity,
|
||||
price=price,
|
||||
raise_to_minimum=True,
|
||||
)
|
||||
|
||||
def normalize_entry_quantity(
|
||||
self,
|
||||
*,
|
||||
side: str,
|
||||
order_type: str,
|
||||
raw_quantity: str,
|
||||
price: str | None = None,
|
||||
) -> str | None:
|
||||
return self._normalize_entry_quantity_with_rules(
|
||||
side=side,
|
||||
order_type=order_type,
|
||||
raw_quantity=raw_quantity,
|
||||
price=price,
|
||||
raise_to_minimum=True,
|
||||
)
|
||||
|
||||
def _normalize_entry_quantity_with_rules(
|
||||
self,
|
||||
*,
|
||||
side: str,
|
||||
order_type: str,
|
||||
raw_quantity: str,
|
||||
price: str | None = None,
|
||||
raise_to_minimum: bool,
|
||||
) -> str | None:
|
||||
validation = self.exchange.validate_symbol(self.settings.default_symbol)
|
||||
if not validation.is_valid or validation.symbol_info is None:
|
||||
return self.normalize_quantity(raw_quantity)
|
||||
|
||||
original_quantity = self._to_decimal((raw_quantity or "").strip().replace(",", "."))
|
||||
if original_quantity is None or original_quantity <= 0:
|
||||
return None
|
||||
|
||||
symbol_info = validation.symbol_info
|
||||
step_size = self._to_decimal(getattr(symbol_info, "step_size", None))
|
||||
min_qty = self._to_decimal(getattr(symbol_info, "min_qty", None))
|
||||
min_notional = self._to_decimal(getattr(symbol_info, "min_notional", None))
|
||||
|
||||
minimum_allowed = min_qty if min_qty is not None and min_qty > 0 else None
|
||||
|
||||
reference_price = self._resolve_reference_price_for_entry(
|
||||
side=side,
|
||||
order_type=order_type,
|
||||
price=price,
|
||||
)
|
||||
if (
|
||||
reference_price is not None
|
||||
and reference_price > 0
|
||||
and min_notional is not None
|
||||
and min_notional > 0
|
||||
):
|
||||
min_by_notional = min_notional / reference_price
|
||||
if step_size is not None and step_size > 0:
|
||||
min_by_notional = self._ceil_to_step(min_by_notional, step_size)
|
||||
|
||||
if minimum_allowed is None or min_by_notional > minimum_allowed:
|
||||
minimum_allowed = min_by_notional
|
||||
|
||||
quantity = original_quantity
|
||||
|
||||
if step_size is not None and step_size > 0:
|
||||
quantity = self._floor_to_step(quantity, step_size)
|
||||
|
||||
if quantity <= 0:
|
||||
if raise_to_minimum and minimum_allowed is not None and minimum_allowed > 0:
|
||||
quantity = minimum_allowed
|
||||
if step_size is not None and step_size > 0 and not self._fits_step(quantity, step_size):
|
||||
quantity = self._ceil_to_step(quantity, step_size)
|
||||
else:
|
||||
return None
|
||||
|
||||
if raise_to_minimum and minimum_allowed is not None and quantity < minimum_allowed:
|
||||
quantity = minimum_allowed
|
||||
if step_size is not None and step_size > 0 and not self._fits_step(quantity, step_size):
|
||||
quantity = self._ceil_to_step(quantity, step_size)
|
||||
|
||||
if quantity <= 0:
|
||||
return None
|
||||
|
||||
return self._format_decimal(quantity)
|
||||
|
||||
def _build_quantity_presets(
|
||||
self,
|
||||
*,
|
||||
@@ -452,6 +547,13 @@ class OrderDraftsService:
|
||||
ratio = (value / step).to_integral_value(rounding=ROUND_DOWN)
|
||||
return ratio * step
|
||||
|
||||
@staticmethod
|
||||
def _ceil_to_step(value: Decimal, step: Decimal) -> Decimal:
|
||||
if step <= 0:
|
||||
return value
|
||||
ratio = (value / step).to_integral_value(rounding=ROUND_UP)
|
||||
return ratio * step
|
||||
|
||||
def _normalize_quantity_to_exchange_rules(
|
||||
self,
|
||||
*,
|
||||
@@ -498,7 +600,9 @@ class OrderDraftsService:
|
||||
price: str | None = None,
|
||||
) -> Decimal | None:
|
||||
if order_type.upper() == "LIMIT":
|
||||
return self._to_decimal(price)
|
||||
explicit_price = self._to_decimal(price)
|
||||
if explicit_price is not None and explicit_price > 0:
|
||||
return explicit_price
|
||||
|
||||
try:
|
||||
market = self.exchange.get_market_snapshot(self.settings.default_symbol)
|
||||
|
||||
Reference in New Issue
Block a user