1
This commit is contained in:
parent
df55f2ff37
commit
37b1dc682d
@ -266,6 +266,66 @@
|
|||||||
.admin-menu-item:last-child {
|
.admin-menu-item:last-child {
|
||||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -436,11 +496,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>交易对</th>
|
<th>交易对</th>
|
||||||
<th>方向</th>
|
<th>方向</th>
|
||||||
<th>数量</th>
|
<th>规模</th>
|
||||||
<th>挂单价</th>
|
<th>挂单信息</th>
|
||||||
<th>杠杆</th>
|
<th>风控目标</th>
|
||||||
<th>止损</th>
|
<th>仓位占用</th>
|
||||||
<th>止盈</th>
|
<th>信号</th>
|
||||||
|
<th>理由</th>
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -453,11 +514,51 @@
|
|||||||
{{ order.side === 'long' ? '做多' : '做空' }}
|
{{ order.side === 'long' ? '做多' : '做空' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ order.quantity ? order.quantity.toFixed(4) : '0.0000' }}</td>
|
<td>
|
||||||
<td>{{ order.entry_price ? '$' + order.entry_price.toFixed(2) : '$0.00' }}</td>
|
<div class="pending-stack">
|
||||||
<td>{{ order.leverage || 0 }}x</td>
|
<div class="pending-main">{{ formatNumber(order.quantity, 4) }}</div>
|
||||||
<td>{{ order.stop_loss ? '$' + order.stop_loss.toFixed(2) : '-' }}</td>
|
<div class="pending-sub">杠杆 {{ order.leverage || 0 }}x</div>
|
||||||
<td>{{ order.take_profit ? '$' + order.take_profit.toFixed(2) : '-' }}</td>
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="pending-stack">
|
||||||
|
<div class="pending-main">{{ formatCurrency(order.display_entry_price) }}</div>
|
||||||
|
<div class="pending-sub">现价 {{ formatCurrency(order.current_price) }}</div>
|
||||||
|
<div class="pending-sub" :class="order.distance_class">{{ order.distance_text }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="pending-stack">
|
||||||
|
<div class="pending-main">{{ formatCurrency(order.stop_loss) }}</div>
|
||||||
|
<div class="pending-sub text-error">止损 {{ formatOptionalSignedPercent(order.stop_loss_percent) }}</div>
|
||||||
|
<div class="pending-main">{{ formatCurrency(order.take_profit) }}</div>
|
||||||
|
<div class="pending-sub text-success">止盈 {{ formatOptionalSignedPercent(order.take_profit_percent) }}</div>
|
||||||
|
<div class="pending-sub">盈亏比 {{ order.risk_reward_text }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="pending-stack">
|
||||||
|
<div class="pending-main">{{ formatCurrency(order.margin) }}</div>
|
||||||
|
<div class="pending-sub">保证金</div>
|
||||||
|
<div class="pending-main">{{ formatCurrency(order.expected_position_value) }}</div>
|
||||||
|
<div class="pending-sub">名义仓位</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="pending-stack pending-signal">
|
||||||
|
<div class="pending-main">{{ order.signal_grade_text }}</div>
|
||||||
|
<div class="pending-chip-row">
|
||||||
|
<span v-if="order.signal_type_text" class="pending-chip">{{ order.signal_type_text }}</span>
|
||||||
|
<span v-if="order.confidence_text" class="pending-chip">{{ order.confidence_text }}</span>
|
||||||
|
<span v-if="order.entry_type_text" class="pending-chip">{{ order.entry_type_text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="pending-stack pending-reason">
|
||||||
|
<div class="pending-main">{{ order.reason_preview }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>{{ formatTime(order.created_at) }}</td>
|
<td>{{ formatTime(order.created_at) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-danger btn-small" @click="cancelOrder(order)">撤单</button>
|
<button class="btn btn-danger btn-small" @click="cancelOrder(order)">撤单</button>
|
||||||
@ -678,7 +779,35 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
pendingOrders() {
|
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() {
|
orderHistory() {
|
||||||
return this.orders.filter(order => {
|
return this.orders.filter(order => {
|
||||||
@ -865,12 +994,25 @@
|
|||||||
return `${number >= 0 ? '+' : '-'}${Math.abs(number).toFixed(2)}%`;
|
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) {
|
formatNumber(value, digits = 2) {
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
if (!Number.isFinite(number)) return (0).toFixed(digits);
|
if (!Number.isFinite(number)) return (0).toFixed(digits);
|
||||||
return number.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) {
|
getDisplayEntryPrice(order) {
|
||||||
const price = Number(order.filled_price || order.entry_price || 0);
|
const price = Number(order.filled_price || order.entry_price || 0);
|
||||||
return Number.isFinite(price) ? price : 0;
|
return Number.isFinite(price) ? price : 0;
|
||||||
@ -914,6 +1056,114 @@
|
|||||||
return positionValue * pnlPercent / 100;
|
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) {
|
getCloseReason(reason) {
|
||||||
const map = {
|
const map = {
|
||||||
'manual': '手动',
|
'manual': '手动',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user