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:
jackyu66git
2026-05-04 14:31:52 +08:00
parent 5fa3e1bac8
commit e5ea78ffd2
8 changed files with 146 additions and 177 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ func NewHyperLiquidWS(tracked []string) *HyperLiquidWS {
}
// Run connects to HyperLiquid WS and streams mid prices.
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.OnConnect = func() {
+19 -4
View File
@@ -1,6 +1,7 @@
package exchange
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
@@ -59,13 +60,27 @@ func NewHyperLiquidTrade(privateKeyHex, address string) (*HyperLiquidTrade, erro
if privateKeyHex == "" {
return &HyperLiquidTrade{client: &http.Client{Timeout: 10 * time.Second}}, nil
}
// Try hex decode first
keyBytes, err := hex.DecodeString(privateKeyHex)
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{
PrivateKey: ed25519.PrivateKey(keyBytes),
Address: address,
@@ -119,7 +134,7 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro
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 {
return "", fmt.Errorf("create request: %w", err)
}