feat: real trading mode, auto-stop after 5 trades, HL testnet support
- config.json: test_mode=false, ready for sim/testnet trading - trader.go: auto-stop after 5 real trades, exchange response logging, Stop()/Start() API, shuttingDown flag for graceful stop - dashboard.go: POST /api/stop + POST /api/start endpoints, trading status in SSE stats - exchange/hyperliquid.go: switch HL WS to testnet endpoint - exchange/hyperliquid_trade.go: switch REST to testnet endpoint, support base64 + 32-byte EVM private keys - main.go: listen on trader.StopCh (graceful, no process exit) - scanner.go: trim TrackedCoins to only 6 core coins (DOGE/LINK/ONDO/OP/WIF/ARB) - .gitignore: ignore main binary
This commit is contained in:
@@ -150,6 +150,12 @@ type Trader struct {
|
||||
// Decoupled snapshot for display — snapMu never contended by trading path
|
||||
snapMu sync.RWMutex
|
||||
positionsSnapshot []ArbPosition
|
||||
|
||||
// Auto-stop after N real trades
|
||||
StopCh chan struct{}
|
||||
realTradesTarget int
|
||||
realTradesDone int
|
||||
shuttingDown bool
|
||||
}
|
||||
|
||||
// TradeRecord stores a finalized trade for stats tracking.
|
||||
@@ -196,6 +202,8 @@ 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),
|
||||
StopCh: make(chan struct{}, 1),
|
||||
realTradesTarget: 5,
|
||||
exchangeFunds: map[string]*ExchangeFund{
|
||||
ExBitget: {Balance: cfg.InitialCapital / 2},
|
||||
ExHyperLiquid: {Balance: cfg.InitialCapital / 2},
|
||||
@@ -260,6 +268,50 @@ func (t *Trader) ModeLabel() string {
|
||||
return "LIVE"
|
||||
}
|
||||
|
||||
// IsShuttingDown returns whether trading is stopped.
|
||||
func (t *Trader) IsShuttingDown() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.shuttingDown
|
||||
}
|
||||
|
||||
// Stop sets shuttingDown flag and force-closes all open positions.
|
||||
func (t *Trader) Stop() {
|
||||
t.mu.Lock()
|
||||
t.shuttingDown = true
|
||||
t.mu.Unlock()
|
||||
log.Println("[Trader] ⏹ Trading STOPPED — no new entries, closing positions...")
|
||||
|
||||
// Force-close all open positions immediately
|
||||
t.mu.Lock()
|
||||
positions := make([]*ArbPosition, 0, len(t.positions))
|
||||
for _, pos := range t.positions {
|
||||
positions = append(positions, pos)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos.Status == "open" || pos.Status == "close_failed" {
|
||||
t.closeBothLegs(pos)
|
||||
pos.Status = "closed"
|
||||
pos.ExitedAt = time.Now()
|
||||
t.mu.Lock()
|
||||
delete(t.positions, pos.Coin)
|
||||
t.mu.Unlock()
|
||||
log.Printf("[Trader] ⏹ Force-closed %s %s (manual stop)", pos.Coin, pos.Direction)
|
||||
}
|
||||
}
|
||||
log.Println("[Trader] ✅ All positions closed, trading stopped. POST /api/start to resume.")
|
||||
}
|
||||
|
||||
// Start clears shuttingDown flag and resumes trading.
|
||||
func (t *Trader) Start() {
|
||||
t.mu.Lock()
|
||||
t.shuttingDown = false
|
||||
t.mu.Unlock()
|
||||
log.Println("[Trader] ▶ Trading RESUMED")
|
||||
}
|
||||
|
||||
// Tick is called every scanner cycle — checks scaling and exit.
|
||||
func (t *Trader) Tick(store *PriceStore, notifier *Notifier) {
|
||||
if !t.IsConfigured() {
|
||||
@@ -272,6 +324,26 @@ func (t *Trader) Tick(store *PriceStore, notifier *Notifier) {
|
||||
for _, pos := range t.positions {
|
||||
positions = append(positions, pos)
|
||||
}
|
||||
|
||||
// Force-close remaining positions when shutting down
|
||||
if t.shuttingDown && len(positions) > 0 {
|
||||
t.mu.Unlock()
|
||||
for _, pos := range positions {
|
||||
if pos.Status == "open" || pos.Status == "close_failed" {
|
||||
t.closeBothLegs(pos)
|
||||
pos.Status = "closed"
|
||||
pos.ExitedAt = time.Now()
|
||||
delete(t.positions, pos.Coin)
|
||||
log.Printf("[Trader] ⏹ Force-closed %s %s (shutdown)", pos.Coin, pos.Direction)
|
||||
}
|
||||
}
|
||||
// All force-closed — signal stop
|
||||
select {
|
||||
case t.StopCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
for _, pos := range positions {
|
||||
@@ -331,6 +403,10 @@ func (t *Trader) TryEntry(opp *ArbOpportunity, store *PriceStore, notifier *Noti
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
if t.shuttingDown {
|
||||
t.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
if _, exists := t.positions[opp.Coin]; exists {
|
||||
t.mu.Unlock()
|
||||
return false
|
||||
@@ -828,8 +904,19 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
delete(t.positions, pos.Coin)
|
||||
t.lastTradeTime[pos.Coin] = time.Now()
|
||||
t.closedTrades = append(t.closedTrades, record)
|
||||
t.realTradesDone++
|
||||
t.mu.Unlock()
|
||||
|
||||
// Auto-stop: after 5 real trades, signal shutdown
|
||||
if t.realTradesTarget > 0 && t.realTradesDone >= t.realTradesTarget {
|
||||
log.Printf("[Trader] ✅ %d real trades completed — shutting down...", t.realTradesDone)
|
||||
t.shuttingDown = true
|
||||
select {
|
||||
case t.StopCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Persist exit orders + close trade in DB
|
||||
if t.db != nil && pos.DBTradeID > 0 {
|
||||
now := time.Now()
|
||||
@@ -932,6 +1019,7 @@ func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) st
|
||||
}
|
||||
leg.Size = size
|
||||
leg.OrderID = oid
|
||||
log.Printf("[ExRes] BG %s %s: size=%s → response=%s", side, leg.Coin+"USDT", size, oid)
|
||||
} else {
|
||||
size := exchange.GetHLSize(leg.Coin, t.cfg.TradeAmountUSD, leg.EntryPrice)
|
||||
resp, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, size)
|
||||
@@ -940,6 +1028,7 @@ func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) st
|
||||
}
|
||||
leg.Size = size
|
||||
leg.OrderID = resp
|
||||
log.Printf("[ExRes] HL %s %s: size=%s → response=%s", side, leg.Coin, size, resp)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -974,14 +1063,18 @@ func (t *Trader) closeLeg(leg *PositionLeg) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
var err error
|
||||
if leg.Exchange == ExBitget {
|
||||
_, err = t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", leg.Size)
|
||||
resp, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", leg.Size)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", err)
|
||||
}
|
||||
log.Printf("[ExRes] BG close %s %s: size=%s → response=%s", side, leg.Coin+"USDT", leg.Size, resp)
|
||||
} else {
|
||||
_, err = t.hyperliquid.PlaceMarketOrder(leg.Coin, side, leg.Size)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", err)
|
||||
resp, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, leg.Size)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", err)
|
||||
}
|
||||
log.Printf("[ExRes] HL close %s %s: size=%s → response=%s", side, leg.Coin, leg.Size, resp)
|
||||
}
|
||||
leg.Closed = true
|
||||
leg.ExitTime = time.Now()
|
||||
|
||||
Reference in New Issue
Block a user