fix: db migration tab char corrupted, restore historical trades

This commit is contained in:
jackyu66git
2026-05-04 13:50:49 +08:00
parent e74fd084ce
commit f29e78a435
5 changed files with 264 additions and 58 deletions
+136 -23
View File
@@ -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() {