feat: 趋势过滤信号记录系统 + 实时涨跌方向判断
- 新增 TrendFilter 信号记录(enter/exit),按完整信号和高分信号两档分类 - 信号持久化到 data/trend_signals_cache.json,开机自动恢复 - 新增 /api/trend-signals API + SSE trend_signal 实时广播 - 前端新增完整信号卡片和高分信号卡片,移除旧趋势检测卡片 - 评分加入 1h 涨跌方向和实时 drift 惩罚,下跌币不触发信号 - OKX 交易所支持(累积变动、动量、趋势检测) - 修复 trend_filter.go 编译错误 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
73dac50a36
commit
559d7bb870
+40
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
-40
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Exchange Monitor Dashboard</title>
|
||||
<script type="module" crossorigin src="/static/assets/index-Dr4jUtK1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-DsdSIpuQ.css">
|
||||
<script type="module" crossorigin src="/static/assets/index-B9OruCsy.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-CDE5zNyv.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -242,3 +242,23 @@ tr:hover td { background: rgba(88, 166, 255, 0.05); }
|
||||
.trend-confirmed:hover td { background: rgba(63, 185, 80, 0.15) !important; }
|
||||
.trend-exhausting { background: rgba(139, 148, 158, 0.05); }
|
||||
.trend-exhausting:hover td { background: rgba(139, 148, 158, 0.1) !important; }
|
||||
|
||||
/* Trend Filter Card */
|
||||
.text-orange { color: var(--yellow); }
|
||||
#trend-filter-card { grid-column: 1 / -1; }
|
||||
#trend-filter-table td { font-variant-numeric: tabular-nums; }
|
||||
.filter-pass td { background: rgba(63, 185, 80, 0.06); }
|
||||
.filter-pass:hover td { background: rgba(63, 185, 80, 0.12) !important; }
|
||||
|
||||
/* blue text for categories */
|
||||
.text-blue { color: #58a6ff; }
|
||||
|
||||
/* Trend Signal Card */
|
||||
#trend-signal-card { grid-column: 1 / -1; }
|
||||
#trend-signal-table td { font-variant-numeric: tabular-nums; }
|
||||
#high-score-card { grid-column: 1 / -1; }
|
||||
#high-score-table td { font-variant-numeric: tabular-nums; }
|
||||
.signal-enter td { background: rgba(63, 185, 80, 0.08); }
|
||||
.signal-enter:hover td { background: rgba(63, 185, 80, 0.15) !important; }
|
||||
.signal-exit td { background: rgba(139, 148, 158, 0.05); }
|
||||
.signal-exit:hover td { background: rgba(139, 148, 158, 0.1) !important; }
|
||||
|
||||
+218
-167
@@ -28,9 +28,10 @@ export default function App() {
|
||||
const [trades, setTrades] = useState([])
|
||||
const [momentum, setMomentum] = useState([])
|
||||
const [trendData, setTrendData] = useState([])
|
||||
const [trendHistory, setTrendHistory] = useState([])
|
||||
const [cmData, setCmData] = useState([])
|
||||
const [cmHistory, setCmHistory] = useState([])
|
||||
const [trendFilter, setTrendFilter] = useState([])
|
||||
const [trendSignals, setTrendSignals] = useState([])
|
||||
const priceCacheRef = useRef({})
|
||||
|
||||
// Clock
|
||||
@@ -83,6 +84,13 @@ export default function App() {
|
||||
case 'cumulative':
|
||||
setCmData(msg.data || [])
|
||||
break
|
||||
case 'trend_filter':
|
||||
setTrendFilter(msg.data || [])
|
||||
break
|
||||
case 'trend_signal':
|
||||
// Prepend new signal to list
|
||||
setTrendSignals(prev => [msg.data, ...prev].slice(0, 100))
|
||||
break
|
||||
case 'stats':
|
||||
setStats(msg.data || {})
|
||||
if (msg.data && msg.data.blacklist) {
|
||||
@@ -118,23 +126,6 @@ export default function App() {
|
||||
return () => clearInterval(id)
|
||||
}, [loadTrades])
|
||||
|
||||
// Load trend history
|
||||
const loadTrendHistory = useCallback(async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/trend-history')
|
||||
const data = await resp.json()
|
||||
setTrendHistory(data.events || [])
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadTrendHistory()
|
||||
const id = setInterval(loadTrendHistory, 5000)
|
||||
return () => clearInterval(id)
|
||||
}, [loadTrendHistory])
|
||||
|
||||
// Load cumulative history
|
||||
const loadCmHistory = useCallback(async () => {
|
||||
try {
|
||||
@@ -152,6 +143,23 @@ export default function App() {
|
||||
return () => clearInterval(id)
|
||||
}, [loadCmHistory])
|
||||
|
||||
// Load trend signals from API
|
||||
const loadTrendSignals = useCallback(async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/trend-signals')
|
||||
const data = await resp.json()
|
||||
if (data.signals) setTrendSignals(data.signals)
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadTrendSignals()
|
||||
const id = setInterval(loadTrendSignals, 5000)
|
||||
return () => clearInterval(id)
|
||||
}, [loadTrendSignals])
|
||||
|
||||
// Handle prices
|
||||
function handlePrices(data) {
|
||||
if (!data || data.length === 0) return
|
||||
@@ -210,6 +218,21 @@ export default function App() {
|
||||
</header>
|
||||
|
||||
<div className="grid">
|
||||
{/* Trend Filter (K-line quiet + EMA52) — 最优先 */}
|
||||
<TrendFilterCard filterData={trendFilter} />
|
||||
|
||||
{/* Full Signal Records (FreshAnomaly + Score >= 70) */}
|
||||
<FullSignalCard signals={trendSignals} />
|
||||
|
||||
{/* High Score Signal Records (Score >= 90) */}
|
||||
<HighScoreCard signals={trendSignals} />
|
||||
|
||||
{/* Cumulative Change (1min consensus) */}
|
||||
<CmCard data={cmData} />
|
||||
|
||||
{/* Cumulative History */}
|
||||
<CmHistoryCard history={cmHistory} />
|
||||
|
||||
{/* Price Table */}
|
||||
<PriceTable coins={coins} prices={prices} getPrevPrice={getPrevPrice} priceClass={priceClass} pricesAge={pricesAge} />
|
||||
|
||||
@@ -219,18 +242,6 @@ export default function App() {
|
||||
{/* Momentum Scanner */}
|
||||
<MomentumCard momentum={momentum} />
|
||||
|
||||
{/* Trend Detection */}
|
||||
<TrendCard trend={trendData} />
|
||||
|
||||
{/* Trend History */}
|
||||
<TrendHistoryCard history={trendHistory} />
|
||||
|
||||
{/* Cumulative Change (1min consensus) */}
|
||||
<CmCard data={cmData} />
|
||||
|
||||
{/* Cumulative History */}
|
||||
<CmHistoryCard history={cmHistory} />
|
||||
|
||||
{/* ---- 交易相关 ---- */}
|
||||
|
||||
{/* Stats Summary */}
|
||||
@@ -958,87 +969,6 @@ function MomentumCard({ momentum }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ============ Trend Detection Card ============
|
||||
function TrendCard({ trend }) {
|
||||
function stateLabel(state) {
|
||||
switch (state) {
|
||||
case 'alert': return '⚠ 异动'
|
||||
case 'confirmed': return '🚀 趋势'
|
||||
case 'exhausting': return '🔄 衰减'
|
||||
default: return state
|
||||
}
|
||||
}
|
||||
|
||||
function stateClass(state) {
|
||||
switch (state) {
|
||||
case 'alert': return 'trend-alert'
|
||||
case 'confirmed': return 'trend-confirmed'
|
||||
case 'exhausting': return 'trend-exhausting'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
function dirIcon(dir) {
|
||||
return dir === 'up' ? '\u2191' : '\u2193'
|
||||
}
|
||||
|
||||
function dirClass(dir) {
|
||||
return dir === 'up' ? 'text-green' : 'text-red'
|
||||
}
|
||||
|
||||
function changeClass(val) {
|
||||
if (val == null || val === 0) return ''
|
||||
return val > 0 ? 'text-green' : 'text-red'
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card card-wide" id="trend-card">
|
||||
<h2>📈 趋势检测 (价格异动)</h2>
|
||||
<div className="table-wrap" style={{ maxHeight: 300 }}>
|
||||
<table id="trend-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>币种</th>
|
||||
<th>状态</th>
|
||||
<th>方向</th>
|
||||
<th>异动分</th>
|
||||
<th>波动率</th>
|
||||
<th>一致数</th>
|
||||
<th>BG 15s</th>
|
||||
<th>HL 15s</th>
|
||||
<th>BN 15s</th>
|
||||
<th>OKX 15s</th>
|
||||
<th>时长</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trend.length === 0 ? (
|
||||
<tr><td colSpan="11" className="text-dim" style={{textAlign:'center',padding:20}}>
|
||||
等待检测数据... (需要至少 3 个交易所数据)
|
||||
</td></tr>
|
||||
) : trend.slice(0, 30).map(entry => (
|
||||
<tr key={entry.coin} className={stateClass(entry.state)}>
|
||||
<td><strong>{entry.coin}</strong></td>
|
||||
<td className="trend-state">{stateLabel(entry.state)}</td>
|
||||
<td className={dirClass(entry.direction)} style={{textAlign:'center',fontSize:18}}>{dirIcon(entry.direction)}</td>
|
||||
<td className="text-right" style={{fontWeight:700}}>{(entry.anomaly_score || 0).toFixed(1)}σ</td>
|
||||
<td className="text-right">{(entry.volatility || 0).toFixed(4)}%</td>
|
||||
<td className="text-right">{entry.ex_changes || 0}/4</td>
|
||||
<td className={'text-right ' + changeClass(entry.bg_change)}>{entry.bg_change != null ? entry.bg_change.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.hl_change)}>{entry.hl_change != null ? entry.hl_change.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.bn_change)}>{entry.bn_change != null ? entry.bn_change.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.okx_change)}>{entry.okx_change != null ? entry.okx_change.toFixed(3) + '%' : '-'}</td>
|
||||
<td className="text-dim">{entry.duration || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ Trend History Card ============
|
||||
// ============ Cumulative Change Card (1min consensus) ============
|
||||
function CmCard({ data }) {
|
||||
function stateLabel(state) {
|
||||
@@ -1087,11 +1017,16 @@ function CmCard({ data }) {
|
||||
<th>HL 5m</th>
|
||||
<th>BN 5m</th>
|
||||
<th>OKX 5m</th>
|
||||
<th colSpan={4} style={{borderLeft:'2px solid var(--border)'}}>1h 趋势</th>
|
||||
<th>BG 1h</th>
|
||||
<th>HL 1h</th>
|
||||
<th>BN 1h</th>
|
||||
<th>OKX 1h</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!data || data.length === 0 ? (
|
||||
<tr><td colSpan="14" className="text-dim" style={{textAlign:'center',padding:20}}>
|
||||
<tr><td colSpan="19" className="text-dim" style={{textAlign:'center',padding:20}}>
|
||||
等待累积数据... (需要至少 1 分钟数据)
|
||||
</td></tr>
|
||||
) : data.slice(0, 30).map(entry => (
|
||||
@@ -1110,6 +1045,11 @@ function CmCard({ data }) {
|
||||
<td className={'text-right ' + changeClass(entry.hl_5m)}>{entry.hl_5m != null ? entry.hl_5m.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.bn_5m)}>{entry.bn_5m != null ? entry.bn_5m.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.okx_5m)}>{entry.okx_5m != null ? entry.okx_5m.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + dirClass(entry.direction)} style={{fontWeight:600,borderLeft:'2px solid var(--border)'}}>{(entry.avg_1h || 0).toFixed(2)}%</td>
|
||||
<td className={'text-right ' + changeClass(entry.bg_1h)}>{entry.bg_1h != null ? entry.bg_1h.toFixed(2) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.hl_1h)}>{entry.hl_1h != null ? entry.hl_1h.toFixed(2) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.bn_1h)}>{entry.bn_1h != null ? entry.bn_1h.toFixed(2) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(entry.okx_1h)}>{entry.okx_1h != null ? entry.okx_1h.toFixed(2) + '%' : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -1201,78 +1141,85 @@ function CmHistoryCard({ history }) {
|
||||
)
|
||||
}
|
||||
|
||||
function TrendHistoryCard({ history }) {
|
||||
function stateLabel(state) {
|
||||
switch (state) {
|
||||
case 'alert': return '⚠ 异动'
|
||||
case 'confirmed': return '🚀 趋势'
|
||||
case 'exhausting': return '🔄 衰减'
|
||||
case 'idle': return '✓ 结束'
|
||||
default: return state
|
||||
function TrendFilterCard({ filterData }) {
|
||||
const passing = filterData.filter(f => f.passes_filter).length
|
||||
const highScore = filterData.filter(f => f.signal_score >= 80).length
|
||||
const midScore = filterData.filter(f => f.signal_score >= 50 && f.signal_score < 80).length
|
||||
const anomalyCount = filterData.filter(f => f.fresh_anomaly).length
|
||||
|
||||
// Build header description
|
||||
let desc = `高分${highScore} 中分${midScore}`
|
||||
if (anomalyCount > 0) {
|
||||
desc += ` | ${anomalyCount}币异动中`
|
||||
if (passing > 0) {
|
||||
desc += ` → ${passing}通过!`
|
||||
}
|
||||
} else {
|
||||
desc += ' | 等待异动信号'
|
||||
}
|
||||
|
||||
function stateClass(state) {
|
||||
switch (state) {
|
||||
case 'alert': return 'text-yellow'
|
||||
case 'confirmed': return 'text-green'
|
||||
case 'exhausting': return 'text-dim'
|
||||
case 'idle': return 'text-dim'
|
||||
default: return ''
|
||||
}
|
||||
function scoreClass(s) {
|
||||
if (s == null) return ''
|
||||
if (s >= 80) return 'text-green'
|
||||
if (s >= 50) return 'text-yellow'
|
||||
return 'text-dim'
|
||||
}
|
||||
|
||||
function dirIcon(dir) {
|
||||
return dir === 'up' ? '\u2191' : '\u2193'
|
||||
function volClass(r) {
|
||||
if (r == null || r <= 1.5) return ''
|
||||
if (r > 3.0) return 'text-red'
|
||||
return 'text-orange'
|
||||
}
|
||||
|
||||
function dirClass(dir) {
|
||||
return dir === 'up' ? 'text-green' : 'text-red'
|
||||
}
|
||||
|
||||
function changeClass(val) {
|
||||
if (val == null || val === 0) return ''
|
||||
return val > 0 ? 'text-green' : 'text-red'
|
||||
function slopeClass(s) {
|
||||
if (s == null || s === 0) return ''
|
||||
return s > 0 ? 'text-green' : 'text-red'
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card card-wide" id="trend-history-card">
|
||||
<h2>📋 趋势事件记录</h2>
|
||||
<section className="card card-wide" id="trend-filter-card">
|
||||
<h2>趋势过滤 ({passing}通过 / {filterData.length}) <span className="text-dim" style={{fontSize:12,fontWeight:400}}>{desc}</span></h2>
|
||||
<div className="table-wrap" style={{ maxHeight: 400 }}>
|
||||
<table id="trend-history-table">
|
||||
<table id="trend-filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>币种</th>
|
||||
<th>转换</th>
|
||||
<th>方向</th>
|
||||
<th>异动分</th>
|
||||
<th>波动率</th>
|
||||
<th>一致</th>
|
||||
<th>BG</th>
|
||||
<th>HL</th>
|
||||
<th>BN</th>
|
||||
<th>OKX</th>
|
||||
<th>分数</th>
|
||||
<th>24h范围</th>
|
||||
<th>基线</th>
|
||||
<th>1h范围</th>
|
||||
<th>成交量比</th>
|
||||
<th>1h变化</th>
|
||||
<th>EMA52</th>
|
||||
<th>EMA斜率</th>
|
||||
<th>现价</th>
|
||||
<th>> EMA</th>
|
||||
<th>安静24h</th>
|
||||
<th>安静1h</th>
|
||||
<th>异动</th>
|
||||
<th>更新于</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{history.length === 0 ? (
|
||||
<tr><td colSpan="11" className="text-dim" style={{textAlign:'center',padding:20}}>
|
||||
暂无趋势事件记录
|
||||
</td></tr>
|
||||
) : history.slice(0, 100).map((ev, i) => (
|
||||
<tr key={(ev.timestamp || ev.id || i) + '-' + i}>
|
||||
<td className="text-dim">{ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString('zh-CN', { hour12: false }) : (ev.created_at ? new Date(ev.created_at).toLocaleTimeString('zh-CN', { hour12: false }) : '-')}</td>
|
||||
<td><strong>{ev.coin}</strong></td>
|
||||
<td className={stateClass(ev.new_state)}>{ev.prev_state} → {stateLabel(ev.new_state)}</td>
|
||||
<td className={dirClass(ev.direction)} style={{textAlign:'center',fontSize:16}}>{dirIcon(ev.direction)}</td>
|
||||
<td className="text-right">{(ev.z_score || 0).toFixed(1)}σ</td>
|
||||
<td className="text-right">{(ev.volatility || 0).toFixed(4)}%</td>
|
||||
<td className="text-right">{ev.ex_agree || 0}/{ev.ex_total || 0}</td>
|
||||
<td className={'text-right ' + changeClass(ev.bg_change)}>{ev.bg_change != null ? ev.bg_change.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(ev.hl_change)}>{ev.hl_change != null ? ev.hl_change.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(ev.bn_change)}>{ev.bn_change != null ? ev.bn_change.toFixed(3) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + changeClass(ev.okx_change)}>{ev.okx_change != null ? ev.okx_change.toFixed(3) + '%' : '-'}</td>
|
||||
{filterData.length === 0 ? (
|
||||
<tr><td colSpan="15" className="text-dim" style={{textAlign:'center',padding:20}}>等待K线数据...</td></tr>
|
||||
) : filterData.map(entry => (
|
||||
<tr key={entry.coin} className={entry.passes_filter ? 'filter-pass' : ''}>
|
||||
<td><strong>{entry.coin}</strong></td>
|
||||
<td className={'text-right ' + scoreClass(entry.signal_score)} style={{fontWeight:700}}>{entry.signal_score != null ? entry.signal_score.toFixed(0) : '-'}</td>
|
||||
<td className={'text-right ' + (entry.quiet_24h ? 'text-green' : '')}>{entry.range_24h != null ? entry.range_24h.toFixed(2) + '%' : '-'}</td>
|
||||
<td className="text-right text-dim">{entry.vol_baseline != null ? entry.vol_baseline.toFixed(2) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + (entry.quiet_1h ? 'text-green' : '')}>{entry.range_1h != null ? entry.range_1h.toFixed(2) + '%' : '-'}</td>
|
||||
<td className={'text-right ' + volClass(entry.volume_ratio)}>{entry.volume_ratio != null ? entry.volume_ratio.toFixed(2) + 'x' : '-'}</td>
|
||||
<td className={'text-right ' + (entry.change_1h > 0 ? 'text-green' : entry.change_1h < 0 ? 'text-red' : '')}>{entry.change_1h != null ? (entry.change_1h > 0 ? '+' : '') + entry.change_1h.toFixed(2) + '%' : '-'}</td>
|
||||
<td className="text-right">{entry.ema_52 ? entry.ema_52.toFixed(4) : '-'}</td>
|
||||
<td className={'text-right ' + slopeClass(entry.ema_slope)}>{entry.ema_slope != null ? (entry.ema_slope > 0 ? '+' : '') + entry.ema_slope.toFixed(3) + '%' : '-'}</td>
|
||||
<td className="text-right">{entry.current_price ? entry.current_price.toFixed(4) : '-'}</td>
|
||||
<td className={entry.price_above_ema ? 'text-green' : 'text-red'}>{entry.price_above_ema != null ? (entry.price_above_ema ? '↑' : '↓') : '-'}</td>
|
||||
<td className={entry.quiet_24h ? 'text-green' : 'text-dim'}>{entry.quiet_24h != null ? (entry.quiet_24h ? '✓' : '✗') : '-'}</td>
|
||||
<td className={entry.quiet_1h ? 'text-green' : 'text-dim'}>{entry.quiet_1h != null ? (entry.quiet_1h ? '✓' : '✗') : '-'}</td>
|
||||
<td className={entry.fresh_anomaly ? 'text-orange' : 'text-dim'}>{entry.fresh_anomaly != null ? (entry.fresh_anomaly ? '⚠' : '-') : '-'}</td>
|
||||
<td className="text-dim">{entry.last_updated ? new Date(entry.last_updated).toLocaleTimeString('zh-CN', {hour12:false}) : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -1281,3 +1228,107 @@ function TrendHistoryCard({ history }) {
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ Full Signal Records Card (FreshAnomaly + Score >= 70) ============
|
||||
function FullSignalCard({ signals }) {
|
||||
const filtered = signals.filter(s => s.category === 'full')
|
||||
const enterCount = filtered.filter(s => s.type === 'enter').length
|
||||
|
||||
return (
|
||||
<section className="card card-wide" id="trend-signal-card">
|
||||
<h2>完整信号 (异动+分数≥70) {enterCount > 0 && <span className="text-green" style={{fontSize:12,fontWeight:400,marginLeft:8}}>共{enterCount}条</span>}</h2>
|
||||
<div className="table-wrap" style={{ maxHeight: 350 }}>
|
||||
<table id="trend-signal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>币种</th>
|
||||
<th>类型</th>
|
||||
<th>分数</th>
|
||||
<th>价格</th>
|
||||
<th>成交量比</th>
|
||||
<th>EMA斜率</th>
|
||||
<th>趋势状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.length === 0 ? (
|
||||
<tr><td colSpan="8" className="text-dim" style={{textAlign:'center',padding:20}}>
|
||||
等待完整信号... (FreshAnomaly + 分数≥70)
|
||||
</td></tr>
|
||||
) : filtered.slice(0, 50).map((s, i) => {
|
||||
const rowClass = s.type === 'enter' ? 'signal-enter' : 'signal-exit'
|
||||
const ts = s.timestamp ? new Date(s.timestamp).toLocaleTimeString('zh-CN', { hour12: false }) : '-'
|
||||
return (
|
||||
<tr key={'full-' + s.timestamp + '-' + s.coin + '-' + i} className={rowClass}>
|
||||
<td className="text-dim">{ts}</td>
|
||||
<td><strong>{s.coin}</strong></td>
|
||||
<td className={s.type === 'enter' ? 'text-green' : 'text-dim'} style={{fontWeight:600}}>
|
||||
{s.type === 'enter' ? '开' : '关'}
|
||||
</td>
|
||||
<td className="text-right" style={{fontWeight:700}}>{s.signal_score != null ? s.signal_score.toFixed(0) : '-'}</td>
|
||||
<td className="text-right">{s.price ? s.price.toFixed(4) : '-'}</td>
|
||||
<td className="text-right">{s.volume_ratio != null ? s.volume_ratio.toFixed(2) + 'x' : '-'}</td>
|
||||
<td className="text-right">{s.ema_slope != null ? (s.ema_slope > 0 ? '+' : '') + s.ema_slope.toFixed(3) + '%' : '-'}</td>
|
||||
<td className="text-dim">{s.state || '-'}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ High Score Signal Records Card (Score >= 90) ============
|
||||
function HighScoreCard({ signals }) {
|
||||
const filtered = signals.filter(s => s.category === 'high')
|
||||
const enterCount = filtered.filter(s => s.type === 'enter').length
|
||||
|
||||
return (
|
||||
<section className="card card-wide" id="high-score-card">
|
||||
<h2>高分信号 (分数≥90) {enterCount > 0 && <span className="text-green" style={{fontSize:12,fontWeight:400,marginLeft:8}}>共{enterCount}条</span>}</h2>
|
||||
<div className="table-wrap" style={{ maxHeight: 350 }}>
|
||||
<table id="high-score-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>币种</th>
|
||||
<th>类型</th>
|
||||
<th>分数</th>
|
||||
<th>价格</th>
|
||||
<th>成交量比</th>
|
||||
<th>EMA斜率</th>
|
||||
<th>趋势状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.length === 0 ? (
|
||||
<tr><td colSpan="8" className="text-dim" style={{textAlign:'center',padding:20}}>
|
||||
等待高分信号... (分数≥90)
|
||||
</td></tr>
|
||||
) : filtered.slice(0, 50).map((s, i) => {
|
||||
const rowClass = s.type === 'enter' ? 'signal-enter' : 'signal-exit'
|
||||
const ts = s.timestamp ? new Date(s.timestamp).toLocaleTimeString('zh-CN', { hour12: false }) : '-'
|
||||
return (
|
||||
<tr key={'high-' + s.timestamp + '-' + s.coin + '-' + i} className={rowClass}>
|
||||
<td className="text-dim">{ts}</td>
|
||||
<td><strong>{s.coin}</strong></td>
|
||||
<td className={s.type === 'enter' ? 'text-green' : 'text-dim'} style={{fontWeight:600}}>
|
||||
{s.type === 'enter' ? '开' : '关'}
|
||||
</td>
|
||||
<td className="text-right" style={{fontWeight:700}}>{s.signal_score != null ? s.signal_score.toFixed(0) : '-'}</td>
|
||||
<td className="text-right">{s.price ? s.price.toFixed(4) : '-'}</td>
|
||||
<td className="text-right">{s.volume_ratio != null ? s.volume_ratio.toFixed(2) + 'x' : '-'}</td>
|
||||
<td className="text-right">{s.ema_slope != null ? (s.ema_slope > 0 ? '+' : '') + s.ema_slope.toFixed(3) + '%' : '-'}</td>
|
||||
<td className="text-dim">{s.state || '-'}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user