diff --git a/README.md b/README.md index 7df3e38e..5741acfd 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ Also compatible with **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** | **Multi-AI** | DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — switch anytime | | **Multi-Exchange** | Binance, Bybit, OKX, Bitget, KuCoin, Gate, Hyperliquid, Aster, Lighter | | **Strategy Studio** | Visual builder — coin sources, indicators, risk controls | -| **AI Debate Arena** | Multiple AIs debate trades (Bull vs Bear vs Analyst), vote, execute | | **AI Competition** | AIs compete in real-time, leaderboard ranks performance | | **Telegram Agent** | Chat with your trading assistant — streaming, tool calling, memory | | **Backtest Lab** | Historical simulation with equity curves and performance metrics | @@ -166,14 +165,6 @@ Crypto · US Stocks · Forex · Metals | | | -
-Debate Arena - -| AI Debate Session | Create Debate | -|:---:|:---:| -| | | -
- --- ## Install @@ -262,8 +253,8 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas ├─────────────────────────────────────────────────┤ │ API Server (Go) │ ├──────────┬──────────┬──────────┬────────────────┤ - │ Strategy │ Debate │ Backtest │ Telegram │ - │ Engine │ Arena │ Lab │ Agent │ + │ Strategy │ Backtest │ Telegram │ + │ Engine │ Lab │ Agent │ ├──────────┴──────────┴──────────┴────────────────┤ │ MCP AI Client Layer │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ @@ -287,7 +278,6 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas | [Architecture](docs/architecture/README.md) | System design and module index | | [Strategy Module](docs/architecture/STRATEGY_MODULE.md) | Coin selection, AI prompts, execution | | [Backtest Module](docs/architecture/BACKTEST_MODULE.md) | Historical simulation, metrics | -| [Debate Module](docs/architecture/DEBATE_MODULE.md) | Multi-AI debate, consensus voting | | [FAQ](docs/faq/README.md) | Common questions | | [Getting Started](docs/getting-started/README.md) | Deployment guide | diff --git a/api/debate.go b/api/debate.go deleted file mode 100644 index 89b31972..00000000 --- a/api/debate.go +++ /dev/null @@ -1,635 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "sync" - - "nofx/debate" - "nofx/logger" - "nofx/provider/nofxos" - "nofx/store" - - "github.com/gin-gonic/gin" -) - -// DebateHandler handles debate-related API requests -type DebateHandler struct { - debateStore *store.DebateStore - strategyStore *store.StrategyStore - aiModelStore *store.AIModelStore - engine *debate.DebateEngine - - // Trader manager for execution - traderManager DebateTraderManager - - // SSE subscribers - subscribers map[string]map[chan []byte]bool // sessionID -> channels - subscribersMu sync.RWMutex -} - -// DebateTraderManager interface for getting trader executors -type DebateTraderManager interface { - GetTraderExecutor(traderID string) (debate.TraderExecutor, error) -} - -// NewDebateHandler creates a new DebateHandler -func NewDebateHandler(debateStore *store.DebateStore, strategyStore *store.StrategyStore, aiModelStore *store.AIModelStore) *DebateHandler { - handler := &DebateHandler{ - debateStore: debateStore, - strategyStore: strategyStore, - aiModelStore: aiModelStore, - subscribers: make(map[string]map[chan []byte]bool), - } - - // Create debate engine with event callbacks - handler.engine = debate.NewDebateEngine(debateStore, strategyStore, aiModelStore) - handler.engine.OnRoundStart = handler.broadcastRoundStart - handler.engine.OnMessage = handler.broadcastMessage - handler.engine.OnRoundEnd = handler.broadcastRoundEnd - handler.engine.OnVote = handler.broadcastVote - handler.engine.OnConsensus = handler.broadcastConsensus - handler.engine.OnError = handler.broadcastError - - return handler -} - -// CreateDebateRequest represents a request to create a new debate -type CreateDebateRequest struct { - Name string `json:"name" binding:"required"` - StrategyID string `json:"strategy_id" binding:"required"` - Symbol string `json:"symbol"` // Optional: auto-selected based on strategy if empty - MaxRounds int `json:"max_rounds"` - IntervalMinutes int `json:"interval_minutes"` - PromptVariant string `json:"prompt_variant"` - AutoExecute bool `json:"auto_execute"` - TraderID string `json:"trader_id"` - Participants []ParticipantConfig `json:"participants" binding:"required,min=2"` - // OI Ranking data options - EnableOIRanking bool `json:"enable_oi_ranking"` // Whether to include OI ranking data - OIRankingLimit int `json:"oi_ranking_limit"` // Number of OI ranking entries (default 10) - OIDuration string `json:"oi_duration"` // Duration for OI data (1h, 4h, 24h, etc.) -} - -// ParticipantConfig represents a participant configuration -type ParticipantConfig struct { - AIModelID string `json:"ai_model_id" binding:"required"` - Personality string `json:"personality" binding:"required"` -} - -// HandleListDebates lists all debates for a user -func (h *DebateHandler) HandleListDebates(c *gin.Context) { - userID := c.GetString("user_id") - if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) - return - } - - sessions, err := h.debateStore.GetSessionsByUser(userID) - if err != nil { - logger.Errorf("Failed to get debates for user %s: %v", userID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get debates"}) - return - } - - // Return empty array instead of null - if sessions == nil { - sessions = []*store.DebateSession{} - } - - c.JSON(http.StatusOK, sessions) -} - -// HandleGetDebate gets a specific debate with all details -func (h *DebateHandler) HandleGetDebate(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - session, err := h.debateStore.GetSessionWithDetails(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - // Check ownership - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - c.JSON(http.StatusOK, session) -} - -// HandleCreateDebate creates a new debate -func (h *DebateHandler) HandleCreateDebate(c *gin.Context) { - userID := c.GetString("user_id") - if userID == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) - return - } - - var req CreateDebateRequest - if err := c.ShouldBindJSON(&req); err != nil { - SafeBadRequest(c, "Invalid request parameters") - return - } - - // Validate strategy exists - strategy, err := h.strategyStore.Get(userID, req.StrategyID) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "strategy not found"}) - return - } - - // Validate strategy belongs to user or is default - if strategy.UserID != userID && !strategy.IsDefault { - c.JSON(http.StatusForbidden, gin.H{"error": "strategy access denied"}) - return - } - - // Auto-select symbol based on strategy if not provided - if req.Symbol == "" { - req.Symbol = "BTCUSDT" // default fallback - if strategyConfig, err := strategy.ParseConfig(); err == nil { - coinSource := strategyConfig.CoinSource - switch coinSource.SourceType { - case "static": - if len(coinSource.StaticCoins) > 0 { - req.Symbol = coinSource.StaticCoins[0] - } - case "ai500": - // Fetch from AI500 API - if coins, err := nofxos.DefaultClient().GetTopRatedCoins(1); err == nil && len(coins) > 0 { - req.Symbol = coins[0] - logger.Infof("Fetched coin from AI500 API: %s", req.Symbol) - } - case "oi_top": - // Fetch from OI top API - if coins, err := nofxos.DefaultClient().GetOITopSymbols(); err == nil && len(coins) > 0 { - req.Symbol = coins[0] - logger.Infof("Fetched coin from OI Top API: %s", req.Symbol) - } - case "mixed": - // Try AI500 first, then OI top - if coinSource.UseAI500 { - if coins, err := nofxos.DefaultClient().GetTopRatedCoins(1); err == nil && len(coins) > 0 { - req.Symbol = coins[0] - logger.Infof("Fetched coin from AI500 API (mixed): %s", req.Symbol) - } - } else if coinSource.UseOITop { - if coins, err := nofxos.DefaultClient().GetOITopSymbols(); err == nil && len(coins) > 0 { - req.Symbol = coins[0] - logger.Infof("Fetched coin from OI Top API (mixed): %s", req.Symbol) - } - } - } - logger.Infof("Auto-selected symbol %s for debate based on strategy %s (source_type=%s)", - req.Symbol, strategy.Name, coinSource.SourceType) - } - } - - // Set defaults - if req.MaxRounds <= 0 || req.MaxRounds > 5 { - req.MaxRounds = 3 - } - if req.IntervalMinutes <= 0 { - req.IntervalMinutes = 5 - } - if req.PromptVariant == "" { - req.PromptVariant = "balanced" - } - - // Create session - session := &store.DebateSession{ - UserID: userID, - Name: req.Name, - StrategyID: req.StrategyID, - Symbol: req.Symbol, - MaxRounds: req.MaxRounds, - IntervalMinutes: req.IntervalMinutes, - PromptVariant: req.PromptVariant, - AutoExecute: req.AutoExecute, - TraderID: req.TraderID, - EnableOIRanking: req.EnableOIRanking, - OIRankingLimit: req.OIRankingLimit, - OIDuration: req.OIDuration, - } - - if err := h.debateStore.CreateSession(session); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create debate"}) - return - } - - // Add participants - for i, p := range req.Participants { - // Validate AI model exists and belongs to user - aiModel, err := h.aiModelStore.GetByID(p.AIModelID) - if err != nil { - logger.Warnf("AI model not found: %s", p.AIModelID) - continue - } - if aiModel.UserID != userID { - logger.Warnf("AI model %s does not belong to user", p.AIModelID) - continue - } - - // Validate personality - personality := store.DebatePersonality(p.Personality) - if _, ok := store.PersonalityColors[personality]; !ok { - personality = store.PersonalityAnalyst - } - - participant := &store.DebateParticipant{ - SessionID: session.ID, - AIModelID: p.AIModelID, - AIModelName: aiModel.Name, - Provider: aiModel.Provider, - Personality: personality, - Color: store.PersonalityColors[personality], - SpeakOrder: i, - } - - if err := h.debateStore.AddParticipant(participant); err != nil { - logger.Errorf("Failed to add participant: %v", err) - } - } - - // Get full session with participants - fullSession, _ := h.debateStore.GetSessionWithDetails(session.ID) - - c.JSON(http.StatusCreated, fullSession) -} - -// HandleStartDebate starts a debate -func (h *DebateHandler) HandleStartDebate(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - session, err := h.debateStore.GetSession(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - if session.Status != store.DebateStatusPending { - c.JSON(http.StatusBadRequest, gin.H{"error": "debate is not in pending status"}) - return - } - - // Start debate asynchronously - if err := h.engine.StartDebate(debateID); err != nil { - SafeInternalError(c, "Start debate", err) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "debate started", "id": debateID}) -} - -// HandleCancelDebate cancels a running debate -func (h *DebateHandler) HandleCancelDebate(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - session, err := h.debateStore.GetSession(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - if err := h.engine.CancelDebate(debateID); err != nil { - SafeInternalError(c, "Cancel debate", err) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "debate cancelled"}) -} - -// HandleDeleteDebate deletes a debate -func (h *DebateHandler) HandleDeleteDebate(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - session, err := h.debateStore.GetSession(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - // Don't allow deleting running debates - if session.Status == store.DebateStatusRunning || session.Status == store.DebateStatusVoting { - c.JSON(http.StatusBadRequest, gin.H{"error": "cannot delete running debate"}) - return - } - - if err := h.debateStore.DeleteSession(debateID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete debate"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "debate deleted"}) -} - -// HandleGetMessages gets all messages for a debate -func (h *DebateHandler) HandleGetMessages(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - session, err := h.debateStore.GetSession(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - messages, err := h.debateStore.GetMessages(debateID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get messages"}) - return - } - - c.JSON(http.StatusOK, messages) -} - -// HandleGetVotes gets all votes for a debate -func (h *DebateHandler) HandleGetVotes(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - session, err := h.debateStore.GetSession(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - votes, err := h.debateStore.GetVotes(debateID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get votes"}) - return - } - - c.JSON(http.StatusOK, votes) -} - -// HandleDebateStream handles SSE streaming for live debate updates -func (h *DebateHandler) HandleDebateStream(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - session, err := h.debateStore.GetSession(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - // Set SSE headers - c.Header("Content-Type", "text/event-stream") - c.Header("Cache-Control", "no-cache") - c.Header("Connection", "keep-alive") - c.Header("Transfer-Encoding", "chunked") - - // Create channel for this subscriber - ch := make(chan []byte, 100) - h.addSubscriber(debateID, ch) - defer h.removeSubscriber(debateID, ch) - - // Send initial state - initialState, _ := h.debateStore.GetSessionWithDetails(debateID) - initialData, _ := json.Marshal(map[string]interface{}{ - "event": "initial", - "data": initialState, - }) - c.Writer.Write([]byte(fmt.Sprintf("event: initial\ndata: %s\n\n", initialData))) - c.Writer.Flush() - - // Stream updates - clientGone := c.Request.Context().Done() - for { - select { - case <-clientGone: - return - case msg := <-ch: - c.Writer.Write(msg) - c.Writer.Flush() - } - } -} - -// SetTraderManager sets the trader manager for executing trades -func (h *DebateHandler) SetTraderManager(tm DebateTraderManager) { - h.traderManager = tm -} - -// ExecuteDebateRequest represents a request to execute a debate's consensus -type ExecuteDebateRequest struct { - TraderID string `json:"trader_id" binding:"required"` -} - -// HandleExecuteDebate executes the consensus decision from a completed debate -func (h *DebateHandler) HandleExecuteDebate(c *gin.Context) { - debateID := c.Param("id") - userID := c.GetString("user_id") - - // Check trader manager is available - if h.traderManager == nil { - c.JSON(http.StatusServiceUnavailable, gin.H{"error": "trading service not available"}) - return - } - - // Get debate session - session, err := h.debateStore.GetSession(debateID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "debate not found"}) - return - } - - // Check ownership - if session.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) - return - } - - // Check status - if session.Status != store.DebateStatusCompleted { - c.JSON(http.StatusBadRequest, gin.H{"error": "debate is not completed"}) - return - } - - // Parse request - var req ExecuteDebateRequest - if err := c.ShouldBindJSON(&req); err != nil { - SafeBadRequest(c, "Invalid request parameters") - return - } - - // Get trader executor - executor, err := h.traderManager.GetTraderExecutor(req.TraderID) - if err != nil { - SafeError(c, http.StatusBadRequest, "Trader not available", err) - return - } - - // Execute consensus - if err := h.engine.ExecuteConsensus(debateID, executor); err != nil { - SafeInternalError(c, "Execute consensus", err) - return - } - - // Get updated session - updatedSession, _ := h.debateStore.GetSessionWithDetails(debateID) - - c.JSON(http.StatusOK, gin.H{ - "message": "consensus executed successfully", - "session": updatedSession, - }) -} - -// GetPersonalities returns available AI personalities -func (h *DebateHandler) HandleGetPersonalities(c *gin.Context) { - personalities := []map[string]interface{}{ - { - "id": "bull", - "name": "Aggressive Bull", - "emoji": "🐂", - "color": store.PersonalityColors[store.PersonalityBull], - "description": "Looks for long opportunities, optimistic about market", - }, - { - "id": "bear", - "name": "Cautious Bear", - "emoji": "🐻", - "color": store.PersonalityColors[store.PersonalityBear], - "description": "Skeptical, focuses on risks and short opportunities", - }, - { - "id": "analyst", - "name": "Data Analyst", - "emoji": "📊", - "color": store.PersonalityColors[store.PersonalityAnalyst], - "description": "Pure technical analysis, neutral and data-driven", - }, - { - "id": "contrarian", - "name": "Contrarian", - "emoji": "🔄", - "color": store.PersonalityColors[store.PersonalityContrarian], - "description": "Challenges majority opinion, looks for overlooked opportunities", - }, - { - "id": "risk_manager", - "name": "Risk Manager", - "emoji": "🛡️", - "color": store.PersonalityColors[store.PersonalityRiskManager], - "description": "Focuses on position sizing, stop losses, and risk control", - }, - } - c.JSON(http.StatusOK, personalities) -} - -// SSE broadcast helpers -func (h *DebateHandler) addSubscriber(sessionID string, ch chan []byte) { - h.subscribersMu.Lock() - defer h.subscribersMu.Unlock() - - if h.subscribers[sessionID] == nil { - h.subscribers[sessionID] = make(map[chan []byte]bool) - } - h.subscribers[sessionID][ch] = true -} - -func (h *DebateHandler) removeSubscriber(sessionID string, ch chan []byte) { - h.subscribersMu.Lock() - defer h.subscribersMu.Unlock() - - if h.subscribers[sessionID] != nil { - delete(h.subscribers[sessionID], ch) - close(ch) - } -} - -func (h *DebateHandler) broadcast(sessionID string, event string, data interface{}) { - h.subscribersMu.RLock() - defer h.subscribersMu.RUnlock() - - subs := h.subscribers[sessionID] - if subs == nil { - return - } - - jsonData, err := json.Marshal(data) - if err != nil { - return - } - - msg := []byte(fmt.Sprintf("event: %s\ndata: %s\n\n", event, jsonData)) - for ch := range subs { - select { - case ch <- msg: - default: - // Channel full, skip - } - } -} - -func (h *DebateHandler) broadcastRoundStart(sessionID string, round int) { - h.broadcast(sessionID, "round_start", map[string]interface{}{ - "round": round, - "status": "running", - }) -} - -func (h *DebateHandler) broadcastMessage(sessionID string, msg *store.DebateMessage) { - h.broadcast(sessionID, "message", msg) -} - -func (h *DebateHandler) broadcastRoundEnd(sessionID string, round int) { - h.broadcast(sessionID, "round_end", map[string]interface{}{ - "round": round, - "status": "completed", - }) -} - -func (h *DebateHandler) broadcastVote(sessionID string, vote *store.DebateVote) { - h.broadcast(sessionID, "vote", vote) -} - -func (h *DebateHandler) broadcastConsensus(sessionID string, decision *store.DebateDecision) { - h.broadcast(sessionID, "consensus", decision) -} - -func (h *DebateHandler) broadcastError(sessionID string, err error) { - // Sanitize error message before broadcasting to client - safeMsg := SanitizeError(err, "An error occurred during debate") - h.broadcast(sessionID, "error", map[string]interface{}{ - "error": safeMsg, - }) -} diff --git a/api/server.go b/api/server.go index 74813fdd..c87cf936 100644 --- a/api/server.go +++ b/api/server.go @@ -45,7 +45,6 @@ type Server struct { store *store.Store cryptoHandler *CryptoHandler backtestManager *backtest.Manager - debateHandler *DebateHandler httpServer *http.Server port int telegramReloadCh chan<- struct{} // signal Telegram bot to reload @@ -64,21 +63,12 @@ func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoServ // Create crypto handler cryptoHandler := NewCryptoHandler(cryptoService) - // Create debate store and handler - debateStore := store.NewDebateStore(st.GormDB()) - if err := debateStore.InitSchema(); err != nil { - logger.Errorf("Failed to initialize debate schema: %v", err) - } - debateHandler := NewDebateHandler(debateStore, st.Strategy(), st.AIModel()) - debateHandler.SetTraderManager(traderManager) - s := &Server{ router: router, traderManager: traderManager, store: st, cryptoHandler: cryptoHandler, backtestManager: backtestManager, - debateHandler: debateHandler, port: port, } @@ -331,19 +321,6 @@ After activating, create or update a trader with this strategy_id to apply it.`, `:id = EXACT id from GET /api/strategies. Creates a copy with " (copy)" appended to the name.`, s.handleDuplicateStrategy) - // Debate Arena - s.route(protected, "GET", "/debates", "List debates", s.debateHandler.HandleListDebates) - s.route(protected, "GET", "/debates/personalities", "Available AI personalities", s.debateHandler.HandleGetPersonalities) - s.route(protected, "GET", "/debates/:id", "Get debate details", s.debateHandler.HandleGetDebate) - s.route(protected, "POST", "/debates", "Create debate", s.debateHandler.HandleCreateDebate) - s.route(protected, "POST", "/debates/:id/start", "Start debate", s.debateHandler.HandleStartDebate) - s.route(protected, "POST", "/debates/:id/cancel", "Cancel debate", s.debateHandler.HandleCancelDebate) - s.route(protected, "POST", "/debates/:id/execute", "Execute debate consensus decision", s.debateHandler.HandleExecuteDebate) - s.route(protected, "DELETE", "/debates/:id", "Delete debate", s.debateHandler.HandleDeleteDebate) - s.route(protected, "GET", "/debates/:id/messages", "Get debate messages", s.debateHandler.HandleGetMessages) - s.route(protected, "GET", "/debates/:id/votes", "Get debate votes", s.debateHandler.HandleGetVotes) - s.route(protected, "GET", "/debates/:id/stream", "SSE stream for live debate", s.debateHandler.HandleDebateStream) - // Data for specified trader (using query parameter ?trader_id=xxx) // IMPORTANT: All ?trader_id= values must be the EXACT "trader_id" field from GET /api/my-traders s.routeWithSchema(protected, "GET", "/status", "Trader running status", diff --git a/debate/engine.go b/debate/engine.go deleted file mode 100644 index 37f8376c..00000000 --- a/debate/engine.go +++ /dev/null @@ -1,1424 +0,0 @@ -package debate - -import ( - "encoding/json" - "fmt" - "regexp" - "strconv" - "strings" - "sync" - "time" - - "nofx/kernel" - "nofx/logger" - "nofx/market" - "nofx/mcp" - "nofx/store" -) - -// TraderExecutor interface for executing trades -type TraderExecutor interface { - ExecuteDecision(decision *kernel.Decision) error - GetBalance() (map[string]interface{}, error) -} - -// DebateEngine orchestrates AI debates using strategy-based market context -type DebateEngine struct { - debateStore *store.DebateStore - strategyStore *store.StrategyStore - aiModelStore *store.AIModelStore - clients map[string]mcp.AIClient - clientsMu sync.RWMutex - - // Event callbacks for SSE streaming - OnRoundStart func(sessionID string, round int) - OnMessage func(sessionID string, msg *store.DebateMessage) - OnRoundEnd func(sessionID string, round int) - OnVote func(sessionID string, vote *store.DebateVote) - OnConsensus func(sessionID string, decision *store.DebateDecision) - OnError func(sessionID string, err error) -} - -// NewDebateEngine creates a new debate engine -func NewDebateEngine(debateStore *store.DebateStore, strategyStore *store.StrategyStore, aiModelStore *store.AIModelStore) *DebateEngine { - engine := &DebateEngine{ - debateStore: debateStore, - strategyStore: strategyStore, - aiModelStore: aiModelStore, - clients: make(map[string]mcp.AIClient), - } - - // Cleanup stale running/voting debates on startup - engine.cleanupStaleDebates() - - return engine -} - -// cleanupStaleDebates marks any running/voting debates as cancelled on startup -func (e *DebateEngine) cleanupStaleDebates() { - sessions, err := e.debateStore.ListAllSessions() - if err != nil { - logger.Warnf("[Debate] Failed to list sessions for cleanup: %v", err) - return - } - - for _, session := range sessions { - if session.Status == store.DebateStatusRunning || session.Status == store.DebateStatusVoting { - logger.Infof("[Debate] Cancelling stale debate: %s (was %s)", session.ID, session.Status) - e.debateStore.UpdateSessionStatus(session.ID, store.DebateStatusCancelled) - } - } -} - -// InitializeClients initializes AI clients for all participants -func (e *DebateEngine) InitializeClients(participants []*store.DebateParticipant) error { - e.clientsMu.Lock() - defer e.clientsMu.Unlock() - - for _, p := range participants { - aiModel, err := e.aiModelStore.GetByID(p.AIModelID) - if err != nil { - return fmt.Errorf("failed to get AI model %s: %w", p.AIModelID, err) - } - - var client mcp.AIClient - switch aiModel.Provider { - case "deepseek": - client = mcp.NewDeepSeekClient() - case "qwen": - client = mcp.NewQwenClient() - case "openai": - client = mcp.NewOpenAIClient() - case "claude": - client = mcp.NewClaudeClient() - case "gemini": - client = mcp.NewGeminiClient() - case "grok": - client = mcp.NewGrokClient() - case "kimi": - client = mcp.NewKimiClient() - case "minimax": - client = mcp.NewMiniMaxClient() - case "blockrun-base": - client = mcp.NewBlockRunBaseClient() - case "blockrun-sol": - client = mcp.NewBlockRunSolClient() - case "claw402": - client = mcp.NewClaw402Client() - default: - client = mcp.New() - } - - // Configure client (convert EncryptedString to string) - client.SetAPIKey(string(aiModel.APIKey), aiModel.CustomAPIURL, aiModel.CustomModelName) - - e.clients[p.AIModelID] = client - } - - return nil -} - -// StartDebate starts a debate session with strategy-based market data -func (e *DebateEngine) StartDebate(sessionID string) error { - // Get session with details - session, err := e.debateStore.GetSessionWithDetails(sessionID) - if err != nil { - return fmt.Errorf("failed to get session: %w", err) - } - - if session.Status != store.DebateStatusPending { - return fmt.Errorf("debate is not in pending status") - } - - if len(session.Participants) < 2 { - return fmt.Errorf("need at least 2 participants") - } - - // Initialize AI clients - if err := e.InitializeClients(session.Participants); err != nil { - return fmt.Errorf("failed to initialize clients: %w", err) - } - - // Get strategy config - strategy, err := e.strategyStore.Get(session.UserID, session.StrategyID) - if err != nil { - return fmt.Errorf("failed to get strategy: %w", err) - } - - strategyConfig, err := strategy.ParseConfig() - if err != nil { - return fmt.Errorf("failed to parse strategy config: %w", err) - } - - // Update status to running - if err := e.debateStore.UpdateSessionStatus(sessionID, store.DebateStatusRunning); err != nil { - return fmt.Errorf("failed to update status: %w", err) - } - - // Run debate asynchronously - go e.runDebate(session, strategyConfig) - - return nil -} - -// runDebate runs the actual debate rounds -func (e *DebateEngine) runDebate(session *store.DebateSessionWithDetails, strategyConfig *store.StrategyConfig) { - defer func() { - if r := recover(); r != nil { - logger.Errorf("Debate panic recovered: %v", r) - e.debateStore.UpdateSessionStatus(session.ID, store.DebateStatusCancelled) - if e.OnError != nil { - e.OnError(session.ID, fmt.Errorf("debate panic: %v", r)) - } - } - }() - - // Create strategy engine for building context - strategyEngine := kernel.NewStrategyEngine(strategyConfig) - - // Build market context using strategy config - ctx, err := e.buildMarketContext(session, strategyEngine) - if err != nil { - logger.Errorf("Failed to build market context: %v", err) - e.debateStore.UpdateSessionStatus(session.ID, store.DebateStatusCancelled) - if e.OnError != nil { - e.OnError(session.ID, err) - } - return - } - - // Build system prompt based on strategy (same as AI Test) - baseSystemPrompt := strategyEngine.BuildSystemPrompt(1000.0, session.PromptVariant) - - // Build user prompt with market data (OI ranking data is included via ctx.OIRankingData) - userPrompt := strategyEngine.BuildUserPrompt(ctx) - - // Run debate rounds - var allMessages []*store.DebateMessage - for round := 1; round <= session.MaxRounds; round++ { - logger.Infof("Starting debate round %d/%d for session %s", round, session.MaxRounds, session.ID) - - if e.OnRoundStart != nil { - e.OnRoundStart(session.ID, round) - } - - e.debateStore.UpdateSessionRound(session.ID, round) - - // Get response from each participant - for i, participant := range session.Participants { - logger.Infof("[Debate] Round %d - Getting response from participant %d/%d: %s (%s)", - round, i+1, len(session.Participants), participant.AIModelName, participant.Provider) - - // Build personality-enhanced system prompt - systemPrompt := e.buildDebateSystemPrompt(baseSystemPrompt, participant, round, session.MaxRounds) - - // Build debate user prompt with previous messages - debateUserPrompt := e.buildDebateUserPrompt(userPrompt, allMessages, participant, round) - - // Get AI response - msg, err := e.getParticipantResponse(session, participant, systemPrompt, debateUserPrompt, round) - if err != nil { - logger.Errorf("[Debate] Failed to get response from %s (%s): %v", participant.AIModelName, participant.Provider, err) - // Send error event to frontend - if e.OnError != nil { - e.OnError(session.ID, fmt.Errorf("%s failed: %v", participant.AIModelName, err)) - } - continue - } - - logger.Infof("[Debate] Got response from %s: %d chars, action=%s, confidence=%d%%", - participant.AIModelName, len(msg.Content), msg.Decision.Action, msg.Confidence) - - // Save message - if err := e.debateStore.AddMessage(msg); err != nil { - logger.Errorf("Failed to save message: %v", err) - } - - allMessages = append(allMessages, msg) - - if e.OnMessage != nil { - e.OnMessage(session.ID, msg) - } - } - - if e.OnRoundEnd != nil { - e.OnRoundEnd(session.ID, round) - } - } - - // Voting phase - logger.Infof("Starting voting phase for session %s", session.ID) - e.debateStore.UpdateSessionStatus(session.ID, store.DebateStatusVoting) - - votes, err := e.collectVotes(session, strategyEngine, allMessages) - if err != nil { - logger.Errorf("Failed to collect votes: %v", err) - } - - // Determine multi-coin consensus - allDecisions := e.determineMultiCoinConsensus(votes) - - // For backward compatibility, also set single consensus - var primaryConsensus *store.DebateDecision - if len(allDecisions) > 0 { - primaryConsensus = allDecisions[0] - // If session has specific symbol, find that decision - if session.Symbol != "" { - for _, d := range allDecisions { - if d.Symbol == session.Symbol { - primaryConsensus = d - break - } - } - } - } else { - primaryConsensus = &store.DebateDecision{ - Action: "hold", - Symbol: session.Symbol, - Confidence: 0, - Reasoning: "No actionable consensus reached", - } - } - - // Store both single and multi-coin decisions - session.FinalDecision = primaryConsensus - session.FinalDecisions = allDecisions - - // Update session with final decisions - e.debateStore.UpdateSessionFinalDecisions(session.ID, primaryConsensus, allDecisions) - e.debateStore.UpdateSessionStatus(session.ID, store.DebateStatusCompleted) - - if e.OnConsensus != nil { - e.OnConsensus(session.ID, primaryConsensus) - } - - logger.Infof("Debate %s completed. %d consensus decisions, primary: %s %s (confidence: %d%%)", - session.ID, len(allDecisions), primaryConsensus.Action, primaryConsensus.Symbol, primaryConsensus.Confidence) -} - -// buildMarketContext builds the market context using strategy engine -func (e *DebateEngine) buildMarketContext(session *store.DebateSessionWithDetails, strategyEngine *kernel.StrategyEngine) (*kernel.Context, error) { - config := strategyEngine.GetConfig() - - // Get candidate coins - candidates, err := strategyEngine.GetCandidateCoins() - if err != nil { - return nil, fmt.Errorf("failed to get candidates: %w", err) - } - - if len(candidates) == 0 { - return nil, fmt.Errorf("no candidate coins found") - } - - // Get timeframe settings - timeframes := config.Indicators.Klines.SelectedTimeframes - primaryTimeframe := config.Indicators.Klines.PrimaryTimeframe - klineCount := config.Indicators.Klines.PrimaryCount - if klineCount <= 0 { - klineCount = 50 - } - - // Fetch market data for each candidate - marketDataMap := make(map[string]*market.Data) - for _, coin := range candidates { - data, err := market.GetWithTimeframes(coin.Symbol, timeframes, primaryTimeframe, klineCount) - if err != nil { - logger.Warnf("Failed to get market data for %s: %v", coin.Symbol, err) - continue - } - marketDataMap[coin.Symbol] = data - } - - if len(marketDataMap) == 0 { - return nil, fmt.Errorf("failed to fetch market data for any candidate") - } - - // Fetch quantitative data (using strategy engine's built-in logic) - symbols := make([]string, 0, len(candidates)) - for _, c := range candidates { - symbols = append(symbols, c.Symbol) - } - quantDataMap := strategyEngine.FetchQuantDataBatch(symbols) - - // Fetch OI ranking data (market-wide position changes) - oiRankingData := strategyEngine.FetchOIRankingData() - - // Fetch NetFlow ranking data (market-wide fund flow) - netFlowRankingData := strategyEngine.FetchNetFlowRankingData() - - // Fetch Price ranking data (market-wide gainers/losers) - priceRankingData := strategyEngine.FetchPriceRankingData() - - // Build context - ctx := &kernel.Context{ - CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"), - RuntimeMinutes: 0, - CallCount: 1, - Account: kernel.AccountInfo{ - TotalEquity: 1000.0, // Simulated for debate - AvailableBalance: 1000.0, - UnrealizedPnL: 0, - TotalPnL: 0, - TotalPnLPct: 0, - MarginUsed: 0, - MarginUsedPct: 0, - PositionCount: 0, - }, - Positions: []kernel.PositionInfo{}, - CandidateCoins: candidates, - PromptVariant: session.PromptVariant, - MarketDataMap: marketDataMap, - QuantDataMap: quantDataMap, - OIRankingData: oiRankingData, - NetFlowRankingData: netFlowRankingData, - PriceRankingData: priceRankingData, - } - - return ctx, nil -} - -// buildDebateSystemPrompt enhances the base strategy prompt with debate-specific instructions -func (e *DebateEngine) buildDebateSystemPrompt(basePrompt string, participant *store.DebateParticipant, round, maxRounds int) string { - personality := getPersonalityDescription(participant.Personality) - emoji := store.PersonalityEmojis[participant.Personality] - - debateInstructions := fmt.Sprintf(` -## DEBATE MODE - ROUND %d/%d - -You are participating in a multi-AI market debate as %s %s. - -### Your Debate Role: -%s - -### Debate Rules: -1. Analyze ALL candidate coins provided in the market data -2. Support your arguments with specific data points and indicators -3. If this is round 2 or later, respond to other participants' arguments -4. Be persuasive but data-driven -5. Your personality should influence your analysis bias but not override data -6. You can recommend multiple coins with different actions - -### CRITICAL: Output Format (MUST follow exactly) - -First write your analysis: - -- Your market analysis for each coin with specific data references -- Your main trading thesis and arguments -- Response to other participants (if round > 1) - - -Then output your decisions in STRICT JSON ARRAY format (can include multiple coins): - -[ - {"symbol": "BTCUSDT", "action": "open_long", "confidence": 75, "leverage": 5, "position_pct": 0.3, "stop_loss": 0.02, "take_profit": 0.04, "reasoning": "BTC showing strength"}, - {"symbol": "ETHUSDT", "action": "open_short", "confidence": 80, "leverage": 3, "position_pct": 0.2, "stop_loss": 0.03, "take_profit": 0.06, "reasoning": "ETH bearish divergence"}, - {"symbol": "SOLUSDT", "action": "wait", "confidence": 60, "reasoning": "SOL needs more confirmation"} -] - - -### IMPORTANT: action field MUST be exactly one of: -- "open_long" (做多/买入) -- "open_short" (做空/卖出) -- "close_long" (平多仓) -- "close_short" (平空仓) -- "hold" (持仓观望) -- "wait" (空仓等待) - -### Field Requirements for each coin: -- symbol: REQUIRED, the trading pair -- action: REQUIRED, exactly one of the above values -- confidence: REQUIRED, integer 0-100 -- leverage: REQUIRED for open_long/open_short, integer 1-20 -- position_pct: REQUIRED for open_long/open_short, float 0.1-1.0 -- stop_loss: REQUIRED for open_long/open_short, float 0.01-0.10 (percentage as decimal) -- take_profit: REQUIRED for open_long/open_short, float 0.02-0.20 (percentage as decimal) -- reasoning: REQUIRED, one sentence summary - ---- - -`, round, maxRounds, emoji, participant.Personality, personality) - - return debateInstructions + basePrompt -} - -// buildDebateUserPrompt adds debate context to the user prompt -func (e *DebateEngine) buildDebateUserPrompt(baseUserPrompt string, previousMessages []*store.DebateMessage, currentParticipant *store.DebateParticipant, round int) string { - var sb strings.Builder - - // Add previous debate messages if any - if len(previousMessages) > 0 && round > 1 { - sb.WriteString("## Previous Debate Arguments\n\n") - for _, msg := range previousMessages { - emoji := store.PersonalityEmojis[msg.Personality] - sb.WriteString(fmt.Sprintf("### %s %s (%s) - Round %d:\n", emoji, msg.AIModelName, msg.Personality, msg.Round)) - // Extract key points from previous messages - if msg.Decision != nil { - sb.WriteString(fmt.Sprintf("**Position:** %s (Confidence: %d%%)\n", msg.Decision.Action, msg.Decision.Confidence)) - } - // Include a summary of their argument - if len(msg.Content) > 500 { - sb.WriteString(msg.Content[:500] + "...\n\n") - } else { - sb.WriteString(msg.Content + "\n\n") - } - } - sb.WriteString("---\n\n") - } - - sb.WriteString("## Current Market Data\n\n") - sb.WriteString(baseUserPrompt) - - return sb.String() -} - -// getParticipantResponse gets a response from a participant with timeout -func (e *DebateEngine) getParticipantResponse( - session *store.DebateSessionWithDetails, - participant *store.DebateParticipant, - systemPrompt, userPrompt string, - round int, -) (*store.DebateMessage, error) { - e.clientsMu.RLock() - client, ok := e.clients[participant.AIModelID] - e.clientsMu.RUnlock() - - if !ok { - return nil, fmt.Errorf("client not found for %s", participant.AIModelID) - } - - // Use channel-based timeout (60 seconds per AI call) - type result struct { - response string - err error - } - resultCh := make(chan result, 1) - - go func() { - resp, err := client.CallWithMessages(systemPrompt, userPrompt) - resultCh <- result{response: resp, err: err} - }() - - var response string - var err error - select { - case res := <-resultCh: - response = res.response - err = res.err - case <-time.After(60 * time.Second): - return nil, fmt.Errorf("AI call timeout after 60s for %s", participant.AIModelName) - } - - if err != nil { - return nil, fmt.Errorf("AI call failed: %w", err) - } - - // Parse multiple decisions from response - decisions, confidence := parseDecisions(response) - - // Validate and fix symbols - if session has a specific symbol, force all decisions to use it - if session.Symbol != "" { - for _, d := range decisions { - if d.Symbol == "" || d.Symbol != session.Symbol { - logger.Warnf("[Debate] Fixing invalid symbol in message '%s' -> '%s'", d.Symbol, session.Symbol) - d.Symbol = session.Symbol - } - } - } - - // For backward compatibility, set Decision to first decision - var primaryDecision *store.DebateDecision - if len(decisions) > 0 { - primaryDecision = decisions[0] - } - - // Determine message type based on round - messageType := "analysis" - if round > 1 { - messageType = "rebuttal" - } - - msg := &store.DebateMessage{ - SessionID: session.ID, - Round: round, - AIModelID: participant.AIModelID, - AIModelName: participant.AIModelName, - Provider: participant.Provider, - Personality: participant.Personality, - MessageType: messageType, - Content: response, - Decision: primaryDecision, - Decisions: decisions, - Confidence: confidence, - } - - return msg, nil -} - -// collectVotes collects final votes from all participants -func (e *DebateEngine) collectVotes(session *store.DebateSessionWithDetails, strategyEngine *kernel.StrategyEngine, allMessages []*store.DebateMessage) ([]*store.DebateVote, error) { - var votes []*store.DebateVote - - // Build voting context - baseSystemPrompt := strategyEngine.BuildSystemPrompt(1000.0, session.PromptVariant) - - for _, participant := range session.Participants { - vote, err := e.getParticipantVote(session, participant, baseSystemPrompt, allMessages) - if err != nil { - logger.Errorf("Failed to get vote from %s: %v", participant.AIModelName, err) - continue - } - - if err := e.debateStore.AddVote(vote); err != nil { - logger.Errorf("Failed to save vote: %v", err) - } - - votes = append(votes, vote) - - if e.OnVote != nil { - e.OnVote(session.ID, vote) - } - } - - return votes, nil -} - -// getParticipantVote gets a final vote from a participant (supports multi-coin) -func (e *DebateEngine) getParticipantVote( - session *store.DebateSessionWithDetails, - participant *store.DebateParticipant, - baseSystemPrompt string, - allMessages []*store.DebateMessage, -) (*store.DebateVote, error) { - e.clientsMu.RLock() - client, ok := e.clients[participant.AIModelID] - e.clientsMu.RUnlock() - - if !ok { - return nil, fmt.Errorf("client not found for %s", participant.AIModelID) - } - - systemPrompt := e.buildVotingSystemPrompt(baseSystemPrompt, participant) - userPrompt := e.buildVotingUserPrompt(allMessages) - - response, err := client.CallWithMessages(systemPrompt, userPrompt) - if err != nil { - return nil, fmt.Errorf("AI call failed: %w", err) - } - - // Parse multi-coin votes - decisions, avgConfidence := parseDecisions(response) - - // Validate and fix symbols - if session has a specific symbol, force all decisions to use it - // This prevents AI from hallucinating random symbols not in the candidate list - if session.Symbol != "" { - for _, d := range decisions { - if d.Symbol == "" || d.Symbol != session.Symbol { - logger.Warnf("[Debate] Fixing invalid symbol '%s' -> '%s'", d.Symbol, session.Symbol) - d.Symbol = session.Symbol - } - } - } - - // Find primary decision (for backward compatibility) - var primaryDecision *store.DebateDecision - if len(decisions) > 0 { - primaryDecision = decisions[0] - } - - // If no valid decisions, create a default one with session symbol - if primaryDecision == nil && session.Symbol != "" { - primaryDecision = &store.DebateDecision{ - Action: "hold", - Symbol: session.Symbol, - Confidence: 50, - Leverage: 5, - PositionPct: 0.2, - } - decisions = []*store.DebateDecision{primaryDecision} - } - - vote := &store.DebateVote{ - SessionID: session.ID, - AIModelID: participant.AIModelID, - AIModelName: participant.AIModelName, - Decisions: decisions, - Confidence: avgConfidence, - } - - // Set backward-compatible fields from primary decision - if primaryDecision != nil { - vote.Action = primaryDecision.Action - vote.Symbol = primaryDecision.Symbol - vote.Leverage = primaryDecision.Leverage - vote.PositionPct = primaryDecision.PositionPct - vote.StopLossPct = primaryDecision.StopLoss - vote.TakeProfitPct = primaryDecision.TakeProfit - vote.Reasoning = primaryDecision.Reasoning - vote.Confidence = primaryDecision.Confidence - } - - logger.Infof("[Debate] Vote from %s: %d decisions", participant.AIModelName, len(decisions)) - for _, d := range decisions { - logger.Infof("[Debate] - %s: %s (confidence: %d%%)", d.Symbol, d.Action, d.Confidence) - } - - return vote, nil -} - -// buildVotingSystemPrompt builds the system prompt for voting -func (e *DebateEngine) buildVotingSystemPrompt(basePrompt string, participant *store.DebateParticipant) string { - personality := getPersonalityDescription(participant.Personality) - emoji := store.PersonalityEmojis[participant.Personality] - - return fmt.Sprintf(`## FINAL VOTE - -You are %s %s. The debate has concluded. - -Your personality: %s - -Review all the arguments presented and cast your final vote for ALL coins discussed. - -Consider: -- The strength of technical arguments -- Data-driven evidence presented -- Risk/reward analysis -- Market timing considerations - -You may vote differently from your earlier position if convinced by others' arguments. - -### CRITICAL: Output your votes in STRICT JSON ARRAY format (one vote per coin): - -[ - {"symbol": "BTCUSDT", "action": "open_long", "confidence": 75, "leverage": 5, "position_pct": 0.3, "stop_loss": 0.02, "take_profit": 0.04, "reasoning": "BTC final vote reason"}, - {"symbol": "ETHUSDT", "action": "open_short", "confidence": 80, "leverage": 3, "position_pct": 0.2, "stop_loss": 0.03, "take_profit": 0.06, "reasoning": "ETH final vote reason"}, - {"symbol": "SOLUSDT", "action": "wait", "confidence": 60, "reasoning": "SOL not ready"} -] - - -### IMPORTANT: action field MUST be exactly one of: -- "open_long" (做多/买入) -- "open_short" (做空/卖出) -- "close_long" (平多仓) -- "close_short" (平空仓) -- "hold" (持仓观望) -- "wait" (空仓等待) - ---- - -%s -`, emoji, participant.Personality, personality, basePrompt) -} - -// buildVotingUserPrompt builds the user prompt for voting -func (e *DebateEngine) buildVotingUserPrompt(allMessages []*store.DebateMessage) string { - var sb strings.Builder - sb.WriteString("## Debate Summary\n\n") - - // Group messages by participant - participantMessages := make(map[string][]*store.DebateMessage) - for _, msg := range allMessages { - participantMessages[msg.AIModelName] = append(participantMessages[msg.AIModelName], msg) - } - - for name, msgs := range participantMessages { - if len(msgs) == 0 { - continue - } - emoji := store.PersonalityEmojis[msgs[0].Personality] - sb.WriteString(fmt.Sprintf("### %s %s:\n", emoji, name)) - for _, msg := range msgs { - if msg.Decision != nil { - sb.WriteString(fmt.Sprintf("- Round %d: %s (Confidence: %d%%)\n", msg.Round, msg.Decision.Action, msg.Decision.Confidence)) - } - } - sb.WriteString("\n") - } - - sb.WriteString("\nCast your final vote based on the debate above.\n") - return sb.String() -} - -// determineConsensus determines the final consensus from votes (supports multi-coin) -func (e *DebateEngine) determineConsensus(symbol string, votes []*store.DebateVote) *store.DebateDecision { - decisions := e.determineMultiCoinConsensus(votes) - - // For backward compatibility, return the first decision or a default - if len(decisions) == 0 { - return &store.DebateDecision{ - Action: "hold", - Symbol: symbol, - Confidence: 0, - Reasoning: "No consensus reached", - } - } - - // If a specific symbol was requested, find it - if symbol != "" { - for _, d := range decisions { - if d.Symbol == symbol { - return d - } - } - } - - return decisions[0] -} - -// determineMultiCoinConsensus determines consensus for all coins from votes -func (e *DebateEngine) determineMultiCoinConsensus(votes []*store.DebateVote) []*store.DebateDecision { - if len(votes) == 0 { - return nil - } - - // Collect all coin decisions from all votes - // Map: symbol -> action -> weighted score and decision data - type actionData struct { - score float64 - totalConf int - totalLeverage int - totalPosPct float64 - totalSLPct float64 - totalTPPct float64 - count int - reasonings []string - } - - symbolActions := make(map[string]map[string]*actionData) - - // Process all votes - logger.Infof("[Debate] Determining multi-coin consensus from %d votes:", len(votes)) - for _, vote := range votes { - // Process multi-coin decisions if available - decisionsProcessed := false - if len(vote.Decisions) > 0 { - for _, d := range vote.Decisions { - // Use vote.Symbol as fallback if decision symbol is empty - symbol := d.Symbol - if symbol == "" { - symbol = vote.Symbol - } - if symbol == "" || !isValidAction(d.Action) { - continue - } - decisionsProcessed = true - if _, ok := symbolActions[symbol]; !ok { - symbolActions[symbol] = make(map[string]*actionData) - } - if _, ok := symbolActions[symbol][d.Action]; !ok { - symbolActions[symbol][d.Action] = &actionData{} - } - ad := symbolActions[symbol][d.Action] - weight := float64(d.Confidence) / 100.0 - if weight < 0.1 { - weight = 0.5 // Default weight for low confidence - } - ad.score += weight - ad.totalConf += d.Confidence - if d.Leverage > 0 { - ad.totalLeverage += d.Leverage - } else { - ad.totalLeverage += 5 // Default leverage - } - if d.PositionPct > 0 { - ad.totalPosPct += d.PositionPct - } else { - ad.totalPosPct += 0.2 // Default position pct - } - ad.totalSLPct += d.StopLoss - ad.totalTPPct += d.TakeProfit - ad.count++ - if d.Reasoning != "" { - ad.reasonings = append(ad.reasonings, d.Reasoning) - } - logger.Infof("[Debate] %s: %s -> %s (conf: %d%%)", vote.AIModelName, symbol, d.Action, d.Confidence) - } - } - - // Fallback to single-coin vote if no decisions were processed - if !decisionsProcessed && vote.Symbol != "" && isValidAction(vote.Action) { - if _, ok := symbolActions[vote.Symbol]; !ok { - symbolActions[vote.Symbol] = make(map[string]*actionData) - } - if _, ok := symbolActions[vote.Symbol][vote.Action]; !ok { - symbolActions[vote.Symbol][vote.Action] = &actionData{} - } - ad := symbolActions[vote.Symbol][vote.Action] - weight := float64(vote.Confidence) / 100.0 - if weight < 0.1 { - weight = 0.5 // Default weight for low confidence - } - ad.score += weight - ad.totalConf += vote.Confidence - if vote.Leverage > 0 { - ad.totalLeverage += vote.Leverage - } else { - ad.totalLeverage += 5 // Default leverage - } - if vote.PositionPct > 0 { - ad.totalPosPct += vote.PositionPct - } else { - ad.totalPosPct += 0.2 // Default position pct - } - ad.totalSLPct += vote.StopLossPct - ad.totalTPPct += vote.TakeProfitPct - ad.count++ - if vote.Reasoning != "" { - ad.reasonings = append(ad.reasonings, vote.Reasoning) - } - logger.Infof("[Debate] %s: %s -> %s (conf: %d%%)", vote.AIModelName, vote.Symbol, vote.Action, vote.Confidence) - } - } - - // Determine winning action for each symbol - var results []*store.DebateDecision - for symbol, actions := range symbolActions { - var winningAction string - var maxScore float64 - for action, ad := range actions { - if ad.score > maxScore { - maxScore = ad.score - winningAction = action - } - } - - if winningAction == "" { - continue - } - - ad := actions[winningAction] - if ad.count == 0 { - continue - } - - // Calculate averages - avgConf := ad.totalConf / ad.count - avgLeverage := ad.totalLeverage / ad.count - avgPosPct := ad.totalPosPct / float64(ad.count) - avgSLPct := ad.totalSLPct / float64(ad.count) - avgTPPct := ad.totalTPPct / float64(ad.count) - - // Apply defaults and limits - if avgLeverage < 1 { - avgLeverage = 5 - } - if avgLeverage > 20 { - avgLeverage = 20 - } - if avgPosPct < 0.1 { - avgPosPct = 0.2 - } - if avgPosPct > 1.0 { - avgPosPct = 1.0 - } - // Apply defaults for SL/TP if not set - if avgSLPct <= 0 && (winningAction == "open_long" || winningAction == "open_short") { - avgSLPct = 0.03 // Default 3% stop loss - } - if avgTPPct <= 0 && (winningAction == "open_long" || winningAction == "open_short") { - avgTPPct = 0.06 // Default 6% take profit - } - - decision := &store.DebateDecision{ - Action: winningAction, - Symbol: symbol, - Confidence: avgConf, - Leverage: avgLeverage, - PositionPct: avgPosPct, - StopLoss: avgSLPct, - TakeProfit: avgTPPct, - Reasoning: strings.Join(ad.reasonings, "; "), - } - - logger.Infof("[Debate] Consensus for %s: %s (score: %.2f, conf: %d%%, leverage: %dx)", - symbol, winningAction, maxScore, avgConf, avgLeverage) - - results = append(results, decision) - } - - logger.Infof("[Debate] Total %d consensus decisions", len(results)) - return results -} - -// CancelDebate cancels a running debate -func (e *DebateEngine) CancelDebate(sessionID string) error { - return e.debateStore.UpdateSessionStatus(sessionID, store.DebateStatusCancelled) -} - -// ExecuteConsensus executes the consensus decision from a completed debate -func (e *DebateEngine) ExecuteConsensus(sessionID string, executor TraderExecutor) error { - session, err := e.debateStore.GetSessionWithDetails(sessionID) - if err != nil { - return fmt.Errorf("failed to get session: %w", err) - } - - if session.Status != store.DebateStatusCompleted { - return fmt.Errorf("debate is not completed (status: %s)", session.Status) - } - - if session.FinalDecision == nil { - return fmt.Errorf("no final decision available") - } - - if session.FinalDecision.Executed { - return fmt.Errorf("consensus already executed at %s", session.FinalDecision.ExecutedAt.Format(time.RFC3339)) - } - - action := session.FinalDecision.Action - if action != "open_long" && action != "open_short" { - return fmt.Errorf("action '%s' does not require execution", action) - } - - // Get current market price - marketData, err := market.Get(session.Symbol) - if err != nil { - return fmt.Errorf("failed to get market data: %w", err) - } - - // Get account balance - balance, err := executor.GetBalance() - if err != nil { - return fmt.Errorf("failed to get balance: %w", err) - } - - // Debug log balance keys and values - logger.Infof("Debate execution - balance data: %+v", balance) - - // Use available_balance for position sizing (not total equity) - availableBalance := 0.0 - if avail, ok := balance["available_balance"].(float64); ok && avail > 0 { - availableBalance = avail - logger.Infof("Using available_balance: %.2f", availableBalance) - } else if eq, ok := balance["total_equity"].(float64); ok && eq > 0 { - // Fallback to total_equity if available_balance not found - availableBalance = eq - logger.Infof("Fallback to total_equity: %.2f", availableBalance) - } else if wallet, ok := balance["wallet_balance"].(float64); ok && wallet > 0 { - availableBalance = wallet - logger.Infof("Fallback to wallet_balance: %.2f", availableBalance) - } - - if availableBalance <= 0 { - // Log all balance keys for debugging - keys := make([]string, 0, len(balance)) - for k, v := range balance { - keys = append(keys, fmt.Sprintf("%s=%v", k, v)) - } - return fmt.Errorf("invalid available balance: %.2f (balance data: %v)", availableBalance, keys) - } - - // Calculate position size = available_balance × position_pct - positionSizeUSD := availableBalance * session.FinalDecision.PositionPct - if positionSizeUSD < 12 { - positionSizeUSD = 12 - } - - // Calculate stop loss and take profit prices - currentPrice := marketData.CurrentPrice - var stopLossPrice, takeProfitPrice float64 - - if action == "open_long" { - stopLossPrice = currentPrice * (1 - session.FinalDecision.StopLoss) - takeProfitPrice = currentPrice * (1 + session.FinalDecision.TakeProfit) - } else { - stopLossPrice = currentPrice * (1 + session.FinalDecision.StopLoss) - takeProfitPrice = currentPrice * (1 - session.FinalDecision.TakeProfit) - } - - // Create decision - tradeDecision := &kernel.Decision{ - Symbol: session.Symbol, - Action: action, - Leverage: session.FinalDecision.Leverage, - PositionSizeUSD: positionSizeUSD, - StopLoss: stopLossPrice, - TakeProfit: takeProfitPrice, - Confidence: session.FinalDecision.Confidence, - Reasoning: fmt.Sprintf("Debate consensus: %s", session.FinalDecision.Reasoning), - } - - logger.Infof("======== EXECUTING DEBATE CONSENSUS ========") - logger.Infof("Session ID: %s", sessionID) - logger.Infof("Symbol: %s", session.Symbol) - logger.Infof("Action: %s (from FinalDecision.Action: %s)", action, session.FinalDecision.Action) - logger.Infof("Position Size: %.2f USD", positionSizeUSD) - logger.Infof("Leverage: %dx", tradeDecision.Leverage) - logger.Infof("StopLoss: %.4f, TakeProfit: %.4f", stopLossPrice, takeProfitPrice) - logger.Infof("=============================================") - logger.Infof("Executing debate consensus: %s %s @ %.2f USD, leverage %dx", - action, session.Symbol, positionSizeUSD, tradeDecision.Leverage) - - // Execute - err = executor.ExecuteDecision(tradeDecision) - - // Update session - session.FinalDecision.Executed = err == nil - session.FinalDecision.ExecutedAt = time.Now() - session.FinalDecision.PositionSizeUSD = positionSizeUSD - if err != nil { - session.FinalDecision.Error = err.Error() - } - - e.debateStore.UpdateSessionFinalDecision(sessionID, session.FinalDecision) - - if err != nil { - return fmt.Errorf("trade execution failed: %w", err) - } - - return nil -} - -// Helper functions - -func getPersonalityDescription(personality store.DebatePersonality) string { - switch personality { - case store.PersonalityBull: - return "Aggressive Bull - You are optimistic and look for long opportunities. You believe in upward momentum and trend continuation. Focus on bullish signals and support levels." - case store.PersonalityBear: - return "Cautious Bear - You are skeptical and focus on risks. You look for short opportunities and warning signs. Question bullish narratives and highlight resistance levels." - case store.PersonalityAnalyst: - return "Data Analyst - You are neutral and purely data-driven. Present technical analysis without bias. Let the indicators speak for themselves." - case store.PersonalityContrarian: - return "Contrarian - You challenge majority opinions and look for overlooked opportunities. Question consensus views and find alternative interpretations of the data." - case store.PersonalityRiskManager: - return "Risk Manager - You focus on position sizing, stop losses, and capital preservation. Evaluate risk/reward ratios and warn about potential downsides." - default: - return "Market Analyst - Provide balanced technical analysis." - } -} - -// parseDecisions extracts multiple decisions from AI response using strict JSON parsing -func parseDecisions(response string) ([]*store.DebateDecision, int) { - avgConfidence := 50 - - // Log first 500 chars of response for debugging - responsePreview := response - if len(responsePreview) > 500 { - responsePreview = responsePreview[:500] + "..." - } - logger.Infof("[Debate] Parsing response (preview): %s", responsePreview) - - // Try to extract JSON from or tag - var jsonContent string - decisionPattern := regexp.MustCompile(`(?s)\s*(.*?)\s*`) - finalVotePattern := regexp.MustCompile(`(?s)\s*(.*?)\s*`) - - if matches := decisionPattern.FindStringSubmatch(response); len(matches) > 1 { - jsonContent = strings.TrimSpace(matches[1]) - logger.Infof("[Debate] Found tag, content length: %d", len(jsonContent)) - } else if matches := finalVotePattern.FindStringSubmatch(response); len(matches) > 1 { - jsonContent = strings.TrimSpace(matches[1]) - logger.Infof("[Debate] Found tag, content length: %d", len(jsonContent)) - } - - if jsonContent != "" { - // Intermediate struct to handle both field naming conventions - type rawDecision struct { - Action string `json:"action"` - Symbol string `json:"symbol"` - Confidence int `json:"confidence"` - Leverage int `json:"leverage"` - PositionPct float64 `json:"position_pct"` - StopLoss float64 `json:"stop_loss"` - TakeProfit float64 `json:"take_profit"` - StopLossPct float64 `json:"stop_loss_pct"` // Alternative field name - TakeProfitPct float64 `json:"take_profit_pct"` // Alternative field name - Reasoning string `json:"reasoning"` - } - - convertRawDecision := func(r *rawDecision) *store.DebateDecision { - d := &store.DebateDecision{ - Action: normalizeAction(r.Action), - Symbol: r.Symbol, - Confidence: r.Confidence, - Leverage: r.Leverage, - PositionPct: r.PositionPct, - Reasoning: r.Reasoning, - } - // Use stop_loss or stop_loss_pct (whichever is set) - if r.StopLoss > 0 { - d.StopLoss = r.StopLoss - } else if r.StopLossPct > 0 { - d.StopLoss = r.StopLossPct - } - // Use take_profit or take_profit_pct (whichever is set) - if r.TakeProfit > 0 { - d.TakeProfit = r.TakeProfit - } else if r.TakeProfitPct > 0 { - d.TakeProfit = r.TakeProfitPct - } - // Apply defaults - if d.Leverage == 0 { - d.Leverage = 5 - } - if d.PositionPct == 0 { - d.PositionPct = 0.2 - } - return d - } - - // Try to parse as JSON array first - var rawDecisions []*rawDecision - if err := json.Unmarshal([]byte(jsonContent), &rawDecisions); err == nil && len(rawDecisions) > 0 { - logger.Infof("[Debate] Parsed %d decisions from JSON array", len(rawDecisions)) - validDecisions := make([]*store.DebateDecision, 0) - totalConfidence := 0 - for _, r := range rawDecisions { - d := convertRawDecision(r) - if isValidAction(d.Action) { - validDecisions = append(validDecisions, d) - totalConfidence += d.Confidence - logger.Infof("[Debate] - %s: %s (conf: %d%%, sl: %.4f, tp: %.4f)", d.Symbol, d.Action, d.Confidence, d.StopLoss, d.TakeProfit) - } - } - if len(validDecisions) > 0 { - avgConfidence = totalConfidence / len(validDecisions) - return validDecisions, avgConfidence - } - } - - // Try to parse as single JSON object - var singleRaw rawDecision - if err := json.Unmarshal([]byte(jsonContent), &singleRaw); err == nil { - d := convertRawDecision(&singleRaw) - if isValidAction(d.Action) { - logger.Infof("[Debate] Parsed single decision: %s %s (conf: %d%%, sl: %.4f, tp: %.4f)", - d.Symbol, d.Action, d.Confidence, d.StopLoss, d.TakeProfit) - return []*store.DebateDecision{d}, d.Confidence - } - } - - // Try to find JSON array in content - jsonArrayPattern := regexp.MustCompile(`\[[\s\S]*\]`) - if jsonArray := jsonArrayPattern.FindString(jsonContent); jsonArray != "" { - if err := json.Unmarshal([]byte(jsonArray), &rawDecisions); err == nil && len(rawDecisions) > 0 { - logger.Infof("[Debate] Parsed %d decisions from embedded JSON array", len(rawDecisions)) - validDecisions := make([]*store.DebateDecision, 0) - totalConfidence := 0 - for _, r := range rawDecisions { - d := convertRawDecision(r) - if isValidAction(d.Action) { - validDecisions = append(validDecisions, d) - totalConfidence += d.Confidence - } - } - if len(validDecisions) > 0 { - avgConfidence = totalConfidence / len(validDecisions) - return validDecisions, avgConfidence - } - } - } - } else { - logger.Warnf("[Debate] No or tag found in response!") - } - - // Fallback: create a single decision with fallback action - logger.Warnf("[Debate] No valid decisions found, using fallback parsing") - fallbackAction := fallbackParseAction(response) - fallbackDecision := &store.DebateDecision{ - Action: fallbackAction, - Confidence: 50, - Leverage: 5, - PositionPct: 0.2, - } - logger.Infof("[Debate] Fallback decision: %s", fallbackAction) - return []*store.DebateDecision{fallbackDecision}, 50 -} - -// parseDecision extracts single decision (backward compatible wrapper) -func parseDecision(response string) (*store.DebateDecision, int) { - decisions, confidence := parseDecisions(response) - if len(decisions) > 0 { - return decisions[0], confidence - } - return &store.DebateDecision{Action: "wait", Confidence: 50}, 50 -} - -// isValidAction checks if action is one of the valid actions -func isValidAction(action string) bool { - validActions := map[string]bool{ - "open_long": true, - "open_short": true, - "close_long": true, - "close_short": true, - "hold": true, - "wait": true, - } - return validActions[strings.ToLower(strings.TrimSpace(action))] -} - -// normalizeAction normalizes action string to standard format -func normalizeAction(action string) string { - action = strings.ToLower(strings.TrimSpace(action)) - action = strings.ReplaceAll(action, " ", "_") - action = strings.ReplaceAll(action, "-", "_") - - // Map common variations - actionMap := map[string]string{ - "long": "open_long", - "openlong": "open_long", - "buy": "open_long", - "short": "open_short", - "openshort": "open_short", - "sell": "open_short", - "closelong": "close_long", - "closeshort": "close_short", - } - - if mapped, ok := actionMap[action]; ok { - return mapped - } - return action -} - -// fallbackParseAction parses action from full response text when parsing fails -func fallbackParseAction(response string) string { - responseLower := strings.ToLower(response) - - // Count specific action keywords only - openLongCount := strings.Count(responseLower, "\"action\": \"open_long\"") + - strings.Count(responseLower, "\"action\":\"open_long\"") + - strings.Count(responseLower, "action: open_long") - openShortCount := strings.Count(responseLower, "\"action\": \"open_short\"") + - strings.Count(responseLower, "\"action\":\"open_short\"") + - strings.Count(responseLower, "action: open_short") - holdCount := strings.Count(responseLower, "\"action\": \"hold\"") + - strings.Count(responseLower, "\"action\":\"hold\"") + - strings.Count(responseLower, "action: hold") - waitCount := strings.Count(responseLower, "\"action\": \"wait\"") + - strings.Count(responseLower, "\"action\":\"wait\"") + - strings.Count(responseLower, "action: wait") - - logger.Infof("[Debate] Fallback action counts: long=%d, short=%d, hold=%d, wait=%d", - openLongCount, openShortCount, holdCount, waitCount) - - // Find max - maxCount := 0 - action := "wait" - if openLongCount > maxCount { - maxCount = openLongCount - action = "open_long" - } - if openShortCount > maxCount { - maxCount = openShortCount - action = "open_short" - } - if holdCount > maxCount { - maxCount = holdCount - action = "hold" - } - if waitCount > maxCount { - action = "wait" - } - - return action -} - -// VoteResult holds the parsed vote details -type VoteResult struct { - Action string - Confidence int - Reasoning string - Leverage int - PositionPct float64 - StopLossPct float64 - TakeProfitPct float64 -} - -// parseVote extracts vote from AI response using strict JSON parsing -func parseVote(response string) *VoteResult { - result := &VoteResult{ - Confidence: 50, - Leverage: 5, - PositionPct: 0.2, - } - - // Try to extract JSON from tag - votePattern := regexp.MustCompile(`(?s)\s*(.*?)\s*`) - if matches := votePattern.FindStringSubmatch(response); len(matches) > 1 { - jsonContent := strings.TrimSpace(matches[1]) - - // Try direct JSON parse first - if err := json.Unmarshal([]byte(jsonContent), result); err == nil { - logger.Infof("[Debate] Parsed vote JSON: action=%s, confidence=%d", result.Action, result.Confidence) - if isValidAction(result.Action) { - result.Action = normalizeAction(result.Action) - return result - } - logger.Warnf("[Debate] Invalid action in vote JSON: %s", result.Action) - } - - // Try to find JSON object in content - jsonObjPattern := regexp.MustCompile(`\{[^}]+\}`) - if jsonObj := jsonObjPattern.FindString(jsonContent); jsonObj != "" { - if err := json.Unmarshal([]byte(jsonObj), result); err == nil { - logger.Infof("[Debate] Parsed vote from JSON object: action=%s, confidence=%d", result.Action, result.Confidence) - if isValidAction(result.Action) { - result.Action = normalizeAction(result.Action) - return result - } - } - } - - // Fallback to key-value parsing - if action := extractValue(jsonContent, "action"); action != "" { - result.Action = normalizeAction(action) - } - if confStr := extractValue(jsonContent, "confidence"); confStr != "" { - if c, err := strconv.Atoi(strings.TrimSpace(confStr)); err == nil { - result.Confidence = c - } - } - result.Reasoning = extractValue(jsonContent, "reasoning") - if leverageStr := extractValue(jsonContent, "leverage"); leverageStr != "" { - if lev, err := strconv.Atoi(strings.TrimSpace(leverageStr)); err == nil { - result.Leverage = lev - } - } - if posPctStr := extractValue(jsonContent, "position_pct"); posPctStr != "" { - if pct, err := strconv.ParseFloat(strings.TrimSpace(posPctStr), 64); err == nil { - result.PositionPct = pct - } - } - if slPctStr := extractValue(jsonContent, "stop_loss_pct"); slPctStr != "" { - if sl, err := strconv.ParseFloat(strings.TrimSpace(slPctStr), 64); err == nil { - result.StopLossPct = sl - } - } - if tpPctStr := extractValue(jsonContent, "take_profit_pct"); tpPctStr != "" { - if tp, err := strconv.ParseFloat(strings.TrimSpace(tpPctStr), 64); err == nil { - result.TakeProfitPct = tp - } - } - } - - // Normalize action if found - if result.Action != "" { - result.Action = normalizeAction(result.Action) - } - - // Only use fallback if no valid action found - if !isValidAction(result.Action) { - logger.Warnf("[Debate] No valid action in tag, using fallback parsing") - result.Action = fallbackParseAction(response) - logger.Infof("[Debate] Fallback parsed vote action: %s", result.Action) - } - - return result -} - -// extractValue extracts a value from key: value format -func extractValue(content, key string) string { - patterns := []string{ - fmt.Sprintf(`(?i)%s:\s*([^\n,]+)`, key), - fmt.Sprintf(`(?i)"%s":\s*"?([^"\n,]+)"?`, key), - fmt.Sprintf(`(?i)'%s':\s*'?([^'\n,]+)'?`, key), - } - - for _, pattern := range patterns { - re := regexp.MustCompile(pattern) - if matches := re.FindStringSubmatch(content); len(matches) > 1 { - return strings.TrimSpace(matches[1]) - } - } - return "" -} diff --git a/docs/architecture/DEBATE_MODULE.md b/docs/architecture/DEBATE_MODULE.md deleted file mode 100644 index 21f0cd1d..00000000 --- a/docs/architecture/DEBATE_MODULE.md +++ /dev/null @@ -1,909 +0,0 @@ -# Debate Arena Module - Technical Documentation - -**Language:** [English](DEBATE_MODULE.md) | [中文](DEBATE_MODULE.zh-CN.md) - -## Overview - -The Debate Arena is a collaborative AI decision-making system where multiple AI models with different personalities debate market conditions and reach consensus on trading decisions. The system supports multi-round debates, real-time streaming, voting mechanisms, and automatic trade execution. - -## Table of Contents - -1. [Architecture Overview](#1-architecture-overview) -2. [Backend Components](#2-backend-components) -3. [Debate Execution Flow](#3-debate-execution-flow) -4. [Personality System](#4-personality-system) -5. [Consensus Algorithm](#5-consensus-algorithm) -6. [Auto-Execution](#6-auto-execution) -7. [API Reference](#7-api-reference) -8. [Real-Time Updates (SSE)](#8-real-time-updates-sse) -9. [Database Schema](#9-database-schema) -10. [Frontend Components](#10-frontend-components) -11. [Integration Points](#11-integration-points) -12. [Error Handling](#12-error-handling) - ---- - -## 1. Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Debate Arena System │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Bull AI │ │ Bear AI │ │ Analyst AI │ │ Risk Mgr AI │ │ -│ │ 🐂 │ │ 🐻 │ │ 📊 │ │ 🛡️ │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -│ │ │ │ │ │ -│ └──────────────────┴──────────────────┴──────────────────┘ │ -│ │ │ -│ ┌─────────▼─────────┐ │ -│ │ Debate Engine │ │ -│ │ (debate/engine) │ │ -│ └─────────┬─────────┘ │ -│ │ │ -│ ┌──────────────────────────┼──────────────────────────┐ │ -│ │ │ │ │ -│ ┌──────▼──────┐ ┌─────────▼─────────┐ ┌────────▼────────┐ │ -│ │ Market Data │ │ Voting System │ │ Auto-Executor │ │ -│ │ Assembly │ │ & Consensus │ │ (optional) │ │ -│ └─────────────┘ └───────────────────┘ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### File Structure - -``` -├── debate/ -│ └── engine.go # Core debate engine logic -├── api/ -│ └── debate.go # HTTP handlers and SSE streaming -├── store/ -│ └── debate.go # Database operations and schema -└── web/src/pages/ - └── DebateArenaPage.tsx # Frontend UI -``` - ---- - -## 2. Backend Components - -### 2.1 Core Files - -| File | Purpose | Key Functions | -|------|---------|---------------| -| `debate/engine.go` | Core debate logic | `StartDebate()`, `runDebate()`, `collectVotes()`, `determineConsensus()` | -| `api/debate.go` | HTTP handlers | `HandleCreateDebate()`, `HandleStartDebate()`, `HandleDebateStream()` | -| `store/debate.go` | Database ops | `CreateSession()`, `AddMessage()`, `AddVote()`, `GetSessionWithDetails()` | - -### 2.2 Debate Engine Structure - -```go -// debate/engine.go - -type DebateEngine struct { - store *store.DebateStore - aiClients map[string]ai.Client - strategyEngine *strategy.Engine - subscribers map[string]map[chan []byte]bool -} - -// Event callbacks for real-time updates -var OnRoundStart func(sessionID string, round int) -var OnMessage func(sessionID string, msg *DebateMessage) -var OnVote func(sessionID string, vote *DebateVote) -var OnConsensus func(sessionID string, decision *DebateDecision) -var OnError func(sessionID string, err error) -``` - ---- - -## 3. Debate Execution Flow - -### 3.1 Session Creation - -``` -POST /api/debates - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. Validate user authentication │ -│ 2. Parse CreateDebateRequest: │ -│ - name, strategy_id, symbol, max_rounds, participants │ -│ - interval_minutes, prompt_variant, auto_execute │ -│ 3. Validate strategy ownership │ -│ 4. Auto-select symbol if not provided: │ -│ - Static coins → Use first coin from strategy │ -│ - CoinPool → Fetch from AI500 API │ -│ - OI Top → Fetch from OI ranking API │ -│ - Mixed → Try pool first, fallback to OI │ -│ 5. Set defaults: │ -│ - max_rounds: 3 (range 2-5) │ -│ - interval_minutes: 5 │ -│ - prompt_variant: "balanced" │ -│ 6. Create DebateSession in database │ -│ 7. Add participants with AI models and personalities │ -│ 8. Return full session with participants │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 3.2 Debate Start - -**Location:** `debate/engine.go:StartDebate()` (Lines 114-154) - -``` -POST /api/debates/:id/start - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. Validate session status (must be pending) │ -│ 2. Validate participants (minimum 2) │ -│ 3. Initialize AI clients for all participants │ -│ 4. Get strategy configuration │ -│ 5. Update status to "running" │ -│ 6. Launch goroutine: runDebate() │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 3.3 Market Context Building - -**Location:** `debate/engine.go:buildMarketContext()` (Lines 292-362) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ buildMarketContext() │ -├─────────────────────────────────────────────────────────────┤ -│ 1. Get candidate coins from strategy engine │ -│ 2. Fetch market data for each candidate: │ -│ - Multiple timeframes (15m, 1h, 4h) │ -│ - K-line count from strategy config │ -│ - OHLCV data, indicators │ -│ 3. Fetch quantitative data batch: │ -│ - Capital flow │ -│ - Position changes │ -│ 4. Fetch OI ranking data (market-wide) │ -│ 5. Build Context object with: │ -│ - Account info (simulated: $1000 equity) │ -│ - Candidate coins │ -│ - Market data map │ -│ - Quant data map │ -│ - OI ranking data │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 3.4 Debate Rounds - -**Location:** `debate/engine.go:runDebate()` (Lines 157-289) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ For each round (1 to max_rounds): │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 1. Broadcast "round_start" event │ │ -│ │ 2. For each participant (in speak_order): │ │ -│ │ ┌─────────────────────────────────────────────────┐ │ │ -│ │ │ a. Build personality-enhanced system prompt │ │ │ -│ │ │ b. Build user prompt with: │ │ │ -│ │ │ - Market data (from strategy engine) │ │ │ -│ │ │ - Previous debate messages (if round > 1) │ │ │ -│ │ │ c. Call AI model with 60s timeout │ │ │ -│ │ │ d. Parse multi-coin decisions from response │ │ │ -│ │ │ e. Save message to database │ │ │ -│ │ │ f. Broadcast "message" event │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ 3. Broadcast "round_end" event │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ After all rounds: │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 1. Enter voting phase (status = "voting") │ │ -│ │ 2. Collect final votes from all participants │ │ -│ │ 3. Determine multi-coin consensus │ │ -│ │ 4. Store final decisions │ │ -│ │ 5. Update status to "completed" │ │ -│ │ 6. Broadcast "consensus" event │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. Personality System - -### 4.1 Available Personalities - -| Personality | Emoji | Name | Description | Trading Bias | -|------------|-------|------|-------------|--------------| -| Bull | 🐂 | Aggressive Bull | Looks for long opportunities | Optimistic, trend-following | -| Bear | 🐻 | Cautious Bear | Skeptical, focuses on risks | Pessimistic, short bias | -| Analyst | 📊 | Data Analyst | Neutral, purely data-driven | No bias, objective analysis | -| Contrarian | 🔄 | Contrarian | Challenges majority view | Alternative perspectives | -| Risk Manager | 🛡️ | Risk Manager | Focus on risk control | Position sizing, stop loss | - -### 4.2 Personality Prompt Enhancement - -**Location:** `debate/engine.go:buildDebateSystemPrompt()` (Lines 365-426) - -``` -## DEBATE MODE - ROUND {round}/{max_rounds} - -You are participating as {emoji} {personality}. - -### Your Debate Role: -{personality_description} - -### Debate Rules: -1. Analyze ALL candidate coins -2. Support arguments with specific data -3. Respond to other participants (round > 1) -4. Be persuasive but data-driven -5. Can recommend multiple coins with different actions - -### Output Format (STRICT JSON): - - - Market analysis with data references - - Main trading thesis - - Response to others (if round > 1) - - - -[ - {"symbol": "BTCUSDT", "action": "open_long", "confidence": 75, ...}, - {"symbol": "ETHUSDT", "action": "open_short", "confidence": 80, ...} -] - -``` - -### 4.3 Personality-Specific Prompts - -**Bull (🐂):** -``` -As a bull, you are optimistic about market trends. -Look for long opportunities, identify bullish patterns, -and support your thesis with technical and fundamental data. -Focus on: breakout patterns, momentum, support levels. -``` - -**Bear (🐻):** -``` -As a bear, you are cautious and skeptical. -Look for short opportunities, identify bearish patterns, -and highlight risks and potential downside. -Focus on: resistance levels, divergences, overbought conditions. -``` - -**Analyst (📊):** -``` -As a data analyst, you are completely neutral. -Provide objective analysis based purely on data. -No emotional bias - let the numbers speak. -Focus on: key metrics, statistical patterns, historical comparisons. -``` - -**Contrarian (🔄):** -``` -As a contrarian, challenge the majority view. -Look for overlooked opportunities and hidden risks. -Play devil's advocate to strengthen the debate. -Focus on: crowd positioning, sentiment extremes, neglected signals. -``` - -**Risk Manager (🛡️):** -``` -As a risk manager, focus on capital preservation. -Evaluate position sizing, stop loss levels, and risk/reward ratios. -Ensure all decisions have appropriate risk controls. -Focus on: max drawdown, position limits, volatility-adjusted sizing. -``` - ---- - -## 5. Consensus Algorithm - -### 5.1 Vote Collection - -**Location:** `debate/engine.go:collectVotes()` (Lines 542-567) - -``` -For each participant: -┌─────────────────────────────────────────────────────────────┐ -│ 1. Build voting system prompt │ -│ 2. Build voting user prompt with debate summary │ -│ 3. Call AI model for final vote │ -│ 4. Parse multi-coin decisions │ -│ 5. Validate/fix symbols against session.Symbol │ -│ 6. Save vote to database │ -│ 7. Broadcast "vote" event │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 5.2 Multi-Coin Consensus Determination - -**Location:** `debate/engine.go:determineMultiCoinConsensus()` (Lines 752-924) - -**Algorithm:** - -``` -1. Collect all coin decisions from all votes -2. Group by: symbol → action → aggregated data - -3. For each vote decision: - weight = confidence / 100.0 - Accumulate: - ┌─────────────────────────────────────────────────────────┐ - │ score += weight │ - │ total_confidence += confidence │ - │ total_leverage += leverage │ - │ total_position_pct += position_pct │ - │ total_stop_loss += stop_loss │ - │ total_take_profit += take_profit │ - │ count++ │ - └─────────────────────────────────────────────────────────┘ - -4. For each symbol: - Find winning action (max score) - Calculate averages: - ┌─────────────────────────────────────────────────────────┐ - │ avg_confidence = total_confidence / count │ - │ avg_leverage = clamp(total_leverage / count, 1, 20) │ - │ avg_position_pct = clamp(total_pct / count, 0.1, 1.0) │ - │ avg_stop_loss = default 3% if not set │ - │ avg_take_profit = default 6% if not set │ - └─────────────────────────────────────────────────────────┘ - -5. Return array of consensus decisions -``` - -### 5.3 Consensus Example - -**Input Votes:** -``` -AI1 (Bull): BTC open_long (conf=80, lev=10, pos=0.3) -AI2 (Bear): BTC open_short (conf=60, lev=5, pos=0.2) -AI3 (Analyst): BTC open_long (conf=70, lev=8, pos=0.25) -``` - -**Calculation:** -``` -open_long: - score = 0.80 + 0.70 = 1.50 - avg_conf = (80 + 70) / 2 = 75 - avg_lev = (10 + 8) / 2 = 9 - avg_pos = (0.3 + 0.25) / 2 = 0.275 - -open_short: - score = 0.60 - avg_conf = 60 - avg_lev = 5 - avg_pos = 0.2 - -Winner: open_long (score 1.50 > 0.60) -``` - -**Output:** -```json -{ - "symbol": "BTCUSDT", - "action": "open_long", - "confidence": 75, - "leverage": 9, - "position_pct": 0.275, - "stop_loss": 0.03, - "take_profit": 0.06 -} -``` - ---- - -## 6. Auto-Execution - -### 6.1 Execution Flow - -**Location:** `debate/engine.go:ExecuteConsensus()` (Lines 932-1052) - -``` -POST /api/debates/:id/execute - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. Validate session status = completed │ -│ 2. Validate final_decision exists and not executed │ -│ 3. Validate action is open_long or open_short │ -│ 4. Get current market price │ -│ 5. Get account balance: │ -│ - Try available_balance │ -│ - Fallback to total_equity or wallet_balance │ -│ 6. Calculate position size: │ -│ position_size_usd = available_balance × position_pct │ -│ (minimum $12 to meet exchange requirements) │ -│ 7. Calculate stop loss and take profit prices: │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ open_long: │ │ -│ │ SL = price × (1 - stop_loss_pct) │ │ -│ │ TP = price × (1 + take_profit_pct) │ │ -│ │ open_short: │ │ -│ │ SL = price × (1 + stop_loss_pct) │ │ -│ │ TP = price × (1 - take_profit_pct) │ │ -│ └───────────────────────────────────────────────────┘ │ -│ 8. Create Decision object │ -│ 9. Call executor.ExecuteDecision() │ -│ 10. Update final_decision: │ -│ - executed = true/false │ -│ - executed_at = timestamp │ -│ - error message if failed │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 6.2 Position Size Calculation - -```go -// Calculate position value -position_size_usd := available_balance * position_pct - -// Ensure minimum size for exchange -if position_size_usd < 12 { - position_size_usd = 12 -} - -// Calculate quantity -quantity := position_size_usd / market_price -``` - ---- - -## 7. API Reference - -### 7.1 Endpoints - -| Method | Path | Description | -|--------|------|-------------| -| GET | `/api/debates` | List all debates for user | -| GET | `/api/debates/personalities` | Get AI personality configs | -| GET | `/api/debates/:id` | Get debate with full details | -| POST | `/api/debates` | Create new debate | -| POST | `/api/debates/:id/start` | Start debate execution | -| POST | `/api/debates/:id/cancel` | Cancel running debate | -| POST | `/api/debates/:id/execute` | Execute consensus trade | -| DELETE | `/api/debates/:id` | Delete debate | -| GET | `/api/debates/:id/messages` | Get all messages | -| GET | `/api/debates/:id/votes` | Get all votes | -| GET | `/api/debates/:id/stream` | SSE live stream | - -### 7.2 Create Debate Request - -```json -POST /api/debates -{ - "name": "BTC Market Debate", - "strategy_id": "strategy-uuid", - "symbol": "BTCUSDT", - "max_rounds": 3, - "interval_minutes": 5, - "prompt_variant": "balanced", - "auto_execute": false, - "trader_id": "trader-uuid", - "enable_oi_ranking": true, - "oi_ranking_limit": 10, - "oi_duration": "1h", - "participants": [ - {"ai_model_id": "deepseek-v3", "personality": "bull"}, - {"ai_model_id": "qwen-max", "personality": "bear"}, - {"ai_model_id": "gpt-5.2", "personality": "analyst"} - ] -} -``` - -### 7.3 Create Debate Response - -```json -{ - "id": "debate-uuid", - "user_id": "user-uuid", - "name": "BTC Market Debate", - "strategy_id": "strategy-uuid", - "status": "pending", - "symbol": "BTCUSDT", - "max_rounds": 3, - "current_round": 0, - "participants": [ - { - "id": "participant-uuid", - "ai_model_id": "deepseek-v3", - "ai_model_name": "DeepSeek V3", - "provider": "deepseek", - "personality": "bull", - "color": "#22C55E", - "speak_order": 0 - } - ], - "messages": [], - "votes": [], - "created_at": "2025-12-15T12:00:00Z" -} -``` - -### 7.4 Execute Consensus Request - -```json -POST /api/debates/:id/execute -{ - "trader_id": "trader-uuid" -} -``` - ---- - -## 8. Real-Time Updates (SSE) - -### 8.1 SSE Endpoint - -**Location:** `api/debate.go:HandleDebateStream()` (Lines 407-453) - -``` -GET /api/debates/:id/stream - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. Validate user ownership │ -│ 2. Set SSE headers: │ -│ Content-Type: text/event-stream │ -│ Cache-Control: no-cache │ -│ Connection: keep-alive │ -│ 3. Send initial state │ -│ 4. Subscribe to events │ -│ 5. Stream updates until client disconnects │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 8.2 Event Types - -| Event | Trigger | Data | -|-------|---------|------| -| `initial` | Connection start | Full session state | -| `round_start` | Round begins | `{round, status}` | -| `message` | AI speaks | DebateMessage object | -| `round_end` | Round complete | `{round, status}` | -| `vote` | AI votes | DebateVote object | -| `consensus` | Debate complete | DebateDecision object | -| `error` | Error occurs | `{error: string}` | - -### 8.3 SSE Message Format - -``` -event: message -data: {"id":"msg-uuid","session_id":"session-uuid","round":1,"ai_model_name":"DeepSeek V3","personality":"bull","content":"...","decision":{"action":"open_long","symbol":"BTCUSDT","confidence":75}} - -event: vote -data: {"id":"vote-uuid","session_id":"session-uuid","ai_model_name":"DeepSeek V3","action":"open_long","symbol":"BTCUSDT","confidence":80,"reasoning":"..."} - -event: consensus -data: {"action":"open_long","symbol":"BTCUSDT","confidence":75,"leverage":8,"position_pct":0.25,"stop_loss":0.03,"take_profit":0.06} -``` - ---- - -## 9. Database Schema - -### 9.1 Tables - -**debate_sessions:** -```sql -CREATE TABLE debate_sessions ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - strategy_id TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - symbol TEXT NOT NULL, - max_rounds INTEGER DEFAULT 3, - current_round INTEGER DEFAULT 0, - interval_minutes INTEGER DEFAULT 5, - prompt_variant TEXT DEFAULT 'balanced', - final_decision TEXT, - final_decisions TEXT, - auto_execute BOOLEAN DEFAULT 0, - trader_id TEXT, - enable_oi_ranking BOOLEAN DEFAULT 0, - oi_ranking_limit INTEGER DEFAULT 10, - oi_duration TEXT DEFAULT '1h', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -``` - -**debate_participants:** -```sql -CREATE TABLE debate_participants ( - id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - ai_model_id TEXT NOT NULL, - ai_model_name TEXT NOT NULL, - provider TEXT NOT NULL, - personality TEXT NOT NULL, - color TEXT NOT NULL, - speak_order INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES debate_sessions(id) ON DELETE CASCADE -); -``` - -**debate_messages:** -```sql -CREATE TABLE debate_messages ( - id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - round INTEGER NOT NULL, - ai_model_id TEXT NOT NULL, - ai_model_name TEXT NOT NULL, - provider TEXT NOT NULL, - personality TEXT NOT NULL, - message_type TEXT NOT NULL, - content TEXT NOT NULL, - decision TEXT, - decisions TEXT, - confidence INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES debate_sessions(id) ON DELETE CASCADE -); -``` - -**debate_votes:** -```sql -CREATE TABLE debate_votes ( - id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - ai_model_id TEXT NOT NULL, - ai_model_name TEXT NOT NULL, - action TEXT NOT NULL, - symbol TEXT NOT NULL, - confidence INTEGER DEFAULT 0, - leverage INTEGER DEFAULT 5, - position_pct REAL DEFAULT 0.2, - stop_loss_pct REAL DEFAULT 0.03, - take_profit_pct REAL DEFAULT 0.06, - reasoning TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES debate_sessions(id) ON DELETE CASCADE -); -``` - -### 9.2 Key Store Methods - -| Method | Description | -|--------|-------------| -| `CreateSession()` | Create new debate session | -| `GetSession()` | Get session by ID | -| `GetSessionWithDetails()` | Get session with participants, messages, votes | -| `UpdateSessionStatus()` | Update session status | -| `UpdateSessionRound()` | Update current round | -| `UpdateSessionFinalDecisions()` | Store consensus decisions | -| `AddParticipant()` | Add AI participant | -| `AddMessage()` | Store debate message | -| `AddVote()` | Store final vote | - ---- - -## 10. Frontend Components - -### 10.1 Page Structure - -**Location:** `web/src/pages/DebateArenaPage.tsx` - -``` -DebateArenaPage -├── Left Sidebar (w-56) -│ ├── New Debate Button -│ ├── Debate Sessions List -│ │ └── SessionItem (status, name, timestamp) -│ └── Online Traders List -│ └── TraderItem (name, status, AI model) -│ -├── Main Content -│ ├── Header Bar -│ │ ├── Session Info (name, status, symbol) -│ │ ├── Participants Avatars -│ │ └── Vote Summary -│ │ -│ ├── Content Area (two-column) -│ │ ├── Left: Discussion Records -│ │ │ ├── Round Headers -│ │ │ └── MessageCards (expandable) -│ │ │ -│ │ └── Right: Final Votes -│ │ └── VoteCards (action, confidence, reasoning) -│ │ -│ └── Consensus Bar -│ ├── Final Decision Display -│ └── Execute Button (if auto_execute disabled) -│ -└── Modals - ├── CreateModal - │ ├── Name Input - │ ├── Strategy Selector - │ ├── Symbol Input (auto-filled) - │ ├── Max Rounds Selector - │ └── Participant Picker (AI model + personality) - │ - └── ExecuteModal - └── Trader Selector -``` - -### 10.2 UI Components - -**MessageCard:** -- Expandable message display -- Shows AI avatar, personality emoji, decision -- Parses reasoning/analysis sections from content -- Displays decision details (leverage, position, SL/TP) -- Supports multi-coin decisions - -**VoteCard:** -- Confidence bar visualization -- Action indicator (long/short/hold/wait) -- Leverage and position size display -- Stop loss and take profit display -- Reasoning preview - -### 10.3 Status Colors - -```typescript -const STATUS_COLOR = { - pending: 'bg-gray-500', - running: 'bg-blue-500 animate-pulse', - voting: 'bg-yellow-500 animate-pulse', - completed: 'bg-green-500', - cancelled: 'bg-red-500', -} -``` - -### 10.4 Action Styling - -```typescript -const ACT = { - open_long: { - color: 'text-green-400', - bg: 'bg-green-500/20', - icon: , - label: 'LONG' - }, - open_short: { - color: 'text-red-400', - bg: 'bg-red-500/20', - icon: , - label: 'SHORT' - }, - hold: { - color: 'text-blue-400', - bg: 'bg-blue-500/20', - icon: , - label: 'HOLD' - }, - wait: { - color: 'text-gray-400', - bg: 'bg-gray-500/20', - icon: , - label: 'WAIT' - }, -} -``` - -### 10.5 Personality Colors - -```typescript -const PERS = { - bull: { emoji: '🐂', color: '#22C55E', name: '多头', nameEn: 'Bull' }, - bear: { emoji: '🐻', color: '#EF4444', name: '空头', nameEn: 'Bear' }, - analyst: { emoji: '📊', color: '#3B82F6', name: '分析', nameEn: 'Analyst' }, - contrarian: { emoji: '🔄', color: '#F59E0B', name: '逆势', nameEn: 'Contrarian' }, - risk_manager: { emoji: '🛡️', color: '#8B5CF6', name: '风控', nameEn: 'Risk Mgr' }, -} -``` - ---- - -## 11. Integration Points - -### 11.1 Strategy System - -Debate sessions depend on saved strategies for: -- **Coin source configuration:** static/pool/OI top -- **Market data indicators:** K-lines, timeframes, technical indicators -- **Risk control parameters:** leverage limits, position sizing -- **Custom prompts:** role definition, trading rules - -### 11.2 AI Model System - -Each participant requires: -- AI model configuration (provider, API key, custom URL) -- Supported providers: deepseek, qwen, openai, claude, gemini, grok, kimi -- Client initialization with timeout handling (60s per call) - -### 11.3 Trader System - -For auto-execution: -- Requires active trader with running status -- Trader must have valid exchange connection -- Executor interface: `ExecuteDecision()`, `GetBalance()` - -### 11.4 Market Data - -Market context building uses: -- Market data service (K-lines, OHLCV) -- Quantitative data (capital flow, position changes) -- OI ranking data (market-wide position changes) - ---- - -## 12. Error Handling - -### 12.1 Cleanup on Startup - -**Location:** `debate/engine.go:cleanupStaleDebates()` (Lines 58-71) - -```go -// On server restart, cancel all running/voting debates -func cleanupStaleDebates() { - sessions := debateStore.ListAllSessions() - for _, session := range sessions { - if session.Status == running || session.Status == voting { - debateStore.UpdateSessionStatus(session.ID, cancelled) - } - } -} -``` - -### 12.2 AI Call Timeout - -```go -// 60 seconds per participant response -select { -case res := <-resultCh: - response = res.response -case <-time.After(60 * time.Second): - return nil, fmt.Errorf("AI call timeout") -} -``` - -### 12.3 Symbol Validation - -```go -// Force all decisions to use session symbol if specified -if session.Symbol != "" { - for _, d := range decisions { - if d.Symbol == "" || d.Symbol != session.Symbol { - logger.Warnf("Fixing invalid symbol '%s' -> '%s'", d.Symbol, session.Symbol) - d.Symbol = session.Symbol - } - } -} -``` - -### 12.4 Panic Recovery - -```go -defer func() { - if r := recover(); r != nil { - logger.Errorf("Debate panic: %v", r) - debateStore.UpdateSessionStatus(sessionID, cancelled) - if OnError != nil { - OnError(sessionID, fmt.Errorf("panic: %v", r)) - } - } -}() -``` - ---- - -## Summary - -The Debate Arena module provides a sophisticated multi-AI collaborative decision system with: - -- **Multi-Personality Debate:** 5 distinct AI personalities (Bull, Bear, Analyst, Contrarian, Risk Manager) with unique trading biases -- **Consensus Mechanism:** Weighted voting based on confidence levels to determine final decisions -- **Real-Time Updates:** SSE streaming for live debate progress -- **Auto-Execution:** Optional automatic trade execution based on consensus -- **Strategy Integration:** Deep integration with strategy configuration for market data and risk parameters -- **Multi-Coin Support:** Ability to analyze and decide on multiple coins simultaneously - -The system enables users to leverage multiple AI perspectives for more robust trading decisions while maintaining full control over execution. diff --git a/docs/architecture/DEBATE_MODULE.zh-CN.md b/docs/architecture/DEBATE_MODULE.zh-CN.md deleted file mode 100644 index 21782e06..00000000 --- a/docs/architecture/DEBATE_MODULE.zh-CN.md +++ /dev/null @@ -1,606 +0,0 @@ -# NOFX 辩论竞技场模块 - 技术文档 - -**语言:** [English](DEBATE_MODULE.md) | [中文](DEBATE_MODULE.zh-CN.md) - -## 概述 - -辩论竞技场是一个多 AI 协作决策系统,多个具有不同性格的 AI 模型对市场状况进行辩论并达成交易决策共识。系统支持多轮辩论、实时流推送、投票机制和自动交易执行。 - ---- - -## 1. 架构概览 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 辩论竞技场系统 │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ 多头 AI │ │ 空头 AI │ │ 分析 AI │ │ 风控 AI │ │ -│ │ 🐂 │ │ 🐻 │ │ 📊 │ │ 🛡️ │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -│ │ │ │ │ │ -│ └──────────────────┴──────────────────┴──────────────────┘ │ -│ │ │ -│ ┌─────────▼─────────┐ │ -│ │ 辩论引擎 │ │ -│ │ (debate/engine) │ │ -│ └─────────┬─────────┘ │ -│ │ │ -│ ┌──────────────────────────┼──────────────────────────┐ │ -│ │ │ │ │ -│ ┌──────▼──────┐ ┌─────────▼─────────┐ ┌────────▼────────┐ │ -│ │ 市场数据 │ │ 投票系统 │ │ 自动执行器 │ │ -│ │ 组装 │ │ 与共识机制 │ │ (可选) │ │ -│ └─────────────┘ └───────────────────┘ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### 文件结构 - -``` -├── debate/ -│ └── engine.go # 核心辩论引擎逻辑 -├── api/ -│ └── debate.go # HTTP 处理器和 SSE 流 -├── store/ -│ └── debate.go # 数据库操作和模式 -└── web/src/pages/ - └── DebateArenaPage.tsx # 前端 UI -``` - ---- - -## 2. 性格系统 - -### 2.1 可用性格 - -| 性格 | 图标 | 名称 | 描述 | 交易偏向 | -|------|------|------|------|----------| -| Bull | 🐂 | 激进多头 | 寻找做多机会 | 乐观,趋势跟随 | -| Bear | 🐻 | 谨慎空头 | 关注风险 | 悲观,做空偏向 | -| Analyst | 📊 | 数据分析师 | 纯数据驱动 | 无偏见,客观分析 | -| Contrarian | 🔄 | 逆势者 | 挑战多数观点 | 另类视角 | -| Risk Manager | 🛡️ | 风控经理 | 关注风险控制 | 仓位管理,止损 | - -### 2.2 性格提示词增强 - -**文件位置:** `debate/engine.go:buildDebateSystemPrompt()` (365-426行) - -``` -## 辩论模式 - 第 {round}/{max_rounds} 轮 - -你作为 {emoji} {personality} 参与辩论。 - -### 你的辩论角色: -{personality_description} - -### 辩论规则: -1. 分析所有候选币种 -2. 用具体数据支持论点 -3. 回应其他参与者 (第2轮起) -4. 有说服力但基于数据 -5. 可以推荐多个不同操作的币种 - -### 输出格式 (严格 JSON): - - - 带数据引用的市场分析 - - 主要交易论点 - - 对他人的回应 (第2轮起) - - - -[ - {"symbol": "BTCUSDT", "action": "open_long", "confidence": 75, ...}, - {"symbol": "ETHUSDT", "action": "open_short", "confidence": 80, ...} -] - -``` - ---- - -## 3. 辩论执行流程 - -### 3.1 会话创建 - -``` -POST /api/debates - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. 验证用户认证 │ -│ 2. 解析 CreateDebateRequest: │ -│ - name, strategy_id, symbol, max_rounds, participants │ -│ - interval_minutes, prompt_variant, auto_execute │ -│ 3. 验证策略所有权 │ -│ 4. 自动选择币种 (如未提供): │ -│ - 静态币种 → 使用策略第一个币种 │ -│ - CoinPool → 从 AI500 API 获取 │ -│ - OI Top → 从 OI 排行 API 获取 │ -│ - Mixed → 先尝试池,回退到 OI │ -│ 5. 设置默认值: │ -│ - max_rounds: 3 (范围 2-5) │ -│ - interval_minutes: 5 │ -│ - prompt_variant: "balanced" │ -│ 6. 在数据库创建 DebateSession │ -│ 7. 添加带 AI 模型和性格的参与者 │ -│ 8. 返回完整会话及参与者 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 3.2 辩论轮次执行 - -**文件位置:** `debate/engine.go:runDebate()` (157-289行) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 每轮 (1 到 max_rounds): │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 1. 广播 "round_start" 事件 │ │ -│ │ 2. 每个参与者 (按 speak_order): │ │ -│ │ ┌─────────────────────────────────────────────────┐ │ │ -│ │ │ a. 构建性格增强的系统提示词 │ │ │ -│ │ │ b. 构建用户提示词: │ │ │ -│ │ │ - 市场数据 (来自策略引擎) │ │ │ -│ │ │ - 之前的辩论消息 (第2轮起) │ │ │ -│ │ │ c. 调用 AI 模型,60秒超时 │ │ │ -│ │ │ d. 从响应解析多币种决策 │ │ │ -│ │ │ e. 保存消息到数据库 │ │ │ -│ │ │ f. 广播 "message" 事件 │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ 3. 广播 "round_end" 事件 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ 所有轮次后: │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 1. 进入投票阶段 (status = "voting") │ │ -│ │ 2. 收集所有参与者的最终投票 │ │ -│ │ 3. 确定多币种共识 │ │ -│ │ 4. 存储最终决策 │ │ -│ │ 5. 更新状态为 "completed" │ │ -│ │ 6. 广播 "consensus" 事件 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. 共识算法 - -### 4.1 投票收集 - -**文件位置:** `debate/engine.go:collectVotes()` (542-567行) - -``` -每个参与者: -┌─────────────────────────────────────────────────────────────┐ -│ 1. 构建投票系统提示词 │ -│ 2. 构建带辩论摘要的投票用户提示词 │ -│ 3. 调用 AI 模型获取最终投票 │ -│ 4. 解析多币种决策 │ -│ 5. 验证/修复币种与 session.Symbol 一致 │ -│ 6. 保存投票到数据库 │ -│ 7. 广播 "vote" 事件 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 4.2 多币种共识确定 - -**文件位置:** `debate/engine.go:determineMultiCoinConsensus()` (752-924行) - -**算法:** - -``` -1. 收集所有投票中的所有币种决策 -2. 按 symbol → action → 聚合数据 分组 - -3. 对每个投票决策: - weight = confidence / 100.0 - 累加: - ┌─────────────────────────────────────────────────────────┐ - │ score += weight │ - │ total_confidence += confidence │ - │ total_leverage += leverage │ - │ total_position_pct += position_pct │ - │ total_stop_loss += stop_loss │ - │ total_take_profit += take_profit │ - │ count++ │ - └─────────────────────────────────────────────────────────┘ - -4. 对每个币种: - 找到胜出操作 (最高 score) - 计算平均值: - ┌─────────────────────────────────────────────────────────┐ - │ avg_confidence = total_confidence / count │ - │ avg_leverage = clamp(total_leverage / count, 1, 20) │ - │ avg_position_pct = clamp(total_pct / count, 0.1, 1.0) │ - │ avg_stop_loss = 默认 3% (如未设置) │ - │ avg_take_profit = 默认 6% (如未设置) │ - └─────────────────────────────────────────────────────────┘ - -5. 返回共识决策数组 -``` - -### 4.3 共识示例 - -**输入投票:** -``` -AI1 (多头): BTC open_long (conf=80, lev=10, pos=0.3) -AI2 (空头): BTC open_short (conf=60, lev=5, pos=0.2) -AI3 (分析): BTC open_long (conf=70, lev=8, pos=0.25) -``` - -**计算:** -``` -open_long: - score = 0.80 + 0.70 = 1.50 - avg_conf = (80 + 70) / 2 = 75 - avg_lev = (10 + 8) / 2 = 9 - avg_pos = (0.3 + 0.25) / 2 = 0.275 - -open_short: - score = 0.60 - avg_conf = 60 - avg_lev = 5 - avg_pos = 0.2 - -胜出: open_long (score 1.50 > 0.60) -``` - -**输出:** -```json -{ - "symbol": "BTCUSDT", - "action": "open_long", - "confidence": 75, - "leverage": 9, - "position_pct": 0.275, - "stop_loss": 0.03, - "take_profit": 0.06 -} -``` - ---- - -## 5. 自动执行 - -### 5.1 执行流程 - -**文件位置:** `debate/engine.go:ExecuteConsensus()` (932-1052行) - -``` -POST /api/debates/:id/execute - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. 验证会话状态 = completed │ -│ 2. 验证 final_decision 存在且未执行 │ -│ 3. 验证操作是 open_long 或 open_short │ -│ 4. 获取当前市场价格 │ -│ 5. 获取账户余额: │ -│ - 尝试 available_balance │ -│ - 回退到 total_equity 或 wallet_balance │ -│ 6. 计算仓位大小: │ -│ position_size_usd = available_balance × position_pct │ -│ (最小 $12 以满足交易所要求) │ -│ 7. 计算止损和止盈价格: │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ open_long: │ │ -│ │ SL = price × (1 - stop_loss_pct) │ │ -│ │ TP = price × (1 + take_profit_pct) │ │ -│ │ open_short: │ │ -│ │ SL = price × (1 + stop_loss_pct) │ │ -│ │ TP = price × (1 - take_profit_pct) │ │ -│ └───────────────────────────────────────────────────┘ │ -│ 8. 创建 Decision 对象 │ -│ 9. 调用 executor.ExecuteDecision() │ -│ 10. 更新 final_decision: │ -│ - executed = true/false │ -│ - executed_at = 时间戳 │ -│ - error 消息 (如失败) │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 6. API 接口 - -### 6.1 接口列表 - -| 接口 | 方法 | 描述 | -|------|------|------| -| `/api/debates` | GET | 列出用户所有辩论 | -| `/api/debates/personalities` | GET | 获取 AI 性格配置 | -| `/api/debates/:id` | GET | 获取辩论详情 | -| `/api/debates` | POST | 创建新辩论 | -| `/api/debates/:id/start` | POST | 开始辩论执行 | -| `/api/debates/:id/cancel` | POST | 取消运行中的辩论 | -| `/api/debates/:id/execute` | POST | 执行共识交易 | -| `/api/debates/:id` | DELETE | 删除辩论 | -| `/api/debates/:id/messages` | GET | 获取所有消息 | -| `/api/debates/:id/votes` | GET | 获取所有投票 | -| `/api/debates/:id/stream` | GET | SSE 实时流 | - -### 6.2 创建辩论请求 - -```json -POST /api/debates -{ - "name": "BTC 市场辩论", - "strategy_id": "strategy-uuid", - "symbol": "BTCUSDT", - "max_rounds": 3, - "interval_minutes": 5, - "prompt_variant": "balanced", - "auto_execute": false, - "trader_id": "trader-uuid", - "enable_oi_ranking": true, - "oi_ranking_limit": 10, - "oi_duration": "1h", - "participants": [ - {"ai_model_id": "deepseek-v3", "personality": "bull"}, - {"ai_model_id": "qwen-max", "personality": "bear"}, - {"ai_model_id": "gpt-5.2", "personality": "analyst"} - ] -} -``` - ---- - -## 7. 实时更新 (SSE) - -### 7.1 SSE 接口 - -**文件位置:** `api/debate.go:HandleDebateStream()` (407-453行) - -``` -GET /api/debates/:id/stream - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. 验证用户所有权 │ -│ 2. 设置 SSE 头: │ -│ Content-Type: text/event-stream │ -│ Cache-Control: no-cache │ -│ Connection: keep-alive │ -│ 3. 发送初始状态 │ -│ 4. 订阅事件 │ -│ 5. 流式推送更新直到客户端断开 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 7.2 事件类型 - -| 事件 | 触发时机 | 数据 | -|------|----------|------| -| `initial` | 连接开始 | 完整会话状态 | -| `round_start` | 轮次开始 | `{round, status}` | -| `message` | AI 发言 | DebateMessage 对象 | -| `round_end` | 轮次结束 | `{round, status}` | -| `vote` | AI 投票 | DebateVote 对象 | -| `consensus` | 辩论完成 | DebateDecision 对象 | -| `error` | 发生错误 | `{error: string}` | - ---- - -## 8. 数据库模式 - -### 8.1 表结构 - -**debate_sessions:** -```sql -CREATE TABLE debate_sessions ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - strategy_id TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - symbol TEXT NOT NULL, - max_rounds INTEGER DEFAULT 3, - current_round INTEGER DEFAULT 0, - interval_minutes INTEGER DEFAULT 5, - prompt_variant TEXT DEFAULT 'balanced', - final_decision TEXT, - final_decisions TEXT, - auto_execute BOOLEAN DEFAULT 0, - trader_id TEXT, - enable_oi_ranking BOOLEAN DEFAULT 0, - oi_ranking_limit INTEGER DEFAULT 10, - oi_duration TEXT DEFAULT '1h', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -``` - -**debate_participants:** -```sql -CREATE TABLE debate_participants ( - id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - ai_model_id TEXT NOT NULL, - ai_model_name TEXT NOT NULL, - provider TEXT NOT NULL, - personality TEXT NOT NULL, - color TEXT NOT NULL, - speak_order INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES debate_sessions(id) ON DELETE CASCADE -); -``` - -**debate_messages:** -```sql -CREATE TABLE debate_messages ( - id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - round INTEGER NOT NULL, - ai_model_id TEXT NOT NULL, - ai_model_name TEXT NOT NULL, - provider TEXT NOT NULL, - personality TEXT NOT NULL, - message_type TEXT NOT NULL, - content TEXT NOT NULL, - decision TEXT, - decisions TEXT, - confidence INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES debate_sessions(id) ON DELETE CASCADE -); -``` - -**debate_votes:** -```sql -CREATE TABLE debate_votes ( - id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - ai_model_id TEXT NOT NULL, - ai_model_name TEXT NOT NULL, - action TEXT NOT NULL, - symbol TEXT NOT NULL, - confidence INTEGER DEFAULT 0, - leverage INTEGER DEFAULT 5, - position_pct REAL DEFAULT 0.2, - stop_loss_pct REAL DEFAULT 0.03, - take_profit_pct REAL DEFAULT 0.06, - reasoning TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES debate_sessions(id) ON DELETE CASCADE -); -``` - ---- - -## 9. 前端组件 - -### 9.1 页面结构 - -**文件位置:** `web/src/pages/DebateArenaPage.tsx` - -``` -DebateArenaPage -├── 左侧边栏 (w-56) -│ ├── 新建辩论按钮 -│ ├── 辩论会话列表 -│ │ └── SessionItem (状态, 名称, 时间戳) -│ └── 在线交易员列表 -│ └── TraderItem (名称, 状态, AI 模型) -│ -├── 主内容区 -│ ├── 头部栏 -│ │ ├── 会话信息 (名称, 状态, 币种) -│ │ ├── 参与者头像 -│ │ └── 投票摘要 -│ │ -│ ├── 内容区 (双栏) -│ │ ├── 左: 讨论记录 -│ │ │ ├── 轮次标题 -│ │ │ └── MessageCards (可展开) -│ │ │ -│ │ └── 右: 最终投票 -│ │ └── VoteCards (操作, 置信度, 理由) -│ │ -│ └── 共识栏 -│ ├── 最终决策显示 -│ └── 执行按钮 (如果 auto_execute 禁用) -│ -└── 弹窗 - ├── CreateModal - │ ├── 名称输入 - │ ├── 策略选择器 - │ ├── 币种输入 (自动填充) - │ ├── 最大轮数选择器 - │ └── 参与者选择器 (AI 模型 + 性格) - │ - └── ExecuteModal - └── 交易员选择器 -``` - -### 9.2 状态颜色 - -```typescript -const STATUS_COLOR = { - pending: 'bg-gray-500', - running: 'bg-blue-500 animate-pulse', - voting: 'bg-yellow-500 animate-pulse', - completed: 'bg-green-500', - cancelled: 'bg-red-500', -} -``` - -### 9.3 操作样式 - -```typescript -const ACT = { - open_long: { - color: 'text-green-400', - bg: 'bg-green-500/20', - icon: , - label: 'LONG' - }, - open_short: { - color: 'text-red-400', - bg: 'bg-red-500/20', - icon: , - label: 'SHORT' - }, - hold: { - color: 'text-blue-400', - bg: 'bg-blue-500/20', - icon: , - label: 'HOLD' - }, - wait: { - color: 'text-gray-400', - bg: 'bg-gray-500/20', - icon: , - label: 'WAIT' - }, -} -``` - ---- - -## 10. 集成点 - -### 10.1 策略系统 - -辩论会话依赖保存的策略: -- **币种来源配置:** static/pool/OI top -- **市场数据指标:** K线、时间周期、技术指标 -- **风控参数:** 杠杆限制、仓位大小 -- **自定义提示词:** 角色定义、交易规则 - -### 10.2 AI 模型系统 - -每个参与者需要: -- AI 模型配置 (provider, API key, 自定义 URL) -- 支持的 providers: deepseek, qwen, openai, claude, gemini, grok, kimi -- 客户端初始化带超时处理 (每次调用 60s) - -### 10.3 交易员系统 - -自动执行需要: -- 运行中状态的活跃交易员 -- 交易员必须有有效的交易所连接 -- 执行器接口: `ExecuteDecision()`, `GetBalance()` - ---- - -## 总结 - -辩论竞技场模块提供了一个复杂的多 AI 协作决策系统: - -- **多性格辩论:** 5 种独特的 AI 性格 (多头、空头、分析师、逆势者、风控经理),具有独特的交易偏向 -- **共识机制:** 基于置信度的加权投票来确定最终决策 -- **实时更新:** SSE 流推送实时辩论进度 -- **自动执行:** 可选的基于共识的自动交易执行 -- **策略集成:** 与策略配置深度集成,用于市场数据和风控参数 -- **多币种支持:** 能够同时分析和决策多个币种 - -该系统使用户能够利用多个 AI 视角做出更稳健的交易决策,同时保持对执行的完全控制。 - ---- - -**文档版本:** 1.0.0 -**最后更新:** 2025-01-15 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 4da12317..fd20dde3 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -25,11 +25,11 @@ NOFX is a full-stack AI trading platform for cryptocurrency and US stock markets ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐│ -│ │ Strategy │ │ Backtest │ │ Debate │ │ Live Trading ││ -│ │ Studio │ │ Engine │ │ Arena │ │ (Auto Trader) ││ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘│ -│ │ │ │ │ │ -│ └────────────────┴────────────────┴────────────────────┘ │ +│ │ Strategy │ │ Backtest │ │ Live Trading ││ +│ │ Studio │ │ Engine │ │ (Auto Trader) ││ +│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘│ +│ │ │ │ │ +│ └────────────────┴────────────────────┘ │ │ │ │ │ ┌─────────▼─────────┐ │ │ │ Core Services │ │ @@ -58,7 +58,6 @@ NOFX is a full-stack AI trading platform for cryptocurrency and US stock markets |--------|-------------|---------------| | **Strategy Studio** | Strategy configuration, coin selection, data assembly, AI prompts | [STRATEGY_MODULE.md](STRATEGY_MODULE.md) | | **Backtest Engine** | Historical simulation, performance metrics, AI decision replay | [BACKTEST_MODULE.md](BACKTEST_MODULE.md) | -| **Debate Arena** | Multi-AI collaborative decision making with voting consensus | [DEBATE_MODULE.md](DEBATE_MODULE.md) | ### Module Overview @@ -82,16 +81,6 @@ Historical trading simulation engine: **[Read Full Documentation →](BACKTEST_MODULE.md)** -#### Debate Module -Multi-AI collaborative decision system: -- 5 AI personalities (Bull, Bear, Analyst, Contrarian, Risk Manager) -- Multi-round debate with market context -- Weighted voting and consensus algorithm -- Auto-execution to live trading -- Real-time SSE streaming - -**[Read Full Documentation →](DEBATE_MODULE.md)** - --- ## Project Structure @@ -103,7 +92,6 @@ nofx/ ├── trader/ # Trading execution layer ├── strategy/ # Strategy engine ├── backtest/ # Backtest simulation engine -├── debate/ # Debate arena engine ├── market/ # Market data service ├── mcp/ # AI model clients ├── store/ # Database operations @@ -144,7 +132,6 @@ nofx/ - [Strategy Module](STRATEGY_MODULE.md) - How strategies work - [Backtest Module](BACKTEST_MODULE.md) - How backtesting works -- [Debate Module](DEBATE_MODULE.md) - How AI debates work - [Getting Started](../getting-started/README.md) - Setup guide - [FAQ](../faq/README.md) - Frequently asked questions diff --git a/docs/architecture/README.zh-CN.md b/docs/architecture/README.zh-CN.md index 995d3200..f3a16b59 100644 --- a/docs/architecture/README.zh-CN.md +++ b/docs/architecture/README.zh-CN.md @@ -25,11 +25,11 @@ NOFX 是一个支持加密货币和美股市场的全栈 AI 交易平台: ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐│ -│ │ 策略 │ │ 回测 │ │ 辩论 │ │ 实盘交易 ││ -│ │ 工作室 │ │ 引擎 │ │ 竞技场 │ │ (自动交易员) ││ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘│ -│ │ │ │ │ │ -│ └────────────────┴────────────────┴────────────────────┘ │ +│ │ 策略 │ │ 回测 │ │ 实盘交易 ││ +│ │ 工作室 │ │ 引擎 │ │ (自动交易员) ││ +│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘│ +│ │ │ │ │ +│ └────────────────┴────────────────────┘ │ │ │ │ │ ┌─────────▼─────────┐ │ │ │ 核心服务 │ │ @@ -58,7 +58,6 @@ NOFX 是一个支持加密货币和美股市场的全栈 AI 交易平台: |------|------|------| | **策略工作室** | 策略配置、币种选择、数据组装、AI 提示词 | [STRATEGY_MODULE.md](STRATEGY_MODULE.md) | | **回测引擎** | 历史模拟、性能指标、AI 决策回放 | [BACKTEST_MODULE.md](BACKTEST_MODULE.md) | -| **辩论竞技场** | 多 AI 协作决策,投票共识机制 | [DEBATE_MODULE.md](DEBATE_MODULE.md) | ### 模块概览 @@ -82,16 +81,6 @@ NOFX 是一个支持加密货币和美股市场的全栈 AI 交易平台: **[阅读完整文档 →](BACKTEST_MODULE.md)** -#### 辩论模块 -多 AI 协作决策系统: -- 5 种 AI 性格(多头、空头、分析师、逆势者、风控) -- 多轮辩论与市场数据上下文 -- 加权投票与共识算法 -- 自动执行到实盘交易 -- SSE 实时流推送 - -**[阅读完整文档 →](DEBATE_MODULE.md)** - --- ## 项目结构 @@ -103,7 +92,6 @@ nofx/ ├── trader/ # 交易执行层 ├── strategy/ # 策略引擎 ├── backtest/ # 回测模拟引擎 -├── debate/ # 辩论竞技场引擎 ├── market/ # 行情数据服务 ├── mcp/ # AI 模型客户端 ├── store/ # 数据库操作 @@ -144,7 +132,6 @@ nofx/ - [策略模块](STRATEGY_MODULE.md) - 策略如何运作 - [回测模块](BACKTEST_MODULE.md) - 回测如何运作 -- [辩论模块](DEBATE_MODULE.md) - AI 辩论如何运作 - [快速开始](../getting-started/README.zh-CN.md) - 部署指南 - [常见问题](../faq/README.md) - FAQ diff --git a/docs/i18n/vi/README.md b/docs/i18n/vi/README.md index 243c6f81..0eafcd48 100644 --- a/docs/i18n/vi/README.md +++ b/docs/i18n/vi/README.md @@ -77,7 +77,6 @@ Tương thích với **[ClawRouter](https://github.com/BlockRunAI/ClawRouter)** | **Đa AI** | DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — chuyển đổi bất cứ lúc nào | | **Đa Sàn** | Binance, Bybit, OKX, Bitget, KuCoin, Gate, Hyperliquid, Aster, Lighter | | **Strategy Studio** | Trình xây dựng trực quan — nguồn coin, chỉ báo, kiểm soát rủi ro | -| **AI Debate Arena** | Nhiều AI tranh luận giao dịch (Bull vs Bear vs Analyst), bỏ phiếu, thực thi | | **AI Competition** | AI cạnh tranh thời gian thực, bảng xếp hạng hiệu suất | | **Telegram Agent** | Chat với trợ lý giao dịch — streaming, gọi công cụ, bộ nhớ | | **Backtest Lab** | Mô phỏng lịch sử, đường vốn và chỉ số hiệu suất | diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 3078c2b7..d25edb6e 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -79,7 +79,6 @@ x402 流程: | **多 AI** | DeepSeek、Qwen、GPT、Claude、Gemini、Grok、Kimi — 随时切换 | | **多交易所** | Binance、Bybit、OKX、Bitget、KuCoin、Gate、Hyperliquid、Aster、Lighter | | **策略工作室** | 可视化构建器 — 币种来源、指标、风控 | -| **AI 辩论竞技场** | 多个 AI 辩论交易(多空对决),投票执行 | | **AI 竞赛** | AI 实时竞争,排行榜排名 | | **Telegram Agent** | 与交易助手对话 — 流式输出、工具调用、记忆 | | **回测实验室** | 历史模拟,权益曲线和性能指标 | @@ -193,7 +192,6 @@ curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bas | [架构概览](../../architecture/README.md) | 系统设计和模块索引 | | [策略模块](../../architecture/STRATEGY_MODULE.md) | 币种选择、AI 提示词、执行 | | [回测模块](../../architecture/BACKTEST_MODULE.md) | 历史模拟、指标计算 | -| [辩论模块](../../architecture/DEBATE_MODULE.md) | 多 AI 辩论、投票共识 | | [常见问题](../../faq/README.md) | FAQ | | [快速开始](../../getting-started/README.md) | 部署指南 | diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 59fa401c..ab4b6583 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -3,8 +3,6 @@ package manager import ( "context" "fmt" - "nofx/debate" - "nofx/kernel" "nofx/logger" "nofx/store" "nofx/trader" @@ -13,27 +11,6 @@ import ( "time" ) -// TraderExecutorAdapter wraps AutoTrader to implement debate.TraderExecutor -type TraderExecutorAdapter struct { - autoTrader *trader.AutoTrader -} - -// ExecuteDecision executes a trading decision -func (a *TraderExecutorAdapter) ExecuteDecision(d *kernel.Decision) error { - return a.autoTrader.ExecuteDecision(d) -} - -// GetBalance returns account balance -func (a *TraderExecutorAdapter) GetBalance() (map[string]interface{}, error) { - info, err := a.autoTrader.GetAccountInfo() - if err != nil { - return nil, fmt.Errorf("failed to get account info: %w", err) - } - // Log the balance for debugging - logger.Infof("[Debate] GetBalance for trader, result: %+v", info) - return info, nil -} - // CompetitionCache competition data cache type CompetitionCache struct { data map[string]interface{} @@ -764,12 +741,3 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg return nil } -// GetTraderExecutor returns a TraderExecutor for the given trader ID -// This is used by the debate module to execute consensus trades -func (tm *TraderManager) GetTraderExecutor(traderID string) (debate.TraderExecutor, error) { - at, err := tm.GetTrader(traderID) - if err != nil { - return nil, err - } - return &TraderExecutorAdapter{autoTrader: at}, nil -} diff --git a/store/ai_model.go b/store/ai_model.go index b74d5780..e3380d3e 100644 --- a/store/ai_model.go +++ b/store/ai_model.go @@ -94,7 +94,7 @@ func (s *AIModelStore) Get(userID, modelID string) (*AIModel, error) { return nil, gorm.ErrRecordNotFound } -// GetByID retrieves an AI model by ID only (for debate engine) +// GetByID retrieves an AI model by ID only func (s *AIModelStore) GetByID(modelID string) (*AIModel, error) { if modelID == "" { return nil, fmt.Errorf("model ID cannot be empty") diff --git a/store/debate.go b/store/debate.go deleted file mode 100644 index 03d7600f..00000000 --- a/store/debate.go +++ /dev/null @@ -1,496 +0,0 @@ -package store - -import ( - "encoding/json" - "time" - - "github.com/google/uuid" - "gorm.io/gorm" -) - -// DebateStatus represents the status of a debate session -type DebateStatus string - -const ( - DebateStatusPending DebateStatus = "pending" - DebateStatusRunning DebateStatus = "running" - DebateStatusVoting DebateStatus = "voting" - DebateStatusCompleted DebateStatus = "completed" - DebateStatusCancelled DebateStatus = "cancelled" -) - -// DebatePersonality represents AI personality types -type DebatePersonality string - -const ( - PersonalityBull DebatePersonality = "bull" // Aggressive Bull - looks for long opportunities - PersonalityBear DebatePersonality = "bear" // Cautious Bear - skeptical, focuses on risks - PersonalityAnalyst DebatePersonality = "analyst" // Data Analyst - pure technical analysis - PersonalityContrarian DebatePersonality = "contrarian" // Contrarian - challenges majority opinion - PersonalityRiskManager DebatePersonality = "risk_manager" // Risk Manager - focuses on position sizing -) - -// PersonalityColors maps personalities to colors for UI -var PersonalityColors = map[DebatePersonality]string{ - PersonalityBull: "#22C55E", // Green - PersonalityBear: "#EF4444", // Red - PersonalityAnalyst: "#3B82F6", // Blue - PersonalityContrarian: "#F59E0B", // Amber - PersonalityRiskManager: "#8B5CF6", // Purple -} - -// PersonalityEmojis maps personalities to emojis -var PersonalityEmojis = map[DebatePersonality]string{ - PersonalityBull: "🐂", - PersonalityBear: "🐻", - PersonalityAnalyst: "📊", - PersonalityContrarian: "🔄", - PersonalityRiskManager: "🛡️", -} - -// DebateDecision represents a trading decision from the debate -type DebateDecision struct { - Action string `json:"action"` // open_long/open_short/close_long/close_short/hold/wait - Symbol string `json:"symbol"` // Trading pair - Confidence int `json:"confidence"` // 0-100 - Leverage int `json:"leverage"` // Recommended leverage - PositionPct float64 `json:"position_pct"` // Position size as percentage of equity (0.0-1.0) - PositionSizeUSD float64 `json:"position_size_usd"` // Position size in USD (calculated from pct) - StopLoss float64 `json:"stop_loss"` // Stop loss price - TakeProfit float64 `json:"take_profit"` // Take profit price - Reasoning string `json:"reasoning"` // Brief reasoning - - // Execution tracking - Executed bool `json:"executed"` // Whether this decision was executed - ExecutedAt time.Time `json:"executed_at,omitempty"` // When it was executed - OrderID string `json:"order_id,omitempty"` // Exchange order ID - Error string `json:"error,omitempty"` // Execution error if any -} - -// DebateSession represents a debate session (API struct) -type DebateSession struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - StrategyID string `json:"strategy_id"` - Status DebateStatus `json:"status"` - Symbol string `json:"symbol"` // Primary symbol (for backward compat, may be empty for multi-coin) - MaxRounds int `json:"max_rounds"` - CurrentRound int `json:"current_round"` - IntervalMinutes int `json:"interval_minutes"` // Debate interval (5, 15, 30, 60 minutes) - PromptVariant string `json:"prompt_variant"` // balanced/aggressive/conservative/scalping - FinalDecision *DebateDecision `json:"final_decision,omitempty"` // Single decision (backward compat) - FinalDecisions []*DebateDecision `json:"final_decisions,omitempty"` // Multi-coin decisions - AutoExecute bool `json:"auto_execute"` - TraderID string `json:"trader_id,omitempty"` // Trader to use for auto-execute - // OI Ranking data options - EnableOIRanking bool `json:"enable_oi_ranking"` // Whether to include OI ranking data - OIRankingLimit int `json:"oi_ranking_limit"` // Number of OI ranking entries (default 10) - OIDuration string `json:"oi_duration"` // Duration for OI data (1h, 4h, 24h, etc.) - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// DebateSessionDB is the GORM model for debate_sessions -type DebateSessionDB struct { - ID string `gorm:"column:id;primaryKey"` - UserID string `gorm:"column:user_id;not null;index"` - Name string `gorm:"column:name;not null"` - StrategyID string `gorm:"column:strategy_id;not null"` - Status DebateStatus `gorm:"column:status;not null;default:pending;index"` - Symbol string `gorm:"column:symbol;not null"` - MaxRounds int `gorm:"column:max_rounds;default:3"` - CurrentRound int `gorm:"column:current_round;default:0"` - IntervalMinutes int `gorm:"column:interval_minutes;default:5"` - PromptVariant string `gorm:"column:prompt_variant;default:balanced"` - FinalDecision string `gorm:"column:final_decision"` // JSON string - AutoExecute bool `gorm:"column:auto_execute;default:false"` - TraderID string `gorm:"column:trader_id"` - EnableOIRanking bool `gorm:"column:enable_oi_ranking;default:false"` - OIRankingLimit int `gorm:"column:oi_ranking_limit;default:10"` - OIDuration string `gorm:"column:oi_duration;default:1h"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` - UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` -} - -func (DebateSessionDB) TableName() string { - return "debate_sessions" -} - -func (db *DebateSessionDB) toSession() *DebateSession { - s := &DebateSession{ - ID: db.ID, - UserID: db.UserID, - Name: db.Name, - StrategyID: db.StrategyID, - Status: db.Status, - Symbol: db.Symbol, - MaxRounds: db.MaxRounds, - CurrentRound: db.CurrentRound, - IntervalMinutes: db.IntervalMinutes, - PromptVariant: db.PromptVariant, - AutoExecute: db.AutoExecute, - TraderID: db.TraderID, - EnableOIRanking: db.EnableOIRanking, - OIRankingLimit: db.OIRankingLimit, - OIDuration: db.OIDuration, - CreatedAt: db.CreatedAt, - UpdatedAt: db.UpdatedAt, - } - - // Set defaults - if s.IntervalMinutes == 0 { - s.IntervalMinutes = 5 - } - if s.PromptVariant == "" { - s.PromptVariant = "balanced" - } - if s.OIRankingLimit == 0 { - s.OIRankingLimit = 10 - } - if s.OIDuration == "" { - s.OIDuration = "1h" - } - - // Parse final decision - if db.FinalDecision != "" { - var decision DebateDecision - if json.Unmarshal([]byte(db.FinalDecision), &decision) == nil { - s.FinalDecision = &decision - } - } - - return s -} - -// DebateParticipant represents an AI participant in a debate -type DebateParticipant struct { - ID string `gorm:"column:id;primaryKey" json:"id"` - SessionID string `gorm:"column:session_id;not null;index" json:"session_id"` - AIModelID string `gorm:"column:ai_model_id;not null" json:"ai_model_id"` - AIModelName string `gorm:"column:ai_model_name;not null" json:"ai_model_name"` - Provider string `gorm:"column:provider;not null" json:"provider"` - Personality DebatePersonality `gorm:"column:personality;not null" json:"personality"` - Color string `gorm:"column:color;not null" json:"color"` - SpeakOrder int `gorm:"column:speak_order;default:0" json:"speak_order"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` -} - -func (DebateParticipant) TableName() string { - return "debate_participants" -} - -// DebateMessage represents a message in the debate -type DebateMessage struct { - ID string `gorm:"column:id;primaryKey" json:"id"` - SessionID string `gorm:"column:session_id;not null;index" json:"session_id"` - Round int `gorm:"column:round;not null" json:"round"` - AIModelID string `gorm:"column:ai_model_id;not null" json:"ai_model_id"` - AIModelName string `gorm:"column:ai_model_name;not null" json:"ai_model_name"` - Provider string `gorm:"column:provider;not null" json:"provider"` - Personality DebatePersonality `gorm:"column:personality;not null" json:"personality"` - MessageType string `gorm:"column:message_type;not null" json:"message_type"` // analysis/rebuttal/final/vote - Content string `gorm:"column:content;not null" json:"content"` - DecisionRaw string `gorm:"column:decision" json:"-"` // JSON string in DB - Decision *DebateDecision `gorm:"-" json:"decision,omitempty"` // Parsed for API - Decisions []*DebateDecision `gorm:"-" json:"decisions,omitempty"` // Multi-coin decisions - Confidence int `gorm:"column:confidence;default:0" json:"confidence"` - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` -} - -func (DebateMessage) TableName() string { - return "debate_messages" -} - -// DebateVote represents a final vote from an AI (can contain multiple coin decisions) -type DebateVote struct { - ID string `gorm:"column:id;primaryKey" json:"id"` - SessionID string `gorm:"column:session_id;not null;index" json:"session_id"` - AIModelID string `gorm:"column:ai_model_id;not null" json:"ai_model_id"` - AIModelName string `gorm:"column:ai_model_name;not null" json:"ai_model_name"` - Action string `gorm:"column:action;not null" json:"action"` // Primary action (backward compat) - Symbol string `gorm:"column:symbol;not null" json:"symbol"` // Primary symbol (backward compat) - Confidence int `gorm:"column:confidence;default:0" json:"confidence"` - Leverage int `gorm:"column:leverage;default:5" json:"leverage"` - PositionPct float64 `gorm:"column:position_pct;default:0.2" json:"position_pct"` - StopLossPct float64 `gorm:"column:stop_loss_pct;default:0.03" json:"stop_loss_pct"` - TakeProfitPct float64 `gorm:"column:take_profit_pct;default:0.06" json:"take_profit_pct"` - Reasoning string `gorm:"column:reasoning" json:"reasoning"` - Decisions []*DebateDecision `gorm:"-" json:"decisions,omitempty"` // Multi-coin decisions - CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"` -} - -func (DebateVote) TableName() string { - return "debate_votes" -} - -// DebateStore handles database operations for debates -type DebateStore struct { - db *gorm.DB -} - -// NewDebateStore creates a new DebateStore -func NewDebateStore(db *gorm.DB) *DebateStore { - return &DebateStore{db: db} -} - -// InitSchema creates the debate tables using GORM AutoMigrate -func (s *DebateStore) InitSchema() error { - return s.db.AutoMigrate( - &DebateSessionDB{}, - &DebateParticipant{}, - &DebateMessage{}, - &DebateVote{}, - ) -} - -// CreateSession creates a new debate session -func (s *DebateStore) CreateSession(session *DebateSession) error { - if session.ID == "" { - session.ID = uuid.New().String() - } - session.Status = DebateStatusPending - session.CurrentRound = 0 - if session.IntervalMinutes == 0 { - session.IntervalMinutes = 5 - } - if session.PromptVariant == "" { - session.PromptVariant = "balanced" - } - if session.OIRankingLimit == 0 { - session.OIRankingLimit = 10 - } - if session.OIDuration == "" { - session.OIDuration = "1h" - } - - db := &DebateSessionDB{ - ID: session.ID, - UserID: session.UserID, - Name: session.Name, - StrategyID: session.StrategyID, - Status: session.Status, - Symbol: session.Symbol, - MaxRounds: session.MaxRounds, - CurrentRound: session.CurrentRound, - IntervalMinutes: session.IntervalMinutes, - PromptVariant: session.PromptVariant, - AutoExecute: session.AutoExecute, - TraderID: session.TraderID, - EnableOIRanking: session.EnableOIRanking, - OIRankingLimit: session.OIRankingLimit, - OIDuration: session.OIDuration, - } - - return s.db.Create(db).Error -} - -// GetSession gets a debate session by ID -func (s *DebateStore) GetSession(id string) (*DebateSession, error) { - var db DebateSessionDB - if err := s.db.Where("id = ?", id).First(&db).Error; err != nil { - return nil, err - } - return db.toSession(), nil -} - -// GetSessionsByUser gets all debate sessions for a user -func (s *DebateStore) GetSessionsByUser(userID string) ([]*DebateSession, error) { - var dbs []DebateSessionDB - if err := s.db.Where("user_id = ?", userID).Order("created_at DESC").Find(&dbs).Error; err != nil { - return nil, err - } - - sessions := make([]*DebateSession, len(dbs)) - for i, db := range dbs { - sessions[i] = db.toSession() - } - return sessions, nil -} - -// ListAllSessions returns all debate sessions (for cleanup on startup) -func (s *DebateStore) ListAllSessions() ([]*DebateSession, error) { - var dbs []DebateSessionDB - if err := s.db.Select("id, status").Find(&dbs).Error; err != nil { - return nil, err - } - - sessions := make([]*DebateSession, len(dbs)) - for i, db := range dbs { - sessions[i] = &DebateSession{ID: db.ID, Status: db.Status} - } - return sessions, nil -} - -// UpdateSessionStatus updates the status of a debate session -func (s *DebateStore) UpdateSessionStatus(id string, status DebateStatus) error { - return s.db.Model(&DebateSessionDB{}).Where("id = ?", id).Update("status", status).Error -} - -// UpdateSessionRound updates the current round of a debate session -func (s *DebateStore) UpdateSessionRound(id string, round int) error { - return s.db.Model(&DebateSessionDB{}).Where("id = ?", id).Update("current_round", round).Error -} - -// UpdateSessionFinalDecision updates the final decision of a debate session (single decision) -func (s *DebateStore) UpdateSessionFinalDecision(id string, decision *DebateDecision) error { - decisionJSON, err := json.Marshal(decision) - if err != nil { - return err - } - return s.db.Model(&DebateSessionDB{}).Where("id = ?", id).Updates(map[string]interface{}{ - "final_decision": string(decisionJSON), - "status": DebateStatusCompleted, - }).Error -} - -// UpdateSessionFinalDecisions updates both single and multi-coin final decisions -func (s *DebateStore) UpdateSessionFinalDecisions(id string, primaryDecision *DebateDecision, allDecisions []*DebateDecision) error { - primaryJSON, err := json.Marshal(primaryDecision) - if err != nil { - return err - } - return s.db.Model(&DebateSessionDB{}).Where("id = ?", id).Updates(map[string]interface{}{ - "final_decision": string(primaryJSON), - "status": DebateStatusCompleted, - }).Error -} - -// DeleteSession deletes a debate session and all related data -func (s *DebateStore) DeleteSession(id string) error { - // Delete related data first - s.db.Where("session_id = ?", id).Delete(&DebateParticipant{}) - s.db.Where("session_id = ?", id).Delete(&DebateMessage{}) - s.db.Where("session_id = ?", id).Delete(&DebateVote{}) - return s.db.Where("id = ?", id).Delete(&DebateSessionDB{}).Error -} - -// AddParticipant adds a participant to a debate session -func (s *DebateStore) AddParticipant(participant *DebateParticipant) error { - if participant.ID == "" { - participant.ID = uuid.New().String() - } - if participant.Color == "" { - if color, ok := PersonalityColors[participant.Personality]; ok { - participant.Color = color - } else { - participant.Color = "#6B7280" // Default gray - } - } - return s.db.Create(participant).Error -} - -// GetParticipants gets all participants for a debate session -func (s *DebateStore) GetParticipants(sessionID string) ([]*DebateParticipant, error) { - var participants []*DebateParticipant - err := s.db.Where("session_id = ?", sessionID).Order("speak_order").Find(&participants).Error - return participants, err -} - -// AddMessage adds a message to a debate session -func (s *DebateStore) AddMessage(msg *DebateMessage) error { - if msg.ID == "" { - msg.ID = uuid.New().String() - } - if msg.Decision != nil { - data, err := json.Marshal(msg.Decision) - if err != nil { - return err - } - msg.DecisionRaw = string(data) - } - return s.db.Create(msg).Error -} - -// GetMessages gets all messages for a debate session -func (s *DebateStore) GetMessages(sessionID string) ([]*DebateMessage, error) { - var messages []*DebateMessage - err := s.db.Where("session_id = ?", sessionID).Order("round, created_at").Find(&messages).Error - if err != nil { - return nil, err - } - - // Parse decision JSON - for _, msg := range messages { - if msg.DecisionRaw != "" { - var decision DebateDecision - if json.Unmarshal([]byte(msg.DecisionRaw), &decision) == nil { - msg.Decision = &decision - } - } - } - return messages, nil -} - -// GetMessagesByRound gets messages for a specific round -func (s *DebateStore) GetMessagesByRound(sessionID string, round int) ([]*DebateMessage, error) { - var messages []*DebateMessage - err := s.db.Where("session_id = ? AND round = ?", sessionID, round).Order("created_at").Find(&messages).Error - if err != nil { - return nil, err - } - - // Parse decision JSON - for _, msg := range messages { - if msg.DecisionRaw != "" { - var decision DebateDecision - if json.Unmarshal([]byte(msg.DecisionRaw), &decision) == nil { - msg.Decision = &decision - } - } - } - return messages, nil -} - -// AddVote adds a vote to a debate session -func (s *DebateStore) AddVote(vote *DebateVote) error { - if vote.ID == "" { - vote.ID = uuid.New().String() - } - return s.db.Create(vote).Error -} - -// GetVotes gets all votes for a debate session -func (s *DebateStore) GetVotes(sessionID string) ([]*DebateVote, error) { - var votes []*DebateVote - err := s.db.Where("session_id = ?", sessionID).Order("created_at").Find(&votes).Error - return votes, err -} - -// DebateSessionWithDetails combines session with participants and messages -type DebateSessionWithDetails struct { - *DebateSession - Participants []*DebateParticipant `json:"participants"` - Messages []*DebateMessage `json:"messages"` - Votes []*DebateVote `json:"votes"` -} - -// GetSessionWithDetails gets a session with all related data -func (s *DebateStore) GetSessionWithDetails(id string) (*DebateSessionWithDetails, error) { - session, err := s.GetSession(id) - if err != nil { - return nil, err - } - - participants, err := s.GetParticipants(id) - if err != nil { - return nil, err - } - - messages, err := s.GetMessages(id) - if err != nil { - return nil, err - } - - votes, err := s.GetVotes(id) - if err != nil { - return nil, err - } - - return &DebateSessionWithDetails{ - DebateSession: session, - Participants: participants, - Messages: messages, - Votes: votes, - }, nil -} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 3ee4777b..ef3a3478 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -1085,33 +1085,6 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *kernel.Decision, actio } } -// ExecuteDecision executes a trading decision from external sources (e.g., debate consensus) -// This is a public method that can be called by other modules -func (at *AutoTrader) ExecuteDecision(d *kernel.Decision) error { - logger.Infof("[%s] Executing external decision: %s %s", at.name, d.Action, d.Symbol) - - // Create a minimal action record for tracking - actionRecord := &store.DecisionAction{ - Symbol: d.Symbol, - Action: d.Action, - Leverage: d.Leverage, - StopLoss: d.StopLoss, - TakeProfit: d.TakeProfit, - Confidence: d.Confidence, - Reasoning: d.Reasoning, - } - - // Execute the decision - err := at.executeDecisionWithRecord(d, actionRecord) - if err != nil { - logger.Errorf("[%s] External decision execution failed: %v", at.name, err) - return err - } - - logger.Infof("[%s] External decision executed successfully: %s %s", at.name, d.Action, d.Symbol) - return nil -} - // executeOpenLongWithRecord executes open long position and records detailed information func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error { logger.Infof(" 📈 Open long: %s", decision.Symbol) diff --git a/web/src/App.tsx b/web/src/App.tsx index 240295a8..e270264d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -13,7 +13,6 @@ import { CompetitionPage } from './components/CompetitionPage' import { LandingPage } from './pages/LandingPage' import { FAQPage } from './pages/FAQPage' import { StrategyStudioPage } from './pages/StrategyStudioPage' -import { DebateArenaPage } from './pages/DebateArenaPage' import { StrategyMarketPage } from './pages/StrategyMarketPage' import { DataPage } from './pages/DataPage' import { LoginRequiredOverlay } from './components/LoginRequiredOverlay' @@ -44,7 +43,6 @@ type Page = | 'strategy' | 'strategy-market' | 'data' - | 'debate' | 'faq' | 'login' | 'register' @@ -72,7 +70,6 @@ function App() { if (path === '/strategy' || hash === 'strategy') return 'strategy' if (path === '/strategy-market' || hash === 'strategy-market') return 'strategy-market' if (path === '/data' || hash === 'data') return 'data' - if (path === '/debate' || hash === 'debate') return 'debate' if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader' return 'competition' // 默认为竞赛页面 @@ -97,7 +94,6 @@ function App() { 'trader': '/dashboard', 'backtest': '/backtest', 'strategy': '/strategy', - 'debate': '/debate', 'faq': '/faq', 'login': '/login', 'register': '/register', @@ -159,8 +155,6 @@ function App() { setCurrentPage('strategy-market') } else if (path === '/data' || hash === 'data') { setCurrentPage('data') - } else if (path === '/debate' || hash === 'debate') { - setCurrentPage('debate') } else if ( path === '/dashboard' || hash === 'trader' || @@ -418,7 +412,6 @@ function App() { 'trader': '/dashboard', 'backtest': '/backtest', 'strategy': '/strategy', - 'debate': '/debate', 'faq': '/faq', } const path = pathMap[page] @@ -507,8 +500,6 @@ function App() { ) : currentPage === 'strategy' ? ( - ) : currentPage === 'debate' ? ( - ) : ( - {/* Footer - Hidden on debate page */} - {currentPage !== 'debate' && ( -
@@ -658,7 +648,6 @@ function App() {
- )} {/* Login Required Overlay */} { - const result = await httpClient.get(`${API_BASE}/debates`) - if (!result.success) throw new Error('获取辩论列表失败') - return Array.isArray(result.data) ? result.data : [] - }, - - async getDebate(debateId: string): Promise { - const result = await httpClient.get(`${API_BASE}/debates/${debateId}`) - if (!result.success) throw new Error('获取辩论详情失败') - return result.data! - }, - - async createDebate(request: CreateDebateRequest): Promise { - const result = await httpClient.post(`${API_BASE}/debates`, request) - if (!result.success) throw new Error('创建辩论失败') - return result.data! - }, - - async startDebate(debateId: string): Promise { - const result = await httpClient.post(`${API_BASE}/debates/${debateId}/start`) - if (!result.success) throw new Error('启动辩论失败') - }, - - async cancelDebate(debateId: string): Promise { - const result = await httpClient.post(`${API_BASE}/debates/${debateId}/cancel`) - if (!result.success) throw new Error('取消辩论失败') - }, - - async executeDebate(debateId: string, traderId: string): Promise { - const result = await httpClient.post<{ message: string; session: DebateSessionWithDetails }>( - `${API_BASE}/debates/${debateId}/execute`, - { trader_id: traderId } - ) - if (!result.success) throw new Error('执行交易失败') - return result.data!.session - }, - - async deleteDebate(debateId: string): Promise { - const result = await httpClient.delete(`${API_BASE}/debates/${debateId}`) - if (!result.success) throw new Error('删除辩论失败') - }, - - async getDebateMessages(debateId: string): Promise { - const result = await httpClient.get(`${API_BASE}/debates/${debateId}/messages`) - if (!result.success) throw new Error('获取辩论消息失败') - return result.data! - }, - - async getDebateVotes(debateId: string): Promise { - const result = await httpClient.get(`${API_BASE}/debates/${debateId}/votes`) - if (!result.success) throw new Error('获取辩论投票失败') - return result.data! - }, - - async getDebatePersonalities(): Promise { - const result = await httpClient.get(`${API_BASE}/debates/personalities`) - if (!result.success) throw new Error('获取AI性格列表失败') - return result.data! - }, - - // SSE stream for live debate updates - createDebateStream(debateId: string): EventSource { - const token = localStorage.getItem('auth_token') - return new EventSource(`${API_BASE}/debates/${debateId}/stream?token=${token}`) - }, - // Position History API async getPositionHistory(traderId: string, limit: number = 100): Promise { const result = await httpClient.get( diff --git a/web/src/pages/DebateArenaPage.tsx b/web/src/pages/DebateArenaPage.tsx deleted file mode 100644 index eda7b9bd..00000000 --- a/web/src/pages/DebateArenaPage.tsx +++ /dev/null @@ -1,800 +0,0 @@ -import { useState, useEffect } from 'react' -import useSWR from 'swr' -import { api } from '../lib/api' -import { notify } from '../lib/notify' -import { useLanguage } from '../contexts/LanguageContext' -import { PunkAvatar } from '../components/PunkAvatar' -import type { - DebateSession, - DebateSessionWithDetails, - DebateMessage, - CreateDebateRequest, - AIModel, - Strategy, - DebatePersonality, - TraderInfo, -} from '../types' -import { - Plus, - X, - Trophy, - Loader2, - TrendingUp, - TrendingDown, - Minus, - Clock, - Zap, - ChevronDown, - ChevronUp, -} from 'lucide-react' -import { DeepVoidBackground } from '../components/DeepVoidBackground' - -// Translations -const T: Record> = { - newDebate: { zh: '新建辩论', en: 'New Debate' }, - debateSessions: { zh: '辩论会话', en: 'Sessions' }, - onlineTraders: { zh: '在线交易员', en: 'Online Traders' }, - offline: { zh: '离线', en: 'Offline' }, - noTraders: { zh: '暂无交易员', en: 'No traders' }, - start: { zh: '开始', en: 'Start' }, - delete: { zh: '删除', en: 'Delete' }, - discussionRecords: { zh: '讨论记录', en: 'Discussion' }, - finalVotes: { zh: '最终投票', en: 'Final Votes' }, - consensus: { zh: '共识', en: 'Consensus' }, - confidence: { zh: '信心', en: 'Confidence' }, - leverage: { zh: '杠杆', en: 'Leverage' }, - position: { zh: '仓位', en: 'Position' }, - execute: { zh: '执行', en: 'Execute' }, - executed: { zh: '已执行', en: 'Executed' }, - selectOrCreate: { zh: '选择或创建辩论', en: 'Select or create a debate' }, - clickToStart: { zh: '点击左侧"开始"启动辩论', en: 'Click "Start" to begin' }, - waitingAI: { zh: '等待AI发言...', en: 'Waiting for AI...' }, - createDebate: { zh: '创建辩论', en: 'Create Debate' }, - debateName: { zh: '辩论名称', en: 'Debate Name' }, - tradingPair: { zh: '交易对', en: 'Trading Pair' }, - strategy: { zh: '策略', en: 'Strategy' }, - rounds: { zh: '轮数', en: 'Rounds' }, - participants: { zh: '参与者', en: 'Participants' }, - addAI: { zh: '添加AI', en: 'Add AI' }, - cancel: { zh: '取消', en: 'Cancel' }, - create: { zh: '创建', en: 'Create' }, - creating: { zh: '创建中...', en: 'Creating...' }, - executeTitle: { zh: '执行交易', en: 'Execute Trade' }, - selectTrader: { zh: '选择交易员', en: 'Select Trader' }, - executing: { zh: '执行中...', en: 'Executing...' }, - fillNameAdd2AI: { zh: '请填写名称并添加至少2个AI', en: 'Please fill name and add at least 2 AI' }, -} -const t = (key: string, lang: string) => T[key]?.[lang] || T[key]?.en || key - -// Personality config -const PERS: Record = { - bull: { emoji: '🐂', color: '#22C55E', name: '多头', nameEn: 'Bull' }, - bear: { emoji: '🐻', color: '#EF4444', name: '空头', nameEn: 'Bear' }, - analyst: { emoji: '📊', color: '#3B82F6', name: '分析', nameEn: 'Analyst' }, - contrarian: { emoji: '🔄', color: '#F59E0B', name: '逆势', nameEn: 'Contrarian' }, - risk_manager: { emoji: '🛡️', color: '#8B5CF6', name: '风控', nameEn: 'Risk Mgr' }, -} - -// Action config -const ACT: Record = { - open_long: { color: 'text-green-400', bg: 'bg-green-500/20', icon: , label: 'LONG' }, - open_short: { color: 'text-red-400', bg: 'bg-red-500/20', icon: , label: 'SHORT' }, - hold: { color: 'text-blue-400', bg: 'bg-blue-500/20', icon: , label: 'HOLD' }, - wait: { color: 'text-gray-400', bg: 'bg-gray-500/20', icon: , label: 'WAIT' }, - close_long: { color: 'text-yellow-400', bg: 'bg-yellow-500/20', icon: , label: 'CLOSE' }, - close_short: { color: 'text-yellow-400', bg: 'bg-yellow-500/20', icon: , label: 'CLOSE' }, -} - -// Status colors -const STATUS_COLOR: Record = { - pending: 'bg-gray-500', - running: 'bg-blue-500 animate-pulse', - voting: 'bg-yellow-500 animate-pulse', - completed: 'bg-green-500', - cancelled: 'bg-red-500', -} - -// AI Provider Avatar -function AIAvatar({ name, size = 24 }: { name: string; size?: number }) { - const providers: Record = { - claude: { bg: 'bg-orange-500', text: 'text-white', letter: 'C' }, - deepseek: { bg: 'bg-blue-600', text: 'text-white', letter: 'D' }, - gemini: { bg: 'bg-blue-400', text: 'text-white', letter: 'G' }, - grok: { bg: 'bg-gray-700', text: 'text-white', letter: 'X' }, - kimi: { bg: 'bg-purple-500', text: 'text-white', letter: 'K' }, - qwen: { bg: 'bg-indigo-500', text: 'text-white', letter: 'Q' }, - openai: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' }, - minimax: { bg: 'bg-red-500', text: 'text-white', letter: 'M' }, - gpt: { bg: 'bg-emerald-600', text: 'text-white', letter: 'O' }, - } - const lower = name.toLowerCase() - const p = Object.entries(providers).find(([k]) => lower.includes(k))?.[1] - || { bg: 'bg-gray-600', text: 'text-white', letter: name[0]?.toUpperCase() || '?' } - return ( -
- {p.letter} -
- ) -} - -// Message Card - Full content display like AI Testing -function MessageCard({ msg }: { msg: DebateMessage }) { - const [open, setOpen] = useState(false) - const p = PERS[msg.personality] || PERS.analyst - const a = ACT[msg.decision?.action || 'wait'] || ACT.wait - - // Parse content into sections - const parseContent = (c: string) => { - const reasoning = c.match(/([\s\S]*?)<\/reasoning>/i)?.[1]?.trim() - const analysis = c.match(/([\s\S]*?)<\/analysis>/i)?.[1]?.trim() - const argument = c.match(/([\s\S]*?)<\/argument>/i)?.[1]?.trim() - const decision = c.match(/([\s\S]*?)<\/decision>/i)?.[1]?.trim() - - // Clean content - remove XML tags - const cleanContent = c.replace(/<\/?[^>]+(>|$)/g, '').trim() - - return { - reasoning: reasoning || analysis || argument, - decision, - fullContent: cleanContent - } - } - - const parsed = parseContent(msg.content) - const previewText = parsed.reasoning?.slice(0, 150) || parsed.fullContent.slice(0, 150) - - return ( -
- {/* Header - Always visible */} -
setOpen(!open)} - > - - {msg.ai_model_name} - {p.nameEn} -
- {msg.decision && ( - - {a.icon} {msg.decision.symbol || ''} {a.label} - - )} - {msg.decision?.confidence || msg.confidence}% - {open ? : } -
- - {/* Preview when collapsed */} - {!open && ( -
- {previewText}... -
- )} - - {/* Expanded Content - Full display */} - {open && ( -
- {/* Reasoning/Analysis Section */} - {parsed.reasoning && ( -
-
💭 思考过程 / Reasoning
-
- {parsed.reasoning} -
-
- )} - - {/* Decision Section */} - {msg.decision && ( -
-
📊 交易决策 / Decision
-
- {msg.decision.symbol && ( -
- 币种 - {msg.decision.symbol} -
- )} -
- 方向 - {a.label} -
-
- 信心 - {msg.decision.confidence}% -
- {(msg.decision.leverage ?? 0) > 0 && ( -
- 杠杆 - {msg.decision.leverage}x -
- )} - {(msg.decision.position_pct ?? 0) > 0 && ( -
- 仓位 - {((msg.decision.position_pct ?? 0) * 100).toFixed(0)}% -
- )} - {(msg.decision.stop_loss ?? 0) > 0 && ( -
- 止损 - {((msg.decision.stop_loss ?? 0) * 100).toFixed(1)}% -
- )} - {(msg.decision.take_profit ?? 0) > 0 && ( -
- 止盈 - {((msg.decision.take_profit ?? 0) * 100).toFixed(1)}% -
- )} -
- {msg.decision.reasoning && ( -
- {msg.decision.reasoning} -
- )} -
- )} - - {/* Full Raw Content (collapsible) */} - {!parsed.reasoning && ( -
-
📝 完整输出 / Full Output
-
- {parsed.fullContent} -
-
- )} - - {/* Multi-coin decisions if available */} - {msg.decisions && msg.decisions.length > 1 && ( -
-
🎯 多币种决策 ({msg.decisions.length})
-
- {msg.decisions.map((d, i) => { - const da = ACT[d.action] || ACT.wait - return ( -
- {d.symbol} - {da.icon} {da.label} - {d.confidence}% - {d.leverage || 0}x / {((d.position_pct || 0) * 100).toFixed(0)}% -
- ) - })} -
-
- )} -
- )} -
- ) -} - -// Vote Card - Beautiful detailed version -function VoteCard({ vote }: { vote: { ai_model_name: string; action: string; symbol?: string; confidence: number; leverage?: number; position_pct?: number; stop_loss_pct?: number; take_profit_pct?: number; reasoning: string } }) { - const a = ACT[vote.action] || ACT.wait - const confColor = vote.confidence >= 70 ? 'bg-green-500' : vote.confidence >= 50 ? 'bg-yellow-500' : 'bg-gray-500' - return ( -
-
-
- -
- {vote.ai_model_name} - {vote.symbol && {vote.symbol}} -
-
- - {a.icon} {vote.action.replace('_', ' ').toUpperCase()} - -
-
-
- Confidence - {vote.confidence}% -
-
-
-
-
-
-
Leverage{vote.leverage || '-'}x
-
Position{vote.position_pct ? `${(vote.position_pct * 100).toFixed(0)}%` : '-'}
-
SL{vote.stop_loss_pct ? `${(vote.stop_loss_pct * 100).toFixed(1)}%` : '-'}
-
TP{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}
-
- {vote.reasoning && ( -

{vote.reasoning}

- )} -
- ) -} - -// Create Modal (simplified) -function CreateModal({ - isOpen, onClose, onCreate, aiModels, strategies, language -}: { - isOpen: boolean; onClose: () => void; onCreate: (r: CreateDebateRequest) => Promise - aiModels: AIModel[]; strategies: Strategy[]; language: string -}) { - const [name, setName] = useState('') - const [symbol, setSymbol] = useState('') - const [strategyId, setStrategyId] = useState('') - const [maxRounds, setMaxRounds] = useState(3) - const [participants, setParticipants] = useState<{ ai_model_id: string; personality: DebatePersonality }[]>([]) - const [creating, setCreating] = useState(false) - - // Get the selected strategy's coin source config - const selectedStrategy = strategies.find(s => s.id === strategyId) - const coinSource = selectedStrategy?.config?.coin_source - const sourceType = coinSource?.source_type || 'static' - const staticCoins = coinSource?.static_coins || [] - // Only show coin selector for static type with coins defined - const isStaticWithCoins = sourceType === 'static' && staticCoins.length > 0 - - useEffect(() => { - if (isOpen) { - const firstStrategy = strategies[0] - const firstStrategyId = firstStrategy?.id || '' - const firstCoinSource = firstStrategy?.config?.coin_source - const firstSourceType = firstCoinSource?.source_type || 'static' - const firstStaticCoins = firstCoinSource?.static_coins || [] - setName('') - setStrategyId(firstStrategyId) - // Only set symbol for static type, otherwise leave empty (backend will choose) - setSymbol(firstSourceType === 'static' && firstStaticCoins.length > 0 ? firstStaticCoins[0] : '') - setMaxRounds(3) - setParticipants([]) - } - }, [isOpen, strategies]) - - // Update symbol when strategy changes - useEffect(() => { - if (isStaticWithCoins) { - if (!staticCoins.includes(symbol)) { - setSymbol(staticCoins[0]) - } - } else { - // Non-static strategy: clear symbol, backend will auto-select - setSymbol('') - } - }, [strategyId, isStaticWithCoins, staticCoins, symbol]) - - const addP = () => { - if (participants.length >= 10 || aiModels.length === 0) return - // Allow same AI model to be used multiple times with different personalities - const order: DebatePersonality[] = ['bull', 'bear', 'analyst', 'contrarian', 'risk_manager'] - // Cycle through personalities - const nextPersonality = order[participants.length % order.length] - setParticipants([...participants, { ai_model_id: aiModels[0].id, personality: nextPersonality }]) - } - - const submit = async () => { - if (!name || !strategyId || participants.length < 2) { - notify.error(t('fillNameAdd2AI', language)) - return - } - setCreating(true) - try { - await onCreate({ name, symbol, strategy_id: strategyId, max_rounds: maxRounds, participants }) - onClose() - } finally { setCreating(false) } - } - - if (!isOpen) return null - - return ( -
-
-
-

