fix: db migration tab char corrupted, restore historical trades
This commit is contained in:
@@ -59,6 +59,10 @@ type ArbPosition struct {
|
||||
ExitTotalFees float64 // total fee %
|
||||
ExitConvergence string // convergence label
|
||||
ExitReasonText string // reason for exit
|
||||
ExitLongPnlUSD float64 // per-exchange PnL in USD (for retryClose)
|
||||
ExitShortPnlUSD float64
|
||||
ExitLongFeeUSD float64 // per-exchange fee in USD
|
||||
ExitShortFeeUSD float64
|
||||
|
||||
// Track all entry prices for weighted-average PnL across scale-ins (Issue #2)
|
||||
LongEntryPrices []float64 // all long entry prices (initial + scale-ins)
|
||||
@@ -138,6 +142,9 @@ type Trader struct {
|
||||
// Historical stats loaded from DB on startup — combined with session stats in GetClosedStats
|
||||
dbConverged, dbDiverged, dbFlat, dbTotal int
|
||||
|
||||
// Per-exchange fund tracking
|
||||
exchangeFunds map[string]*ExchangeFund
|
||||
|
||||
OnTradeEvent func(event string, data interface{}) // P3-4: real-time SSE push
|
||||
|
||||
// Decoupled snapshot for display — snapMu never contended by trading path
|
||||
@@ -160,6 +167,17 @@ type TradeRecord struct {
|
||||
ClosedAt time.Time
|
||||
ScaleLevels int
|
||||
AmountUSD float64
|
||||
PnlLongUSD float64 // per-exchange PnL in USD
|
||||
PnlShortUSD float64
|
||||
FeeLongUSD float64 // per-exchange total fee in USD (entry+exit)
|
||||
FeeShortUSD float64
|
||||
}
|
||||
|
||||
// ExchangeFund tracks balance and PnL for one exchange.
|
||||
type ExchangeFund struct {
|
||||
Balance float64 // current available balance
|
||||
TotalFee float64 // cumulative fees paid
|
||||
TotalPnl float64 // cumulative realized PnL
|
||||
}
|
||||
|
||||
func NewTrader(cfg *Config, database *db.DB) *Trader {
|
||||
@@ -178,6 +196,10 @@ func NewTrader(cfg *Config, database *db.DB) *Trader {
|
||||
entering: make(map[string]bool),
|
||||
lastTradeTime: make(map[string]time.Time),
|
||||
blacklist: make(map[string]time.Time),
|
||||
exchangeFunds: map[string]*ExchangeFund{
|
||||
ExBitget: {Balance: cfg.InitialCapital / 2},
|
||||
ExHyperLiquid: {Balance: cfg.InitialCapital / 2},
|
||||
},
|
||||
}
|
||||
|
||||
// Restore open positions from DB on restart
|
||||
@@ -333,6 +355,18 @@ func (t *Trader) TryEntry(opp *ArbOpportunity, store *PriceStore, notifier *Noti
|
||||
t.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
// Margin check: verify both exchanges have sufficient funds
|
||||
reqAmt := t.cfg.TradeAmountUSD * (1 + takerFees[opp.BuyEx]/100 + takerFees[opp.SellEx]/100)
|
||||
if t.exchangeFunds[opp.BuyEx].Balance < reqAmt {
|
||||
t.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
if t.exchangeFunds[opp.SellEx].Balance < reqAmt {
|
||||
t.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
t.entering[opp.Coin] = true
|
||||
t.mu.Unlock()
|
||||
|
||||
@@ -692,6 +726,24 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
pos.ExitTotalFees = totalFees
|
||||
pos.ExitConvergence = convergenceLabel
|
||||
pos.ExitReasonText = exitReason
|
||||
// Pre-compute per-exchange PnL/fees for retryClose
|
||||
numBatchesRetry := 1 + pos.ScaleLevels
|
||||
pos.ExitLongPnlUSD = longPnl / 100 * float64(numBatchesRetry) * t.cfg.TradeAmountUSD
|
||||
pos.ExitShortPnlUSD = shortPnl / 100 * float64(numBatchesRetry) * t.cfg.TradeAmountUSD
|
||||
{
|
||||
totalLongSharesRetry := 0.0
|
||||
for _, p := range pos.LongEntryPrices {
|
||||
totalLongSharesRetry += t.cfg.TradeAmountUSD / p
|
||||
}
|
||||
totalShortSharesRetry := 0.0
|
||||
for _, p := range pos.ShortEntryPrices {
|
||||
totalShortSharesRetry += t.cfg.TradeAmountUSD / p
|
||||
}
|
||||
pos.ExitLongFeeUSD = float64(numBatchesRetry)*t.cfg.TradeAmountUSD*takerFees[pos.LongLeg.Exchange]/100 +
|
||||
totalLongSharesRetry*longCurrent*takerFees[pos.LongLeg.Exchange]/100
|
||||
pos.ExitShortFeeUSD = float64(numBatchesRetry)*t.cfg.TradeAmountUSD*takerFees[pos.ShortLeg.Exchange]/100 +
|
||||
totalShortSharesRetry*shortCurrent*takerFees[pos.ShortLeg.Exchange]/100
|
||||
}
|
||||
|
||||
closeErr := t.closeBothLegs(pos)
|
||||
|
||||
@@ -713,6 +765,44 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
pos.ExitedAt = time.Now()
|
||||
pos.Status = "closed"
|
||||
|
||||
// Compute per-leg PnL and fees in USD
|
||||
numBatches := 1 + pos.ScaleLevels
|
||||
legCapital := t.cfg.TradeAmountUSD
|
||||
longPnlUSD := longPnl / 100 * float64(numBatches) * legCapital
|
||||
shortPnlUSD := shortPnl / 100 * float64(numBatches) * legCapital
|
||||
|
||||
totalLongShares := 0.0
|
||||
for _, p := range pos.LongEntryPrices {
|
||||
totalLongShares += legCapital / p
|
||||
}
|
||||
totalShortShares := 0.0
|
||||
for _, p := range pos.ShortEntryPrices {
|
||||
totalShortShares += legCapital / p
|
||||
}
|
||||
|
||||
longEntryFeeSum := float64(numBatches) * legCapital * takerFees[pos.LongLeg.Exchange] / 100
|
||||
shortEntryFeeSum := float64(numBatches) * legCapital * takerFees[pos.ShortLeg.Exchange] / 100
|
||||
longExitFeeAmt := totalLongShares * pos.LongLeg.ExitPrice * takerFees[pos.LongLeg.Exchange] / 100
|
||||
shortExitFeeAmt := totalShortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
|
||||
longFeeUSD := longEntryFeeSum + longExitFeeAmt
|
||||
shortFeeUSD := shortEntryFeeSum + shortExitFeeAmt
|
||||
|
||||
// Update per-exchange fund tracking
|
||||
t.mu.Lock()
|
||||
if ef, ok := t.exchangeFunds[pos.LongLeg.Exchange]; ok {
|
||||
ef.Balance -= longFeeUSD
|
||||
ef.Balance += longPnlUSD
|
||||
ef.TotalFee += longFeeUSD
|
||||
ef.TotalPnl += longPnlUSD
|
||||
}
|
||||
if ef, ok := t.exchangeFunds[pos.ShortLeg.Exchange]; ok {
|
||||
ef.Balance -= shortFeeUSD
|
||||
ef.Balance += shortPnlUSD
|
||||
ef.TotalFee += shortFeeUSD
|
||||
ef.TotalPnl += shortPnlUSD
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
// Save trade record for stats
|
||||
record := TradeRecord{
|
||||
Coin: pos.Coin,
|
||||
@@ -728,6 +818,10 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
ClosedAt: pos.ExitedAt,
|
||||
ScaleLevels: pos.ScaleLevels,
|
||||
AmountUSD: pos.AmountUSD,
|
||||
PnlLongUSD: longPnlUSD,
|
||||
PnlShortUSD: shortPnlUSD,
|
||||
FeeLongUSD: longFeeUSD,
|
||||
FeeShortUSD: shortFeeUSD,
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
@@ -740,33 +834,21 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
if t.db != nil && pos.DBTradeID > 0 {
|
||||
now := time.Now()
|
||||
status := "filled"
|
||||
tradeUnit := t.cfg.TradeAmountUSD
|
||||
|
||||
totalLongShares := 0.0
|
||||
for _, p := range pos.LongEntryPrices {
|
||||
totalLongShares += tradeUnit / p
|
||||
}
|
||||
totalShortShares := 0.0
|
||||
for _, p := range pos.ShortEntryPrices {
|
||||
totalShortShares += tradeUnit / p
|
||||
}
|
||||
|
||||
// Save exit orders
|
||||
longExitFee := totalLongShares * pos.LongLeg.ExitPrice * takerFees[pos.LongLeg.Exchange] / 100
|
||||
longExitShares := totalLongShares
|
||||
longOID, _ := t.db.SaveOrder(&db.OrderRecord{
|
||||
TradeID: pos.DBTradeID, Leg: "long", Type: "exit",
|
||||
Exchange: pos.LongLeg.Exchange, Side: "sell",
|
||||
Price: &pos.LongLeg.ExitPrice, Size: &longExitShares,
|
||||
Fee: &longExitFee, Status: &status, CreatedAt: now,
|
||||
Fee: &longExitFeeAmt, Status: &status, CreatedAt: now,
|
||||
})
|
||||
shortExitFee := totalShortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
|
||||
shortExitShares := totalShortShares
|
||||
shortOID, _ := t.db.SaveOrder(&db.OrderRecord{
|
||||
TradeID: pos.DBTradeID, Leg: "short", Type: "exit",
|
||||
Exchange: pos.ShortLeg.Exchange, Side: "buy",
|
||||
Price: &pos.ShortLeg.ExitPrice, Size: &shortExitShares,
|
||||
Fee: &shortExitFee, Status: &status, CreatedAt: now,
|
||||
Fee: &shortExitFeeAmt, Status: &status, CreatedAt: now,
|
||||
})
|
||||
// Save exit system order
|
||||
t.db.SaveSystemOrder(&db.SystemOrderRecord{
|
||||
@@ -777,15 +859,9 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
CreatedAt: now,
|
||||
})
|
||||
|
||||
// Close trade: sum fees from in-memory calculation, update status
|
||||
feeEntrySum, feeExitSum := 0.0, 0.0
|
||||
for range pos.LongEntryPrices {
|
||||
feeEntrySum += tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
|
||||
}
|
||||
for range pos.ShortEntryPrices {
|
||||
feeEntrySum += tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
|
||||
}
|
||||
feeExitSum = longExitFee + shortExitFee
|
||||
// Close trade with per-exchange fee/pnl
|
||||
feeEntrySum := longEntryFeeSum + shortEntryFeeSum
|
||||
feeExitSum := longExitFeeAmt + shortExitFeeAmt
|
||||
|
||||
t.db.UpdateTradeStatus(pos.DBTradeID, &db.TradeRecord{
|
||||
Status: "closed",
|
||||
@@ -802,6 +878,10 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
ExitReason: &exitReason,
|
||||
Convergence: &convergenceLabel,
|
||||
ClosedAt: &now,
|
||||
PnlLongUSD: &longPnlUSD,
|
||||
PnlShortUSD: &shortPnlUSD,
|
||||
FeeLongUSD: &longFeeUSD,
|
||||
FeeShortUSD: &shortFeeUSD,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -926,6 +1006,7 @@ func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifi
|
||||
EntrySpread: pos.EntrySpread,
|
||||
ExitSpread: pos.ExitDiffPct,
|
||||
PnlPct: pos.ExitNetPnl,
|
||||
PnlUSD: 2 * pos.AmountUSD * pos.ExitNetPnl / 100,
|
||||
Convergence: pos.ExitConvergence,
|
||||
Reason: pos.ExitReasonText,
|
||||
Duration: elapsed.Round(time.Second).String(),
|
||||
@@ -933,12 +1014,29 @@ func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifi
|
||||
ClosedAt: pos.ExitedAt,
|
||||
ScaleLevels: pos.ScaleLevels,
|
||||
AmountUSD: pos.AmountUSD,
|
||||
PnlLongUSD: pos.ExitLongPnlUSD,
|
||||
PnlShortUSD: pos.ExitShortPnlUSD,
|
||||
FeeLongUSD: pos.ExitLongFeeUSD,
|
||||
FeeShortUSD: pos.ExitShortFeeUSD,
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
delete(t.positions, pos.Coin)
|
||||
t.lastTradeTime[pos.Coin] = time.Now()
|
||||
t.closedTrades = append(t.closedTrades, record)
|
||||
// Update exchange funds
|
||||
if ef, ok := t.exchangeFunds[pos.LongLeg.Exchange]; ok {
|
||||
ef.Balance -= pos.ExitLongFeeUSD
|
||||
ef.Balance += pos.ExitLongPnlUSD
|
||||
ef.TotalFee += pos.ExitLongFeeUSD
|
||||
ef.TotalPnl += pos.ExitLongPnlUSD
|
||||
}
|
||||
if ef, ok := t.exchangeFunds[pos.ShortLeg.Exchange]; ok {
|
||||
ef.Balance -= pos.ExitShortFeeUSD
|
||||
ef.Balance += pos.ExitShortPnlUSD
|
||||
ef.TotalFee += pos.ExitShortFeeUSD
|
||||
ef.TotalPnl += pos.ExitShortPnlUSD
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
// Persist exit orders + close trade in DB (only for legs that weren't already closed)
|
||||
@@ -1002,6 +1100,10 @@ func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifi
|
||||
ExitReason: &pos.ExitReasonText,
|
||||
Convergence: &pos.ExitConvergence,
|
||||
ClosedAt: &now,
|
||||
PnlLongUSD: &pos.ExitLongPnlUSD,
|
||||
PnlShortUSD: &pos.ExitShortPnlUSD,
|
||||
FeeLongUSD: &pos.ExitLongFeeUSD,
|
||||
FeeShortUSD: &pos.ExitShortFeeUSD,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1167,6 +1269,17 @@ func (t *Trader) GetClosedTrades() []TradeRecord {
|
||||
return r
|
||||
}
|
||||
|
||||
// GetExchangeFunds returns a copy of per-exchange fund states.
|
||||
func (t *Trader) GetExchangeFunds() map[string]ExchangeFund {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
r := make(map[string]ExchangeFund, len(t.exchangeFunds))
|
||||
for ex, ef := range t.exchangeFunds {
|
||||
r[ex] = *ef
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// persistTrade saves a completed trade to SQLite, with per-leg orders and system_orders.
|
||||
// restoreOpenPositions loads open trades from DB and recreates their positions.
|
||||
func (t *Trader) restoreOpenPositions() {
|
||||
|
||||
Reference in New Issue
Block a user