feat: add light/dark theme to exchange instances with hub SSO sync
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-title" content="监控">
|
||||
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
|
||||
@@ -229,6 +231,8 @@
|
||||
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
|
||||
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body data-page="{{ page }}">
|
||||
{% macro period_stats(title, s) %}
|
||||
@@ -253,7 +257,21 @@
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="header-row">
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-nav">
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
|
||||
@@ -1,261 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ exchange_display }} | 关键位放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = data.current_price_display || fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = key.upper_display || fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = key.lower_display || fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>{{ exchange_display }} | 关键位放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = data.current_price_display || fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = key.upper_display || fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = key.lower_display || fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,118 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,214 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ exchange_display }} | 实盘下单放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
|
||||
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
|
||||
document.getElementById("m-breakeven").innerText =
|
||||
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
|
||||
document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>{{ exchange_display }} | 实盘下单放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
|
||||
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
|
||||
document.getElementById("m-breakeven").innerText =
|
||||
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
|
||||
document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-title" content="监控">
|
||||
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
|
||||
@@ -229,6 +231,8 @@
|
||||
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
|
||||
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body data-page="{{ page }}">
|
||||
{% macro period_stats(title, s) %}
|
||||
@@ -253,7 +257,21 @@
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="header-row">
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-nav">
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
|
||||
@@ -1,261 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ exchange_display }} | 关键位放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>{{ exchange_display }} | 关键位放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,118 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,214 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ exchange_display }} | 实盘下单放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
|
||||
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
|
||||
document.getElementById("m-breakeven").innerText =
|
||||
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
|
||||
document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>{{ exchange_display }} | 实盘下单放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
|
||||
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
|
||||
document.getElementById("m-breakeven").innerText =
|
||||
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
|
||||
document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-title" content="监控">
|
||||
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
|
||||
@@ -209,6 +211,8 @@
|
||||
.stats-split-row{grid-template-columns:1fr}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{% macro period_metrics_cells(s) %}
|
||||
@@ -243,7 +247,21 @@
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>加密货币|Gate 机器人交易监控</h1>
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="header-row">
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-nav">
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
|
||||
|
||||
@@ -1,261 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ exchange_display }} | 关键位放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>{{ exchange_display }} | 关键位放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,118 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,214 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ exchange_display }} | 实盘下单放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
|
||||
document.getElementById("m-breakeven").innerText =
|
||||
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
|
||||
document.getElementById("m-price").innerText = fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>{{ exchange_display }} | 实盘下单放大</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
|
||||
document.getElementById("m-breakeven").innerText =
|
||||
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
|
||||
document.getElementById("m-price").innerText = fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-title" content="监控">
|
||||
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
|
||||
@@ -229,6 +231,8 @@
|
||||
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
|
||||
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body data-page="{{ page }}">
|
||||
{% macro period_stats(title, s) %}
|
||||
@@ -253,7 +257,21 @@
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="header-row">
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-nav">
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
|
||||
@@ -1,260 +1,277 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>关键位放大 | K线查看</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>关键位放大 | K线查看</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
|
||||
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
|
||||
rightPriceScale:{borderColor:"#2a3150"},
|
||||
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
|
||||
crosshair:{mode:0}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") return;
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p<=0) return;
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
|
||||
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,107 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>系统登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>系统登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,211 +1,228 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>实盘下单放大 | 100根K线</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
const rr = order.rr_ratio;
|
||||
document.getElementById("m-rr").innerText = (rr === null || typeof rr === "undefined") ? "-:1" : `${Number(rr).toFixed(2)}:1`;
|
||||
document.getElementById("m-price").innerText = fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=1"></script>
|
||||
|
||||
<title>实盘下单放大 | 100根K线</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
|
||||
.container{width:min(98vw,1900px);margin:0 auto}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
|
||||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
|
||||
.btn:hover{background:#1f2740}
|
||||
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
|
||||
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
|
||||
.meta-item .k{font-size:.76rem;color:#9fb0d8}
|
||||
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
|
||||
.status{font-size:.84rem;color:#95a2c2}
|
||||
.status.err{color:#ff8080}
|
||||
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
.empty{padding:18px;color:#95a2c2}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a class="btn" href="/">返回首页</a>
|
||||
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
{% if orders %}
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>订单</label>
|
||||
<select id="order-id">
|
||||
{% for o in orders %}
|
||||
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
|
||||
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>周期</label>
|
||||
<select id="timeframe">
|
||||
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
|
||||
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<div class="card">
|
||||
<div class="meta">
|
||||
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
|
||||
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
|
||||
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
|
||||
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
|
||||
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
|
||||
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
|
||||
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
|
||||
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="chart-wrap"><div id="chart"></div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if orders %}
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const orderSelect = document.getElementById("order-id");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const statusEl = document.getElementById("load-status");
|
||||
const updatedAtEl = document.getElementById("updated-at");
|
||||
const chartHost = document.getElementById("chart");
|
||||
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
|
||||
function ensureChart(){
|
||||
if(chart){ return true; }
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
|
||||
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
|
||||
rightPriceScale: { borderColor: "#2a3150" },
|
||||
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
|
||||
crosshair: { mode: 0 }
|
||||
});
|
||||
candleSeries = chart.addCandlestickSeries({
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries){ return; }
|
||||
priceLines.forEach(line => {
|
||||
try { candleSeries.removePriceLine(line); } catch (_) {}
|
||||
});
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
|
||||
const p = Number(price);
|
||||
if(Number.isNaN(p) || p <= 0){ return; }
|
||||
priceLines.push(candleSeries.createPriceLine({
|
||||
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
|
||||
}));
|
||||
}
|
||||
|
||||
function paintOrder(order){
|
||||
document.getElementById("m-symbol").innerText = order.symbol || "-";
|
||||
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
|
||||
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
|
||||
const rr = order.rr_ratio;
|
||||
document.getElementById("m-rr").innerText = (rr === null || typeof rr === "undefined") ? "-:1" : `${Number(rr).toFixed(2)}:1`;
|
||||
document.getElementById("m-price").innerText = fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`;
|
||||
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
|
||||
}
|
||||
|
||||
async function loadOrderKline(){
|
||||
if(!ensureChart()){ return; }
|
||||
const orderId = orderSelect.value;
|
||||
const timeframe = tfSelect.value;
|
||||
if(!orderId){ return; }
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
try{
|
||||
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
|
||||
const data = await resp.json();
|
||||
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
|
||||
const candles = Array.isArray(data.candles) ? data.candles : [];
|
||||
if(!candles.length){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "暂无K线数据";
|
||||
return;
|
||||
}
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.order.trigger_price, "成交价", "#42a5f5");
|
||||
addLine(data.order.stop_loss, "止损", "#ff6666");
|
||||
addLine(data.order.take_profit, "止盈", "#4cd97f");
|
||||
chart.timeScale().fitContent();
|
||||
paintOrder(data.order || {});
|
||||
updatedAtEl.innerText = data.updated_at || "--";
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = `已加载 ${candles.length} 根K线`;
|
||||
}catch(err){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = err && err.message ? err.message : "加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
|
||||
orderSelect.addEventListener("change", loadOrderKline);
|
||||
tfSelect.addEventListener("change", loadOrderKline);
|
||||
loadOrderKline();
|
||||
setInterval(loadOrderKline, refreshMs);
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+62
-4
@@ -28,6 +28,46 @@ from hub_sso import (
|
||||
)
|
||||
|
||||
|
||||
def _merge_query_into_path(path: str, **params: str) -> str:
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||
|
||||
split = urlsplit(path or "/")
|
||||
q = list(parse_qsl(split.query, keep_blank_values=True))
|
||||
keys = {k for k, _ in q}
|
||||
for k, v in params.items():
|
||||
if not v or k in keys:
|
||||
continue
|
||||
q.append((k, str(v)))
|
||||
return urlunsplit((split.scheme, split.netloc, split.path, urlencode(q), split.fragment))
|
||||
|
||||
|
||||
def install_instance_theme_static(app) -> None:
|
||||
"""仓库根 static/instance_theme.* 供四所页面共用。"""
|
||||
import os
|
||||
|
||||
from flask import Response, send_file
|
||||
|
||||
repo_static = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
||||
assets = {
|
||||
"instance_theme.js": "application/javascript; charset=utf-8",
|
||||
"instance_theme.css": "text/css; charset=utf-8",
|
||||
}
|
||||
|
||||
for name, mime in assets.items():
|
||||
path = os.path.join(repo_static, name)
|
||||
|
||||
def _view(p=path, m=mime):
|
||||
if not os.path.isfile(p):
|
||||
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
|
||||
return send_file(p, mimetype=m)
|
||||
|
||||
app.add_url_rule(
|
||||
f"/static/{name}",
|
||||
endpoint=f"repo_static_{name.replace('.', '_')}",
|
||||
view_func=_view,
|
||||
)
|
||||
|
||||
|
||||
def _hub_auth_required(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
@@ -149,6 +189,7 @@ def install_on_app(
|
||||
}
|
||||
install_hub_embed_headers(app)
|
||||
configure_hub_embed_session(app)
|
||||
install_instance_theme_static(app)
|
||||
register_hub_routes(app)
|
||||
|
||||
|
||||
@@ -421,11 +462,22 @@ def register_hub_routes(app):
|
||||
if _sso_wants_embed_auth() and request.is_secure:
|
||||
boot = mint_hub_embed_bootstrap(ex, next_path)
|
||||
if boot:
|
||||
q = urlencode({"t": boot, "next": next_path, "embed": "1"})
|
||||
return redirect(f"/hub-embed-auth?{q}")
|
||||
from urllib.parse import urlencode as _ue
|
||||
|
||||
qdict = {"t": boot, "next": next_path, "embed": "1"}
|
||||
ht0 = (request.args.get("hub_theme") or "").strip().lower()
|
||||
if ht0 in ("light", "dark"):
|
||||
qdict["hub_theme"] = ht0
|
||||
return redirect(f"/hub-embed-auth?{_ue(qdict)}")
|
||||
session["logged_in"] = True
|
||||
session.modified = True
|
||||
return redirect(next_path)
|
||||
dest = next_path
|
||||
if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
|
||||
dest = _merge_query_into_path(dest, embed="1")
|
||||
ht = (request.args.get("hub_theme") or "").strip().lower()
|
||||
if ht in ("light", "dark"):
|
||||
dest = _merge_query_into_path(dest, hub_theme=ht)
|
||||
return redirect(dest)
|
||||
hint = err or "校验失败"
|
||||
flash(
|
||||
f"中控 SSO 未生效({hint})。"
|
||||
@@ -449,7 +501,13 @@ def register_hub_routes(app):
|
||||
if ok:
|
||||
session["logged_in"] = True
|
||||
session.modified = True
|
||||
return redirect(next_path)
|
||||
dest = next_path
|
||||
if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
|
||||
dest = _merge_query_into_path(dest, embed="1")
|
||||
ht = (request.args.get("hub_theme") or "").strip().lower()
|
||||
if ht in ("light", "dark"):
|
||||
dest = _merge_query_into_path(dest, hub_theme=ht)
|
||||
return redirect(dest)
|
||||
hint = err or "校验失败"
|
||||
flash(f"iframe 登录未生效({hint})。可点本地导航工具栏「实例免密」重试。")
|
||||
return redirect("/login")
|
||||
|
||||
@@ -1057,7 +1057,11 @@ def _require_hub_logged_in(request: Request) -> None:
|
||||
|
||||
@app.get("/api/instance/open-url")
|
||||
def api_instance_open_url(
|
||||
request: Request, exchange_id: str, next: str = "/", embed: str = ""
|
||||
request: Request,
|
||||
exchange_id: str,
|
||||
next: str = "/",
|
||||
embed: str = "",
|
||||
hub_theme: str = "",
|
||||
):
|
||||
"""已登录中控时生成实例 SSO 打开链接(2h 有效、单次使用,复用 HUB_BRIDGE_TOKEN)。"""
|
||||
_require_hub_logged_in(request)
|
||||
@@ -1079,6 +1083,9 @@ def api_instance_open_url(
|
||||
params = {"token": token, "next": nxt}
|
||||
if (embed or "").strip().lower() in ("1", "true", "yes", "on"):
|
||||
params["embed"] = "1"
|
||||
ht = (hub_theme or "").strip().lower()
|
||||
if ht in ("light", "dark"):
|
||||
params["hub_theme"] = ht
|
||||
q = urlencode(params)
|
||||
return {
|
||||
"ok": True,
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
const next = nextPath || "/";
|
||||
const q = new URLSearchParams({ exchange_id: String(exchangeId), next });
|
||||
if (options.embed) q.set("embed", "1");
|
||||
if (globalThis.HubTheme && typeof HubTheme.get === "function") {
|
||||
q.set("hub_theme", HubTheme.get());
|
||||
}
|
||||
const r = await apiFetch("/api/instance/open-url?" + q.toString());
|
||||
const j = await r.json();
|
||||
if (!j.ok || !j.url) {
|
||||
@@ -135,6 +138,16 @@
|
||||
shell.classList.remove("hidden");
|
||||
shell.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("hub-instance-frame-open");
|
||||
frame.addEventListener("load", function syncInstanceFrameTheme() {
|
||||
try {
|
||||
if (globalThis.HubTheme && typeof HubTheme.get === "function" && frame.contentWindow) {
|
||||
frame.contentWindow.postMessage(
|
||||
{ type: "hub-theme-sync", theme: HubTheme.get() },
|
||||
"*"
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
function closeInstanceFrame() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="/assets/theme.js?v=20260604-hub-theme4"></script>
|
||||
<script src="/assets/theme.js?v=20260604-hub-inst-theme"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0b0e18" />
|
||||
<meta name="apple-mobile-web-app-title" content="中控" />
|
||||
@@ -248,7 +248,7 @@
|
||||
|
||||
<div id="toast"></div>
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/assets/chart.js?v=20260604-hub-chart-sse"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-chart-sse"></script>
|
||||
<script src="/assets/chart.js?v=20260604-hub-inst-theme"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-inst-theme"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastThemeToInstances() {
|
||||
const msg = { type: "hub-theme-sync", theme: get() };
|
||||
document.querySelectorAll("iframe#instance-frame, iframe.instance-frame").forEach((frame) => {
|
||||
try {
|
||||
if (frame.contentWindow) frame.contentWindow.postMessage(msg, "*");
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
function apply(theme) {
|
||||
const t = normalize(theme);
|
||||
const root = document.documentElement;
|
||||
@@ -26,6 +35,7 @@
|
||||
if (meta) meta.setAttribute("content", META[t]);
|
||||
root.style.colorScheme = t;
|
||||
document.dispatchEvent(new CustomEvent("hub-theme-change", { detail: { theme: t } }));
|
||||
broadcastThemeToInstances();
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""为四所 templates 注入 instance_theme 脚本/样式与切换按钮。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
EXCHANGES = ("crypto_monitor_binance", "crypto_monitor_okx", "crypto_monitor_gate", "crypto_monitor_gate_bot")
|
||||
FILES = ("index.html", "login.html", "key_focus_v2.html", "order_focus_v2.html")
|
||||
|
||||
SCRIPT_TAG = ' <script src="/static/instance_theme.js?v=1"></script>\n'
|
||||
CSS_LINK = ' <link rel="stylesheet" href="/static/instance_theme.css?v=1">\n'
|
||||
|
||||
THEME_TOGGLE = """ <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
|
||||
INDEX_HEADER_OLD = """ <div class="header">
|
||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
</div>"""
|
||||
|
||||
INDEX_HEADER_NEW = """ <div class="header">
|
||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||
<div class="header-row">
|
||||
<div class="exchange-tag">{{ exchange_display }}</div>
|
||||
""" + THEME_TOGGLE + """ </div>
|
||||
</div>"""
|
||||
|
||||
|
||||
def patch_file(path: Path) -> bool:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
orig = text
|
||||
if 'data-theme="dark"' not in text:
|
||||
text = text.replace('<html lang="zh-CN">', '<html lang="zh-CN" data-theme="dark">', 1)
|
||||
if "/static/instance_theme.js" not in text:
|
||||
text = text.replace(
|
||||
"<meta charset=\"UTF-8\">",
|
||||
"<meta charset=\"UTF-8\">\n" + SCRIPT_TAG.strip() + "\n",
|
||||
1,
|
||||
)
|
||||
if "/static/instance_theme.css" not in text:
|
||||
text = text.replace("</style>", "</style>\n" + CSS_LINK, 1)
|
||||
if path.name == "index.html" and INDEX_HEADER_OLD in text and "instance-theme-toggle" not in text:
|
||||
text = text.replace(INDEX_HEADER_OLD, INDEX_HEADER_NEW)
|
||||
if path.name == "login.html" and "instance-theme-toggle" not in text:
|
||||
text = text.replace(
|
||||
"<body>",
|
||||
'<div class="login-theme-bar">\n' + THEME_TOGGLE + "</div>\n<body>",
|
||||
1,
|
||||
)
|
||||
if path.name == "key_focus_v2.html" and "instance-theme-toggle" not in text:
|
||||
marker = '<div class="row" style="justify-content:space-between">'
|
||||
if marker in text:
|
||||
text = text.replace(
|
||||
marker,
|
||||
marker + "\n " + THEME_TOGGLE.replace("\n", "\n "),
|
||||
1,
|
||||
)
|
||||
if path.name == "order_focus_v2.html" and "instance-theme-toggle" not in text:
|
||||
marker = '<div class="row" style="justify-content:space-between">'
|
||||
if marker in text:
|
||||
text = text.replace(
|
||||
marker,
|
||||
marker + "\n " + THEME_TOGGLE.replace("\n", "\n "),
|
||||
1,
|
||||
)
|
||||
if text != orig:
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
n = 0
|
||||
for ex in EXCHANGES:
|
||||
for fn in FILES:
|
||||
p = ROOT / ex / "templates" / fn
|
||||
if p.is_file() and patch_file(p):
|
||||
print("patched", p.relative_to(ROOT))
|
||||
n += 1
|
||||
print("done", n, "files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
/* 实例页亮色主题(覆盖模板内联暗色样式) */
|
||||
html[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body {
|
||||
background: #d8e2ec !important;
|
||||
color: #1a2838 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .header h1 {
|
||||
color: #142232 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .exchange-tag {
|
||||
color: #087a50 !important;
|
||||
background: rgba(10, 143, 92, 0.12) !important;
|
||||
border-color: rgba(10, 143, 92, 0.35) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .top-nav a {
|
||||
background: #fff !important;
|
||||
color: #006e9a !important;
|
||||
border-color: rgba(0, 95, 140, 0.22) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .top-nav a.active {
|
||||
background: rgba(0, 110, 154, 0.12) !important;
|
||||
color: #142232 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .stat-item,
|
||||
html[data-theme="light"] .card,
|
||||
html[data-theme="light"] .meta-item,
|
||||
html[data-theme="light"] .list-item {
|
||||
background: #fff !important;
|
||||
border-color: #b8c8d8 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .stat-item .label,
|
||||
html[data-theme="light"] .status,
|
||||
html[data-theme="light"] .rule-tip {
|
||||
color: #4a6078 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .stat-item .value,
|
||||
html[data-theme="light"] .card h2 {
|
||||
color: #142232 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] input,
|
||||
html[data-theme="light"] select,
|
||||
html[data-theme="light"] textarea {
|
||||
background: #f6f9fc !important;
|
||||
color: #142232 !important;
|
||||
border-color: #b8c8d8 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .flash {
|
||||
background: rgba(0, 110, 154, 0.1) !important;
|
||||
color: #006e9a !important;
|
||||
border-color: rgba(0, 95, 140, 0.22) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] th {
|
||||
color: #4a6078 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] td {
|
||||
color: #142232 !important;
|
||||
border-bottom-color: #d0dae4 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .ai-result,
|
||||
html[data-theme="light"] .login-box {
|
||||
background: #fff !important;
|
||||
border-color: #b8c8d8 !important;
|
||||
color: #142232 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] #chart-wrap {
|
||||
background: #f0f4f9 !important;
|
||||
border-color: #b8c8d8 !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .btn {
|
||||
background: #fff !important;
|
||||
color: #006e9a !important;
|
||||
border-color: rgba(0, 95, 140, 0.22) !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .btn:hover {
|
||||
background: #eef3f8 !important;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #304164;
|
||||
background: #151a2a;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .theme-toggle {
|
||||
background: #fff;
|
||||
border-color: #b8c8d8;
|
||||
}
|
||||
|
||||
.theme-toggle.is-hub-linked {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #8fc8ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .theme-toggle-btn {
|
||||
color: #4a6078;
|
||||
}
|
||||
|
||||
.theme-toggle-btn.is-active {
|
||||
color: #dbe4ff;
|
||||
background: rgba(79, 121, 255, 0.2);
|
||||
box-shadow: inset 0 0 0 1px #304164;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .theme-toggle-btn.is-active {
|
||||
color: #006e9a;
|
||||
background: rgba(0, 110, 154, 0.12);
|
||||
box-shadow: inset 0 0 0 1px #b8c8d8;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.login-theme-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 四所实例主题:默认暗色;单独登录用 instance-theme;中控 iframe/SSO 随 hub-theme 联动。
|
||||
*/
|
||||
(function (global) {
|
||||
const STANDALONE_KEY = "instance-theme";
|
||||
const META = { dark: "#0b0d14", light: "#d8e2ec" };
|
||||
|
||||
function normalize(theme) {
|
||||
return theme === "light" ? "light" : "dark";
|
||||
}
|
||||
|
||||
function isHubLinked() {
|
||||
try {
|
||||
const p = new URLSearchParams(location.search);
|
||||
if (p.get("embed") === "1") return true;
|
||||
const ht = p.get("hub_theme");
|
||||
if (ht === "light" || ht === "dark") return true;
|
||||
} catch (_) {}
|
||||
try {
|
||||
if (window.self !== window.top) return true;
|
||||
} catch (_) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function themeFromUrl() {
|
||||
try {
|
||||
const t = new URLSearchParams(location.search).get("hub_theme");
|
||||
if (t === "light" || t === "dark") return t;
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getStandalone() {
|
||||
try {
|
||||
return normalize(localStorage.getItem(STANDALONE_KEY));
|
||||
} catch (_) {
|
||||
return "dark";
|
||||
}
|
||||
}
|
||||
|
||||
function setStandalone(theme) {
|
||||
try {
|
||||
localStorage.setItem(STANDALONE_KEY, normalize(theme));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function get() {
|
||||
if (isHubLinked()) {
|
||||
return themeFromUrl() || _linkedTheme || "dark";
|
||||
}
|
||||
return getStandalone();
|
||||
}
|
||||
|
||||
let _linkedTheme = null;
|
||||
|
||||
function apply(theme, opts) {
|
||||
const options = opts || {};
|
||||
const linked = isHubLinked();
|
||||
const t = normalize(theme);
|
||||
if (linked) {
|
||||
_linkedTheme = t;
|
||||
} else if (!options.skipStore) {
|
||||
setStandalone(t);
|
||||
}
|
||||
const root = document.documentElement;
|
||||
root.setAttribute("data-theme", t);
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute("content", META[t]);
|
||||
root.style.colorScheme = t;
|
||||
syncToggleUI();
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("instance-theme-change", { detail: { theme: t, hubLinked: linked } })
|
||||
);
|
||||
return t;
|
||||
}
|
||||
|
||||
function syncToggleUI(root) {
|
||||
const scope = root || document;
|
||||
const linked = isHubLinked();
|
||||
const toggle = scope.querySelector(".instance-theme-toggle");
|
||||
if (toggle) {
|
||||
toggle.classList.toggle("is-hub-linked", linked);
|
||||
toggle.setAttribute("aria-hidden", linked ? "true" : "false");
|
||||
}
|
||||
if (linked) return;
|
||||
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
|
||||
const on = btn.getAttribute("data-theme-value") === getStandalone();
|
||||
btn.classList.toggle("is-active", on);
|
||||
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function initToggleUI(root) {
|
||||
const scope = root || document;
|
||||
syncToggleUI(scope);
|
||||
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
|
||||
if (btn.dataset.themeBound === "1") return;
|
||||
btn.dataset.themeBound = "1";
|
||||
btn.addEventListener("click", () => {
|
||||
if (isHubLinked()) return;
|
||||
apply(btn.getAttribute("data-theme-value"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initFromHubMessage(data) {
|
||||
if (!data || data.type !== "hub-theme-sync") return;
|
||||
if (!isHubLinked()) return;
|
||||
apply(data.theme, { skipStore: true });
|
||||
}
|
||||
|
||||
function boot() {
|
||||
if (isHubLinked()) {
|
||||
apply(themeFromUrl() || "dark", { skipStore: true });
|
||||
window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
|
||||
try {
|
||||
window.parent.postMessage({ type: "instance-theme-ready" }, "*");
|
||||
} catch (_) {}
|
||||
} else {
|
||||
apply(getStandalone());
|
||||
}
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => initToggleUI());
|
||||
} else {
|
||||
initToggleUI();
|
||||
}
|
||||
}
|
||||
|
||||
boot();
|
||||
|
||||
global.InstanceTheme = {
|
||||
STANDALONE_KEY,
|
||||
isHubLinked,
|
||||
get,
|
||||
apply,
|
||||
initToggleUI,
|
||||
syncToggleUI,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
Reference in New Issue
Block a user