Разработка системы маржинальной торговли
Маржинальная торговля — это торговля с использованием заёмных средств. Трейдер депонирует залог (margin), получает кредитное плечо (2×, 5×, 10×), и может торговать бо́льшим объёмом. Биржа зарабатывает на funding rate / interest. Задача разработки — построить систему, которая не допустит убытков больше, чем залог (иначе биржа несёт риск).
Типы маржинальной торговли
Isolated Margin
Каждая позиция имеет свой залог. Риск ограничен залогом конкретной позиции — другие позиции не затрагиваются при ликвидации.
type IsolatedPosition struct {
UserID int64
Pair string
Side Side // Long | Short
EntryPrice Decimal
Quantity Decimal
Leverage int // 1-100x
InitialMargin Decimal // залог при открытии
MaintenanceMargin Decimal // минимальный залог для поддержания
UnrealizedPnL Decimal // текущий P&L
LiquidationPrice Decimal // цена принудительного закрытия
}
Cross Margin
Все позиции пользователя используют общий пул залога. Прибыльные позиции поддерживают убыточные. Ликвидация наступает когда весь баланс уходит в минус.
type CrossMarginAccount struct {
UserID int64
TotalEquity Decimal // залог + unrealized PnL
TotalPositions []CrossPosition
MaintenanceMargin Decimal // суммарный минимальный залог по всем позициям
// Liquidation при: TotalEquity < MaintenanceMargin
}
Расчёт ликвидационной цены
Для Long позиции с isolated margin:
func CalculateLiquidationPrice(pos IsolatedPosition, config MarginConfig) Decimal {
// Liquidation при: unrealized loss = initial margin - maintenance margin fee
// (entry_price - liq_price) * quantity = initial_margin - maintenance_margin_amount
maintenanceMarginAmount := pos.EntryPrice.Mul(pos.Quantity).Mul(config.MaintenanceMarginRate)
lossAtLiquidation := pos.InitialMargin.Sub(maintenanceMarginAmount)
priceDropAllowed := lossAtLiquidation.Div(pos.Quantity)
if pos.Side == Long {
return pos.EntryPrice.Sub(priceDropAllowed)
} else {
return pos.EntryPrice.Add(priceDropAllowed)
}
}
// Пример: Long BTC, entry $42,000, leverage 10x
// Initial Margin: $42,000 / 10 = $4,200
// Maintenance Margin Rate: 0.5%
// MM Amount: $42,000 * 0.5% = $210
// Loss at liquidation: $4,200 - $210 = $3,990
// Price drop allowed: $3,990 / 1 BTC = $3,990
// Liquidation price: $42,000 - $3,990 = $38,010
Ликвидационный движок
Ликвидация должна происходить быстро: при резких движениях рынка позиции нескольких пользователей могут достичь liquidation price одновременно.
type LiquidationEngine struct {
marketDataFeed *MarketDataFeed
orderEngine *OrderEngine
db *DB
ticker *time.Ticker
}
func (le *LiquidationEngine) Run(ctx context.Context) {
for {
select {
case price := <-le.marketDataFeed.PriceUpdates:
le.checkLiquidations(price)
case <-ctx.Done():
return
}
}
}
func (le *LiquidationEngine) checkLiquidations(update PriceUpdate) {
// Находим все позиции, где current price пересёк liquidation price
positions := le.db.GetPositionsForLiquidation(update.Pair, update.Price)
for _, pos := range positions {
go le.liquidatePosition(pos, update.Price)
}
}
func (le *LiquidationEngine) liquidatePosition(pos IsolatedPosition, markPrice Decimal) {
// 1. Помечаем позицию как "liquidating" (атомарно, один ликвидатор)
if !le.db.TryMarkForLiquidation(pos.ID) {
return // другой воркер уже берёт эту позицию
}
// 2. Размещаем market ордер на закрытие по текущей цене
liquidationOrder := Order{
UserID: pos.UserID,
Pair: pos.Pair,
Side: pos.Side.Opposite(),
Type: Market,
Quantity: pos.Quantity,
IsLiquidation: true,
}
trades, err := le.orderEngine.PlaceOrder(liquidationOrder)
if err != nil {
log.Error("Liquidation order failed", "posID", pos.ID, "error", err)
le.db.RevertLiquidationMark(pos.ID)
return
}
// 3. Рассчитываем liquidation fee (обычно 0.5-1.5% от позиции)
executedPrice := averageExecutionPrice(trades)
positionValue := pos.Quantity.Mul(executedPrice)
liquidationFee := positionValue.Mul(LIQUIDATION_FEE_RATE)
// 4. Распределяем residual margin: покрываем убыток, остаток возвращаем пользователю
loss := pos.EntryPrice.Sub(executedPrice).Mul(pos.Quantity)
if pos.Side == Short {
loss = executedPrice.Sub(pos.EntryPrice).Mul(pos.Quantity)
}
residual := pos.InitialMargin.Sub(loss).Sub(liquidationFee)
if residual.IsPositive() {
le.db.CreditUserBalance(pos.UserID, residual, "liquidation_refund")
}
// Если residual < 0 — Insurance Fund покрывает убыток
le.db.MarkLiquidationComplete(pos.ID, executedPrice)
le.notifyUser(pos.UserID, pos, executedPrice)
}
Insurance Fund
При резких движениях liquidation может исполниться по худшей цене, чем liquidation price. Remaining loss покрывает Insurance Fund — пул, формируемый из liquidation fees:
type InsuranceFund struct {
db *DB
}
func (if_ *InsuranceFund) Cover(loss Decimal, currency string) error {
balance := if_.db.GetBalance(currency)
if balance.LessThan(loss) {
// Insurance Fund иссяк — auto-deleveraging (ADL)
if_.triggerADL(loss.Sub(balance), currency)
loss = balance
}
if_.db.DeductBalance(currency, loss)
log.Info("Insurance fund covered loss", "amount", loss, "currency", currency)
return nil
}
ADL (Auto-Deleveraging): если Insurance Fund исчерпан, принудительно закрываются наиболее прибыльные позиции трейдеров для покрытия убытка. Это жёсткая мера — используется только как последний резорт.
Funding Rate для perpetual contracts
Perpetual futures (бессрочные контракты) не имеют срока экспирации. Цена привязывается к spot через механизм funding rate:
type FundingRate struct {
Rate Decimal // может быть положительным или отрицательным
NextFunding time.Time
Interval time.Duration // обычно 8 часов
}
func (fe *FundingEngine) CalculateRate(pair string) Decimal {
markPrice := fe.getMarkPrice(pair)
indexPrice := fe.getIndexPrice(pair) // цена spot с крупных бирж
// Premium = (mark - index) / index
premium := markPrice.Sub(indexPrice).Div(indexPrice)
// Funding Rate = Premium + clamp(InterestRate - Premium, -0.05%, 0.05%)
interestRate := Decimal("0.0001") // 0.01% базовая ставка (Binance default)
clampedDiff := Clamp(
interestRate.Sub(premium),
Decimal("-0.0005"),
Decimal("0.0005"),
)
return premium.Add(clampedDiff)
}
func (fe *FundingEngine) ApplyFunding(pair string) {
rate := fe.CalculateRate(pair)
positions := fe.db.GetOpenPositions(pair)
for _, pos := range positions {
fundingPayment := pos.Quantity.Mul(pos.EntryPrice).Mul(rate)
if pos.Side == Long {
// Long платит если rate > 0 (mark > index), получает если rate < 0
fe.db.DebitUserBalance(pos.UserID, fundingPayment)
} else {
fe.db.CreditUserBalance(pos.UserID, fundingPayment)
}
}
}
Mark Price vs Last Price
Liquidation должна основываться на mark price (агрегированная цена с нескольких бирж), а не на last trade price биржи. Иначе возможна манипуляция: крупный трейдер делает маленькую сделку по экстремальной цене и триггерит ликвидации.
func (fe *FundingEngine) GetMarkPrice(pair string) Decimal {
// Median цена с Binance, OKX, Bybit
prices := []Decimal{
fe.binanceFeed.GetPrice(pair),
fe.okxFeed.GetPrice(pair),
fe.bybitFeed.GetPrice(pair),
}
sort.Slice(prices, func(i, j int) bool { return prices[i].LessThan(prices[j]) })
return prices[1] // медиана из 3
}
Margin Management UI
Пользователь должен видеть в реальном времени:
- Текущее margin ratio (насколько далеко от ликвидации)
- Liquidation price для каждой позиции
- P&L (realized и unrealized)
- Возможность добавить/вывести margin (изменить leverage)
function PositionCard({ position }: { position: Position }) {
const marginRatio = position.equity / position.maintenanceMargin * 100;
const urgency = marginRatio < 110 ? 'critical' : marginRatio < 150 ? 'warning' : 'safe';
return (
<Card>
<PnLDisplay pnl={position.unrealizedPnl} />
<div>Liquidation: ${position.liquidationPrice.toFixed(2)}</div>
<MarginRatioBar ratio={marginRatio} urgency={urgency} />
<div>Margin Ratio: {marginRatio.toFixed(1)}%</div>
<AddMarginButton position={position} />
</Card>
);
}
Сроки разработки
| Компонент | Срок |
|---|---|
| Isolated margin engine | 4–5 недель |
| Liquidation engine | 3–4 недели |
| Cross margin | 3–4 недели |
| Perpetual funding rate | 2–3 недели |
| Insurance fund + ADL | 2–3 недели |
| Mark price oracle | 1–2 недели |
| UI для маржин торговли | 4–6 недель |
Полная система маржинальной торговли: 4–6 месяцев.







