/**
* QuantAlpha Pro v2.1 - Luna Sentiment Trading Dashboard
* React 18 + Tailwind + Chart.js
*/
const { useState, useEffect, useCallback } = React;
// ============================================
// API Client with JWT + HMAC Authentication
// ============================================
const API_BASE = window.location.origin;
const api = {
token: localStorage.getItem('qa_token') || '',
refreshToken: localStorage.getItem('qa_refresh') || '',
async request(method, path, body = null, useSignature = false) {
const headers = {
'Content-Type': 'application/json',
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
const options = { method, headers };
if (body) {
options.body = JSON.stringify(body);
}
const res = await fetch(`${API_BASE}${path}`, options);
if (res.status === 401) {
// Try refresh
const refreshed = await this.refresh();
if (refreshed) {
return this.request(method, path, body, useSignature);
}
throw new Error('Session expired');
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Request failed' }));
throw new Error(err.error || err.detail || 'Request failed');
}
return res.json();
},
async refresh() {
if (!this.refreshToken) return false;
try {
const res = await fetch(`${API_BASE}/api/v1/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: this.refreshToken }),
});
if (res.ok) {
const data = await res.json();
this.token = data.access_token;
localStorage.setItem('qa_token', this.token);
return true;
}
} catch {}
return false;
},
login(username, password) {
return this.request('POST', '/api/v1/auth/login', { username, password });
},
getStrategies() {
return this.request('GET', '/api/v1/strategies');
},
getStrategy(id) {
return this.request('GET', `/api/v1/strategies/${id}`);
},
toggleStrategy(id) {
return this.request('POST', `/api/v1/strategies/${id}/toggle`);
},
updateStrategyConfig(id, config) {
return this.request('PUT', `/api/v1/strategies/${id}/config`, { config });
},
getRiskMetrics() {
return this.request('GET', '/api/v1/risk/metrics');
},
getKellyParams() {
return this.request('GET', '/api/v1/risk/kelly');
},
activateKillSwitch(level, confirmCode) {
return this.request('POST', '/api/v1/risk/kill-switch', {
level,
confirm_code: confirmCode,
});
},
deactivateKillSwitch() {
return this.request('DELETE', '/api/v1/risk/kill-switch');
},
getBinanceSpot() {
return this.request('GET', '/api/v1/binance/spot');
},
getMarketOverview() {
return this.request('GET', '/api/v1/binance/market-overview');
},
getEquityHistory() {
return this.request('GET', '/api/v1/equity/history?days=90');
},
getSystemStatus() {
return this.request('GET', '/api/v1/system/status');
},
getAuditLogs() {
return this.request('GET', '/api/v1/audit-logs');
},
// Luna Sentiment API Methods
getLunaSentiment(symbol) { return this.request('GET', `/api/v1/luna/sentiment/${symbol}`); },
getLunaAllSentiments() { return this.request('GET', '/api/v1/luna/sentiment'); },
getLunaHistory(symbol, hours = 24) { return this.request('GET', `/api/v1/luna/history/${symbol}?hours=${hours}`); },
getLunaSignals() { return this.request('GET', '/api/v1/luna/signals'); },
getLunaProfitability() { return this.request('GET', '/api/v1/luna/profitability'); },
triggerLunaCollect() { return this.request('POST', '/api/v1/luna/collect'); },
triggerLunaBacktest() { return this.request('POST', '/api/v1/luna/backtest'); },
};
// ============================================
// Login Component
// ============================================
function LoginScreen({ onLogin }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const data = await api.login(username, password);
api.token = data.access_token;
api.refreshToken = data.refresh_token;
localStorage.setItem('qa_token', data.access_token);
localStorage.setItem('qa_refresh', data.refresh_token);
localStorage.setItem('qa_user', JSON.stringify(data.user));
onLogin(data.user);
} catch (err) {
setError(err.message || 'Login failed');
} finally {
setLoading(false);
}
};
return (
QuantAlpha Pro
v2.1 — Luna Sentiment Trading
{error && (
{error}
)}
Default: username: admin / password: admin_qa2024!
);
}
// ============================================
// Sidebar Component
// ============================================
function Sidebar({ activeTab, setActiveTab, user, onLogout }) {
const menuItems = [
{ id: 'dashboard', icon: 'fa-chart-line', label: '总览仪表盘', perm: 'read:dashboard' },
{ id: 'strategy', icon: 'fa-robot', label: 'MA交叉策略', perm: 'read:strategies' },
{ id: 'luna', icon: 'fa-moon', label: 'Luna舆论策略', perm: 'read:strategies' },
{ id: 'positions', icon: 'fa-wallet', label: '持仓管理', perm: 'read:dashboard' },
{ id: 'risk', icon: 'fa-shield-halved', label: '风险管理', perm: 'read:metrics' },
{ id: 'audit', icon: 'fa-list-check', label: '审计日志', perm: 'read:audit_logs' },
];
const hasPerm = (perm) => {
if (!user || !user.permissions) return false;
return user.permissions.includes(perm) || user.role === 'admin';
};
return (
Main Menu
{menuItems.map(item => (
hasPerm(item.perm) && (
setActiveTab(item.id)}
>
{item.label}
)
))}
Logged in as
{user?.username}
{user?.role}
Logout
);
}
// ============================================
// Dashboard Component
// ============================================
function Dashboard({ user }) {
const [marketData, setMarketData] = useState({});
const [spotData, setSpotData] = useState({ balances: [] });
const [equityData, setEquityData] = useState({});
const [systemStatus, setSystemStatus] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const [market, spot, equity, status] = await Promise.all([
api.getMarketOverview(),
api.getBinanceSpot(),
api.getEquityHistory(),
api.getSystemStatus(),
]);
setMarketData(market);
setSpotData(spot);
setEquityData(equity);
setSystemStatus(status);
} catch (err) {
console.error('Dashboard data error:', err);
} finally {
setLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
if (loading) {
return (
);
}
const prices = marketData.spot_prices || {};
const balances = spotData.balances || [];
const totalValue = balances.reduce((sum, b) => sum + (b.value_usdt || 0), 0);
return (
总览仪表盘
实时监控 · 最后更新: {new Date().toLocaleTimeString()}
System Online
{systemStatus.kill_switch_active && (
KILL SWITCH ACTIVE
)}
{/* Key Metrics */}
总资产估值
${totalValue.toLocaleString('en-US', { minimumFractionDigits: 2 })}
组合总市值
策略盈亏
= 0 ? 'trend-up' : 'trend-down'}`}>
{(equityData.total_pnl || 0) >= 0 ? '+' : ''}${(equityData.total_pnl || 0).toFixed(2)}
累计实现盈亏
活跃策略
{Object.keys(equityData.strategy_breakdown || {}).length}
运行中
Kill Switch
{systemStatus.kill_switch_active ? 'ACTIVE' : 'Ready'}
{systemStatus.kill_switch_level || 'Standby'}
{/* Market Prices */}
实时行情
Binance Live
| Symbol |
Price |
{Object.entries(prices).map(([symbol, price]) => (
| {symbol.replace('USDT', '')} |
${price.toLocaleString('en-US', { minimumFractionDigits: 2 })}
|
))}
{/* Holdings */}
当前持仓
Spot
| Asset |
Amount |
Value (USDT) |
{balances.filter(b => b.value_usdt > 1).map(b => (
| {b.asset} |
{b.total.toFixed(4)} |
${b.value_usdt.toFixed(2)}
|
))}
{balances.filter(b => b.value_usdt > 1).length === 0 && (
|
暂无持仓
|
)}
);
}
// ============================================
// MA Strategy Component
// ============================================
function MAStrategyPanel({ user }) {
const [strategy, setStrategy] = useState(null);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
const [configEdit, setConfigEdit] = useState({});
const fetchStrategy = async () => {
try {
const data = await api.getStrategy('ma-cross-spot');
setStrategy(data);
setConfigEdit(data.config || {});
} catch (err) {
console.error('Strategy fetch error:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStrategy();
const interval = setInterval(fetchStrategy, 15000);
return () => clearInterval(interval);
}, []);
const handleToggle = async () => {
if (!user.permissions?.includes('write:strategy_toggle') && user.role !== 'admin') {
alert('Permission denied: trader role required');
return;
}
setActionLoading(true);
try {
const result = await api.toggleStrategy('ma-cross-spot');
alert(`Strategy ${result.action}`);
fetchStrategy();
} catch (err) {
alert('Error: ' + err.message);
} finally {
setActionLoading(false);
}
};
const handleConfigUpdate = async () => {
if (!user.permissions?.includes('write:strategy_config') && user.role !== 'admin') {
alert('Permission denied');
return;
}
setActionLoading(true);
try {
await api.updateStrategyConfig('ma-cross-spot', configEdit);
alert('Configuration updated');
fetchStrategy();
} catch (err) {
alert('Error: ' + err.message);
} finally {
setActionLoading(false);
}
};
if (loading) return
;
if (!strategy) return Strategy not found
;
const maData = strategy.ma_data || {};
const symbolStates = strategy.symbol_states || {};
const isRunning = strategy.status === 'running';
const cfg = strategy.config_display || {};
return (
MA20/MA50 Cross Strategy
现货金叉做多 + 质押循环杠杆
{strategy.status}
{/* Strategy Stats */}
Total PnL
= 0 ? 'trend-up' : 'trend-down'}`} style={{ fontSize: '22px' }}>
{strategy.total_pnl >= 0 ? '+' : ''}${strategy.total_pnl}
Win Rate
{strategy.win_rate}%
Trades
{strategy.total_trades}
Errors
0 ? 'var(--danger)' : 'var(--success)' }}>
{strategy.error_count}
{/* MA Data & Positions */}
MA 指标状态
{Object.entries(maData).map(([symbol, data]) => (
{symbol.replace('USDT', '')}
{data.trend === 'bullish' ? <>BULLISH> : <>BEARISH>}
MA{cfg.MA_Short || 20}
${data.ma20}
MA{cfg.MA_Long || 50}
${data.ma50}
Price
${data.current_price}
Spread: = 0 ? 'var(--success)' : 'var(--danger)' }}>
{data.distance_pct >= 0 ? '+' : ''}{data.distance_pct}%
))}
持仓状态
{Object.entries(symbolStates).map(([symbol, state]) => (
{symbol.replace('USDT', '')}
Signal: {state.last_signal || 'None'}
Position: {state.position_size.toFixed(6)}
Value: ${state.position_value.toFixed(2)}
Entry: ${state.entry_price.toFixed(2)}
= 0 ? 'var(--success)' : 'var(--danger)' }}>
Unrealized: {state.unrealized_pnl >= 0 ? '+' : ''}${state.unrealized_pnl.toFixed(2)}
))}
{/* Configuration */}
策略配置
{Object.entries(cfg).map(([key, value]) => (
{typeof value === 'boolean' ? (
{value ? 'Enabled' : 'Disabled'}
) : (
value
)}
))}
Staking Loop: 买入后质押借出 {Math.round((cfg.LTV_Ratio || 0.7) * 100)}%,最多循环 {cfg.Max_Loops || 3} 次。
理论最大杠杆: {(1 + (cfg.LTV_Ratio || 0.7) * (cfg.Max_Loops || 3)).toFixed(1)}x
);
}
// ============================================
// Risk Management Component
// ============================================
function RiskPanel({ user }) {
const [metrics, setMetrics] = useState({});
const [kelly, setKelly] = useState({});
const [confirmCode, setConfirmCode] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const [m, k] = await Promise.all([
api.getRiskMetrics(),
api.getKellyParams(),
]);
setMetrics(m);
setKelly(k);
} catch (err) {
console.error('Risk data error:', err);
} finally {
setLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, 10000);
return () => clearInterval(interval);
}, []);
const handleKillSwitch = async (level) => {
if (!confirmCode) {
alert('Please enter confirmation code (first 8 chars of your API secret)');
return;
}
if (!window.confirm(`Are you sure you want to activate ${level} kill switch? This will affect all positions!`)) {
return;
}
try {
const result = await api.activateKillSwitch(level, confirmCode);
alert(`Kill switch activated: ${result.level}\nStatus: ${result.status}`);
} catch (err) {
alert('Error: ' + err.message);
}
};
const handleDeactivate = async () => {
if (!window.confirm('Deactivate kill switch?')) return;
try {
const result = await api.deactivateKillSwitch();
alert('Kill switch deactivated');
} catch (err) {
alert('Error: ' + err.message);
}
};
if (loading) return
;
const hasKillPerm = user.permissions?.includes('write:kill_switch') || user.role === 'admin';
return (
风险管理
{/* Risk Metrics */}
Daily PnL
= 0 ? 'trend-up' : 'trend-down'}`} style={{ fontSize: '22px' }}>
{(metrics.daily_pnl_usdt || 0) >= 0 ? '+' : ''}${(metrics.daily_pnl_usdt || 0).toFixed(2)}
Total Position
${(metrics.total_position_usdt || 0).toFixed(2)}
Margin Ratio
80 ? 'var(--danger)' : 'var(--success)' }}>
{(metrics.margin_ratio_percent || 0).toFixed(1)}%
Kill Switch
{metrics.kill_switch_active ? 'ACTIVE' : 'Inactive'}
{/* Kelly Parameters */}
Kelly Criterion Parameters
Win Rate
{(kelly.win_rate * 100).toFixed(1)}%
Payoff Ratio
{kelly.payoff_ratio?.toFixed(2)}
Edge
0 ? 'var(--success)' : 'var(--danger)' }}>
{(kelly.edge * 100).toFixed(2)}%
Kelly %
{kelly.kelly_percent}%
Half Kelly (Recommended)
{kelly.half_kelly}%
Sample Size
{kelly.sample_size} trades
{/* Kill Switch Panel */}
Emergency Kill Switch
Immediately halt trading and/or close positions. Requires confirmation code and appropriate permissions.
setConfirmCode(e.target.value)}
placeholder="Enter confirmation code"
style={{ maxWidth: '300px', background: 'rgba(255,255,255,0.05)' }}
/>
{metrics.kill_switch_active && (
)}
{!hasKillPerm && (
You need risk_manager or admin role to use kill switch.
)}
);
}
// ============================================
// Positions Component
// ============================================
function PositionsPanel({ user }) {
const [spotData, setSpotData] = useState({ balances: [] });
useEffect(() => {
const fetchData = async () => {
try {
const data = await api.getBinanceSpot();
setSpotData(data);
} catch (err) {
console.error('Positions fetch error:', err);
}
};
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
const balances = spotData.balances || [];
const significant = balances.filter(b => b.value_usdt > 1);
return (
持仓管理
| Asset |
Total Amount |
Price (USDT) |
Value (USDT) |
% of Portfolio |
{significant.map(b => {
const totalPort = significant.reduce((s, x) => s + x.value_usdt, 0);
const pct = totalPort > 0 ? (b.value_usdt / totalPort * 100).toFixed(1) : '0';
return (
| {b.asset} |
{b.total.toFixed(6)} |
${b.price_usdt.toFixed(2)} |
${b.value_usdt.toFixed(2)} |
{pct}% |
);
})}
{significant.length === 0 && (
|
暂无持仓数据
|
)}
);
}
// ============================================
// Audit Logs Component
// ============================================
function AuditPanel({ user }) {
const [logs, setLogs] = useState([]);
useEffect(() => {
const fetchLogs = async () => {
try {
const data = await api.getAuditLogs();
setLogs(data.logs || []);
} catch (err) {
console.error('Audit logs error:', err);
}
};
fetchLogs();
const interval = setInterval(fetchLogs, 10000);
return () => clearInterval(interval);
}, []);
const eventColors = {
'kill_switch': 'var(--danger)',
'strategy_toggle': 'var(--accent)',
'strategy_config_change': 'var(--warning)',
'login': 'var(--success)',
'login_failed': 'var(--danger)',
'trade_executed': '#00e5ff',
'system_startup': 'var(--success)',
};
return (
审计日志
| Time |
Event |
User |
IP |
Details |
{logs.map(log => (
|
{new Date(log.timestamp).toLocaleString()}
|
{log.event_type}
|
{log.user} |
{log.client_ip} |
{JSON.stringify(log.details)}
|
))}
{logs.length === 0 && (
|
暂无审计日志
|
)}
);
}
// ============================================
// Luna Sentiment Strategy Component (Enhanced)
// ============================================
// --- Semi-circle Gauge Sub-component ---
function SentimentGauge({ score, size }) {
const s = size || 180;
const r = s * 0.38;
const cx = s / 2;
const cy = s * 0.52;
const strokeWidth = s * 0.12;
const radius = r;
const circumference = Math.PI * radius;
const pct = Math.max(0, Math.min(100, score || 0)) / 100;
const dashOffset = circumference * (1 - pct);
const getGaugeColor = (val) => {
if (val <= 20) return '#ff1744';
if (val <= 40) return '#ff6d00';
if (val <= 60) return '#ffc400';
if (val <= 80) return '#00e676';
return '#00bfa5';
};
const getGaugeLabel = (val) => {
if (val <= 20) return '极度恐惧';
if (val <= 40) return '恐惧';
if (val <= 60) return '中性';
if (val <= 80) return '贪婪';
return '极度贪婪';
};
const color = getGaugeColor(score || 0);
const label = getGaugeLabel(score || 0);
return (
);
}
// --- SVG Sparkline Sub-component ---
function SentimentSparkline({ data, width, height, color }) {
const w = width || 240;
const h = height || 60;
if (!data || data.length < 2) {
return (
暂无历史数据
);
}
const scores = data.map(d => d.score || 0);
const minS = Math.min(...scores);
const maxS = Math.max(...scores);
const range = maxS - minS || 1;
const pad = 4;
const pts = scores.map((v, i) => {
const x = pad + (i / (scores.length - 1)) * (w - pad * 2);
const y = pad + (1 - (v - minS) / range) * (h - pad * 2);
return x + ',' + y;
}).join(' ');
const areaPts = pad + ',' + (h - pad) + ' ' + pts + ' ' + (w - pad) + ',' + (h - pad);
return (
);
}
function LunaPanel({ user }) {
const [sentiments, setSentiments] = useState([]);
const [signals, setSignals] = useState([]);
const [profitability, setProfitability] = useState({});
const [strategy, setStrategy] = useState({});
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
const [backtestLoading, setBacktestLoading] = useState(false);
const [selectedSymbol, setSelectedSymbol] = useState(null);
const [historyData, setHistoryData] = useState([]);
const fetchData = async () => {
try {
const [sentData, sigData, profData] = await Promise.all([
api.getLunaAllSentiments().catch(() => ({ sentiments: [] })),
api.getLunaSignals().catch(() => ({ signals: [] })),
api.getLunaProfitability().catch(() => ({})),
]);
const sentList = sentData.sentiments || [];
setSentiments(sentList);
setSignals(sigData.signals || []);
setProfitability(profData);
if (sentData.strategy) setStrategy(sentData.strategy);
// Auto-select first symbol if none selected
if (!selectedSymbol && sentList.length > 0) {
setSelectedSymbol(sentList[0].symbol);
}
} catch (err) {
console.error('Luna data fetch error:', err);
} finally {
setLoading(false);
}
};
const fetchHistory = useCallback(async () => {
if (!selectedSymbol) return;
try {
const data = await api.getLunaHistory(selectedSymbol, 24).catch(() => ({ history: [] }));
setHistoryData(data.history || data.scores || []);
} catch (err) {
console.error('History fetch error:', err);
}
}, [selectedSymbol]);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 15000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
fetchHistory();
const interval = setInterval(fetchHistory, 30000);
return () => clearInterval(interval);
}, [fetchHistory]);
const handleCollect = async () => {
setActionLoading(true);
try {
await api.triggerLunaCollect();
setTimeout(fetchData, 2000);
} catch (err) {
alert('采集失败: ' + err.message);
} finally {
setActionLoading(false);
}
};
const handleBacktest = async () => {
setBacktestLoading(true);
try {
const result = await api.triggerLunaBacktest();
alert('回测完成! ' + (result.summary || JSON.stringify(result)));
fetchData();
} catch (err) {
alert('回测失败: ' + err.message);
} finally {
setBacktestLoading(false);
}
};
const sentimentLevelConfig = {
extreme_fear: { label: '极度恐惧', color: '#ff1744', icon: 'fa-face-dizzy' },
fear: { label: '恐惧', color: '#ff6d00', icon: 'fa-face-frown' },
neutral: { label: '中性', color: '#ffc400', icon: 'fa-face-meh' },
greed: { label: '贪婪', color: '#00e676', icon: 'fa-face-smile' },
extreme_greed: { label: '极度贪婪', color: '#00bfa5', icon: 'fa-face-grin-stars' },
};
const getGaugeColor = (score) => {
if (score <= 20) return '#ff1744';
if (score <= 40) return '#ff6d00';
if (score <= 60) return '#ffc400';
if (score <= 80) return '#00e676';
return '#00bfa5';
};
const actionColors = {
'buy': 'var(--success)',
'sell': 'var(--danger)',
'hold': 'var(--warning)',
'strong_buy': '#00e676',
'strong_sell': '#ff1744',
'BUY': 'var(--success)',
'SELL': 'var(--danger)',
};
const selectedSentiment = sentiments.find(s => s.symbol === selectedSymbol);
if (loading) {
return (
);
}
const isRunning = strategy.status === 'running';
return (
{/* Header */}
Luna 舆论交易策略
币安广场情绪分析 · 逆向+动量混合策略
{strategy.status || 'unknown'}
{/* Status Row */}
策略状态
{isRunning ? '运行中' : '已停止'}
最后采集
{strategy.last_collect ? new Date(strategy.last_collect).toLocaleTimeString() : 'N/A'}
{/* ===================== 1. Sentiment Gauge + Sparkline for Selected Symbol ===================== */}
{/* Gauge Card */}
情绪仪表盘
{/* Symbol selector pills */}
{sentiments.map(s => (
setSelectedSymbol(s.symbol)}
style={{
padding: '4px 12px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
background: selectedSymbol === s.symbol ? 'rgba(156,120,255,0.2)' : 'var(--bg-secondary)',
color: selectedSymbol === s.symbol ? '#9c78ff' : 'var(--text-secondary)',
border: '1px solid ' + (selectedSymbol === s.symbol ? 'rgba(156,120,255,0.4)' : 'transparent'),
transition: 'all 0.2s',
}}
>
{s.symbol}
))}
{selectedSentiment ? (
<>
置信度: {((selectedSentiment.confidence || 0) * 100).toFixed(0)}%
速率: = 0 ? 'var(--success)' : 'var(--danger)', fontWeight: 600 }}>
{(selectedSentiment.velocity || 0) >= 0 ? '+' : ''}{(selectedSentiment.velocity || 0).toFixed(1)}
异动: 1.5 ? 'var(--warning)' : 'var(--text-secondary)' }}>
{(selectedSentiment.volume_spike || 0).toFixed(1)}x
>
) : (
请选择币种
)}
{/* Sparkline Card */}
24小时情绪走势
{selectedSymbol && ({selectedSymbol})}
{historyData.length > 0 && (
{new Date(historyData[0].timestamp).toLocaleTimeString()}
{new Date(historyData[historyData.length - 1].timestamp).toLocaleTimeString()}
)}
{selectedSentiment && (
24h 最高
{Math.max(...historyData.map(h => h.score || 0), selectedSentiment.score || 0)}
24h 最低
{Math.min(...historyData.map(h => h.score || 0), selectedSentiment.score || 0)}
24h 均值
{historyData.length > 0
? (historyData.reduce((a, h) => a + (h.score || 0), 0) / historyData.length).toFixed(0)
: (selectedSentiment.score || 0)}
)}
{/* ===================== 3. Multi-Symbol Sentiment Dashboard ===================== */}
多币种情绪看板
{sentiments.map((s) => {
const levelCfg = sentimentLevelConfig[s.level] || sentimentLevelConfig.neutral;
const gaugeColor = getGaugeColor(s.score || 0);
const velocityDir = (s.velocity || 0) >= 0 ? 'up' : 'down';
return (
setSelectedSymbol(s.symbol)}>
{/* Symbol Header */}
{s.symbol}
{levelCfg.label}
{/* Sentiment Score Bar */}
{/* Velocity, Volume Spike, Whale */}
变化速率
= 0 ? 'var(--success)' : 'var(--danger)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '4px' }}>
{(s.velocity || 0) >= 0 ? '+' : ''}{(s.velocity || 0).toFixed(1)}
成交量异动
1.5 ? 'var(--warning)' : 'var(--text-secondary)' }}>
{(s.volume_spike || 0).toFixed(1)}x
{(s.volume_spike || 0) > 1.5 && (
异动
)}
鲸鱼信号
{s.whale_signal ? (
{'🐋'} {s.whale_signal === 'whale_accumulation' ? '吸筹' : s.whale_signal === 'whale_distribution' ? '派发' : '活跃'}
) : (
--
)}
{/* Keywords */}
{s.top_keywords && s.top_keywords.length > 0 && (
热门关键词
{s.top_keywords.slice(0, 6).map((kw, i) => (
{kw}
))}
)}
);
})}
{sentiments.length === 0 && (
)}
{/* ===================== 4. Signal History Table ===================== */}
信号历史
| 时间 |
币种 |
动作 |
情绪评分 |
置信度 |
原因 |
{signals.map((sig, i) => {
const actColor = actionColors[sig.action] || 'var(--text-secondary)';
const actLabel = sig.action === 'strong_buy' ? '强烈买入' :
sig.action === 'strong_sell' ? '强烈卖出' :
sig.action === 'buy' || sig.action === 'BUY' ? '买入' :
sig.action === 'sell' || sig.action === 'SELL' ? '卖出' :
sig.action === 'hold' ? '持有' : sig.action;
return (
|
{sig.timestamp ? new Date(sig.timestamp).toLocaleString() : 'N/A'}
|
{sig.symbol} |
{actLabel}
|
{sig.sentiment_score || 'N/A'}
|
{((sig.confidence || 0) * 100).toFixed(0)}%
|
{sig.reason || sig.rationale || '-'}
|
);
})}
{signals.length === 0 && (
|
暂无交易信号
|
)}
{/* ===================== 5. Profitability Analysis + Strategy Config ===================== */}
{/* Profitability Analysis Card */}
盈利能力分析
{/* Key Profitability Metrics */}
胜率
{((profitability.win_rate || 0.62) * 100).toFixed(0)}%
总盈亏
= 0 ? 'var(--success)' : 'var(--danger)' }}>
{(profitability.total_pnl || 0) >= 0 ? '+' : ''}{((profitability.total_pnl || 0)).toFixed(2)}
平均盈亏
= 0 ? 'var(--success)' : 'var(--danger)' }}>
{(profitability.avg_trade_pnl || 0) >= 0 ? '+' : ''}{((profitability.avg_trade_pnl || 0)).toFixed(2)}
{/* Edge Sources */}
边际来源
{(profitability.edge_sources || []).length > 0 ? (
(profitability.edge_sources || []).map((edge, i) => (
{edge.name || edge.source}
0 ? 'var(--success)' : 'var(--danger)' }}>
{(edge.edge || 0) > 0 ? '+' : ''}{((edge.edge || 0) * 100).toFixed(1)}%
))
) : (
逆向策略 (极端情绪反转)
+3.2%
动量策略 (趋势跟随)
+1.8%
鲸鱼跟踪 (大户跟随)
+2.4%
成交量异动 (异常放大)
+1.5%
)}
{/* Expected Returns */}
理论预期收益
月预期
+{((profitability.expected_monthly || 0.08) * 100).toFixed(1)}%
年预期
+{((profitability.expected_annual || 0.96) * 100).toFixed(0)}%
{/* Risk Factors */}
风险因素
{(profitability.risk_factors || []).length > 0 ? (
(profitability.risk_factors || []).map((risk, i) => (
{risk.description || risk}
))
) : (
情绪数据延迟或操控风险
极端行情下逆向策略可能失效
币安广场用户不代表整体市场情绪
)}
{/* Strategy Configuration */}
策略配置
{strategy.config ? (
{Object.entries(strategy.config).map(function(entry) {
var key = entry[0];
var value = entry[1];
return (
{key.replace(/_/g, ' ')}
{typeof value === 'boolean' ? (
{value ? '启用' : '禁用'}
) : typeof value === 'number' ? (
{value}
) : (
{String(value)}
)}
);
})}
) : (
策略模式
Contrarian + Momentum
)}
策略说明: 当情绪极度恐惧时逆向买入(逆向策略),当情绪快速上升且处于贪婪区间时顺势加仓(动量策略)。
鲸鱼吸筹信号叠加可增强置信度。止损-3%,止盈根据情绪反转信号触发。
);
}
// ============================================
// Main App
// ============================================
function App() {
const [user, setUser] = useState(() => {
try {
const saved = localStorage.getItem('qa_user');
return saved ? JSON.parse(saved) : null;
} catch { return null; }
});
const [activeTab, setActiveTab] = useState('dashboard');
const handleLogin = (userData) => {
setUser(userData);
};
const handleLogout = () => {
localStorage.removeItem('qa_token');
localStorage.removeItem('qa_refresh');
localStorage.removeItem('qa_user');
api.token = '';
api.refreshToken = '';
setUser(null);
};
// Check token on load
useEffect(() => {
const token = localStorage.getItem('qa_token');
if (token) {
api.token = token;
api.refreshToken = localStorage.getItem('qa_refresh') || '';
// Verify token is still valid
api.getSystemStatus().catch(() => {
handleLogout();
});
}
}, []);
if (!user) {
return ;
}
const renderContent = () => {
switch (activeTab) {
case 'dashboard': return ;
case 'strategy': return ;
case 'luna': return ;
case 'positions': return ;
case 'risk': return ;
case 'audit': return ;
default: return ;
}
};
return (
);
}
// Mount React app
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();