Stage 05.6 - order draft logic improvements

This commit is contained in:
2026-04-18 20:45:33 +03:00
parent 2be2ac1d30
commit 39b35d742a
6 changed files with 413 additions and 38 deletions

View File

@@ -1,3 +1,5 @@
# app/src/integrations/exchange/models.py
from __future__ import annotations
from dataclasses import dataclass
@@ -36,6 +38,9 @@ class ExchangeSymbol:
market_modes: list[str]
market_type: str
tick_size: float | None
step_size: float | None
min_qty: float | None
min_notional: float | None
@dataclass(slots=True)
@@ -50,4 +55,4 @@ class SymbolValidationResult:
@dataclass(slots=True)
class PrivateAuthHealth:
ok: bool
message: str
message: str

View File

@@ -1,3 +1,5 @@
# app/src/integrations/exchange/service.py
from __future__ import annotations
from datetime import datetime
@@ -227,28 +229,85 @@ class ExchangeService:
if isinstance(inner, dict) and isinstance(inner.get("symbols"), list):
symbols_raw = inner["symbols"]
else:
raise ExchangeError(
"Field 'symbols' is missing in exchangeInfo response."
)
raise ExchangeError("Field 'symbols' is missing in exchangeInfo response.")
def _safe_str(value: object, default: str = "") -> str:
if value is None:
return default
return str(value).strip()
def _safe_float(value: object) -> float | None:
if value in (None, ""):
return None
try:
return float(str(value).strip())
except (TypeError, ValueError):
return None
def _extract_filter_value(
filters: object,
filter_names: list[str],
keys: list[str],
) -> float | None:
if not isinstance(filters, list):
return None
normalized_filter_names = {name.upper() for name in filter_names}
for entry in filters:
if not isinstance(entry, dict):
continue
filter_type = str(entry.get("filterType", "")).strip().upper()
if filter_type not in normalized_filter_names:
continue
for key in keys:
value = _safe_float(entry.get(key))
if value is not None:
return value
return None
items: list[ExchangeSymbol] = []
for item in symbols_raw:
if not isinstance(item, dict):
continue
tick_size_raw = item.get("tickSize")
tick_size = None
if tick_size_raw not in (None, ""):
try:
tick_size = float(str(tick_size_raw))
except (TypeError, ValueError):
tick_size = None
filters = item.get("filters")
tick_size = _safe_float(item.get("tickSize"))
if tick_size is None:
tick_size = _extract_filter_value(
filters,
filter_names=["PRICE_FILTER"],
keys=["tickSize"],
)
step_size = _safe_float(item.get("stepSize"))
if step_size is None:
step_size = _extract_filter_value(
filters,
filter_names=["LOT_SIZE", "MARKET_LOT_SIZE"],
keys=["stepSize"],
)
min_qty = _safe_float(item.get("minQty"))
if min_qty is None:
min_qty = _extract_filter_value(
filters,
filter_names=["LOT_SIZE", "MARKET_LOT_SIZE"],
keys=["minQty"],
)
min_notional = _safe_float(item.get("minNotional"))
if min_notional is None:
min_notional = _extract_filter_value(
filters,
filter_names=["MIN_NOTIONAL", "NOTIONAL"],
keys=["minNotional", "notional"],
)
market_modes_raw = item.get("marketModes")
if isinstance(market_modes_raw, list):
@@ -259,7 +318,11 @@ class ExchangeService:
market_modes = []
market_type_raw = item.get("marketType")
market_type = str(market_type_raw).strip() if market_type_raw is not None else "unknown"
market_type = (
str(market_type_raw).strip()
if market_type_raw is not None
else "unknown"
)
items.append(
ExchangeSymbol(
@@ -271,6 +334,9 @@ class ExchangeService:
market_modes=market_modes,
market_type=market_type,
tick_size=tick_size,
step_size=step_size,
min_qty=min_qty,
min_notional=min_notional,
)
)