Разработка системы фиксации курса обмена
Фиксация курса (rate lock / rate fixing) — это механизм гарантии конкретного обменного курса на определённое время. Пользователь видит курс 1 BTC = 50,000 USDT и имеет 10 минут для проведения сделки именно по этому курсу, независимо от движения рынка. Для обменника это риск — рынок может уйти против вас. Для пользователя — предсказуемость операции.
Зачем нужна фиксация курса
Без фиксации: пользователь начинает процесс обмена, отправляет BTC, за это время курс упал на 2%, получает меньше USDT чем ожидал. Негативный UX, жалобы, отток.
С фиксацией: пользователь видит гарантированную сумму, осознанно принимает решение, получает именно то что было обещано. Trust и retention выше.
Архитектура системы фиксации
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 # locked rate
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 минут с динамической маржой.







