639 lines
29 KiB
JavaScript
639 lines
29 KiB
JavaScript
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 [sltpSymbol, setSltpSymbol] = useState(null)
|
||
const [sltpAllBusy, setSltpAllBusy] = useState(false)
|
||
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(() => {
|
||
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 的 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 handleEnsureSLTP = async (symbol) => {
|
||
if (!window.confirm(`确定要为 ${symbol} 补挂“币安止损/止盈保护单”吗?\n\n说明:将自动取消该交易对已有的 STOP/TP 保护单并重新挂单(避免重复)。`)) {
|
||
return
|
||
}
|
||
setSltpSymbol(symbol)
|
||
setMessage('')
|
||
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} 条)`)
|
||
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('')
|
||
try {
|
||
const res = await api.ensureAllPositionsSLTP(50)
|
||
const ok = res?.ok ?? 0
|
||
const failed = res?.failed ?? 0
|
||
setMessage(`一键补挂完成:成功 ${ok} / 失败 ${failed}(请在币安【条件单/止盈止损】里查看)`)
|
||
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 <div className="loading">加载中...</div>
|
||
|
||
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 (
|
||
<div className="dashboard">
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||
<h2>仪表板</h2>
|
||
<button
|
||
onClick={handleSyncPositions}
|
||
style={{
|
||
padding: '8px 16px',
|
||
backgroundColor: '#007bff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px'
|
||
}}
|
||
title="同步币安实际持仓状态与数据库状态"
|
||
>
|
||
同步持仓状态
|
||
</button>
|
||
</div>
|
||
|
||
{message && (
|
||
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
|
||
{message}
|
||
</div>
|
||
)}
|
||
|
||
<div className="dashboard-notice important">
|
||
<div className="notice-title">重要说明:币安账户请使用「单向持仓模式(One-way)」</div>
|
||
<div className="notice-body">
|
||
<div className="notice-row">
|
||
<span className="notice-label">当前检测:</span>
|
||
<span className={`notice-value ${account?.position_mode === 'one_way' ? 'ok' : 'warn'}`}>
|
||
{account?.position_mode === 'one_way'
|
||
? '单向(推荐)'
|
||
: account?.position_mode === 'hedge'
|
||
? '对冲(不推荐,容易出现 positionSide/mode 相关错误)'
|
||
: '未知(建议重启后端/交易系统后再看)'}
|
||
</span>
|
||
</div>
|
||
<div className="notice-row">
|
||
<span className="notice-label">操作指引:</span>
|
||
<span className="notice-text">
|
||
币安 U本位合约 → 偏好设置/设置 → 持仓模式(Position Mode) → 选择「单向」。切换通常要求当前无持仓/无挂单。
|
||
</span>
|
||
</div>
|
||
<div className="notice-row">
|
||
<span className="notice-label">入场逻辑:</span>
|
||
<span className="notice-text">
|
||
智能入场(方案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'))}。
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="dashboard-grid">
|
||
<div className="dashboard-card">
|
||
<h3>账户信息</h3>
|
||
{account ? (
|
||
<div className="account-info">
|
||
<div className="info-item">
|
||
<span className="label">总余额:</span>
|
||
<span className="value">{parseFloat(account.total_balance).toFixed(2)} USDT</span>
|
||
</div>
|
||
<div className="info-item">
|
||
<span className="label">可用余额:</span>
|
||
<span className="value">{parseFloat(account.available_balance).toFixed(2)} USDT</span>
|
||
</div>
|
||
<div className="info-item">
|
||
<span className="label">总仓位(名义):</span>
|
||
<span className="value">{parseFloat(account.total_position_value).toFixed(2)} USDT</span>
|
||
</div>
|
||
{account.total_margin_value !== undefined && account.total_margin_value !== null && (
|
||
<div className="info-item">
|
||
<span className="label">保证金占用(估算):</span>
|
||
<span className="value">{parseFloat(account.total_margin_value).toFixed(2)} USDT</span>
|
||
</div>
|
||
)}
|
||
<div className="info-item">
|
||
<span className="label">总盈亏:</span>
|
||
<span className={`value ${parseFloat(account.total_pnl) >= 0 ? 'positive' : 'negative'}`}>
|
||
{parseFloat(account.total_pnl).toFixed(2)} USDT
|
||
</span>
|
||
</div>
|
||
<div className="info-item">
|
||
<span className="label">持仓数量:</span>
|
||
<span className="value">{account.open_positions}</span>
|
||
</div>
|
||
<div className="info-item">
|
||
<span className="label">持仓模式:</span>
|
||
<span className={`value ${account.position_mode === 'one_way' ? 'positive' : 'negative'}`}>
|
||
{account.position_mode === 'one_way' ? '单向' : account.position_mode === 'hedge' ? '对冲' : '未知'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div>暂无数据</div>
|
||
)}
|
||
</div>
|
||
|
||
{dashboardData?.position_stats && (
|
||
<div className="dashboard-card">
|
||
<h3>仓位占比</h3>
|
||
<div className="account-info">
|
||
<div className="info-item">
|
||
<span className="label">当前仓位占比(保证金):</span>
|
||
<span className="value">
|
||
<span>{dashboardData.position_stats.current_position_percent}%</span>
|
||
<span className="position-bar-container">
|
||
<span
|
||
className="position-bar"
|
||
style={{
|
||
width: `${Math.min((dashboardData.position_stats.current_position_percent / dashboardData.position_stats.max_position_percent) * 100, 100)}%`,
|
||
backgroundColor: 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'
|
||
}}
|
||
/>
|
||
</span>
|
||
</span>
|
||
</div>
|
||
<div className="info-item">
|
||
<span className="label">最大仓位占比:</span>
|
||
<span className="value">{dashboardData.position_stats.max_position_percent}%</span>
|
||
</div>
|
||
{dashboardData.position_stats.current_notional_percent !== undefined && (
|
||
<div className="info-item">
|
||
<span className="label">名义占比(参考):</span>
|
||
<span className="value">{dashboardData.position_stats.current_notional_percent}%</span>
|
||
</div>
|
||
)}
|
||
<div className="info-item">
|
||
<span className="label">最大保证金:</span>
|
||
<span className="value">{dashboardData.position_stats.max_position_value.toFixed(2)} USDT</span>
|
||
</div>
|
||
<div className="info-item">
|
||
<span className="label">已用保证金:</span>
|
||
<span className="value">{dashboardData.position_stats.total_position_value.toFixed(2)} USDT</span>
|
||
</div>
|
||
<div className="info-item">
|
||
<span className="label">可用保证金:</span>
|
||
<span className="value">
|
||
{(dashboardData.position_stats.max_position_value - dashboardData.position_stats.total_position_value).toFixed(2)} USDT
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="dashboard-card">
|
||
<div className="positions-header">
|
||
<h3>当前持仓</h3>
|
||
<button
|
||
className="sltp-all-btn"
|
||
onClick={handleEnsureAllSLTP}
|
||
disabled={sltpAllBusy || openTrades.length === 0}
|
||
title="为所有持仓在币安侧补挂 STOP_MARKET + TAKE_PROFIT_MARKET 保护单(closePosition)"
|
||
>
|
||
{sltpAllBusy ? '一键补挂中...' : '一键补挂止盈止损'}
|
||
</button>
|
||
</div>
|
||
<div className="entry-type-summary">
|
||
<span className="entry-type-badge limit">限价入场: {entryTypeCounts.limit}</span>
|
||
<span className="entry-type-badge market">市价入场: {entryTypeCounts.market}</span>
|
||
<span className="entry-type-badge unknown">未知: {entryTypeCounts.unknown}</span>
|
||
</div>
|
||
{openTrades.length > 0 ? (
|
||
<div className="trades-list">
|
||
{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 (
|
||
<div key={trade.id || trade.symbol || index} className="trade-item">
|
||
<div className="trade-symbol">{trade.symbol}</div>
|
||
<div className={`trade-side ${trade.side === 'BUY' ? 'buy' : 'sell'}`}>
|
||
{trade.side}
|
||
</div>
|
||
<div className="trade-info">
|
||
<div className="entry-type-line">
|
||
入场类型:{' '}
|
||
<span
|
||
className={`entry-type-badge ${
|
||
String(trade.entry_order_type || '').toUpperCase() === 'LIMIT'
|
||
? 'limit'
|
||
: String(trade.entry_order_type || '').toUpperCase() === 'MARKET'
|
||
? 'market'
|
||
: 'unknown'
|
||
}`}
|
||
title="来自币安 entry_order_id 查询的订单类型"
|
||
>
|
||
{String(trade.entry_order_type || '').toUpperCase() === 'LIMIT'
|
||
? '限价'
|
||
: String(trade.entry_order_type || '').toUpperCase() === 'MARKET'
|
||
? '市价'
|
||
: '未知'}
|
||
</span>
|
||
</div>
|
||
<div>数量: {parseFloat(trade.quantity || 0).toFixed(4)}</div>
|
||
<div>名义: {entryValue >= 0.01 ? entryValue.toFixed(2) : entryValue.toFixed(4)} USDT</div>
|
||
<div>保证金: {margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</div>
|
||
|
||
<div>入场价: {fmtPrice(entryPrice)}</div>
|
||
{trade.mark_price && (
|
||
<div>标记价: {fmtPrice(markPrice)}</div>
|
||
)}
|
||
{trade.leverage && (
|
||
<div>杠杆: {trade.leverage}x</div>
|
||
)}
|
||
{trade.atr !== undefined && trade.atr !== null && (
|
||
<div>ATR: {parseFloat(trade.atr).toFixed(6)}</div>
|
||
)}
|
||
|
||
{/* 价格涨跌比例 */}
|
||
<div className={`price-change ${priceChangePercent >= 0 ? 'positive' : 'negative'}`}>
|
||
价格涨跌: {priceChangePercent >= 0 ? '+' : ''}{priceChangePercent.toFixed(2)}%
|
||
</div>
|
||
|
||
{/* 止损止盈比例 */}
|
||
|
||
|
||
{trade.entry_time && (
|
||
<div className="entry-time">开仓时间: {formatEntryTime(trade.entry_time)}</div>
|
||
)}
|
||
</div>
|
||
<div className="stop-take-info">
|
||
<div className="stop-loss-info">
|
||
止损: <span className="negative">-{stopLossPercent.toFixed(2)}%</span>
|
||
<span className="stop-note">(of margin)</span>
|
||
<span className="stop-price">(价: {fmtPrice(stopLossPrice)})</span>
|
||
<span className="stop-amount">(金额: -{stopLossAmount >= 0.01 ? stopLossAmount.toFixed(2) : stopLossAmount.toFixed(4)} USDT)</span>
|
||
</div>
|
||
<div className="take-profit-info">
|
||
止盈: <span className="positive">+{takeProfitPercent.toFixed(2)}%</span>
|
||
<span className="take-note">(of margin)</span>
|
||
<span className="take-price">(价: {fmtPrice(takeProfitPrice)})</span>
|
||
<span className="take-amount">(金额: +{takeProfitAmount >= 0.01 ? takeProfitAmount.toFixed(2) : takeProfitAmount.toFixed(4)} USDT)</span>
|
||
</div>
|
||
{(trade.take_profit_1 || trade.take_profit_2) && (
|
||
<div style={{ marginTop: '6px', fontSize: '12px', color: '#666' }}>
|
||
{trade.take_profit_1 && (
|
||
<span style={{ marginRight: '10px' }}>TP1: {fmtPrice(parseFloat(trade.take_profit_1))}</span>
|
||
)}
|
||
{trade.take_profit_2 && (
|
||
<span>TP2: {fmtPrice(parseFloat(trade.take_profit_2))}</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="trade-actions">
|
||
<div className={`trade-pnl ${parseFloat(trade.pnl || 0) >= 0 ? 'positive' : 'negative'}`}>
|
||
{parseFloat(trade.pnl || 0).toFixed(2)} USDT
|
||
{trade.pnl_percent !== undefined && (
|
||
<span> ({parseFloat(trade.pnl_percent).toFixed(2)}%)</span>
|
||
)}
|
||
</div>
|
||
<button
|
||
className="sltp-btn"
|
||
onClick={() => handleEnsureSLTP(trade.symbol)}
|
||
disabled={sltpSymbol === trade.symbol}
|
||
title="在币安侧补挂 STOP_MARKET + TAKE_PROFIT_MARKET 保护单(closePosition)"
|
||
>
|
||
{sltpSymbol === trade.symbol ? '补挂中...' : '补挂止盈止损'}
|
||
</button>
|
||
<button
|
||
className="close-btn"
|
||
onClick={() => handleClosePosition(trade.symbol)}
|
||
disabled={closingSymbol === trade.symbol}
|
||
title={`平仓 ${trade.symbol}`}
|
||
>
|
||
{closingSymbol === trade.symbol ? '平仓中...' : '平仓'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
) : (
|
||
<div>暂无持仓</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default StatsDashboard
|