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:
@@ -1,5 +1,6 @@
|
|||||||
# Binaries
|
# Binaries
|
||||||
exchange-monitor
|
exchange-monitor
|
||||||
|
main
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"test_mode": true,
|
"test_mode": false,
|
||||||
"trade_enabled": true,
|
"trade_enabled": true,
|
||||||
"arb_threshold": 0.03,
|
"arb_threshold": 0.03,
|
||||||
"scan_interval_ms": 200,
|
"scan_interval_ms": 200,
|
||||||
|
|||||||
@@ -240,6 +240,8 @@ func (d *Dashboard) Run() {
|
|||||||
mux.HandleFunc("GET /api/trade/", d.handleTradeDetail)
|
mux.HandleFunc("GET /api/trade/", d.handleTradeDetail)
|
||||||
mux.HandleFunc("GET /api/connections", d.handleConnStatus) // P3-5
|
mux.HandleFunc("GET /api/connections", d.handleConnStatus) // P3-5
|
||||||
mux.HandleFunc("GET /events", d.handleSSE)
|
mux.HandleFunc("GET /events", d.handleSSE)
|
||||||
|
mux.HandleFunc("POST /api/stop", d.handleStop)
|
||||||
|
mux.HandleFunc("POST /api/start", d.handleStart)
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: d.addr,
|
Addr: d.addr,
|
||||||
@@ -479,6 +481,15 @@ func (d *Dashboard) broadcastLoop() {
|
|||||||
d.connMu.RUnlock()
|
d.connMu.RUnlock()
|
||||||
stats["connections"] = connInfo
|
stats["connections"] = connInfo
|
||||||
|
|
||||||
|
// Trading status
|
||||||
|
stats["trading"] = map[string]interface{}{
|
||||||
|
"active": !d.trader.IsShuttingDown(),
|
||||||
|
"mode": d.trader.ModeLabel(),
|
||||||
|
"test": d.trader.cfg.TestMode,
|
||||||
|
"target": d.trader.realTradesTarget,
|
||||||
|
"done": d.trader.realTradesDone,
|
||||||
|
}
|
||||||
|
|
||||||
// Per-exchange fund tracking
|
// Per-exchange fund tracking
|
||||||
exFunds := d.trader.GetExchangeFunds()
|
exFunds := d.trader.GetExchangeFunds()
|
||||||
exFundsMap := make(map[string]map[string]float64, len(exFunds))
|
exFundsMap := make(map[string]map[string]float64, len(exFunds))
|
||||||
@@ -708,6 +719,16 @@ func (d *Dashboard) handleSSE(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dashboard) handleStop(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.trader.Stop()
|
||||||
|
writeJSON(w, map[string]string{"status": "stopped", "message": "Trading stopped, positions closing"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dashboard) handleStart(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.trader.Start()
|
||||||
|
writeJSON(w, map[string]string{"status": "started", "message": "Trading resumed"})
|
||||||
|
}
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, v interface{}) {
|
func writeJSON(w http.ResponseWriter, v interface{}) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(v)
|
json.NewEncoder(w).Encode(v)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func NewHyperLiquidWS(tracked []string) *HyperLiquidWS {
|
|||||||
}
|
}
|
||||||
// Run connects to HyperLiquid WS and streams mid prices.
|
// Run connects to HyperLiquid WS and streams mid prices.
|
||||||
func (h *HyperLiquidWS) Run(updateFn func(coin string, price, bid, ask float64)) error {
|
func (h *HyperLiquidWS) Run(updateFn func(coin string, price, bid, ask float64)) error {
|
||||||
conn := NewPriceConnector("wss://api.hyperliquid.xyz/ws", "HyperLiquid", 120*time.Second, 30*time.Second)
|
conn := NewPriceConnector("wss://api.hyperliquid-testnet.xyz/ws", "HyperLiquid", 120*time.Second, 30*time.Second)
|
||||||
conn.PingInterval = 45 * time.Second
|
conn.PingInterval = 45 * time.Second
|
||||||
|
|
||||||
conn.OnConnect = func() {
|
conn.OnConnect = func() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package exchange
|
package exchange
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -59,13 +60,27 @@ func NewHyperLiquidTrade(privateKeyHex, address string) (*HyperLiquidTrade, erro
|
|||||||
if privateKeyHex == "" {
|
if privateKeyHex == "" {
|
||||||
return &HyperLiquidTrade{client: &http.Client{Timeout: 10 * time.Second}}, nil
|
return &HyperLiquidTrade{client: &http.Client{Timeout: 10 * time.Second}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try hex decode first
|
||||||
keyBytes, err := hex.DecodeString(privateKeyHex)
|
keyBytes, err := hex.DecodeString(privateKeyHex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decode private key: %w", err)
|
// Fallback to base64 decode
|
||||||
|
keyBytes, err = base64.StdEncoding.DecodeString(privateKeyHex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode private key: not valid hex or base64")
|
||||||
}
|
}
|
||||||
if len(keyBytes) != ed25519.PrivateKeySize {
|
|
||||||
return nil, fmt.Errorf("invalid private key length: %d (expected %d)", len(keyBytes), ed25519.PrivateKeySize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept both 32-byte (EVM) and 64-byte (Ed25519) keys
|
||||||
|
// 32-byte EVM keys are padded to 64 bytes for Ed25519
|
||||||
|
if len(keyBytes) == 32 {
|
||||||
|
// Use as Ed25519 seed — append the public key part
|
||||||
|
priv := ed25519.NewKeyFromSeed(keyBytes)
|
||||||
|
keyBytes = []byte(priv)
|
||||||
|
} else if len(keyBytes) != ed25519.PrivateKeySize {
|
||||||
|
return nil, fmt.Errorf("invalid private key length: %d (expected 32 or %d)", len(keyBytes), ed25519.PrivateKeySize)
|
||||||
|
}
|
||||||
|
|
||||||
return &HyperLiquidTrade{
|
return &HyperLiquidTrade{
|
||||||
PrivateKey: ed25519.PrivateKey(keyBytes),
|
PrivateKey: ed25519.PrivateKey(keyBytes),
|
||||||
Address: address,
|
Address: address,
|
||||||
@@ -119,7 +134,7 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro
|
|||||||
|
|
||||||
bodyJSON, _ := json.Marshal(signed)
|
bodyJSON, _ := json.Marshal(signed)
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "https://api.hyperliquid.xyz/exchange", strings.NewReader(string(bodyJSON)))
|
req, err := http.NewRequest("POST", "https://api.hyperliquid-testnet.xyz/exchange", strings.NewReader(string(bodyJSON)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("create request: %w", err)
|
return "", fmt.Errorf("create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ func main() {
|
|||||||
cancel() // B#1: cancel context to stop all WS goroutines
|
cancel() // B#1: cancel context to stop all WS goroutines
|
||||||
runLoop = false
|
runLoop = false
|
||||||
|
|
||||||
|
case <-trader.StopCh:
|
||||||
|
log.Println("[Monitor] 5 real trades completed — trading stopped. System still running (dashboard active)")
|
||||||
|
log.Println("[Monitor] Use POST /api/start to resume trading, POST /api/stop to stop manually")
|
||||||
|
|
||||||
case <-statusTick.C:
|
case <-statusTick.C:
|
||||||
snap := store.GetAll()
|
snap := store.GetAll()
|
||||||
count := 0
|
count := 0
|
||||||
|
|||||||
-165
@@ -24,171 +24,6 @@ var TrackedCoins = []TrackedCoin{
|
|||||||
{Name: "OP", BN: "OPUSDT", BG: "OPUSDT", HL: "OP"},
|
{Name: "OP", BN: "OPUSDT", BG: "OPUSDT", HL: "OP"},
|
||||||
{Name: "WIF", BN: "WIFUSDT", BG: "WIFUSDT", HL: "WIF"},
|
{Name: "WIF", BN: "WIFUSDT", BG: "WIFUSDT", HL: "WIF"},
|
||||||
{Name: "ARB", BN: "ARBUSDT", BG: "ARBUSDT", HL: "ARB"},
|
{Name: "ARB", BN: "ARBUSDT", BG: "ARBUSDT", HL: "ARB"},
|
||||||
{Name: "0G", BN: "", BG: "0GUSDT", HL: "0G"},
|
|
||||||
{Name: "2Z", BN: "", BG: "2ZUSDT", HL: "2Z"},
|
|
||||||
{Name: "AAVE", BN: "", BG: "AAVEUSDT", HL: "AAVE"},
|
|
||||||
{Name: "ACE", BN: "", BG: "ACEUSDT", HL: "ACE"},
|
|
||||||
{Name: "ADA", BN: "", BG: "ADAUSDT", HL: "ADA"},
|
|
||||||
{Name: "AERO", BN: "", BG: "AEROUSDT", HL: "AERO"},
|
|
||||||
{Name: "AIXBT", BN: "", BG: "AIXBTUSDT", HL: "AIXBT"},
|
|
||||||
{Name: "ALGO", BN: "", BG: "ALGOUSDT", HL: "ALGO"},
|
|
||||||
{Name: "ALT", BN: "", BG: "ALTUSDT", HL: "ALT"},
|
|
||||||
{Name: "ANIME", BN: "", BG: "ANIMEUSDT", HL: "ANIME"},
|
|
||||||
{Name: "APE", BN: "", BG: "APEUSDT", HL: "APE"},
|
|
||||||
{Name: "APT", BN: "", BG: "APTUSDT", HL: "APT"},
|
|
||||||
{Name: "AR", BN: "", BG: "ARUSDT", HL: "AR"},
|
|
||||||
{Name: "ARK", BN: "", BG: "ARKUSDT", HL: "ARK"},
|
|
||||||
{Name: "ASTER", BN: "", BG: "ASTERUSDT", HL: "ASTER"},
|
|
||||||
{Name: "ATOM", BN: "", BG: "ATOMUSDT", HL: "ATOM"},
|
|
||||||
{Name: "AVAX", BN: "", BG: "AVAXUSDT", HL: "AVAX"},
|
|
||||||
{Name: "AVNT", BN: "", BG: "AVNTUSDT", HL: "AVNT"},
|
|
||||||
{Name: "AXS", BN: "", BG: "AXSUSDT", HL: "AXS"},
|
|
||||||
{Name: "AZTEC", BN: "", BG: "AZTECUSDT", HL: "AZTEC"},
|
|
||||||
{Name: "BABY", BN: "", BG: "BABYUSDT", HL: "BABY"},
|
|
||||||
{Name: "BANANA", BN: "", BG: "BANANAUSDT", HL: "BANANA"},
|
|
||||||
{Name: "BCH", BN: "", BG: "BCHUSDT", HL: "BCH"},
|
|
||||||
{Name: "BERA", BN: "", BG: "BERAUSDT", HL: "BERA"},
|
|
||||||
{Name: "BIGTIME", BN: "", BG: "BIGTIMEUSDT", HL: "BIGTIME"},
|
|
||||||
{Name: "BIO", BN: "", BG: "BIOUSDT", HL: "BIO"},
|
|
||||||
{Name: "BLUR", BN: "", BG: "BLURUSDT", HL: "BLUR"},
|
|
||||||
{Name: "BNB", BN: "", BG: "BNBUSDT", HL: "BNB"},
|
|
||||||
{Name: "BOME", BN: "", BG: "BOMEUSDT", HL: "BOME"},
|
|
||||||
{Name: "BRETT", BN: "", BG: "BRETTUSDT", HL: "BRETT"},
|
|
||||||
{Name: "BSV", BN: "", BG: "BSVUSDT", HL: "BSV"},
|
|
||||||
{Name: "BTC", BN: "", BG: "BTCUSDT", HL: "BTC"},
|
|
||||||
{Name: "CAKE", BN: "", BG: "CAKEUSDT", HL: "CAKE"},
|
|
||||||
{Name: "CC", BN: "", BG: "CCUSDT", HL: "CC"},
|
|
||||||
{Name: "CELO", BN: "", BG: "CELOUSDT", HL: "CELO"},
|
|
||||||
{Name: "CFX", BN: "", BG: "CFXUSDT", HL: "CFX"},
|
|
||||||
{Name: "CHILLGUY", BN: "", BG: "CHILLGUYUSDT", HL: "CHILLGUY"},
|
|
||||||
{Name: "CHIP", BN: "", BG: "CHIPUSDT", HL: "CHIP"},
|
|
||||||
{Name: "COMP", BN: "", BG: "COMPUSDT", HL: "COMP"},
|
|
||||||
{Name: "CRV", BN: "", BG: "CRVUSDT", HL: "CRV"},
|
|
||||||
{Name: "DASH", BN: "", BG: "DASHUSDT", HL: "DASH"},
|
|
||||||
{Name: "DOOD", BN: "", BG: "DOODUSDT", HL: "DOOD"},
|
|
||||||
{Name: "DOT", BN: "", BG: "DOTUSDT", HL: "DOT"},
|
|
||||||
{Name: "DYDX", BN: "", BG: "DYDXUSDT", HL: "DYDX"},
|
|
||||||
{Name: "DYM", BN: "", BG: "DYMUSDT", HL: "DYM"},
|
|
||||||
{Name: "EIGEN", BN: "", BG: "EIGENUSDT", HL: "EIGEN"},
|
|
||||||
{Name: "ENA", BN: "", BG: "ENAUSDT", HL: "ENA"},
|
|
||||||
{Name: "ENS", BN: "", BG: "ENSUSDT", HL: "ENS"},
|
|
||||||
{Name: "ETC", BN: "", BG: "ETCUSDT", HL: "ETC"},
|
|
||||||
{Name: "ETH", BN: "", BG: "ETHUSDT", HL: "ETH"},
|
|
||||||
{Name: "ETHFI", BN: "", BG: "ETHFIUSDT", HL: "ETHFI"},
|
|
||||||
{Name: "FARTCOIN", BN: "", BG: "FARTCOINUSDT", HL: "FARTCOIN"},
|
|
||||||
{Name: "FET", BN: "", BG: "FETUSDT", HL: "FET"},
|
|
||||||
{Name: "FIL", BN: "", BG: "FILUSDT", HL: "FIL"},
|
|
||||||
{Name: "FOGO", BN: "", BG: "FOGOUSDT", HL: "FOGO"},
|
|
||||||
{Name: "GALA", BN: "", BG: "GALAUSDT", HL: "GALA"},
|
|
||||||
{Name: "GAS", BN: "", BG: "GASUSDT", HL: "GAS"},
|
|
||||||
{Name: "GMT", BN: "", BG: "GMTUSDT", HL: "GMT"},
|
|
||||||
{Name: "GMX", BN: "", BG: "GMXUSDT", HL: "GMX"},
|
|
||||||
{Name: "GOAT", BN: "", BG: "GOATUSDT", HL: "GOAT"},
|
|
||||||
{Name: "GRASS", BN: "", BG: "GRASSUSDT", HL: "GRASS"},
|
|
||||||
{Name: "GRIFFAIN", BN: "", BG: "GRIFFAINUSDT", HL: "GRIFFAIN"},
|
|
||||||
{Name: "HBAR", BN: "", BG: "HBARUSDT", HL: "HBAR"},
|
|
||||||
{Name: "HYPE", BN: "", BG: "HYPEUSDT", HL: "HYPE"},
|
|
||||||
{Name: "HYPER", BN: "", BG: "HYPERUSDT", HL: "HYPER"},
|
|
||||||
{Name: "ICP", BN: "", BG: "ICPUSDT", HL: "ICP"},
|
|
||||||
{Name: "IMX", BN: "", BG: "IMXUSDT", HL: "IMX"},
|
|
||||||
{Name: "INIT", BN: "", BG: "INITUSDT", HL: "INIT"},
|
|
||||||
{Name: "INJ", BN: "", BG: "INJUSDT", HL: "INJ"},
|
|
||||||
{Name: "IO", BN: "", BG: "IOUSDT", HL: "IO"},
|
|
||||||
{Name: "IOTA", BN: "", BG: "IOTAUSDT", HL: "IOTA"},
|
|
||||||
{Name: "IP", BN: "", BG: "IPUSDT", HL: "IP"},
|
|
||||||
{Name: "JTO", BN: "", BG: "JTOUSDT", HL: "JTO"},
|
|
||||||
{Name: "JUP", BN: "", BG: "JUPUSDT", HL: "JUP"},
|
|
||||||
{Name: "KAITO", BN: "", BG: "KAITOUSDT", HL: "KAITO"},
|
|
||||||
{Name: "KAS", BN: "", BG: "KASUSDT", HL: "KAS"},
|
|
||||||
{Name: "LAYER", BN: "", BG: "LAYERUSDT", HL: "LAYER"},
|
|
||||||
{Name: "LDO", BN: "", BG: "LDOUSDT", HL: "LDO"},
|
|
||||||
{Name: "LINEA", BN: "", BG: "LINEAUSDT", HL: "LINEA"},
|
|
||||||
{Name: "LIT", BN: "", BG: "LITUSDT", HL: "LIT"},
|
|
||||||
{Name: "LTC", BN: "", BG: "LTCUSDT", HL: "LTC"},
|
|
||||||
{Name: "MANTA", BN: "", BG: "MANTAUSDT", HL: "MANTA"},
|
|
||||||
{Name: "MAV", BN: "", BG: "MAVUSDT", HL: "MAV"},
|
|
||||||
{Name: "ME", BN: "", BG: "MEUSDT", HL: "ME"},
|
|
||||||
{Name: "MEGA", BN: "", BG: "MEGAUSDT", HL: "MEGA"},
|
|
||||||
{Name: "MELANIA", BN: "", BG: "MELANIAUSDT", HL: "MELANIA"},
|
|
||||||
{Name: "MEME", BN: "", BG: "MEMEUSDT", HL: "MEME"},
|
|
||||||
{Name: "MERL", BN: "", BG: "MERLUSDT", HL: "MERL"},
|
|
||||||
{Name: "MET", BN: "", BG: "METUSDT", HL: "MET"},
|
|
||||||
{Name: "MEW", BN: "", BG: "MEWUSDT", HL: "MEW"},
|
|
||||||
{Name: "MINA", BN: "", BG: "MINAUSDT", HL: "MINA"},
|
|
||||||
{Name: "MON", BN: "", BG: "MONUSDT", HL: "MON"},
|
|
||||||
{Name: "MOODENG", BN: "", BG: "MOODENGUSDT", HL: "MOODENG"},
|
|
||||||
{Name: "MORPHO", BN: "", BG: "MORPHOUSDT", HL: "MORPHO"},
|
|
||||||
{Name: "MOVE", BN: "", BG: "MOVEUSDT", HL: "MOVE"},
|
|
||||||
{Name: "NEAR", BN: "", BG: "NEARUSDT", HL: "NEAR"},
|
|
||||||
{Name: "NEO", BN: "", BG: "NEOUSDT", HL: "NEO"},
|
|
||||||
{Name: "NIL", BN: "", BG: "NILUSDT", HL: "NIL"},
|
|
||||||
{Name: "NOT", BN: "", BG: "NOTUSDT", HL: "NOT"},
|
|
||||||
{Name: "NXPC", BN: "", BG: "NXPCUSDT", HL: "NXPC"},
|
|
||||||
{Name: "ORDI", BN: "", BG: "ORDIUSDT", HL: "ORDI"},
|
|
||||||
{Name: "PAXG", BN: "", BG: "PAXGUSDT", HL: "PAXG"},
|
|
||||||
{Name: "PENDLE", BN: "", BG: "PENDLEUSDT", HL: "PENDLE"},
|
|
||||||
{Name: "PENGU", BN: "", BG: "PENGUUSDT", HL: "PENGU"},
|
|
||||||
{Name: "PEOPLE", BN: "", BG: "PEOPLEUSDT", HL: "PEOPLE"},
|
|
||||||
{Name: "PNUT", BN: "", BG: "PNUTUSDT", HL: "PNUT"},
|
|
||||||
{Name: "POL", BN: "", BG: "POLUSDT", HL: "POL"},
|
|
||||||
{Name: "POLYX", BN: "", BG: "POLYXUSDT", HL: "POLYX"},
|
|
||||||
{Name: "POPCAT", BN: "", BG: "POPCATUSDT", HL: "POPCAT"},
|
|
||||||
{Name: "PROVE", BN: "", BG: "PROVEUSDT", HL: "PROVE"},
|
|
||||||
{Name: "PUMP", BN: "", BG: "PUMPUSDT", HL: "PUMP"},
|
|
||||||
{Name: "PYTH", BN: "", BG: "PYTHUSDT", HL: "PYTH"},
|
|
||||||
{Name: "RENDER", BN: "", BG: "RENDERUSDT", HL: "RENDER"},
|
|
||||||
{Name: "RESOLV", BN: "", BG: "RESOLVUSDT", HL: "RESOLV"},
|
|
||||||
{Name: "REZ", BN: "", BG: "REZUSDT", HL: "REZ"},
|
|
||||||
{Name: "RSR", BN: "", BG: "RSRUSDT", HL: "RSR"},
|
|
||||||
{Name: "RUNE", BN: "", BG: "RUNEUSDT", HL: "RUNE"},
|
|
||||||
{Name: "S", BN: "", BG: "SUSDT", HL: "S"},
|
|
||||||
{Name: "SAGA", BN: "", BG: "SAGAUSDT", HL: "SAGA"},
|
|
||||||
{Name: "SAND", BN: "", BG: "SANDUSDT", HL: "SAND"},
|
|
||||||
{Name: "SEI", BN: "", BG: "SEIUSDT", HL: "SEI"},
|
|
||||||
{Name: "SKR", BN: "", BG: "SKRUSDT", HL: "SKR"},
|
|
||||||
{Name: "SKY", BN: "", BG: "SKYUSDT", HL: "SKY"},
|
|
||||||
{Name: "SNX", BN: "", BG: "SNXUSDT", HL: "SNX"},
|
|
||||||
{Name: "SOL", BN: "", BG: "SOLUSDT", HL: "SOL"},
|
|
||||||
{Name: "SOPH", BN: "", BG: "SOPHUSDT", HL: "SOPH"},
|
|
||||||
{Name: "SPX", BN: "", BG: "SPXUSDT", HL: "SPX"},
|
|
||||||
{Name: "STABLE", BN: "", BG: "STABLEUSDT", HL: "STABLE"},
|
|
||||||
{Name: "STBL", BN: "", BG: "STBLUSDT", HL: "STBL"},
|
|
||||||
{Name: "STRK", BN: "", BG: "STRKUSDT", HL: "STRK"},
|
|
||||||
{Name: "STX", BN: "", BG: "STXUSDT", HL: "STX"},
|
|
||||||
{Name: "SUI", BN: "", BG: "SUIUSDT", HL: "SUI"},
|
|
||||||
{Name: "SUPER", BN: "", BG: "SUPERUSDT", HL: "SUPER"},
|
|
||||||
{Name: "SUSHI", BN: "", BG: "SUSHIUSDT", HL: "SUSHI"},
|
|
||||||
{Name: "SYRUP", BN: "", BG: "SYRUPUSDT", HL: "SYRUP"},
|
|
||||||
{Name: "TAO", BN: "", BG: "TAOUSDT", HL: "TAO"},
|
|
||||||
{Name: "TIA", BN: "", BG: "TIAUSDT", HL: "TIA"},
|
|
||||||
{Name: "TNSR", BN: "", BG: "TNSRUSDT", HL: "TNSR"},
|
|
||||||
{Name: "TON", BN: "", BG: "TONUSDT", HL: "TON"},
|
|
||||||
{Name: "TRB", BN: "", BG: "TRBUSDT", HL: "TRB"},
|
|
||||||
{Name: "TRUMP", BN: "", BG: "TRUMPUSDT", HL: "TRUMP"},
|
|
||||||
{Name: "TRX", BN: "", BG: "TRXUSDT", HL: "TRX"},
|
|
||||||
{Name: "TURBO", BN: "", BG: "TURBOUSDT", HL: "TURBO"},
|
|
||||||
{Name: "UMA", BN: "", BG: "UMAUSDT", HL: "UMA"},
|
|
||||||
{Name: "UNI", BN: "", BG: "UNIUSDT", HL: "UNI"},
|
|
||||||
{Name: "USTC", BN: "", BG: "USTCUSDT", HL: "USTC"},
|
|
||||||
{Name: "USUAL", BN: "", BG: "USUALUSDT", HL: "USUAL"},
|
|
||||||
{Name: "VIRTUAL", BN: "", BG: "VIRTUALUSDT", HL: "VIRTUAL"},
|
|
||||||
{Name: "VVV", BN: "", BG: "VVVUSDT", HL: "VVV"},
|
|
||||||
{Name: "W", BN: "", BG: "WUSDT", HL: "W"},
|
|
||||||
{Name: "WCT", BN: "", BG: "WCTUSDT", HL: "WCT"},
|
|
||||||
{Name: "WLD", BN: "", BG: "WLDUSDT", HL: "WLD"},
|
|
||||||
{Name: "WLFI", BN: "", BG: "WLFIUSDT", HL: "WLFI"},
|
|
||||||
{Name: "XAI", BN: "", BG: "XAIUSDT", HL: "XAI"},
|
|
||||||
{Name: "XLM", BN: "", BG: "XLMUSDT", HL: "XLM"},
|
|
||||||
{Name: "XMR", BN: "", BG: "XMRUSDT", HL: "XMR"},
|
|
||||||
{Name: "XPL", BN: "", BG: "XPLUSDT", HL: "XPL"},
|
|
||||||
{Name: "XRP", BN: "", BG: "XRPUSDT", HL: "XRP"},
|
|
||||||
{Name: "YGG", BN: "", BG: "YGGUSDT", HL: "YGG"},
|
|
||||||
{Name: "ZEC", BN: "", BG: "ZECUSDT", HL: "ZEC"},
|
|
||||||
{Name: "ZEN", BN: "", BG: "ZENUSDT", HL: "ZEN"},
|
|
||||||
{Name: "ZETA", BN: "", BG: "ZETAUSDT", HL: "ZETA"},
|
|
||||||
{Name: "ZK", BN: "", BG: "ZKUSDT", HL: "ZK"},
|
|
||||||
{Name: "ZORA", BN: "", BG: "ZORAUSDT", HL: "ZORA"},
|
|
||||||
{Name: "ZRO", BN: "", BG: "ZROUSDT", HL: "ZRO"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// netProfit calculates net profit % after fees for a complete round trip (entry + exit).
|
// netProfit calculates net profit % after fees for a complete round trip (entry + exit).
|
||||||
|
|||||||
@@ -150,6 +150,12 @@ type Trader struct {
|
|||||||
// Decoupled snapshot for display — snapMu never contended by trading path
|
// Decoupled snapshot for display — snapMu never contended by trading path
|
||||||
snapMu sync.RWMutex
|
snapMu sync.RWMutex
|
||||||
positionsSnapshot []ArbPosition
|
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.
|
// 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),
|
entering: make(map[string]bool),
|
||||||
lastTradeTime: make(map[string]time.Time),
|
lastTradeTime: make(map[string]time.Time),
|
||||||
blacklist: make(map[string]time.Time),
|
blacklist: make(map[string]time.Time),
|
||||||
|
StopCh: make(chan struct{}, 1),
|
||||||
|
realTradesTarget: 5,
|
||||||
exchangeFunds: map[string]*ExchangeFund{
|
exchangeFunds: map[string]*ExchangeFund{
|
||||||
ExBitget: {Balance: cfg.InitialCapital / 2},
|
ExBitget: {Balance: cfg.InitialCapital / 2},
|
||||||
ExHyperLiquid: {Balance: cfg.InitialCapital / 2},
|
ExHyperLiquid: {Balance: cfg.InitialCapital / 2},
|
||||||
@@ -260,6 +268,50 @@ func (t *Trader) ModeLabel() string {
|
|||||||
return "LIVE"
|
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.
|
// Tick is called every scanner cycle — checks scaling and exit.
|
||||||
func (t *Trader) Tick(store *PriceStore, notifier *Notifier) {
|
func (t *Trader) Tick(store *PriceStore, notifier *Notifier) {
|
||||||
if !t.IsConfigured() {
|
if !t.IsConfigured() {
|
||||||
@@ -272,6 +324,26 @@ func (t *Trader) Tick(store *PriceStore, notifier *Notifier) {
|
|||||||
for _, pos := range t.positions {
|
for _, pos := range t.positions {
|
||||||
positions = append(positions, pos)
|
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()
|
t.mu.Unlock()
|
||||||
|
|
||||||
for _, pos := range positions {
|
for _, pos := range positions {
|
||||||
@@ -331,6 +403,10 @@ func (t *Trader) TryEntry(opp *ArbOpportunity, store *PriceStore, notifier *Noti
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
|
if t.shuttingDown {
|
||||||
|
t.mu.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
if _, exists := t.positions[opp.Coin]; exists {
|
if _, exists := t.positions[opp.Coin]; exists {
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
return false
|
return false
|
||||||
@@ -828,8 +904,19 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
|||||||
delete(t.positions, pos.Coin)
|
delete(t.positions, pos.Coin)
|
||||||
t.lastTradeTime[pos.Coin] = time.Now()
|
t.lastTradeTime[pos.Coin] = time.Now()
|
||||||
t.closedTrades = append(t.closedTrades, record)
|
t.closedTrades = append(t.closedTrades, record)
|
||||||
|
t.realTradesDone++
|
||||||
t.mu.Unlock()
|
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
|
// Persist exit orders + close trade in DB
|
||||||
if t.db != nil && pos.DBTradeID > 0 {
|
if t.db != nil && pos.DBTradeID > 0 {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -932,6 +1019,7 @@ func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) st
|
|||||||
}
|
}
|
||||||
leg.Size = size
|
leg.Size = size
|
||||||
leg.OrderID = oid
|
leg.OrderID = oid
|
||||||
|
log.Printf("[ExRes] BG %s %s: size=%s → response=%s", side, leg.Coin+"USDT", size, oid)
|
||||||
} else {
|
} else {
|
||||||
size := exchange.GetHLSize(leg.Coin, t.cfg.TradeAmountUSD, leg.EntryPrice)
|
size := exchange.GetHLSize(leg.Coin, t.cfg.TradeAmountUSD, leg.EntryPrice)
|
||||||
resp, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, size)
|
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.Size = size
|
||||||
leg.OrderID = resp
|
leg.OrderID = resp
|
||||||
|
log.Printf("[ExRes] HL %s %s: size=%s → response=%s", side, leg.Coin, size, resp)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -974,15 +1063,19 @@ func (t *Trader) closeLeg(leg *PositionLeg) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
if leg.Exchange == ExBitget {
|
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)
|
||||||
} else {
|
|
||||||
_, err = t.hyperliquid.PlaceMarketOrder(leg.Coin, side, leg.Size)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("%v", err)
|
return fmt.Sprintf("%v", err)
|
||||||
}
|
}
|
||||||
|
log.Printf("[ExRes] BG close %s %s: size=%s → response=%s", side, leg.Coin+"USDT", leg.Size, resp)
|
||||||
|
} else {
|
||||||
|
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.Closed = true
|
||||||
leg.ExitTime = time.Now()
|
leg.ExitTime = time.Now()
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user