Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ type ExchangeConfig struct {
// SafeExchangeConfig Safe exchange configuration structure (does not contain sensitive information)
type SafeExchangeConfig struct {
ID string `json:"id"` // UUID
ExchangeType string `json:"exchange_type"` // "binance", "bybit", "okx", "hyperliquid", "aster", "lighter"
ExchangeType string `json:"exchange_type"` // "binance", "bybit", "okx", "bitget", "weex", "hyperliquid", "aster", "lighter"
AccountName string `json:"account_name"` // User-defined account name
Name string `json:"name"` // Display name
Type string `json:"type"` // "cex" or "dex"
Expand Down Expand Up @@ -1171,6 +1171,13 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
)
case "weex":
tempTrader, createErr = trader.NewWeexTrader(
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
exchangeCfg.Testnet,
)
case "lighter":
if exchangeCfg.LighterWalletAddr != "" && string(exchangeCfg.LighterAPIKeyPrivateKey) != "" {
// Lighter only supports mainnet
Expand Down Expand Up @@ -1323,6 +1330,13 @@ func (s *Server) handleClosePosition(c *gin.Context) {
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
)
case "weex":
tempTrader, createErr = trader.NewWeexTrader(
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
exchangeCfg.Testnet,
)
case "lighter":
if exchangeCfg.LighterWalletAddr != "" && string(exchangeCfg.LighterAPIKeyPrivateKey) != "" {
// Lighter only supports mainnet
Expand Down Expand Up @@ -1888,7 +1902,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {

// CreateExchangeRequest request structure for creating a new exchange account
type CreateExchangeRequest struct {
ExchangeType string `json:"exchange_type" binding:"required"` // "binance", "bybit", "okx", "hyperliquid", "aster", "lighter"
ExchangeType string `json:"exchange_type" binding:"required"` // "binance", "bybit", "okx", "bitget", "weex", "hyperliquid", "aster", "lighter"
AccountName string `json:"account_name"` // User-defined account name
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
Expand Down Expand Up @@ -1958,7 +1972,7 @@ func (s *Server) handleCreateExchange(c *gin.Context) {

// Validate exchange type
validTypes := map[string]bool{
"binance": true, "bybit": true, "okx": true, "bitget": true,
"binance": true, "bybit": true, "okx": true, "bitget": true, "weex": true,
"hyperliquid": true, "aster": true, "lighter": true,
}
if !validTypes[req.ExchangeType] {
Expand Down Expand Up @@ -3340,6 +3354,8 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) {
{ExchangeType: "binance", Name: "Binance Futures", Type: "cex"},
{ExchangeType: "bybit", Name: "Bybit Futures", Type: "cex"},
{ExchangeType: "okx", Name: "OKX Futures", Type: "cex"},
{ExchangeType: "bitget", Name: "Bitget Futures", Type: "cex"},
{ExchangeType: "weex", Name: "Weex Futures", Type: "cex"},
{ExchangeType: "hyperliquid", Name: "Hyperliquid", Type: "dex"},
{ExchangeType: "aster", Name: "Aster DEX", Type: "dex"},
{ExchangeType: "lighter", Name: "LIGHTER DEX", Type: "dex"},
Expand Down
1 change: 1 addition & 0 deletions hook/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ const (
GETIP = "GETIP" // func (userID string) *IpResult
NEW_BINANCE_TRADER = "NEW_BINANCE_TRADER" // func (userID string, client *futures.Client) *NewBinanceTraderResult
NEW_ASTER_TRADER = "NEW_ASTER_TRADER" // func (userID string, client *http.Client) *NewAsterTraderResult
NEW_WEEX_TRADER = "NEW_WEEX_TRADER" // func (userID string, client *http.Client) *NewWeexTraderResult
SET_HTTP_CLIENT = "SET_HTTP_CLIENT" // func (client *http.Client) *SetHttpClientResult
)
17 changes: 17 additions & 0 deletions hook/trader_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,20 @@ func (r *NewAsterTraderResult) GetResult() *http.Client {
r.Error()
return r.Client
}

type NewWeexTraderResult struct {
Err error
Client *http.Client
}

func (r *NewWeexTraderResult) Error() error {
if r.Err != nil {
log.Printf("⚠️ ζ‰§θ‘ŒNewWeexTraderResultζ—Άε‡Ίι”™: %v", r.Err)
}
return r.Err
}

func (r *NewWeexTraderResult) GetResult() *http.Client {
r.Error()
return r.Client
}
18 changes: 11 additions & 7 deletions manager/trader_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,6 @@ func (tm *TraderManager) GetTopTradersData() (map[string]interface{}, error) {
return result, nil
}


// RemoveTrader removes a trader from memory (does not affect database)
// Used to force reload when updating trader configuration
// If the trader is running, it will be stopped first
Expand Down Expand Up @@ -664,17 +663,17 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
QwenKey: "",
CustomAPIURL: aiModelCfg.CustomAPIURL,
CustomModelName: aiModelCfg.CustomModelName,
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
InitialBalance: traderCfg.InitialBalance,
IsCrossMargin: traderCfg.IsCrossMargin,
ShowInCompetition: traderCfg.ShowInCompetition,
StrategyConfig: strategyConfig,
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
InitialBalance: traderCfg.InitialBalance,
IsCrossMargin: traderCfg.IsCrossMargin,
ShowInCompetition: traderCfg.ShowInCompetition,
StrategyConfig: strategyConfig,
}

logger.Infof("πŸ“Š Loading trader %s: ScanIntervalMinutes=%d (from DB), ScanInterval=%v",
traderCfg.Name, traderCfg.ScanIntervalMinutes, traderConfig.ScanInterval)

// Set API keys based on exchange type (convert EncryptedString to string)
// Set API keys based on exchange type
switch exchangeCfg.ExchangeType {
case "binance":
traderConfig.BinanceAPIKey = string(exchangeCfg.APIKey)
Expand All @@ -690,6 +689,11 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
traderConfig.BitgetAPIKey = string(exchangeCfg.APIKey)
traderConfig.BitgetSecretKey = string(exchangeCfg.SecretKey)
traderConfig.BitgetPassphrase = string(exchangeCfg.Passphrase)
case "weex":
traderConfig.WeexAPIKey = string(exchangeCfg.APIKey)
traderConfig.WeexSecretKey = string(exchangeCfg.SecretKey)
traderConfig.WeexPassphrase = string(exchangeCfg.Passphrase)
traderConfig.WeexTestnet = exchangeCfg.Testnet
case "hyperliquid":
traderConfig.HyperliquidPrivateKey = string(exchangeCfg.APIKey)
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr
Expand Down
38 changes: 20 additions & 18 deletions store/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,27 @@ type ExchangeStore struct {

// Exchange exchange configuration
type Exchange struct {
ID string `gorm:"primaryKey" json:"id"`
ExchangeType string `gorm:"column:exchange_type;not null;default:''" json:"exchange_type"`
AccountName string `gorm:"column:account_name;not null;default:''" json:"account_name"`
UserID string `gorm:"column:user_id;not null;default:default;index" json:"user_id"`
Name string `gorm:"not null" json:"name"`
Type string `gorm:"not null" json:"type"` // "cex" or "dex"
Enabled bool `gorm:"default:false" json:"enabled"`
ID string `gorm:"primaryKey" json:"id"`
ExchangeType string `gorm:"column:exchange_type;not null;default:''" json:"exchange_type"`
AccountName string `gorm:"column:account_name;not null;default:''" json:"account_name"`
UserID string `gorm:"column:user_id;not null;default:default;index" json:"user_id"`
Name string `gorm:"not null" json:"name"`
Type string `gorm:"not null" json:"type"` // "cex" or "dex"
Enabled bool `gorm:"default:false" json:"enabled"`
APIKey crypto.EncryptedString `gorm:"column:api_key;default:''" json:"apiKey"`
SecretKey crypto.EncryptedString `gorm:"column:secret_key;default:''" json:"secretKey"`
Passphrase crypto.EncryptedString `gorm:"column:passphrase;default:''" json:"passphrase"`
Testnet bool `gorm:"default:false" json:"testnet"`
HyperliquidWalletAddr string `gorm:"column:hyperliquid_wallet_addr;default:''" json:"hyperliquidWalletAddr"`
AsterUser string `gorm:"column:aster_user;default:''" json:"asterUser"`
AsterSigner string `gorm:"column:aster_signer;default:''" json:"asterSigner"`
Testnet bool `gorm:"default:false" json:"testnet"`
HyperliquidWalletAddr string `gorm:"column:hyperliquid_wallet_addr;default:''" json:"hyperliquidWalletAddr"`
AsterUser string `gorm:"column:aster_user;default:''" json:"asterUser"`
AsterSigner string `gorm:"column:aster_signer;default:''" json:"asterSigner"`
AsterPrivateKey crypto.EncryptedString `gorm:"column:aster_private_key;default:''" json:"asterPrivateKey"`
LighterWalletAddr string `gorm:"column:lighter_wallet_addr;default:''" json:"lighterWalletAddr"`
LighterWalletAddr string `gorm:"column:lighter_wallet_addr;default:''" json:"lighterWalletAddr"`
LighterPrivateKey crypto.EncryptedString `gorm:"column:lighter_private_key;default:''" json:"lighterPrivateKey"`
LighterAPIKeyPrivateKey crypto.EncryptedString `gorm:"column:lighter_api_key_private_key;default:''" json:"lighterAPIKeyPrivateKey"`
LighterAPIKeyIndex int `gorm:"column:lighter_api_key_index;default:0" json:"lighterAPIKeyIndex"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LighterAPIKeyIndex int `gorm:"column:lighter_api_key_index;default:0" json:"lighterAPIKeyIndex"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func (Exchange) TableName() string { return "exchanges" }
Expand Down Expand Up @@ -80,7 +80,7 @@ func (s *ExchangeStore) migrateToMultiAccount() error {
// Check if migration is needed by looking for old-style IDs (non-UUID)
var count int64
err := s.db.Model(&Exchange{}).
Where("exchange_type = '' AND id IN ?", []string{"binance", "bybit", "okx", "bitget", "hyperliquid", "aster", "lighter"}).
Where("exchange_type = '' AND id IN ?", []string{"binance", "bybit", "okx", "bitget", "hyperliquid", "aster", "lighter", "weex"}).
Count(&count).Error
if err != nil {
return err
Expand All @@ -94,7 +94,7 @@ func (s *ExchangeStore) migrateToMultiAccount() error {

// Get all old records
var records []Exchange
err = s.db.Where("exchange_type = '' AND id IN ?", []string{"binance", "bybit", "okx", "bitget", "hyperliquid", "aster", "lighter"}).
err = s.db.Where("exchange_type = '' AND id IN ?", []string{"binance", "bybit", "okx", "bitget", "hyperliquid", "aster", "lighter", "weex"}).
Find(&records).Error
if err != nil {
return err
Expand Down Expand Up @@ -167,6 +167,8 @@ func getExchangeNameAndType(exchangeType string) (name string, typ string) {
return "OKX Futures", "cex"
case "bitget":
return "Bitget Futures", "cex"
case "weex":
return "Weex Futures", "cex"
case "hyperliquid":
return "Hyperliquid", "dex"
case "aster":
Expand Down Expand Up @@ -305,7 +307,7 @@ func (s *ExchangeStore) CreateLegacy(userID, id, name, typ string, enabled bool,
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error {

// Check if this is an old-style ID (exchange type as ID)
if id == "binance" || id == "bybit" || id == "okx" || id == "bitget" || id == "hyperliquid" || id == "aster" || id == "lighter" {
if id == "binance" || id == "bybit" || id == "okx" || id == "bitget" || id == "weex" || id == "hyperliquid" || id == "aster" || id == "lighter" {
_, err := s.Create(userID, id, "Default", enabled, apiKey, secretKey, "", testnet,
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, "", "", "", 0)
return err
Expand Down
104 changes: 76 additions & 28 deletions trader/auto_trader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"math"
"nofx/kernel"
"nofx/experience"
"nofx/kernel"
"nofx/logger"
"nofx/market"
"nofx/mcp"
Expand All @@ -23,7 +23,7 @@ type AutoTraderConfig struct {
AIModel string // AI model: "qwen" or "deepseek"

// Trading platform selection
Exchange string // Exchange type: "binance", "bybit", "okx", "bitget", "hyperliquid", "aster" or "lighter"
Exchange string // Exchange type: "binance", "bybit", "okx", "bitget", "weex", "hyperliquid", "aster" or "lighter"
ExchangeID string // Exchange account UUID (for multi-account support)

// Binance API configuration
Expand All @@ -35,15 +35,21 @@ type AutoTraderConfig struct {
BybitSecretKey string

// OKX API configuration
OKXAPIKey string
OKXSecretKey string
OKXAPIKey string
OKXSecretKey string
OKXPassphrase string

// Bitget API configuration
BitgetAPIKey string
BitgetSecretKey string
BitgetAPIKey string
BitgetSecretKey string
BitgetPassphrase string

// Weex API configuration
WeexAPIKey string
WeexSecretKey string
WeexPassphrase string
WeexTestnet bool

// Hyperliquid configuration
HyperliquidPrivateKey string
HyperliquidWalletAddr string
Expand Down Expand Up @@ -103,9 +109,9 @@ type AutoTrader struct {
config AutoTraderConfig
trader Trader // Use Trader interface (supports multiple platforms)
mcpClient mcp.AIClient
store *store.Store // Data storage (decision records, etc.)
store *store.Store // Data storage (decision records, etc.)
strategyEngine *kernel.StrategyEngine // Strategy engine (uses strategy configuration)
cycleNumber int // Current cycle number
cycleNumber int // Current cycle number
initialBalance float64
dailyPnL float64
customPrompt string // Custom trading strategy prompt
Expand Down Expand Up @@ -234,6 +240,12 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
case "bitget":
logger.Infof("🏦 [%s] Using Bitget Futures trading", config.Name)
trader = NewBitgetTrader(config.BitgetAPIKey, config.BitgetSecretKey, config.BitgetPassphrase)
case "weex":
logger.Infof("🏦 [%s] Using Weex Futures trading", config.Name)
trader, err = NewWeexTrader(config.WeexAPIKey, config.WeexSecretKey, config.WeexPassphrase, config.WeexTestnet)
if err != nil {
return nil, fmt.Errorf("failed to initialize Weex trader: %w", err)
}
case "hyperliquid":
logger.Infof("🏦 [%s] Using Hyperliquid trading", config.Name)
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
Expand Down Expand Up @@ -766,7 +778,8 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
var updateTime int64
// Priority 1: Get from database (trader_positions table) - most accurate
if at.store != nil {
if dbPos, err := at.store.Position().GetOpenPositionBySymbol(at.id, symbol, side); err == nil && dbPos != nil {
dbSide := strings.ToUpper(side)
if dbPos, err := at.store.Position().GetOpenPositionBySymbol(at.id, symbol, dbSide); err == nil && dbPos != nil {
if dbPos.EntryTime > 0 {
updateTime = dbPos.EntryTime
}
Expand Down Expand Up @@ -1107,7 +1120,25 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio
// Continue execution, doesn't affect trading
}

// Open position
// Open position (prefer preset-aware API when available)
if traderWithPreset, ok := at.trader.(interface {
OpenLongWithPreset(symbol string, quantity float64, leverage int, preset OrderPreset) (map[string]interface{}, error)
}); ok {
order, err := traderWithPreset.OpenLongWithPreset(decision.Symbol, quantity, decision.Leverage, OrderPreset{
StopLoss: decision.StopLoss,
TakeProfit: decision.TakeProfit,
})
if err != nil {
return err
}
actionRecord.OrderID, _ = order["orderId"].(int64)
logger.Infof(" βœ“ Position opened successfully, order ID: %v, quantity: %.4f", order["orderId"], quantity)
at.recordAndConfirmOrder(order, decision.Symbol, "open_long", quantity, marketData.CurrentPrice, decision.Leverage, 0)
posKey := decision.Symbol + "_long"
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
return nil
}

order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
Expand Down Expand Up @@ -1224,7 +1255,25 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti
// Continue execution, doesn't affect trading
}

// Open position
// Open position (prefer preset-aware API when available)
if traderWithPreset, ok := at.trader.(interface {
OpenShortWithPreset(symbol string, quantity float64, leverage int, preset OrderPreset) (map[string]interface{}, error)
}); ok {
order, err := traderWithPreset.OpenShortWithPreset(decision.Symbol, quantity, decision.Leverage, OrderPreset{
StopLoss: decision.StopLoss,
TakeProfit: decision.TakeProfit,
})
if err != nil {
return err
}
actionRecord.OrderID, _ = order["orderId"].(int64)
logger.Infof(" βœ“ Position opened successfully, order ID: %v, quantity: %.4f", order["orderId"], quantity)
at.recordAndConfirmOrder(order, decision.Symbol, "open_short", quantity, marketData.CurrentPrice, decision.Leverage, 0)
posKey := decision.Symbol + "_short"
at.positionFirstSeenTime[posKey] = time.Now().UnixMilli()
return nil
}

order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
Expand Down Expand Up @@ -2118,22 +2167,22 @@ func (at *AutoTrader) recordOrderFill(orderRecordID int64, exchangeOrderID, symb
normalizedSymbol := market.Normalize(symbol)

fill := &store.TraderFill{
TraderID: at.id,
ExchangeID: at.exchangeID,
ExchangeType: at.exchange,
OrderID: orderRecordID,
ExchangeOrderID: exchangeOrderID,
ExchangeTradeID: tradeID,
Symbol: normalizedSymbol,
Side: side,
Price: price,
Quantity: quantity,
QuoteQuantity: price * quantity,
Commission: fee,
CommissionAsset: "USDT",
RealizedPnL: 0, // Will be calculated for close orders
IsMaker: false, // Market orders are usually taker
CreatedAt: time.Now().UTC().UnixMilli(),
TraderID: at.id,
ExchangeID: at.exchangeID,
ExchangeType: at.exchange,
OrderID: orderRecordID,
ExchangeOrderID: exchangeOrderID,
ExchangeTradeID: tradeID,
Symbol: normalizedSymbol,
Side: side,
Price: price,
Quantity: quantity,
QuoteQuantity: price * quantity,
Commission: fee,
CommissionAsset: "USDT",
RealizedPnL: 0, // Will be calculated for close orders
IsMaker: false, // Market orders are usually taker
CreatedAt: time.Now().UTC().UnixMilli(),
}

// Calculate realized PnL for close orders
Expand Down Expand Up @@ -2261,4 +2310,3 @@ func getSideFromAction(action string) string {
func (at *AutoTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) {
return at.trader.GetOpenOrders(symbol)
}

Loading