misc: binance momentum, telegram, frontend updates
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Binance Momentum Monitor</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/binance-main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{r as c,j as t,c as B,R as E}from"./client-DYDkQYN6.js";const w="https://www.binance.com/en/futures/";function M(){const[r,S]=c.useState([]),[h,g]=c.useState([]),[C,x]=c.useState("● 连接中..."),[v,u]=c.useState(!1),[i,y]=c.useState("change_abs"),[l,j]=c.useState("desc"),d=c.useRef(null),p=c.useCallback(()=>{const e=new EventSource("/events");d.current=e,e.addEventListener("connected",()=>{x("● 已连接"),u(!0)}),e.onmessage=a=>{try{const n=JSON.parse(a.data);switch(n.event){case"binance_momentum":S(n.data);break;case"binance_alert":g(s=>[n.data,...s].slice(0,200));break}}catch{}},e.onerror=()=>{x("● 已断开 (重连中...)"),u(!1),e.close(),setTimeout(p,3e3)}},[]);c.useEffect(()=>(p(),()=>{d.current&&d.current.close()}),[p]),c.useEffect(()=>{const a=setInterval(async()=>{try{const s=await(await fetch("/api/binance-alerts?limit=50")).json();s.alerts&&s.alerts.length>0&&g(_=>{const $=new Set(_.map(o=>o.timestamp)),N=[..._];for(const o of s.alerts)$.has(o.timestamp)||N.push(o);return N.slice(0,200)})}catch{}},1e4);return()=>clearInterval(a)},[]);const m=e=>{i===e?j(a=>a==="asc"?"desc":"asc"):(y(e),j("desc"))},f=[...r].sort((e,a)=>{let n,s;switch(i){case"coin":return n=e.coin,s=a.coin,l==="asc"?n.localeCompare(s):s.localeCompare(n);case"price":n=e.price||0,s=a.price||0;break;case"change_abs":n=Math.abs(e.change_pct)||0,s=Math.abs(a.change_pct)||0;break;case"change":n=e.change_pct||0,s=a.change_pct||0;break;default:return 0}return l==="asc"?n-s:s-n}),k=r.filter(e=>e.buffer_full).length,b=r.filter(e=>Math.abs(e.change_pct)>=10).length;return t.jsxs("div",{id:"bm-app",children:[t.jsxs("header",{children:[t.jsxs("div",{children:[t.jsx("h1",{children:"BN 1-Min Momentum Monitor"}),t.jsx("span",{className:"header-subtitle",children:"Binance 1分钟涨跌监控 | 阈值 ≥10%"})]}),t.jsxs("div",{className:"header-right",children:[t.jsxs("span",{className:"stat-badge",children:[r.length," coins"]}),t.jsxs("span",{className:"stat-badge",children:[k," ready"]}),b>0&&t.jsxs("span",{className:"stat-badge stat-alert",children:[b," alerting"]}),t.jsx("span",{className:v?"status-online":"status-offline",children:C})]})]}),h.length>0&&t.jsxs("section",{className:"card card-wide alert-section",children:[t.jsxs("h2",{children:["Alert History (",h.length,")"]}),t.jsx("div",{className:"alert-scroll",children:h.slice(0,50).map((e,a)=>{var n,s;return t.jsxs("div",{className:`alert-item alert-${e.direction}`,children:[t.jsx("span",{className:"alert-icon",children:e.direction==="up"?"🟢":"🔴"}),t.jsx("a",{href:`${w}${e.coin}USDT`,target:"_blank",rel:"noreferrer",className:"alert-coin",children:e.coin}),t.jsxs("span",{className:e.direction==="up"?"text-green":"text-red",children:[e.change_pct>=0?"+":"",(n=e.change_pct)==null?void 0:n.toFixed(2),"%"]}),t.jsxs("span",{className:"alert-price",children:["$",(s=e.price)==null?void 0:s.toFixed(4)]}),t.jsx("span",{className:"alert-time text-dim",children:new Date(e.timestamp).toLocaleTimeString()})]},a)})})]}),t.jsxs("section",{className:"card card-wide",children:[t.jsxs("h2",{children:["All Coins (",r.length,")"]}),t.jsx("div",{className:"table-wrap",style:{maxHeight:"calc(100vh - 200px)"},children:t.jsxs("table",{children:[t.jsx("thead",{children:t.jsxs("tr",{children:[t.jsxs("th",{onClick:()=>m("coin"),style:{cursor:"pointer"},children:["Coin ",i==="coin"?l==="asc"?"▲":"▼":""]}),t.jsxs("th",{onClick:()=>m("price"),style:{cursor:"pointer"},children:["Price ",i==="price"?l==="asc"?"▲":"▼":""]}),t.jsxs("th",{onClick:()=>m("change_abs"),style:{cursor:"pointer"},children:["1m Change ",i==="change_abs"?l==="asc"?"▲":"▼":""]}),t.jsx("th",{children:"Bar"})]})}),t.jsxs("tbody",{children:[f.map(e=>{const a=e.buffer_full?e.change_pct>=10?"row-surge-up":e.change_pct<=-10?"row-surge-down":e.change_pct>0?"row-up":e.change_pct<0?"row-down":"":"row-warmup",n=Math.min(Math.abs(e.change_pct)/15*100,100),s=e.change_pct>=10?"bar-green":e.change_pct<=-10?"bar-red":e.change_pct>0?"bar-green-dim":"bar-red-dim";return t.jsxs("tr",{className:a,children:[t.jsxs("td",{children:[t.jsx("a",{href:`${w}${e.coin}USDT`,target:"_blank",rel:"noreferrer",className:"coin-link",children:e.coin}),!e.buffer_full&&t.jsx("span",{className:"warmup-badge",children:"···"})]}),t.jsx("td",{className:"text-right mono",children:e.price?`$${e.price.toFixed(4)}`:"-"}),t.jsx("td",{className:`text-right mono ${e.change_pct>=10?"text-green":e.change_pct<=-10?"text-red":e.change_pct>0?"text-green":e.change_pct<0?"text-red":"text-dim"}`,children:e.buffer_full?`${e.change_pct>=0?"+":""}${e.change_pct.toFixed(2)}%`:"warming..."}),t.jsx("td",{children:t.jsx("div",{className:"bar-track",children:t.jsx("div",{className:`bar-fill ${s}`,style:{width:`${n}%`}})})})]},e.coin)}),f.length===0&&t.jsx("tr",{children:t.jsx("td",{colSpan:4,className:"loading",children:"Waiting for data..."})})]})]})})]})]})}B.createRoot(document.getElementById("root")).render(t.jsx(E.StrictMode,{children:t.jsx(M,{})}));
|
||||
+1
@@ -0,0 +1 @@
|
||||
:root{--bg: #0d1117;--card: #161b22;--border: #30363d;--text: #c9d1d9;--text-dim: #8b949e;--accent: #58a6ff;--green: #3fb950;--red: #f85149;--yellow: #d29922}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);font-size:14px;line-height:1.5;min-height:100vh}#bm-app{max-width:1200px;margin:0 auto;padding:16px}header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--card);border:1px solid var(--border);border-radius:8px;margin-bottom:12px;flex-wrap:wrap;gap:8px}header h1{font-size:18px;font-weight:600}.header-subtitle{font-size:12px;color:var(--text-dim);margin-left:12px}.header-right{display:flex;align-items:center;gap:10px;font-size:13px}.stat-badge{background:#58a6ff1a;color:var(--accent);padding:2px 8px;border-radius:10px;font-size:12px;font-weight:500}.stat-alert{background:#f8514926;color:var(--red);animation:pulse 2s infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.status-online{color:var(--green)}.status-offline{color:var(--red)}.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:12px}.card h2{font-size:14px;font-weight:600;color:var(--text-dim);margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}.alert-scroll{display:flex;flex-wrap:wrap;gap:6px;max-height:120px;overflow-y:auto}.alert-item{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:4px;font-size:13px;font-variant-numeric:tabular-nums}.alert-up{background:#3fb95014}.alert-down{background:#f8514914}.alert-icon{font-size:14px}.alert-coin{color:var(--accent);text-decoration:none;font-weight:600;min-width:50px}.alert-coin:hover{text-decoration:underline}.alert-price{color:var(--text-dim);min-width:90px}.alert-time{font-size:11px}.table-wrap{overflow-x:auto;overflow-y:auto}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;padding:6px 8px;color:var(--text-dim);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.5px;position:sticky;top:0;background:var(--card);border-bottom:1px solid var(--border);-webkit-user-select:none;user-select:none}th:hover{color:var(--accent)}td{padding:4px 8px;border-bottom:1px solid rgba(48,54,61,.5);white-space:nowrap}tr:hover td{background:#58a6ff0d}.coin-link{color:var(--accent);text-decoration:none;font-weight:600}.coin-link:hover{text-decoration:underline}.warmup-badge{font-size:10px;color:var(--text-dim);margin-left:6px}.row-warmup td{opacity:.45}.row-surge-up td{background:#3fb9501a!important}.row-surge-up:hover td{background:#3fb9502e!important}.row-surge-down td{background:#f851491a!important}.row-surge-down:hover td{background:#f851492e!important}.row-up td{background:#3fb95008}.row-down td{background:#f8514908}.bar-track{width:100px;height:6px;background:#30363d80;border-radius:3px;overflow:hidden}.bar-fill{height:100%;border-radius:3px;transition:width .3s ease}.bar-green{background:var(--green)}.bar-red{background:var(--red)}.bar-green-dim{background:#3fb95080}.bar-red-dim{background:#f8514980}.text-green{color:var(--green)}.text-red{color:var(--red)}.text-dim{color:var(--text-dim)}.text-right{text-align:right}.mono{font-variant-numeric:tabular-nums;font-family:SF Mono,Cascadia Code,monospace}.loading{text-align:center;color:var(--text-dim);padding:20px!important}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#484f58}@media (max-width: 768px){header{flex-direction:column}#bm-app{padding:8px}}
|
||||
+40
File diff suppressed because one or more lines are too long
-40
File diff suppressed because one or more lines are too long
-40
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Binance Momentum Monitor</title>
|
||||
<script type="module" crossorigin src="/static/assets/binance-C6HzvyUG.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/static/assets/client-DYDkQYN6.js">
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/binance-DE6-y9x2.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+3
-2
@@ -4,8 +4,9 @@
|
||||
<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-Czt_9K6K.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-vvNDQq2K.css">
|
||||
<script type="module" crossorigin src="/static/assets/main-Cg5_dPUr.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/static/assets/client-DYDkQYN6.js">
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/main-vvNDQq2K.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--card: #161b22;
|
||||
--border: #30363d;
|
||||
--text: #c9d1d9;
|
||||
--text-dim: #8b949e;
|
||||
--accent: #58a6ff;
|
||||
--green: #3fb950;
|
||||
--red: #f85149;
|
||||
--yellow: #d29922;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#bm-app { max-width: 1200px; margin: 0 auto; padding: 16px; }
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
header h1 { font-size: 18px; font-weight: 600; }
|
||||
.header-subtitle { font-size: 12px; color: var(--text-dim); margin-left: 12px; }
|
||||
.header-right { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||
|
||||
.stat-badge {
|
||||
background: rgba(88, 166, 255, 0.1);
|
||||
color: var(--accent);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.stat-alert {
|
||||
background: rgba(248, 81, 73, 0.15);
|
||||
color: var(--red);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.status-online { color: var(--green); }
|
||||
.status-offline { color: var(--red); }
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-wide { }
|
||||
.card h2 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Alert Section */
|
||||
.alert-scroll {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.alert-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.alert-up { background: rgba(63, 185, 80, 0.08); }
|
||||
.alert-down { background: rgba(248, 81, 73, 0.08); }
|
||||
.alert-icon { font-size: 14px; }
|
||||
.alert-coin {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
min-width: 50px;
|
||||
}
|
||||
.alert-coin:hover { text-decoration: underline; }
|
||||
.alert-price { color: var(--text-dim); min-width: 90px; }
|
||||
.alert-time { font-size: 11px; }
|
||||
|
||||
/* Table */
|
||||
.table-wrap { overflow-x: auto; overflow-y: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 6px 8px;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
user-select: none;
|
||||
}
|
||||
th:hover { color: var(--accent); }
|
||||
td {
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
tr:hover td { background: rgba(88, 166, 255, 0.05); }
|
||||
|
||||
.coin-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.coin-link:hover { text-decoration: underline; }
|
||||
.warmup-badge {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* Row highlights */
|
||||
.row-warmup td { opacity: 0.45; }
|
||||
.row-surge-up td { background: rgba(63, 185, 80, 0.1) !important; }
|
||||
.row-surge-up:hover td { background: rgba(63, 185, 80, 0.18) !important; }
|
||||
.row-surge-down td { background: rgba(248, 81, 73, 0.1) !important; }
|
||||
.row-surge-down:hover td { background: rgba(248, 81, 73, 0.18) !important; }
|
||||
.row-up td { background: rgba(63, 185, 80, 0.03); }
|
||||
.row-down td { background: rgba(248, 81, 73, 0.03); }
|
||||
|
||||
/* Bar */
|
||||
.bar-track {
|
||||
width: 100px;
|
||||
height: 6px;
|
||||
background: rgba(48, 54, 61, 0.5);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.bar-green { background: var(--green); }
|
||||
.bar-red { background: var(--red); }
|
||||
.bar-green-dim { background: rgba(63, 185, 80, 0.5); }
|
||||
.bar-red-dim { background: rgba(248, 81, 73, 0.5); }
|
||||
|
||||
/* Utility */
|
||||
.text-green { color: var(--green); }
|
||||
.text-red { color: var(--red); }
|
||||
.text-dim { color: var(--text-dim); }
|
||||
.text-right { text-align: right; }
|
||||
.mono { font-variant-numeric: tabular-nums; font-family: 'SF Mono', 'Cascadia Code', monospace; }
|
||||
.loading { text-align: center; color: var(--text-dim); padding: 20px !important; }
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #484f58; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
header { flex-direction: column; }
|
||||
#bm-app { padding: 8px; }
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
const BINANCE_BASE = 'https://www.binance.com/en/futures/'
|
||||
|
||||
function BinanceApp() {
|
||||
const [coins, setCoins] = useState([])
|
||||
const [alerts, setAlerts] = useState([])
|
||||
const [connStatus, setConnStatus] = useState('● 连接中...')
|
||||
const [connOnline, setConnOnline] = useState(false)
|
||||
const [sortBy, setSortBy] = useState('change_abs')
|
||||
const [sortDir, setSortDir] = useState('desc')
|
||||
const esRef = useRef(null)
|
||||
|
||||
// Connect to SSE
|
||||
const connect = useCallback(() => {
|
||||
const es = new EventSource('/events')
|
||||
esRef.current = es
|
||||
|
||||
es.addEventListener('connected', () => {
|
||||
setConnStatus('● 已连接')
|
||||
setConnOnline(true)
|
||||
})
|
||||
|
||||
// Backend sends events as unnamed SSE messages with JSON {event, data, ts}.
|
||||
// Use onmessage and dispatch by msg.event field.
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const msg = JSON.parse(e.data)
|
||||
switch (msg.event) {
|
||||
case 'binance_momentum':
|
||||
setCoins(msg.data)
|
||||
break
|
||||
case 'binance_alert':
|
||||
setAlerts(prev => [msg.data, ...prev].slice(0, 200))
|
||||
break
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
es.onerror = () => {
|
||||
setConnStatus('● 已断开 (重连中...)')
|
||||
setConnOnline(false)
|
||||
es.close()
|
||||
setTimeout(connect, 3000)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
connect()
|
||||
return () => {
|
||||
if (esRef.current) esRef.current.close()
|
||||
}
|
||||
}, [connect])
|
||||
|
||||
// Poll alerts as fallback
|
||||
useEffect(() => {
|
||||
const fetchAlerts = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/binance-alerts?limit=50')
|
||||
const data = await res.json()
|
||||
if (data.alerts && data.alerts.length > 0) {
|
||||
setAlerts(prev => {
|
||||
const existing = new Set(prev.map(a => a.timestamp))
|
||||
const merged = [...prev]
|
||||
for (const a of data.alerts) {
|
||||
if (!existing.has(a.timestamp)) {
|
||||
merged.push(a)
|
||||
}
|
||||
}
|
||||
return merged.slice(0, 200)
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
const timer = setInterval(fetchAlerts, 10000)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
// Sort coins
|
||||
const handleSort = (col) => {
|
||||
if (sortBy === col) {
|
||||
setSortDir(d => d === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortBy(col)
|
||||
setSortDir('desc')
|
||||
}
|
||||
}
|
||||
|
||||
const sortedCoins = [...coins].sort((a, b) => {
|
||||
let va, vb
|
||||
switch (sortBy) {
|
||||
case 'coin':
|
||||
va = a.coin; vb = b.coin
|
||||
return sortDir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va)
|
||||
case 'price':
|
||||
va = a.price || 0; vb = b.price || 0
|
||||
break
|
||||
case 'change_abs':
|
||||
va = Math.abs(a.change_pct) || 0; vb = Math.abs(b.change_pct) || 0
|
||||
break
|
||||
case 'change':
|
||||
va = a.change_pct || 0; vb = b.change_pct || 0
|
||||
break
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
return sortDir === 'asc' ? va - vb : vb - va
|
||||
})
|
||||
|
||||
// Stats
|
||||
const fullCount = coins.filter(c => c.buffer_full).length
|
||||
const alertingCount = coins.filter(c => Math.abs(c.change_pct) >= 10).length
|
||||
|
||||
return (
|
||||
<div id="bm-app">
|
||||
{/* Header */}
|
||||
<header>
|
||||
<div>
|
||||
<h1>BN 1-Min Momentum Monitor</h1>
|
||||
<span className="header-subtitle">Binance 1分钟涨跌监控 | 阈值 ≥10%</span>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<span className="stat-badge">{coins.length} coins</span>
|
||||
<span className="stat-badge">{fullCount} ready</span>
|
||||
{alertingCount > 0 && <span className="stat-badge stat-alert">{alertingCount} alerting</span>}
|
||||
<span className={connOnline ? 'status-online' : 'status-offline'}>{connStatus}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Alert History */}
|
||||
{alerts.length > 0 && (
|
||||
<section className="card card-wide alert-section">
|
||||
<h2>Alert History ({alerts.length})</h2>
|
||||
<div className="alert-scroll">
|
||||
{alerts.slice(0, 50).map((a, i) => (
|
||||
<div key={i} className={`alert-item alert-${a.direction}`}>
|
||||
<span className="alert-icon">{a.direction === 'up' ? '🟢' : '🔴'}</span>
|
||||
<a href={`${BINANCE_BASE}${a.coin}USDT`} target="_blank" rel="noreferrer" className="alert-coin">{a.coin}</a>
|
||||
<span className={a.direction === 'up' ? 'text-green' : 'text-red'}>
|
||||
{a.change_pct >= 0 ? '+' : ''}{a.change_pct?.toFixed(2)}%
|
||||
</span>
|
||||
<span className="alert-price">${a.price?.toFixed(4)}</span>
|
||||
<span className="alert-time text-dim">{new Date(a.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Coins Table */}
|
||||
<section className="card card-wide">
|
||||
<h2>All Coins ({coins.length})</h2>
|
||||
<div className="table-wrap" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th onClick={() => handleSort('coin')} style={{ cursor: 'pointer' }}>
|
||||
Coin {sortBy === 'coin' ? (sortDir === 'asc' ? '▲' : '▼') : ''}
|
||||
</th>
|
||||
<th onClick={() => handleSort('price')} style={{ cursor: 'pointer' }}>
|
||||
Price {sortBy === 'price' ? (sortDir === 'asc' ? '▲' : '▼') : ''}
|
||||
</th>
|
||||
<th onClick={() => handleSort('change_abs')} style={{ cursor: 'pointer' }}>
|
||||
1m Change {sortBy === 'change_abs' ? (sortDir === 'asc' ? '▲' : '▼') : ''}
|
||||
</th>
|
||||
<th>Bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedCoins.map(c => {
|
||||
const cls = !c.buffer_full ? 'row-warmup'
|
||||
: c.change_pct >= 10 ? 'row-surge-up'
|
||||
: c.change_pct <= -10 ? 'row-surge-down'
|
||||
: c.change_pct > 0 ? 'row-up'
|
||||
: c.change_pct < 0 ? 'row-down'
|
||||
: ''
|
||||
const barPct = Math.min(Math.abs(c.change_pct) / 15 * 100, 100)
|
||||
const barCls = c.change_pct >= 10 ? 'bar-green'
|
||||
: c.change_pct <= -10 ? 'bar-red'
|
||||
: c.change_pct > 0 ? 'bar-green-dim'
|
||||
: 'bar-red-dim'
|
||||
return (
|
||||
<tr key={c.coin} className={cls}>
|
||||
<td>
|
||||
<a href={`${BINANCE_BASE}${c.coin}USDT`} target="_blank" rel="noreferrer" className="coin-link">
|
||||
{c.coin}
|
||||
</a>
|
||||
{!c.buffer_full && <span className="warmup-badge">···</span>}
|
||||
</td>
|
||||
<td className="text-right mono">
|
||||
{c.price ? `$${c.price.toFixed(4)}` : '-'}
|
||||
</td>
|
||||
<td className={`text-right mono ${c.change_pct >= 10 ? 'text-green' : c.change_pct <= -10 ? 'text-red' : c.change_pct > 0 ? 'text-green' : c.change_pct < 0 ? 'text-red' : 'text-dim'}`}>
|
||||
{c.buffer_full ? `${c.change_pct >= 0 ? '+' : ''}${c.change_pct.toFixed(2)}%` : 'warming...'}
|
||||
</td>
|
||||
<td>
|
||||
<div className="bar-track">
|
||||
<div className={`bar-fill ${barCls}`} style={{ width: `${barPct}%` }} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{sortedCoins.length === 0 && (
|
||||
<tr><td colSpan={4} className="loading">Waiting for data...</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BinanceApp
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import BinanceApp from './BinanceApp'
|
||||
import './BinanceApp.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BinanceApp />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
@@ -13,5 +14,11 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
binance: resolve(__dirname, 'binance.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user