refactor: remove all Debate Arena feature code

Remove the entire AI Debate Arena module (~5,300 lines) to simplify
the codebase. This removes the multi-AI debate trading decision system
including backend engine, API handlers, database store, frontend page,
navigation, translations, and documentation references.
This commit is contained in:
tinkle-community
2026-03-11 17:32:41 +08:00
parent 5b82b51b17
commit 94ef009bb5
23 changed files with 22 additions and 5439 deletions

View File

@@ -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
| <img src="screenshots/competition-page.png" width="400"/> | <img src="screenshots/backtest-lab.png" width="400"/> |
</details>
<details>
<summary><b>Debate Arena</b></summary>
| AI Debate Session | Create Debate |
|:---:|:---:|
| <img src="screenshots/debate-arena.png" width="400"/> | <img src="screenshots/debate-create.png" width="400"/> |
</details>
---
## 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 |

View File

@@ -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,
})
}

View File

@@ -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",

File diff suppressed because it is too large Load Diff

View File

@@ -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):
<reasoning>
- Market analysis with data references
- Main trading thesis
- Response to others (if round > 1)
</reasoning>
<decision>
[
{"symbol": "BTCUSDT", "action": "open_long", "confidence": 75, ...},
{"symbol": "ETHUSDT", "action": "open_short", "confidence": 80, ...}
]
</decision>
```
### 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: <TrendingUp />,
label: 'LONG'
},
open_short: {
color: 'text-red-400',
bg: 'bg-red-500/20',
icon: <TrendingDown />,
label: 'SHORT'
},
hold: {
color: 'text-blue-400',
bg: 'bg-blue-500/20',
icon: <Minus />,
label: 'HOLD'
},
wait: {
color: 'text-gray-400',
bg: 'bg-gray-500/20',
icon: <Clock />,
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.

View File

@@ -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):
<reasoning>
- 带数据引用的市场分析
- 主要交易论点
- 对他人的回应 (第2轮起)
</reasoning>
<decision>
[
{"symbol": "BTCUSDT", "action": "open_long", "confidence": 75, ...},
{"symbol": "ETHUSDT", "action": "open_short", "confidence": 80, ...}
]
</decision>
```
---
## 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: <TrendingUp />,
label: 'LONG'
},
open_short: {
color: 'text-red-400',
bg: 'bg-red-500/20',
icon: <TrendingDown />,
label: 'SHORT'
},
hold: {
color: 'text-blue-400',
bg: 'bg-blue-500/20',
icon: <Minus />,
label: 'HOLD'
},
wait: {
color: 'text-gray-400',
bg: 'bg-gray-500/20',
icon: <Clock />,
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

View File

@@ -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

View File

@@ -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

View File

@@ -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ợ giao dịch streaming, gọi công cụ, bộ nhớ |
| **Backtest Lab** | phỏng lịch sử, đường vốn chỉ số hiệu suất |

View File

@@ -79,7 +79,6 @@ x402 流程:
| **多 AI** | DeepSeekQwenGPTClaudeGeminiGrokKimi 随时切换 |
| **多交易所** | BinanceBybitOKXBitgetKuCoinGateHyperliquidAsterLighter |
| **策略工作室** | 可视化构建器 币种来源指标风控 |
| **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) | 部署指南 |

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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() {
<BacktestPage />
) : currentPage === 'strategy' ? (
<StrategyStudioPage />
) : currentPage === 'debate' ? (
<DebateArenaPage />
) : (
<TraderDashboardPage
selectedTrader={selectedTrader}
@@ -546,9 +537,8 @@ function App() {
</AnimatePresence>
</main>
{/* Footer - Hidden on debate page */}
{currentPage !== 'debate' && (
<footer
{/* Footer */}
<footer
className="mt-16"
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
>
@@ -658,7 +648,6 @@ function App() {
</div>
</div>
</footer>
)}
{/* Login Required Overlay */}
<LoginRequiredOverlay

View File

@@ -13,7 +13,6 @@ type Page =
| 'strategy'
| 'strategy-market'
| 'data'
| 'debate'
| 'faq'
| 'login'
| 'register'
@@ -101,7 +100,6 @@ export default function HeaderBar({
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
{ page: 'debate', path: '/debate', label: t('debateNav', language), requiresAuth: true },
{ page: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
]
@@ -344,7 +342,6 @@ export default function HeaderBar({
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
{ page: 'debate', path: '/debate', label: t('debateNav', language), requiresAuth: true },
{ page: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
]

View File

@@ -15,7 +15,7 @@ const features = [
{
icon: Share2,
title: "PUNK SOCIAL",
description: "Follow, copy, and debate with AI traders. A social layer built for the post-human economy."
description: "Follow and copy AI traders. A social layer built for the post-human economy."
},
{
icon: Shield,

View File

@@ -35,7 +35,7 @@ export interface FAQCategory {
* 4. Trading - How trading works, common issues
* 5. Technical Issues - Troubleshooting common problems
* 6. Security - API keys, encryption, best practices
* 7. Features - Strategy Studio, Backtest, Debate Arena
* 7. Features - Strategy Studio, Backtest
* 8. Contributing - How to contribute to the project
*/
export const faqCategories: FAQCategory[] = [
@@ -299,11 +299,6 @@ export const faqCategories: FAQCategory[] = [
questionKey: 'faqBacktestLab',
answerKey: 'faqBacktestLabAnswer',
},
{
id: 'debate-arena',
questionKey: 'faqDebateArena',
answerKey: 'faqDebateArenaAnswer',
},
{
id: 'competition-mode',
questionKey: 'faqCompetitionMode',

View File

@@ -22,7 +22,6 @@ export const translations = {
configNav: 'Config',
dashboardNav: 'Dashboard',
strategyNav: 'Strategy',
debateNav: 'Arena',
faqNav: 'FAQ',
// Footer
@@ -839,7 +838,7 @@ export const translations = {
// ===== GETTING STARTED =====
faqWhatIsNOFX: 'What is NOFX?',
faqWhatIsNOFXAnswer:
'NOFX is an open-source AI-powered trading operating system for cryptocurrency and US stock markets. It uses large language models (LLMs) like DeepSeek, GPT, Claude, Gemini to analyze market data and make autonomous trading decisions. Key features include: multi-AI model support, multi-exchange trading, visual strategy builder, backtesting, and AI debate arena for consensus decisions.',
'NOFX is an open-source AI-powered trading operating system for cryptocurrency and US stock markets. It uses large language models (LLMs) like DeepSeek, GPT, Claude, Gemini to analyze market data and make autonomous trading decisions. Key features include: multi-AI model support, multi-exchange trading, visual strategy builder, and backtesting.',
faqHowDoesItWork: 'How does NOFX work?',
faqHowDoesItWorkAnswer:
@@ -1003,10 +1002,6 @@ export const translations = {
faqBacktestLabAnswer:
'Backtest Lab tests your strategy against historical data without risking real funds. Features: 1) Configure AI model, date range, initial balance; 2) Watch real-time progress with equity curve; 3) View metrics: Return %, Max Drawdown, Sharpe Ratio, Win Rate; 4) Analyze individual trades and AI reasoning. Essential for validating strategies before live trading.',
faqDebateArena: 'What is Debate Arena?',
faqDebateArenaAnswer:
'Debate Arena lets multiple AI models debate trading decisions before execution. Setup: 1) Choose 2-5 AI models; 2) Assign personalities (Bull, Bear, Analyst, Contrarian, Risk Manager); 3) Watch them debate in rounds; 4) Final decision based on consensus voting. Useful for high-conviction trades where you want multiple perspectives.',
faqCompetitionMode: 'What is Competition Mode?',
faqCompetitionModeAnswer:
'Competition page shows a real-time leaderboard of all your traders. Compare: ROI, P&L, Sharpe ratio, win rate, number of trades. Use this to A/B test different AI models, strategies, or configurations. Traders can be marked as "Show in Competition" to appear on the leaderboard.',
@@ -1030,7 +1025,7 @@ export const translations = {
faqCompareAIModels: 'How do I compare different AI models?',
faqCompareAIModelsAnswer:
'Create multiple traders with different AI models but same strategy/exchange. Run them simultaneously and compare on Competition page. Metrics to watch: ROI, win rate, Sharpe ratio, max drawdown. Alternatively, use Backtest Lab to test models against same historical data. The Debate Arena also shows how different models reason about the same situation.',
'Create multiple traders with different AI models but same strategy/exchange. Run them simultaneously and compare on Competition page. Metrics to watch: ROI, win rate, Sharpe ratio, max drawdown. Alternatively, use Backtest Lab to test models against same historical data.',
// ===== CONTRIBUTING =====
faqHowToContribute: 'How can I contribute to NOFX?',
@@ -1161,74 +1156,6 @@ export const translations = {
closedAt: 'Closed At',
},
// Debate Arena Page
debatePage: {
title: 'Market Debate Arena',
subtitle: 'Watch AI models debate market conditions and reach consensus',
newDebate: 'New Debate',
noDebates: 'No debates yet',
createFirst: 'Create your first debate to get started',
selectDebate: 'Select a debate to view details',
createDebate: 'Create Debate',
creating: 'Creating...',
debateName: 'Debate Name',
debateNamePlaceholder: 'e.g., BTC Bull or Bear?',
tradingPair: 'Trading Pair',
strategy: 'Strategy',
selectStrategy: 'Select a strategy',
maxRounds: 'Max Rounds',
autoExecute: 'Auto Execute',
autoExecuteHint: 'Automatically execute the consensus trade',
participants: 'Participants',
addParticipant: 'Add AI Participant',
noModels: 'No AI models available',
atLeast2: 'Add at least 2 participants',
personalities: {
bull: 'Aggressive Bull',
bear: 'Cautious Bear',
analyst: 'Data Analyst',
contrarian: 'Contrarian',
risk_manager: 'Risk Manager',
},
status: {
pending: 'Pending',
running: 'Running',
voting: 'Voting',
completed: 'Completed',
cancelled: 'Cancelled',
},
actions: {
start: 'Start Debate',
starting: 'Starting...',
cancel: 'Cancel',
delete: 'Delete',
execute: 'Execute Trade',
},
round: 'Round',
roundOf: 'Round {current} of {max}',
messages: 'Messages',
noMessages: 'No messages yet',
waitingStart: 'Waiting for debate to start...',
votes: 'Votes',
consensus: 'Consensus',
finalDecision: 'Final Decision',
confidence: 'Confidence',
votesCount: '{count} votes',
decision: {
open_long: 'Open Long',
open_short: 'Open Short',
close_long: 'Close Long',
close_short: 'Close Short',
hold: 'Hold',
wait: 'Wait',
},
messageTypes: {
analysis: 'Analysis',
rebuttal: 'Rebuttal',
vote: 'Vote',
summary: 'Summary',
},
},
},
zh: {
// Header
@@ -1251,7 +1178,6 @@ export const translations = {
configNav: '配置',
dashboardNav: '看板',
strategyNav: '策略',
debateNav: '竞技场',
faqNav: '常见问题',
// Footer
@@ -2017,7 +1943,7 @@ export const translations = {
// ===== 入门指南 =====
faqWhatIsNOFX: 'NOFX 是什么?',
faqWhatIsNOFXAnswer:
'NOFX 是一个开源的 AI 驱动交易操作系统支持加密货币和美股市场。它使用大语言模型LLM如 DeepSeek、GPT、Claude、Gemini 来分析市场数据,进行自主交易决策。核心功能包括:多 AI 模型支持、多交易所交易、可视化策略构建器、回测系统、以及用于共识决策的 AI 辩论竞技场。',
'NOFX 是一个开源的 AI 驱动交易操作系统支持加密货币和美股市场。它使用大语言模型LLM如 DeepSeek、GPT、Claude、Gemini 来分析市场数据,进行自主交易决策。核心功能包括:多 AI 模型支持、多交易所交易、可视化策略构建器、回测系统。',
faqHowDoesItWork: 'NOFX 是如何工作的?',
faqHowDoesItWorkAnswer:
@@ -2181,10 +2107,6 @@ export const translations = {
faqBacktestLabAnswer:
'回测实验室用历史数据测试您的策略无需冒真金风险。功能1配置 AI 模型、日期范围、初始余额2实时观看进度和权益曲线3查看指标收益率、最大回撤、夏普比率、胜率4分析单笔交易和 AI 推理。实盘交易前验证策略的必备工具。',
faqDebateArena: '什么是辩论竞技场?',
faqDebateArenaAnswer:
'辩论竞技场让多个 AI 模型在执行前辩论交易决策。设置1选择 2-5 个 AI 模型2分配角色多头、空头、分析师、逆向者、风险经理3观看他们多轮辩论4基于共识投票做最终决策。适用于需要多角度考虑的高确信度交易。',
faqCompetitionMode: '什么是竞赛模式?',
faqCompetitionModeAnswer:
'竞赛页面显示所有交易员的实时排行榜。比较ROI、盈亏、夏普比率、胜率、交易次数。用于 A/B 测试不同 AI 模型、策略或配置。交易员可标记为"在竞赛中显示"以出现在排行榜上。',
@@ -2208,7 +2130,7 @@ export const translations = {
faqCompareAIModels: '如何比较不同 AI 模型?',
faqCompareAIModelsAnswer:
'创建多个交易员,使用不同 AI 模型但相同策略/交易所。同时运行并在竞赛页面比较。关注指标ROI、胜率、夏普比率、最大回撤。或者使用回测实验室用相同历史数据测试模型。辩论竞技场也展示不同模型对同一情况的推理方式。',
'创建多个交易员,使用不同 AI 模型但相同策略/交易所。同时运行并在竞赛页面比较。关注指标ROI、胜率、夏普比率、最大回撤。或者使用回测实验室用相同历史数据测试模型。',
// ===== 参与贡献 =====
faqHowToContribute: '如何为 NOFX 做贡献?',
@@ -2332,74 +2254,6 @@ export const translations = {
closedAt: '平仓时间',
},
// Debate Arena Page
debatePage: {
title: '行情辩论大赛',
subtitle: '观看AI模型辩论市场行情并达成共识',
newDebate: '新建辩论',
noDebates: '暂无辩论',
createFirst: '创建您的第一场辩论开始',
selectDebate: '选择辩论查看详情',
createDebate: '创建辩论',
creating: '创建中...',
debateName: '辩论名称',
debateNamePlaceholder: '例如BTC是牛还是熊',
tradingPair: '交易对',
strategy: '策略',
selectStrategy: '选择策略',
maxRounds: '最大回合',
autoExecute: '自动执行',
autoExecuteHint: '自动执行共识交易',
participants: '参与者',
addParticipant: '添加AI参与者',
noModels: '暂无可用AI模型',
atLeast2: '至少添加2名参与者',
personalities: {
bull: '激进多头',
bear: '谨慎空头',
analyst: '数据分析师',
contrarian: '逆势者',
risk_manager: '风控经理',
},
status: {
pending: '待开始',
running: '进行中',
voting: '投票中',
completed: '已完成',
cancelled: '已取消',
},
actions: {
start: '开始辩论',
starting: '启动中...',
cancel: '取消',
delete: '删除',
execute: '执行交易',
},
round: '回合',
roundOf: '第 {current} / {max} 回合',
messages: '消息',
noMessages: '暂无消息',
waitingStart: '等待辩论开始...',
votes: '投票',
consensus: '共识',
finalDecision: '最终决定',
confidence: '信心度',
votesCount: '{count} 票',
decision: {
open_long: '开多',
open_short: '开空',
close_long: '平多',
close_short: '平空',
hold: '持有',
wait: '观望',
},
messageTypes: {
analysis: '分析',
rebuttal: '反驳',
vote: '投票',
summary: '总结',
},
},
},
id: {
// Header
@@ -2422,7 +2276,6 @@ export const translations = {
configNav: 'Konfigurasi',
dashboardNav: 'Dasbor',
strategyNav: 'Strategi',
debateNav: 'Arena',
faqNav: 'FAQ',
// Footer
@@ -3117,7 +2970,7 @@ export const translations = {
faqCategoryAIModels: 'Model AI',
faqCategoryContributing: 'Kontribusi',
faqWhatIsNOFX: 'Apa itu NOFX?',
faqWhatIsNOFXAnswer: 'NOFX adalah sistem operasi trading bertenaga AI open-source untuk pasar kripto dan saham AS. Ia menggunakan model bahasa besar (LLM) seperti DeepSeek, GPT, Claude, Gemini untuk menganalisis data pasar dan membuat keputusan trading secara otonom. Fitur utama: dukungan multi-model AI, trading multi-bursa, pembangun strategi visual, backtesting, dan arena debat AI untuk keputusan konsensus.',
faqWhatIsNOFXAnswer: 'NOFX adalah sistem operasi trading bertenaga AI open-source untuk pasar kripto dan saham AS. Ia menggunakan model bahasa besar (LLM) seperti DeepSeek, GPT, Claude, Gemini untuk menganalisis data pasar dan membuat keputusan trading secara otonom. Fitur utama: dukungan multi-model AI, trading multi-bursa, pembangun strategi visual, dan backtesting.',
faqHowDoesItWork: 'Bagaimana cara kerja NOFX?',
faqHowDoesItWorkAnswer: 'NOFX bekerja dalam 5 langkah: 1) Konfigurasi model AI dan kredensial API bursa; 2) Buat strategi trading (pemilihan koin, indikator, kontrol risiko); 3) Buat "Trader" menggabungkan Model AI + Bursa + Strategi; 4) Mulai trader - dia akan menganalisis data pasar secara berkala dan membuat keputusan beli/jual/tahan; 5) Pantau performa di dasbor.',
faqIsProfitable: 'Apakah NOFX menguntungkan?',
@@ -3196,8 +3049,6 @@ export const translations = {
faqStrategyStudioAnswer: 'Strategy Studio adalah pembangun strategi visual untuk konfigurasi: Sumber Koin, Indikator Teknikal, Kontrol Risiko, dan Prompt Kustom. Tanpa coding.',
faqBacktestLab: 'Apa itu Lab Backtest?',
faqBacktestLabAnswer: 'Lab Backtest menguji strategi Anda terhadap data historis tanpa risiko dana nyata.',
faqDebateArena: 'Apa itu Arena Debat?',
faqDebateArenaAnswer: 'Arena Debat membiarkan beberapa model AI berdebat tentang keputusan trading sebelum eksekusi. Berguna untuk trading dengan keyakinan tinggi.',
faqCompetitionMode: 'Apa itu Mode Kompetisi?',
faqCompetitionModeAnswer: 'Halaman kompetisi menampilkan papan peringkat realtime semua trader Anda. Bandingkan ROI, L/R, rasio Sharpe, win rate.',
faqChainOfThought: 'Apa itu Chain of Thought (CoT)?',
@@ -3306,73 +3157,6 @@ export const translations = {
duration: 'Durasi',
closedAt: 'Ditutup Pada',
},
debatePage: {
title: 'Arena Debat Pasar',
subtitle: 'Saksikan model AI berdebat tentang kondisi pasar dan mencapai konsensus',
newDebate: 'Debat Baru',
noDebates: 'Belum ada debat',
createFirst: 'Buat debat pertama Anda untuk memulai',
selectDebate: 'Pilih debat untuk melihat detail',
createDebate: 'Buat Debat',
creating: 'Membuat...',
debateName: 'Nama Debat',
debateNamePlaceholder: 'misal, BTC Bull atau Bear?',
tradingPair: 'Pasangan Trading',
strategy: 'Strategi',
selectStrategy: 'Pilih strategi',
maxRounds: 'Ronde Maksimum',
autoExecute: 'Eksekusi Otomatis',
autoExecuteHint: 'Otomatis mengeksekusi trading konsensus',
participants: 'Peserta',
addParticipant: 'Tambah Peserta AI',
noModels: 'Tidak ada model AI tersedia',
atLeast2: 'Tambahkan minimal 2 peserta',
personalities: {
bull: 'Bull Agresif',
bear: 'Bear Hati-hati',
analyst: 'Analis Data',
contrarian: 'Kontrarian',
risk_manager: 'Manajer Risiko',
},
status: {
pending: 'Menunggu',
running: 'Berjalan',
voting: 'Pemungutan Suara',
completed: 'Selesai',
cancelled: 'Dibatalkan',
},
actions: {
start: 'Mulai Debat',
starting: 'Memulai...',
cancel: 'Batal',
delete: 'Hapus',
execute: 'Eksekusi Trading',
},
round: 'Ronde',
roundOf: 'Ronde {current} dari {max}',
messages: 'Pesan',
noMessages: 'Belum ada pesan',
waitingStart: 'Menunggu debat dimulai...',
votes: 'Suara',
consensus: 'Konsensus',
finalDecision: 'Keputusan Akhir',
confidence: 'Keyakinan',
votesCount: '{count} suara',
decision: {
open_long: 'Buka Long',
open_short: 'Buka Short',
close_long: 'Tutup Long',
close_short: 'Tutup Short',
hold: 'Tahan',
wait: 'Tunggu',
},
messageTypes: {
analysis: 'Analisis',
rebuttal: 'Bantahan',
vote: 'Suara',
summary: 'Ringkasan',
},
},
},
}

View File

@@ -24,12 +24,6 @@ import type {
BacktestKlinesResponse,
Strategy,
StrategyConfig,
DebateSession,
DebateSessionWithDetails,
CreateDebateRequest,
DebateMessage,
DebateVote,
DebatePersonalityInfo,
PositionHistoryResponse,
} from '../types'
import { CryptoService } from './crypto'
@@ -711,73 +705,6 @@ export const api = {
return result.data!
},
// Debate Arena APIs
async getDebates(): Promise<DebateSession[]> {
const result = await httpClient.get<DebateSession[]>(`${API_BASE}/debates`)
if (!result.success) throw new Error('获取辩论列表失败')
return Array.isArray(result.data) ? result.data : []
},
async getDebate(debateId: string): Promise<DebateSessionWithDetails> {
const result = await httpClient.get<DebateSessionWithDetails>(`${API_BASE}/debates/${debateId}`)
if (!result.success) throw new Error('获取辩论详情失败')
return result.data!
},
async createDebate(request: CreateDebateRequest): Promise<DebateSessionWithDetails> {
const result = await httpClient.post<DebateSessionWithDetails>(`${API_BASE}/debates`, request)
if (!result.success) throw new Error('创建辩论失败')
return result.data!
},
async startDebate(debateId: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/debates/${debateId}/start`)
if (!result.success) throw new Error('启动辩论失败')
},
async cancelDebate(debateId: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/debates/${debateId}/cancel`)
if (!result.success) throw new Error('取消辩论失败')
},
async executeDebate(debateId: string, traderId: string): Promise<DebateSessionWithDetails> {
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<void> {
const result = await httpClient.delete(`${API_BASE}/debates/${debateId}`)
if (!result.success) throw new Error('删除辩论失败')
},
async getDebateMessages(debateId: string): Promise<DebateMessage[]> {
const result = await httpClient.get<DebateMessage[]>(`${API_BASE}/debates/${debateId}/messages`)
if (!result.success) throw new Error('获取辩论消息失败')
return result.data!
},
async getDebateVotes(debateId: string): Promise<DebateVote[]> {
const result = await httpClient.get<DebateVote[]>(`${API_BASE}/debates/${debateId}/votes`)
if (!result.success) throw new Error('获取辩论投票失败')
return result.data!
},
async getDebatePersonalities(): Promise<DebatePersonalityInfo[]> {
const result = await httpClient.get<DebatePersonalityInfo[]>(`${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<PositionHistoryResponse> {
const result = await httpClient.get<PositionHistoryResponse>(

View File

@@ -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<string, Record<string, string>> = {
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<DebatePersonality, { emoji: string; color: string; name: string; nameEn: string }> = {
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<string, { color: string; bg: string; icon: JSX.Element; label: string }> = {
open_long: { color: 'text-green-400', bg: 'bg-green-500/20', icon: <TrendingUp size={14} />, label: 'LONG' },
open_short: { color: 'text-red-400', bg: 'bg-red-500/20', icon: <TrendingDown size={14} />, label: 'SHORT' },
hold: { color: 'text-blue-400', bg: 'bg-blue-500/20', icon: <Minus size={14} />, label: 'HOLD' },
wait: { color: 'text-gray-400', bg: 'bg-gray-500/20', icon: <Clock size={14} />, label: 'WAIT' },
close_long: { color: 'text-yellow-400', bg: 'bg-yellow-500/20', icon: <X size={14} />, label: 'CLOSE' },
close_short: { color: 'text-yellow-400', bg: 'bg-yellow-500/20', icon: <X size={14} />, label: 'CLOSE' },
}
// Status colors
const STATUS_COLOR: Record<string, string> = {
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<string, { bg: string; text: string; letter: string }> = {
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 (
<div className={`${p.bg} ${p.text} rounded-md flex items-center justify-center font-bold`}
style={{ width: size, height: size, fontSize: size * 0.5 }}>
{p.letter}
</div>
)
}
// 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(/<reasoning>([\s\S]*?)<\/reasoning>/i)?.[1]?.trim()
const analysis = c.match(/<analysis>([\s\S]*?)<\/analysis>/i)?.[1]?.trim()
const argument = c.match(/<argument>([\s\S]*?)<\/argument>/i)?.[1]?.trim()
const decision = c.match(/<decision>([\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 (
<div
className="p-3 rounded-lg hover:bg-nofx-bg-lighter/60 transition-all border border-nofx-gold/20 backdrop-blur-sm bg-nofx-bg-lighter/20"
style={{ borderLeft: `3px solid ${p.color}` }}
>
{/* Header - Always visible */}
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setOpen(!open)}
>
<AIAvatar name={msg.ai_model_name} size={24} />
<span className="text-sm text-nofx-text font-medium">{msg.ai_model_name}</span>
<span className="text-xs text-nofx-text-muted">{p.nameEn}</span>
<div className="flex-1" />
{msg.decision && (
<span className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded ${a.bg} ${a.color}`}>
{a.icon} {msg.decision.symbol || ''} {a.label}
</span>
)}
<span className="text-xs text-nofx-gold font-medium">{msg.decision?.confidence || msg.confidence}%</span>
{open ? <ChevronUp size={14} className="text-nofx-text-muted" /> : <ChevronDown size={14} className="text-nofx-text-muted" />}
</div>
{/* Preview when collapsed */}
{!open && (
<div className="mt-2 text-xs text-gray-400 line-clamp-2">
{previewText}...
</div>
)}
{/* Expanded Content - Full display */}
{open && (
<div className="mt-3 space-y-3">
{/* Reasoning/Analysis Section */}
{parsed.reasoning && (
<div className="bg-black/20 rounded-lg p-3">
<div className="text-xs text-blue-400 font-medium mb-2">💭 / Reasoning</div>
<div className="text-xs text-gray-300 leading-relaxed whitespace-pre-wrap max-h-64 overflow-y-auto select-text">
{parsed.reasoning}
</div>
</div>
)}
{/* Decision Section */}
{msg.decision && (
<div className="bg-black/20 rounded-lg p-3">
<div className="text-xs text-green-400 font-medium mb-2">📊 / Decision</div>
<div className="grid grid-cols-2 gap-2 text-xs">
{msg.decision.symbol && (
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-white font-medium">{msg.decision.symbol}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className={a.color}>{a.label}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-yellow-400">{msg.decision.confidence}%</span>
</div>
{(msg.decision.leverage ?? 0) > 0 && (
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-white">{msg.decision.leverage}x</span>
</div>
)}
{(msg.decision.position_pct ?? 0) > 0 && (
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-white">{((msg.decision.position_pct ?? 0) * 100).toFixed(0)}%</span>
</div>
)}
{(msg.decision.stop_loss ?? 0) > 0 && (
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-red-400">{((msg.decision.stop_loss ?? 0) * 100).toFixed(1)}%</span>
</div>
)}
{(msg.decision.take_profit ?? 0) > 0 && (
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="text-green-400">{((msg.decision.take_profit ?? 0) * 100).toFixed(1)}%</span>
</div>
)}
</div>
{msg.decision.reasoning && (
<div className="mt-2 pt-2 border-t border-white/10 text-xs text-gray-400">
{msg.decision.reasoning}
</div>
)}
</div>
)}
{/* Full Raw Content (collapsible) */}
{!parsed.reasoning && (
<div className="bg-black/20 rounded-lg p-3">
<div className="text-xs text-gray-400 font-medium mb-2">📝 / Full Output</div>
<div className="text-xs text-gray-300 leading-relaxed whitespace-pre-wrap max-h-96 overflow-y-auto select-text">
{parsed.fullContent}
</div>
</div>
)}
{/* Multi-coin decisions if available */}
{msg.decisions && msg.decisions.length > 1 && (
<div className="bg-black/20 rounded-lg p-3">
<div className="text-xs text-purple-400 font-medium mb-2">🎯 ({msg.decisions.length})</div>
<div className="space-y-2">
{msg.decisions.map((d, i) => {
const da = ACT[d.action] || ACT.wait
return (
<div key={i} className="flex items-center justify-between text-xs p-2 bg-white/5 rounded">
<span className="text-white font-medium">{d.symbol}</span>
<span className={da.color}>{da.icon} {da.label}</span>
<span className="text-yellow-400">{d.confidence}%</span>
<span className="text-gray-400">{d.leverage || 0}x / {((d.position_pct || 0) * 100).toFixed(0)}%</span>
</div>
)
})}
</div>
</div>
)}
</div>
)}
</div>
)
}
// 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 (
<div className="bg-nofx-bg-lighter/40 backdrop-blur-md rounded-xl p-4 border border-nofx-gold/20 hover:border-nofx-gold/50 transition-all shadow-lg hover:shadow-[0_0_20px_rgba(240,185,11,0.1)]">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<AIAvatar name={vote.ai_model_name} size={28} />
<div>
<span className="text-nofx-text font-semibold block">{vote.ai_model_name}</span>
{vote.symbol && <span className="text-xs text-nofx-text-muted">{vote.symbol}</span>}
</div>
</div>
<span className={`flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-bold ${a.bg} ${a.color}`}>
{a.icon} {vote.action.replace('_', ' ').toUpperCase()}
</span>
</div>
<div className="mb-3">
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-400">Confidence</span>
<span className="text-white font-bold">{vote.confidence}%</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<div className={`h-full ${confColor} rounded-full transition-all`} style={{ width: `${vote.confidence}%` }} />
</div>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
<div className="flex justify-between"><span className="text-nofx-text-muted">Leverage</span><span className="text-nofx-text font-semibold">{vote.leverage || '-'}x</span></div>
<div className="flex justify-between"><span className="text-nofx-text-muted">Position</span><span className="text-nofx-text font-semibold">{vote.position_pct ? `${(vote.position_pct * 100).toFixed(0)}%` : '-'}</span></div>
<div className="flex justify-between"><span className="text-nofx-text-muted">SL</span><span className="text-red-400 font-semibold">{vote.stop_loss_pct ? `${(vote.stop_loss_pct * 100).toFixed(1)}%` : '-'}</span></div>
<div className="flex justify-between"><span className="text-nofx-text-muted">TP</span><span className="text-green-400 font-semibold">{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}</span></div>
</div>
{vote.reasoning && (
<p className="mt-3 text-xs text-nofx-text-muted leading-relaxed line-clamp-2 border-t border-nofx-gold/10 pt-2">{vote.reasoning}</p>
)}
</div>
)
}
// Create Modal (simplified)
function CreateModal({
isOpen, onClose, onCreate, aiModels, strategies, language
}: {
isOpen: boolean; onClose: () => void; onCreate: (r: CreateDebateRequest) => Promise<void>
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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="bg-nofx-bg-lighter/90 backdrop-blur-xl rounded-xl w-full max-w-md p-6 border border-nofx-gold/30 shadow-2xl shadow-nofx-gold/10">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-nofx-text">{t('createDebate', language)}</h3>
<button onClick={onClose}><X size={20} className="text-nofx-text-muted" /></button>
</div>
<div className="space-y-3">
<input
value={name} onChange={e => 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 */}
<select value={strategyId} onChange={e => setStrategyId(e.target.value)}
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">
{strategies.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
<div className="flex gap-2">
{/* Show dropdown only for static type with coins defined */}
{isStaticWithCoins ? (
<select value={symbol} onChange={e => setSymbol(e.target.value)}
className="flex-1 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">
{staticCoins.map(coin => <option key={coin} value={coin}>{coin}</option>)}
</select>
) : (
<div className="flex-1 px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text-muted text-sm">
{language === 'zh' ? '根据策略规则自动选择' : 'Auto-selected by strategy'}
</div>
)}
<select value={maxRounds} onChange={e => setMaxRounds(+e.target.value)}
className="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">
{[2, 3, 4, 5].map(n => <option key={n} value={n}>{n} {language === 'zh' ? '轮' : 'rounds'}</option>)}
</select>
</div>
{/* Participants */}
<div className="flex items-center gap-2 flex-wrap">
{participants.map((p, i) => (
<div key={i} className="flex items-center gap-1 px-2 py-1 rounded-lg text-xs"
style={{ backgroundColor: `${PERS[p.personality].color}20`, border: `1px solid ${PERS[p.personality].color}40` }}>
{/* Personality selector */}
<select value={p.personality} onChange={e => {
const up = [...participants]; up[i].personality = e.target.value as DebatePersonality; setParticipants(up)
}} className="bg-transparent text-nofx-text text-xs border-0 outline-none cursor-pointer">
{Object.entries(PERS).map(([k, v]) => (
<option key={k} value={k}>{v.emoji} {language === 'zh' ? v.name : v.nameEn}</option>
))}
</select>
{/* AI model selector */}
<select value={p.ai_model_id} onChange={e => {
const up = [...participants]; up[i].ai_model_id = e.target.value; setParticipants(up)
}} className="bg-transparent text-nofx-text text-xs border-0 outline-none">
{aiModels.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
</select>
<button onClick={() => setParticipants(participants.filter((_, j) => j !== i))}
className="text-nofx-danger hover:text-red-300"><X size={12} /></button>
</div>
))}
<button onClick={addP} className="px-2 py-1 text-xs text-nofx-gold hover:bg-nofx-gold/10 rounded">
+ {t('addAI', language)}
</button>
</div>
</div>
<div className="flex gap-2 mt-4">
<button onClick={onClose} className="flex-1 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm hover:bg-nofx-bg-lighter transition-colors">{t('cancel', language)}</button>
<button onClick={submit} disabled={creating}
className="flex-1 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm disabled:opacity-50 hover:bg-yellow-500 transition-colors">
{creating ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('create', language)}
</button>
</div>
</div>
</div>
)
}
// Main Page
export function DebateArenaPage() {
const { language } = useLanguage()
const [selectedId, setSelectedId] = useState<string | null>(null)
const [showCreate, setShowCreate] = useState(false)
const [execId, setExecId] = useState<string | null>(null)
const [traderId, setTraderId] = useState('')
const [executing, setExecuting] = useState(false)
const { data: debates, mutate: mutateList } = useSWR<DebateSession[]>('debates', api.getDebates, { refreshInterval: 5000 })
const { data: aiModels } = useSWR<AIModel[]>('ai-models', api.getModelConfigs)
const { data: strategies } = useSWR<Strategy[]>('strategies', api.getStrategies)
const { data: traders } = useSWR<TraderInfo[]>('traders', api.getTraders)
const { data: detail, mutate: mutateDetail } = useSWR<DebateSessionWithDetails>(
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<number, DebateMessage[]> = {}
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<string, number>)
return (
<DeepVoidBackground className="h-full flex overflow-hidden relative" disableAnimation>
{/* Left - Debate List + Online Traders */}
<div className="w-56 flex-shrink-0 bg-nofx-bg/80 backdrop-blur-md border-r border-nofx-gold/20 flex flex-col z-10">
{/* New Debate Button */}
<button onClick={() => setShowCreate(true)}
className="m-2 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm flex items-center justify-center gap-1 hover:bg-yellow-500 transition-colors">
<Plus size={16} /> {t('newDebate', language)}
</button>
{/* Debate List */}
<div className="px-2 py-1 text-xs text-nofx-text-muted font-semibold">{t('debateSessions', language)}</div>
<div className="overflow-y-auto" style={{ maxHeight: '30%' }}>
{debates?.map(d => (
<div key={d.id} onClick={() => 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'}`}>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${STATUS_COLOR[d.status]}`} />
<span className="text-sm text-nofx-text truncate flex-1">{d.name}</span>
</div>
<div className="text-xs text-nofx-text-muted mt-1">{d.symbol} · R{d.current_round}/{d.max_rounds}</div>
{d.status === 'pending' && selectedId === d.id && (
<div className="flex gap-1 mt-1">
<button onClick={e => { e.stopPropagation(); onStart(d.id) }}
className="text-xs px-2 py-0.5 bg-green-500/20 text-green-400 rounded">{t('start', language)}</button>
<button onClick={e => { e.stopPropagation(); onDelete(d.id) }}
className="text-xs px-2 py-0.5 bg-red-500/20 text-red-400 rounded">{t('delete', language)}</button>
</div>
)}
</div>
))}
</div>
{/* Online Traders Section */}
<div className="flex-1 border-t border-nofx-gold/20 mt-2 overflow-hidden flex flex-col">
<div className="px-2 py-2 text-xs text-nofx-text-muted font-semibold flex items-center gap-1">
<Zap size={12} className="text-nofx-success" />
{t('onlineTraders', language)}
</div>
<div className="flex-1 overflow-y-auto px-2 space-y-2">
{traders?.filter(tr => tr.is_running).map(tr => (
<div key={tr.trader_id}
onClick={() => { 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'}`}>
<div className="flex items-center gap-2">
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
<div className="flex-1 min-w-0">
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
<div className="text-xs text-nofx-text-muted truncate">{tr.ai_model}</div>
</div>
<span className="w-2 h-2 rounded-full bg-nofx-success animate-pulse" />
</div>
</div>
))}
{traders?.filter(tr => !tr.is_running).slice(0, 3).map(tr => (
<div key={tr.trader_id} className="p-2 rounded-lg bg-nofx-bg-lighter opacity-50">
<div className="flex items-center gap-2">
<div className="grayscale">
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
<div className="text-xs text-nofx-text-muted">{t('offline', language)}</div>
</div>
</div>
</div>
))}
{(!traders || traders.length === 0) && (
<div className="text-xs text-nofx-text-muted text-center py-4">{t('noTraders', language)}</div>
)}
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
{detail ? (
<>
{/* Header Bar - Compact */}
<div className="px-3 py-2 border-b border-nofx-gold/20 bg-nofx-bg/60 backdrop-blur-md flex items-center gap-3 flex-shrink-0 shadow-sm">
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${STATUS_COLOR[detail.status]}`} />
<span className="font-bold text-nofx-text truncate">{detail.name}</span>
<span className="text-nofx-gold font-semibold">{detail.symbol}</span>
{strategyName && <span className="text-xs px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded">{strategyName}</span>}
<span className="text-xs text-nofx-text-muted">R{detail.current_round}/{detail.max_rounds}</span>
{/* Participants */}
<div className="flex gap-1 ml-2">
{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 (
<div key={p.id} className="flex items-center gap-1 px-1 py-0.5 rounded bg-nofx-bg-lighter text-xs">
<AIAvatar name={p.ai_model_name} size={14} />
{act && <span className={`${act.color}`}>{act.icon}</span>}
</div>
)
})}
</div>
<div className="flex-1" />
{/* Vote Summary */}
{votes.length > 0 && (
<div className="flex gap-1">
{Object.entries(voteSum).map(([action, count]) => {
const cfg = ACT[action] || ACT.wait
return (
<div key={action} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${cfg.bg} ${cfg.color} text-xs font-semibold`}>
{cfg.icon} {cfg.label}×{count}
</div>
)
})}
</div>
)}
</div>
{/* Main Content Area - Two Column Layout */}
<div className="flex-1 flex overflow-hidden">
{Object.keys(rounds).length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-gray-500">
<div className="text-6xl mb-4">{detail.status === 'pending' ? '🎯' : '⏳'}</div>
<div className="text-lg">{detail.status === 'pending' ? t('clickToStart', language) : t('waitingAI', language)}</div>
</div>
) : (
<>
{/* Left - Rounds */}
<div className="flex-1 overflow-y-auto p-4 border-r border-nofx-gold/20">
<div className="text-sm text-nofx-text-muted font-semibold mb-3 flex items-center gap-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
{t('discussionRecords', language)}
</div>
<div className="space-y-3">
{Object.entries(rounds).map(([round, msgs]) => (
<div key={round} className="bg-white/5 rounded-xl p-3">
<div className="text-xs text-blue-400 font-bold mb-2">Round {round}</div>
<div className="space-y-2">
{msgs.map(m => <MessageCard key={m.id} msg={m} />)}
</div>
</div>
))}
</div>
</div>
{/* Right - Votes */}
{votes.length > 0 && (
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 bg-nofx-bg/30 backdrop-blur-sm">
<div className="text-sm text-nofx-text-muted font-semibold mb-3 flex items-center gap-2">
<Trophy size={16} className="text-nofx-gold" />
{t('finalVotes', language)}
</div>
<div className="space-y-3">
{votes.map(v => (
<VoteCard key={v.id} vote={{
ai_model_name: v.ai_model_name,
action: v.action,
symbol: v.symbol,
confidence: v.confidence,
leverage: v.leverage,
position_pct: v.position_pct,
stop_loss_pct: v.stop_loss_pct,
take_profit_pct: v.take_profit_pct,
reasoning: v.reasoning
}} />
))}
</div>
</div>
)}
</>
)}
</div>
{/* Consensus Bar - Show when votes exist */}
{(decision || votes.length > 0) && (
<div className="p-3 border-t border-nofx-gold/20 bg-gradient-to-r from-nofx-gold/10 via-nofx-bg-lighter/50 to-orange-500/10 backdrop-blur-md flex items-center gap-4 flex-shrink-0">
<div className="flex items-center gap-2">
<Trophy size={20} className="text-nofx-gold" />
<span className="text-sm text-nofx-text-muted">{t('consensus', language)}:</span>
{decision ? (
<>
{decision.symbol && <span className="text-nofx-gold font-bold mr-1">{decision.symbol}</span>}
<span className={`flex items-center gap-1 px-2 py-1 rounded font-bold ${(ACT[decision.action] || ACT.wait).bg} ${(ACT[decision.action] || ACT.wait).color}`}>
{(ACT[decision.action] || ACT.wait).icon}
{decision.action.replace('_', ' ').toUpperCase()}
</span>
</>
) : (
<span className="flex items-center gap-1 px-2 py-1 rounded font-bold bg-nofx-text-muted/20 text-nofx-text-muted">
<Clock size={14} /> VOTING...
</span>
)}
</div>
{decision && (
<div className="flex items-center gap-4 text-sm">
<span><span className="text-nofx-text-muted">{t('confidence', language)}</span> <span className="text-nofx-gold font-bold">{decision.confidence || 0}%</span></span>
{(decision.leverage ?? 0) > 0 && <span><span className="text-nofx-text-muted">{t('leverage', language)}</span> <span className="text-nofx-text font-bold">{decision.leverage}x</span></span>}
{(decision.position_pct ?? 0) > 0 && <span><span className="text-nofx-text-muted">{t('position', language)}</span> <span className="text-nofx-text font-bold">{((decision.position_pct ?? 0) * 100).toFixed(0)}%</span></span>}
{(decision.stop_loss ?? 0) > 0 && <span><span className="text-nofx-text-muted">SL</span> <span className="text-red-400 font-bold">{((decision.stop_loss ?? 0) * 100).toFixed(1)}%</span></span>}
{(decision.take_profit ?? 0) > 0 && <span><span className="text-nofx-text-muted">TP</span> <span className="text-green-400 font-bold">{((decision.take_profit ?? 0) * 100).toFixed(1)}%</span></span>}
</div>
)}
<div className="flex-1" />
{decision && !decision.executed && (decision.action === 'open_long' || decision.action === 'open_short') && (
<button onClick={() => setExecId(detail.id)}
className="px-4 py-1.5 rounded-lg bg-nofx-gold text-black font-semibold text-sm flex items-center gap-1 hover:bg-yellow-500 transition-colors">
<Zap size={14} /> {t('execute', language)}
</button>
)}
{decision?.executed && <span className="text-green-400 text-sm font-semibold"> {t('executed', language)}</span>}
</div>
)}
</>
) : (
<div className="flex-1 flex items-center justify-center text-nofx-text-muted">
<div className="text-center">
<div className="text-4xl mb-2">🗳</div>
<div>{t('selectOrCreate', language)}</div>
</div>
</div>
)}
</div>
{/* Create Modal */}
<CreateModal isOpen={showCreate} onClose={() => setShowCreate(false)} onCreate={onCreate}
aiModels={aiModels || []} strategies={strategies || []} language={language} />
{/* Execute Modal */}
{execId && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="bg-nofx-bg-lighter/90 backdrop-blur-xl rounded-xl w-full max-w-sm p-6 border border-nofx-gold/30 shadow-2xl shadow-nofx-gold/10">
<h3 className="text-lg font-bold text-nofx-text mb-4 flex items-center gap-2">
<Zap className="text-nofx-gold" /> {t('executeTitle', language)}
</h3>
<select value={traderId} onChange={e => setTraderId(e.target.value)}
className="w-full px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm mb-3">
<option value="">{t('selectTrader', language)}...</option>
{traders?.filter(tr => tr.is_running).map(tr => (
<option key={tr.trader_id} value={tr.trader_id}> {tr.trader_name}</option>
))}
{traders?.filter(tr => !tr.is_running).map(tr => (
<option key={tr.trader_id} value={tr.trader_id} disabled> {tr.trader_name} ({t('offline', language)})</option>
))}
</select>
<div className="text-xs text-yellow-300 bg-nofx-gold/10 p-2 rounded mb-3">
{language === 'zh' ? '将使用账户余额执行真实交易' : 'Will execute real trade with account balance'}
</div>
<div className="flex gap-2">
<button onClick={() => { setExecId(null); setTraderId('') }}
className="flex-1 py-2 rounded-lg bg-nofx-bg text-nofx-text text-sm hover:bg-nofx-bg-light transition-colors">{t('cancel', language)}</button>
<button onClick={onExecute} disabled={!traderId || executing || !traders?.find(tr => tr.trader_id === traderId)?.is_running}
className="flex-1 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm disabled:opacity-50 hover:bg-yellow-500 transition-colors">
{executing ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('execute', language)}
</button>
</div>
</div>
</div>
)}
</DeepVoidBackground>
)
}

View File

@@ -43,7 +43,6 @@ export function LandingPage() {
'trader': '/dashboard',
'backtest': '/backtest',
'strategy': '/strategy',
'debate': '/debate',
'faq': '/faq',
}
const path = pathMap[page]

View File

@@ -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;