diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 7f755b68..dddd011c 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -148,6 +148,9 @@ type AutoTrader struct { 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 diff --git a/trader/auto_trader_loop.go b/trader/auto_trader_loop.go index 0242e3bf..ae440699 100644 --- a/trader/auto_trader_loop.go +++ b/trader/auto_trader_loop.go @@ -124,9 +124,20 @@ func (at *AutoTrader) runCycle() error { } if err != nil { + at.consecutiveAIFailures++ record.Success = false record.ErrorMessage = fmt.Sprintf("Failed to get AI decision: %v", err) + // Activate safe mode after 3 consecutive failures + 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) + } + // Print system prompt and AI chain of thought (output even with errors for debugging) if aiDecision != nil { logger.Info("\n" + strings.Repeat("=", 70) + "\n") @@ -145,9 +156,27 @@ func (at *AutoTrader) runCycle() error { } at.saveDecision(record) + + // 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) + return nil + } + return fmt.Errorf("failed to get AI decision: %w", err) } + // 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.consecutiveAIFailures = 0 + if at.safeMode { + logger.Infof("🛡️ [%s] SAFE MODE DEACTIVATED — AI is working again. Resuming normal trading.", at.name) + at.safeMode = false + at.safeModeReason = "" + } + // // 5. Print system prompt // logger.Infof("\n" + strings.Repeat("=", 70)) // logger.Infof("📋 System prompt [template: %s]", at.systemPromptTemplate) @@ -194,6 +223,22 @@ func (at *AutoTrader) runCycle() error { return nil } + // Safe mode: filter out open positions, only allow close/hold + if at.safeMode { + 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) + 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) + } + } + // Execute decisions and record results for _, d := range sortedDecisions { // Check if trader is stopped before each decision (allow immediate stop during execution)