Files

95 строки
4.8 KiB
Python

import json
import math
from datetime import datetime, timezone
from typing import Any
from sqlalchemy.orm import Session
from app.coinex import CoinExClient
from app.models import MarketRule
class MarketRuleService:
def __init__(self, coinex: CoinExClient) -> None:
self.coinex = coinex
async def sync(self, db: Session, markets: list[str] | None = None, quote_asset: str = 'USDT') -> dict[str, Any]:
payload = await self.coinex.get_market_info()
rows = payload.get('data') or []
if isinstance(rows, dict):
rows = [rows]
wanted = {m.upper() for m in markets or []}
synced = 0
active_markets: list[str] = []
for row in rows:
if not isinstance(row, dict):
continue
market = str(row.get('market') or row.get('name') or '').upper()
if not market:
continue
quote = str(row.get('quote_ccy') or row.get('quote_currency') or row.get('quote') or quote_asset).upper()
if wanted and market not in wanted:
continue
if not wanted and quote != quote_asset.upper():
continue
rule = db.query(MarketRule).filter(MarketRule.market == market).first()
if rule is None:
rule = MarketRule(market=market)
db.add(rule)
rule.base_asset = str(row.get('base_ccy') or row.get('base_currency') or row.get('base') or market.replace(quote, '')).upper()
rule.quote_asset = quote
rule.min_amount = self._float(row.get('min_amount') or row.get('min_base_amount') or row.get('min_asset_amount'), 0.0)
rule.min_quote_amount = self._float(row.get('min_quote_amount') or row.get('min_amount_value') or row.get('min_value'), 0.0)
rule.amount_precision = self._int(row.get('amount_precision') or row.get('base_ccy_precision') or row.get('trading_precision'), 8)
rule.price_precision = self._int(row.get('price_precision') or row.get('quote_ccy_precision'), 8)
rule.maker_fee_rate = self._float(row.get('maker_fee_rate') or row.get('maker_fee') or row.get('maker'), 0.002)
rule.taker_fee_rate = self._float(row.get('taker_fee_rate') or row.get('taker_fee') or row.get('taker'), 0.002)
rule.is_trading_enabled = str(row.get('status') or row.get('state') or 'online').lower() not in {'offline', 'disabled', 'suspend'}
rule.raw_json = json.dumps(row, ensure_ascii=False)
rule.synced_at = datetime.now(timezone.utc)
synced += 1
if rule.is_trading_enabled:
active_markets.append(market)
db.commit()
return {'synced': synced, 'markets': active_markets}
def active_markets(self, db: Session, quote_asset: str = 'USDT', fallback: list[str] | None = None) -> list[str]:
rows = db.query(MarketRule).filter(MarketRule.quote_asset == quote_asset.upper(), MarketRule.is_trading_enabled.is_(True)).order_by(MarketRule.market.asc()).all()
markets = [row.market for row in rows]
return markets or list(fallback or [])
def get(self, db: Session, market: str) -> MarketRule | None:
return db.query(MarketRule).filter(MarketRule.market == market.upper()).first()
def ensure_amount(self, db: Session, market: str, quote_amount: float, price: float) -> tuple[float, str]:
rule = self.get(db, market)
if price <= 0:
raise ValueError('price must be positive')
raw_amount = quote_amount / price
if rule is None:
return raw_amount, 'Лимиты CoinEx еще не синхронизированы, используется расчет без округления.'
amount = self.floor_amount(raw_amount, rule.amount_precision)
min_amount = rule.min_amount or 0.0
min_quote = rule.min_quote_amount or 0.0
if min_amount and amount < min_amount:
raise ValueError(f'amount ниже лимита CoinEx: {amount} < {min_amount}')
if min_quote and amount * price < min_quote:
raise ValueError(f'quote amount ниже лимита CoinEx: {amount * price:.8f} < {min_quote}')
return amount, f'Лимиты CoinEx применены: amount_precision={rule.amount_precision}, min_amount={rule.min_amount}, min_quote={rule.min_quote_amount}, taker_fee={rule.taker_fee_rate}'
def floor_amount(self, amount: float, precision: int) -> float:
factor = 10 ** max(0, precision)
return math.floor(amount * factor) / factor
def _float(self, value: Any, default: float) -> float:
try:
return float(value)
except (TypeError, ValueError):
return default
def _int(self, value: Any, default: int) -> int:
try:
return int(value)
except (TypeError, ValueError):
return default