feat: port NOFXi agent module onto latest dev base (#1485)

* feat: integrate NOFXi agent into dev

* Enhance NOFXi agent workflow and diagnostics
This commit is contained in:
lky-spec
2026-04-21 23:47:55 +08:00
committed by GitHub
parent 1ba50bdedf
commit 3ca95b294d
88 changed files with 22630 additions and 1143 deletions
+59 -18
View File
@@ -24,6 +24,31 @@ import (
"time"
)
func (at *AutoTrader) logTag() string {
if at == nil {
return "[trader_id=unknown]"
}
if at.name != "" {
return fmt.Sprintf("[trader_id=%s trader_name=%s]", at.id, at.name)
}
return fmt.Sprintf("[trader_id=%s]", at.id)
}
func (at *AutoTrader) logInfof(format string, args ...interface{}) {
values := append([]interface{}{at.logTag()}, args...)
logger.Infof("%s "+format, values...)
}
func (at *AutoTrader) logWarnf(format string, args ...interface{}) {
values := append([]interface{}{at.logTag()}, args...)
logger.Warnf("%s "+format, values...)
}
func (at *AutoTrader) logErrorf(format string, args ...interface{}) {
values := append([]interface{}{at.logTag()}, args...)
logger.Errorf("%s "+format, values...)
}
// AutoTraderConfig auto trading configuration (simplified version - AI makes all decisions)
type AutoTraderConfig struct {
// Trader identification
@@ -381,8 +406,8 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("💰 Initial balance: %.2f USDT", at.initialBalance)
at.logInfof("⚙️ 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
@@ -397,7 +422,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 Lighter order+position sync enabled (every 30s)")
}
}
@@ -405,7 +430,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 Hyperliquid order+position sync enabled (every 30s)")
}
}
@@ -413,7 +438,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 Bybit order+position sync enabled (every 30s)")
}
}
@@ -421,7 +446,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 OKX order+position sync enabled (every 30s)")
}
}
@@ -429,7 +454,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 Bitget order+position sync enabled (every 30s)")
}
}
@@ -437,7 +462,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 Aster order+position sync enabled (every 30s)")
}
}
@@ -445,7 +470,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 Binance order+position sync enabled (every 30s)")
}
}
@@ -453,7 +478,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 Gate order+position sync enabled (every 30s)")
}
}
@@ -461,7 +486,7 @@ func (at *AutoTrader) Run() error {
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)
at.logInfof("🔄 KuCoin order+position sync enabled (every 30s)")
}
}
@@ -471,9 +496,9 @@ func (at *AutoTrader) Run() error {
// Check if this is a grid trading strategy
isGridStrategy := at.IsGridStrategy()
if isGridStrategy {
logger.Infof("🔲 [%s] Grid trading strategy detected, initializing grid...", at.name)
at.logInfof("🔲 Grid trading strategy detected, initializing grid...")
if err := at.InitializeGrid(); err != nil {
logger.Errorf("❌ [%s] Failed to initialize grid: %v", at.name, err)
at.logErrorf("❌ Failed to initialize grid: %v", err)
return fmt.Errorf("grid initialization failed: %w", err)
}
}
@@ -481,11 +506,11 @@ func (at *AutoTrader) Run() error {
// Execute immediately on first run
if isGridStrategy {
if err := at.RunGridCycle(); err != nil {
logger.Infof("❌ Grid execution failed: %v", err)
at.logErrorf("❌ Grid execution failed: %v", err)
}
} else {
if err := at.runCycle(); err != nil {
logger.Infof("❌ Execution failed: %v", err)
at.logErrorf("❌ Execution failed: %v", err)
}
}
@@ -502,15 +527,15 @@ func (at *AutoTrader) Run() error {
case <-ticker.C:
if isGridStrategy {
if err := at.RunGridCycle(); err != nil {
logger.Infof("❌ Grid execution failed: %v", err)
at.logErrorf("❌ Grid execution failed: %v", err)
}
} else {
if err := at.runCycle(); err != nil {
logger.Infof("❌ Execution failed: %v", err)
at.logErrorf("❌ Execution failed: %v", err)
}
}
case <-at.stopMonitorCh:
logger.Infof("[%s] ⏹ Stop signal received, exiting automatic trading main loop", at.name)
at.logInfof("⏹ Stop signal received, exiting automatic trading main loop")
return nil
}
}
@@ -590,6 +615,22 @@ func (at *AutoTrader) GetSystemPromptTemplate() string {
return "strategy"
}
// GetCandidateCoins returns the current candidate coin set from the trader's strategy engine.
func (at *AutoTrader) GetCandidateCoins() ([]kernel.CandidateCoin, error) {
if at.strategyEngine == nil {
return nil, fmt.Errorf("strategy engine not configured")
}
return at.strategyEngine.GetCandidateCoins()
}
// GetStrategyConfig returns the current strategy config used by the trader.
func (at *AutoTrader) GetStrategyConfig() *store.StrategyConfig {
if at.strategyEngine == nil {
return at.config.StrategyConfig
}
return at.strategyEngine.GetConfig()
}
// GetStore gets data store (for external access to decision records, etc.)
func (at *AutoTrader) GetStore() *store.Store {
return at.store
+30 -30
View File
@@ -24,7 +24,7 @@ func (at *AutoTrader) runCycle() error {
running := at.isRunning
at.isRunningMutex.RUnlock()
if !running {
logger.Infof("⏹ Trader is stopped, aborting cycle #%d", at.callCount)
at.logInfof("⏹ Trader is stopped, aborting cycle #%d", at.callCount)
return nil
}
@@ -42,7 +42,7 @@ func (at *AutoTrader) runCycle() error {
// 1. Check if trading needs to be stopped
if time.Now().Before(at.stopUntil) {
remaining := at.stopUntil.Sub(time.Now())
logger.Infof("⏸ Risk control: Trading paused, remaining %.0f minutes", remaining.Minutes())
at.logWarnf("⏸ Risk control: Trading paused, remaining %.0f minutes", remaining.Minutes())
record.Success = false
record.ErrorMessage = fmt.Sprintf("Risk control paused, remaining %.0f minutes", remaining.Minutes())
at.saveDecision(record)
@@ -59,6 +59,7 @@ func (at *AutoTrader) runCycle() error {
// 4. Collect trading context
ctx, err := at.buildTradingContext()
if err != nil {
at.logErrorf("failed to build trading context: %v", err)
record.Success = false
record.ErrorMessage = fmt.Sprintf("Failed to build trading context: %v", err)
at.saveDecision(record)
@@ -71,7 +72,7 @@ func (at *AutoTrader) runCycle() error {
// If no candidate coins available, log but do not error
if len(ctx.CandidateCoins) == 0 {
logger.Infof(" No candidate coins available, skipping this cycle")
at.logInfof("️ No candidate coins available, skipping this cycle")
record.Success = true // Not an error, just no candidate coins
record.ExecutionLog = append(record.ExecutionLog, "No candidate coins available, cycle skipped")
record.AccountState = store.AccountSnapshot{
@@ -90,16 +91,16 @@ func (at *AutoTrader) runCycle() error {
record.CandidateCoins = append(record.CandidateCoins, coin.Symbol)
}
logger.Infof("📊 Account equity: %.2f USDT | Available: %.2f USDT | Positions: %d",
at.logInfof("📊 Account equity: %.2f USDT | Available: %.2f USDT | Positions: %d",
ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount)
// 5. Use strategy engine to call AI for decision
logger.Infof("🤖 Requesting AI analysis and decision... [Strategy Engine]")
at.logInfof("🤖 Requesting AI analysis and decision... [Strategy Engine]")
aiDecision, err := kernel.GetFullDecisionWithStrategy(ctx, at.mcpClient, at.strategyEngine, "balanced")
if aiDecision != nil && aiDecision.AIRequestDurationMs > 0 {
record.AIRequestDurationMs = aiDecision.AIRequestDurationMs
logger.Infof("⏱️ AI call duration: %.2f seconds", float64(record.AIRequestDurationMs)/1000)
at.logInfof("⏱️ AI call duration: %.2f seconds", float64(record.AIRequestDurationMs)/1000)
record.ExecutionLog = append(record.ExecutionLog,
fmt.Sprintf("AI call duration: %d ms", record.AIRequestDurationMs))
}
@@ -119,7 +120,7 @@ func (at *AutoTrader) runCycle() error {
// Record AI charge (track cost regardless of decision outcome)
if aiDecision != nil && at.store != nil {
if chargeErr := at.store.AICharge().Record(at.id, at.aiModel, at.config.AIModel); chargeErr != nil {
logger.Warnf("⚠️ Failed to record AI charge: %v", chargeErr)
at.logWarnf("⚠️ Failed to record AI charge: %v", chargeErr)
}
}
@@ -132,10 +133,9 @@ func (at *AutoTrader) runCycle() error {
if at.consecutiveAIFailures >= 3 && !at.safeMode {
at.safeMode = true
at.safeModeReason = fmt.Sprintf("AI failed %d consecutive times: %v", at.consecutiveAIFailures, err)
logger.Errorf("🛡️ [%s] SAFE MODE ACTIVATED — AI failed %d times in a row. No new positions will be opened. Existing positions are protected with current stop-loss settings.",
at.name, at.consecutiveAIFailures)
logger.Errorf("🛡️ [%s] Reason: %v", at.name, err)
logger.Errorf("🛡️ [%s] Action: Will keep trying AI each cycle. Safe mode auto-deactivates when AI recovers.", at.name)
at.logErrorf("🛡️ SAFE MODE ACTIVATED — AI failed %d times in a row. No new positions will be opened. Existing positions are protected with current stop-loss settings.", at.consecutiveAIFailures)
at.logErrorf("🛡️ Reason: %v", err)
at.logErrorf("🛡️ Action: Will keep trying AI each cycle. Safe mode auto-deactivates when AI recovers.")
}
// Print system prompt and AI chain of thought (output even with errors for debugging)
@@ -159,7 +159,7 @@ func (at *AutoTrader) runCycle() error {
// In safe mode, don't return error — keep the loop running to retry next cycle
if at.safeMode {
logger.Warnf("🛡️ [%s] Safe mode: skipping this cycle, will retry in %v", at.name, at.config.ScanInterval)
at.logWarnf("🛡️ Safe mode: skipping this cycle, will retry in %v", at.config.ScanInterval)
return nil
}
@@ -168,11 +168,11 @@ func (at *AutoTrader) runCycle() error {
// AI succeeded — reset failure counter and deactivate safe mode
if at.consecutiveAIFailures > 0 {
logger.Infof("✅ [%s] AI recovered after %d consecutive failures", at.name, at.consecutiveAIFailures)
at.logInfof("✅ AI recovered after %d consecutive failures", at.consecutiveAIFailures)
}
at.consecutiveAIFailures = 0
if at.safeMode {
logger.Infof("🛡️ [%s] SAFE MODE DEACTIVATED — AI is working again. Resuming normal trading.", at.name)
at.logInfof("🛡️ SAFE MODE DEACTIVATED — AI is working again. Resuming normal trading.")
at.safeMode = false
at.safeModeReason = ""
}
@@ -219,7 +219,7 @@ func (at *AutoTrader) runCycle() error {
running = at.isRunning
at.isRunningMutex.RUnlock()
if !running {
logger.Infof("⏹ Trader stopped before decision execution, aborting cycle #%d", at.callCount)
at.logInfof("⏹ Trader stopped before decision execution, aborting cycle #%d", at.callCount)
return nil
}
@@ -228,14 +228,14 @@ func (at *AutoTrader) runCycle() error {
filtered := make([]kernel.Decision, 0)
for _, d := range sortedDecisions {
if d.Action == "open_long" || d.Action == "open_short" {
logger.Warnf("🛡️ [%s] Safe mode: BLOCKED %s %s (no new positions allowed)", at.name, d.Action, d.Symbol)
at.logWarnf("🛡️ Safe mode: BLOCKED %s %s (no new positions allowed)", d.Action, d.Symbol)
continue
}
filtered = append(filtered, d)
}
sortedDecisions = filtered
if len(sortedDecisions) == 0 {
logger.Infof("🛡️ [%s] Safe mode: all decisions were open positions, nothing to execute", at.name)
at.logInfof("🛡️ Safe mode: all decisions were open positions, nothing to execute")
}
}
@@ -246,7 +246,7 @@ func (at *AutoTrader) runCycle() error {
running = at.isRunning
at.isRunningMutex.RUnlock()
if !running {
logger.Infof("⏹ Trader stopped during decision execution, aborting remaining decisions")
at.logInfof("⏹ Trader stopped during decision execution, aborting remaining decisions")
break
}
@@ -265,7 +265,7 @@ func (at *AutoTrader) runCycle() error {
}
if err := at.executeDecisionWithRecord(&d, &actionRecord); err != nil {
logger.Infof("❌ Failed to execute decision (%s %s): %v", d.Symbol, d.Action, err)
at.logErrorf("❌ Failed to execute decision (%s %s): %v", d.Symbol, d.Action, err)
actionRecord.Error = err.Error()
record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("❌ %s %s failed: %v", d.Symbol, d.Action, err))
} else {
@@ -280,7 +280,7 @@ func (at *AutoTrader) runCycle() error {
// 9. Save decision record
if err := at.saveDecision(record); err != nil {
logger.Infof("⚠ Failed to save decision record: %v", err)
at.logWarnf("⚠ Failed to save decision record: %v", err)
}
return nil
@@ -417,12 +417,12 @@ 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 {
logger.Infof("⚠️ [%s] No strategy engine configured, skipping candidate coins", at.name)
at.logWarnf("⚠️ No strategy engine configured, skipping candidate coins")
} 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)
at.logWarnf("⚠️ Failed to get candidate coins: %v (will use empty list)", err)
} else {
candidateCoins = coins
logger.Infof("📋 [%s] Strategy engine fetched candidate coins: %d", at.name, len(candidateCoins))
@@ -473,7 +473,7 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
// Get recent 10 closed trades for AI context
recentTrades, err := at.store.Position().GetRecentTrades(at.id, 10)
if err != nil {
logger.Infof("⚠️ [%s] Failed to get recent trades: %v", at.name, err)
at.logWarnf("⚠️ Failed to get recent trades: %v", err)
} else {
logger.Infof("📊 [%s] Found %d recent closed trades for AI context", at.name, len(recentTrades))
for _, trade := range recentTrades {
@@ -503,11 +503,11 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
// Get trading statistics for AI context
stats, err := at.store.Position().GetFullStats(at.id)
if err != nil {
logger.Infof("⚠️ [%s] Failed to get trading stats: %v", at.name, err)
at.logWarnf("⚠️ Failed to get trading stats: %v", err)
} else if stats == nil {
logger.Infof("⚠️ [%s] GetFullStats returned nil", at.name)
at.logWarnf("⚠️ GetFullStats returned nil")
} else if stats.TotalTrades == 0 {
logger.Infof("⚠️ [%s] GetFullStats returned 0 trades (traderID=%s)", at.name, at.id)
at.logWarnf("⚠️ GetFullStats returned 0 trades")
} else {
ctx.TradingStats = &kernel.TradingStats{
TotalTrades: stats.TotalTrades,
@@ -523,7 +523,7 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
at.name, stats.TotalTrades, stats.WinRate, stats.ProfitFactor, stats.SharpeRatio, stats.MaxDrawdownPct)
}
} else {
logger.Infof("⚠️ [%s] Store is nil, cannot get recent trades", at.name)
at.logWarnf("⚠️ Store is nil, cannot get recent trades")
}
// 8. Get quantitative data (if enabled in strategy config)
@@ -630,15 +630,15 @@ func (at *AutoTrader) checkClaw402Balance() {
if at.claw402WalletAddr != "" {
balance, err := wallet.QueryUSDCBalance(at.claw402WalletAddr)
if err != nil {
logger.Warnf("⚠️ [%s] Failed to query USDC balance: %v", at.name, err)
at.logWarnf("⚠️ Failed to query USDC balance: %v", err)
return
}
if balance < 1.0 {
logger.Warnf("⚠️ [%s] Low USDC balance: $%.2f — AI may stop soon!", at.name, balance)
at.logWarnf("⚠️ Low USDC balance: $%.2f — AI may stop soon!", balance)
}
if balance <= 0 {
logger.Errorf("🚨 [%s] USDC balance is ZERO — AI calls will fail!", at.name)
at.logErrorf("🚨 USDC balance is ZERO — AI calls will fail!")
}
runway := float64(0)