Phase 1: SQLite persistence layer
- Add modernc.org/sqlite (pure Go, no CGO) - db/ package: trades, orders, config_log tables + CRUD - Trade persistence: every closed trade saved to SQLite - Restart recovery: open positions restored from DB - Automatic migration on startup
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"exchange-monitor/db"
|
||||
"exchange-monitor/exchange"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,7 @@ type Trader struct {
|
||||
bitget *exchange.BitgetTrade
|
||||
hyperliquid *exchange.HyperLiquidTrade
|
||||
|
||||
db *db.DB
|
||||
mu sync.Mutex
|
||||
positions map[string]*ArbPosition // coin -> position
|
||||
lastTradeTime map[string]time.Time
|
||||
@@ -79,20 +81,28 @@ type TradeRecord struct {
|
||||
AmountUSD float64
|
||||
}
|
||||
|
||||
func NewTrader(cfg *Config) *Trader {
|
||||
func NewTrader(cfg *Config, database *db.DB) *Trader {
|
||||
var bt *exchange.BitgetTrade
|
||||
if cfg.BitgetAPIKey != "" {
|
||||
bt = exchange.NewBitgetTrade(cfg.BitgetAPIKey, cfg.BitgetAPISecret, cfg.BitgetPassphrase)
|
||||
}
|
||||
hl, _ := exchange.NewHyperLiquidTrade(cfg.HLPrivateKey, cfg.HLAddress)
|
||||
|
||||
return &Trader{
|
||||
t := &Trader{
|
||||
cfg: cfg,
|
||||
db: database,
|
||||
bitget: bt,
|
||||
hyperliquid: hl,
|
||||
positions: make(map[string]*ArbPosition),
|
||||
lastTradeTime: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
// Restore open positions from DB on restart
|
||||
if database != nil {
|
||||
t.restoreOpenPositions()
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Trader) IsConfigured() bool {
|
||||
@@ -395,6 +405,11 @@ 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 {
|
||||
go t.persistTrade(pos, diffPct, convergenceLabel, exitReason, netPnl, longPnl, shortPnl, totalFees)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(
|
||||
"<b>[平仓]</b> %s/USDT %s\n"+
|
||||
" 持仓: %s 加仓: %d次\n"+
|
||||
@@ -555,3 +570,81 @@ func (t *Trader) GetClosedTrades() []TradeRecord {
|
||||
copy(r, t.closedTrades)
|
||||
return r
|
||||
}
|
||||
|
||||
// persistTrade saves a completed trade to SQLite.
|
||||
func (t *Trader) persistTrade(pos *ArbPosition, exitSpread float64, convergence, exitReason string, netPnl, longPnl, shortPnl, totalFees float64) {
|
||||
var entrySpread, fe float64
|
||||
if pos.LongLeg != nil {
|
||||
entrySpread = pos.EntrySpread
|
||||
}
|
||||
fe = totalFees / 2 // split into entry/exit halves
|
||||
|
||||
now := time.Now()
|
||||
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: &fe,
|
||||
FeeExit: &fe,
|
||||
NetPnl: &netPnl,
|
||||
AmountUSD: pos.AmountUSD,
|
||||
ScaleCount: pos.ScaleLevels,
|
||||
ExitReason: &exitReason,
|
||||
Convergence: &convergence,
|
||||
OpenedAt: pos.StartedAt,
|
||||
ClosedAt: &now,
|
||||
}
|
||||
if _, err := t.db.SaveTrade(dbTrade); err != nil {
|
||||
log.Printf("[Trader] Failed to save trade to DB: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// restoreOpenPositions loads open trades from DB and recreates their positions.
|
||||
func (t *Trader) restoreOpenPositions() {
|
||||
openTrades, err := t.db.GetOpenTrades()
|
||||
if err != nil {
|
||||
log.Printf("[Trader] Failed to load open trades: %v", err)
|
||||
return
|
||||
}
|
||||
for i := range openTrades {
|
||||
tr := &openTrades[i]
|
||||
// Recreate position structure from DB record
|
||||
pos := &ArbPosition{
|
||||
Coin: tr.Coin,
|
||||
Direction: tr.Direction,
|
||||
AmountUSD: tr.AmountUSD,
|
||||
EntrySpread: *tr.EntrySpread,
|
||||
ScaleLevels: tr.ScaleCount,
|
||||
StartedAt: tr.OpenedAt,
|
||||
Status: "open",
|
||||
}
|
||||
if tr.LongEntry != nil {
|
||||
pos.LongLeg = &PositionLeg{
|
||||
Coin: tr.Coin, Exchange: tr.LongExchange, Side: Long,
|
||||
EntryPrice: *tr.LongEntry, EntryTime: tr.OpenedAt,
|
||||
}
|
||||
}
|
||||
if tr.ShortEntry != nil {
|
||||
pos.ShortLeg = &PositionLeg{
|
||||
Coin: tr.Coin, Exchange: tr.ShortExchange, Side: Short,
|
||||
EntryPrice: *tr.ShortEntry, EntryTime: tr.OpenedAt,
|
||||
}
|
||||
}
|
||||
t.positions[tr.Coin] = pos
|
||||
// Prevent immediate re-trading of the same coin
|
||||
t.lastTradeTime[tr.Coin] = tr.OpenedAt
|
||||
}
|
||||
if len(openTrades) > 0 {
|
||||
log.Printf("[Trader] Restored %d open positions from DB", len(openTrades))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user