fix: unify order/key focus K-line theme, PnL, RR and exchange price tick

Share focus_chart templates and APIs across four instances; align chart Y-axis, price lines and meta bar with exchange symbol precision and live unrealized PnL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 16:45:51 +08:00
parent 3d55aa0975
commit 93e148a3e7
18 changed files with 1366 additions and 2128 deletions
+57 -21
View File
@@ -6290,35 +6290,51 @@ def api_order_kline():
"volume": float(bar[5]),
})
from focus_chart_lib import (
build_order_kline_order_payload,
load_swap_positions_for_order_kline,
metrics_for_order_item,
)
current_price = get_price(order_item["symbol"])
margin = float(order_item.get("margin_capital") or 0)
leverage = float(order_item.get("leverage") or 0)
entry = float(order_item.get("trigger_price") or 0)
float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0
float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0
positions = load_swap_positions_for_order_kline(
exchange,
private_configured=exchange_private_api_configured(),
ensure_markets_fn=ensure_markets_loaded,
)
ex_metrics = metrics_for_order_item(
order_item,
positions,
resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
select_live_fn=_select_live_position_row,
parse_metrics_fn=parse_ccxt_position_metrics,
)
order_payload = build_order_kline_order_payload(
order_item,
ticker_price=current_price,
format_price_fn=format_price_for_symbol,
calc_pnl_fn=calc_pnl,
calc_rr_ratio_fn=calc_rr_ratio,
ex_metrics=ex_metrics,
)
from focus_chart_lib import kline_api_price_fields
price_fields = kline_api_price_fields(
exchange,
exchange_symbol,
candles,
ensure_markets_fn=ensure_markets_loaded,
)
return jsonify({
"ok": True,
"timeframe": timeframe,
"limit": limit,
"order": {
"id": order_item["id"],
"symbol": order_item["symbol"],
"direction": order_item.get("direction") or "long",
"trigger_price": order_item.get("trigger_price"),
"stop_loss": order_item.get("stop_loss"),
"take_profit": order_item.get("take_profit"),
"margin_capital": order_item.get("margin_capital"),
"leverage": order_item.get("leverage"),
"position_ratio": order_item.get("position_ratio"),
"rr_ratio": order_item.get("rr_ratio"),
"breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
"current_price": round(float(current_price), 8) if current_price else None,
"float_pnl": round(float(float_pnl), 6),
"float_pct": float_pct,
},
"order": order_payload,
"candles": candles,
"updated_at": app_now_str(),
**price_fields,
})
@@ -6419,15 +6435,35 @@ def api_key_kline():
"lower_pct": lower_pct,
}
from focus_chart_lib import enrich_key_kline_response
price_display, key_info = enrich_key_kline_response(
symbol=symbol,
current_price=current_price,
key_info=key_info,
format_price_fn=format_price_for_symbol,
)
from focus_chart_lib import kline_api_price_fields
price_fields = kline_api_price_fields(
exchange,
exchange_symbol,
candles,
ensure_markets_fn=ensure_markets_loaded,
)
return jsonify({
"ok": True,
"symbol": symbol,
"timeframe": timeframe,
"limit": limit,
"current_price": round(float(current_price), 8) if current_price is not None else None,
"current_price_display": price_display,
"key_monitor": key_info,
"candles": candles,
"updated_at": app_now_str(),
**price_fields,
})
@@ -1,278 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></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=4">
</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,231 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></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=4">
</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>