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
+17
View File
@@ -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))
+12
View File
@@ -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
}
+21 -7
View File
@@ -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
+78 -28
View File
@@ -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 (
<section className="card" id="stats-card">
<h2>📊 统计数据</h2>
@@ -210,6 +217,11 @@ function StatsCard({ stats }) {
<div className="stat"><label>币种</label><span className="pct-blue">{stats.coins || 0}</span></div>
<div className="stat" id="conn-stats"><label>连接</label><span id="conn-detail" style={{fontSize:11}}>{connHtml}</span></div>
</div>
{exFundsHtml && (
<div className="stats-row" style={{ marginTop: 2, fontSize: 11, opacity: 0.85 }}>
<div className="stat" style={{gridColumn:'1 / -1'}}><label>资金</label><span style={{fontWeight:600}}>{exFundsHtml}</span></div>
</div>
)}
{d && (
<div className="stats-row detail-stats" style={{ marginTop: 4, fontSize: 12, opacity: 0.85 }}>
<div className="stat"><label>总PnL</label><span>{(d.total_pnl_usd != null ? '$' + d.total_pnl_usd.toFixed(2) : '—') + (d.capital_pnl != null ? ' (' + d.capital_pnl.toFixed(4) + '%)' : '')}</span></div>
@@ -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 (
<section className="card" id="positions-card">
<h2>🔒 当前持仓</h2>
<div className="table-wrap">
<table id="positions-table">
<thead>
<tr><th>币种</th><th>方向</th><th>规模</th><th>入价差</th><th>现价差</th><th>估盈亏</th><th>加仓</th><th>时长</th></tr>
</thead>
<tbody id="positions-body">
{positions.length === 0 ? (
<tr><td colSpan="8" className="loading">无持仓</td></tr>
) : (
[...positions].sort((a, b) => a.coin.localeCompare(b.coin)).map(p => (
<tr key={p.coin}>
<td><strong>{p.coin}</strong></td>
<td>{p.direction}</td>
<td className="text-right">${(p.amount_usd || 0).toFixed(0)}</td>
<td className="text-right">{(p.entry_spread || 0).toFixed(4)}%</td>
<td className="text-right">{p.current_spread != null ? p.current_spread.toFixed(4) + '%' : '-'}</td>
<td className={'text-right ' + pnlClass(p.pnl_est)}><strong>{p.pnl_est != null ? '$' + p.pnl_est.toFixed(4) : '-'}</strong></td>
<td className="text-right">{p.scales || 0}</td>
<td>{p.duration || '-'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
<>
<section className="card" id="positions-card">
<h2>🔒 当前持仓</h2>
<div className="table-wrap">
<table id="positions-table">
<thead>
<tr><th>币种</th><th>方向</th><th>规模</th><th>入价差</th><th>现价差</th><th>估盈亏</th><th>加仓</th><th>时长</th></tr>
</thead>
<tbody id="positions-body">
{positions.length === 0 ? (
<tr><td colSpan="8" className="loading">无持仓</td></tr>
) : (
[...positions].sort((a, b) => a.coin.localeCompare(b.coin)).map(p => (
<tr key={p.coin} className="trade-row" onClick={() => openPositionDetail(p.db_trade_id)}>
<td><strong>{p.coin}</strong></td>
<td>{p.direction}</td>
<td className="text-right">${(p.amount_usd || 0).toFixed(0)}</td>
<td className="text-right">{(p.entry_spread || 0).toFixed(4)}%</td>
<td className="text-right">{p.current_spread != null ? p.current_spread.toFixed(4) + '%' : '-'}</td>
<td className={'text-right ' + pnlClass(p.pnl_est)}><strong>{p.pnl_est != null ? '$' + p.pnl_est.toFixed(4) : '-'}</strong></td>
<td className="text-right">{p.scales || 0}</td>
<td>{p.duration || '-'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
{modalOpen && (
<TradeDetailModal trade={modalTrade} orders={modalOrders} onClose={closeModal} />
)}
</>
)
}
+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() {