This commit is contained in:
aaron 2025-12-09 21:09:42 +08:00
parent 08f4eb6c9b
commit 1bc61cf8f5

View File

@ -257,6 +257,8 @@
<script>
let ws = null;
let reconnectInterval = null;
let currentPrice = 0; // 保存当前价格用于实时计算 PnL
let lastState = null; // 保存最新状态用于价格更新时重新计算 PnL
const TF_NAMES = { short: 'Short', medium: 'Medium', long: 'Long' };
@ -280,9 +282,21 @@
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'init') { updateState(data.state); updateSignal(data.signal); }
else if (data.type === 'state_update') { updateState(data.state); }
else if (data.type === 'signal_update') { updateSignal(data.signal); }
if (data.type === 'init') {
// 先更新信号获取当前价格,再更新状态
updateSignal(data.signal);
updateState(data.state);
}
else if (data.type === 'state_update') {
updateState(data.state);
}
else if (data.type === 'signal_update') {
updateSignal(data.signal);
// 价格更新后,重新计算 PnL
if (lastState) {
updateState(lastState);
}
}
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
};
}
@ -290,20 +304,33 @@
function updateState(state) {
if (!state || !state.accounts) return;
// 保存最新状态用于价格更新时重新计算
lastState = state;
const accounts = state.accounts;
let totalInitial = 0, totalEquity = 0, totalRealizedPnl = 0, totalUnrealizedPnl = 0;
for (const [tf, acc] of Object.entries(accounts)) {
const initial = acc.initial_balance || 0;
const realizedPnl = acc.realized_pnl || 0;
// 兼容旧数据
const equity = acc.equity || (acc.balance || initial + realizedPnl);
const unrealizedPnl = acc.position?.unrealized_pnl || 0;
const position = acc.position;
// 使用当前价格实时计算未实现盈亏
let unrealizedPnl = 0;
if (position && position.side && position.side !== 'FLAT' && currentPrice > 0) {
const entryPrice = position.entry_price || 0;
const size = position.size || 0;
if (position.side === 'LONG') {
unrealizedPnl = (currentPrice - entryPrice) * size;
} else {
unrealizedPnl = (entryPrice - currentPrice) * size;
}
}
totalInitial += initial;
totalRealizedPnl += realizedPnl;
totalUnrealizedPnl += unrealizedPnl;
totalEquity += equity + unrealizedPnl;
totalEquity += initial + realizedPnl + unrealizedPnl;
updateTimeframeCard(tf, acc);
}
@ -330,7 +357,21 @@
const initial = acc.initial_balance || 0;
const realizedPnl = acc.realized_pnl || 0;
const position = acc.position;
const unrealizedPnl = position?.unrealized_pnl || 0;
// 使用当前价格实时计算未实现盈亏
let unrealizedPnl = 0;
let unrealizedPct = 0;
if (position && position.side && position.side !== 'FLAT' && currentPrice > 0) {
const entryPrice = position.entry_price || 0;
const size = position.size || 0;
const margin = position.margin || 0;
if (position.side === 'LONG') {
unrealizedPnl = (currentPrice - entryPrice) * size;
} else {
unrealizedPnl = (entryPrice - currentPrice) * size;
}
unrealizedPct = margin > 0 ? (unrealizedPnl / margin * 100) : 0;
}
// 权益 = 初始本金 + 已实现盈亏 + 未实现盈亏
const equity = initial + realizedPnl + unrealizedPnl;
@ -354,27 +395,54 @@
badge.className = `badge ${isLong ? 'badge-long' : 'badge-short'} tf-position-badge`;
badge.textContent = position.side;
const unrealized = position.unrealized_pnl || 0;
const unrealizedPct = position.unrealized_pnl_pct || 0;
const pyramidLevel = position.pyramid_level || 1;
const pnlColor = unrealized >= 0 ? 'text-success' : 'text-danger';
const pnlColor = unrealizedPnl >= 0 ? 'text-success' : 'text-danger';
const posMargin = position.margin || 0;
const posSize = position.size || 0;
const entryPrice = position.entry_price || 0;
// 仓位价值 = 持仓数量 * 当前价格
const positionValue = currentPrice > 0 ? posSize * currentPrice : posSize * entryPrice;
// 实际杠杆 = 仓位价值 / 保证金
const actualLeverage = posMargin > 0 ? (positionValue / posMargin).toFixed(1) : '0';
// 计算爆仓价格 (假设维持保证金率为 0.5%,即亏损超过 99.5% 保证金时爆仓)
// LONG: 爆仓价 = 入场价 * (1 - 保证金 / 仓位价值 + 维持保证金率)
// SHORT: 爆仓价 = 入场价 * (1 + 保证金 / 仓位价值 - 维持保证金率)
const maintenanceMarginRate = 0.005; // 0.5% 维持保证金率
const entryValue = posSize * entryPrice;
let liquidationPrice = 0;
if (posSize > 0 && entryPrice > 0) {
if (isLong) {
// 做多爆仓价 = 入场价 - (保证金 - 维持保证金) / 持仓数量
liquidationPrice = entryPrice - (posMargin * (1 - maintenanceMarginRate)) / posSize;
} else {
// 做空爆仓价 = 入场价 + (保证金 - 维持保证金) / 持仓数量
liquidationPrice = entryPrice + (posMargin * (1 - maintenanceMarginRate)) / posSize;
}
}
// 确保爆仓价不为负
liquidationPrice = Math.max(0, liquidationPrice);
posInfo.innerHTML = `
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">Entry <span class="text-slate-600">(L${pyramidLevel}/4)</span></span>
<span class="text-white font-mono">$${(position.entry_price || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
<span class="text-white font-mono">$${entryPrice.toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
</div>
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">Stop Loss</span>
<span class="text-danger font-mono">$${(position.stop_loss || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
<span class="text-slate-500">Value / Margin</span>
<span class="text-white font-mono">$${positionValue.toLocaleString('en-US', {minimumFractionDigits: 0})} / $${posMargin.toLocaleString('en-US', {minimumFractionDigits: 0})} <span class="text-primary-400">(${actualLeverage}x)</span></span>
</div>
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">Take Profit</span>
<span class="text-success font-mono">$${(position.take_profit || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
<span class="text-slate-500">Liq. Price</span>
<span class="text-warning font-mono">$${liquidationPrice.toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
</div>
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">SL / TP</span>
<span class="font-mono"><span class="text-danger">$${(position.stop_loss || 0).toLocaleString('en-US', {minimumFractionDigits: 0})}</span> / <span class="text-success">$${(position.take_profit || 0).toLocaleString('en-US', {minimumFractionDigits: 0})}</span></span>
</div>
<div class="flex justify-between text-xs pt-1 border-t border-slate-700/50">
<span class="text-slate-500">PnL</span>
<span class="${pnlColor} font-mono font-medium">${unrealized >= 0 ? '+' : ''}$${Math.abs(unrealized).toFixed(2)} (${unrealizedPct >= 0 ? '+' : ''}${unrealizedPct.toFixed(1)}%)</span>
<span class="${pnlColor} font-mono font-medium">${unrealizedPnl >= 0 ? '+' : ''}$${Math.abs(unrealizedPnl).toFixed(2)} (${unrealizedPct >= 0 ? '+' : ''}${unrealizedPct.toFixed(1)}%)</span>
</div>
`;
posInfo.className = 'tf-position-info bg-slate-800/30 rounded-lg p-3';
@ -401,6 +469,11 @@
const price = agg.levels?.current_price || signal.market_analysis?.price || 0;
const timestamp = agg.timestamp || signal.timestamp;
// 更新全局当前价格
if (price > 0) {
currentPrice = price;
}
document.getElementById('current-price').textContent = `$${price.toLocaleString('en-US', {minimumFractionDigits: 2})}`;
// Update signal time
@ -507,7 +580,10 @@
const status = await statusRes.json();
const signal = await signalRes.json();
// Convert API response to state format
// 先更新信号获取当前价格
updateSignal({ aggregated_signal: { llm_signal: { opportunities: signal.opportunities }, levels: { current_price: signal.current_price }, timestamp: signal.timestamp } });
// 再更新状态(这样 PnL 才能用当前价格计算)
if (status.timeframes) {
const state = { accounts: {} };
for (const [tf, data] of Object.entries(status.timeframes)) {
@ -526,8 +602,6 @@
updateState(state);
}
updateSignal({ aggregated_signal: { llm_signal: { opportunities: signal.opportunities }, levels: { current_price: signal.current_price } } });
// Load trades
const tradesRes = await fetch('/api/trades?limit=50');
const tradesData = await tradesRes.json();