diff --git a/frontend/trading.html b/frontend/trading.html index 5ead61e..bdd623e 100644 --- a/frontend/trading.html +++ b/frontend/trading.html @@ -266,6 +266,66 @@ .admin-menu-item:last-child { border-radius: 0 0 var(--radius-md) var(--radius-md); } + + .pending-stack { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + } + + .pending-main { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.4; + } + + .pending-sub { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.4; + } + + .pending-sub.text-success, + .pending-sub.text-error { + font-weight: 600; + } + + .pending-chip-row { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .pending-chip { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + background: var(--bg-secondary); + border: 1px solid var(--border); + font-size: 11px; + color: var(--text-secondary); + line-height: 1.4; + white-space: nowrap; + } + + .pending-reason { + max-width: 280px; + white-space: normal; + word-break: break-word; + } + + .pending-signal { + min-width: 140px; + } + + @media (max-width: 1200px) { + .pending-reason { + max-width: 220px; + } + } @@ -436,11 +496,12 @@ 交易对 方向 - 数量 - 挂单价 - 杠杆 - 止损 - 止盈 + 规模 + 挂单信息 + 风控目标 + 仓位占用 + 信号 + 理由 时间 操作 @@ -453,11 +514,51 @@ {{ order.side === 'long' ? '做多' : '做空' }} - {{ order.quantity ? order.quantity.toFixed(4) : '0.0000' }} - {{ order.entry_price ? '$' + order.entry_price.toFixed(2) : '$0.00' }} - {{ order.leverage || 0 }}x - {{ order.stop_loss ? '$' + order.stop_loss.toFixed(2) : '-' }} - {{ order.take_profit ? '$' + order.take_profit.toFixed(2) : '-' }} + +
+
{{ formatNumber(order.quantity, 4) }}
+
杠杆 {{ order.leverage || 0 }}x
+
+ + +
+
{{ formatCurrency(order.display_entry_price) }}
+
现价 {{ formatCurrency(order.current_price) }}
+
{{ order.distance_text }}
+
+ + +
+
{{ formatCurrency(order.stop_loss) }}
+
止损 {{ formatOptionalSignedPercent(order.stop_loss_percent) }}
+
{{ formatCurrency(order.take_profit) }}
+
止盈 {{ formatOptionalSignedPercent(order.take_profit_percent) }}
+
盈亏比 {{ order.risk_reward_text }}
+
+ + +
+
{{ formatCurrency(order.margin) }}
+
保证金
+
{{ formatCurrency(order.expected_position_value) }}
+
名义仓位
+
+ + +
+
{{ order.signal_grade_text }}
+
+ {{ order.signal_type_text }} + {{ order.confidence_text }} + {{ order.entry_type_text }} +
+
+ + +
+
{{ order.reason_preview }}
+
+ {{ formatTime(order.created_at) }} @@ -678,7 +779,35 @@ }); }, pendingOrders() { - return this.orders.filter(order => order.status === 'pending'); + return this.orders + .filter(order => order.status === 'pending') + .map(order => { + const entryPrice = Number(order.entry_price || 0); + const currentPrice = this.resolveOrderCurrentPrice(order); + const stopLossPercent = this.calculateOrderTargetPercent(order.side, entryPrice, order.stop_loss); + const takeProfitPercent = this.calculateOrderTargetPercent(order.side, entryPrice, order.take_profit); + const margin = Number(order.margin || 0); + const leverage = Number(order.leverage || 0); + + return { + ...order, + display_entry_price: entryPrice, + current_price: currentPrice || null, + distance_percent: this.calculatePendingDistancePercent(currentPrice, entryPrice), + distance_text: this.getPendingDistanceText(currentPrice, entryPrice), + distance_class: this.getPendingDistanceClass(currentPrice, entryPrice), + stop_loss_percent: stopLossPercent, + take_profit_percent: takeProfitPercent, + risk_reward_ratio: this.calculateRiskRewardRatio(stopLossPercent, takeProfitPercent), + risk_reward_text: this.formatRiskRewardRatio(stopLossPercent, takeProfitPercent), + expected_position_value: margin > 0 && leverage > 0 ? margin * leverage : 0, + signal_grade_text: order.signal_grade || '-', + signal_type_text: this.getSignalTypeText(order.signal_type), + confidence_text: this.formatConfidence(order.confidence), + entry_type_text: this.getEntryTypeText(order.entry_type), + reason_preview: this.getPendingReasonPreview(order) + }; + }); }, orderHistory() { return this.orders.filter(order => { @@ -865,12 +994,25 @@ return `${number >= 0 ? '+' : '-'}${Math.abs(number).toFixed(2)}%`; }, + formatOptionalSignedPercent(value) { + const number = Number(value); + if (!Number.isFinite(number)) return '-'; + return this.formatSignedPercent(number); + }, + formatNumber(value, digits = 2) { const number = Number(value); if (!Number.isFinite(number)) return (0).toFixed(digits); return number.toFixed(digits); }, + formatConfidence(value) { + const number = Number(value); + if (!Number.isFinite(number) || number <= 0) return ''; + const percent = number <= 1 ? number * 100 : number; + return `置信度 ${percent.toFixed(1)}%`; + }, + getDisplayEntryPrice(order) { const price = Number(order.filled_price || order.entry_price || 0); return Number.isFinite(price) ? price : 0; @@ -914,6 +1056,114 @@ return positionValue * pnlPercent / 100; }, + calculatePendingDistancePercent(currentPrice, entryPrice) { + const current = Number(currentPrice); + const entry = Number(entryPrice); + if (!Number.isFinite(current) || current <= 0 || !Number.isFinite(entry) || entry <= 0) { + return null; + } + return ((entry - current) / current) * 100; + }, + + getPendingDistanceText(currentPrice, entryPrice) { + const distancePercent = this.calculatePendingDistancePercent(currentPrice, entryPrice); + if (!Number.isFinite(distancePercent)) { + return '等待触发'; + } + + if (Math.abs(distancePercent) < 0.05) { + return '接近触发'; + } + + const direction = distancePercent > 0 ? '需上涨触发' : '需回落触发'; + return `${direction} ${this.formatPercent(Math.abs(distancePercent))}`; + }, + + getPendingDistanceClass(currentPrice, entryPrice) { + const distancePercent = this.calculatePendingDistancePercent(currentPrice, entryPrice); + if (!Number.isFinite(distancePercent)) { + return ''; + } + + if (Math.abs(distancePercent) < 0.35) { + return 'text-success'; + } + + if (Math.abs(distancePercent) < 1) { + return ''; + } + + return 'text-error'; + }, + + calculateOrderTargetPercent(side, entryPrice, targetPrice) { + const entry = Number(entryPrice); + const target = Number(targetPrice); + if (!Number.isFinite(entry) || entry <= 0 || !Number.isFinite(target) || target <= 0) { + return null; + } + + if (side === 'long') { + return ((target - entry) / entry) * 100; + } + + if (side === 'short') { + return ((entry - target) / entry) * 100; + } + + return null; + }, + + calculateRiskRewardRatio(stopLossPercent, takeProfitPercent) { + const stop = Number(stopLossPercent); + const take = Number(takeProfitPercent); + if (!Number.isFinite(stop) || !Number.isFinite(take) || stop >= 0 || take <= 0) { + return null; + } + return take / Math.abs(stop); + }, + + formatRiskRewardRatio(stopLossPercent, takeProfitPercent) { + const ratio = this.calculateRiskRewardRatio(stopLossPercent, takeProfitPercent); + if (!Number.isFinite(ratio) || ratio <= 0) { + return '-'; + } + return `1:${ratio.toFixed(2)}`; + }, + + formatPercent(value, digits = 2) { + const number = Number(value); + if (!Number.isFinite(number)) return '-'; + return `${number.toFixed(digits)}%`; + }, + + getSignalTypeText(signalType) { + const map = { + short_term: '日内', + medium_term: '中线', + long_term: '趋势', + swing: '波段' + }; + return map[signalType] || signalType || ''; + }, + + getEntryTypeText(entryType) { + const map = { + market: '市价', + limit: '限价', + pending: '挂单' + }; + return map[entryType] || entryType || ''; + }, + + getPendingReasonPreview(order) { + const reasons = Array.isArray(order.entry_reasons) + ? order.entry_reasons.filter(Boolean) + : []; + const baseText = reasons.length > 0 ? reasons[0] : '暂无入场理由'; + return baseText.length > 58 ? `${baseText.slice(0, 58)}...` : baseText; + }, + getCloseReason(reason) { const map = { 'manual': '手动',