From f29e78a435eb78c75b97592d19158de8cfed2860 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Mon, 4 May 2026 13:50:49 +0800 Subject: [PATCH] fix: db migration tab char corrupted, restore historical trades --- dashboard.go | 17 +++++ db/db.go | 12 ++++ db/trade_repo.go | 28 ++++++-- frontend/src/App.jsx | 106 +++++++++++++++++++++-------- trader.go | 159 ++++++++++++++++++++++++++++++++++++------- 5 files changed, 264 insertions(+), 58 deletions(-) diff --git a/dashboard.go b/dashboard.go index 321deaf..756a20c 100644 --- a/dashboard.go +++ b/dashboard.go @@ -373,6 +373,11 @@ func (d *Dashboard) broadcastLoop() { "duration": time.Since(pos.StartedAt).Round(time.Second).String(), "started_at": pos.StartedAt.Format("15:04:05"), "started_ts": pos.StartedAt.UnixMilli(), + "long_exchange": pos.LongLeg.Exchange, + "short_exchange": pos.ShortLeg.Exchange, + "long_entry": pos.LongLeg.EntryPrice, + "short_entry": pos.ShortLeg.EntryPrice, + "db_trade_id": pos.DBTradeID, } // Calculate live PnL from current prices — use weighted average for scale-ins @@ -474,6 +479,18 @@ func (d *Dashboard) broadcastLoop() { d.connMu.RUnlock() stats["connections"] = connInfo + // Per-exchange fund tracking + exFunds := d.trader.GetExchangeFunds() + exFundsMap := make(map[string]map[string]float64, len(exFunds)) + for ex, ef := range exFunds { + exFundsMap[ex] = map[string]float64{ + "balance": math.Round(ef.Balance*100) / 100, + "total_fee": math.Round(ef.TotalFee*100) / 100, + "total_pnl": math.Round(ef.TotalPnl*100) / 100, + } + } + stats["exchange_funds"] = exFundsMap + // Blacklist — stale spread coins bl := d.trader.GetBlacklist() blList := make([]map[string]interface{}, 0, len(bl)) diff --git a/db/db.go b/db/db.go index 0262f4e..4cbfb30 100644 --- a/db/db.go +++ b/db/db.go @@ -113,6 +113,18 @@ func (d *DB) migrate() error { if err != nil { return err } + + // Migration v2: add per-exchange fee/pnl columns (idempotent) + for _, col := range []string{"pnl_long_usd", "pnl_short_usd", "fee_long_usd", "fee_short_usd"} { + var found int + d.QueryRow("SELECT COUNT(*) FROM pragma_table_info('trades') WHERE name=?", col).Scan(&found) + if found == 0 { + if _, err := d.Exec("ALTER TABLE trades ADD COLUMN " + col + " REAL"); err != nil { + log.Printf("[DB] Migration: add column %s: %v", col, err) + } + } + } + log.Printf("[DB] SQLite ready: %s", d.Path) return nil } diff --git a/db/trade_repo.go b/db/trade_repo.go index 52b7523..0975561 100644 --- a/db/trade_repo.go +++ b/db/trade_repo.go @@ -30,6 +30,10 @@ type TradeRecord struct { Convergence *string OpenedAt time.Time ClosedAt *time.Time + PnlLongUSD *float64 // per-exchange PnL in USD + PnlShortUSD *float64 + FeeLongUSD *float64 // per-exchange fee in USD + FeeShortUSD *float64 } // OrderRecord mirrors the database row for orders table. @@ -68,12 +72,14 @@ func (d *DB) SaveTrade(t *TradeRecord) (int64, error) { coin, direction, status, entry_spread, exit_spread, long_exchange, short_exchange, long_entry, long_exit, short_entry, short_exit, long_pnl, short_pnl, fee_entry, fee_exit, net_pnl, - amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at - ) VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?)`, + amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at, + pnl_long_usd, pnl_short_usd, fee_long_usd, fee_short_usd + ) VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?)`, t.Coin, t.Direction, t.Status, t.EntrySpread, t.ExitSpread, t.LongExchange, t.ShortExchange, t.LongEntry, t.LongExit, t.ShortEntry, t.ShortExit, t.LongPnl, t.ShortPnl, t.FeeEntry, t.FeeExit, t.NetPnl, t.AmountUSD, t.ScaleCount, t.ExitReason, t.Convergence, t.OpenedAt, t.ClosedAt, + t.PnlLongUSD, t.PnlShortUSD, t.FeeLongUSD, t.FeeShortUSD, ) if err != nil { return 0, err @@ -86,12 +92,14 @@ func (d *DB) UpdateTradeStatus(id int64, t *TradeRecord) error { _, err := d.Exec(`UPDATE trades SET status=?, exit_spread=?, long_exit=?, short_exit=?, long_pnl=?, short_pnl=?, fee_entry=?, fee_exit=?, net_pnl=?, - amount_usd=?, scale_count=?, exit_reason=?, convergence=?, closed_at=? + amount_usd=?, scale_count=?, exit_reason=?, convergence=?, closed_at=?, + pnl_long_usd=?, pnl_short_usd=?, fee_long_usd=?, fee_short_usd=? WHERE id=?`, t.Status, t.ExitSpread, t.LongExit, t.ShortExit, t.LongPnl, t.ShortPnl, t.FeeEntry, t.FeeExit, t.NetPnl, t.AmountUSD, t.ScaleCount, t.ExitReason, t.Convergence, t.ClosedAt, + t.PnlLongUSD, t.PnlShortUSD, t.FeeLongUSD, t.FeeShortUSD, id, ) return err @@ -102,7 +110,8 @@ func (d *DB) GetOpenTrades() ([]TradeRecord, error) { rows, err := d.Query(`SELECT id, coin, direction, status, entry_spread, exit_spread, long_exchange, short_exchange, long_entry, long_exit, short_entry, short_exit, long_pnl, short_pnl, fee_entry, fee_exit, net_pnl, - amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at + amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at, + pnl_long_usd, pnl_short_usd, fee_long_usd, fee_short_usd FROM trades WHERE status='open'`) if err != nil { return nil, err @@ -130,7 +139,8 @@ func (d *DB) GetTrades(page, limit int, coin string) ([]TradeRecord, int, error) query := `SELECT id, coin, direction, status, entry_spread, exit_spread, long_exchange, short_exchange, long_entry, long_exit, short_entry, short_exit, long_pnl, short_pnl, fee_entry, fee_exit, net_pnl, - amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at + amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at, + pnl_long_usd, pnl_short_usd, fee_long_usd, fee_short_usd FROM trades WHERE status='closed'` if coin != "" { query += " AND coin=?" @@ -183,7 +193,8 @@ func (d *DB) GetTradeByID(id int64) (*TradeRecord, []OrderRecord, error) { row := d.QueryRow(`SELECT id, coin, direction, status, entry_spread, exit_spread, long_exchange, short_exchange, long_entry, long_exit, short_entry, short_exit, long_pnl, short_pnl, fee_entry, fee_exit, net_pnl, - amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at + amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at, + pnl_long_usd, pnl_short_usd, fee_long_usd, fee_short_usd FROM trades WHERE id=?`, id) var t TradeRecord @@ -192,6 +203,7 @@ func (d *DB) GetTradeByID(id int64) (*TradeRecord, []OrderRecord, error) { &t.LongExchange, &t.ShortExchange, &t.LongEntry, &t.LongExit, &t.ShortEntry, &t.ShortExit, &t.LongPnl, &t.ShortPnl, &t.FeeEntry, &t.FeeExit, &t.NetPnl, &t.AmountUSD, &t.ScaleCount, &t.ExitReason, &t.Convergence, &t.OpenedAt, &t.ClosedAt, + &t.PnlLongUSD, &t.PnlShortUSD, &t.FeeLongUSD, &t.FeeShortUSD, ) if err != nil { return nil, nil, err @@ -227,6 +239,7 @@ func scanTrades(rows *sql.Rows) ([]TradeRecord, error) { &t.LongExchange, &t.ShortExchange, &t.LongEntry, &t.LongExit, &t.ShortEntry, &t.ShortExit, &t.LongPnl, &t.ShortPnl, &t.FeeEntry, &t.FeeExit, &t.NetPnl, &t.AmountUSD, &t.ScaleCount, &t.ExitReason, &t.Convergence, &t.OpenedAt, &t.ClosedAt, + &t.PnlLongUSD, &t.PnlShortUSD, &t.FeeLongUSD, &t.FeeShortUSD, ); err != nil { return nil, err } @@ -265,7 +278,8 @@ func (d *DB) GetAllClosedTrades() ([]TradeRecord, error) { rows, err := d.Query(`SELECT id, coin, direction, status, entry_spread, exit_spread, long_exchange, short_exchange, long_entry, long_exit, short_entry, short_exit, long_pnl, short_pnl, fee_entry, fee_exit, net_pnl, - amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at + amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at, + pnl_long_usd, pnl_short_usd, fee_long_usd, fee_short_usd FROM trades WHERE status='closed' ORDER BY id`) if err != nil { return nil, err diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 29a84f7..3831447 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -198,6 +198,13 @@ function StatsCard({ stats }) { connHtml = Object.entries(stats.connections) .map(([ex, status]) => `${ex}:${status}`).join(' ') } + // Format exchange funds + let exFundsHtml = '' + if (stats.exchange_funds) { + exFundsHtml = Object.entries(stats.exchange_funds) + .map(([ex, f]) => `${ex}: $${f.balance.toFixed(2)}`) + .join(' | ') + } return (

