import React, { useState, useEffect } from 'react'
import { api } from '../services/api'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
import './StatsDashboard.css'
const StatsDashboard = () => {
const [dashboardData, setDashboardData] = useState(null)
const [loading, setLoading] = useState(true)
const [closingSymbol, setClosingSymbol] = useState(null)
const [message, setMessage] = useState('')
const [tradingConfig, setTradingConfig] = useState(null)
useEffect(() => {
loadDashboard()
loadTradingConfig()
const interval = setInterval(() => {
loadDashboard()
loadTradingConfig() // 同时刷新配置
}, 30000) // 每30秒刷新
return () => clearInterval(interval)
}, [])
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数据中包含配置,也更新配置状态
if (data.trading_config) {
setTradingConfig(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 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 || []
return (
仪表板
{message && (
{message}
)}
账户信息
{account ? (
总余额:
{parseFloat(account.total_balance).toFixed(2)} USDT
可用余额:
{parseFloat(account.available_balance).toFixed(2)} USDT
总仓位:
{parseFloat(account.total_position_value).toFixed(2)} USDT
总盈亏:
= 0 ? 'positive' : 'negative'}`}>
{parseFloat(account.total_pnl).toFixed(2)} USDT
持仓数量:
{account.open_positions}
) : (
暂无数据
)}
{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.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
)}
当前持仓
{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
}
}
// 计算保证金(用于基于保证金的止损止盈)
const entryValue = trade.entry_value_usdt !== undefined
? parseFloat(trade.entry_value_usdt)
: (quantity * entryPrice)
const leverage = parseFloat(trade.leverage || 10)
const margin = 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
}
}
}
// 计算止损止盈金额(相对于保证金)
const stopLossAmount = margin * stopLossPercentMargin
const takeProfitAmount = margin * takeProfitPercentMargin
// 计算止损价和止盈价(基于保证金金额)
// 优先使用后端返回的止损止盈价格,如果没有则自己计算
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 {
// 自己计算止损止盈价格
// 止损金额 = (开仓价 - 止损价) × 数量 或 (止损价 - 开仓价) × 数量
if (side === 'BUY') {
// 做多:止损价 = 开仓价 - (止损金额 / 数量)
stopLossPrice = entryPrice - (stopLossAmount / quantity)
// 做多:止盈价 = 开仓价 + (止盈金额 / 数量)
takeProfitPrice = entryPrice + (takeProfitAmount / quantity)
} else {
// 做空:止损价 = 开仓价 + (止损金额 / 数量)
stopLossPrice = entryPrice + (stopLossAmount / quantity)
// 做空:止盈价 = 开仓价 - (止盈金额 / 数量)
takeProfitPrice = entryPrice - (takeProfitAmount / quantity)
}
}
// 计算止损比例和止盈比例(相对于保证金,用于显示)
const stopLossPercent = stopLossPercentMargin * 100 // 相对于保证金
const takeProfitPercent = 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
// 格式化开仓时间为具体的年月日时分秒
const formatEntryTime = (timeStr) => {
if (!timeStr) return null
try {
const date = new Date(timeStr)
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 timeStr
}
}
return (
{trade.symbol}
{trade.side}
数量: {parseFloat(trade.quantity || 0).toFixed(4)}
保证金: {margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT
入场价: {entryPrice.toFixed(4)}
{trade.mark_price && (
标记价: {markPrice.toFixed(4)}
)}
{trade.leverage && (
杠杆: {trade.leverage}x
)}
{/* 价格涨跌比例 */}
= 0 ? 'positive' : 'negative'}`}>
价格涨跌: {priceChangePercent >= 0 ? '+' : ''}{priceChangePercent.toFixed(2)}%
{/* 止损止盈比例 */}
{trade.entry_time && (
开仓时间: {formatEntryTime(trade.entry_time)}
)}
止损: -{stopLossPercent.toFixed(2)}%
(of margin)
(价: {stopLossPrice.toFixed(4)})
(金额: -{stopLossAmount >= 0.01 ? stopLossAmount.toFixed(2) : stopLossAmount.toFixed(4)} USDT)
止盈: +{takeProfitPercent.toFixed(2)}%
(of margin)
(价: {takeProfitPrice.toFixed(4)})
(金额: +{takeProfitAmount >= 0.01 ? takeProfitAmount.toFixed(2) : takeProfitAmount.toFixed(4)} USDT)
= 0 ? 'positive' : 'negative'}`}>
{parseFloat(trade.pnl || 0).toFixed(2)} USDT
{trade.pnl_percent !== undefined && (
({parseFloat(trade.pnl_percent).toFixed(2)}%)
)}
)
})}
) : (
暂无持仓
)}
)
}
export default StatsDashboard