update
This commit is contained in:
parent
08f4eb6c9b
commit
1bc61cf8f5
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user