mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
Renamed files: - lending_pool.go → lending.go - lending_pool_test.go → lending_test.go - margin_trading.go → margin.go - margin_trading_test.go → margin_test.go - liquidation_engine.go → liquidation.go - liquidation_engine_test.go → liquidation_test.go - liquidation_engine_comprehensive_test.go → liquidation_comprehensive_test.go No API changes - all public types/functions remain the same.
869 lines
24 KiB
Go
869 lines
24 KiB
Go
package lx
|
|
|
|
import (
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestMarginEngine(t *testing.T) {
|
|
t.Run("CreateMarginAccount", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
// Create cross margin account
|
|
account, err := engine.CreateMarginAccount("user1", CrossMargin)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, account)
|
|
assert.Equal(t, "user1", account.UserID)
|
|
assert.Equal(t, CrossMargin, account.AccountType)
|
|
assert.Equal(t, 10.0, account.MaxLeverage)
|
|
|
|
// Try to create duplicate account
|
|
_, err = engine.CreateMarginAccount("user1", CrossMargin)
|
|
assert.Error(t, err)
|
|
|
|
// Create isolated margin account
|
|
account2, err := engine.CreateMarginAccount("user2", IsolatedMargin)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 20.0, account2.MaxLeverage)
|
|
|
|
// Create portfolio margin account
|
|
account3, err := engine.CreateMarginAccount("user3", PortfolioMargin)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 100.0, account3.MaxLeverage)
|
|
assert.True(t, account3.PortfolioMarginMode)
|
|
})
|
|
|
|
t.Run("GetAccount", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
account := engine.GetAccount("user1")
|
|
assert.NotNil(t, account)
|
|
assert.Equal(t, "user1", account.UserID)
|
|
|
|
// Non-existent account
|
|
account2 := engine.GetAccount("nonexistent")
|
|
assert.Nil(t, account2)
|
|
})
|
|
|
|
t.Run("DepositCollateral", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
// Set price through CurrentPrices map
|
|
oracle.CurrentPrices = make(map[string]*PriceData)
|
|
oracle.CurrentPrices["BTC"] = &PriceData{Price: 50000.0}
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
// Deposit BTC collateral
|
|
amount := big.NewInt(1000000) // 0.01 BTC in satoshis
|
|
err := engine.DepositCollateral("user1", "BTC", amount)
|
|
assert.NoError(t, err)
|
|
|
|
account := engine.GetAccount("user1")
|
|
collateral := account.CollateralAssets["BTC"]
|
|
assert.NotNil(t, collateral)
|
|
assert.Equal(t, amount, collateral.Amount)
|
|
assert.Equal(t, amount, collateral.Available)
|
|
|
|
// Test non-existent account
|
|
err = engine.DepositCollateral("nonexistent", "BTC", amount)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("OpenPosition", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
oracle.CurrentPrices = make(map[string]*PriceData)
|
|
oracle.CurrentPrices["BTC-USDT"] = &PriceData{Price: 50000.0}
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
// Deposit enough collateral to not need borrowing
|
|
collateral := big.NewInt(1000000) // $10,000 USDT
|
|
engine.DepositCollateral("user1", "USDT", collateral)
|
|
account.FreeMargin = collateral // Set free margin
|
|
|
|
// Open position with low leverage (no borrowing needed)
|
|
order := &Order{
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Price: 50000.0,
|
|
Size: 0.01,
|
|
User: "user1",
|
|
}
|
|
|
|
position, err := engine.OpenPosition("user1", order, 1.0) // Low leverage
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, position)
|
|
assert.Equal(t, "BTC-USDT", position.Symbol)
|
|
assert.Equal(t, Buy, position.Side)
|
|
assert.Equal(t, 0.01, position.Size)
|
|
assert.Equal(t, 1.0, position.Leverage)
|
|
|
|
// Test excessive leverage
|
|
order2 := &Order{
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Price: 50000.0,
|
|
Size: 0.01,
|
|
User: "user1",
|
|
}
|
|
_, err = engine.OpenPosition("user1", order2, 200.0)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "leverage exceeds maximum")
|
|
|
|
// Test insufficient margin - adjust test to use up free margin first
|
|
account.FreeMargin = big.NewInt(100) // Very small free margin
|
|
order3 := &Order{
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Price: 50000.0,
|
|
Size: 1.0,
|
|
User: "user1",
|
|
}
|
|
_, err = engine.OpenPosition("user1", order3, 1.0)
|
|
assert.Error(t, err)
|
|
// Error could be either insufficient margin or insufficient liquidity
|
|
assert.True(t, err != nil)
|
|
})
|
|
|
|
t.Run("ClosePosition", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
oracle.CurrentPrices = make(map[string]*PriceData)
|
|
oracle.CurrentPrices["BTC-USDT"] = &PriceData{Price: 50000.0}
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
collateral := big.NewInt(1000000) // Large collateral
|
|
engine.DepositCollateral("user1", "USDT", collateral)
|
|
account.FreeMargin = collateral
|
|
account.Balance = collateral
|
|
|
|
// Open position with low leverage
|
|
order := &Order{
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Price: 50000.0,
|
|
Size: 0.01,
|
|
User: "user1",
|
|
}
|
|
position, err := engine.OpenPosition("user1", order, 1.0)
|
|
if err != nil {
|
|
t.Skipf("Skipping ClosePosition test due to OpenPosition error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Update price for profit
|
|
oracle.CurrentPrices["BTC-USDT"] = &PriceData{Price: 51000.0}
|
|
|
|
// Close position
|
|
err = engine.ClosePosition("user1", position.ID, 0.01)
|
|
assert.NoError(t, err)
|
|
|
|
// Check position was removed
|
|
assert.NotContains(t, account.Positions, position.ID)
|
|
|
|
// Test closing non-existent position
|
|
err = engine.ClosePosition("user1", "nonexistent", 0.01)
|
|
assert.Error(t, err)
|
|
|
|
// Test closing for non-existent account
|
|
err = engine.ClosePosition("nonexistent", position.ID, 0.01)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("ModifyLeverage", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
oracle.CurrentPrices = make(map[string]*PriceData)
|
|
oracle.CurrentPrices["BTC-USDT"] = &PriceData{Price: 50000.0}
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
collateral := big.NewInt(1000000) // Large collateral
|
|
engine.DepositCollateral("user1", "USDT", collateral)
|
|
account.FreeMargin = collateral
|
|
|
|
// Open position with low leverage
|
|
order := &Order{
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Price: 50000.0,
|
|
Size: 0.01,
|
|
User: "user1",
|
|
}
|
|
position, err := engine.OpenPosition("user1", order, 2.0)
|
|
if err != nil {
|
|
t.Skipf("Skipping ModifyLeverage test due to OpenPosition error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Increase leverage
|
|
err = engine.ModifyLeverage("user1", position.ID, 20.0)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 20.0, position.Leverage)
|
|
|
|
// Decrease leverage
|
|
err = engine.ModifyLeverage("user1", position.ID, 5.0)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 5.0, position.Leverage)
|
|
|
|
// Test excessive leverage
|
|
err = engine.ModifyLeverage("user1", position.ID, 200.0)
|
|
assert.Error(t, err)
|
|
|
|
// Test non-existent position
|
|
err = engine.ModifyLeverage("user1", "nonexistent", 10.0)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("CalculateLiquidationPrice", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
// Long position
|
|
longPosition := &MarginPosition{
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
EntryPrice: 50000.0,
|
|
Leverage: 10.0,
|
|
}
|
|
liquidationPrice := engine.calculateLiquidationPrice(longPosition, account)
|
|
assert.Greater(t, longPosition.EntryPrice, liquidationPrice)
|
|
|
|
// Short position
|
|
shortPosition := &MarginPosition{
|
|
Symbol: "BTC-USDT",
|
|
Side: Sell,
|
|
EntryPrice: 50000.0,
|
|
Leverage: 10.0,
|
|
}
|
|
liquidationPrice = engine.calculateLiquidationPrice(shortPosition, account)
|
|
assert.Less(t, shortPosition.EntryPrice, liquidationPrice)
|
|
})
|
|
|
|
t.Run("UpdateAccountMetrics", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
oracle.CurrentPrices = make(map[string]*PriceData)
|
|
oracle.CurrentPrices["BTC-USDT"] = &PriceData{Price: 50000.0}
|
|
oracle.CurrentPrices["BTC"] = &PriceData{Price: 50000.0}
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
// Add collateral
|
|
btcAmount := big.NewInt(1000000) // 0.01 BTC
|
|
engine.DepositCollateral("user1", "BTC", btcAmount)
|
|
|
|
// Add position
|
|
position := &MarginPosition{
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Size: 0.01,
|
|
EntryPrice: 50000.0,
|
|
UnrealizedPnL: big.NewInt(0),
|
|
}
|
|
account.Positions["pos1"] = position
|
|
account.MarginUsed = big.NewInt(500)
|
|
|
|
// Update metrics
|
|
engine.updateAccountMetrics(account)
|
|
|
|
// Check metrics were updated
|
|
assert.NotNil(t, account.Equity)
|
|
assert.GreaterOrEqual(t, account.MarginLevel, 0.0)
|
|
assert.NotZero(t, account.LastUpdate)
|
|
})
|
|
|
|
t.Run("CheckLiquidations", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
oracle.CurrentPrices = make(map[string]*PriceData)
|
|
oracle.CurrentPrices["BTC-USDT"] = &PriceData{Price: 50000.0}
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
account.MarginLevel = 90.0 // Below liquidation level
|
|
account.LiquidationLevel = 100.0
|
|
|
|
// Add position
|
|
position := &MarginPosition{
|
|
ID: "pos1",
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Size: 0.01,
|
|
EntryPrice: 50000.0,
|
|
LiquidationPrice: 45000.0,
|
|
UnrealizedPnL: big.NewInt(-1000),
|
|
Margin: big.NewInt(500),
|
|
}
|
|
account.Positions["pos1"] = position
|
|
|
|
// Run liquidation check
|
|
engine.CheckLiquidations()
|
|
|
|
// For isolated margin, check position-specific liquidation
|
|
account2, _ := engine.CreateMarginAccount("user2", IsolatedMargin)
|
|
position2 := &MarginPosition{
|
|
ID: "pos2",
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Size: 0.01,
|
|
EntryPrice: 50000.0,
|
|
LiquidationPrice: 51000.0, // Price below liquidation
|
|
UnrealizedPnL: big.NewInt(-1000),
|
|
Margin: big.NewInt(500),
|
|
}
|
|
account2.Positions["pos2"] = position2
|
|
oracle.CurrentPrices["BTC-USDT"] = &PriceData{Price: 45000.0} // Trigger liquidation
|
|
|
|
engine.CheckLiquidations()
|
|
})
|
|
|
|
t.Run("BorrowForPosition", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
// Create pool and add liquidity to lending pool
|
|
poolConfig := PoolConfig{
|
|
ReserveFactor: 0.1,
|
|
CollateralFactor: 0.75,
|
|
LiquidationThreshold: 0.85,
|
|
LiquidationPenalty: 0.05,
|
|
MaxBorrowRate: 0.5,
|
|
MinBorrowRate: 0.02,
|
|
OptimalUtilization: 0.8,
|
|
}
|
|
engine.LendingPool.CreatePool("BTC-USDT", poolConfig)
|
|
engine.LendingPool.Supply("lender1", "BTC-USDT", big.NewInt(1000000))
|
|
|
|
// Borrow for position
|
|
borrowAmount := big.NewInt(5000)
|
|
err := engine.borrowForPosition(account, "BTC-USDT", borrowAmount)
|
|
assert.NoError(t, err)
|
|
|
|
// Check borrowed amount
|
|
borrowed := account.BorrowedAmounts["BTC-USDT"]
|
|
assert.NotNil(t, borrowed)
|
|
assert.Equal(t, borrowAmount, borrowed.Amount)
|
|
|
|
// Test insufficient liquidity
|
|
hugeBorrow := big.NewInt(10000000)
|
|
err = engine.borrowForPosition(account, "BTC-USDT", hugeBorrow)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "insufficient liquidity")
|
|
})
|
|
|
|
t.Run("RepayBorrowed", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
// Create pool first
|
|
poolConfig := PoolConfig{
|
|
ReserveFactor: 0.1,
|
|
CollateralFactor: 0.75,
|
|
LiquidationThreshold: 0.85,
|
|
LiquidationPenalty: 0.05,
|
|
MaxBorrowRate: 0.5,
|
|
MinBorrowRate: 0.02,
|
|
OptimalUtilization: 0.8,
|
|
}
|
|
engine.LendingPool.CreatePool("BTC-USDT", poolConfig)
|
|
|
|
// Setup borrowed asset
|
|
borrowed := &BorrowedAsset{
|
|
Asset: "BTC-USDT",
|
|
Amount: big.NewInt(5000),
|
|
AccruedInterest: big.NewInt(0),
|
|
InterestRate: 0.1,
|
|
BorrowTime: time.Now(),
|
|
LastUpdate: time.Now(),
|
|
}
|
|
account.BorrowedAmounts["BTC-USDT"] = borrowed
|
|
account.BorrowingFees = big.NewInt(0)
|
|
|
|
// Initialize total borrowed
|
|
engine.TotalBorrowed["BTC-USDT"] = big.NewInt(5000)
|
|
engine.LendingPool.Supply("lender1", "BTC-USDT", big.NewInt(100000))
|
|
|
|
// Repay
|
|
repayAmount := big.NewInt(2500)
|
|
err := engine.repayBorrowed(account, "BTC-USDT", repayAmount)
|
|
assert.NoError(t, err)
|
|
|
|
// Check remaining borrowed
|
|
assert.Equal(t, big.NewInt(2500), borrowed.Amount)
|
|
|
|
// Repay non-existent borrow
|
|
err = engine.repayBorrowed(account, "ETH-USDT", repayAmount)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("CalculateAccruedInterest", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
borrowed := &BorrowedAsset{
|
|
Asset: "BTC-USDT",
|
|
Amount: big.NewInt(10000),
|
|
InterestRate: 0.1, // 10% annual
|
|
LastUpdate: time.Now().Add(-24 * time.Hour), // 1 day ago
|
|
}
|
|
|
|
interest := engine.calculateAccruedInterest(borrowed)
|
|
assert.NotNil(t, interest)
|
|
assert.Greater(t, interest.Int64(), int64(0))
|
|
})
|
|
|
|
t.Run("GetMaxLeverage", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
// Test known symbol
|
|
leverage := engine.getMaxLeverage("BTC-USDT", CrossMargin)
|
|
assert.Equal(t, 100.0, leverage)
|
|
|
|
// Test portfolio margin (doubles leverage)
|
|
leverage = engine.getMaxLeverage("BTC-USDT", PortfolioMargin)
|
|
assert.Equal(t, 200.0, leverage)
|
|
|
|
// Test unknown symbol
|
|
leverage = engine.getMaxLeverage("UNKNOWN-PAIR", CrossMargin)
|
|
assert.Equal(t, 10.0, leverage) // Default
|
|
})
|
|
|
|
t.Run("GetAssetHaircut", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
// Test BTC haircut
|
|
haircut := engine.getAssetHaircut("BTC")
|
|
assert.Equal(t, 0.10, haircut)
|
|
|
|
// Test stablecoin (no haircut)
|
|
haircut = engine.getAssetHaircut("USDT")
|
|
assert.Equal(t, 0.00, haircut)
|
|
|
|
// Test unknown asset
|
|
haircut = engine.getAssetHaircut("UNKNOWN")
|
|
assert.Equal(t, 0.20, haircut) // Default
|
|
})
|
|
|
|
t.Run("GetAssetLTV", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
// Test BTC LTV
|
|
ltv := engine.getAssetLTV("BTC")
|
|
assert.Equal(t, 0.80, ltv)
|
|
|
|
// Test stablecoin LTV
|
|
ltv = engine.getAssetLTV("USDT")
|
|
assert.Equal(t, 0.95, ltv)
|
|
|
|
// Test unknown asset
|
|
ltv = engine.getAssetLTV("UNKNOWN")
|
|
assert.Equal(t, 0.50, ltv) // Default
|
|
})
|
|
|
|
t.Run("ShouldLiquidatePosition", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
// Long position
|
|
longPosition := &MarginPosition{
|
|
Side: Buy,
|
|
LiquidationPrice: 45000.0,
|
|
}
|
|
|
|
// Price above liquidation - should not liquidate
|
|
assert.False(t, engine.shouldLiquidatePosition(longPosition, 46000.0))
|
|
|
|
// Price at liquidation - should liquidate
|
|
assert.True(t, engine.shouldLiquidatePosition(longPosition, 45000.0))
|
|
|
|
// Price below liquidation - should liquidate
|
|
assert.True(t, engine.shouldLiquidatePosition(longPosition, 44000.0))
|
|
|
|
// Short position
|
|
shortPosition := &MarginPosition{
|
|
Side: Sell,
|
|
LiquidationPrice: 55000.0,
|
|
}
|
|
|
|
// Price below liquidation - should not liquidate
|
|
assert.False(t, engine.shouldLiquidatePosition(shortPosition, 54000.0))
|
|
|
|
// Price at liquidation - should liquidate
|
|
assert.True(t, engine.shouldLiquidatePosition(shortPosition, 55000.0))
|
|
|
|
// Price above liquidation - should liquidate
|
|
assert.True(t, engine.shouldLiquidatePosition(shortPosition, 56000.0))
|
|
})
|
|
|
|
t.Run("LiquidatePosition", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
|
|
// Create position with negative PnL
|
|
position := &MarginPosition{
|
|
ID: "pos1",
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Size: 0.01,
|
|
UnrealizedPnL: big.NewInt(-1000),
|
|
Margin: big.NewInt(500),
|
|
}
|
|
account.Positions["pos1"] = position
|
|
|
|
// Liquidate
|
|
engine.liquidatePosition("user1", "pos1", position)
|
|
|
|
// Check position was removed
|
|
assert.NotContains(t, account.Positions, "pos1")
|
|
})
|
|
|
|
t.Run("LiquidateAccount", func(t *testing.T) {
|
|
oracle := NewPriceOracle()
|
|
riskEngine := NewRiskEngine()
|
|
engine := NewMarginEngine(oracle, riskEngine)
|
|
|
|
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
|
account.Equity = big.NewInt(1000)
|
|
account.MarginUsed = big.NewInt(500)
|
|
|
|
// Add positions
|
|
position1 := &MarginPosition{
|
|
ID: "pos1",
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Size: 0.01,
|
|
UnrealizedPnL: big.NewInt(-500),
|
|
Margin: big.NewInt(250),
|
|
}
|
|
position2 := &MarginPosition{
|
|
ID: "pos2",
|
|
Symbol: "ETH-USDT",
|
|
Side: Sell,
|
|
Size: 0.1,
|
|
UnrealizedPnL: big.NewInt(-300),
|
|
Margin: big.NewInt(250),
|
|
}
|
|
account.Positions["pos1"] = position1
|
|
account.Positions["pos2"] = position2
|
|
|
|
// Liquidate account
|
|
engine.liquidateAccount("user1", account)
|
|
|
|
// Check all positions removed
|
|
assert.Empty(t, account.Positions)
|
|
assert.Equal(t, big.NewInt(0), account.MarginUsed)
|
|
assert.Equal(t, 0.0, account.Leverage)
|
|
})
|
|
|
|
t.Run("GeneratePositionID", func(t *testing.T) {
|
|
id1 := generatePositionID()
|
|
id2 := generatePositionID()
|
|
|
|
assert.NotEmpty(t, id1)
|
|
assert.NotEmpty(t, id2)
|
|
assert.NotEqual(t, id1, id2)
|
|
assert.Contains(t, id1, "pos_")
|
|
})
|
|
|
|
t.Run("OppositeSide", func(t *testing.T) {
|
|
assert.Equal(t, Sell, oppositeSide(Buy))
|
|
assert.Equal(t, Buy, oppositeSide(Sell))
|
|
})
|
|
}
|
|
|
|
// TestLendingPool tests lending pool functionality
|
|
func TestLendingPool(t *testing.T) {
|
|
t.Run("NewLendingPool", func(t *testing.T) {
|
|
pool := NewLendingPool()
|
|
assert.NotNil(t, pool)
|
|
assert.NotNil(t, pool.Pools)
|
|
assert.NotNil(t, pool.Suppliers)
|
|
assert.NotNil(t, pool.Borrowers)
|
|
assert.NotNil(t, pool.TotalSupplied)
|
|
assert.NotNil(t, pool.TotalBorrowed)
|
|
})
|
|
|
|
t.Run("Supply", func(t *testing.T) {
|
|
pool := NewLendingPool()
|
|
|
|
// Create pool first
|
|
poolConfig := PoolConfig{
|
|
ReserveFactor: 0.1,
|
|
CollateralFactor: 0.75,
|
|
LiquidationThreshold: 0.85,
|
|
LiquidationPenalty: 0.05,
|
|
MaxBorrowRate: 0.5,
|
|
MinBorrowRate: 0.02,
|
|
OptimalUtilization: 0.8,
|
|
}
|
|
pool.CreatePool("BTC", poolConfig)
|
|
|
|
amount := big.NewInt(10000)
|
|
err := pool.Supply("user1", "BTC", amount)
|
|
assert.NoError(t, err)
|
|
|
|
available := pool.GetAvailable("BTC")
|
|
assert.Equal(t, amount, available)
|
|
})
|
|
|
|
t.Run("Borrow", func(t *testing.T) {
|
|
pool := NewLendingPool()
|
|
|
|
// Create pool first
|
|
poolConfig := PoolConfig{
|
|
ReserveFactor: 0.1,
|
|
CollateralFactor: 0.75,
|
|
LiquidationThreshold: 0.85,
|
|
LiquidationPenalty: 0.05,
|
|
MaxBorrowRate: 0.5,
|
|
MinBorrowRate: 0.02,
|
|
OptimalUtilization: 0.8,
|
|
}
|
|
pool.CreatePool("BTC", poolConfig)
|
|
|
|
// Supply first
|
|
supplied := big.NewInt(10000)
|
|
pool.Supply("user1", "BTC", supplied)
|
|
|
|
// Borrow
|
|
borrowAmount := big.NewInt(5000)
|
|
err := pool.Borrow("BTC", borrowAmount)
|
|
assert.NoError(t, err)
|
|
|
|
// Check available reduced
|
|
available := pool.GetAvailable("BTC")
|
|
expected := new(big.Int).Sub(supplied, borrowAmount)
|
|
assert.Equal(t, expected, available)
|
|
|
|
// Try to borrow more than available
|
|
err = pool.Borrow("BTC", big.NewInt(10000))
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("Repay", func(t *testing.T) {
|
|
pool := NewLendingPool()
|
|
|
|
// Create pool first
|
|
poolConfig := PoolConfig{
|
|
ReserveFactor: 0.1,
|
|
CollateralFactor: 0.75,
|
|
LiquidationThreshold: 0.85,
|
|
LiquidationPenalty: 0.05,
|
|
MaxBorrowRate: 0.5,
|
|
MinBorrowRate: 0.02,
|
|
OptimalUtilization: 0.8,
|
|
}
|
|
pool.CreatePool("BTC", poolConfig)
|
|
|
|
// Setup
|
|
pool.Supply("user1", "BTC", big.NewInt(10000))
|
|
pool.Borrow("BTC", big.NewInt(5000))
|
|
|
|
// Repay with interest
|
|
repayAmount := big.NewInt(5000)
|
|
interest := big.NewInt(50)
|
|
pool.Repay("BTC", repayAmount, interest)
|
|
|
|
// Check available increased
|
|
available := pool.GetAvailable("BTC")
|
|
assert.Greater(t, available.Int64(), int64(10000))
|
|
})
|
|
|
|
t.Run("GetBorrowRate", func(t *testing.T) {
|
|
pool := NewLendingPool()
|
|
|
|
// Test getting borrow rate
|
|
rate := pool.GetBorrowRate("BTC")
|
|
assert.GreaterOrEqual(t, rate, 0.0)
|
|
|
|
// Unknown asset should return default
|
|
rate = pool.GetBorrowRate("UNKNOWN")
|
|
assert.GreaterOrEqual(t, rate, 0.0)
|
|
})
|
|
|
|
t.Run("GetSupplyRate", func(t *testing.T) {
|
|
pool := NewLendingPool()
|
|
|
|
// Create pool first
|
|
poolConfig := PoolConfig{
|
|
ReserveFactor: 0.1,
|
|
CollateralFactor: 0.75,
|
|
LiquidationThreshold: 0.85,
|
|
LiquidationPenalty: 0.05,
|
|
MaxBorrowRate: 0.5,
|
|
MinBorrowRate: 0.02,
|
|
OptimalUtilization: 0.8,
|
|
}
|
|
pool.CreatePool("BTC", poolConfig)
|
|
|
|
// Supply and borrow to calculate utilization
|
|
pool.Supply("user1", "BTC", big.NewInt(10000))
|
|
pool.Borrow("BTC", big.NewInt(5000))
|
|
|
|
rate := pool.GetSupplyRate("BTC")
|
|
assert.Greater(t, rate, 0.0)
|
|
})
|
|
}
|
|
|
|
// TestLiquidationEngine tests liquidation engine
|
|
func TestLiquidationEngine(t *testing.T) {
|
|
t.Run("NewLiquidationEngine", func(t *testing.T) {
|
|
engine := NewLiquidationEngine()
|
|
assert.NotNil(t, engine)
|
|
assert.NotNil(t, engine.InsuranceFund)
|
|
assert.NotNil(t, engine.LiquidationQueue)
|
|
assert.NotNil(t, engine.Liquidators)
|
|
assert.NotNil(t, engine.MaintenanceMargin)
|
|
})
|
|
|
|
t.Run("ProcessLiquidation", func(t *testing.T) {
|
|
engine := NewLiquidationEngine()
|
|
|
|
position := &MarginPosition{
|
|
ID: "pos1",
|
|
Symbol: "BTC-USDT",
|
|
Side: Buy,
|
|
Size: 0.01,
|
|
Margin: big.NewInt(500),
|
|
UnrealizedPnL: big.NewInt(-100),
|
|
}
|
|
|
|
liquidationOrder := &Order{
|
|
Symbol: "BTC-USDT",
|
|
Side: Sell,
|
|
Type: Market,
|
|
Size: 0.01,
|
|
}
|
|
|
|
engine.ProcessLiquidation("user1", position, liquidationOrder)
|
|
|
|
// Check liquidation metrics updated
|
|
assert.Greater(t, engine.TotalLiquidations, uint64(0))
|
|
})
|
|
|
|
t.Run("InsuranceFund", func(t *testing.T) {
|
|
engine := NewLiquidationEngine()
|
|
|
|
// Test insurance fund operations
|
|
assert.NotNil(t, engine.InsuranceFund)
|
|
assert.NotNil(t, engine.InsuranceFund.Balance)
|
|
|
|
// Add to insurance fund for BTC
|
|
if engine.InsuranceFund.Balance["BTC"] == nil {
|
|
engine.InsuranceFund.Balance["BTC"] = big.NewInt(0)
|
|
}
|
|
initialBalance := new(big.Int).Set(engine.InsuranceFund.Balance["BTC"])
|
|
|
|
amount := big.NewInt(1000)
|
|
engine.InsuranceFund.Balance["BTC"].Add(engine.InsuranceFund.Balance["BTC"], amount)
|
|
|
|
// Check balance increased
|
|
assert.Greater(t, engine.InsuranceFund.Balance["BTC"].Int64(), initialBalance.Int64())
|
|
})
|
|
}
|
|
|
|
// TestRiskEngine tests risk engine functionality
|
|
func TestRiskEngine(t *testing.T) {
|
|
t.Run("NewRiskEngine", func(t *testing.T) {
|
|
engine := NewRiskEngine()
|
|
assert.NotNil(t, engine)
|
|
assert.NotNil(t, engine.MaxLeverage)
|
|
assert.NotNil(t, engine.MaintenanceMargin)
|
|
assert.NotNil(t, engine.InitialMargin)
|
|
assert.NotNil(t, engine.MaxPositionSize)
|
|
assert.Greater(t, engine.MaxTotalExposure, 0.0)
|
|
})
|
|
|
|
t.Run("CheckPositionRisk", func(t *testing.T) {
|
|
engine := NewRiskEngine()
|
|
|
|
// Test within limits
|
|
approved := engine.CheckPositionRisk("BTC-USDT", 1.0, 10.0)
|
|
assert.True(t, approved)
|
|
|
|
// Test excessive leverage
|
|
approved = engine.CheckPositionRisk("BTC-USDT", 1.0, 200.0)
|
|
assert.False(t, approved)
|
|
|
|
// Test excessive size
|
|
approved = engine.CheckPositionRisk("BTC-USDT", 1000.0, 10.0)
|
|
assert.False(t, approved)
|
|
})
|
|
|
|
t.Run("UpdateExposure", func(t *testing.T) {
|
|
engine := NewRiskEngine()
|
|
|
|
// Update exposure with delta
|
|
initialExposure := engine.TotalExposure
|
|
delta := 5000.0
|
|
engine.UpdateExposure(delta)
|
|
|
|
// Check exposure updated
|
|
assert.Equal(t, initialExposure+delta, engine.TotalExposure)
|
|
|
|
// Test negative delta
|
|
engine.UpdateExposure(-delta)
|
|
assert.Equal(t, initialExposure, engine.TotalExposure)
|
|
})
|
|
|
|
t.Run("CalculateVaR", func(t *testing.T) {
|
|
engine := NewRiskEngine()
|
|
|
|
// Test VaR calculation with positions
|
|
positions := []*MarginPosition{
|
|
{
|
|
Symbol: "BTC-USDT",
|
|
Size: 1.0,
|
|
EntryPrice: 50000.0,
|
|
MarkPrice: 49000.0,
|
|
UnrealizedPnL: big.NewInt(-1000),
|
|
},
|
|
{
|
|
Symbol: "ETH-USDT",
|
|
Size: 10.0,
|
|
EntryPrice: 3000.0,
|
|
MarkPrice: 2950.0,
|
|
UnrealizedPnL: big.NewInt(-500),
|
|
},
|
|
}
|
|
var_ := engine.CalculateVaR(positions, 0.95)
|
|
assert.GreaterOrEqual(t, var_, 0.0)
|
|
})
|
|
}
|