{t('createDebate', language)}

- -
- -
- setName(e.target.value)} - placeholder={t('debateName', language)} className="w-full px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm outline-none focus:border-nofx-gold" - /> - - {/* Strategy selector - moved up */} - - -
- {/* Show dropdown only for static type with coins defined */} - {isStaticWithCoins ? ( - - ) : ( -
- {language === 'zh' ? '根据策略规则自动选择' : 'Auto-selected by strategy'} -
- )} - -
- - {/* Participants */} -
- {participants.map((p, i) => ( -
- {/* Personality selector */} - - {/* AI model selector */} - - -
- ))} - -
-
- -
- - -
-
-
- ) -} - -// Main Page -export function DebateArenaPage() { - const { language } = useLanguage() - const [selectedId, setSelectedId] = useState(null) - const [showCreate, setShowCreate] = useState(false) - const [execId, setExecId] = useState(null) - const [traderId, setTraderId] = useState('') - const [executing, setExecuting] = useState(false) - - const { data: debates, mutate: mutateList } = useSWR('debates', api.getDebates, { refreshInterval: 5000 }) - const { data: aiModels } = useSWR('ai-models', api.getModelConfigs) - const { data: strategies } = useSWR('strategies', api.getStrategies) - const { data: traders } = useSWR('traders', api.getTraders) - const { data: detail, mutate: mutateDetail } = useSWR( - selectedId ? `debate-${selectedId}` : null, - () => api.getDebate(selectedId!), - { refreshInterval: selectedId ? 3000 : 0 } - ) - - useEffect(() => { - if (debates?.length && !selectedId) setSelectedId(debates[0].id) - }, [debates, selectedId]) - - const onCreate = async (r: CreateDebateRequest) => { - const d = await api.createDebate(r) - notify.success('创建成功') - mutateList() - setSelectedId(d.id) - } - - const onStart = async (id: string) => { - await api.startDebate(id) - notify.success('已开始') - mutateList(); mutateDetail() - } - - const onDelete = async (id: string) => { - await api.deleteDebate(id) - notify.success('已删除') - if (selectedId === id) setSelectedId(null) - mutateList() - } - - const onExecute = async () => { - if (!execId || !traderId) return - setExecuting(true) - try { - await api.executeDebate(execId, traderId) - notify.success('已执行') - mutateDetail(); mutateList() - setExecId(null); setTraderId('') - } catch (e: any) { notify.error(e.message) } - finally { setExecuting(false) } - } - - // Process data - const messages = detail?.messages || [] - const participants = detail?.participants || [] - const votes = detail?.votes || [] - const decision = detail?.final_decision - - // Get strategy name - const strategyName = strategies?.find(s => s.id === detail?.strategy_id)?.name || '' - - // Group by round - const rounds: Record = {} - messages.forEach(m => { if (!rounds[m.round]) rounds[m.round] = []; rounds[m.round].push(m) }) - - // Vote summary - const voteSum = votes.reduce((a, v) => { a[v.action] = (a[v.action] || 0) + 1; return a }, {} as Record) - - return ( - - - {/* Left - Debate List + Online Traders */} -
- {/* New Debate Button */} - - - {/* Debate List */} -
{t('debateSessions', language)}
-
- {debates?.map(d => ( -
setSelectedId(d.id)} - className={`p-2 cursor-pointer border-l-2 transition-all ${selectedId === d.id ? 'bg-nofx-gold/10 border-nofx-gold shadow-[inset_10px_0_20px_-10px_rgba(240,185,11,0.2)]' : 'border-transparent hover:bg-nofx-bg-lighter/50'}`}> -
- - {d.name} -
-
{d.symbol} · R{d.current_round}/{d.max_rounds}
- {d.status === 'pending' && selectedId === d.id && ( -
- - -
- )} -
- ))} -
- - {/* Online Traders Section */} -
-
- - {t('onlineTraders', language)} -
-
- {traders?.filter(tr => tr.is_running).map(tr => ( -
{ setTraderId(tr.trader_id); if (decision && !decision.executed) setExecId(detail?.id || null) }} - className={`p-2 rounded-lg cursor-pointer transition-all ${traderId === tr.trader_id ? 'bg-nofx-success/20 ring-1 ring-nofx-success' : 'bg-nofx-bg-lighter hover:bg-nofx-bg-light'}`}> -
- -
-
{tr.trader_name}
-
{tr.ai_model}
-
- -
-
- ))} - {traders?.filter(tr => !tr.is_running).slice(0, 3).map(tr => ( -
-
-
- -
-
-
{tr.trader_name}
-
{t('offline', language)}
-
-
-
- ))} - {(!traders || traders.length === 0) && ( -
{t('noTraders', language)}
- )} -
-
-
- - {/* Main Content */} -
- {detail ? ( - <> - {/* Header Bar - Compact */} -
- - {detail.name} - {detail.symbol} - {strategyName && {strategyName}} - R{detail.current_round}/{detail.max_rounds} - - {/* Participants */} -
- {participants.map(p => { - const vote = votes.find(v => v.ai_model_id === p.ai_model_id) - const act = vote ? (ACT[vote.action] || ACT.wait) : null - return ( -
- - {act && {act.icon}} -
- ) - })} -
- -
- - {/* Vote Summary */} - {votes.length > 0 && ( -
- {Object.entries(voteSum).map(([action, count]) => { - const cfg = ACT[action] || ACT.wait - return ( -
- {cfg.icon} {cfg.label}×{count} -
- ) - })} -
- )} -
- - {/* Main Content Area - Two Column Layout */} -
- {Object.keys(rounds).length === 0 ? ( -
-
{detail.status === 'pending' ? '🎯' : '⏳'}
-
{detail.status === 'pending' ? t('clickToStart', language) : t('waitingAI', language)}
-
- ) : ( - <> - {/* Left - Rounds */} -
-
- - {t('discussionRecords', language)} -
-
- {Object.entries(rounds).map(([round, msgs]) => ( -
-
Round {round}
-
- {msgs.map(m => )} -
-
- ))} -
-
- - {/* Right - Votes */} - {votes.length > 0 && ( -
-
- - {t('finalVotes', language)} -
-
- {votes.map(v => ( - - ))} -
-
- )} - - )} -
- - {/* Consensus Bar - Show when votes exist */} - {(decision || votes.length > 0) && ( -
-
- - {t('consensus', language)}: - {decision ? ( - <> - {decision.symbol && {decision.symbol}} - - {(ACT[decision.action] || ACT.wait).icon} - {decision.action.replace('_', ' ').toUpperCase()} - - - ) : ( - - VOTING... - - )} -
- {decision && ( -
- {t('confidence', language)} {decision.confidence || 0}% - {(decision.leverage ?? 0) > 0 && {t('leverage', language)} {decision.leverage}x} - {(decision.position_pct ?? 0) > 0 && {t('position', language)} {((decision.position_pct ?? 0) * 100).toFixed(0)}%} - {(decision.stop_loss ?? 0) > 0 && SL {((decision.stop_loss ?? 0) * 100).toFixed(1)}%} - {(decision.take_profit ?? 0) > 0 && TP {((decision.take_profit ?? 0) * 100).toFixed(1)}%} -
- )} -
- {decision && !decision.executed && (decision.action === 'open_long' || decision.action === 'open_short') && ( - - )} - {decision?.executed && ✓ {t('executed', language)}} -
- )} - - ) : ( -
-
-
🗳️
-
{t('selectOrCreate', language)}
-
-
- )} -
- - {/* Create Modal */} - setShowCreate(false)} onCreate={onCreate} - aiModels={aiModels || []} strategies={strategies || []} language={language} /> - - {/* Execute Modal */} - {execId && ( -
-
-

- {t('executeTitle', language)} -

- -
- ⚠️ {language === 'zh' ? '将使用账户余额执行真实交易' : 'Will execute real trade with account balance'} -
-
- - -
-
-
- )} - - ) -} diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx index 4c2c41ec..1ee97ccf 100644 --- a/web/src/pages/LandingPage.tsx +++ b/web/src/pages/LandingPage.tsx @@ -43,7 +43,6 @@ export function LandingPage() { 'trader': '/dashboard', 'backtest': '/backtest', 'strategy': '/strategy', - 'debate': '/debate', 'faq': '/faq', } const path = pathMap[page] diff --git a/web/src/types.ts b/web/src/types.ts index 6532d96f..b29a3440 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -616,123 +616,6 @@ export interface RiskControlConfig { min_confidence: number; // Min AI confidence to open position (AI guided) } -// Debate Arena Types -export type DebateStatus = 'pending' | 'running' | 'voting' | 'completed' | 'cancelled'; -export type DebatePersonality = 'bull' | 'bear' | 'analyst' | 'contrarian' | 'risk_manager'; - -export interface DebateDecision { - action: string; - symbol: string; - confidence: number; - leverage?: number; - position_pct?: number; - position_size_usd?: number; - stop_loss?: number; - take_profit?: number; - reasoning: string; - // Execution tracking - executed?: boolean; - executed_at?: string; - order_id?: string; - error?: string; -} - -export interface DebateSession { - id: string; - user_id: string; - name: string; - strategy_id: string; - status: DebateStatus; - symbol: string; - interval_minutes: number; - prompt_variant: string; - trader_id?: string; - max_rounds: number; - current_round: number; - final_decision?: DebateDecision; - final_decisions?: DebateDecision[]; // Multi-coin decisions - auto_execute: boolean; - created_at: string; - updated_at: string; -} - -export interface DebateParticipant { - id: string; - session_id: string; - ai_model_id: string; - ai_model_name: string; - provider: string; - personality: DebatePersonality; - color: string; - speak_order: number; - created_at: string; -} - -export interface DebateMessage { - id: string; - session_id: string; - round: number; - ai_model_id: string; - ai_model_name: string; - provider: string; - personality: DebatePersonality; - message_type: string; - content: string; - decision?: DebateDecision; - decisions?: DebateDecision[]; // Multi-coin decisions - confidence: number; - created_at: string; -} - -export interface DebateVote { - id: string; - session_id: string; - ai_model_id: string; - ai_model_name: string; - action: string; - symbol: string; - confidence: number; - leverage?: number; - position_pct?: number; - stop_loss_pct?: number; - take_profit_pct?: number; - reasoning: string; - created_at: string; -} - -export interface DebateSessionWithDetails extends DebateSession { - participants: DebateParticipant[]; - messages: DebateMessage[]; - votes: DebateVote[]; -} - -export interface CreateDebateRequest { - name: string; - strategy_id: string; - symbol: string; - max_rounds?: number; - interval_minutes?: number; // 5, 15, 30, 60 minutes - prompt_variant?: string; // balanced, aggressive, conservative, scalping - auto_execute?: boolean; - trader_id?: string; // Trader to use for auto-execute - // OI Ranking data options - enable_oi_ranking?: boolean; // Whether to include OI ranking data - oi_ranking_limit?: number; // Number of OI ranking entries (default 10) - oi_duration?: string; // Duration for OI data (1h, 4h, 24h, etc.) - participants: { - ai_model_id: string; - personality: DebatePersonality; - }[]; -} - -export interface DebatePersonalityInfo { - id: DebatePersonality; - name: string; - emoji: string; - color: string; - description: string; -} - // Position History Types export interface HistoricalPosition { id: number;