838 lines
27 KiB
HTML
838 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>交易信号 - Stock Agent</title>
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
<style>
|
||
/* 防止横向滚动 */
|
||
html, body {
|
||
overflow-x: hidden;
|
||
max-width: 100vw;
|
||
}
|
||
|
||
/* 覆盖全局 #app 样式 */
|
||
#app {
|
||
height: auto;
|
||
display: block;
|
||
align-items: initial;
|
||
justify-content: initial;
|
||
padding: 0;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.signals-page {
|
||
min-height: 100vh;
|
||
background: var(--bg-primary);
|
||
padding: 20px;
|
||
}
|
||
|
||
.signals-container {
|
||
max-width: 1400px;
|
||
min-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 固定顶部区域 */
|
||
.sticky-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
background: var(--bg-primary);
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.signals-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding: 10px 0 20px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
.signals-title {
|
||
font-size: 24px;
|
||
font-weight: 300;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.signals-title span {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.refresh-btn {
|
||
padding: 8px 16px;
|
||
background: transparent;
|
||
border: 1px solid var(--accent);
|
||
color: var(--accent);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.refresh-btn:hover {
|
||
background: var(--accent);
|
||
color: var(--bg-primary);
|
||
}
|
||
|
||
/* 统计卡片 */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
font-weight: 300;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.stat-value.positive {
|
||
color: #00ff41;
|
||
}
|
||
|
||
.stat-value.negative {
|
||
color: #ff4444;
|
||
}
|
||
|
||
/* 标签页 */
|
||
.tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 20px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 24px;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab:hover {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.tab.active {
|
||
color: var(--accent);
|
||
border-bottom-color: var(--accent);
|
||
}
|
||
|
||
/* 信号卡片网格 */
|
||
.signals-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.signal-card {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 20px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.signal-card:hover {
|
||
border-color: var(--accent);
|
||
box-shadow: 0 4px 12px rgba(0, 200, 150, 0.1);
|
||
}
|
||
|
||
.signal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.signal-symbol-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.signal-symbol {
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.signal-type-badge {
|
||
font-size: 11px;
|
||
padding: 3px 8px;
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-secondary);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.signal-action-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.signal-action {
|
||
padding: 6px 16px;
|
||
border-radius: 2px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.signal-action.buy {
|
||
background: rgba(0, 255, 65, 0.1);
|
||
color: #00ff41;
|
||
}
|
||
|
||
.signal-action.sell {
|
||
background: rgba(255, 68, 68, 0.1);
|
||
color: #ff4444;
|
||
}
|
||
|
||
.signal-action.hold {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.signal-grade {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 2px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.signal-grade.A {
|
||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 179, 0, 0.2));
|
||
color: gold;
|
||
}
|
||
|
||
.signal-grade.B {
|
||
background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(160, 160, 160, 0.2));
|
||
color: silver;
|
||
}
|
||
|
||
.signal-grade.C {
|
||
background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(160, 82, 45, 0.2));
|
||
color: #cd7f32;
|
||
}
|
||
|
||
.signal-grade.D {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* 置信度条 */
|
||
.confidence-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.confidence-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 6px;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.confidence-value {
|
||
color: var(--accent);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.confidence-bar {
|
||
height: 4px;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.confidence-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--accent) 0%, #00ff41 100%);
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
/* 价格信息 */
|
||
.price-section {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
padding: 12px 0;
|
||
margin-bottom: 12px;
|
||
border-top: 1px solid var(--border);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.price-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.price-label {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.price-value {
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
font-family: monospace;
|
||
}
|
||
|
||
/* 信号详情 */
|
||
.signal-details {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
margin-bottom: 6px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.detail-label {
|
||
color: var(--text-secondary);
|
||
min-width: 70px;
|
||
}
|
||
|
||
.detail-value {
|
||
color: var(--text-primary);
|
||
flex: 1;
|
||
}
|
||
|
||
/* 分析理由 */
|
||
.signal-reason {
|
||
background: var(--bg-tertiary);
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.reason-label {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.reason-text {
|
||
font-size: 13px;
|
||
color: var(--text-primary);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 时间戳 */
|
||
.signal-time {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.empty-state svg {
|
||
width: 48px;
|
||
height: 48px;
|
||
margin-bottom: 16px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.signals-page {
|
||
padding: 10px;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.signals-container {
|
||
min-width: auto;
|
||
max-width: 100%;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* 取消顶部固定 */
|
||
.sticky-header {
|
||
position: static;
|
||
}
|
||
|
||
.signals-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
}
|
||
|
||
.signals-title {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.signals-title span {
|
||
display: block;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.stats-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
.stat-card {
|
||
padding: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 16px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.tabs {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.tab {
|
||
padding: 10px 16px;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.signals-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.price-section {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.stats-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.signals-title {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
/* 等级统计 */
|
||
.grade-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.grade-stat-card {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.grade-stat-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.grade-stat-title {
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.grade-stat-count {
|
||
font-size: 20px;
|
||
font-weight: 300;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.grade-stat-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.grade-stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
color: var(--text-secondary);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<div class="signals-page">
|
||
<div class="signals-container">
|
||
<!-- 固定顶部区域 -->
|
||
<div class="sticky-header">
|
||
<!-- 头部 -->
|
||
<div class="signals-header">
|
||
<h1 class="signals-title">交易信号中心 <span>| Trading Signals</span></h1>
|
||
<button class="refresh-btn" @click="loadSignals">刷新</button>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">加密货币信号</div>
|
||
<div class="stat-value">{{ stats.crypto.total }}</div>
|
||
<div class="stat-label" style="margin-top: 6px;">最近24小时: {{ stats.crypto.recent_24h }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">美股信号</div>
|
||
<div class="stat-value">{{ stats.stock.total }}</div>
|
||
<div class="stat-label" style="margin-top: 6px;">最近24小时: {{ stats.stock.recent_24h }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">总信号数</div>
|
||
<div class="stat-value">{{ stats.total }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">买入信号</div>
|
||
<div class="stat-value positive">{{ stats.crypto.buy + stats.stock.buy }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">卖出信号</div>
|
||
<div class="stat-value negative">{{ stats.crypto.sell + stats.stock.sell }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 等级统计 -->
|
||
<div class="grade-stats" v-if="Object.keys(stats.grades).length > 0">
|
||
<div class="grade-stat-card" v-for="(count, grade) in stats.grades" :key="grade">
|
||
<div class="grade-stat-header">
|
||
<span class="grade-stat-title">
|
||
<span class="signal-grade" :class="grade">{{ grade }}</span> 级信号
|
||
</span>
|
||
<span class="grade-stat-count">{{ count }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签页 -->
|
||
<div class="tabs">
|
||
<button class="tab" :class="{ active: currentTab === 'crypto' }" @click="switchTab('crypto')">
|
||
加密货币 ({{ cryptoSignals.length }})
|
||
</button>
|
||
<button class="tab" :class="{ active: currentTab === 'stock' }" @click="switchTab('stock')">
|
||
美股 ({{ stockSignals.length }})
|
||
</button>
|
||
<button class="tab" :class="{ active: currentTab === 'all' }" @click="switchTab('all')">
|
||
全部信号 ({{ allSignals.length }})
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 信号列表 -->
|
||
<div v-if="loading" class="loading">加载中...</div>
|
||
<div v-else-if="currentSignals.length === 0" class="empty-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<path d="M12 6v6l4 2"/>
|
||
</svg>
|
||
<p>暂无信号</p>
|
||
</div>
|
||
<div v-else class="signals-grid">
|
||
<div v-for="signal in currentSignals" :key="signal.id" class="signal-card" :class="signal.action">
|
||
<!-- 信号头部 -->
|
||
<div class="signal-header">
|
||
<div class="signal-symbol-group">
|
||
<span class="signal-symbol">{{ signal.symbol }}</span>
|
||
<span class="signal-type-badge">{{ signal.signal_type === 'crypto' ? '加密货币' : '美股' }}</span>
|
||
</div>
|
||
<div class="signal-action-group">
|
||
<span class="signal-action" :class="signal.action">
|
||
{{ signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '持有' }}
|
||
</span>
|
||
<span class="signal-grade" :class="signal.grade">{{ signal.grade }}级</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 置信度 -->
|
||
<div class="confidence-section">
|
||
<div class="confidence-label">
|
||
<span>置信度</span>
|
||
<span class="confidence-value">{{ signal.confidence }}%</span>
|
||
</div>
|
||
<div class="confidence-bar">
|
||
<div class="confidence-fill" :style="{ width: signal.confidence + '%' }"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 价格信息 -->
|
||
<div class="price-section" v-if="getEntryPrice(signal) || signal.stop_loss || signal.take_profit">
|
||
<div class="price-item" v-if="getEntryPrice(signal)">
|
||
<div class="price-label">{{ getEntryPriceLabel(signal) }}</div>
|
||
<div class="price-value">${{ getEntryPrice(signal).toFixed(2) }}</div>
|
||
</div>
|
||
<div class="price-item" v-if="signal.stop_loss">
|
||
<div class="price-label">止损</div>
|
||
<div class="price-value">${{ signal.stop_loss?.toFixed(2) }}</div>
|
||
</div>
|
||
<div class="price-item" v-if="signal.take_profit">
|
||
<div class="price-label">止盈</div>
|
||
<div class="price-value">${{ signal.take_profit?.toFixed(2) }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 信号详情 -->
|
||
<div class="signal-details" v-if="signal.signal_type_detail || signal.entry_type || signal.position_size || signal.current_price">
|
||
<div class="detail-row" v-if="signal.current_price">
|
||
<span class="detail-label">当前价:</span>
|
||
<span class="detail-value">${{ signal.current_price?.toFixed(2) }}</span>
|
||
</div>
|
||
<div class="detail-row" v-if="signal.signal_type_detail">
|
||
<span class="detail-label">周期:</span>
|
||
<span class="detail-value">{{ getSignalTypeText(signal.signal_type_detail) }}</span>
|
||
</div>
|
||
<div class="detail-row" v-if="signal.entry_type">
|
||
<span class="detail-label">入场:</span>
|
||
<span class="detail-value">{{ getEntryTypeText(signal.entry_type) }}</span>
|
||
</div>
|
||
<div class="detail-row" v-if="signal.position_size">
|
||
<span class="detail-label">仓位:</span>
|
||
<span class="detail-value">{{ getPositionSizeText(signal.position_size) }}</span>
|
||
</div>
|
||
<div class="detail-row" v-if="signal.news_sentiment">
|
||
<span class="detail-label">情绪:</span>
|
||
<span class="detail-value">{{ getNewsSentimentText(signal.news_sentiment) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分析理由(合并 reason 和 analysis_summary) -->
|
||
<div class="signal-reason" v-if="getCombinedReason(signal)">
|
||
<div class="reason-label">分析理由</div>
|
||
<div class="reason-text">{{ getCombinedReason(signal) }}</div>
|
||
</div>
|
||
|
||
<!-- 时间戳 -->
|
||
<div class="signal-time">
|
||
{{ formatTime(signal.timestamp || signal.created_at) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||
<script>
|
||
const { createApp } = Vue;
|
||
|
||
createApp({
|
||
data() {
|
||
return {
|
||
currentTab: 'crypto',
|
||
loading: true,
|
||
cryptoSignals: [],
|
||
stockSignals: [],
|
||
allSignals: [],
|
||
stats: {
|
||
crypto: { total: 0, buy: 0, sell: 0, recent_24h: 0 },
|
||
stock: { total: 0, buy: 0, sell: 0, recent_24h: 0 },
|
||
grades: {},
|
||
total: 0
|
||
},
|
||
refreshInterval: null
|
||
};
|
||
},
|
||
computed: {
|
||
currentSignals() {
|
||
if (this.currentTab === 'crypto') return this.cryptoSignals;
|
||
if (this.currentTab === 'stock') return this.stockSignals;
|
||
return this.allSignals;
|
||
}
|
||
},
|
||
methods: {
|
||
switchTab(tab) {
|
||
this.currentTab = tab;
|
||
},
|
||
|
||
async loadSignals() {
|
||
this.loading = true;
|
||
try {
|
||
await Promise.all([
|
||
this.loadCryptoSignals(),
|
||
this.loadStockSignals(),
|
||
this.loadStats()
|
||
]);
|
||
} catch (e) {
|
||
console.error('加载信号失败:', e);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
async loadCryptoSignals() {
|
||
const response = await fetch('/api/signals/crypto?limit=50&days=7');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.cryptoSignals = data.signals || [];
|
||
}
|
||
},
|
||
|
||
async loadStockSignals() {
|
||
const response = await fetch('/api/signals/stock?limit=50&days=7');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.stockSignals = data.signals || [];
|
||
}
|
||
},
|
||
|
||
async loadStats() {
|
||
const response = await fetch('/api/signals/stats?days=7');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.stats = {
|
||
crypto: data.crypto || { total: 0, buy: 0, sell: 0, recent_24h: 0 },
|
||
stock: data.stock || { total: 0, buy: 0, sell: 0, recent_24h: 0 },
|
||
grades: data.grades || {},
|
||
total: data.total || 0
|
||
};
|
||
}
|
||
},
|
||
|
||
formatTime(timeStr) {
|
||
if (!timeStr) return '-';
|
||
const date = new Date(timeStr);
|
||
return date.toLocaleString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
},
|
||
|
||
getSignalTypeText(type) {
|
||
const map = {
|
||
'short_term': '短期',
|
||
'medium_term': '中期',
|
||
'long_term': '长期'
|
||
};
|
||
return map[type] || type;
|
||
},
|
||
|
||
getEntryTypeText(type) {
|
||
const map = {
|
||
'market': '市价',
|
||
'limit': '限价'
|
||
};
|
||
return map[type] || type;
|
||
},
|
||
|
||
getPositionSizeText(size) {
|
||
const map = {
|
||
'light': '轻仓',
|
||
'medium': '中仓',
|
||
'heavy': '重仓'
|
||
};
|
||
return map[size] || size;
|
||
},
|
||
|
||
// 获取入场价格(限价单显示挂单价格,市价单显示入场价)
|
||
getEntryPrice(signal) {
|
||
// 如果是限价单且有挂单价格,优先显示挂单价格
|
||
if (signal.entry_type === 'limit' && signal.entry_zone) {
|
||
return signal.entry_zone;
|
||
}
|
||
// 否则显示普通入场价
|
||
return signal.entry_price;
|
||
},
|
||
|
||
// 获取入场价格标签
|
||
getEntryPriceLabel(signal) {
|
||
if (signal.entry_type === 'limit' && signal.entry_zone) {
|
||
return '挂单价';
|
||
}
|
||
return '入场价';
|
||
},
|
||
|
||
// 获取新闻情绪文本
|
||
getNewsSentimentText(sentiment) {
|
||
const map = {
|
||
'bullish': '看涨',
|
||
'bearish': '看跌',
|
||
'neutral': '中性',
|
||
'positive': '积极',
|
||
'negative': '消极'
|
||
};
|
||
return map[sentiment] || sentiment;
|
||
},
|
||
|
||
// 格式化价位数据
|
||
formatLevels(levels) {
|
||
if (Array.isArray(levels)) {
|
||
return levels.map(l => '$' + l.toFixed(2)).join(', ');
|
||
} else if (typeof levels === 'number') {
|
||
return '$' + levels.toFixed(2);
|
||
} else if (typeof levels === 'string') {
|
||
return levels;
|
||
}
|
||
return '';
|
||
},
|
||
|
||
// 合并理由和分析摘要
|
||
getCombinedReason(signal) {
|
||
// 优先使用 analysis_summary,如果不存在则使用 reason
|
||
return signal.analysis_summary || signal.reason || '';
|
||
}
|
||
},
|
||
mounted() {
|
||
this.loadSignals();
|
||
// 每30秒自动刷新
|
||
this.refreshInterval = setInterval(() => {
|
||
this.loadSignals();
|
||
}, 30000);
|
||
},
|
||
beforeUnmount() {
|
||
if (this.refreshInterval) {
|
||
clearInterval(this.refreshInterval);
|
||
}
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html>
|