mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
1442 lines
42 KiB
Go
1442 lines
42 KiB
Go
// Package defi provides DeFi protocol indexing for the Lux EVM indexer.
|
|
package defi
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Order book event signatures (keccak256 hashes)
|
|
var (
|
|
// Order Events
|
|
OrderPlacedSig = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
|
|
OrderCancelledSig = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
|
OrderFilledSig = "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"
|
|
OrderPartialSig = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"
|
|
|
|
// Trade Events
|
|
TradeSig = "0x9b3561d1d0bf65f6b8f53bf57c4e227ab4e9b6f7d8b9c8c7c6b5a4b3c2d1e0f1"
|
|
TradeSettledSig = "0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
|
|
|
// Position Events
|
|
PositionOpenedSig = "0x2fe68525253654c21998f35787a8d0f361905ef647c854092430ab65f2f15022"
|
|
PositionClosedSig = "0x93f19bf1ec58a15dc643b37e7e18a1c13e85f4e4c4e3e4e5e6e7e8e9e0e1e2e3"
|
|
PositionModifiedSig = "0x4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5"
|
|
PositionLiquidatedSig = "0x5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4"
|
|
|
|
// Margin Events
|
|
MarginDepositedSig = "0x6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5"
|
|
MarginWithdrawnSig = "0x7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6"
|
|
MarginCallSig = "0x8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7"
|
|
|
|
// Lending Events
|
|
LendingDepositSig = "0x9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8"
|
|
LendingWithdrawSig = "0xa0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9"
|
|
LendingBorrowSig = "0xb1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0"
|
|
LendingRepaySig = "0xc2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1"
|
|
|
|
// Funding Events
|
|
FundingRateSig = "0xd3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2"
|
|
FundingPaymentSig = "0xe4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3"
|
|
|
|
// Clearinghouse Events
|
|
SettlementSig = "0xf5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4"
|
|
InsuranceFundSig = "0xa6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5"
|
|
)
|
|
|
|
// OrderSide represents buy or sell
|
|
type OrderSide string
|
|
|
|
const (
|
|
OrderSideBuy OrderSide = "buy"
|
|
OrderSideSell OrderSide = "sell"
|
|
)
|
|
|
|
// OrderType represents order types
|
|
type OrderType string
|
|
|
|
const (
|
|
OrderTypeLimit OrderType = "limit"
|
|
OrderTypeMarket OrderType = "market"
|
|
OrderTypeStopLimit OrderType = "stop_limit"
|
|
OrderTypeStopMarket OrderType = "stop_market"
|
|
OrderTypeTakeProfit OrderType = "take_profit"
|
|
OrderTypeIOC OrderType = "ioc" // Immediate or Cancel
|
|
OrderTypeFOK OrderType = "fok" // Fill or Kill
|
|
OrderTypeGTC OrderType = "gtc" // Good Till Cancelled
|
|
OrderTypeGTD OrderType = "gtd" // Good Till Date
|
|
)
|
|
|
|
// OrderStatus represents order status
|
|
type OrderStatus string
|
|
|
|
const (
|
|
OrderStatusOpen OrderStatus = "open"
|
|
OrderStatusPartiallyFilled OrderStatus = "partially_filled"
|
|
OrderStatusFilled OrderStatus = "filled"
|
|
OrderStatusCancelled OrderStatus = "cancelled"
|
|
OrderStatusExpired OrderStatus = "expired"
|
|
OrderStatusRejected OrderStatus = "rejected"
|
|
)
|
|
|
|
// Order represents an order in the order book
|
|
type Order struct {
|
|
ID string `json:"id"`
|
|
Market string `json:"market"` // e.g., "BTC-USD", "LUX-USDC"
|
|
Trader string `json:"trader"`
|
|
Side OrderSide `json:"side"`
|
|
Type OrderType `json:"type"`
|
|
Status OrderStatus `json:"status"`
|
|
Price *big.Int `json:"price"` // Price in base units
|
|
Size *big.Int `json:"size"` // Size in base units
|
|
FilledSize *big.Int `json:"filledSize"`
|
|
RemainingSize *big.Int `json:"remainingSize"`
|
|
AveragePrice *big.Int `json:"averagePrice"`
|
|
StopPrice *big.Int `json:"stopPrice,omitempty"`
|
|
TriggerPrice *big.Int `json:"triggerPrice,omitempty"`
|
|
ReduceOnly bool `json:"reduceOnly"`
|
|
PostOnly bool `json:"postOnly"`
|
|
TimeInForce string `json:"timeInForce"`
|
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
FilledAt *time.Time `json:"filledAt,omitempty"`
|
|
CancelledAt *time.Time `json:"cancelledAt,omitempty"`
|
|
TxHash string `json:"txHash"`
|
|
BlockNumber uint64 `json:"blockNumber"`
|
|
LogIndex uint64 `json:"logIndex"`
|
|
Fees *big.Int `json:"fees"`
|
|
FeeToken string `json:"feeToken"`
|
|
}
|
|
|
|
// Trade represents a matched trade
|
|
type Trade struct {
|
|
ID string `json:"id"`
|
|
Market string `json:"market"`
|
|
MakerOrderID string `json:"makerOrderId"`
|
|
TakerOrderID string `json:"takerOrderId"`
|
|
Maker string `json:"maker"`
|
|
Taker string `json:"taker"`
|
|
Side OrderSide `json:"side"` // Taker's side
|
|
Price *big.Int `json:"price"`
|
|
Size *big.Int `json:"size"`
|
|
Volume *big.Int `json:"volume"` // Price * Size
|
|
MakerFee *big.Int `json:"makerFee"`
|
|
TakerFee *big.Int `json:"takerFee"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
TxHash string `json:"txHash"`
|
|
BlockNumber uint64 `json:"blockNumber"`
|
|
LogIndex uint64 `json:"logIndex"`
|
|
Settled bool `json:"settled"`
|
|
}
|
|
|
|
// MarginPosition represents a margin/leveraged position
|
|
type MarginPosition struct {
|
|
ID string `json:"id"`
|
|
Market string `json:"market"`
|
|
Trader string `json:"trader"`
|
|
Side OrderSide `json:"side"`
|
|
Size *big.Int `json:"size"`
|
|
EntryPrice *big.Int `json:"entryPrice"`
|
|
MarkPrice *big.Int `json:"markPrice"`
|
|
LiquidationPrice *big.Int `json:"liquidationPrice"`
|
|
Margin *big.Int `json:"margin"`
|
|
Leverage float64 `json:"leverage"`
|
|
UnrealizedPnL *big.Int `json:"unrealizedPnl"`
|
|
RealizedPnL *big.Int `json:"realizedPnl"`
|
|
FundingPaid *big.Int `json:"fundingPaid"`
|
|
OpenedAt time.Time `json:"openedAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
ClosedAt *time.Time `json:"closedAt,omitempty"`
|
|
Status PositionStatusOB `json:"status"`
|
|
}
|
|
|
|
// PositionStatusOB for order book positions
|
|
type PositionStatusOB string
|
|
|
|
const (
|
|
PositionStatusOBOpen PositionStatusOB = "open"
|
|
PositionStatusOBClosed PositionStatusOB = "closed"
|
|
PositionStatusOBLiquidated PositionStatusOB = "liquidated"
|
|
)
|
|
|
|
// MarginAccount represents a trader's margin account
|
|
type MarginAccount struct {
|
|
Trader string `json:"trader"`
|
|
TotalDeposited *big.Int `json:"totalDeposited"`
|
|
TotalWithdrawn *big.Int `json:"totalWithdrawn"`
|
|
AvailableMargin *big.Int `json:"availableMargin"`
|
|
LockedMargin *big.Int `json:"lockedMargin"`
|
|
UnrealizedPnL *big.Int `json:"unrealizedPnl"`
|
|
RealizedPnL *big.Int `json:"realizedPnl"`
|
|
MaintenanceMargin *big.Int `json:"maintenanceMargin"`
|
|
MarginRatio float64 `json:"marginRatio"`
|
|
Positions map[string]*MarginPosition `json:"positions"`
|
|
LastUpdated time.Time `json:"lastUpdated"`
|
|
}
|
|
|
|
// LendingPosition represents a lending/borrowing position
|
|
type LendingPosition struct {
|
|
ID string `json:"id"`
|
|
Lender string `json:"lender"`
|
|
Token string `json:"token"`
|
|
DepositedAmount *big.Int `json:"depositedAmount"`
|
|
BorrowedAmount *big.Int `json:"borrowedAmount"`
|
|
InterestEarned *big.Int `json:"interestEarned"`
|
|
InterestOwed *big.Int `json:"interestOwed"`
|
|
APY float64 `json:"apy"`
|
|
BorrowAPY float64 `json:"borrowApy"`
|
|
LastAccrual time.Time `json:"lastAccrual"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
// FundingRate represents a funding rate event
|
|
type FundingRate struct {
|
|
Market string `json:"market"`
|
|
Rate *big.Int `json:"rate"` // Rate in basis points
|
|
Premium *big.Int `json:"premium"`
|
|
IndexPrice *big.Int `json:"indexPrice"`
|
|
MarkPrice *big.Int `json:"markPrice"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
NextFunding time.Time `json:"nextFunding"`
|
|
BlockNumber uint64 `json:"blockNumber"`
|
|
}
|
|
|
|
// FundingPayment represents a funding payment
|
|
type FundingPayment struct {
|
|
ID string `json:"id"`
|
|
Market string `json:"market"`
|
|
Trader string `json:"trader"`
|
|
PositionID string `json:"positionId"`
|
|
Amount *big.Int `json:"amount"`
|
|
Rate *big.Int `json:"rate"`
|
|
Side OrderSide `json:"side"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
TxHash string `json:"txHash"`
|
|
BlockNumber uint64 `json:"blockNumber"`
|
|
}
|
|
|
|
// Liquidation represents a position liquidation
|
|
type Liquidation struct {
|
|
ID string `json:"id"`
|
|
Market string `json:"market"`
|
|
Trader string `json:"trader"`
|
|
Liquidator string `json:"liquidator"`
|
|
PositionID string `json:"positionId"`
|
|
Size *big.Int `json:"size"`
|
|
Price *big.Int `json:"price"`
|
|
Collateral *big.Int `json:"collateral"`
|
|
Penalty *big.Int `json:"penalty"`
|
|
InsurancePayout *big.Int `json:"insurancePayout"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
TxHash string `json:"txHash"`
|
|
BlockNumber uint64 `json:"blockNumber"`
|
|
}
|
|
|
|
// OrderBookLevel represents a price level in the order book
|
|
type OrderBookLevel struct {
|
|
Price *big.Int `json:"price"`
|
|
Size *big.Int `json:"size"`
|
|
OrderCount int `json:"orderCount"`
|
|
}
|
|
|
|
// OrderBookSnapshot represents a snapshot of the order book
|
|
type OrderBookSnapshot struct {
|
|
Market string `json:"market"`
|
|
Bids []*OrderBookLevel `json:"bids"`
|
|
Asks []*OrderBookLevel `json:"asks"`
|
|
BestBid *big.Int `json:"bestBid"`
|
|
BestAsk *big.Int `json:"bestAsk"`
|
|
Spread *big.Int `json:"spread"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// MarketStats represents market statistics
|
|
type MarketStats struct {
|
|
Market string `json:"market"`
|
|
LastPrice *big.Int `json:"lastPrice"`
|
|
High24h *big.Int `json:"high24h"`
|
|
Low24h *big.Int `json:"low24h"`
|
|
Volume24h *big.Int `json:"volume24h"`
|
|
Trades24h uint64 `json:"trades24h"`
|
|
OpenInterest *big.Int `json:"openInterest"`
|
|
FundingRate *big.Int `json:"fundingRate"`
|
|
MarkPrice *big.Int `json:"markPrice"`
|
|
IndexPrice *big.Int `json:"indexPrice"`
|
|
BestBid *big.Int `json:"bestBid"`
|
|
BestAsk *big.Int `json:"bestAsk"`
|
|
LastUpdated time.Time `json:"lastUpdated"`
|
|
}
|
|
|
|
// OrderBookIndexer indexes order book data
|
|
type OrderBookIndexer struct {
|
|
mu sync.RWMutex
|
|
orders map[string]*Order
|
|
ordersByTrader map[string]map[string]*Order
|
|
ordersByMarket map[string]map[string]*Order
|
|
trades []*Trade
|
|
tradesByMarket map[string][]*Trade
|
|
positions map[string]*MarginPosition
|
|
marginAccounts map[string]*MarginAccount
|
|
lendingPositions map[string]*LendingPosition
|
|
fundingRates map[string][]*FundingRate
|
|
fundingPayments []*FundingPayment
|
|
liquidations []*Liquidation
|
|
marketStats map[string]*MarketStats
|
|
orderBooks map[string]*OrderBookSnapshot
|
|
}
|
|
|
|
// NewOrderBookIndexer creates a new order book indexer
|
|
func NewOrderBookIndexer() *OrderBookIndexer {
|
|
return &OrderBookIndexer{
|
|
orders: make(map[string]*Order),
|
|
ordersByTrader: make(map[string]map[string]*Order),
|
|
ordersByMarket: make(map[string]map[string]*Order),
|
|
trades: make([]*Trade, 0),
|
|
tradesByMarket: make(map[string][]*Trade),
|
|
positions: make(map[string]*MarginPosition),
|
|
marginAccounts: make(map[string]*MarginAccount),
|
|
lendingPositions: make(map[string]*LendingPosition),
|
|
fundingRates: make(map[string][]*FundingRate),
|
|
fundingPayments: make([]*FundingPayment, 0),
|
|
liquidations: make([]*Liquidation, 0),
|
|
marketStats: make(map[string]*MarketStats),
|
|
orderBooks: make(map[string]*OrderBookSnapshot),
|
|
}
|
|
}
|
|
|
|
// IndexLog processes a log entry for order book events
|
|
func (o *OrderBookIndexer) IndexLog(log *LogEntry) error {
|
|
if len(log.Topics) == 0 {
|
|
return nil
|
|
}
|
|
|
|
topic0 := log.Topics[0]
|
|
|
|
switch topic0 {
|
|
// Order Events
|
|
case OrderPlacedSig:
|
|
return o.indexOrderPlaced(log)
|
|
case OrderCancelledSig:
|
|
return o.indexOrderCancelled(log)
|
|
case OrderFilledSig:
|
|
return o.indexOrderFilled(log)
|
|
case OrderPartialSig:
|
|
return o.indexOrderPartial(log)
|
|
|
|
// Trade Events
|
|
case TradeSig:
|
|
return o.indexTrade(log)
|
|
case TradeSettledSig:
|
|
return o.indexTradeSettled(log)
|
|
|
|
// Position Events
|
|
case PositionOpenedSig:
|
|
return o.indexPositionOpened(log)
|
|
case PositionClosedSig:
|
|
return o.indexPositionClosed(log)
|
|
case PositionModifiedSig:
|
|
return o.indexPositionModified(log)
|
|
case PositionLiquidatedSig:
|
|
return o.indexPositionLiquidated(log)
|
|
|
|
// Margin Events
|
|
case MarginDepositedSig:
|
|
return o.indexMarginDeposited(log)
|
|
case MarginWithdrawnSig:
|
|
return o.indexMarginWithdrawn(log)
|
|
case MarginCallSig:
|
|
return o.indexMarginCall(log)
|
|
|
|
// Lending Events
|
|
case LendingDepositSig:
|
|
return o.indexLendingDeposit(log)
|
|
case LendingWithdrawSig:
|
|
return o.indexLendingWithdraw(log)
|
|
case LendingBorrowSig:
|
|
return o.indexLendingBorrow(log)
|
|
case LendingRepaySig:
|
|
return o.indexLendingRepay(log)
|
|
|
|
// Funding Events
|
|
case FundingRateSig:
|
|
return o.indexFundingRate(log)
|
|
case FundingPaymentSig:
|
|
return o.indexFundingPayment(log)
|
|
|
|
// Clearinghouse Events
|
|
case SettlementSig:
|
|
return o.indexSettlement(log)
|
|
case InsuranceFundSig:
|
|
return o.indexInsuranceFund(log)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexOrderPlaced handles new order placement
|
|
func (o *OrderBookIndexer) indexOrderPlaced(log *LogEntry) error {
|
|
if len(log.Topics) < 4 || len(log.Data) < 256 {
|
|
return fmt.Errorf("invalid order placed log")
|
|
}
|
|
|
|
orderID := log.Topics[1]
|
|
trader := "0x" + log.Topics[2][26:]
|
|
market := parseMarketFromTopic(log.Topics[3])
|
|
|
|
// Parse data: side, type, price, size, etc.
|
|
sideRaw := new(big.Int).SetBytes(hexToBytes(log.Data[0:64])).Uint64()
|
|
typeRaw := new(big.Int).SetBytes(hexToBytes(log.Data[64:128])).Uint64()
|
|
price := new(big.Int).SetBytes(hexToBytes(log.Data[128:192]))
|
|
size := new(big.Int).SetBytes(hexToBytes(log.Data[192:256]))
|
|
|
|
side := OrderSideBuy
|
|
if sideRaw == 1 {
|
|
side = OrderSideSell
|
|
}
|
|
|
|
orderType := parseOrderType(typeRaw)
|
|
|
|
order := &Order{
|
|
ID: orderID,
|
|
Market: market,
|
|
Trader: trader,
|
|
Side: side,
|
|
Type: orderType,
|
|
Status: OrderStatusOpen,
|
|
Price: price,
|
|
Size: size,
|
|
FilledSize: big.NewInt(0),
|
|
RemainingSize: new(big.Int).Set(size),
|
|
AveragePrice: big.NewInt(0),
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
TxHash: log.TxHash,
|
|
BlockNumber: log.BlockNumber,
|
|
LogIndex: log.LogIndex,
|
|
Fees: big.NewInt(0),
|
|
}
|
|
|
|
// Parse optional fields
|
|
if len(log.Data) >= 320 {
|
|
order.StopPrice = new(big.Int).SetBytes(hexToBytes(log.Data[256:320]))
|
|
}
|
|
if len(log.Data) >= 384 {
|
|
flags := new(big.Int).SetBytes(hexToBytes(log.Data[320:384])).Uint64()
|
|
order.ReduceOnly = (flags & 1) != 0
|
|
order.PostOnly = (flags & 2) != 0
|
|
}
|
|
|
|
return o.addOrder(order)
|
|
}
|
|
|
|
// indexOrderCancelled handles order cancellation
|
|
func (o *OrderBookIndexer) indexOrderCancelled(log *LogEntry) error {
|
|
if len(log.Topics) < 2 {
|
|
return fmt.Errorf("invalid order cancelled log")
|
|
}
|
|
|
|
orderID := log.Topics[1]
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if order, ok := o.orders[orderID]; ok {
|
|
order.Status = OrderStatusCancelled
|
|
now := time.Now()
|
|
order.CancelledAt = &now
|
|
order.UpdatedAt = now
|
|
o.updateOrderBook(order.Market)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexOrderFilled handles full order fill
|
|
func (o *OrderBookIndexer) indexOrderFilled(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 128 {
|
|
return fmt.Errorf("invalid order filled log")
|
|
}
|
|
|
|
orderID := log.Topics[1]
|
|
fillPrice := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
fillSize := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if order, ok := o.orders[orderID]; ok {
|
|
order.Status = OrderStatusFilled
|
|
order.FilledSize = fillSize
|
|
order.RemainingSize = big.NewInt(0)
|
|
order.AveragePrice = fillPrice
|
|
now := time.Now()
|
|
order.FilledAt = &now
|
|
order.UpdatedAt = now
|
|
|
|
if len(log.Data) >= 192 {
|
|
order.Fees = new(big.Int).SetBytes(hexToBytes(log.Data[128:192]))
|
|
}
|
|
|
|
o.updateOrderBook(order.Market)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexOrderPartial handles partial order fill
|
|
func (o *OrderBookIndexer) indexOrderPartial(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 128 {
|
|
return fmt.Errorf("invalid order partial log")
|
|
}
|
|
|
|
orderID := log.Topics[1]
|
|
fillPrice := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
fillSize := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if order, ok := o.orders[orderID]; ok {
|
|
order.Status = OrderStatusPartiallyFilled
|
|
order.FilledSize = new(big.Int).Add(order.FilledSize, fillSize)
|
|
order.RemainingSize = new(big.Int).Sub(order.Size, order.FilledSize)
|
|
|
|
// Calculate weighted average price
|
|
if order.AveragePrice.Sign() == 0 {
|
|
order.AveragePrice = fillPrice
|
|
} else {
|
|
// weighted avg = (old_avg * old_filled + new_price * new_filled) / total_filled
|
|
oldTotal := new(big.Int).Mul(order.AveragePrice, new(big.Int).Sub(order.FilledSize, fillSize))
|
|
newTotal := new(big.Int).Mul(fillPrice, fillSize)
|
|
order.AveragePrice = new(big.Int).Div(new(big.Int).Add(oldTotal, newTotal), order.FilledSize)
|
|
}
|
|
|
|
order.UpdatedAt = time.Now()
|
|
o.updateOrderBook(order.Market)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexTrade handles trade execution
|
|
func (o *OrderBookIndexer) indexTrade(log *LogEntry) error {
|
|
if len(log.Topics) < 4 || len(log.Data) < 192 {
|
|
return fmt.Errorf("invalid trade log")
|
|
}
|
|
|
|
tradeID := log.Topics[1]
|
|
makerOrderID := log.Topics[2]
|
|
takerOrderID := log.Topics[3]
|
|
|
|
price := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
size := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
sideRaw := new(big.Int).SetBytes(hexToBytes(log.Data[128:192])).Uint64()
|
|
|
|
side := OrderSideBuy
|
|
if sideRaw == 1 {
|
|
side = OrderSideSell
|
|
}
|
|
|
|
volume := new(big.Int).Mul(price, size)
|
|
volume.Div(volume, big.NewInt(1e18)) // Adjust for decimals
|
|
|
|
// Get maker and taker from orders
|
|
var maker, taker, market string
|
|
o.mu.RLock()
|
|
if makerOrder, ok := o.orders[makerOrderID]; ok {
|
|
maker = makerOrder.Trader
|
|
market = makerOrder.Market
|
|
}
|
|
if takerOrder, ok := o.orders[takerOrderID]; ok {
|
|
taker = takerOrder.Trader
|
|
if market == "" {
|
|
market = takerOrder.Market
|
|
}
|
|
}
|
|
o.mu.RUnlock()
|
|
|
|
trade := &Trade{
|
|
ID: tradeID,
|
|
Market: market,
|
|
MakerOrderID: makerOrderID,
|
|
TakerOrderID: takerOrderID,
|
|
Maker: maker,
|
|
Taker: taker,
|
|
Side: side,
|
|
Price: price,
|
|
Size: size,
|
|
Volume: volume,
|
|
MakerFee: big.NewInt(0),
|
|
TakerFee: big.NewInt(0),
|
|
Timestamp: time.Now(),
|
|
TxHash: log.TxHash,
|
|
BlockNumber: log.BlockNumber,
|
|
LogIndex: log.LogIndex,
|
|
Settled: false,
|
|
}
|
|
|
|
// Parse fees if present
|
|
if len(log.Data) >= 320 {
|
|
trade.MakerFee = new(big.Int).SetBytes(hexToBytes(log.Data[192:256]))
|
|
trade.TakerFee = new(big.Int).SetBytes(hexToBytes(log.Data[256:320]))
|
|
}
|
|
|
|
o.mu.Lock()
|
|
o.trades = append(o.trades, trade)
|
|
o.tradesByMarket[market] = append(o.tradesByMarket[market], trade)
|
|
o.updateMarketStats(market, trade)
|
|
o.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexTradeSettled handles trade settlement
|
|
func (o *OrderBookIndexer) indexTradeSettled(log *LogEntry) error {
|
|
if len(log.Topics) < 2 {
|
|
return fmt.Errorf("invalid trade settled log")
|
|
}
|
|
|
|
tradeID := log.Topics[1]
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
for _, trade := range o.trades {
|
|
if trade.ID == tradeID {
|
|
trade.Settled = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexPositionOpened handles new position opening
|
|
func (o *OrderBookIndexer) indexPositionOpened(log *LogEntry) error {
|
|
if len(log.Topics) < 4 || len(log.Data) < 256 {
|
|
return fmt.Errorf("invalid position opened log")
|
|
}
|
|
|
|
positionID := log.Topics[1]
|
|
trader := "0x" + log.Topics[2][26:]
|
|
market := parseMarketFromTopic(log.Topics[3])
|
|
|
|
sideRaw := new(big.Int).SetBytes(hexToBytes(log.Data[0:64])).Uint64()
|
|
size := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
entryPrice := new(big.Int).SetBytes(hexToBytes(log.Data[128:192]))
|
|
margin := new(big.Int).SetBytes(hexToBytes(log.Data[192:256]))
|
|
|
|
side := OrderSideBuy
|
|
if sideRaw == 1 {
|
|
side = OrderSideSell
|
|
}
|
|
|
|
// Calculate leverage
|
|
notional := new(big.Int).Mul(size, entryPrice)
|
|
notional.Div(notional, big.NewInt(1e18))
|
|
leverage := 1.0
|
|
if margin.Sign() > 0 {
|
|
leverageBig := new(big.Int).Div(notional, margin)
|
|
leverage = float64(leverageBig.Uint64())
|
|
}
|
|
|
|
position := &MarginPosition{
|
|
ID: positionID,
|
|
Market: market,
|
|
Trader: trader,
|
|
Side: side,
|
|
Size: size,
|
|
EntryPrice: entryPrice,
|
|
MarkPrice: entryPrice,
|
|
Margin: margin,
|
|
Leverage: leverage,
|
|
UnrealizedPnL: big.NewInt(0),
|
|
RealizedPnL: big.NewInt(0),
|
|
FundingPaid: big.NewInt(0),
|
|
OpenedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
Status: PositionStatusOBOpen,
|
|
}
|
|
|
|
// Calculate liquidation price
|
|
position.LiquidationPrice = o.calculateLiquidationPrice(position)
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
o.positions[positionID] = position
|
|
|
|
// Update margin account
|
|
o.updateMarginAccount(trader, position, true)
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexPositionClosed handles position closing
|
|
func (o *OrderBookIndexer) indexPositionClosed(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 128 {
|
|
return fmt.Errorf("invalid position closed log")
|
|
}
|
|
|
|
positionID := log.Topics[1]
|
|
exitPrice := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
realizedPnL := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if position, ok := o.positions[positionID]; ok {
|
|
position.Status = PositionStatusOBClosed
|
|
position.RealizedPnL = realizedPnL
|
|
position.MarkPrice = exitPrice
|
|
now := time.Now()
|
|
position.ClosedAt = &now
|
|
position.UpdatedAt = now
|
|
|
|
o.updateMarginAccount(position.Trader, position, false)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexPositionModified handles position modification
|
|
func (o *OrderBookIndexer) indexPositionModified(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 192 {
|
|
return fmt.Errorf("invalid position modified log")
|
|
}
|
|
|
|
positionID := log.Topics[1]
|
|
newSize := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
newMargin := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
newAvgPrice := new(big.Int).SetBytes(hexToBytes(log.Data[128:192]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if position, ok := o.positions[positionID]; ok {
|
|
position.Size = newSize
|
|
position.Margin = newMargin
|
|
position.EntryPrice = newAvgPrice
|
|
position.UpdatedAt = time.Now()
|
|
position.LiquidationPrice = o.calculateLiquidationPrice(position)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexPositionLiquidated handles position liquidation
|
|
func (o *OrderBookIndexer) indexPositionLiquidated(log *LogEntry) error {
|
|
if len(log.Topics) < 3 || len(log.Data) < 192 {
|
|
return fmt.Errorf("invalid position liquidated log")
|
|
}
|
|
|
|
positionID := log.Topics[1]
|
|
liquidator := "0x" + log.Topics[2][26:]
|
|
|
|
liquidationPrice := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
penalty := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
insurancePayout := new(big.Int).SetBytes(hexToBytes(log.Data[128:192]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
var position *MarginPosition
|
|
if pos, ok := o.positions[positionID]; ok {
|
|
position = pos
|
|
position.Status = PositionStatusOBLiquidated
|
|
position.MarkPrice = liquidationPrice
|
|
now := time.Now()
|
|
position.ClosedAt = &now
|
|
position.UpdatedAt = now
|
|
}
|
|
|
|
liquidation := &Liquidation{
|
|
ID: fmt.Sprintf("liq-%s-%s", log.TxHash, positionID),
|
|
Market: "",
|
|
Trader: "",
|
|
Liquidator: liquidator,
|
|
PositionID: positionID,
|
|
Price: liquidationPrice,
|
|
Penalty: penalty,
|
|
InsurancePayout: insurancePayout,
|
|
Timestamp: time.Now(),
|
|
TxHash: log.TxHash,
|
|
BlockNumber: log.BlockNumber,
|
|
}
|
|
|
|
if position != nil {
|
|
liquidation.Market = position.Market
|
|
liquidation.Trader = position.Trader
|
|
liquidation.Size = position.Size
|
|
liquidation.Collateral = position.Margin
|
|
}
|
|
|
|
o.liquidations = append(o.liquidations, liquidation)
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexMarginDeposited handles margin deposits
|
|
func (o *OrderBookIndexer) indexMarginDeposited(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 64 {
|
|
return fmt.Errorf("invalid margin deposited log")
|
|
}
|
|
|
|
trader := "0x" + log.Topics[1][26:]
|
|
amount := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
account := o.getOrCreateMarginAccount(trader)
|
|
account.TotalDeposited = new(big.Int).Add(account.TotalDeposited, amount)
|
|
account.AvailableMargin = new(big.Int).Add(account.AvailableMargin, amount)
|
|
account.LastUpdated = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexMarginWithdrawn handles margin withdrawals
|
|
func (o *OrderBookIndexer) indexMarginWithdrawn(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 64 {
|
|
return fmt.Errorf("invalid margin withdrawn log")
|
|
}
|
|
|
|
trader := "0x" + log.Topics[1][26:]
|
|
amount := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
account := o.getOrCreateMarginAccount(trader)
|
|
account.TotalWithdrawn = new(big.Int).Add(account.TotalWithdrawn, amount)
|
|
account.AvailableMargin = new(big.Int).Sub(account.AvailableMargin, amount)
|
|
account.LastUpdated = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexMarginCall handles margin calls
|
|
func (o *OrderBookIndexer) indexMarginCall(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 64 {
|
|
return fmt.Errorf("invalid margin call log")
|
|
}
|
|
|
|
trader := "0x" + log.Topics[1][26:]
|
|
requiredMargin := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
account := o.getOrCreateMarginAccount(trader)
|
|
account.MaintenanceMargin = requiredMargin
|
|
account.LastUpdated = time.Now()
|
|
|
|
// Calculate margin ratio
|
|
if account.LockedMargin.Sign() > 0 {
|
|
ratio := new(big.Int).Mul(account.AvailableMargin, big.NewInt(10000))
|
|
ratio.Div(ratio, account.LockedMargin)
|
|
account.MarginRatio = float64(ratio.Uint64()) / 100.0
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexLendingDeposit handles lending deposits
|
|
func (o *OrderBookIndexer) indexLendingDeposit(log *LogEntry) error {
|
|
if len(log.Topics) < 3 || len(log.Data) < 64 {
|
|
return fmt.Errorf("invalid lending deposit log")
|
|
}
|
|
|
|
lender := "0x" + log.Topics[1][26:]
|
|
token := "0x" + log.Topics[2][26:]
|
|
amount := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
|
|
positionID := fmt.Sprintf("%s-%s", lender, token)
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
position, ok := o.lendingPositions[positionID]
|
|
if !ok {
|
|
position = &LendingPosition{
|
|
ID: positionID,
|
|
Lender: lender,
|
|
Token: token,
|
|
DepositedAmount: big.NewInt(0),
|
|
BorrowedAmount: big.NewInt(0),
|
|
InterestEarned: big.NewInt(0),
|
|
InterestOwed: big.NewInt(0),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
o.lendingPositions[positionID] = position
|
|
}
|
|
|
|
position.DepositedAmount = new(big.Int).Add(position.DepositedAmount, amount)
|
|
position.UpdatedAt = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexLendingWithdraw handles lending withdrawals
|
|
func (o *OrderBookIndexer) indexLendingWithdraw(log *LogEntry) error {
|
|
if len(log.Topics) < 3 || len(log.Data) < 64 {
|
|
return fmt.Errorf("invalid lending withdraw log")
|
|
}
|
|
|
|
lender := "0x" + log.Topics[1][26:]
|
|
token := "0x" + log.Topics[2][26:]
|
|
amount := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
|
|
positionID := fmt.Sprintf("%s-%s", lender, token)
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if position, ok := o.lendingPositions[positionID]; ok {
|
|
position.DepositedAmount = new(big.Int).Sub(position.DepositedAmount, amount)
|
|
position.UpdatedAt = time.Now()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexLendingBorrow handles borrowing
|
|
func (o *OrderBookIndexer) indexLendingBorrow(log *LogEntry) error {
|
|
if len(log.Topics) < 3 || len(log.Data) < 64 {
|
|
return fmt.Errorf("invalid lending borrow log")
|
|
}
|
|
|
|
borrower := "0x" + log.Topics[1][26:]
|
|
token := "0x" + log.Topics[2][26:]
|
|
amount := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
|
|
positionID := fmt.Sprintf("%s-%s", borrower, token)
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
position, ok := o.lendingPositions[positionID]
|
|
if !ok {
|
|
position = &LendingPosition{
|
|
ID: positionID,
|
|
Lender: borrower,
|
|
Token: token,
|
|
DepositedAmount: big.NewInt(0),
|
|
BorrowedAmount: big.NewInt(0),
|
|
InterestEarned: big.NewInt(0),
|
|
InterestOwed: big.NewInt(0),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
o.lendingPositions[positionID] = position
|
|
}
|
|
|
|
position.BorrowedAmount = new(big.Int).Add(position.BorrowedAmount, amount)
|
|
position.UpdatedAt = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexLendingRepay handles loan repayment
|
|
func (o *OrderBookIndexer) indexLendingRepay(log *LogEntry) error {
|
|
if len(log.Topics) < 3 || len(log.Data) < 128 {
|
|
return fmt.Errorf("invalid lending repay log")
|
|
}
|
|
|
|
borrower := "0x" + log.Topics[1][26:]
|
|
token := "0x" + log.Topics[2][26:]
|
|
principal := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
interest := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
|
|
positionID := fmt.Sprintf("%s-%s", borrower, token)
|
|
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if position, ok := o.lendingPositions[positionID]; ok {
|
|
position.BorrowedAmount = new(big.Int).Sub(position.BorrowedAmount, principal)
|
|
position.InterestOwed = new(big.Int).Sub(position.InterestOwed, interest)
|
|
position.UpdatedAt = time.Now()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexFundingRate handles funding rate updates
|
|
func (o *OrderBookIndexer) indexFundingRate(log *LogEntry) error {
|
|
if len(log.Topics) < 2 || len(log.Data) < 192 {
|
|
return fmt.Errorf("invalid funding rate log")
|
|
}
|
|
|
|
market := parseMarketFromTopic(log.Topics[1])
|
|
|
|
rate := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
markPrice := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
indexPrice := new(big.Int).SetBytes(hexToBytes(log.Data[128:192]))
|
|
|
|
premium := new(big.Int).Sub(markPrice, indexPrice)
|
|
|
|
funding := &FundingRate{
|
|
Market: market,
|
|
Rate: rate,
|
|
Premium: premium,
|
|
IndexPrice: indexPrice,
|
|
MarkPrice: markPrice,
|
|
Timestamp: time.Now(),
|
|
NextFunding: time.Now().Add(8 * time.Hour),
|
|
BlockNumber: log.BlockNumber,
|
|
}
|
|
|
|
o.mu.Lock()
|
|
o.fundingRates[market] = append(o.fundingRates[market], funding)
|
|
|
|
// Update market stats
|
|
if stats, ok := o.marketStats[market]; ok {
|
|
stats.FundingRate = rate
|
|
stats.MarkPrice = markPrice
|
|
stats.IndexPrice = indexPrice
|
|
stats.LastUpdated = time.Now()
|
|
}
|
|
o.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexFundingPayment handles funding payment
|
|
func (o *OrderBookIndexer) indexFundingPayment(log *LogEntry) error {
|
|
if len(log.Topics) < 3 || len(log.Data) < 128 {
|
|
return fmt.Errorf("invalid funding payment log")
|
|
}
|
|
|
|
trader := "0x" + log.Topics[1][26:]
|
|
positionID := log.Topics[2]
|
|
|
|
amount := new(big.Int).SetBytes(hexToBytes(log.Data[0:64]))
|
|
rate := new(big.Int).SetBytes(hexToBytes(log.Data[64:128]))
|
|
|
|
var market string
|
|
var side OrderSide = OrderSideBuy
|
|
|
|
o.mu.RLock()
|
|
if position, ok := o.positions[positionID]; ok {
|
|
market = position.Market
|
|
side = position.Side
|
|
}
|
|
o.mu.RUnlock()
|
|
|
|
payment := &FundingPayment{
|
|
ID: fmt.Sprintf("funding-%s-%d", log.TxHash, log.LogIndex),
|
|
Market: market,
|
|
Trader: trader,
|
|
PositionID: positionID,
|
|
Amount: amount,
|
|
Rate: rate,
|
|
Side: side,
|
|
Timestamp: time.Now(),
|
|
TxHash: log.TxHash,
|
|
BlockNumber: log.BlockNumber,
|
|
}
|
|
|
|
o.mu.Lock()
|
|
o.fundingPayments = append(o.fundingPayments, payment)
|
|
|
|
// Update position's funding paid
|
|
if position, ok := o.positions[positionID]; ok {
|
|
position.FundingPaid = new(big.Int).Add(position.FundingPaid, amount)
|
|
}
|
|
o.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// indexSettlement handles settlement events
|
|
func (o *OrderBookIndexer) indexSettlement(log *LogEntry) error {
|
|
// Settlement event handling
|
|
return nil
|
|
}
|
|
|
|
// indexInsuranceFund handles insurance fund events
|
|
func (o *OrderBookIndexer) indexInsuranceFund(log *LogEntry) error {
|
|
// Insurance fund event handling
|
|
return nil
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (o *OrderBookIndexer) addOrder(order *Order) error {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
o.orders[order.ID] = order
|
|
|
|
// Index by trader
|
|
if o.ordersByTrader[order.Trader] == nil {
|
|
o.ordersByTrader[order.Trader] = make(map[string]*Order)
|
|
}
|
|
o.ordersByTrader[order.Trader][order.ID] = order
|
|
|
|
// Index by market
|
|
if o.ordersByMarket[order.Market] == nil {
|
|
o.ordersByMarket[order.Market] = make(map[string]*Order)
|
|
}
|
|
o.ordersByMarket[order.Market][order.ID] = order
|
|
|
|
// Update order book
|
|
o.updateOrderBook(order.Market)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (o *OrderBookIndexer) updateOrderBook(market string) {
|
|
orders, ok := o.ordersByMarket[market]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Collect bids and asks
|
|
bids := make(map[string]*OrderBookLevel)
|
|
asks := make(map[string]*OrderBookLevel)
|
|
|
|
for _, order := range orders {
|
|
if order.Status != OrderStatusOpen && order.Status != OrderStatusPartiallyFilled {
|
|
continue
|
|
}
|
|
|
|
priceStr := order.Price.String()
|
|
|
|
if order.Side == OrderSideBuy {
|
|
if level, ok := bids[priceStr]; ok {
|
|
level.Size.Add(level.Size, order.RemainingSize)
|
|
level.OrderCount++
|
|
} else {
|
|
bids[priceStr] = &OrderBookLevel{
|
|
Price: new(big.Int).Set(order.Price),
|
|
Size: new(big.Int).Set(order.RemainingSize),
|
|
OrderCount: 1,
|
|
}
|
|
}
|
|
} else {
|
|
if level, ok := asks[priceStr]; ok {
|
|
level.Size.Add(level.Size, order.RemainingSize)
|
|
level.OrderCount++
|
|
} else {
|
|
asks[priceStr] = &OrderBookLevel{
|
|
Price: new(big.Int).Set(order.Price),
|
|
Size: new(big.Int).Set(order.RemainingSize),
|
|
OrderCount: 1,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert to sorted slices
|
|
bidSlice := make([]*OrderBookLevel, 0, len(bids))
|
|
for _, level := range bids {
|
|
bidSlice = append(bidSlice, level)
|
|
}
|
|
sort.Slice(bidSlice, func(i, j int) bool {
|
|
return bidSlice[i].Price.Cmp(bidSlice[j].Price) > 0 // Descending
|
|
})
|
|
|
|
askSlice := make([]*OrderBookLevel, 0, len(asks))
|
|
for _, level := range asks {
|
|
askSlice = append(askSlice, level)
|
|
}
|
|
sort.Slice(askSlice, func(i, j int) bool {
|
|
return askSlice[i].Price.Cmp(askSlice[j].Price) < 0 // Ascending
|
|
})
|
|
|
|
snapshot := &OrderBookSnapshot{
|
|
Market: market,
|
|
Bids: bidSlice,
|
|
Asks: askSlice,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if len(bidSlice) > 0 {
|
|
snapshot.BestBid = bidSlice[0].Price
|
|
}
|
|
if len(askSlice) > 0 {
|
|
snapshot.BestAsk = askSlice[0].Price
|
|
}
|
|
if snapshot.BestBid != nil && snapshot.BestAsk != nil {
|
|
snapshot.Spread = new(big.Int).Sub(snapshot.BestAsk, snapshot.BestBid)
|
|
}
|
|
|
|
o.orderBooks[market] = snapshot
|
|
|
|
// Update market stats
|
|
if stats, ok := o.marketStats[market]; ok {
|
|
stats.BestBid = snapshot.BestBid
|
|
stats.BestAsk = snapshot.BestAsk
|
|
stats.LastUpdated = time.Now()
|
|
}
|
|
}
|
|
|
|
func (o *OrderBookIndexer) updateMarketStats(market string, trade *Trade) {
|
|
stats, ok := o.marketStats[market]
|
|
if !ok {
|
|
stats = &MarketStats{
|
|
Market: market,
|
|
High24h: big.NewInt(0),
|
|
Low24h: big.NewInt(0),
|
|
Volume24h: big.NewInt(0),
|
|
OpenInterest: big.NewInt(0),
|
|
}
|
|
o.marketStats[market] = stats
|
|
}
|
|
|
|
stats.LastPrice = trade.Price
|
|
stats.Volume24h = new(big.Int).Add(stats.Volume24h, trade.Volume)
|
|
stats.Trades24h++
|
|
|
|
if stats.High24h.Sign() == 0 || trade.Price.Cmp(stats.High24h) > 0 {
|
|
stats.High24h = trade.Price
|
|
}
|
|
if stats.Low24h.Sign() == 0 || trade.Price.Cmp(stats.Low24h) < 0 {
|
|
stats.Low24h = trade.Price
|
|
}
|
|
|
|
stats.LastUpdated = time.Now()
|
|
}
|
|
|
|
func (o *OrderBookIndexer) getOrCreateMarginAccount(trader string) *MarginAccount {
|
|
account, ok := o.marginAccounts[trader]
|
|
if !ok {
|
|
account = &MarginAccount{
|
|
Trader: trader,
|
|
TotalDeposited: big.NewInt(0),
|
|
TotalWithdrawn: big.NewInt(0),
|
|
AvailableMargin: big.NewInt(0),
|
|
LockedMargin: big.NewInt(0),
|
|
UnrealizedPnL: big.NewInt(0),
|
|
RealizedPnL: big.NewInt(0),
|
|
MaintenanceMargin: big.NewInt(0),
|
|
Positions: make(map[string]*MarginPosition),
|
|
LastUpdated: time.Now(),
|
|
}
|
|
o.marginAccounts[trader] = account
|
|
}
|
|
return account
|
|
}
|
|
|
|
func (o *OrderBookIndexer) updateMarginAccount(trader string, position *MarginPosition, isOpening bool) {
|
|
account := o.getOrCreateMarginAccount(trader)
|
|
|
|
if isOpening {
|
|
account.Positions[position.ID] = position
|
|
account.LockedMargin = new(big.Int).Add(account.LockedMargin, position.Margin)
|
|
account.AvailableMargin = new(big.Int).Sub(account.AvailableMargin, position.Margin)
|
|
} else {
|
|
delete(account.Positions, position.ID)
|
|
account.LockedMargin = new(big.Int).Sub(account.LockedMargin, position.Margin)
|
|
account.RealizedPnL = new(big.Int).Add(account.RealizedPnL, position.RealizedPnL)
|
|
// Return margin + PnL to available
|
|
returned := new(big.Int).Add(position.Margin, position.RealizedPnL)
|
|
account.AvailableMargin = new(big.Int).Add(account.AvailableMargin, returned)
|
|
}
|
|
|
|
account.LastUpdated = time.Now()
|
|
}
|
|
|
|
func (o *OrderBookIndexer) calculateLiquidationPrice(position *MarginPosition) *big.Int {
|
|
// Simplified liquidation price calculation
|
|
// For longs: liqPrice = entryPrice * (1 - 1/leverage + maintenanceMargin)
|
|
// For shorts: liqPrice = entryPrice * (1 + 1/leverage - maintenanceMargin)
|
|
|
|
maintenanceRatio := big.NewInt(50) // 0.5% in basis points
|
|
leverageBig := big.NewInt(int64(position.Leverage * 1000))
|
|
|
|
// 1/leverage in basis points
|
|
inverseLeverage := new(big.Int).Div(big.NewInt(1000000), leverageBig)
|
|
|
|
if position.Side == OrderSideBuy {
|
|
// Long: liqPrice = entryPrice * (1 - 1/leverage + maintenance)
|
|
factor := new(big.Int).Sub(big.NewInt(10000), inverseLeverage)
|
|
factor.Add(factor, maintenanceRatio)
|
|
liqPrice := new(big.Int).Mul(position.EntryPrice, factor)
|
|
return liqPrice.Div(liqPrice, big.NewInt(10000))
|
|
} else {
|
|
// Short: liqPrice = entryPrice * (1 + 1/leverage - maintenance)
|
|
factor := new(big.Int).Add(big.NewInt(10000), inverseLeverage)
|
|
factor.Sub(factor, maintenanceRatio)
|
|
liqPrice := new(big.Int).Mul(position.EntryPrice, factor)
|
|
return liqPrice.Div(liqPrice, big.NewInt(10000))
|
|
}
|
|
}
|
|
|
|
// Query methods
|
|
|
|
// GetOrder returns an order by ID
|
|
func (o *OrderBookIndexer) GetOrder(id string) (*Order, bool) {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
order, ok := o.orders[id]
|
|
return order, ok
|
|
}
|
|
|
|
// GetOrdersByTrader returns all orders for a trader
|
|
func (o *OrderBookIndexer) GetOrdersByTrader(trader string) []*Order {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
|
|
orders := make([]*Order, 0)
|
|
if traderOrders, ok := o.ordersByTrader[trader]; ok {
|
|
for _, order := range traderOrders {
|
|
orders = append(orders, order)
|
|
}
|
|
}
|
|
return orders
|
|
}
|
|
|
|
// GetOrdersByMarket returns all orders for a market
|
|
func (o *OrderBookIndexer) GetOrdersByMarket(market string) []*Order {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
|
|
orders := make([]*Order, 0)
|
|
if marketOrders, ok := o.ordersByMarket[market]; ok {
|
|
for _, order := range marketOrders {
|
|
orders = append(orders, order)
|
|
}
|
|
}
|
|
return orders
|
|
}
|
|
|
|
// GetOpenOrders returns all open orders
|
|
func (o *OrderBookIndexer) GetOpenOrders(trader string) []*Order {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
|
|
var result []*Order
|
|
if traderOrders, ok := o.ordersByTrader[trader]; ok {
|
|
for _, order := range traderOrders {
|
|
if order.Status == OrderStatusOpen || order.Status == OrderStatusPartiallyFilled {
|
|
result = append(result, order)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetTrades returns all trades
|
|
func (o *OrderBookIndexer) GetTrades() []*Trade {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.trades
|
|
}
|
|
|
|
// GetTradesByMarket returns trades for a market
|
|
func (o *OrderBookIndexer) GetTradesByMarket(market string) []*Trade {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.tradesByMarket[market]
|
|
}
|
|
|
|
// GetPosition returns a position by ID
|
|
func (o *OrderBookIndexer) GetPosition(id string) (*MarginPosition, bool) {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
position, ok := o.positions[id]
|
|
return position, ok
|
|
}
|
|
|
|
// GetPositionsByTrader returns all positions for a trader
|
|
func (o *OrderBookIndexer) GetPositionsByTrader(trader string) []*MarginPosition {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
|
|
var result []*MarginPosition
|
|
for _, position := range o.positions {
|
|
if position.Trader == trader {
|
|
result = append(result, position)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetMarginAccount returns a margin account
|
|
func (o *OrderBookIndexer) GetMarginAccount(trader string) (*MarginAccount, bool) {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
account, ok := o.marginAccounts[trader]
|
|
return account, ok
|
|
}
|
|
|
|
// GetLendingPosition returns a lending position
|
|
func (o *OrderBookIndexer) GetLendingPosition(lender, token string) (*LendingPosition, bool) {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
positionID := fmt.Sprintf("%s-%s", lender, token)
|
|
position, ok := o.lendingPositions[positionID]
|
|
return position, ok
|
|
}
|
|
|
|
// GetOrderBook returns the order book snapshot
|
|
func (o *OrderBookIndexer) GetOrderBook(market string) (*OrderBookSnapshot, bool) {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
ob, ok := o.orderBooks[market]
|
|
return ob, ok
|
|
}
|
|
|
|
// GetMarketStats returns market statistics
|
|
func (o *OrderBookIndexer) GetMarketStats(market string) (*MarketStats, bool) {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
stats, ok := o.marketStats[market]
|
|
return stats, ok
|
|
}
|
|
|
|
// GetFundingRates returns funding rates for a market
|
|
func (o *OrderBookIndexer) GetFundingRates(market string) []*FundingRate {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.fundingRates[market]
|
|
}
|
|
|
|
// GetLiquidations returns all liquidations
|
|
func (o *OrderBookIndexer) GetLiquidations() []*Liquidation {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.liquidations
|
|
}
|
|
|
|
// GetFundingPayments returns all funding payments
|
|
func (o *OrderBookIndexer) GetFundingPayments() []*FundingPayment {
|
|
o.mu.RLock()
|
|
defer o.mu.RUnlock()
|
|
return o.fundingPayments
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func parseMarketFromTopic(topic string) string {
|
|
if len(topic) > 10 {
|
|
return topic[:10] + "..."
|
|
}
|
|
return topic
|
|
}
|
|
|
|
func parseOrderType(typeRaw uint64) OrderType {
|
|
switch typeRaw {
|
|
case 0:
|
|
return OrderTypeLimit
|
|
case 1:
|
|
return OrderTypeMarket
|
|
case 2:
|
|
return OrderTypeStopLimit
|
|
case 3:
|
|
return OrderTypeStopMarket
|
|
case 4:
|
|
return OrderTypeTakeProfit
|
|
default:
|
|
return OrderTypeLimit
|
|
}
|
|
}
|