refactor: 逐批次实时写入DB,删除persistTrade

This commit is contained in:
jackyu66git
2026-05-04 05:29:04 +08:00
parent ec725ed11a
commit a9a21de5a2
2 changed files with 219 additions and 205 deletions
+4 -4
View File
@@ -85,13 +85,13 @@ func (d *DB) SaveTrade(t *TradeRecord) (int64, error) {
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=?, net_pnl=?,
scale_count=?, exit_reason=?, convergence=?, closed_at=?
long_pnl=?, short_pnl=?, fee_entry=?, fee_exit=?, net_pnl=?,
amount_usd=?, scale_count=?, exit_reason=?, convergence=?, closed_at=?
WHERE id=?`,
t.Status, t.ExitSpread,
t.LongExit, t.ShortExit,
t.LongPnl, t.ShortPnl, t.NetPnl,
t.ScaleCount, t.ExitReason, t.Convergence, t.ClosedAt,
t.LongPnl, t.ShortPnl, t.FeeEntry, t.FeeExit, t.NetPnl,
t.AmountUSD, t.ScaleCount, t.ExitReason, t.Convergence, t.ClosedAt,
id,
)
return err
+215 -201
View File
@@ -63,6 +63,9 @@ type ArbPosition struct {
// Track all entry prices for weighted-average PnL across scale-ins (Issue #2)
LongEntryPrices []float64 // all long entry prices (initial + scale-ins)
ShortEntryPrices []float64 // all short entry prices (initial + scale-ins)
// DB trade ID — set after first save, used for incremental order/scale/exit persists
DBTradeID int64
}
// DeepCopy returns a copy-safe snapshot of the position (no shared pointers).
@@ -410,6 +413,55 @@ func (t *Trader) executeEntry(opp *ArbOpportunity, store *PriceStore, notifier *
pos.LastScaleAt = time.Now()
pos.Status = "open" // both legs placed, ready for Tick/exit logic
// Persist entry to DB immediately (incremental — not batch at close)
if t.db != nil {
now := time.Now()
status := "filled"
tradeUnit := t.cfg.TradeAmountUSD
es := pos.EntrySpread
dbTrade := &db.TradeRecord{
Coin: pos.Coin,
Direction: pos.Direction,
Status: "open",
EntrySpread: &es,
LongExchange: pos.LongLeg.Exchange,
ShortExchange: pos.ShortLeg.Exchange,
LongEntry: &pos.LongLeg.EntryPrice,
ShortEntry: &pos.ShortLeg.EntryPrice,
AmountUSD: t.cfg.TradeAmountUSD,
OpenedAt: now,
}
if tradeID, err := t.db.SaveTrade(dbTrade); err == nil {
pos.DBTradeID = tradeID
longFee := tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
shortFee := tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
longShares := tradeUnit / pos.LongLeg.EntryPrice
shortShares := tradeUnit / pos.ShortLeg.EntryPrice
longOID, _ := t.db.SaveOrder(&db.OrderRecord{
TradeID: tradeID, Leg: "long", Type: "entry",
Exchange: pos.LongLeg.Exchange, Side: "buy",
Price: &pos.LongLeg.EntryPrice, Size: &longShares,
Fee: &longFee, Status: &status, CreatedAt: now,
})
shortOID, _ := t.db.SaveOrder(&db.OrderRecord{
TradeID: tradeID, Leg: "short", Type: "entry",
Exchange: pos.ShortLeg.Exchange, Side: "sell",
Price: &pos.ShortLeg.EntryPrice, Size: &shortShares,
Fee: &shortFee, Status: &status, CreatedAt: now,
})
t.db.SaveSystemOrder(&db.SystemOrderRecord{
TradeID: tradeID, Type: "entry", Status: "filled",
Spread: &es,
LongPrice: &pos.LongLeg.EntryPrice, ShortPrice: &pos.ShortLeg.EntryPrice,
LongOrderID: &longOID, ShortOrderID: &shortOID,
CreatedAt: now,
})
}
}
log.Printf("[Trader] %s: Opened %s | Long %s @ %.2f Short %s @ %.2f | $%.0f",
pos.Coin, pos.Direction, pos.LongLeg.Exchange, pos.LongLeg.EntryPrice,
pos.ShortLeg.Exchange, pos.ShortLeg.EntryPrice, t.cfg.TradeAmountUSD)
@@ -495,6 +547,39 @@ func (t *Trader) checkScaleIn(pos *ArbPosition, bgP, hlP, diffPct float64, store
pos.LongEntryPrices = append(pos.LongEntryPrices, longPrice)
pos.ShortEntryPrices = append(pos.ShortEntryPrices, shortPrice)
// Persist scale orders to DB immediately
if t.db != nil && pos.DBTradeID > 0 {
now := time.Now()
status := "filled"
tradeUnit := t.cfg.TradeAmountUSD
es := pos.EntrySpread
longFee := tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
shortFee := tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
longShares := tradeUnit / longPrice
shortShares := tradeUnit / shortPrice
longOID, _ := t.db.SaveOrder(&db.OrderRecord{
TradeID: pos.DBTradeID, Leg: "long", Type: "scale",
Exchange: pos.LongLeg.Exchange, Side: "buy",
Price: &longPrice, Size: &longShares,
Fee: &longFee, Status: &status, CreatedAt: now,
})
shortOID, _ := t.db.SaveOrder(&db.OrderRecord{
TradeID: pos.DBTradeID, Leg: "short", Type: "scale",
Exchange: pos.ShortLeg.Exchange, Side: "sell",
Price: &shortPrice, Size: &shortShares,
Fee: &shortFee, Status: &status, CreatedAt: now,
})
t.db.SaveSystemOrder(&db.SystemOrderRecord{
TradeID: pos.DBTradeID, Type: "scale", Status: "filled",
Spread: &es,
LongPrice: &longPrice, ShortPrice: &shortPrice,
LongOrderID: &longOID, ShortOrderID: &shortOID,
CreatedAt: now,
})
}
log.Printf("[Trader] %s: Scale-in #%d executed | spread=%.4f%% (entry=%.4f%%) | total=$%.0f",
pos.Coin, pos.ScaleLevels, diffPct, entryDiff, pos.AmountUSD)
}
@@ -617,9 +702,73 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
t.closedTrades = append(t.closedTrades, record)
t.mu.Unlock()
// Persist to SQLite
if t.db != nil {
t.persistTrade(pos, diffPct, convergenceLabel, exitReason, netPnl, longPnl, shortPnl, totalFees)
// Persist exit orders + close trade in DB
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,
})
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,
})
// Save exit system order
t.db.SaveSystemOrder(&db.SystemOrderRecord{
TradeID: pos.DBTradeID, Type: "exit", Status: "filled",
Spread: &diffPct,
LongPrice: &pos.LongLeg.ExitPrice, ShortPrice: &pos.ShortLeg.ExitPrice,
LongOrderID: &longOID, ShortOrderID: &shortOID,
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
t.db.UpdateTradeStatus(pos.DBTradeID, &db.TradeRecord{
Status: "closed",
ExitSpread: &diffPct,
LongExit: &pos.LongLeg.ExitPrice,
ShortExit: &pos.ShortLeg.ExitPrice,
LongPnl: &longPnl,
ShortPnl: &shortPnl,
FeeEntry: &feeEntrySum,
FeeExit: &feeExitSum,
NetPnl: &netPnl,
AmountUSD: pos.AmountUSD,
ScaleCount: pos.ScaleLevels,
ExitReason: &exitReason,
Convergence: &convergenceLabel,
ClosedAt: &now,
})
}
msg := fmt.Sprintf(
@@ -732,7 +881,7 @@ func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifi
closeErr := t.closeBothLegs(pos)
if closeErr == "" {
// All legs finally closed — record + persist
// All legs finally closed — record + update DB
pos.Status = "closed"
pos.ExitedAt = time.Now()
@@ -758,9 +907,68 @@ func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifi
t.closedTrades = append(t.closedTrades, record)
t.mu.Unlock()
if t.db != nil {
t.persistTrade(pos, pos.ExitDiffPct, pos.ExitConvergence, pos.ExitReasonText,
pos.ExitNetPnl, pos.ExitLongPnl, pos.ExitShortPnl, pos.ExitTotalFees)
// Persist exit orders + close trade in DB (only for legs that weren't already closed)
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 for legs that were just now closed
if pos.LongLeg.Closed {
longExitFee := totalLongShares * pos.LongLeg.ExitPrice * takerFees[pos.LongLeg.Exchange] / 100
longExitShares := totalLongShares
_, _ = 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,
})
}
if pos.ShortLeg.Closed {
shortExitFee := totalShortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
shortExitShares := totalShortShares
_, _ = 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,
})
}
// Save exit system order (idempotent-safe since we always overwrite on retry)
t.db.SaveSystemOrder(&db.SystemOrderRecord{
TradeID: pos.DBTradeID, Type: "exit", Status: "filled",
Spread: &pos.ExitDiffPct,
LongPrice: &pos.LongLeg.ExitPrice, ShortPrice: &pos.ShortLeg.ExitPrice,
CreatedAt: now,
})
// Close trade using previously saved exit metadata
feePct := pos.ExitTotalFees
t.db.UpdateTradeStatus(pos.DBTradeID, &db.TradeRecord{
Status: "closed",
ExitSpread: &pos.ExitDiffPct,
LongExit: &pos.LongLeg.ExitPrice,
ShortExit: &pos.ShortLeg.ExitPrice,
LongPnl: &pos.ExitLongPnl,
ShortPnl: &pos.ExitShortPnl,
FeeEntry: &feePct,
FeeExit: &feePct,
NetPnl: &pos.ExitNetPnl,
AmountUSD: pos.AmountUSD,
ScaleCount: pos.ScaleLevels,
ExitReason: &pos.ExitReasonText,
Convergence: &pos.ExitConvergence,
ClosedAt: &now,
})
}
notifier.Send(fmt.Sprintf(
@@ -926,200 +1134,6 @@ func (t *Trader) GetClosedTrades() []TradeRecord {
}
// persistTrade saves a completed trade to SQLite, with per-leg orders and system_orders.
func (t *Trader) persistTrade(pos *ArbPosition, exitSpread float64, convergence, exitReason string, netPnl, longPnl, shortPnl, totalFees float64) {
var entrySpread float64
if pos.LongLeg != nil {
entrySpread = pos.EntrySpread
}
now := time.Now()
tradeUnit := t.cfg.TradeAmountUSD
// Pre-calculate all fees BEFORE saving the trade
totalFeeEntryUSD := 0.0
for range pos.LongEntryPrices {
totalFeeEntryUSD += tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
}
for range pos.ShortEntryPrices {
totalFeeEntryUSD += tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
}
totalLongShares := 0.0
for _, p := range pos.LongEntryPrices {
totalLongShares += tradeUnit / p
}
totalShortShares := 0.0
for _, p := range pos.ShortEntryPrices {
totalShortShares += tradeUnit / p
}
totalFeeExitUSD := totalLongShares*pos.LongLeg.ExitPrice*takerFees[pos.LongLeg.Exchange]/100 +
totalShortShares*pos.ShortLeg.ExitPrice*takerFees[pos.ShortLeg.Exchange]/100
dbTrade := &db.TradeRecord{
Coin: pos.Coin,
Direction: pos.Direction,
Status: "closed",
EntrySpread: &entrySpread,
ExitSpread: &exitSpread,
LongExchange: pos.LongLeg.Exchange,
ShortExchange: pos.ShortLeg.Exchange,
LongEntry: &pos.LongLeg.EntryPrice,
LongExit: &pos.LongLeg.ExitPrice,
ShortEntry: &pos.ShortLeg.EntryPrice,
ShortExit: &pos.ShortLeg.ExitPrice,
LongPnl: &longPnl,
ShortPnl: &shortPnl,
FeeEntry: &totalFeeEntryUSD,
FeeExit: &totalFeeExitUSD,
NetPnl: &netPnl,
AmountUSD: pos.AmountUSD,
ScaleCount: pos.ScaleLevels,
ExitReason: &exitReason,
Convergence: &convergence,
OpenedAt: pos.StartedAt,
ClosedAt: &now,
}
tradeID, err := t.db.SaveTrade(dbTrade)
if err != nil {
log.Printf("[Trader] Failed to save trade to DB: %v", err)
return
}
// Save per-leg order records
// Long leg: entry (buy), scales (buy), exit (sell)
status := "filled"
var longEntryOrderIDs []int64
for i, p := range pos.LongEntryPrices {
shares := tradeUnit / p
orderType := "entry"
if i > 0 {
orderType = "scale"
}
fee := tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
oid, oErr := t.db.SaveOrder(&db.OrderRecord{
TradeID: tradeID,
Leg: "long",
Type: orderType,
Exchange: pos.LongLeg.Exchange,
Side: "buy",
Price: &p,
Size: &shares,
Fee: &fee,
Status: &status,
CreatedAt: pos.StartedAt,
})
if oErr != nil {
log.Printf("[Trader] Failed to save long entry order: %v", oErr)
} else {
longEntryOrderIDs = append(longEntryOrderIDs, oid)
}
}
// Long exit (sell)
longExitShares := totalLongShares
longExitFee := totalLongShares * pos.LongLeg.ExitPrice * takerFees[pos.LongLeg.Exchange] / 100
status = "filled"
var longExitOrderID int64
if oid, oErr := t.db.SaveOrder(&db.OrderRecord{
TradeID: tradeID,
Leg: "long",
Type: "exit",
Exchange: pos.LongLeg.Exchange,
Side: "sell",
Price: &pos.LongLeg.ExitPrice,
Size: &longExitShares,
Fee: &longExitFee,
Status: &status,
CreatedAt: now,
}); oErr != nil {
log.Printf("[Trader] Failed to save long exit order: %v", oErr)
} else {
longExitOrderID = oid
}
// Short leg: entry (sell), scales (sell), exit (buy)
var shortEntryOrderIDs []int64
for i, p := range pos.ShortEntryPrices {
shares := tradeUnit / p
orderType := "entry"
if i > 0 {
orderType = "scale"
}
fee := tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
oid, oErr := t.db.SaveOrder(&db.OrderRecord{
TradeID: tradeID,
Leg: "short",
Type: orderType,
Exchange: pos.ShortLeg.Exchange,
Side: "sell",
Price: &p,
Size: &shares,
Fee: &fee,
Status: &status,
CreatedAt: pos.StartedAt,
})
if oErr != nil {
log.Printf("[Trader] Failed to save short entry order: %v", oErr)
} else {
shortEntryOrderIDs = append(shortEntryOrderIDs, oid)
}
}
// Short exit (buy)
shortExitShares := totalShortShares
shortExitFee := totalShortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
var shortExitOrderID int64
if oid, oErr := t.db.SaveOrder(&db.OrderRecord{
TradeID: tradeID,
Leg: "short",
Type: "exit",
Exchange: pos.ShortLeg.Exchange,
Side: "buy",
Price: &pos.ShortLeg.ExitPrice,
Size: &shortExitShares,
Fee: &shortExitFee,
Status: &status,
CreatedAt: now,
}); oErr != nil {
log.Printf("[Trader] Failed to save short exit order: %v", oErr)
} else {
shortExitOrderID = oid
}
// Save system orders linking long+short legs
es := pos.EntrySpread
for i := 0; i < len(longEntryOrderIDs) && i < len(shortEntryOrderIDs); i++ {
sysType := "entry"
if i > 0 {
sysType = "scale"
}
if _, sErr := t.db.SaveSystemOrder(&db.SystemOrderRecord{
TradeID: tradeID,
Type: sysType,
Status: "filled",
Spread: &es,
LongPrice: &pos.LongEntryPrices[i],
ShortPrice: &pos.ShortEntryPrices[i],
LongOrderID: &longEntryOrderIDs[i],
ShortOrderID: &shortEntryOrderIDs[i],
CreatedAt: pos.StartedAt,
}); sErr != nil {
log.Printf("[Trader] Failed to save entry system order: %v", sErr)
}
}
// Exit system order
if _, sErr := t.db.SaveSystemOrder(&db.SystemOrderRecord{
TradeID: tradeID,
Type: "exit",
Status: "filled",
Spread: &exitSpread,
LongPrice: &pos.LongLeg.ExitPrice,
ShortPrice: &pos.ShortLeg.ExitPrice,
LongOrderID: &longExitOrderID,
ShortOrderID: &shortExitOrderID,
CreatedAt: now,
}); sErr != nil {
log.Printf("[Trader] Failed to save exit system order: %v", sErr)
}
}
// restoreOpenPositions loads open trades from DB and recreates their positions.
func (t *Trader) restoreOpenPositions() {
openTrades, err := t.db.GetOpenTrades()