Розробка системи фіксації курсу обміну
Фіксація курсу (rate lock / rate fixing) — це механізм гарантії конкретного обмінного курсу на певний час. Користувач бачить курс 1 BTC = 50,000 USDT і має 10 хвилин для проведення угоди саме по цьому курсу, незалежно від рухання ринку. Для обмінника це ризик — ринок може піти проти вас. Для користувача — передбачуваність операції.
Навіщо потрібна фіксація курсу
Без фіксації: користувач починає процес обміну, відправляє BTC, за цей час курс впав на 2%, отримує менше USDT, ніж очікував. Негативний UX, скарги, відтік.
З фіксацією: користувач бачить гарантовану суму, усвідомлено приймає рішення, отримує саме те, що було обіцяно. Довіра та утримання вищі.
Архітектура системи фіксації курсу
Структура Rate Lock
from dataclasses import dataclass
from decimal import Decimal
from datetime import datetime, timedelta
import uuid
@dataclass
class LockedRate:
lock_id: str
from_currency: str
to_currency: str
from_amount: Decimal
to_amount: Decimal # гарантована сума
rate: Decimal # зафіксований курс
market_rate_at_lock: Decimal # рыночний курс у момент фіксації
our_margin: Decimal # наша маржа в to_currency
locked_at: datetime
expires_at: datetime
status: str = 'active' # active / used / expired / cancelled
class RateLockService:
def __init__(self, price_feed, margin_calculator, risk_manager):
self.price_feed = price_feed
self.margin_calc = margin_calculator
self.risk = risk_manager
async def create_rate_lock(
self,
from_currency: str,
to_currency: str,
from_amount: Decimal,
lock_duration_seconds: int = 600
) -> LockedRate:
# Отримуємо поточний рыночний курс
market_rate = await self.price_feed.get_rate(from_currency, to_currency)
# Розраховуємо нашу маржу з буфером на волатильність
margin = self.margin_calc.calculate(
from_currency=from_currency,
to_currency=to_currency,
from_amount=from_amount,
lock_duration=lock_duration_seconds
)
# Гарантований курс = рыночний мінус маржа
locked_rate = market_rate * (1 - margin)
to_amount = from_amount * locked_rate
lock = LockedRate(
lock_id=str(uuid.uuid4()),
from_currency=from_currency,
to_currency=to_currency,
from_amount=from_amount,
to_amount=to_amount.quantize(Decimal('0.000001')),
rate=locked_rate,
market_rate_at_lock=market_rate,
our_margin=from_amount * market_rate - to_amount,
locked_at=datetime.utcnow(),
expires_at=datetime.utcnow() + timedelta(seconds=lock_duration_seconds)
)
# Перевіряємо ризик: не беремо забагато locks в одному напрямку
if not await self.risk.can_accept_lock(lock):
raise RiskLimitExceeded("Rate lock rejected by risk manager")
await self.db.save_lock(lock)
return lock
Розрахунок маржи з поправкою на волатильність
Фіксована маржа неефективна: у спокійний ринок беремо забагато, у волатильний — занадто мало.
class DynamicMarginCalculator:
def calculate(
self,
from_currency: str,
to_currency: str,
from_amount: Decimal,
lock_duration: int # секунди
) -> Decimal:
# Історична волатильність (останні 24 години)
vol_24h = self.get_volatility(from_currency, to_currency)
# Очікуване рухання за період lock
# Для нормального розподілу: sigma_t = sigma_daily * sqrt(t/86400)
expected_move = vol_24h * (lock_duration / 86400) ** 0.5
# Беремо 2-sigma (95% покриття) + базова маржа
safety_margin = expected_move * 2
base_margin = Decimal('0.003') # 0.3% мінімум
# Знижка для крупних обсягів
volume_discount = Decimal('0.001') if from_amount * self.get_price(from_currency) > 10000 else Decimal('0')
return max(base_margin, Decimal(str(safety_margin))) - volume_discount
Управління ризиком locked rates
Якщо багато користувачів одночасно фіксують курс в одному напрямку (усі хочуть продати BTC), обмінник накопичує directional risk:
class RateLockRiskManager:
def __init__(self, max_net_exposure_usd: float = 100_000):
self.max_net_exposure = max_net_exposure_usd
async def can_accept_lock(self, lock: LockedRate) -> bool:
# Рахуємо поточну сумарну експозицію
active_locks = await self.db.get_active_locks()
net_exposure = sum(
float(l.from_amount) * float(l.rate)
if l.from_currency == lock.from_currency
else -float(l.from_amount) * float(l.rate)
for l in active_locks
)
new_exposure = float(lock.from_amount) * float(lock.rate)
total_exposure = abs(net_exposure + new_exposure)
return total_exposure < self.max_net_exposure
async def hedge_if_needed(self, lock: LockedRate):
"""При крупних locks — хеджуємо на зовнішній біржі"""
threshold_usd = 5000
if float(lock.from_amount) * float(lock.rate) > threshold_usd:
await self.exchange.hedge_position(
currency=lock.from_currency,
amount=lock.from_amount,
direction='buy' if lock.from_currency == 'USDT' else 'sell'
)
Істечення та інвалідація
async def cleanup_expired_locks(self):
"""Періодично очищаємо просроченні locks"""
expired = await self.db.get_expired_active_locks()
for lock in expired:
await self.db.update_lock_status(lock.lock_id, 'expired')
# Звільняємо захеджовану позицію
if lock.was_hedged:
await self.exchange.close_hedge(lock.lock_id)
logger.info(f"Expired {len(expired)} rate locks")
async def use_rate_lock(self, lock_id: str, actual_from_amount: Decimal) -> ExchangeResult:
lock = await self.db.get_lock(lock_id)
if lock.status != 'active':
raise LockNotActive(f"Lock {lock_id} is {lock.status}")
if datetime.utcnow() > lock.expires_at:
await self.db.update_lock_status(lock_id, 'expired')
raise LockExpired("Rate lock has expired")
# Допускаємо невелике відхилення по сумі (±1% від заблокованої)
amount_deviation = abs(actual_from_amount - lock.from_amount) / lock.from_amount
if amount_deviation > Decimal('0.01'):
raise AmountMismatch("Amount differs by more than 1% from locked amount")
# Пересчет пропорційно фактичній сумі (якщо немного відрізняється)
actual_to_amount = actual_from_amount * lock.rate
await self.db.update_lock_status(lock_id, 'used')
return ExchangeResult(
from_amount=actual_from_amount,
to_amount=actual_to_amount,
rate=lock.rate,
lock_id=lock_id
)
Відображення таймера на фронтенді
const RateLockTimer: React.FC<{expiresAt: Date; onExpired: () => void}> = ({
expiresAt, onExpired
}) => {
const [secondsLeft, setSecondsLeft] = useState(0);
useEffect(() => {
const update = () => {
const left = Math.max(0, Math.floor((expiresAt.getTime() - Date.now()) / 1000));
setSecondsLeft(left);
if (left === 0) onExpired();
};
update();
const timer = setInterval(update, 1000);
return () => clearInterval(timer);
}, [expiresAt]);
const isUrgent = secondsLeft < 60;
return (
<div className={`flex items-center gap-2 ${isUrgent ? 'text-red-500 animate-pulse' : 'text-gray-600'}`}>
<ClockIcon />
<span>Курс зафіксований на {Math.floor(secondsLeft/60)}:{String(secondsLeft%60).padStart(2,'0')}</span>
</div>
);
};
Система фіксації курсу — це баланс між користувацьким досвідом і фінансовим ризиком. Занадто короткий період (2-3 хвилини) — поганий UX, користувачі не встигають. Занадто довгий (30+ хвилин) — високий ризик для обмінника при волатильному ринку. Оптимум для крипто: 10-15 хвилин з динамічною маржею.







