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