Files

674 lines
20 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"errors"
"math/big"
"sync"
"github.com/luxfi/geth/common"
)
// Margin account types
const (
CrossMargin MarginAccountType = iota // Share margin across all positions
IsolatedMargin // Separate margin for each position
PortfolioMargin // Risk-based margining
)
// MarginAccountType represents the type of margin account
type MarginAccountType uint8
// Default margin parameters
const (
DefaultMaxLeverage = 100 // 100x for cross margin
IsolatedMaxLeverage = 200 // 200x for isolated margin
PortfolioMaxLeverage = 1111 // 1111x for portfolio margin (pro traders)
DefaultMaintenanceMargin = 500 // 0.5% (basis points)
DefaultInitialMargin = 1000 // 1% (basis points)
DefaultLiquidationPenalty = 500 // 0.5% goes to insurance fund
DefaultLiquidatorReward = 250 // 0.25% to liquidator
MarginPrecision = 10000 // Basis point precision
)
// MarginAccount represents a user's margin trading account
type MarginAccount struct {
Owner common.Address
AccountType MarginAccountType
Collateral map[common.Address]*big.Int // Asset -> Amount
CollateralValue *big.Int // Total USD value of collateral
Positions map[[32]byte]*MarginPosition
TotalBorrowed map[common.Address]*big.Int // Asset -> Borrowed amount
MaxLeverage uint32
MaintenanceMargin uint32 // Basis points
InitialMargin uint32 // Basis points
}
// MarginPosition represents a leveraged trading position
type MarginPosition struct {
MarketID [32]byte
Side PositionSide
Size *big.Int
EntryPrice *big.Int // Q96
MarkPrice *big.Int // Q96
Margin *big.Int // Allocated margin
UnrealizedPnL *big.Int
RealizedPnL *big.Int
Leverage uint32
LiquidationPrice *big.Int // Q96
StopLoss *big.Int // Q96 (optional)
TakeProfit *big.Int // Q96 (optional)
IsIsolated bool
}
// PositionSide represents long or short
type PositionSide uint8
const (
Long PositionSide = iota
Short
)
// MarginEngine manages margin accounts and positions
type MarginEngine struct {
Accounts map[common.Address]*MarginAccount
CollateralRates map[common.Address]*CollateralRate // Asset -> collateral parameters
InsuranceFund *big.Int
TotalCollateral map[common.Address]*big.Int
TotalBorrowed map[common.Address]*big.Int
mu sync.RWMutex
}
// CollateralRate defines collateral parameters for an asset
type CollateralRate struct {
Asset common.Address
CollateralRatio *big.Int // Discount applied (e.g., 0.9e18 = 90%)
BorrowRate *big.Int // Annual borrow rate (e.g., 0.05e18 = 5%)
MaxBorrowable *big.Int // Maximum borrowable amount
IsActive bool
}
// NewMarginEngine creates a new margin trading engine
func NewMarginEngine() *MarginEngine {
return &MarginEngine{
Accounts: make(map[common.Address]*MarginAccount),
CollateralRates: make(map[common.Address]*CollateralRate),
InsuranceFund: big.NewInt(0),
TotalCollateral: make(map[common.Address]*big.Int),
TotalBorrowed: make(map[common.Address]*big.Int),
}
}
// CreateAccount creates a new margin account
func (me *MarginEngine) CreateAccount(owner common.Address, accountType MarginAccountType) (*MarginAccount, error) {
me.mu.Lock()
defer me.mu.Unlock()
if _, exists := me.Accounts[owner]; exists {
return nil, ErrAccountExists
}
var maxLeverage uint32
switch accountType {
case CrossMargin:
maxLeverage = DefaultMaxLeverage
case IsolatedMargin:
maxLeverage = IsolatedMaxLeverage
case PortfolioMargin:
maxLeverage = PortfolioMaxLeverage
}
account := &MarginAccount{
Owner: owner,
AccountType: accountType,
Collateral: make(map[common.Address]*big.Int),
CollateralValue: big.NewInt(0),
Positions: make(map[[32]byte]*MarginPosition),
TotalBorrowed: make(map[common.Address]*big.Int),
MaxLeverage: maxLeverage,
MaintenanceMargin: DefaultMaintenanceMargin,
InitialMargin: DefaultInitialMargin,
}
me.Accounts[owner] = account
return account, nil
}
// DepositCollateral adds collateral to an account
func (me *MarginEngine) DepositCollateral(owner common.Address, asset common.Address, amount *big.Int) error {
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return ErrAccountNotFound
}
// Validate asset is accepted as collateral
rate := me.CollateralRates[asset]
if rate == nil || !rate.IsActive {
return ErrInvalidCollateral
}
// Update collateral
if account.Collateral[asset] == nil {
account.Collateral[asset] = big.NewInt(0)
}
account.Collateral[asset].Add(account.Collateral[asset], amount)
// Update totals
if me.TotalCollateral[asset] == nil {
me.TotalCollateral[asset] = big.NewInt(0)
}
me.TotalCollateral[asset].Add(me.TotalCollateral[asset], amount)
return nil
}
// WithdrawCollateral removes collateral from an account if safe
func (me *MarginEngine) WithdrawCollateral(owner common.Address, asset common.Address, amount *big.Int) error {
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return ErrAccountNotFound
}
collateral := account.Collateral[asset]
if collateral == nil || collateral.Cmp(amount) < 0 {
return ErrInsufficientCollateral
}
// Check if withdrawal keeps account healthy
newCollateral := new(big.Int).Sub(collateral, amount)
if !me.isAccountSafeWithCollateral(account, asset, newCollateral) {
return ErrWithdrawalUnsafe
}
account.Collateral[asset] = newCollateral
me.TotalCollateral[asset].Sub(me.TotalCollateral[asset], amount)
return nil
}
// OpenPosition opens a new leveraged position
func (me *MarginEngine) OpenPosition(
owner common.Address,
marketID [32]byte,
side PositionSide,
size *big.Int,
leverage uint32,
markPrice *big.Int, // Current mark price
isIsolated bool,
) (*MarginPosition, error) {
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return nil, ErrAccountNotFound
}
// Validate leverage
if leverage > account.MaxLeverage {
return nil, ErrExcessiveLeverage
}
// Calculate required margin
notionalValue := new(big.Int).Mul(size, markPrice)
notionalValue.Div(notionalValue, Q96)
requiredMargin := new(big.Int).Mul(notionalValue, big.NewInt(int64(account.InitialMargin)))
requiredMargin.Div(requiredMargin, big.NewInt(MarginPrecision))
// For isolated margin, require specific margin allocation
// For cross margin, check total account equity
if isIsolated || account.AccountType == IsolatedMargin {
// Need explicit margin allocation (handled by caller)
} else {
// Cross margin - check if account has enough free margin
freeMargin := me.calculateFreeMargin(account)
if freeMargin.Cmp(requiredMargin) < 0 {
return nil, ErrInsufficientMargin
}
}
// Calculate liquidation price
liquidationPrice := me.calculateLiquidationPrice(side, markPrice, leverage, account.MaintenanceMargin)
position := &MarginPosition{
MarketID: marketID,
Side: side,
Size: new(big.Int).Set(size),
EntryPrice: new(big.Int).Set(markPrice),
MarkPrice: new(big.Int).Set(markPrice),
Margin: new(big.Int).Set(requiredMargin),
UnrealizedPnL: big.NewInt(0),
RealizedPnL: big.NewInt(0),
Leverage: leverage,
LiquidationPrice: liquidationPrice,
IsIsolated: isIsolated,
}
// Check for existing position
existing := account.Positions[marketID]
if existing != nil {
// Combine positions or flip
if existing.Side == side {
// Same direction - increase position
return me.increasePosition(account, existing, position)
} else {
// Opposite direction - reduce or flip
return me.reduceOrFlipPosition(account, existing, position)
}
}
account.Positions[marketID] = position
return position, nil
}
// ClosePosition closes a leveraged position
func (me *MarginEngine) ClosePosition(
owner common.Address,
marketID [32]byte,
sizeToClose *big.Int,
closePrice *big.Int,
) (*big.Int, error) { // Returns realized PnL
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return nil, ErrAccountNotFound
}
position := account.Positions[marketID]
if position == nil {
return nil, ErrPositionNotFound
}
// Calculate PnL
pnl := me.calculatePnL(position, closePrice, sizeToClose)
// Update or remove position
if sizeToClose.Cmp(position.Size) >= 0 {
// Full close
delete(account.Positions, marketID)
} else {
// Partial close
position.Size.Sub(position.Size, sizeToClose)
position.RealizedPnL.Add(position.RealizedPnL, pnl)
// Proportionally reduce margin
closeFraction := new(big.Int).Mul(sizeToClose, big.NewInt(1e18))
closeFraction.Div(closeFraction, new(big.Int).Add(position.Size, sizeToClose))
marginReduction := new(big.Int).Mul(position.Margin, closeFraction)
marginReduction.Div(marginReduction, big.NewInt(1e18))
position.Margin.Sub(position.Margin, marginReduction)
}
return pnl, nil
}
// UpdatePositionMargin adds or removes margin from an isolated position
func (me *MarginEngine) UpdatePositionMargin(
owner common.Address,
marketID [32]byte,
marginDelta *big.Int, // Positive = add, negative = remove
) error {
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return ErrAccountNotFound
}
position := account.Positions[marketID]
if position == nil {
return ErrPositionNotFound
}
if !position.IsIsolated {
return ErrNotIsolatedPosition
}
newMargin := new(big.Int).Add(position.Margin, marginDelta)
if newMargin.Sign() <= 0 {
return ErrInsufficientMargin
}
// If removing margin, check if position is still safe
if marginDelta.Sign() < 0 {
// Calculate new leverage
notional := new(big.Int).Mul(position.Size, position.MarkPrice)
notional.Div(notional, Q96)
newLeverage := new(big.Int).Div(notional, newMargin)
if newLeverage.Uint64() > uint64(account.MaxLeverage) {
return ErrExcessiveLeverage
}
}
position.Margin = newMargin
// Recalculate liquidation price
position.LiquidationPrice = me.calculateLiquidationPrice(
position.Side, position.EntryPrice, position.Leverage, account.MaintenanceMargin,
)
return nil
}
// SetStopLoss sets stop loss for a position
func (me *MarginEngine) SetStopLoss(owner common.Address, marketID [32]byte, stopPrice *big.Int) error {
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return ErrAccountNotFound
}
position := account.Positions[marketID]
if position == nil {
return ErrPositionNotFound
}
position.StopLoss = new(big.Int).Set(stopPrice)
return nil
}
// SetTakeProfit sets take profit for a position
func (me *MarginEngine) SetTakeProfit(owner common.Address, marketID [32]byte, takeProfitPrice *big.Int) error {
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return ErrAccountNotFound
}
position := account.Positions[marketID]
if position == nil {
return ErrPositionNotFound
}
position.TakeProfit = new(big.Int).Set(takeProfitPrice)
return nil
}
// LiquidatePosition liquidates an underwater margin position
func (me *MarginEngine) LiquidatePosition(
liquidator common.Address,
owner common.Address,
marketID [32]byte,
currentPrice *big.Int,
) (*big.Int, error) { // Returns liquidator reward
me.mu.Lock()
defer me.mu.Unlock()
account := me.Accounts[owner]
if account == nil {
return nil, ErrAccountNotFound
}
position := account.Positions[marketID]
if position == nil {
return nil, ErrPositionNotFound
}
// Check if position is liquidatable
if !me.isPositionLiquidatable(position, currentPrice, account.MaintenanceMargin) {
return nil, ErrPositionNotLiquidatable
}
// Calculate remaining value
pnl := me.calculatePnL(position, currentPrice, position.Size)
remainingMargin := new(big.Int).Add(position.Margin, pnl)
// Calculate rewards
notional := new(big.Int).Mul(position.Size, currentPrice)
notional.Div(notional, Q96)
// Liquidator gets 0.25% of notional
liquidatorReward := new(big.Int).Mul(notional, big.NewInt(DefaultLiquidatorReward))
liquidatorReward.Div(liquidatorReward, big.NewInt(MarginPrecision))
// Insurance fund gets 0.5% of notional
insuranceFee := new(big.Int).Mul(notional, big.NewInt(DefaultLiquidationPenalty))
insuranceFee.Div(insuranceFee, big.NewInt(MarginPrecision))
// Cap rewards at remaining margin
totalFees := new(big.Int).Add(liquidatorReward, insuranceFee)
if totalFees.Cmp(remainingMargin) > 0 {
// Socialize loss
if remainingMargin.Sign() > 0 {
// Split remaining between liquidator and insurance
liquidatorReward.Div(remainingMargin, big.NewInt(2))
insuranceFee.Sub(remainingMargin, liquidatorReward)
} else {
// Use insurance fund
deficit := new(big.Int).Neg(remainingMargin)
if me.InsuranceFund.Cmp(deficit) >= 0 {
me.InsuranceFund.Sub(me.InsuranceFund, deficit)
}
liquidatorReward = big.NewInt(0)
insuranceFee = big.NewInt(0)
}
}
// Add to insurance fund
me.InsuranceFund.Add(me.InsuranceFund, insuranceFee)
// Remove position
delete(account.Positions, marketID)
return liquidatorReward, nil
}
// GetAccountHealth returns the health factor of an account (>1 = healthy)
func (me *MarginEngine) GetAccountHealth(owner common.Address) (*big.Int, error) {
me.mu.RLock()
defer me.mu.RUnlock()
account := me.Accounts[owner]
if account == nil {
return nil, ErrAccountNotFound
}
totalEquity := me.calculateTotalEquity(account)
totalMaintenanceMargin := me.calculateTotalMaintenanceMargin(account)
if totalMaintenanceMargin.Sign() == 0 {
return big.NewInt(1e18), nil // No positions, max health
}
// Health = equity / maintenanceMargin
health := new(big.Int).Mul(totalEquity, big.NewInt(1e18))
health.Div(health, totalMaintenanceMargin)
return health, nil
}
// Helper functions
func (me *MarginEngine) calculateFreeMargin(account *MarginAccount) *big.Int {
totalEquity := me.calculateTotalEquity(account)
usedMargin := me.calculateUsedMargin(account)
return new(big.Int).Sub(totalEquity, usedMargin)
}
func (me *MarginEngine) calculateTotalEquity(account *MarginAccount) *big.Int {
// Collateral value + unrealized PnL
equity := new(big.Int).Set(account.CollateralValue)
for _, pos := range account.Positions {
equity.Add(equity, pos.UnrealizedPnL)
}
return equity
}
func (me *MarginEngine) calculateUsedMargin(account *MarginAccount) *big.Int {
used := big.NewInt(0)
for _, pos := range account.Positions {
used.Add(used, pos.Margin)
}
return used
}
func (me *MarginEngine) calculateTotalMaintenanceMargin(account *MarginAccount) *big.Int {
total := big.NewInt(0)
for _, pos := range account.Positions {
notional := new(big.Int).Mul(pos.Size, pos.MarkPrice)
notional.Div(notional, Q96)
maintenance := new(big.Int).Mul(notional, big.NewInt(int64(account.MaintenanceMargin)))
maintenance.Div(maintenance, big.NewInt(MarginPrecision))
total.Add(total, maintenance)
}
return total
}
func (me *MarginEngine) calculateLiquidationPrice(
side PositionSide,
entryPrice *big.Int,
leverage uint32,
maintenanceMargin uint32,
) *big.Int {
// For long: liqPrice = entryPrice * (1 - 1/leverage + maintenanceMargin)
// For short: liqPrice = entryPrice * (1 + 1/leverage - maintenanceMargin)
leverageInverse := new(big.Int).Div(big.NewInt(MarginPrecision), big.NewInt(int64(leverage)))
maintenanceRatio := big.NewInt(int64(maintenanceMargin))
var priceMultiplier *big.Int
if side == Long {
// 1 - 1/leverage + maintenance
priceMultiplier = new(big.Int).Sub(big.NewInt(MarginPrecision), leverageInverse)
priceMultiplier.Add(priceMultiplier, maintenanceRatio)
} else {
// 1 + 1/leverage - maintenance
priceMultiplier = new(big.Int).Add(big.NewInt(MarginPrecision), leverageInverse)
priceMultiplier.Sub(priceMultiplier, maintenanceRatio)
}
liqPrice := new(big.Int).Mul(entryPrice, priceMultiplier)
liqPrice.Div(liqPrice, big.NewInt(MarginPrecision))
return liqPrice
}
func (me *MarginEngine) calculatePnL(position *MarginPosition, currentPrice *big.Int, size *big.Int) *big.Int {
priceDiff := new(big.Int).Sub(currentPrice, position.EntryPrice)
pnl := new(big.Int).Mul(size, priceDiff)
pnl.Div(pnl, Q96)
if position.Side == Short {
pnl.Neg(pnl)
}
return pnl
}
func (me *MarginEngine) isPositionLiquidatable(position *MarginPosition, currentPrice *big.Int, maintenanceMargin uint32) bool {
// Calculate PnL at current price
pnl := me.calculatePnL(position, currentPrice, position.Size)
// Current equity in position
equity := new(big.Int).Add(position.Margin, pnl)
// Required maintenance margin
notional := new(big.Int).Mul(position.Size, currentPrice)
notional.Div(notional, Q96)
required := new(big.Int).Mul(notional, big.NewInt(int64(maintenanceMargin)))
required.Div(required, big.NewInt(MarginPrecision))
return equity.Cmp(required) < 0
}
func (me *MarginEngine) isAccountSafeWithCollateral(account *MarginAccount, asset common.Address, newAmount *big.Int) bool {
// Simplified check - in production would need full recalculation
return newAmount.Sign() >= 0
}
func (me *MarginEngine) increasePosition(account *MarginAccount, existing *MarginPosition, incoming *MarginPosition) (*MarginPosition, error) {
// Calculate new average entry price
oldNotional := new(big.Int).Mul(existing.Size, existing.EntryPrice)
newNotional := new(big.Int).Mul(incoming.Size, incoming.EntryPrice)
totalNotional := new(big.Int).Add(oldNotional, newNotional)
totalSize := new(big.Int).Add(existing.Size, incoming.Size)
existing.EntryPrice.Div(totalNotional, totalSize)
existing.Size = totalSize
existing.Margin.Add(existing.Margin, incoming.Margin)
// Update leverage
notional := new(big.Int).Mul(existing.Size, existing.MarkPrice)
notional.Div(notional, Q96)
if existing.Margin.Sign() > 0 {
existing.Leverage = uint32(notional.Div(notional, existing.Margin).Uint64())
}
// Recalculate liquidation price
existing.LiquidationPrice = me.calculateLiquidationPrice(
existing.Side, existing.EntryPrice, existing.Leverage, account.MaintenanceMargin,
)
return existing, nil
}
func (me *MarginEngine) reduceOrFlipPosition(account *MarginAccount, existing *MarginPosition, incoming *MarginPosition) (*MarginPosition, error) {
// Realize PnL on closed portion
if incoming.Size.Cmp(existing.Size) <= 0 {
// Partial reduce
pnl := me.calculatePnL(existing, incoming.EntryPrice, incoming.Size)
existing.RealizedPnL.Add(existing.RealizedPnL, pnl)
existing.Size.Sub(existing.Size, incoming.Size)
if existing.Size.Sign() == 0 {
delete(account.Positions, existing.MarketID)
return nil, nil
}
return existing, nil
}
// Flip position
pnl := me.calculatePnL(existing, incoming.EntryPrice, existing.Size)
remainingSize := new(big.Int).Sub(incoming.Size, existing.Size)
flipped := &MarginPosition{
MarketID: incoming.MarketID,
Side: incoming.Side,
Size: remainingSize,
EntryPrice: new(big.Int).Set(incoming.EntryPrice),
MarkPrice: new(big.Int).Set(incoming.MarkPrice),
Margin: new(big.Int).Set(incoming.Margin),
UnrealizedPnL: big.NewInt(0),
RealizedPnL: pnl,
Leverage: incoming.Leverage,
IsIsolated: incoming.IsIsolated,
}
flipped.LiquidationPrice = me.calculateLiquidationPrice(
flipped.Side, flipped.EntryPrice, flipped.Leverage, account.MaintenanceMargin,
)
account.Positions[incoming.MarketID] = flipped
return flipped, nil
}
// Additional errors for margin (others in types.go)
var (
ErrAccountExists = errors.New("account already exists")
ErrAccountNotFound = errors.New("account not found")
ErrInvalidCollateral = errors.New("invalid or inactive collateral asset")
ErrWithdrawalUnsafe = errors.New("withdrawal would make account unsafe")
ErrNotIsolatedPosition = errors.New("position is not isolated margin")
)