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

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