feat: 币种输入旁实时显示交易所现价

Shared SymbolLivePrice polls /api/order_defaults on input with debounce; wired to trade, key monitor, trend, and key focus forms.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-05 00:42:44 +08:00
parent efe7a57e60
commit 3a740235ac
13 changed files with 273 additions and 14 deletions
+10 -3
View File
@@ -243,7 +243,7 @@
.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=49">
<link rel="stylesheet" href="/static/instance_theme.css?v=50">
</head>
<body
@@ -253,6 +253,7 @@
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
@@ -408,7 +409,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
@@ -848,6 +851,7 @@
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=1"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
@@ -2022,7 +2026,10 @@ function refreshAccountSnapshot(){
const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults);
if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){
+10 -3
View File
@@ -243,7 +243,7 @@
.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=49">
<link rel="stylesheet" href="/static/instance_theme.css?v=50">
</head>
<body
@@ -253,6 +253,7 @@
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
@@ -388,7 +389,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
@@ -815,6 +818,7 @@
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=1"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
@@ -1948,7 +1952,10 @@ function refreshAccountSnapshot(){
const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults);
if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){
+10 -3
View File
@@ -243,7 +243,7 @@
.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=49">
<link rel="stylesheet" href="/static/instance_theme.css?v=50">
</head>
<body
@@ -253,6 +253,7 @@
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
@@ -417,7 +418,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
@@ -844,6 +847,7 @@
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=1"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
@@ -2000,7 +2004,10 @@ if(allowOpenBeforeResetEl){
const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults);
if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){
+4
View File
@@ -85,6 +85,10 @@
if (typeof global.refreshPriceSnapshotConditional === "function") {
global.refreshPriceSnapshotConditional();
}
if (global.SymbolLivePrice && typeof global.SymbolLivePrice.init === "function") {
const root = document.getElementById("embed-page-root") || document;
global.SymbolLivePrice.init(root);
}
}
function injectFragment(html) {
+48
View File
@@ -1611,3 +1611,51 @@ html[data-theme="light"] .trade-policy-dir-lock {
border-color: #9ed4b8;
}
/* ── 币种输入实时现价 ── */
.symbol-live-price {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 600;
color: #8fc8ff;
background: rgba(31, 58, 90, 0.35);
border: 1px solid rgba(143, 200, 255, 0.22);
white-space: nowrap;
line-height: 1.35;
}
.symbol-live-price--ok {
color: #4cd97f;
border-color: rgba(76, 217, 127, 0.35);
background: rgba(76, 217, 127, 0.08);
}
.symbol-live-price--loading {
opacity: 0.75;
}
.symbol-live-price--err {
color: #e8a090;
border-color: rgba(232, 160, 144, 0.35);
}
.symbol-live-price-note {
font-size: 0.72rem;
color: #8892b0;
white-space: nowrap;
}
html[data-theme="light"] .symbol-live-price {
color: #1a4a7a;
background: #eef4fb;
border-color: #b8cfe8;
}
html[data-theme="light"] .symbol-live-price--ok {
color: #087a50;
background: #e8f8f0;
border-color: #9ed4b8;
}
+163
View File
@@ -0,0 +1,163 @@
/**
* 表单币种输入:防抖 + 定时刷新,展示交易所最新价(/api/order_defaults)。
*/
(function (global) {
"use strict";
const DEFAULT_DEBOUNCE_MS = 350;
const DEFAULT_POLL_MS = 5000;
const bound = new WeakSet();
function $(id) {
return id ? document.getElementById(id) : null;
}
function symbolValue(el) {
if (!el) return "";
return (el.value || "").trim();
}
function directionValue(dirId) {
const el = dirId ? $(dirId) : null;
const v = (el && el.value ? el.value : "long").trim().toLowerCase();
return v === "short" ? "short" : "long";
}
function formatPrice(px, sym) {
const n = Number(px);
if (!Number.isFinite(n)) return "—";
const u = (sym || "").trim().toUpperCase();
let digits = 4;
if (u.startsWith("BTC") || u.startsWith("ETH") || n >= 1000) digits = 2;
else if (n >= 10) digits = 3;
else if (n >= 1) digits = 4;
else if (n >= 0.01) digits = 5;
else digits = 6;
return n.toFixed(digits);
}
function pollMs() {
const raw =
(document.body && document.body.getAttribute("data-price-refresh-ms")) || "";
const n = Number(raw);
return Number.isFinite(n) && n >= 2000 ? n : DEFAULT_POLL_MS;
}
function paint(el, sym, px, err) {
if (!el) return;
if (err) {
el.textContent = "现价:—";
el.classList.add("symbol-live-price--err");
el.classList.remove("symbol-live-price--ok");
el.title = err;
return;
}
if (px === null || typeof px === "undefined") {
el.textContent = "现价:—";
el.classList.remove("symbol-live-price--ok", "symbol-live-price--err");
el.title = sym ? "无法读取交易所价格" : "";
return;
}
const label = sym ? sym.toUpperCase().replace(/\/USDT.*/, "") : "";
el.textContent = label ? label + " 现价 " + formatPrice(px, sym) : "现价 " + formatPrice(px, sym);
el.classList.add("symbol-live-price--ok");
el.classList.remove("symbol-live-price--err");
el.title = "交易所最新价(约 " + pollMs() / 1000 + "s 刷新)";
}
function bindOne(el) {
if (!el || bound.has(el)) return;
bound.add(el);
const symId = el.getAttribute("data-symbol-input");
const dirId = el.getAttribute("data-direction-input") || "";
let debounceTimer = null;
let pollTimer = null;
let fetchSeq = 0;
function clearPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startPoll() {
clearPoll();
pollTimer = setInterval(refresh, pollMs());
}
function refresh() {
const symEl = $(symId);
const sym = symbolValue(symEl);
if (!sym) {
paint(el, "", null, "");
clearPoll();
return;
}
const dir = directionValue(dirId);
const seq = ++fetchSeq;
el.classList.add("symbol-live-price--loading");
fetch(
"/api/order_defaults?symbol=" +
encodeURIComponent(sym) +
"&direction=" +
encodeURIComponent(dir)
)
.then(function (r) {
return r.json().then(function (d) {
return { status: r.status, data: d };
});
})
.then(function (res) {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
const data = res.data || {};
if (res.status >= 400 || !data.ok) {
paint(el, sym, null, (data && data.msg) || "读取失败");
return;
}
const px = data.last_price != null ? data.last_price : data.price;
paint(el, data.symbol || sym, px, "");
if (!pollTimer) startPoll();
})
.catch(function () {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
paint(el, sym, null, "网络错误");
});
}
function schedule() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(refresh, DEFAULT_DEBOUNCE_MS);
}
const symEl = $(symId);
if (symEl) {
symEl.addEventListener("input", schedule);
symEl.addEventListener("change", schedule);
}
const dirEl = dirId ? $(dirId) : null;
if (dirEl) {
dirEl.addEventListener("change", schedule);
}
schedule();
}
function init(root) {
const scope = root || document;
scope.querySelectorAll(".symbol-live-price").forEach(bindOne);
}
global.SymbolLivePrice = { init: init, bind: bindOne };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
init(document);
});
} else {
init(document);
}
})(typeof window !== "undefined" ? window : globalThis);
@@ -1155,7 +1155,10 @@ function refreshAccountSnapshot(){
const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults);
if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){
@@ -65,7 +65,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
+2 -1
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<link rel="stylesheet" href="/static/instance_page.css?v=2">
<link rel="stylesheet" href="/static/instance_theme.css?v=49">
<link rel="stylesheet" href="/static/instance_theme.css?v=50">
<script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
@@ -121,6 +121,7 @@
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=1"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script src="/static/key_monitor_form.js?v=2"></script>
{% include 'embed_boot_scripts.html' %}
+5 -2
View File
@@ -4,10 +4,11 @@
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=5"></script>
<title>{{ exchange_display }} | 关键位放大</title>
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
<link rel="stylesheet" href="/static/instance_theme.css?v=50">
<script src="/static/symbol_live_price.js?v=1"></script>
<link rel="stylesheet" href="/static/focus_chart_page.css?v=1">
</head>
<body class="focus-page">
<body class="focus-page" data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}">
{% if trade_policy is not defined %}
{% set trade_policy = {'symbol_restrict_enabled': false, 'direction_restrict_enabled': false, 'symbol_whitelist': [], 'allows_long': true, 'allows_short': true, 'badge_text': ''} %}
{% endif %}
@@ -36,6 +37,8 @@
<label>币种</label>
{% from 'trade_policy_fields.html' import trade_policy_symbol with context %}
{{ trade_policy_symbol('symbol', 'symbol-input', default_symbol, placeholder='BTC/USDT') }}
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('key-focus-symbol-live-price', 'symbol-input') }}
<label>关键位</label>
<select id="key-id">
<option value="">无(仅看K线)</option>
@@ -157,6 +157,8 @@
<option value="关键支撑阻力">关键支撑阻力</option>
</select>
{{ trade_policy_direction('direction', 'key-direction') }}
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('key-symbol-live-price', 'key-symbol', 'key-direction') }}
<input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none">
<input name="trigger_entry" id="key-trigger-entry" step="0.0001" placeholder="计划入场价" style="display:none">
<input name="trigger_sl" id="key-trigger-sl" step="0.0001" placeholder="止损价" style="display:none">
@@ -27,6 +27,8 @@
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
{{ trade_policy_symbol('symbol', 'trend-symbol', placeholder='BTC 或 ETH/USDT') }}
{{ trade_policy_direction('direction', 'trend-direction') }}
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('trend-symbol-live-price', 'trend-symbol', 'trend-direction') }}
<input name="leverage" type="number" min="1" step="1" placeholder="杠杆(必填)" required>
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" placeholder="风险%相对可用快照" title="默认5:最坏亏损约≤可用余额×5%">
<input name="sl" step="any" placeholder="止损价" required>
@@ -0,0 +1,10 @@
{# 币种输入旁实时现价(须加载 symbol_live_price.js #}
{% macro symbol_live_price_hint(price_id, symbol_input_id, direction_input_id='') -%}
<span
id="{{ price_id }}"
class="symbol-live-price"
data-symbol-input="{{ symbol_input_id }}"
{% if direction_input_id %}data-direction-input="{{ direction_input_id }}"{% endif %}
aria-live="polite"
>现价:—</span>
{%- endmacro %}