import React, { useState, useEffect } from 'react' import { useSelector } from 'react-redux' import { api } from '../services/api' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' import { selectAccountId } from '../store/appSlice' import './StatsDashboard.css' const StatsDashboard = () => { const accountId = useSelector(selectAccountId) // 从 Redux 获取当前账号ID const [dashboardData, setDashboardData] = useState(null) const [loading, setLoading] = useState(true) const [closingSymbol, setClosingSymbol] = useState(null) const [sltpSymbol, setSltpSymbol] = useState(null) const [sltpAllBusy, setSltpAllBusy] = useState(false) const [sltpDebugText, setSltpDebugText] = useState('') const [message, setMessage] = useState('') const [tradingConfig, setTradingConfig] = useState(null) const getCfgVal = (key, fallback = null) => { try { const cfg = tradingConfig || {} const item = cfg?.[key] if (item && typeof item === 'object' && 'value' in item) return item.value return fallback } catch (e) { return fallback } } const fmtPrice = (v) => { const n = Number(v) if (!isFinite(n)) return '-' const a = Math.abs(n) // 低价币需要更高精度,否则“止损价=入场价”只是显示被四舍五入了 const dp = a >= 1000 ? 2 : a >= 100 ? 3 : a >= 1 ? 4 : a >= 0.01 ? 6 : a >= 0.0001 ? 8 : 10 return n.toFixed(dp).replace(/\.?0+$/, '') } useEffect(() => { // 只在 accountId 存在时才加载数据,避免加载上一个账号的数据 if (!accountId) { setLoading(false) return } // 重置状态,避免显示上一个账号的数据 setDashboardData(null) setLoading(true) loadDashboard() loadTradingConfig() const interval = setInterval(() => { if (accountId) { loadDashboard() loadTradingConfig() // 同时刷新配置 } }, 30000) // 每30秒刷新 return () => { clearInterval(interval) } }, [accountId]) // 当 accountId 变化时重新加载 const loadTradingConfig = async () => { try { const configs = await api.getConfigs() setTradingConfig(configs) } catch (error) { console.error('Failed to load trading config:', error) } } const loadDashboard = async () => { try { const data = await api.getDashboard() setDashboardData(data) // dashboard 的 trading_config 可能是“子集”,不要覆盖完整配置;合并即可 if (data.trading_config) { setTradingConfig((prev) => ({ ...(prev || {}), ...(data.trading_config || {}) })) } } catch (error) { console.error('Failed to load dashboard:', error) } finally { setLoading(false) } } const handleClosePosition = async (symbol) => { if (!window.confirm(`确定要平仓 ${symbol} 吗?`)) { return } setClosingSymbol(symbol) setMessage('') try { console.log(`开始平仓 ${symbol}...`) const result = await api.closePosition(symbol) console.log('平仓结果:', result) setMessage(result.message || `${symbol} 平仓成功`) // 立即刷新数据 await loadDashboard() // 3秒后清除消息 setTimeout(() => { setMessage('') }, 3000) } catch (error) { console.error('Close position error:', error) const errorMessage = error.message || error.toString() || '平仓失败,请检查网络连接或后端服务' setMessage(`平仓失败: ${errorMessage}`) // 错误消息5秒后清除 setTimeout(() => { setMessage('') }, 5000) } finally { setClosingSymbol(null) } } const handleCloseAllPositions = async () => { if (!window.confirm(`确定要一键全平所有持仓吗?\n\n这将使用市价单平仓所有持仓,请谨慎操作!`)) { return } setClosingSymbol('ALL') // 使用特殊标记表示全平操作 setMessage('') try { console.log('开始一键全平...') const result = await api.closeAllPositions() console.log('一键全平结果:', result) let message = result.message || '一键全平完成' if (result.closed > 0 || result.failed > 0) { message = `一键全平完成: 成功 ${result.closed} / 失败 ${result.failed}` } setMessage(message) // 立即刷新数据 await loadDashboard() // 5秒后清除消息 setTimeout(() => { setMessage('') }, 5000) } catch (error) { console.error('Close all positions error:', error) const errorMessage = error.message || error.toString() || '一键全平失败,请检查网络连接或后端服务' setMessage(`一键全平失败: ${errorMessage}`) // 错误消息5秒后清除 setTimeout(() => { setMessage('') }, 5000) } finally { setClosingSymbol(null) } } const handleEnsureSLTP = async (symbol) => { if (!window.confirm(`确定要为 ${symbol} 补挂“币安止损/止盈保护单”吗?\n\n说明:将自动取消该交易对已有的 STOP/TP 保护单并重新挂单(避免重复)。`)) { return } setSltpSymbol(symbol) setMessage('') setSltpDebugText('') try { const res = await api.ensurePositionSLTP(symbol) const slId = res?.orders?.stop_market?.orderId const tpId = res?.orders?.take_profit_market?.orderId const cnt = Array.isArray(res?.open_protection_orders) ? res.open_protection_orders.length : 0 setMessage(`${symbol} 已尝试补挂:SL=${slId || '-'} / TP=${tpId || '-'}(当前检测到保护单 ${cnt} 条)`) setSltpDebugText(JSON.stringify(res || {}, null, 2)) await loadDashboard() } catch (error) { setMessage(`补挂失败 ${symbol}: ${error.message || '未知错误'}`) } finally { setSltpSymbol(null) setTimeout(() => setMessage(''), 4000) } } const handleEnsureAllSLTP = async () => { if (!window.confirm('确定要为【所有当前持仓】一键补挂“币安止损/止盈保护单”吗?\n\n说明:会对每个持仓 symbol 自动取消旧的 STOP/TP 保护单并重新挂单(避免重复)。')) { return } setSltpAllBusy(true) setMessage('') setSltpDebugText('') try { const res = await api.ensureAllPositionsSLTP(50) const ok = res?.ok ?? 0 const failed = res?.failed ?? 0 let openCnt = 0 try { const arr = Array.isArray(res?.results) ? res.results : [] openCnt = arr.reduce((acc, it) => acc + (Array.isArray(it?.open_protection_orders) ? it.open_protection_orders.length : 0), 0) } catch (e) { openCnt = 0 } setMessage(`一键补挂完成:成功 ${ok} / 失败 ${failed}(检测到保护单 ${openCnt} 条)`) setSltpDebugText(JSON.stringify(res || {}, null, 2)) await loadDashboard() } catch (error) { setMessage(`一键补挂失败: ${error.message || '未知错误'}`) } finally { setSltpAllBusy(false) setTimeout(() => setMessage(''), 5000) } } const handleSyncPositions = async () => { if (!window.confirm('确定要同步持仓状态吗?这将检查币安实际持仓并更新数据库状态。')) { return } setMessage('正在同步持仓状态...') try { const result = await api.syncPositions() console.log('同步结果:', result) let message = result.message || '同步完成' if (result.updated_to_closed > 0) { message += `,已更新 ${result.updated_to_closed} 条记录为已平仓` } if (result.missing_in_db && result.missing_in_db.length > 0) { message += `,发现 ${result.missing_in_db.length} 个币安持仓在数据库中没有记录` } setMessage(message) // 立即刷新数据 await loadDashboard() // 5秒后清除消息 setTimeout(() => { setMessage('') }, 5000) } catch (error) { console.error('Sync positions error:', error) const errorMessage = error.message || error.toString() || '同步失败,请检查网络连接或后端服务' setMessage(`同步失败: ${errorMessage}`) // 错误消息5秒后清除 setTimeout(() => { setMessage('') }, 5000) } } if (loading) return
加载中...
const account = dashboardData?.account const openTrades = dashboardData?.open_trades || [] const entryTypeCounts = openTrades.reduce( (acc, t) => { const tp = String(t?.entry_order_type || '').toUpperCase() if (tp === 'LIMIT') acc.limit += 1 else if (tp === 'MARKET') acc.market += 1 else acc.unknown += 1 return acc }, { limit: 0, market: 0, unknown: 0 } ) return (

仪表板

{message && (
{message} {sltpDebugText ? (
查看补挂返回详情(用于排查)
{sltpDebugText}
              
) : null}
)}
重要说明:币安账户请使用「单向持仓模式(One-way)」
当前检测: {account?.position_mode === 'one_way' ? '单向(推荐)' : account?.position_mode === 'hedge' ? '对冲(不推荐,容易出现 positionSide/mode 相关错误)' : '未知(建议重启后端/交易系统后再看)'}
操作指引: 币安 U本位合约 → 偏好设置/设置 → 持仓模式(Position Mode) → 选择「单向」。切换通常要求当前无持仓/无挂单。
入场逻辑: 智能入场(方案C):先限价回调入场,未成交会有限追价;趋势强时在偏离可控范围内可市价兜底(减少错过)。 当前配置:SMART_ENTRY_ENABLED={String(getCfgVal('SMART_ENTRY_ENABLED', true))}, LIMIT_ORDER_OFFSET_PCT={String(getCfgVal('LIMIT_ORDER_OFFSET_PCT', '0.5'))}, ENTRY_MAX_DRIFT_PCT_TRENDING={String(getCfgVal('ENTRY_MAX_DRIFT_PCT_TRENDING', '0.6'))}, ENTRY_MAX_DRIFT_PCT_RANGING={String(getCfgVal('ENTRY_MAX_DRIFT_PCT_RANGING', '0.3'))}。

账户信息

{account ? (
总余额: {parseFloat(account.total_balance).toFixed(2)} USDT
可用余额: {parseFloat(account.available_balance).toFixed(2)} USDT
总仓位(名义): {parseFloat(account.total_position_value).toFixed(2)} USDT
{account.total_margin_value !== undefined && account.total_margin_value !== null && (
保证金占用(估算): {parseFloat(account.total_margin_value).toFixed(2)} USDT
)}
总盈亏: = 0 ? 'positive' : 'negative'}`}> {parseFloat(account.total_pnl).toFixed(2)} USDT
持仓数量: {account.open_positions}
持仓模式: {account.position_mode === 'one_way' ? '单向' : account.position_mode === 'hedge' ? '对冲' : '未知'}
) : (
暂无数据
)}
{dashboardData?.position_stats && (

仓位占比

当前仓位占比(保证金): {dashboardData.position_stats.current_position_percent}% dashboardData.position_stats.max_position_percent * 0.8 ? '#ff6b6b' : dashboardData.position_stats.current_position_percent > dashboardData.position_stats.max_position_percent * 0.6 ? '#ffa500' : '#51cf66' }} />
最大仓位占比: {dashboardData.position_stats.max_position_percent}%
{dashboardData.position_stats.current_notional_percent !== undefined && (
名义占比(参考): {dashboardData.position_stats.current_notional_percent}%
)}
最大保证金: {dashboardData.position_stats.max_position_value.toFixed(2)} USDT
已用保证金: {dashboardData.position_stats.total_position_value.toFixed(2)} USDT
可用保证金: {(dashboardData.position_stats.max_position_value - dashboardData.position_stats.total_position_value).toFixed(2)} USDT
)}

当前持仓

限价入场: {entryTypeCounts.limit} 市价入场: {entryTypeCounts.market} 未知: {entryTypeCounts.unknown}
{openTrades.length > 0 ? (
{openTrades.map((trade, index) => { // 计算价格涨跌比例、止损比例、止盈比例 const entryPrice = parseFloat(trade.entry_price || 0) const markPrice = parseFloat(trade.mark_price || entryPrice) const side = trade.side || 'BUY' const quantity = parseFloat(trade.quantity || 0) // 价格涨跌比例(当前价格相对于入场价) let priceChangePercent = 0 if (entryPrice > 0) { if (side === 'BUY') { priceChangePercent = ((markPrice - entryPrice) / entryPrice) * 100 } else { priceChangePercent = ((entryPrice - markPrice) / entryPrice) * 100 } } // 名义/保证金:优先使用后端返回字段(ATR接入后也用于统一口径) const entryValue = trade.notional_usdt !== undefined && trade.notional_usdt !== null ? parseFloat(trade.notional_usdt) : ( trade.entry_value_usdt !== undefined && trade.entry_value_usdt !== null ? parseFloat(trade.entry_value_usdt) : (quantity * entryPrice) ) const leverage = parseFloat(trade.leverage || 10) const margin = trade.margin_usdt !== undefined && trade.margin_usdt !== null ? parseFloat(trade.margin_usdt) : (leverage > 0 ? entryValue / leverage : entryValue) // 从配置获取止损止盈比例(相对于保证金) const configSource = dashboardData?.trading_config || tradingConfig const stopLossConfig = configSource?.STOP_LOSS_PERCENT const takeProfitConfig = configSource?.TAKE_PROFIT_PERCENT // 配置值是小数形式(0.08表示8%),相对于保证金 let stopLossPercentMargin = 0.08 // 默认8%(相对于保证金,更宽松) let takeProfitPercentMargin = 0.15 // 默认15%(相对于保证金,给趋势更多空间) if (stopLossConfig) { const configValue = stopLossConfig.value if (typeof configValue === 'number') { stopLossPercentMargin = configValue } else { const parsed = parseFloat(configValue) if (!isNaN(parsed)) { stopLossPercentMargin = parsed } } } if (takeProfitConfig) { const configValue = takeProfitConfig.value if (typeof configValue === 'number') { takeProfitPercentMargin = configValue } else { const parsed = parseFloat(configValue) if (!isNaN(parsed)) { takeProfitPercentMargin = parsed } } } // 计算止损价和止盈价(基于保证金金额) // 优先使用后端返回的止损止盈价格,如果没有则自己计算 let stopLossPrice = 0 let takeProfitPrice = 0 if (trade.stop_loss_price && trade.take_profit_price) { // 使用后端返回的止损止盈价格(如果可用) stopLossPrice = parseFloat(trade.stop_loss_price) takeProfitPrice = parseFloat(trade.take_profit_price) } else { // 计算止损止盈金额(相对于保证金) const stopLossAmount = margin * stopLossPercentMargin const takeProfitAmount = margin * takeProfitPercentMargin // 自己计算止损止盈价格 // 止损金额 = (开仓价 - 止损价) × 数量 或 (止损价 - 开仓价) × 数量 if (side === 'BUY') { // 做多:止损价 = 开仓价 - (止损金额 / 数量) stopLossPrice = entryPrice - (stopLossAmount / quantity) // 做多:止盈价 = 开仓价 + (止盈金额 / 数量) takeProfitPrice = entryPrice + (takeProfitAmount / quantity) } else { // 做空:止损价 = 开仓价 + (止损金额 / 数量) stopLossPrice = entryPrice + (stopLossAmount / quantity) // 做空:止盈价 = 开仓价 - (止盈金额 / 数量) takeProfitPrice = entryPrice - (takeProfitAmount / quantity) } } // ATR 接入后,止损/止盈“金额与比例”要以实际价格反推(否则会显示成配置值而非真实值) let stopLossAmount = 0 let takeProfitAmount = 0 if (entryPrice > 0 && quantity > 0 && stopLossPrice > 0 && takeProfitPrice > 0) { if (side === 'BUY') { stopLossAmount = Math.max(0, (entryPrice - stopLossPrice) * quantity) takeProfitAmount = Math.max(0, (takeProfitPrice - entryPrice) * quantity) } else { stopLossAmount = Math.max(0, (stopLossPrice - entryPrice) * quantity) takeProfitAmount = Math.max(0, (entryPrice - takeProfitPrice) * quantity) } } else { stopLossAmount = margin * stopLossPercentMargin takeProfitAmount = margin * takeProfitPercentMargin } const stopLossPercent = margin > 0 ? (stopLossAmount / margin) * 100 : (stopLossPercentMargin * 100) const takeProfitPercent = margin > 0 ? (takeProfitAmount / margin) * 100 : (takeProfitPercentMargin * 100) // 也计算价格百分比(用于参考) const stopLossPercentPrice = side === 'BUY' ? ((entryPrice - stopLossPrice) / entryPrice) * 100 : ((stopLossPrice - entryPrice) / entryPrice) * 100 const takeProfitPercentPrice = side === 'BUY' ? ((takeProfitPrice - entryPrice) / entryPrice) * 100 : ((entryPrice - takeProfitPrice) / entryPrice) * 100 // 格式化开仓时间为具体的年月日时分秒 // 支持Unix时间戳(秒数)或日期字符串 const formatEntryTime = (timeValue) => { if (!timeValue) return null try { let date // 如果是数字(Unix时间戳),转换为毫秒 if (typeof timeValue === 'number') { date = new Date(timeValue * 1000) } else { date = new Date(timeValue) } if (isNaN(date.getTime())) return String(timeValue) const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') const seconds = String(date.getSeconds()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } catch (e) { return String(timeValue) } } return (
{trade.symbol}
{trade.side}
入场类型:{' '} {String(trade.entry_order_type || '').toUpperCase() === 'LIMIT' ? '限价' : String(trade.entry_order_type || '').toUpperCase() === 'MARKET' ? '市价' : '未知'}
数量: {parseFloat(trade.quantity || 0).toFixed(4)}
名义: {entryValue >= 0.01 ? entryValue.toFixed(2) : entryValue.toFixed(4)} USDT
保证金: {margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT
入场价: {fmtPrice(entryPrice)}
{trade.mark_price && (
标记价: {fmtPrice(markPrice)}
)} {trade.leverage && (
杠杆: {trade.leverage}x
)} {trade.atr !== undefined && trade.atr !== null && (
ATR: {parseFloat(trade.atr).toFixed(6)}
)} {/* 价格涨跌比例 */}
= 0 ? 'positive' : 'negative'}`}> 价格涨跌: {priceChangePercent >= 0 ? '+' : ''}{priceChangePercent.toFixed(2)}%
{/* 止损止盈比例 */} {trade.entry_time && (
开仓时间: {formatEntryTime(trade.entry_time)}
)}
止损: -{stopLossPercent.toFixed(2)}% (of margin) (价: {fmtPrice(stopLossPrice)}) (金额: -{stopLossAmount >= 0.01 ? stopLossAmount.toFixed(2) : stopLossAmount.toFixed(4)} USDT)
止盈: +{takeProfitPercent.toFixed(2)}% (of margin) (价: {fmtPrice(takeProfitPrice)}) (金额: +{takeProfitAmount >= 0.01 ? takeProfitAmount.toFixed(2) : takeProfitAmount.toFixed(4)} USDT)
{(trade.take_profit_1 || trade.take_profit_2) && (
{trade.take_profit_1 && ( TP1: {fmtPrice(parseFloat(trade.take_profit_1))} )} {trade.take_profit_2 && ( TP2: {fmtPrice(parseFloat(trade.take_profit_2))} )}
)}
= 0 ? 'positive' : 'negative'}`}> {parseFloat(trade.pnl || 0).toFixed(2)} USDT {trade.pnl_percent !== undefined && ( ({parseFloat(trade.pnl_percent).toFixed(2)}%) )}
) })}
) : (
暂无持仓
)}
) } export default StatsDashboard