package api import ( "encoding/json" "fmt" "net/http" "nofx/decision" "nofx/market" "nofx/mcp" "nofx/store" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // handleGetStrategies Get strategy list func (s *Server) handleGetStrategies(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } strategies, err := s.store.Strategy().List(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get strategy list: " + err.Error()}) return } // Convert to frontend format result := make([]gin.H, 0, len(strategies)) for _, st := range strategies { var config store.StrategyConfig json.Unmarshal([]byte(st.Config), &config) result = append(result, gin.H{ "id": st.ID, "name": st.Name, "description": st.Description, "is_active": st.IsActive, "is_default": st.IsDefault, "config": config, "created_at": st.CreatedAt, "updated_at": st.UpdatedAt, }) } c.JSON(http.StatusOK, gin.H{ "strategies": result, }) } // handleGetStrategy Get single strategy func (s *Server) handleGetStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } strategy, err := s.store.Strategy().Get(userID, strategyID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Strategy not found"}) return } var config store.StrategyConfig json.Unmarshal([]byte(strategy.Config), &config) c.JSON(http.StatusOK, gin.H{ "id": strategy.ID, "name": strategy.Name, "description": strategy.Description, "is_active": strategy.IsActive, "is_default": strategy.IsDefault, "config": config, "created_at": strategy.CreatedAt, "updated_at": strategy.UpdatedAt, }) } // handleCreateStrategy Create strategy func (s *Server) handleCreateStrategy(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } var req struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Config store.StrategyConfig `json:"config" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } // Serialize configuration configJSON, err := json.Marshal(req.Config) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"}) return } strategy := &store.Strategy{ ID: uuid.New().String(), UserID: userID, Name: req.Name, Description: req.Description, IsActive: false, IsDefault: false, Config: string(configJSON), } if err := s.store.Strategy().Create(strategy); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create strategy: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "id": strategy.ID, "message": "Strategy created successfully", }) } // handleUpdateStrategy Update strategy func (s *Server) handleUpdateStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Check if it's a system default strategy existing, err := s.store.Strategy().Get(userID, strategyID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Strategy not found"}) return } if existing.IsDefault { c.JSON(http.StatusForbidden, gin.H{"error": "Cannot modify system default strategy"}) return } var req struct { Name string `json:"name"` Description string `json:"description"` Config store.StrategyConfig `json:"config"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } // Serialize configuration configJSON, err := json.Marshal(req.Config) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"}) return } strategy := &store.Strategy{ ID: strategyID, UserID: userID, Name: req.Name, Description: req.Description, Config: string(configJSON), } if err := s.store.Strategy().Update(strategy); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update strategy: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Strategy updated successfully"}) } // handleDeleteStrategy Delete strategy func (s *Server) handleDeleteStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } if err := s.store.Strategy().Delete(userID, strategyID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete strategy: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Strategy deleted successfully"}) } // handleActivateStrategy Activate strategy func (s *Server) handleActivateStrategy(c *gin.Context) { userID := c.GetString("user_id") strategyID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } if err := s.store.Strategy().SetActive(userID, strategyID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to activate strategy: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Strategy activated successfully"}) } // handleDuplicateStrategy Duplicate strategy func (s *Server) handleDuplicateStrategy(c *gin.Context) { userID := c.GetString("user_id") sourceID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } var req struct { Name string `json:"name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } newID := uuid.New().String() if err := s.store.Strategy().Duplicate(userID, sourceID, newID, req.Name); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to duplicate strategy: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "id": newID, "message": "Strategy duplicated successfully", }) } // handleGetActiveStrategy Get currently active strategy func (s *Server) handleGetActiveStrategy(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } strategy, err := s.store.Strategy().GetActive(userID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "No active strategy"}) return } var config store.StrategyConfig json.Unmarshal([]byte(strategy.Config), &config) c.JSON(http.StatusOK, gin.H{ "id": strategy.ID, "name": strategy.Name, "description": strategy.Description, "is_active": strategy.IsActive, "is_default": strategy.IsDefault, "config": config, "created_at": strategy.CreatedAt, "updated_at": strategy.UpdatedAt, }) } // handleGetDefaultStrategyConfig Get default strategy configuration template func (s *Server) handleGetDefaultStrategyConfig(c *gin.Context) { // Return default configuration structure for frontend to use when creating new strategies defaultConfig := store.StrategyConfig{ CoinSource: store.CoinSourceConfig{ SourceType: "coinpool", UseCoinPool: true, CoinPoolLimit: 30, UseOITop: true, OITopLimit: 20, StaticCoins: []string{}, }, Indicators: store.IndicatorConfig{ Klines: store.KlineConfig{ PrimaryTimeframe: "5m", PrimaryCount: 30, LongerTimeframe: "4h", LongerCount: 10, EnableMultiTimeframe: true, SelectedTimeframes: []string{"5m", "15m", "1h", "4h"}, }, EnableEMA: true, EnableMACD: true, EnableRSI: true, EnableATR: true, EnableVolume: true, EnableOI: true, EnableFundingRate: true, EMAPeriods: []int{20, 50}, RSIPeriods: []int{7, 14}, ATRPeriods: []int{14}, }, RiskControl: store.RiskControlConfig{ MaxPositions: 3, BTCETHMaxLeverage: 5, AltcoinMaxLeverage: 5, MinRiskRewardRatio: 3.0, MaxMarginUsage: 0.9, MaxPositionRatio: 1.5, MinPositionSize: 12, MinConfidence: 75, }, PromptSections: store.PromptSectionsConfig{ RoleDefinition: `# You are a professional cryptocurrency trading AI You focus on technical analysis and risk management, making rational trading decisions based on market data. Your goal is to capture high-probability trading opportunities while controlling risk.`, TradingFrequency: `# ⏱️ Trading Frequency Awareness - Excellent traders: 2-4 trades per day ≈ 0.1-0.2 trades per hour - >2 trades per hour = overtrading - Single position holding time ≥30-60 minutes If you find yourself trading every cycle → standards too low; if closing positions <30 minutes → too impatient.`, EntryStandards: `# 🎯 Entry Standards (Strict) Only enter when multiple signals align: - Clear trend direction (EMA alignment, price position) - Momentum confirmation (MACD, RSI cooperation) - Moderate volatility (ATR reasonable range) - Volume-price coordination (volume supports direction) Avoid: single indicator, conflicting signals, sideways consolidation, reopening immediately after closing.`, DecisionProcess: `# 📋 Decision Process 1. Check positions → Should take profit/stop loss 2. Scan candidate coins + multiple timeframes → Are there strong signals 3. Evaluate risk-reward ratio → Does it meet minimum requirements 4. Write chain of thought first, then output structured JSON`, }, } c.JSON(http.StatusOK, defaultConfig) } // handlePreviewPrompt Preview prompt generated by strategy func (s *Server) handlePreviewPrompt(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } var req struct { Config store.StrategyConfig `json:"config" binding:"required"` AccountEquity float64 `json:"account_equity"` PromptVariant string `json:"prompt_variant"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } // Use default values if req.AccountEquity <= 0 { req.AccountEquity = 1000.0 // Default simulated account equity } if req.PromptVariant == "" { req.PromptVariant = "balanced" } // Create strategy engine to build prompt engine := decision.NewStrategyEngine(&req.Config) // Build system prompt (using built-in method from strategy engine) systemPrompt := engine.BuildSystemPrompt( req.AccountEquity, req.PromptVariant, ) // Get list of available prompt templates templateNames := decision.GetAllPromptTemplateNames() c.JSON(http.StatusOK, gin.H{ "system_prompt": systemPrompt, "prompt_variant": req.PromptVariant, "available_templates": templateNames, "config_summary": gin.H{ "coin_source": req.Config.CoinSource.SourceType, "primary_tf": req.Config.Indicators.Klines.PrimaryTimeframe, "btc_eth_leverage": req.Config.RiskControl.BTCETHMaxLeverage, "altcoin_leverage": req.Config.RiskControl.AltcoinMaxLeverage, "max_positions": req.Config.RiskControl.MaxPositions, }, }) } // handleStrategyTestRun AI test run (does not execute trades, only returns AI analysis results) func (s *Server) handleStrategyTestRun(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } var req struct { Config store.StrategyConfig `json:"config" binding:"required"` PromptVariant string `json:"prompt_variant"` AIModelID string `json:"ai_model_id"` RunRealAI bool `json:"run_real_ai"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) return } if req.PromptVariant == "" { req.PromptVariant = "balanced" } // Create strategy engine to build prompt engine := decision.NewStrategyEngine(&req.Config) // Get candidate coins candidates, err := engine.GetCandidateCoins() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get candidate coins: " + err.Error(), "ai_response": "", }) return } // Get timeframe configuration timeframes := req.Config.Indicators.Klines.SelectedTimeframes primaryTimeframe := req.Config.Indicators.Klines.PrimaryTimeframe klineCount := req.Config.Indicators.Klines.PrimaryCount // If no timeframes selected, use default values if len(timeframes) == 0 { // Backward compatibility: use primary and longer timeframes if primaryTimeframe != "" { timeframes = append(timeframes, primaryTimeframe) } else { timeframes = append(timeframes, "3m") } if req.Config.Indicators.Klines.LongerTimeframe != "" { timeframes = append(timeframes, req.Config.Indicators.Klines.LongerTimeframe) } } if primaryTimeframe == "" { primaryTimeframe = timeframes[0] } if klineCount <= 0 { klineCount = 30 } fmt.Printf("📊 Using timeframes: %v, primary: %s, kline count: %d\n", timeframes, primaryTimeframe, klineCount) // Get real market data (using multiple timeframes) marketDataMap := make(map[string]*market.Data) for _, coin := range candidates { data, err := market.GetWithTimeframes(coin.Symbol, timeframes, primaryTimeframe, klineCount) if err != nil { // If getting data for a coin fails, log but continue fmt.Printf("⚠️ Failed to get market data for %s: %v\n", coin.Symbol, err) continue } marketDataMap[coin.Symbol] = data } // Build real context (for generating User Prompt) testContext := &decision.Context{ CurrentTime: time.Now().Format("2006-01-02 15:04:05"), RuntimeMinutes: 0, CallCount: 1, Account: decision.AccountInfo{ TotalEquity: 1000.0, AvailableBalance: 1000.0, UnrealizedPnL: 0, TotalPnL: 0, TotalPnLPct: 0, MarginUsed: 0, MarginUsedPct: 0, PositionCount: 0, }, Positions: []decision.PositionInfo{}, CandidateCoins: candidates, PromptVariant: req.PromptVariant, MarketDataMap: marketDataMap, } // Build System Prompt systemPrompt := engine.BuildSystemPrompt(1000.0, req.PromptVariant) // Build User Prompt (using real market data) userPrompt := engine.BuildUserPrompt(testContext) // If requesting real AI call if req.RunRealAI && req.AIModelID != "" { aiResponse, aiErr := s.runRealAITest(userID, req.AIModelID, systemPrompt, userPrompt) if aiErr != nil { c.JSON(http.StatusOK, gin.H{ "system_prompt": systemPrompt, "user_prompt": userPrompt, "candidate_count": len(candidates), "candidates": candidates, "prompt_variant": req.PromptVariant, "ai_response": fmt.Sprintf("❌ AI call failed: %s", aiErr.Error()), "ai_error": aiErr.Error(), "note": "AI call error", }) return } c.JSON(http.StatusOK, gin.H{ "system_prompt": systemPrompt, "user_prompt": userPrompt, "candidate_count": len(candidates), "candidates": candidates, "prompt_variant": req.PromptVariant, "ai_response": aiResponse, "note": "✅ Real AI test run successful", }) return } // Return result (without actually calling AI, only return built prompt) c.JSON(http.StatusOK, gin.H{ "system_prompt": systemPrompt, "user_prompt": userPrompt, "candidate_count": len(candidates), "candidates": candidates, "prompt_variant": req.PromptVariant, "ai_response": "Please select an AI model and click 'Run Test' to perform real AI analysis.", "note": "AI model not selected or real AI call not enabled", }) } // runRealAITest Execute real AI test call func (s *Server) runRealAITest(userID, modelID, systemPrompt, userPrompt string) (string, error) { // Get AI model configuration model, err := s.store.AIModel().Get(userID, modelID) if err != nil { return "", fmt.Errorf("failed to get AI model: %w", err) } if !model.Enabled { return "", fmt.Errorf("AI model %s is not enabled", model.Name) } if model.APIKey == "" { return "", fmt.Errorf("AI model %s is missing API Key", model.Name) } // Create AI client var aiClient mcp.AIClient provider := model.Provider switch provider { case "qwen": aiClient = mcp.NewQwenClient() aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName) case "deepseek": aiClient = mcp.NewDeepSeekClient() aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName) default: // Use generic client aiClient = mcp.NewClient() aiClient.SetAPIKey(model.APIKey, model.CustomAPIURL, model.CustomModelName) } // Call AI API response, err := aiClient.CallWithMessages(systemPrompt, userPrompt) if err != nil { return "", fmt.Errorf("AI API call failed: %w", err) } return response, nil }