feat(kucoin): integrate KuCoin exchange support

- Add kucoin to validTypes in api/server.go
- Add KuCoin trader creation in trader_manager.go
- Fix PostgreSQL duplicate key in equity.go (Omit ID)
- Start KuCoin order sync in auto_trader.go
- Update FooterSection UI
This commit is contained in:
tinkle-community
2026-02-04 02:12:37 +08:00
parent b32a3566e6
commit 23dbbf6bdd
5 changed files with 65 additions and 12 deletions
+24 -1
View File
@@ -26,6 +26,7 @@ import (
"nofx/trader/bybit"
"nofx/trader/gate"
hyperliquidtrader "nofx/trader/hyperliquid"
"nofx/trader/kucoin"
"nofx/trader/lighter"
"nofx/trader/okx"
"strconv"
@@ -628,6 +629,12 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
)
case "kucoin":
tempTrader = kucoin.NewKuCoinTrader(
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
)
case "lighter":
if exchangeCfg.LighterWalletAddr != "" && string(exchangeCfg.LighterAPIKeyPrivateKey) != "" {
// Lighter only supports mainnet
@@ -1191,6 +1198,12 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
)
case "kucoin":
tempTrader = kucoin.NewKuCoinTrader(
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
)
case "lighter":
if exchangeCfg.LighterWalletAddr != "" && string(exchangeCfg.LighterAPIKeyPrivateKey) != "" {
// Lighter only supports mainnet
@@ -1348,6 +1361,12 @@ func (s *Server) handleClosePosition(c *gin.Context) {
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
)
case "kucoin":
tempTrader = kucoin.NewKuCoinTrader(
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
)
case "lighter":
if exchangeCfg.LighterWalletAddr != "" && string(exchangeCfg.LighterAPIKeyPrivateKey) != "" {
// Lighter only supports mainnet
@@ -1984,7 +2003,7 @@ func (s *Server) handleCreateExchange(c *gin.Context) {
// Validate exchange type
validTypes := map[string]bool{
"binance": true, "bybit": true, "okx": true, "bitget": true,
"hyperliquid": true, "aster": true, "lighter": true, "gate": true,
"hyperliquid": true, "aster": true, "lighter": true, "gate": true, "kucoin": true,
}
if !validTypes[req.ExchangeType] {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid exchange type: %s", req.ExchangeType)})
@@ -2523,6 +2542,9 @@ func (s *Server) getKlinesFromCoinank(symbol, interval, exchange string, limit i
case "lighter":
// Lighter doesn't have direct CoinAnk support, use Binance data as fallback
coinankExchange = coinank_enum.Binance
case "kucoin":
// KuCoin doesn't have direct CoinAnk support, use Binance data as fallback
coinankExchange = coinank_enum.Binance
default:
// For any unknown exchange, default to Binance
logger.Warnf("⚠️ Unknown exchange '%s', defaulting to Binance for CoinAnk", exchange)
@@ -3368,6 +3390,7 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) {
{ExchangeType: "bybit", Name: "Bybit Futures", Type: "cex"},
{ExchangeType: "okx", Name: "OKX Futures", Type: "cex"},
{ExchangeType: "gate", Name: "Gate.io Futures", Type: "cex"},
{ExchangeType: "kucoin", Name: "KuCoin Futures", Type: "cex"},
{ExchangeType: "hyperliquid", Name: "Hyperliquid", Type: "dex"},
{ExchangeType: "aster", Name: "Aster DEX", Type: "dex"},
{ExchangeType: "lighter", Name: "LIGHTER DEX", Type: "dex"},
+4
View File
@@ -693,6 +693,10 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
case "gate":
traderConfig.GateAPIKey = string(exchangeCfg.APIKey)
traderConfig.GateSecretKey = string(exchangeCfg.SecretKey)
case "kucoin":
traderConfig.KuCoinAPIKey = string(exchangeCfg.APIKey)
traderConfig.KuCoinSecretKey = string(exchangeCfg.SecretKey)
traderConfig.KuCoinPassphrase = string(exchangeCfg.Passphrase)
case "hyperliquid":
traderConfig.HyperliquidPrivateKey = string(exchangeCfg.APIKey)
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr
+3 -1
View File
@@ -53,7 +53,9 @@ func (s *EquityStore) Save(snapshot *EquitySnapshot) error {
snapshot.Timestamp = snapshot.Timestamp.UTC()
}
if err := s.db.Create(snapshot).Error; err != nil {
// Omit ID to let PostgreSQL sequence auto-generate it
// Without this, GORM inserts ID=0 which causes duplicate key errors
if err := s.db.Omit("ID").Create(snapshot).Error; err != nil {
return fmt.Errorf("failed to save equity snapshot: %w", err)
}
return nil
+33 -10
View File
@@ -16,6 +16,7 @@ import (
"nofx/trader/bybit"
"nofx/trader/gate"
"nofx/trader/hyperliquid"
"nofx/trader/kucoin"
"nofx/trader/lighter"
"nofx/trader/okx"
"strings"
@@ -56,6 +57,11 @@ type AutoTraderConfig struct {
GateAPIKey string
GateSecretKey string
// KuCoin API configuration
KuCoinAPIKey string
KuCoinSecretKey string
KuCoinPassphrase string
// Hyperliquid configuration
HyperliquidPrivateKey string
HyperliquidWalletAddr string
@@ -249,6 +255,9 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
case "gate":
logger.Infof("🏦 [%s] Using Gate.io Futures trading", config.Name)
trader = gate.NewGateTrader(config.GateAPIKey, config.GateSecretKey)
case "kucoin":
logger.Infof("🏦 [%s] Using KuCoin Futures trading", config.Name)
trader = kucoin.NewKuCoinTrader(config.KuCoinAPIKey, config.KuCoinSecretKey, config.KuCoinPassphrase)
case "hyperliquid":
logger.Infof("🏦 [%s] Using Hyperliquid trading", config.Name)
trader, err = hyperliquid.NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
@@ -440,6 +449,14 @@ func (at *AutoTrader) Run() error {
}
}
// Start KuCoin order sync if using KuCoin exchange
if at.exchange == "kucoin" {
if kucoinTrader, ok := at.trader.(*kucoin.KuCoinTrader); ok && at.store != nil {
kucoinTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
logger.Infof("🔄 [%s] KuCoin order+position sync enabled (every 30s)", at.name)
}
}
ticker := time.NewTicker(at.config.ScanInterval)
defer ticker.Stop()
@@ -557,15 +574,16 @@ func (at *AutoTrader) runCycle() error {
return fmt.Errorf("failed to build trading context: %w", err)
}
// Save equity snapshot independently (decoupled from AI decision, used for drawing profit curve)
// NOTE: Must be called BEFORE candidate coins check to ensure equity is always recorded
at.saveEquitySnapshot(ctx)
// 如果没有候选币种,友好提示并跳过本周期
if len(ctx.CandidateCoins) == 0 {
logger.Infof("️ No candidate coins available, skipping this cycle")
return nil
}
// Save equity snapshot independently (decoupled from AI decision, used for drawing profit curve)
at.saveEquitySnapshot(ctx)
logger.Info(strings.Repeat("=", 70))
for _, coin := range ctx.CandidateCoins {
record.CandidateCoins = append(record.CandidateCoins, coin.Symbol)
@@ -844,14 +862,19 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
}
// 3. Use strategy engine to get candidate coins (must have strategy engine)
var candidateCoins []kernel.CandidateCoin
if at.strategyEngine == nil {
return nil, fmt.Errorf("trader has no strategy engine configured")
logger.Infof("⚠️ [%s] No strategy engine configured, skipping candidate coins", at.name)
} else {
coins, err := at.strategyEngine.GetCandidateCoins()
if err != nil {
// Log warning but don't fail - equity snapshot should still be saved
logger.Infof("⚠️ [%s] Failed to get candidate coins: %v (will use empty list)", at.name, err)
} else {
candidateCoins = coins
logger.Infof("📋 [%s] Strategy engine fetched candidate coins: %d", at.name, len(candidateCoins))
}
}
candidateCoins, err := at.strategyEngine.GetCandidateCoins()
if err != nil {
return nil, fmt.Errorf("failed to get candidate coins: %w", err)
}
logger.Infof("📋 [%s] Strategy engine fetched candidate coins: %d", at.name, len(candidateCoins))
// 4. Calculate total P&L
totalPnL := totalEquity - at.initialBalance
@@ -1949,7 +1972,7 @@ func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{},
// Exchanges with OrderSync: Skip immediate order recording, let OrderSync handle it
// This ensures accurate data from GetTrades API and avoids duplicate records
switch at.exchange {
case "binance", "lighter", "hyperliquid", "bybit", "okx", "bitget", "aster":
case "binance", "lighter", "hyperliquid", "bybit", "okx", "bitget", "aster", "kucoin":
logger.Infof(" 📝 Order submitted (id: %s), will be synced by OrderSync", orderID)
return
}
@@ -35,6 +35,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
{ name: 'OKX', href: 'https://www.okx.com/join/1865360' },
{ name: 'Bitget', href: 'https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172' },
{ name: 'Gate.io', href: 'https://www.gatenode.xyz/share/VQBGUAxY' },
{ name: 'KuCoin', href: 'https://www.kucoin.com/r/broker/CXEV7XKK' },
{ name: 'Hyperliquid', href: 'https://app.hyperliquid.xyz/join/AITRADING' },
{ name: 'Aster DEX', href: 'https://www.asterdex.com/en/referral/fdfc0e' },
{ name: 'Lighter', href: 'https://app.lighter.xyz/?referral=68151432' },