📊 统计数据

@@ -210,6 +217,11 @@ function StatsCard({ stats }) {
{stats.coins || 0}
{connHtml}
+ {exFundsHtml && ( +
+
{exFundsHtml}
+
+ )} {d && (
{(d.total_pnl_usd != null ? '$' + d.total_pnl_usd.toFixed(2) : '—') + (d.capital_pnl != null ? ' (' + d.capital_pnl.toFixed(4) + '%)' : '')}
@@ -225,35 +237,73 @@ function StatsCard({ stats }) { } function PositionsCard({ positions }) { + const [modalOpen, setModalOpen] = useState(false) + const [modalTrade, setModalTrade] = useState(null) + const [modalOrders, setModalOrders] = useState([]) + + function openPositionDetail(id) { + if (!id) return + setModalOpen(true) + setModalTrade(null) + setModalOrders([]) + fetch('/api/trade/' + id) + .then(r => r.json()) + .then(data => { + setModalTrade(data.trade) + setModalOrders(data.orders || []) + }) + .catch(() => { + setModalTrade({ ID: id }) + }) + } + + function closeModal() { + setModalOpen(false) + } + + useEffect(() => { + if (!modalOpen) return + function handler(e) { + if (e.key === 'Escape') closeModal() + } + document.addEventListener('keydown', handler) + return () => document.removeEventListener('keydown', handler) + }, [modalOpen]) + return ( -
-

🔒 当前持仓

-
- - - - - - {positions.length === 0 ? ( - - ) : ( - [...positions].sort((a, b) => a.coin.localeCompare(b.coin)).map(p => ( - - - - - - - - - - - )) - )} - -
币种方向规模入价差现价差估盈亏加仓时长
无持仓
{p.coin}{p.direction}${(p.amount_usd || 0).toFixed(0)}{(p.entry_spread || 0).toFixed(4)}%{p.current_spread != null ? p.current_spread.toFixed(4) + '%' : '-'}{p.pnl_est != null ? '$' + p.pnl_est.toFixed(4) : '-'}{p.scales || 0}{p.duration || '-'}
-
-
+ <> +
+

🔒 当前持仓

+
+ + + + + + {positions.length === 0 ? ( + + ) : ( + [...positions].sort((a, b) => a.coin.localeCompare(b.coin)).map(p => ( + openPositionDetail(p.db_trade_id)}> + + + + + + + + + + )) + )} + +
币种方向规模入价差现价差估盈亏加仓时长
无持仓
{p.coin}{p.direction}${(p.amount_usd || 0).toFixed(0)}{(p.entry_spread || 0).toFixed(4)}%{p.current_spread != null ? p.current_spread.toFixed(4) + '%' : '-'}{p.pnl_est != null ? '$' + p.pnl_est.toFixed(4) : '-'}{p.scales || 0}{p.duration || '-'}
+
+
+ {modalOpen && ( + + )} + ) } diff --git a/trader.go b/trader.go index f95dc4a..041c6a8 100644 --- a/trader.go +++ b/trader.go @@ -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() {