fix: db migration tab char corrupted, restore historical trades
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+51
-1
@@ -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,7 +237,41 @@ 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">
|
||||
@@ -238,7 +284,7 @@ function PositionsCard({ positions }) {
|
||||
<tr><td colSpan="8" className="loading">无持仓</td></tr>
|
||||
) : (
|
||||
[...positions].sort((a, b) => a.coin.localeCompare(b.coin)).map(p => (
|
||||
<tr key={p.coin}>
|
||||
<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>
|
||||
@@ -254,6 +300,10 @@ function PositionsCard({ positions }) {
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{modalOpen && (
|
||||
<TradeDetailModal trade={modalTrade} orders={modalOrders} onClose={closeModal} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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