mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
b9b0a52137
Implemented a new method, `resolveTraderDataWalletKey`, to retrieve the claw402 wallet key based on the selected AI model and user ID. This enhancement allows for better integration of the claw402 model within the trader configuration, ensuring that the correct wallet key is used for trading operations. The `AutoTraderConfig` struct has been updated to include the new `Claw402WalletKey` field, improving the overall handling of wallet keys in the trading process.
666 lines
23 KiB
Go
666 lines
23 KiB
Go
package trader
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"nofx/kernel"
|
|
"nofx/logger"
|
|
"nofx/mcp"
|
|
_ "nofx/mcp/payment"
|
|
_ "nofx/mcp/provider"
|
|
"nofx/store"
|
|
"nofx/trader/aster"
|
|
"nofx/trader/binance"
|
|
"nofx/trader/bitget"
|
|
"nofx/trader/bybit"
|
|
"nofx/trader/gate"
|
|
"nofx/trader/hyperliquid"
|
|
"nofx/trader/indodax"
|
|
"nofx/trader/kucoin"
|
|
"nofx/trader/lighter"
|
|
"nofx/trader/okx"
|
|
"nofx/wallet"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// AutoTraderConfig auto trading configuration (simplified version - AI makes all decisions)
|
|
type AutoTraderConfig struct {
|
|
// Trader identification
|
|
ID string // Trader unique identifier (for log directory, etc.)
|
|
Name string // Trader display name
|
|
AIModel string // AI model: "qwen" or "deepseek"
|
|
|
|
// Trading platform selection
|
|
Exchange string // Exchange type: "binance", "bybit", "okx", "bitget", "gate", "hyperliquid", "aster" or "lighter"
|
|
ExchangeID string // Exchange account UUID (for multi-account support)
|
|
|
|
// Binance API configuration
|
|
BinanceAPIKey string
|
|
BinanceSecretKey string
|
|
|
|
// Bybit API configuration
|
|
BybitAPIKey string
|
|
BybitSecretKey string
|
|
|
|
// OKX API configuration
|
|
OKXAPIKey string
|
|
OKXSecretKey string
|
|
OKXPassphrase string
|
|
|
|
// Bitget API configuration
|
|
BitgetAPIKey string
|
|
BitgetSecretKey string
|
|
BitgetPassphrase string
|
|
|
|
// Gate API configuration
|
|
GateAPIKey string
|
|
GateSecretKey string
|
|
|
|
// KuCoin API configuration
|
|
KuCoinAPIKey string
|
|
KuCoinSecretKey string
|
|
KuCoinPassphrase string
|
|
|
|
// Indodax API configuration
|
|
IndodaxAPIKey string
|
|
IndodaxSecretKey string
|
|
|
|
// Hyperliquid configuration
|
|
HyperliquidPrivateKey string
|
|
HyperliquidWalletAddr string
|
|
HyperliquidTestnet bool
|
|
HyperliquidUnifiedAcct bool // Unified Account mode: Spot USDC as Perp collateral
|
|
|
|
// Aster configuration
|
|
AsterUser string // Aster main wallet address
|
|
AsterSigner string // Aster API wallet address
|
|
AsterPrivateKey string // Aster API wallet private key
|
|
|
|
// LIGHTER configuration
|
|
LighterWalletAddr string // LIGHTER wallet address (L1 wallet)
|
|
LighterPrivateKey string // LIGHTER L1 private key (for account identification)
|
|
LighterAPIKeyPrivateKey string // LIGHTER API Key private key (40 bytes, for transaction signing)
|
|
LighterAPIKeyIndex int // LIGHTER API Key index (0-255)
|
|
LighterTestnet bool // Whether to use testnet
|
|
|
|
// AI configuration
|
|
UseQwen bool
|
|
DeepSeekKey string
|
|
QwenKey string
|
|
|
|
// Custom AI API configuration
|
|
CustomAPIURL string
|
|
CustomAPIKey string
|
|
CustomModelName string
|
|
Claw402WalletKey string
|
|
|
|
// Scan configuration
|
|
ScanInterval time.Duration // Scan interval (recommended 3 minutes)
|
|
|
|
// Account configuration
|
|
InitialBalance float64 // Initial balance (for P&L calculation, must be set manually)
|
|
|
|
// Risk control (only as hints, AI can make autonomous decisions)
|
|
MaxDailyLoss float64 // Maximum daily loss percentage (hint)
|
|
MaxDrawdown float64 // Maximum drawdown percentage (hint)
|
|
StopTradingTime time.Duration // Pause duration after risk control triggers
|
|
|
|
// Position mode
|
|
IsCrossMargin bool // true=cross margin mode, false=isolated margin mode
|
|
|
|
// Competition visibility
|
|
ShowInCompetition bool // Whether to show in competition page
|
|
|
|
// Strategy configuration (use complete strategy config)
|
|
StrategyConfig *store.StrategyConfig // Strategy configuration (includes coin sources, indicators, risk control, prompts, etc.)
|
|
}
|
|
|
|
// AutoTrader automatic trader
|
|
type AutoTrader struct {
|
|
id string // Trader unique identifier
|
|
name string // Trader display name
|
|
aiModel string // AI model name
|
|
exchange string // Trading platform type (binance/bybit/etc)
|
|
exchangeID string // Exchange account UUID
|
|
showInCompetition bool // Whether to show in competition page
|
|
config AutoTraderConfig
|
|
trader Trader // Use Trader interface (supports multiple platforms)
|
|
mcpClient mcp.AIClient
|
|
store *store.Store // Data storage (decision records, etc.)
|
|
strategyEngine *kernel.StrategyEngine // Strategy engine (uses strategy configuration)
|
|
cycleNumber int // Current cycle number
|
|
initialBalance float64
|
|
dailyPnL float64
|
|
customPrompt string // Custom trading strategy prompt
|
|
overrideBasePrompt bool // Whether to override base prompt
|
|
lastResetTime time.Time
|
|
stopUntil time.Time
|
|
isRunning bool
|
|
isRunningMutex sync.RWMutex // Mutex to protect isRunning flag
|
|
startTime time.Time // System start time
|
|
callCount int // AI call count
|
|
positionFirstSeenTime map[string]int64 // Position first seen time (symbol_side -> timestamp in milliseconds)
|
|
stopMonitorCh chan struct{} // Used to stop monitoring goroutine
|
|
monitorWg sync.WaitGroup // Used to wait for monitoring goroutine to finish
|
|
peakPnLCache map[string]float64 // Peak profit cache (symbol -> peak P&L percentage)
|
|
peakPnLCacheMutex sync.RWMutex // Cache read-write lock
|
|
lastBalanceSyncTime time.Time // Last balance sync time
|
|
userID string // User ID
|
|
gridState *GridState // Grid trading state (only used when StrategyType == "grid_trading")
|
|
claw402WalletAddr string // Claw402 wallet address (derived from private key at start)
|
|
consecutiveAIFailures int // Consecutive AI call failures
|
|
safeMode bool // Safe mode: no new positions, protect existing ones
|
|
safeModeReason string // Why safe mode was activated
|
|
}
|
|
|
|
// NewAutoTrader creates an automatic trader
|
|
// st parameter is used to store decision records to database
|
|
func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*AutoTrader, error) {
|
|
// Set default values
|
|
if config.ID == "" {
|
|
config.ID = "default_trader"
|
|
}
|
|
if config.Name == "" {
|
|
config.Name = "Default Trader"
|
|
}
|
|
if config.AIModel == "" {
|
|
if config.UseQwen {
|
|
config.AIModel = "qwen"
|
|
} else {
|
|
config.AIModel = "deepseek"
|
|
}
|
|
}
|
|
|
|
// Initialize AI client based on provider
|
|
var mcpClient mcp.AIClient
|
|
aiModel := config.AIModel
|
|
if config.UseQwen && aiModel == "" {
|
|
aiModel = "qwen"
|
|
}
|
|
|
|
// Resolve API key (provider-specific overrides)
|
|
apiKey := config.CustomAPIKey
|
|
customURL := config.CustomAPIURL
|
|
switch aiModel {
|
|
case "qwen":
|
|
if config.QwenKey != "" {
|
|
apiKey = config.QwenKey
|
|
}
|
|
case "deepseek", "":
|
|
if config.DeepSeekKey != "" {
|
|
apiKey = config.DeepSeekKey
|
|
}
|
|
}
|
|
|
|
// Create client via registry (covers all registered providers)
|
|
if aiModel == "custom" {
|
|
mcpClient = mcp.New()
|
|
} else if aiModel == "" {
|
|
aiModel = "deepseek"
|
|
mcpClient = mcp.NewAIClientByProvider(aiModel)
|
|
} else {
|
|
mcpClient = mcp.NewAIClientByProvider(aiModel)
|
|
}
|
|
if mcpClient == nil {
|
|
mcpClient = mcp.New()
|
|
}
|
|
|
|
// Payment providers (claw402) ignore customURL
|
|
switch aiModel {
|
|
case "claw402":
|
|
mcpClient.SetAPIKey(apiKey, "", config.CustomModelName)
|
|
default:
|
|
mcpClient.SetAPIKey(apiKey, customURL, config.CustomModelName)
|
|
}
|
|
logger.Infof("🤖 [%s] Using %s AI", config.Name, aiModel)
|
|
|
|
if config.CustomAPIURL != "" || config.CustomModelName != "" {
|
|
logger.Infof("🔧 [%s] Custom config - URL: %s, Model: %s", config.Name, config.CustomAPIURL, config.CustomModelName)
|
|
}
|
|
|
|
// Set default trading platform
|
|
if config.Exchange == "" {
|
|
config.Exchange = "binance"
|
|
}
|
|
|
|
// Create corresponding trader based on configuration
|
|
var trader Trader
|
|
var err error
|
|
|
|
// Record position mode (general)
|
|
marginModeStr := "Cross Margin"
|
|
if !config.IsCrossMargin {
|
|
marginModeStr = "Isolated Margin"
|
|
}
|
|
logger.Infof("📊 [%s] Position mode: %s", config.Name, marginModeStr)
|
|
|
|
switch config.Exchange {
|
|
case "binance":
|
|
logger.Infof("🏦 [%s] Using Binance Futures trading", config.Name)
|
|
trader = binance.NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey, userID)
|
|
case "bybit":
|
|
logger.Infof("🏦 [%s] Using Bybit Futures trading", config.Name)
|
|
trader = bybit.NewBybitTrader(config.BybitAPIKey, config.BybitSecretKey)
|
|
case "okx":
|
|
logger.Infof("🏦 [%s] Using OKX Futures trading", config.Name)
|
|
trader = okx.NewOKXTrader(config.OKXAPIKey, config.OKXSecretKey, config.OKXPassphrase)
|
|
case "bitget":
|
|
logger.Infof("🏦 [%s] Using Bitget Futures trading", config.Name)
|
|
trader = bitget.NewBitgetTrader(config.BitgetAPIKey, config.BitgetSecretKey, config.BitgetPassphrase)
|
|
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, config.HyperliquidUnifiedAcct)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize Hyperliquid trader: %w", err)
|
|
}
|
|
case "aster":
|
|
logger.Infof("🏦 [%s] Using Aster trading", config.Name)
|
|
trader, err = aster.NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize Aster trader: %w", err)
|
|
}
|
|
case "lighter":
|
|
logger.Infof("🏦 [%s] Using LIGHTER trading", config.Name)
|
|
|
|
if config.LighterWalletAddr == "" || config.LighterAPIKeyPrivateKey == "" {
|
|
return nil, fmt.Errorf("Lighter requires wallet address and API Key private key")
|
|
}
|
|
|
|
// Lighter only supports mainnet (testnet disabled)
|
|
trader, err = lighter.NewLighterTraderV2(
|
|
config.LighterWalletAddr,
|
|
config.LighterAPIKeyPrivateKey,
|
|
config.LighterAPIKeyIndex,
|
|
false, // Always use mainnet for Lighter
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize LIGHTER trader: %w", err)
|
|
}
|
|
logger.Infof("✓ LIGHTER trader initialized successfully")
|
|
case "indodax":
|
|
logger.Infof("🏦 [%s] Using Indodax Spot trading", config.Name)
|
|
trader = indodax.NewIndodaxTrader(config.IndodaxAPIKey, config.IndodaxSecretKey)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported trading platform: %s", config.Exchange)
|
|
}
|
|
|
|
// Validate initial balance configuration, auto-fetch from exchange if 0
|
|
if config.InitialBalance <= 0 {
|
|
logger.Infof("📊 [%s] Initial balance not set, attempting to fetch current balance from exchange...", config.Name)
|
|
account, err := trader.GetBalance()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initial balance not set and unable to fetch balance from exchange: %w", err)
|
|
}
|
|
// Try multiple balance field names (different exchanges return different formats)
|
|
balanceKeys := []string{"total_equity", "totalWalletBalance", "wallet_balance", "totalEq", "balance"}
|
|
var foundBalance float64
|
|
for _, key := range balanceKeys {
|
|
if balance, ok := account[key].(float64); ok && balance > 0 {
|
|
foundBalance = balance
|
|
break
|
|
}
|
|
}
|
|
if foundBalance > 0 {
|
|
config.InitialBalance = foundBalance
|
|
logger.Infof("✓ [%s] Auto-fetched initial balance: %.2f USDT", config.Name, foundBalance)
|
|
// Save to database so it persists across restarts
|
|
if st != nil {
|
|
if err := st.Trader().UpdateInitialBalance(userID, config.ID, foundBalance); err != nil {
|
|
logger.Infof("⚠️ [%s] Failed to save initial balance to database: %v", config.Name, err)
|
|
} else {
|
|
logger.Infof("✓ [%s] Initial balance saved to database", config.Name)
|
|
}
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("initial balance must be greater than 0, please set InitialBalance in config or ensure exchange account has balance")
|
|
}
|
|
}
|
|
|
|
// Get last cycle number (for recovery)
|
|
var cycleNumber int
|
|
if st != nil {
|
|
cycleNumber, _ = st.Decision().GetLastCycleNumber(config.ID)
|
|
logger.Infof("📊 [%s] Decision records will be stored to database", config.Name)
|
|
}
|
|
|
|
// Create strategy engine (must have strategy config)
|
|
if config.StrategyConfig == nil {
|
|
return nil, fmt.Errorf("[%s] strategy not configured", config.Name)
|
|
}
|
|
// Pass claw402 wallet key to strategy engine so nofxos data requests
|
|
// are routed through claw402 (reuses the same wallet as AI calls)
|
|
claw402Key := config.Claw402WalletKey
|
|
if claw402Key == "" && config.AIModel == "claw402" && config.CustomAPIKey != "" {
|
|
claw402Key = config.CustomAPIKey
|
|
}
|
|
strategyEngine := kernel.NewStrategyEngine(config.StrategyConfig, claw402Key)
|
|
logger.Infof("✓ [%s] Using strategy engine (strategy configuration loaded)", config.Name)
|
|
|
|
return &AutoTrader{
|
|
id: config.ID,
|
|
name: config.Name,
|
|
aiModel: config.AIModel,
|
|
exchange: config.Exchange,
|
|
exchangeID: config.ExchangeID,
|
|
showInCompetition: config.ShowInCompetition,
|
|
config: config,
|
|
trader: trader,
|
|
mcpClient: mcpClient,
|
|
store: st,
|
|
strategyEngine: strategyEngine,
|
|
cycleNumber: cycleNumber,
|
|
initialBalance: config.InitialBalance,
|
|
lastResetTime: time.Now(),
|
|
startTime: time.Now(),
|
|
callCount: 0,
|
|
isRunning: false,
|
|
positionFirstSeenTime: make(map[string]int64),
|
|
stopMonitorCh: make(chan struct{}),
|
|
monitorWg: sync.WaitGroup{},
|
|
peakPnLCache: make(map[string]float64),
|
|
peakPnLCacheMutex: sync.RWMutex{},
|
|
lastBalanceSyncTime: time.Now(),
|
|
userID: userID,
|
|
}, nil
|
|
}
|
|
|
|
// Run runs the automatic trading main loop
|
|
func (at *AutoTrader) Run() error {
|
|
at.isRunningMutex.Lock()
|
|
at.isRunning = true
|
|
at.isRunningMutex.Unlock()
|
|
|
|
at.stopMonitorCh = make(chan struct{})
|
|
at.startTime = time.Now()
|
|
|
|
logger.Info("🚀 AI-driven automatic trading system started")
|
|
logger.Infof("💰 Initial balance: %.2f USDT", at.initialBalance)
|
|
logger.Infof("⚙️ Scan interval: %v", at.config.ScanInterval)
|
|
logger.Info("🤖 AI will make full decisions on leverage, position size, stop loss/take profit, etc.")
|
|
|
|
// Pre-launch checks for claw402 users
|
|
at.runPreLaunchChecks()
|
|
at.monitorWg.Add(1)
|
|
defer at.monitorWg.Done()
|
|
|
|
// Start drawdown monitoring
|
|
at.startDrawdownMonitor()
|
|
|
|
// Start Lighter order sync if using Lighter exchange
|
|
if at.exchange == "lighter" {
|
|
if lighterTrader, ok := at.trader.(*lighter.LighterTraderV2); ok && at.store != nil {
|
|
lighterTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] Lighter order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// Start Hyperliquid order sync if using Hyperliquid exchange
|
|
if at.exchange == "hyperliquid" {
|
|
if hyperliquidTrader, ok := at.trader.(*hyperliquid.HyperliquidTrader); ok && at.store != nil {
|
|
hyperliquidTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] Hyperliquid order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// Start Bybit order sync if using Bybit exchange
|
|
if at.exchange == "bybit" {
|
|
if bybitTrader, ok := at.trader.(*bybit.BybitTrader); ok && at.store != nil {
|
|
bybitTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] Bybit order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// Start OKX order sync if using OKX exchange
|
|
if at.exchange == "okx" {
|
|
if okxTrader, ok := at.trader.(*okx.OKXTrader); ok && at.store != nil {
|
|
okxTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] OKX order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// Start Bitget order sync if using Bitget exchange
|
|
if at.exchange == "bitget" {
|
|
if bitgetTrader, ok := at.trader.(*bitget.BitgetTrader); ok && at.store != nil {
|
|
bitgetTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] Bitget order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// Start Aster order sync if using Aster exchange
|
|
if at.exchange == "aster" {
|
|
if asterTrader, ok := at.trader.(*aster.AsterTrader); ok && at.store != nil {
|
|
asterTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] Aster order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// Start Binance order sync if using Binance exchange
|
|
if at.exchange == "binance" {
|
|
if binanceTrader, ok := at.trader.(*binance.FuturesTrader); ok && at.store != nil {
|
|
binanceTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] Binance order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// Start Gate order sync if using Gate exchange
|
|
if at.exchange == "gate" {
|
|
if gateTrader, ok := at.trader.(*gate.GateTrader); ok && at.store != nil {
|
|
gateTrader.StartOrderSync(at.id, at.exchangeID, at.exchange, at.store, 30*time.Second)
|
|
logger.Infof("🔄 [%s] Gate order+position sync enabled (every 30s)", at.name)
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Check if this is a grid trading strategy
|
|
isGridStrategy := at.IsGridStrategy()
|
|
if isGridStrategy {
|
|
logger.Infof("🔲 [%s] Grid trading strategy detected, initializing grid...", at.name)
|
|
if err := at.InitializeGrid(); err != nil {
|
|
logger.Errorf("❌ [%s] Failed to initialize grid: %v", at.name, err)
|
|
return fmt.Errorf("grid initialization failed: %w", err)
|
|
}
|
|
}
|
|
|
|
// Execute immediately on first run
|
|
if isGridStrategy {
|
|
if err := at.RunGridCycle(); err != nil {
|
|
logger.Infof("❌ Grid execution failed: %v", err)
|
|
}
|
|
} else {
|
|
if err := at.runCycle(); err != nil {
|
|
logger.Infof("❌ Execution failed: %v", err)
|
|
}
|
|
}
|
|
|
|
for {
|
|
at.isRunningMutex.RLock()
|
|
running := at.isRunning
|
|
at.isRunningMutex.RUnlock()
|
|
|
|
if !running {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ticker.C:
|
|
if isGridStrategy {
|
|
if err := at.RunGridCycle(); err != nil {
|
|
logger.Infof("❌ Grid execution failed: %v", err)
|
|
}
|
|
} else {
|
|
if err := at.runCycle(); err != nil {
|
|
logger.Infof("❌ Execution failed: %v", err)
|
|
}
|
|
}
|
|
case <-at.stopMonitorCh:
|
|
logger.Infof("[%s] ⏹ Stop signal received, exiting automatic trading main loop", at.name)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the automatic trading
|
|
func (at *AutoTrader) Stop() {
|
|
at.isRunningMutex.Lock()
|
|
if !at.isRunning {
|
|
at.isRunningMutex.Unlock()
|
|
return
|
|
}
|
|
at.isRunning = false
|
|
at.isRunningMutex.Unlock()
|
|
|
|
close(at.stopMonitorCh) // Notify monitoring goroutine to stop
|
|
at.monitorWg.Wait() // Wait for monitoring goroutine to finish
|
|
logger.Info("⏹ Automatic trading system stopped")
|
|
}
|
|
|
|
// GetID gets trader ID
|
|
func (at *AutoTrader) GetID() string {
|
|
return at.id
|
|
}
|
|
|
|
// GetUnderlyingTrader returns the underlying Trader interface implementation
|
|
// This is used by grid trading and other components that need direct exchange access
|
|
func (at *AutoTrader) GetUnderlyingTrader() Trader {
|
|
return at.trader
|
|
}
|
|
|
|
// GetName gets trader name
|
|
func (at *AutoTrader) GetName() string {
|
|
return at.name
|
|
}
|
|
|
|
// GetAIModel gets AI model
|
|
func (at *AutoTrader) GetAIModel() string {
|
|
return at.aiModel
|
|
}
|
|
|
|
// GetExchange gets exchange
|
|
func (at *AutoTrader) GetExchange() string {
|
|
return at.exchange
|
|
}
|
|
|
|
// GetShowInCompetition returns whether trader should be shown in competition
|
|
func (at *AutoTrader) GetShowInCompetition() bool {
|
|
return at.showInCompetition
|
|
}
|
|
|
|
// SetShowInCompetition sets whether trader should be shown in competition
|
|
func (at *AutoTrader) SetShowInCompetition(show bool) {
|
|
at.showInCompetition = show
|
|
}
|
|
|
|
// SetCustomPrompt sets custom trading strategy prompt
|
|
func (at *AutoTrader) SetCustomPrompt(prompt string) {
|
|
at.customPrompt = prompt
|
|
}
|
|
|
|
// SetOverrideBasePrompt sets whether to override base prompt
|
|
func (at *AutoTrader) SetOverrideBasePrompt(override bool) {
|
|
at.overrideBasePrompt = override
|
|
}
|
|
|
|
// GetSystemPromptTemplate gets current system prompt template name (from strategy config)
|
|
func (at *AutoTrader) GetSystemPromptTemplate() string {
|
|
if at.strategyEngine != nil {
|
|
config := at.strategyEngine.GetConfig()
|
|
if config.CustomPrompt != "" {
|
|
return "custom"
|
|
}
|
|
}
|
|
return "strategy"
|
|
}
|
|
|
|
// GetStore gets data store (for external access to decision records, etc.)
|
|
func (at *AutoTrader) GetStore() *store.Store {
|
|
return at.store
|
|
}
|
|
|
|
// calculatePnLPercentage calculates P&L percentage (based on margin, automatically considers leverage)
|
|
// Return rate = Unrealized P&L / Margin x 100%
|
|
func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 {
|
|
if marginUsed > 0 {
|
|
return (unrealizedPnl / marginUsed) * 100
|
|
}
|
|
return 0.0
|
|
}
|
|
|
|
// runPreLaunchChecks performs pre-launch checks for claw402 users (wallet balance, runway estimate)
|
|
func (at *AutoTrader) runPreLaunchChecks() {
|
|
if !store.IsClaw402Config(at.config.AIModel) {
|
|
return
|
|
}
|
|
|
|
logger.Info("🔍 Running pre-launch checks (claw402)...")
|
|
|
|
// Derive wallet address from CustomAPIKey (which is the private key for claw402)
|
|
if at.config.CustomAPIKey != "" {
|
|
// Try to derive address using go-ethereum
|
|
addr := deriveWalletAddress(at.config.CustomAPIKey)
|
|
if addr != "" {
|
|
at.claw402WalletAddr = addr
|
|
logger.Infof("💳 [%s] Claw402 wallet: %s", at.name, addr)
|
|
|
|
// Query USDC balance
|
|
balance, err := wallet.QueryUSDCBalance(addr)
|
|
if err != nil {
|
|
logger.Warnf("⚠️ [%s] Could not query USDC balance: %v", at.name, err)
|
|
} else {
|
|
// Estimate runway
|
|
scanMinutes := int(at.config.ScanInterval.Minutes())
|
|
modelName := at.config.CustomModelName
|
|
if modelName == "" {
|
|
modelName = "deepseek"
|
|
}
|
|
dailyCost, runway := store.EstimateRunway(balance, modelName, scanMinutes)
|
|
logger.Infof("💰 [%s] USDC Balance: $%.2f | Daily AI cost: ~$%.2f | Runway: ~%.1f days",
|
|
at.name, balance, dailyCost, runway)
|
|
|
|
if balance < 1.0 {
|
|
logger.Warnf("⚠️ [%s] Low USDC balance! Consider topping up.", at.name)
|
|
}
|
|
if balance <= 0 {
|
|
logger.Errorf("🚨 [%s] USDC balance is ZERO — AI calls will fail!", at.name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.Info("✅ Pre-launch checks complete")
|
|
}
|
|
|
|
// deriveWalletAddress derives an Ethereum address from a hex private key
|
|
func deriveWalletAddress(privateKeyHex string) string {
|
|
// Remove 0x prefix if present
|
|
if len(privateKeyHex) > 2 && privateKeyHex[:2] == "0x" {
|
|
privateKeyHex = privateKeyHex[2:]
|
|
}
|
|
|
|
privateKey, err := crypto.HexToECDSA(privateKeyHex)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
address := crypto.PubkeyToAddress(privateKey.PublicKey)
|
|
return address.Hex()
|
|
}
|