Files
crypto_monitor/lib/instance/templates/embed_boot_scripts.html
T
dekun f27b4bc1ec Fix OKX header trade stats stuck at zero after settings shell load.
Always SSR records summary on embed shell (including settings/risk/env), and refresh total/win-rate/PL ratio via account_snapshot so soft-nav cannot leave the strip blank.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 13:03:04 +08:00

1500 lines
66 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ORDER_TYPE_OPTIONS = {{ order_type_options | tojson }};
function reloadInstancePage(){
if(document.body && document.body.getAttribute("data-embed-shell") === "1" && window.InstanceEmbed){
window.InstanceEmbed.reloadCurrentTab();
return;
}
window.location.href = `${window.location.pathname}?_ts=${Date.now()}`;
}
function validateJournalEntryReason(){
const form = document.getElementById("journal-form");
if(!form) return true;
const orderSel = form.querySelector('[name="order_type"]');
if(!orderSel || !orderSel.value){
alert("请选择下单类型");
return false;
}
const sel = form.querySelector('[name="entry_reason"]');
if(!sel || !sel.value){
alert("请选择开仓类型");
return false;
}
return true;
}
function showImage(src){document.getElementById("bigImg").src=src;document.getElementById("imgModal").style.display="flex";}
function closeModal(){document.getElementById("imgModal").style.display="none";}
function setDetailModalFullscreen(on){
const modal = document.getElementById("detailModal");
if(modal){ modal.classList.toggle("fullscreen", !!on); }
}
function forceCloseDetailModal(){
const modal = document.getElementById("detailModal");
if(modal){ modal.style.display = "none"; modal.classList.remove("fullscreen"); }
}
function closeDetailModal(e){if(e.target && e.target.id==="detailModal"){forceCloseDetailModal();}}
function expandDetailToFullscreen(){ setDetailModalFullscreen(true); }
function toggleReviewCardFullscreen(){
const card = document.getElementById("review-card");
if(!card) return;
const on = !card.classList.contains("is-fullscreen");
card.classList.toggle("is-fullscreen", on);
document.body.classList.toggle("review-card-fullscreen-open", on);
const btn = document.getElementById("review-card-fs-btn");
if(btn){ btn.textContent = on ? "退出全屏" : "全屏"; }
}
document.addEventListener("keydown", function(e){
if(e.key !== "Escape") return;
const card = document.getElementById("review-card");
if(card && card.classList.contains("is-fullscreen")){ toggleReviewCardFullscreen(); }
});
function setAiReviewMarkdown(el, rawText){
if(!el) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
AiReviewRender.setElementMarkdown(el, rawText || "");
} else {
el.classList.remove("ai-result-md");
el.innerText = rawText || "";
}
}
function setDetailBodyPlain(text){
const body = document.getElementById("detailBody");
if(!body) return;
body.classList.remove("md-review");
body.innerText = text || "";
}
function setDetailBodyMarkdown(text){
if(window.InstanceUI && InstanceUI.clearDetailActions) InstanceUI.clearDetailActions();
const body = document.getElementById("detailBody");
if(!body) return;
body.classList.remove("trade-record-detail-wrap", "journal-detail-meta");
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
body.classList.add("md-review");
AiReviewRender.setElementMarkdown(body, text || "");
} else {
setDetailBodyPlain(text);
}
}
function openAiInlineResultFullscreen(title, elementId){
const el = document.getElementById(elementId || "daily_result");
const text = (window.AiReviewRender && AiReviewRender.getElementMarkdown)
? String(AiReviewRender.getElementMarkdown(el) || "").trim()
: String((el && el.innerText) || "").trim();
if(!text){ alert("暂无内容"); return; }
document.getElementById("detailTitle").innerText = title || "AI复盘";
setDetailBodyMarkdown(text);
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
setDetailModalFullscreen(true);
document.getElementById("detailModal").style.display = "flex";
}
const journalCache = {};
const reviewCache = {};
window.journalCache = journalCache;
window.reviewCache = reviewCache;
function formatJournalExitOneLine(o){
const t = (o.early_exit_trigger || "").trim();
const n = (o.early_exit_note || "").trim();
if(t === "手动平仓") return n || (o.exit_reason || "").trim() || "无";
if(t) return t;
return (o.exit_reason || "").trim() || (o.early_exit_reason || "").trim() || "无";
}
function openJournalDetail(id){
InstanceUI.openJournalDetailModal(id, journalCache, formatJournalExitOneLine);
}
function openReviewDetail(id, fullscreen){
const r = reviewCache[id];
if(!r){ return; }
document.getElementById("detailTitle").innerText = `${r.review_type === "daily" ? "日复盘" : "周复盘"}${r.target_date || "-"}`;
setDetailBodyMarkdown(r.content || "");
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
setDetailModalFullscreen(!!fullscreen);
document.getElementById("detailModal").style.display = "flex";
}
function deleteJournal(id){
if(!confirm("确定删除该交易复盘记录?")) return;
fetch(`/delete_journal/${id}`,{method:"POST"}).then(()=>loadJournals());
}
function deleteReview(id){
if(!confirm("确定删除该AI复盘?")) return;
fetch(`/delete_review/${id}`,{method:"POST"}).then(()=>loadReviews());
}
function deleteTradeRecord(id){
if(!confirm("确定删除这条交易记录?")) return;
fetch(`/delete_trade_record/${id}`,{method:"POST"})
.then(r=>r.json())
.then(data=>{
if(data && data.ok){
const row = document.getElementById(`trade-row-${id}`);
if(row){ row.remove(); return; }
}
reloadInstancePage();
})
.catch(()=>{ reloadInstancePage(); });
}
function normalizeBeijingDatetimeString(v){
const raw = String(v || "").trim().replace("T"," ");
const m = raw.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2}:\d{2})(:\d{2})?/);
if(!m) return "";
const sec = m[3] ? m[3].slice(1,3) : "00";
return `${m[1]} ${m[2]}:${sec}`;
}
function toggleReviewMode(){
if(window.InstanceTheme && typeof InstanceTheme.syncReviewEditButtons === "function"){
InstanceTheme.syncReviewEditButtons();
return;
}
const on = !!(document.getElementById("review-mode-toggle") || {}).checked;
document.querySelectorAll(".review-edit-btn").forEach(btn=>{
btn.disabled = !on;
});
}
function editTradeRecordReview(t){
if(!t) return;
const pickEntryReason = function(){
if(window.InstanceUI && typeof InstanceUI.promptReviewEntryReason === "function"){
return InstanceUI.promptReviewEntryReason(JOURNAL_ENTRY_REASON_OPTIONS, t.effective_entry_reason || "");
}
const entryHint = "开仓类型(下拉选项之一,留空=不改该项)";
return Promise.resolve(prompt(entryHint, String(t.effective_entry_reason || "")));
};
const opened = prompt("开仓时间(YYYY-MM-DD HH:MM:SS)", normalizeBeijingDatetimeString(t.opened_at || ""));
if(opened === null) return;
const closed = prompt("平仓时间(YYYY-MM-DD HH:MM:SS)", normalizeBeijingDatetimeString(t.closed_at || ""));
if(closed === null) return;
const stopLoss = prompt("止损价格(核对后用于统计)", formatPriceForInput(t.stop_loss));
if(stopLoss === null) return;
const takeProfit = prompt("止盈价格(核对后用于统计)", formatPriceForInput(t.take_profit));
if(takeProfit === null) return;
const pnl = prompt("最终盈亏(可手工核对后填写)", String(t.pnl_amount ?? ""));
if(pnl === null) return;
const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓/强制清仓/时间平仓)", String(t.result || ""));
if(result === null) return;
const note = prompt("备注(可空)", String(t.miss_reason || "")) ?? "";
pickEntryReason().then(function(entryIn){
if(entryIn === null) return;
const payload = {
id: t.id,
reviewed_opened_at: normalizeBeijingDatetimeString(opened),
reviewed_closed_at: normalizeBeijingDatetimeString(closed),
reviewed_stop_loss: stopLoss,
reviewed_take_profit: takeProfit,
reviewed_pnl_amount: pnl,
reviewed_result: String(result || "").trim(),
reviewed_miss_reason: String(note || "").trim()
};
const entryTrim = String(entryIn || "").trim();
if(entryTrim) payload.reviewed_entry_reason = entryTrim;
fetch("/api/trade_record_review_update",{
method:"POST",
headers:{"Content-Type":"application/json"},
body: JSON.stringify(payload)
})
.then(r=>r.json().then(d=>({status:r.status, data:d})))
.then(({status,data})=>{
if(status >= 400 || !data.ok){
alert((data && data.msg) || "核对保存失败");
return;
}
alert(`核对已保存:持仓分钟=${data.hold_minutes} 实际RR=${data.actual_rr ?? "-"}`);
reloadInstancePage();
})
.catch(()=>alert("核对保存请求失败"));
});
}
function deleteKeyMonitor(id){
if(!confirm("删除该关键位?将写入下方历史并刷新页面.")) return;
fetch(`/delete_key_monitor/${id}`,{method:"POST"})
.then(r=>r.json())
.then(data=>{
reloadInstancePage();
})
.catch(()=>{ reloadInstancePage(); });
}
function deleteKeyHistory(id){
if(!confirm("确定删除这条关键位历史?")) return;
fetch(`/delete_key_history/${id}`,{method:"POST"})
.then(r=>r.json())
.then(()=>{
reloadInstancePage();
})
.catch(()=>{ reloadInstancePage(); });
}
function listWindowQueryString(){
const presetEl = document.getElementById("win-preset-select");
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_this_month";
const q = new URLSearchParams(window.location.search);
q.set("win_preset", preset);
if(preset === "custom"){
const fromEl = document.getElementById("win-from-utc");
const toEl = document.getElementById("win-to-utc");
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
else q.delete("from_utc");
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
else q.delete("to_utc");
} else {
q.delete("from_utc");
q.delete("to_utc");
}
return q.toString();
}
function toggleListWindowCustom(){
const preset = document.getElementById("win-preset-select");
const box = document.getElementById("win-custom-range");
if(!preset || !box) return;
box.style.display = preset.value === "custom" ? "" : "none";
}
function applyListWindow(){
const qs = listWindowQueryString();
const path = window.location.pathname || "/trade";
window.location.href = qs ? (path + "?" + qs) : path;
}
function attachListWindowToExports(){
const qs = listWindowQueryString();
if(!qs) return;
document.querySelectorAll('.export-bar a[href^="/export/trade_records"], .export-bar a[href^="/export/key_monitor_history"]').forEach(a=>{
const base = a.getAttribute("href").split("?")[0];
a.setAttribute("href", base + "?" + qs);
});
}
function loadJournals(){
const qs = listWindowQueryString();
fetch("/api/journals" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
data.forEach(o=>{ journalCache[o.id] = o; });
const box = document.getElementById("journal-list");
if(box){
const html = InstanceUI.renderJournalListHtml(data);
box.innerHTML = html || "<div class='journal-empty-msg'>暂无数据</div>";
}
});
}
function loadReviews(){
const qs = listWindowQueryString();
fetch("/api/reviews" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
let html="";
data.forEach(r=>{
reviewCache[r.id] = r;
const preview = (r.content || "").replace(/\s+/g, " ").trim();
const shortText = preview.length > 90 ? `${preview.slice(0, 90)}...` : preview;
html += `<div class="entry">
<div><strong>${r.review_type === "daily" ? "日复盘" : "周复盘"}</strong> | ${r.target_date}</div>
<div style="font-size:12px;color:#9aa">${r.created_at || ""}</div>
<div style="margin-top:4px;color:#c9d2ff">${shortText || "(空)"}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px">
<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openReviewDetail('${r.id}', false)">查看</button>
<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openReviewDetail('${r.id}', true)">全屏</button>
<a class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff" href="/export/review_md/${r.id}">导出MD</a>
<button type="button" class="btn-del" onclick="deleteReview('${r.id}')">删除</button>
</div>
</div>`;
});
const box = document.getElementById("review-list");
if(box){ box.innerHTML = html || "<div class='entry'>暂无数据</div>"; }
});
}
function genDaily(){
if(window.AiReviewRender && AiReviewRender.isGenerating && AiReviewRender.isGenerating()) return;
const d = document.getElementById("day_date").value;
if(!d){alert("请选择日期");return;}
if(window.AiReviewRender && AiReviewRender.setGenerating){
AiReviewRender.setGenerating({
wrapId:"daily_result_wrap",
elId:"daily_result",
btnId:"gen-daily-btn",
message:"生成日复盘中,请稍候…(AI 分析可能需要 1~3 分钟)",
btnLabel:"日复盘生成中…"
});
}
const ac = new AbortController();
const timer = setTimeout(()=>ac.abort(), 360000);
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`,signal:ac.signal})
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
.then(data=>{
if(!data || data.result == null) throw new Error("返回数据为空");
const el=document.getElementById("daily_result");
const wrap=document.getElementById("daily_result_wrap");
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else if(el){ el.style.display="block"; }
loadReviews();
})
.catch(err=>{
const el=document.getElementById("daily_result");
if(el && el.classList.contains("is-loading")){
el.classList.remove("is-loading","ai-result-md");
el.innerText = err.name === "AbortError"
? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS."
: "生成失败,请重试.";
}
alert("生成日复盘失败:"+(err.message||err));
})
.finally(()=>{
clearTimeout(timer);
if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn");
});
}
function genWeekly(){
if(window.AiReviewRender && AiReviewRender.isGenerating && AiReviewRender.isGenerating()) return;
const s=document.getElementById("week_start").value;
const e=document.getElementById("week_end").value;
if(!s || !e){alert("请选择起止日期");return;}
if(window.AiReviewRender && AiReviewRender.setGenerating){
AiReviewRender.setGenerating({
wrapId:"weekly_result_wrap",
elId:"weekly_result",
btnId:"gen-weekly-btn",
message:"生成周复盘中,请稍候…(AI 分析可能需要 1~3 分钟)",
btnLabel:"周复盘生成中…"
});
}
const ac = new AbortController();
const timer = setTimeout(()=>ac.abort(), 360000);
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`,signal:ac.signal})
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
.then(data=>{
if(!data || data.result == null) throw new Error("返回数据为空");
const el=document.getElementById("weekly_result");
const wrap=document.getElementById("weekly_result_wrap");
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else if(el){ el.style.display="block"; }
loadReviews();
})
.catch(err=>{
const el=document.getElementById("weekly_result");
if(el && el.classList.contains("is-loading")){
el.classList.remove("is-loading","ai-result-md");
el.innerText = err.name === "AbortError"
? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS."
: "生成失败,请重试.";
}
alert("生成周复盘失败:"+(err.message||err));
})
.finally(()=>{
clearTimeout(timer);
if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn");
});
}
function exportDailyBundleMd(){
const d = document.getElementById("day_date").value;
if(!d){ alert("请先选择日期"); return; }
const url = `/export/reviews_md_bundle?review_type=daily&target_date=${encodeURIComponent(d)}`;
window.location.href = url;
}
function exportWeeklyBundleMd(){
const s = document.getElementById("week_start").value;
const e = document.getElementById("week_end").value;
if(!s || !e){ alert("请先选择周起止日期"); return; }
const target = `${s}~${e}`;
const url = `/export/reviews_md_bundle?review_type=weekly&target_date=${encodeURIComponent(target)}`;
window.location.href = url;
}
function setJournalField(name, value){
const form = document.getElementById("journal-form");
const el = form ? form.querySelector(`[name="${name}"]`) : null;
if(!el) return;
if(typeof value === "undefined" || value === null) return;
el.value = String(value);
}
const EARLY_EXIT_TRIGGERS = new Set(["止盈","保本止盈","移动止盈","时间平仓","强制清仓","手动平仓","止损","其他"]);
const KEY_ENTRY_REASON_BY_SIGNAL = {
"箱体突破": "关键位箱体突破",
"收敛突破": "关键位收敛突破",
"斐波回调0.618": "关键位斐波0.618",
"斐波回调0.786": "关键位斐波0.786",
"假突破": "关键位假突破"
};
function splitLegacyEarlyExitReason(raw){
const s = String(raw || "").trim();
if(!s) return { trigger: "", note: "" };
const sep = s.indexOf("");
if(sep > -1){
const a = s.slice(0, sep).trim();
const b = s.slice(sep + 1).trim();
if(EARLY_EXIT_TRIGGERS.has(a)){
return { trigger: a, note: b };
}
}
if(EARLY_EXIT_TRIGGERS.has(s)){
return { trigger: s, note: "" };
}
return { trigger: "", note: s };
}
function normalizeDatetimeLocal(v){
const raw = String(v || "").trim();
if(!raw) return "";
const m = raw.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/);
if(m) return `${m[1]}T${m[2]}`;
return raw;
}
function toDatetimeLocalFromBeijing(v){
const raw = String(v || "").trim();
if(!raw) return "";
const m = raw.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/);
if(m) return `${m[1]}T${m[2]}`;
return "";
}
function coinFromSymbol(symbol){
const s = String(symbol || "").trim().toUpperCase();
if(!s) return "";
if(s.includes("/")) return s.split("/")[0];
if(s.includes("-")) return s.split("-")[0];
if(s.endsWith("USDT")) return s.slice(0, -4);
return s;
}
/** 输入框/备注用价格:去掉浮点尾数,按量级保留有效小数(与后端 price_fmt 兜底一致) */
function formatPriceForInput(val){
if(val === null || val === undefined || val === "") return "";
const v = Number(val);
if(!Number.isFinite(v)) return String(val);
const av = Math.abs(v);
let d;
if(av >= 10000) d = 2;
else if(av >= 100) d = 3;
else if(av >= 1) d = 4;
else if(av >= 0.01) d = 6;
else if(av >= 0.0001) d = 8;
else d = 10;
let text = v.toFixed(d);
if(text.includes(".")) text = text.replace(/\.?0+$/, "");
return text;
}
function calcExpectedRrFromTrade(t){
const entry = Number(t.trigger_price);
const sl = Number(t.stop_loss);
const tp = Number(t.take_profit);
if(!Number.isFinite(entry) || !Number.isFinite(sl) || !Number.isFinite(tp)) return "";
if(entry <= 0 || sl <= 0 || tp <= 0) return "";
const direction = (t.direction || "long").toLowerCase();
let risk = 0;
let reward = 0;
if(direction === "short"){
risk = sl - entry;
reward = entry - tp;
} else {
risk = entry - sl;
reward = tp - entry;
}
if(risk <= 0 || reward <= 0) return "";
return (reward / risk).toFixed(2);
}
function fillJournalFromTrade(t){
if(!t){ return; }
setJournalField("open_datetime", toDatetimeLocalFromBeijing(t.opened_at));
setJournalField("close_datetime", toDatetimeLocalFromBeijing(t.closed_at));
setJournalField("coin", coinFromSymbol(t.symbol));
setJournalField("tf", "5m");
setJournalField("pnl", (t.pnl_amount === null || typeof t.pnl_amount === "undefined") ? "" : String(t.pnl_amount));
const rr = calcExpectedRrFromTrade(t);
setJournalField("expect_rr", rr);
let realRr = rr;
const riskAmount = Number(t.risk_amount);
const pnlAmount = Number(t.pnl_amount);
if(Number.isFinite(riskAmount) && riskAmount > 0 && Number.isFinite(pnlAmount)){
realRr = (pnlAmount / riskAmount).toFixed(2);
}
setJournalField("real_rr", realRr);
const riskHint = document.getElementById("risk-amount-hint");
if(riskHint){ riskHint.value = (Number.isFinite(riskAmount) && riskAmount > 0) ? String(riskAmount) : ""; }
const entryPx = formatPriceForInput(t.trigger_price);
const slPx = formatPriceForInput(t.stop_loss);
const tpPx = formatPriceForInput(t.take_profit);
const entryHint = document.getElementById("entry-price-hint");
if(entryHint){ entryHint.value = entryPx; }
const stopHint = document.getElementById("stop-loss-hint");
if(stopHint){ stopHint.value = slPx; }
const dirHint = document.getElementById("direction-hint");
if(dirHint){ dirHint.value = t.direction || "long"; }
const dirNorm = String(t.direction || "long").toLowerCase() === "short" ? "short" : "long";
setJournalField("direction", dirNorm);
setJournalField("early_exit_trigger", "");
setJournalField("early_exit_note", "");
const kst = String(t.key_signal_type || "").trim();
const mt = String(t.monitor_type || "").trim();
const orderType = (function(){
if(mt === "趋势回调" && JOURNAL_ORDER_TYPE_OPTIONS.includes("趋势回调")) return "趋势回调";
if(mt === "顺势加仓" && JOURNAL_ORDER_TYPE_OPTIONS.includes("顺势加仓")) return "顺势加仓";
if(mt === "关键位监控" || mt.includes("关键位")) return "关键位监控";
return "下单监控";
})();
if(JOURNAL_ORDER_TYPE_OPTIONS.includes(orderType)){
setJournalField("order_type", orderType);
} else {
setJournalField("order_type", "");
}
if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
setJournalField("entry_reason", t.effective_entry_reason);
} else {
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
setJournalField("entry_reason", erFromKey);
} else {
setJournalField("entry_reason", "");
}
}
const er = String(t.result || "").trim();
const exitTrigMap = { 止盈: "止盈", 保本止盈: "保本止盈", 移动止盈: "移动止盈", 时间平仓: "时间平仓", 强制清仓: "强制清仓", 手动平仓: "手动平仓", 止损: "止损" };
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${entryPx || "-"} 止损:${slPx || "-"} 止盈:${tpPx || "-"} | 下单类型:${orderType || t.monitor_type || "-"}`;
setJournalField("note", note);
const form = document.getElementById("journal-form");
if(form && typeof form.scrollIntoView === "function"){
form.scrollIntoView({behavior:"smooth", block:"start"});
}
recomputeJournalRealRr();
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
}
function recomputeJournalRealRr(){
const form = document.getElementById("journal-form");
if(!form) return;
const pnlEl = form.querySelector('[name="pnl"]');
const rrEl = form.querySelector('[name="real_rr"]');
const riskHint = document.getElementById("risk-amount-hint");
if(!pnlEl || !rrEl || !riskHint) return;
const pnl = Number(String(pnlEl.value || "").trim());
const risk = Number(String(riskHint.value || "").trim());
if(Number.isFinite(pnl) && Number.isFinite(risk) && risk > 0){
rrEl.value = (pnl / risk).toFixed(2);
}
}
function toggleStatsCard(){
const card = document.getElementById("stats-card");
const btn = document.getElementById("stats-toggle-btn");
if(!card || !btn) return;
const collapsed = card.classList.toggle("collapsed");
btn.innerText = collapsed ? "展开" : "折叠";
}
function bindListWindowDateAutoCustom(){
const preset = document.getElementById("win-preset-select");
const fromEl = document.getElementById("win-from-utc");
const toEl = document.getElementById("win-to-utc");
function toCustom(){
if(preset) preset.value = "custom";
toggleListWindowCustom();
}
if(fromEl) fromEl.addEventListener("change", toCustom);
if(toEl) toEl.addEventListener("change", toCustom);
}
attachListWindowToExports();
toggleListWindowCustom();
bindListWindowDateAutoCustom();
initStatsSegmentFromUrl();
if(document.getElementById("journal-list")) loadJournals();
if(document.getElementById("review-list")) loadReviews();
const reviewToggle = document.getElementById("review-mode-toggle");
if(reviewToggle){
reviewToggle.addEventListener("change", toggleReviewMode);
reviewToggle.addEventListener("input", toggleReviewMode);
toggleReviewMode();
}
if(window.InstanceTheme && typeof InstanceTheme.initReviewEditModeSync === "function"){
InstanceTheme.initReviewEditModeSync();
}
const journalForm = document.getElementById("journal-form");
if(journalForm){
const pnlInput = journalForm.querySelector('[name="pnl"]');
if(pnlInput){
pnlInput.addEventListener("input", recomputeJournalRealRr);
pnlInput.addEventListener("change", recomputeJournalRealRr);
}
const earlyTrig = journalForm.querySelector('[name="early_exit_trigger"]');
const earlyNote = journalForm.querySelector('[name="early_exit_note"]');
function syncEarlyExitNoteRequired(){
if(!earlyTrig || !earlyNote) return;
if(earlyTrig.value === "手动平仓"){
earlyNote.setAttribute("required", "required");
earlyNote.placeholder = "手工平仓须说明原因(必填)";
} else {
earlyNote.removeAttribute("required");
earlyNote.placeholder = "离场补充(仅手工平仓必填)";
}
}
window.syncEarlyExitNoteRequired = syncEarlyExitNoteRequired;
if(earlyTrig){
earlyTrig.addEventListener("change", syncEarlyExitNoteRequired);
syncEarlyExitNoteRequired();
}
}
if(window.TimeCloseUI){
TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap");
}
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
setTimeout(() => {
if(document.getElementById("journal-list")) loadJournals();
if(document.getElementById("review-list")) loadReviews();
}, 300);
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
const MANUAL_FIXED_RR_DEFAULT = 1.5;
const FIXED_RR_LS_KEY = "manualFixedRr";
function loadFixedRrPref(){
try{
const raw = localStorage.getItem(FIXED_RR_LS_KEY);
const el = document.getElementById("order-fixed-rr");
if(!el || raw == null || raw === "") return;
const v = Number(raw);
if(Number.isFinite(v) && v > 0) el.value = raw;
}catch(_){}
}
function saveFixedRrPref(){
try{
const el = document.getElementById("order-fixed-rr");
if(el && el.value) localStorage.setItem(FIXED_RR_LS_KEY, el.value);
}catch(_){}
}
function calcTpFromFixedRr(direction, entry, sl, rr){
const e = Number(entry), s = Number(sl), r = Number(rr);
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(r) || r <= 0) return null;
if(direction === "short"){
if(s <= e) return null;
return e - (s - e) * r;
}
if(s >= e) return null;
return e + (e - s) * r;
}
function refreshOrderTpPreview(entryPx){
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
}
function calcClientRr(direction, entry, sl, tp){
const e = Number(entry), s = Number(sl), t = Number(tp);
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
if(direction === 'short'){
if(s <= e || t >= e) return null;
return (e - t) / (s - e);
}
if(s >= e || t <= e) return null;
return (t - e) / (e - s);
}
function calcClientRrFromPct(slPct, tpPct){
const sl = Number(slPct), tp = Number(tpPct);
if(!Number.isFinite(sl) || !Number.isFinite(tp) || sl <= 0 || tp <= 0) return null;
return tp / sl;
}
function rejectManualOrderRr(rr){
if(rr !== null && rr >= MANUAL_MIN_PLANNED_RR) return false;
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单.`);
return true;
}
let tpslEntrustMonitorId = null;
function formatExTpslLine(role, slot){
const label = role === 'sl' ? '止损' : '止盈';
if(!slot || !slot.order_id) return label + ':未挂单';
const px = slot.trigger_display || slot.trigger_price || '-';
const amt = slot.amount != null && !Number.isNaN(Number(slot.amount)) ? ` 数量 ${Number(slot.amount)}` : '';
return `${label}:触发 ${px}${amt}`;
}
function paintExchangeTpslRow(orderId, tpsl){
const data = tpsl || {};
const slText = document.getElementById(`ex-sl-text-${orderId}`);
const tpText = document.getElementById(`ex-tp-text-${orderId}`);
const slBtn = document.getElementById(`ex-sl-cancel-${orderId}`);
const tpBtn = document.getElementById(`ex-tp-cancel-${orderId}`);
const intraday = (document.body && document.body.getAttribute("data-intraday-discipline")) === "1";
if(slText) slText.innerText = formatExTpslLine('sl', data.sl);
if(tpText) tpText.innerText = formatExTpslLine('tp', data.tp);
if(!intraday){
if(slBtn) slBtn.disabled = !(data.sl && data.sl.order_id);
if(tpBtn) tpBtn.disabled = !(data.tp && data.tp.order_id);
}
}
function toggleTpslModalMode(){
const mode = (document.getElementById('tpsl-modal-mode')||{}).value || 'price';
const pct = mode === 'pct';
['tpsl-modal-sl','tpsl-modal-tp'].forEach(id=>{ const el=document.getElementById(id); if(el) el.style.display=pct?'none':''; });
['tpsl-modal-sl-pct','tpsl-modal-tp-pct'].forEach(id=>{ const el=document.getElementById(id); if(el) el.style.display=pct?'':'none'; });
}
function openTpslEntrustModal(orderId){
const card = document.getElementById(`order-row-${orderId}`);
if(!card) return;
tpslEntrustMonitorId = orderId;
const slEl = document.getElementById('tpsl-modal-sl');
const tpEl = document.getElementById('tpsl-modal-tp');
if(slEl) slEl.value = formatPriceForInput(card.getAttribute('data-plan-sl') || '');
if(tpEl) tpEl.value = formatPriceForInput(card.getAttribute('data-plan-tp') || '');
const modeEl = document.getElementById('tpsl-modal-mode');
if(modeEl) modeEl.value = 'price';
toggleTpslModalMode();
const title = document.getElementById('tpsl-modal-title');
if(title) title.innerText = `挂止盈止损 · ${card.getAttribute('data-symbol')||''}`;
const modal = document.getElementById('tpsl-modal');
if(modal) modal.classList.add('open');
}
function closeTpslEntrustModal(){
tpslEntrustMonitorId = null;
const modal = document.getElementById('tpsl-modal');
if(modal) modal.classList.remove('open');
}
function submitTpslEntrust(){
const orderId = tpslEntrustMonitorId;
if(!orderId) return;
const mode = (document.getElementById('tpsl-modal-mode')||{}).value || 'price';
const body = { sltp_mode: mode };
if(mode === 'pct'){
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
}else{
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
body.tp = (document.getElementById('tpsl-modal-tp')||{}).value;
}
fetch(`/api/order/${orderId}/place_tpsl`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
.then(r=>r.json()).then(data=>{
if(!data.ok){ alert(data.msg || '委托失败'); return; }
alert(data.msg || '已提交');
closeTpslEntrustModal();
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
paintPlanTpslDisplay(orderId, data);
paintLatestRiskDisplay(orderId, data);
paintContractsDisplay(orderId, data);
paintTpProfitDisplay(orderId, data);
const rrEl = document.getElementById(`order-rr-${orderId}`);
if(rrEl){
const rr = data.display_rr_ratio != null && data.display_rr_ratio !== "" ? data.display_rr_ratio : data.planned_rr;
rrEl.innerText = formatRrRatio(rr);
}
refreshPriceSnapshotConditional();
}).catch(()=>alert('委托请求失败'));
}
function cancelExchangeTpsl(orderId, role){
const label = role === 'sl' ? '止损' : '止盈';
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
fetch(`/api/order/${orderId}/cancel_tpsl`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ role }) })
.then(r=>r.json()).then(data=>{
if(!data.ok){ alert(data.msg || '撤单失败'); return; }
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
else refreshPriceSnapshotConditional();
}).catch(()=>alert('撤单请求失败'));
}
function allowManualOrderSubmit(form){
form.dataset.rrOk = "1";
if(document.body && document.body.getAttribute("data-embed-shell") === "1" && window.InstanceEmbed && window.InstanceEmbed.postFormAndReload){
window.InstanceEmbed.postFormAndReload(form, "开仓提交中…");
return;
}
if(window.FormSubmitGuard){
if(FormSubmitGuard.isLocked(form)){
FormSubmitGuard.setSubmitLabel(form, "开仓提交中…");
} else {
FormSubmitGuard.lock(form, "开仓提交中…");
}
}
form.submit();
}
let latestAvailableUsdt = null;
const lastPriceMap = {};
function formatSigned(v, digits=2){
if(v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-";
const n = Number(v);
const sign = n > 0 ? "+" : "";
return `${sign}${n.toFixed(digits)}`;
}
function formatRrRatio(rr){
if(rr === null || typeof rr === "undefined") return "-:1";
const n = Number(rr);
if(Number.isNaN(n)) return "-:1";
const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2)));
return `${body}:1`;
}
function resolveSlBreakevenSecured(orderId, snap){
if(!snap) return false;
const et = snap.exchange_tpsl;
const slSlot = et && et.sl ? et.sl : null;
let slPx = NaN;
if(slSlot){
const raw = slSlot.trigger_price != null ? slSlot.trigger_price : slSlot.trigger_display;
slPx = raw != null && raw !== "" ? Number(raw) : NaN;
}
if(!Number.isFinite(slPx) || slPx <= 0){
slPx = snap.stop_loss != null && snap.stop_loss !== "" ? Number(snap.stop_loss) : NaN;
}
let entry = snap.avg_entry_price != null && snap.avg_entry_price !== "" ? Number(snap.avg_entry_price) : NaN;
let direction = snap.direction ? String(snap.direction).toLowerCase() : "";
const card = document.getElementById(`order-row-${orderId}`);
if(card){
if(!direction) direction = String(card.getAttribute("data-direction") || "long").toLowerCase();
if(!Number.isFinite(entry)){
const ent = card.getAttribute("data-entry");
if(ent){
const n = Number(String(ent).replace(/,/g, ""));
if(Number.isFinite(n) && n > 0) entry = n;
}
}
}
if(Number.isFinite(slPx) && Number.isFinite(entry) && entry > 0){
if(direction === "short") return slPx <= entry;
return slPx >= entry;
}
return snap.sl_breakeven_secured === true || snap.sl_breakeven_secured === 1;
}
function paintBreakevenBadge(orderId, snap){
const wrap = document.getElementById(`order-be-wrap-${orderId}`);
if(!wrap) return;
const secured = (snap && typeof snap === "object") ? resolveSlBreakevenSecured(orderId, snap) : !!snap;
wrap.style.display = secured ? "inline-flex" : "none";
}
function paintPlanTpslDisplay(orderId, snap){
if(!snap) return;
const card = document.getElementById(`order-row-${orderId}`);
const slEl = document.getElementById(`order-plan-sl-${orderId}`);
const tpEl = document.getElementById(`order-plan-tp-${orderId}`);
const slRaw = snap.stop_loss_raw != null && snap.stop_loss_raw !== "" ? snap.stop_loss_raw : snap.stop_loss;
const tpRaw = snap.take_profit_raw != null && snap.take_profit_raw !== "" ? snap.take_profit_raw : snap.take_profit;
const slDisp = snap.stop_loss_display || (slRaw != null && slRaw !== "" ? formatPriceForInput(slRaw) : null);
const tpDisp = snap.take_profit_display || (tpRaw != null && tpRaw !== "" ? formatPriceForInput(tpRaw) : null);
if(slEl) slEl.innerText = slDisp || "—";
if(tpEl) tpEl.innerText = tpDisp || "—";
if(card){
if(slRaw != null && slRaw !== "") card.setAttribute("data-plan-sl", formatPriceForInput(slRaw));
else if(slDisp) card.setAttribute("data-plan-sl", slDisp);
if(tpRaw != null && tpRaw !== "") card.setAttribute("data-plan-tp", formatPriceForInput(tpRaw));
else if(tpDisp) card.setAttribute("data-plan-tp", tpDisp);
}
}
function paintLatestRiskDisplay(orderId, snap){
const wrap = document.getElementById(`order-latest-risk-wrap-${orderId}`);
if(!wrap) return;
const v = snap && snap.latest_risk_amount;
const n = v != null && v !== "" ? Number(v) : NaN;
if(Number.isFinite(n)){
wrap.style.display = "inline-flex";
wrap.textContent = `最新风险: ${n.toFixed(2)}U`;
} else {
wrap.style.display = "none";
}
}
function paintContractsDisplay(orderId, snap){
const el = document.getElementById(`order-contracts-${orderId}`);
if(!el || !snap) return;
const v = snap.contracts != null && snap.contracts !== "" ? snap.contracts : snap.order_amount;
const n = v != null && v !== "" ? Number(v) : NaN;
el.innerText = Number.isFinite(n) ? n.toFixed(2) : "—";
}
function paintTpProfitDisplay(orderId, snap){
const el = document.getElementById(`order-tp-profit-${orderId}`);
if(!el) return;
const v = snap && snap.reward_at_tp_usdt;
const n = v != null && v !== "" ? Number(v) : NaN;
if(Number.isFinite(n)){
el.innerText = `${n.toFixed(2)}U`;
el.classList.add("pos-tp-profit");
} else {
el.innerText = "—";
el.classList.remove("pos-tp-profit");
}
}
function paintPriceTrend(el, key, value){
if(!el) return;
const prev = lastPriceMap[key];
el.classList.remove("price-up","price-down","price-flat");
if(typeof prev === "number"){
if(value > prev) el.classList.add("price-up");
else if(value < prev) el.classList.add("price-down");
else el.classList.add("price-flat");
} else {
el.classList.add("price-flat");
}
lastPriceMap[key] = value;
}
function formatOrderMarkDisplay(o){
const row = o || {};
const markRaw = row.exchange_mark_price;
const markNum = Number(markRaw);
const hasMark = markRaw !== null && markRaw !== undefined && markRaw !== "" && Number.isFinite(markNum) && markNum > 0;
if(hasMark){
if(row.exchange_mark_price_display && row.exchange_mark_price_display !== "-") return row.exchange_mark_price_display;
if(row.price_display && row.price_display !== "-") return row.price_display;
return markNum.toFixed(6);
}
if(row.price_display && row.price_display !== "-") return row.price_display;
const px = Number(row.price);
return Number.isFinite(px) && px > 0 ? px.toFixed(6) : "-";
}
function paintOrderMarkAndPnl(orderId, o){
const pEl = document.getElementById(`order-price-${orderId}`);
if(pEl){
const disp = formatOrderMarkDisplay(o);
pEl.innerText = disp;
const markNum = Number(o && o.exchange_mark_price);
const pxNum = Number.isFinite(markNum) && markNum > 0 ? markNum : Number(o && o.price);
paintPriceTrend(pEl, `o-${orderId}`, Number.isFinite(pxNum) ? pxNum : null);
}
const pnlEl = document.getElementById(`order-pnl-${orderId}`);
if(pnlEl){
pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
pnlEl.classList.remove("price-up","price-down","price-flat");
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
else pnlEl.classList.add("price-flat");
}
}
function refreshPriceSnapshot(){
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
const updatedEl = document.getElementById("price-last-updated");
if(data.updated_at && updatedEl){
updatedEl.innerText = data.updated_at;
}
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
TimeCloseUI.paintForceCloseHeader(data.force_close);
}
(data.key_prices || []).forEach(k=>{
const pEl = document.getElementById(`key-price-${k.id}`);
if(pEl){
pEl.innerText = k.price_display || (Number.isFinite(Number(k.price)) ? Number(k.price).toFixed(6) : "-");
paintPriceTrend(pEl, `k-${k.id}`, Number(k.price));
}
const upEl = document.getElementById(`key-up-diff-${k.id}`);
if(upEl){
upEl.innerText = `${formatSigned(k.upper_diff, 4)} (${formatSigned(k.upper_pct, 2)}%)`;
}
const lowEl = document.getElementById(`key-low-diff-${k.id}`);
if(lowEl){
lowEl.innerText = `${formatSigned(k.lower_diff, 4)} (${formatSigned(k.lower_pct, 2)}%)`;
}
const gateEl = document.getElementById(`key-gate-${k.id}`);
if(gateEl){
gateEl.innerText = k.gate_summary || "-";
gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f";
}
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
if(gateMetricEl){
gateMetricEl.innerText = k.gate_metrics || "";
}
});
(data.order_prices || []).forEach(o=>{
paintOrderMarkAndPnl(o.id, o);
const exM = document.getElementById(`order-ex-margin-${o.id}`);
if(exM){
const mv = o.exchange_initial_margin;
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
if(!Number.isNaN(mn)){
exM.innerText = `${mn.toFixed(2)}U`;
} else {
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
exM.innerText = (prc === 0) ? "无仓数据" : "-";
}
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl){
const rr = o.display_rr_ratio != null && o.display_rr_ratio !== "" ? o.display_rr_ratio : o.rr_ratio;
rrEl.innerText = formatRrRatio(rr);
}
paintLatestRiskDisplay(o.id, o);
paintContractsDisplay(o.id, o);
paintTpProfitDisplay(o.id, o);
paintBreakevenBadge(o.id, o);
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
paintPlanTpslDisplay(o.id, o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderForceClose(o);
});
}).catch(()=>{});
}
function refreshOrderDefaults(){
const symbolEl = document.getElementById("order-symbol");
const directionEl = document.getElementById("order-direction");
if(!symbolEl || !directionEl){ return; }
const symbol = (symbolEl.value || "").trim();
const direction = directionEl.value || "long";
if(!symbol || !direction){ return; }
fetch(`/api/order_defaults?symbol=${encodeURIComponent(symbol)}&direction=${encodeURIComponent(direction)}`)
.then(r=>r.json())
.then(data=>{
if(!data.ok){ return; }
if(data.leverage){
paintOrderLeverageHint(data.leverage);
}
if(typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null){
latestAvailableUsdt = Number(data.available_trading_usdt);
const fullEl = document.getElementById("use-full-margin");
const marginEl = document.getElementById("order-margin");
if(fullEl && marginEl && fullEl.checked){
const m = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
marginEl.value = m;
}
}
const px = data.last_price || data.price;
if(px) refreshOrderTpPreview(px);
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
}).catch(()=>{});
}
function paintRealtimePnl(v){
const nodes = document.querySelectorAll('[data-funds-field="realtime-pnl"]');
if(!nodes.length) return;
if(v === null || v === undefined || Number.isNaN(Number(v))){
nodes.forEach((pnlEl) => {
pnlEl.innerText = "—";
pnlEl.classList.remove("pnl-pos", "pnl-neg");
});
return;
}
const n = Number(v);
const sign = n > 0 ? "+" : "";
const text = `${sign}${n.toFixed(2)}U`;
nodes.forEach((pnlEl) => {
pnlEl.innerText = text;
pnlEl.classList.toggle("pnl-pos", n > 0);
pnlEl.classList.toggle("pnl-neg", n < 0);
});
}
let lastRealtimePnl = null;
function updateRealtimePnl(v){
if(v != null && !Number.isNaN(Number(v))){
lastRealtimePnl = Number(v);
paintRealtimePnl(v);
return;
}
if(lastRealtimePnl != null) return;
paintRealtimePnl(v);
}
function sumOrdersFloatPnl(orders){
if(!orders || !orders.length) return null;
let total = 0, found = false;
orders.forEach(o=>{
if(o.float_pnl != null && !Number.isNaN(Number(o.float_pnl))){
total += Number(o.float_pnl);
found = true;
}
});
return found ? total : null;
}
function combineRealtimeFloatPnl(perpTotal, optionsTotal){
let total = 0, found = false;
[perpTotal, optionsTotal].forEach(v=>{
if(v != null && !Number.isNaN(Number(v))){
total += Number(v);
found = true;
}
});
return found ? total : null;
}
function paintRealtimePnlFromSnapshot(data){
if(!data) return;
const perp = data.order_prices && data.order_prices.length
? sumOrdersFloatPnl(data.order_prices)
: null;
const combined = combineRealtimeFloatPnl(perp, data.options_unrealized_pnl);
if(combined !== null || perp !== null || data.options_unrealized_pnl != null){
paintRealtimePnl(combined);
}
}
function formatOptionsFundingLabel(usdc, usdt) {
const parts = [];
if (usdc !== null && usdc !== undefined && Number(usdc) > 0) parts.push(`${Number(usdc).toFixed(2)} USDC`);
if (usdt !== null && usdt !== undefined && Number(usdt) > 0) parts.push(`${Number(usdt).toFixed(2)} USDT`);
return parts.length ? parts.join(" · ") : "—";
}
function setFundsFieldText(field, text){
if(text == null || text === "") return;
document.querySelectorAll(`[data-funds-field="${field}"]`).forEach((el) => {
el.innerText = text;
});
}
function accountSnapshotFundingMissing(data){
if(!data || typeof data !== "object") return true;
const hasFunding = data.funding_usdt != null && data.funding_usdt !== "";
const hasTotal = data.total_funds != null && data.total_funds !== "";
const hasTrading = data.current_capital != null && data.current_capital !== "";
return !hasFunding && !hasTotal && !hasTrading;
}
let accountSnapshotRetryCount = 0;
function applyAccountSnapshot(data){
if(!data || typeof data !== "object") return;
if(data.funding_usdt != null && data.funding_usdt !== ""){
setFundsFieldText("total-capital", `${Number(data.funding_usdt).toFixed(2)}U`);
}
if(data.total_funds != null && data.total_funds !== ""){
setFundsFieldText("total-funds", `${Number(data.total_funds).toFixed(2)}U`);
}
if(data.current_capital != null && data.current_capital !== "" && !Number.isNaN(Number(data.current_capital))){
setFundsFieldText("current-capital", `${Number(data.current_capital).toFixed(2)}U`);
}
if(data.options_funding_usdc != null || data.options_funding_usdt != null){
const optFunding = formatOptionsFundingLabel(data.options_funding_usdc, data.options_funding_usdt);
setFundsFieldText("options-funding-usdc", optFunding);
}
if(data.options_trading_usdc != null || data.options_trading_usdt != null){
const optTrading = formatOptionsFundingLabel(data.options_trading_usdc, data.options_trading_usdt);
setFundsFieldText("options-trading-usdc", optTrading);
}
if(typeof data.unrealized_pnl !== "undefined"){
updateRealtimePnl(data.unrealized_pnl);
}
if(typeof data.total !== "undefined" && data.total !== null){
setFundsFieldText("stat-total", String(data.total));
}
if(typeof data.rate !== "undefined" && data.rate !== null){
setFundsFieldText("stat-rate", `${Number(data.rate)}%`);
}
if(typeof data.profit_loss_ratio !== "undefined"){
setFundsFieldText(
"stat-pl-ratio",
data.profit_loss_ratio != null && data.profit_loss_ratio !== ""
? String(data.profit_loss_ratio)
: "—"
);
}
if(typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null){
latestAvailableUsdt = Number(data.available_trading_usdt);
}
if(data.risk_status){
document.querySelectorAll("#account-risk-badge").forEach((badge) => {
if(window.AccountRiskBadge){
AccountRiskBadge.applyToElement(badge, data.risk_status);
}else{
const st = data.risk_status.status || "normal";
badge.className = "risk-status-badge risk-status-" + st;
badge.innerText = data.risk_status.status_label || "正常";
badge.title = data.risk_status.reason || "";
}
});
}
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
TimeCloseUI.paintForceCloseHeader(data.force_close);
}
let canTradeText = "可开仓";
if(!data.can_trade){
const parts = [];
if(data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason){
parts.push(data.risk_status.reason);
}
const ac = Number(data.active_count || 0);
const max = Number(data.max_active_positions || {{ max_active_positions }});
if(ac >= max) parts.push(`持仓 ${ac}/${max}`);
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
const opens = Number(data.opens_today);
if(hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
if(!parts.length) parts.push(`未到北京时间 {{ reset_hour }}:00`);
else parts.push(`或未到北京时间 {{ reset_hour }}:00`);
canTradeText = `不可开仓(${parts.join(";")})`;
}
const opensToday = Number(data.opens_today);
const hardLim = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
const alertLim = Number(data.daily_open_alert_threshold != null ? data.daily_open_alert_threshold : {{ daily_open_alert_threshold }});
const openCntTxt = !Number.isNaN(opensToday)
? `本交易日开仓 ${opensToday}${hardLim > 0 ? ` / 硬上限 ${hardLim}` : ""}(AI 提醒 ${alertLim})`
: "";
const tip = document.getElementById("order-rule-tip");
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : "";
if(tip){
tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${openCntTxt ? openCntTxt + ";" : ""}${canTradeText}${avail};人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1`;
}
}
function refreshAccountSnapshot(opts){
const options = opts || {};
const qs = options.force ? "?force=1" : "";
fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{
applyAccountSnapshot(data);
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
accountSnapshotRetryCount += 1;
setTimeout(() => refreshAccountSnapshot({ silent: true, force: accountSnapshotRetryCount >= 2 }), 1200 * accountSnapshotRetryCount);
}else if(!accountSnapshotFundingMissing(data)){
accountSnapshotRetryCount = 0;
}
}).catch(()=>{
if(!options.silent && accountSnapshotRetryCount < 3){
accountSnapshotRetryCount += 1;
setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount);
}
});
}
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);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){
const marginEl = document.getElementById("order-margin");
if(marginEl && this.checked && latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)){
marginEl.value = Math.max(latestAvailableUsdt * {{ full_margin_buffer_ratio }}, 0).toFixed(2);
}
});
}
const sltpModeEl = document.getElementById("sltp-mode");
function toggleSltpMode(){
const mode = sltpModeEl ? sltpModeEl.value : "fixed_rr";
const slEl = document.getElementById("order-sl");
const tpEl = document.getElementById("order-tp");
const fixedRrEl = document.getElementById("order-fixed-rr");
const slPctEl = document.getElementById("order-sl-pct");
const tpPctEl = document.getElementById("order-tp-pct");
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
const pct = mode === "pct";
const fixed = mode === "fixed_rr";
slEl.style.display = pct ? "none" : "";
tpEl.style.display = (pct || fixed) ? "none" : "";
if(fixedRrEl) fixedRrEl.style.display = fixed ? "" : "none";
slEl.required = !pct;
tpEl.required = !pct && !fixed;
if(fixedRrEl) fixedRrEl.required = fixed;
slPctEl.style.display = pct ? "" : "none";
tpPctEl.style.display = pct ? "" : "none";
slPctEl.required = pct;
tpPctEl.required = pct;
refreshOrderTpPreview();
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
}
if(sltpModeEl){
sltpModeEl.addEventListener("change", toggleSltpMode);
loadFixedRrPref();
toggleSltpMode();
}
if(window.ManualOrderRrPreview){
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
}
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
const el = document.getElementById(id);
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
if(el) el.addEventListener("change", function(){ refreshOrderTpPreview(); });
});
refreshAccountSnapshot();
if (window.AccountRiskBadge) AccountRiskBadge.startTicker();
const _journalFormEl = document.getElementById("journal-form");
if(_journalFormEl){
_journalFormEl.addEventListener("submit", function(ev){
if(!validateJournalEntryReason()) ev.preventDefault();
});
}
const addOrderForm = document.getElementById("add-order-form");
if(addOrderForm){
addOrderForm.addEventListener("submit", function(ev){
if(addOrderForm.dataset.rrOk === "1"){
addOrderForm.dataset.rrOk = "0";
return;
}
ev.preventDefault();
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
const direction = (document.getElementById("order-direction")||{}).value || "long";
const mode = (document.getElementById("sltp-mode")||{}).value || "fixed_rr";
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
if(mode === "fixed_rr"){
saveFixedRrPref();
const rr = Number((document.getElementById("order-fixed-rr")||{}).value);
if(!Number.isFinite(rr) || rr <= 0){
alert("请填写正数盈亏比");
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
if(rejectManualOrderRr(rr)){
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
return;
}
allowManualOrderSubmit(addOrderForm);
return;
}
if(mode === "pct"){
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
const rr = calcClientRrFromPct(
(document.getElementById("order-sl-pct")||{}).value,
(document.getElementById("order-tp-pct")||{}).value
);
if(rejectManualOrderRr(rr)){
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
return;
}
allowManualOrderSubmit(addOrderForm);
return;
}
const sl = Number((document.getElementById("order-sl")||{}).value);
const tp = Number((document.getElementById("order-tp")||{}).value);
let entry = sl;
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
if(!symbol){
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
return;
}
allowManualOrderSubmit(addOrderForm);
return;
}
fetch(`/api/order_defaults?symbol=${encodeURIComponent(symbol)}&direction=${encodeURIComponent(direction)}`)
.then(r=>r.json())
.then(data=>{
const px = data.last_price || data.price;
if(px) entry = Number(px);
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
return;
}
allowManualOrderSubmit(addOrderForm);
})
.catch(()=>{
alert("无法校验盈亏比,请稍后重试");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
});
});
}
refreshOrderDefaults();
if(typeof initOrderEntryModelSelect === "function") initOrderEntryModelSelect();
refreshPriceSnapshotConditional();
function refreshPriceSnapshotConditional(){
const page = document.body.getAttribute("data-page") || "";
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
const updatedEl = document.getElementById("price-last-updated");
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
TimeCloseUI.paintForceCloseHeader(data.force_close);
}
if(page === "key_monitor"){
(data.key_prices || []).forEach(k=>{
const pEl = document.getElementById(`key-price-${k.id}`);
if(pEl){ pEl.innerText = k.price_display || (Number.isFinite(Number(k.price)) ? Number(k.price).toFixed(6) : "-"); paintPriceTrend(pEl, `k-${k.id}`, Number(k.price)); }
const upEl = document.getElementById(`key-up-diff-${k.id}`);
if(upEl) upEl.innerText = `${formatSigned(k.upper_diff, 4)} (${formatSigned(k.upper_pct, 2)}%)`;
const lowEl = document.getElementById(`key-low-diff-${k.id}`);
if(lowEl) lowEl.innerText = `${formatSigned(k.lower_diff, 4)} (${formatSigned(k.lower_pct, 2)}%)`;
const gateEl = document.getElementById(`key-gate-${k.id}`);
if(gateEl){ gateEl.innerText = k.gate_summary || "-"; gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f"; }
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
if(typeof paintKeyMonitorSummary === "function") paintKeyMonitorSummary(k.id, k);
});
}
const tradePage = page === "trade" || !!document.querySelector("[id^='order-price-']");
if(tradePage){
(data.order_prices || []).forEach(o=>{
paintOrderMarkAndPnl(o.id, o);
paintLatestRiskDisplay(o.id, o);
paintContractsDisplay(o.id, o);
paintTpProfitDisplay(o.id, o);
const exM = document.getElementById(`order-ex-margin-${o.id}`);
if(exM){
const mv = o.exchange_initial_margin;
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
if(!Number.isNaN(mn)) exM.innerText = `${mn.toFixed(2)}U`;
else { const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null; exM.innerText = (prc === 0) ? "无仓数据" : "-"; }
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
paintBreakevenBadge(o.id, o);
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
paintPlanTpslDisplay(o.id, o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderForceClose(o);
const holdEl = document.getElementById(`order-hold-duration-${o.id}`);
if(holdEl && o.opened_at_ms != null && o.opened_at_ms !== ""){
holdEl.setAttribute("data-order-opened-ms", String(o.opened_at_ms));
}
});
tickOrderHoldDurations();
}
if(data.order_prices && data.order_prices.length){
paintRealtimePnlFromSnapshot(data);
} else if (typeof data.options_unrealized_pnl !== "undefined") {
paintRealtimePnlFromSnapshot(data);
}
}).catch(()=>{});
}
function formatLiveHoldDurationFromMs(openedMs, nowMs){
if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—";
const ms = Number(openedMs);
const now = (nowMs != null) ? nowMs : Date.now();
let sec = Math.floor((now - ms) / 1000);
if(sec < 0) sec = 0;
if(sec <= 0) return "0分钟";
const d = Math.floor(sec / 86400); sec %= 86400;
const h = Math.floor(sec / 3600); sec %= 3600;
const m = Math.floor(sec / 60);
const parts = [];
if(d) parts.push(`${d}`);
if(h) parts.push(`${h}小时`);
if(m || !parts.length) parts.push(`${m}分钟`);
return parts.join("");
}
function tickOrderHoldDurations(){
const now = Date.now();
document.querySelectorAll(".order-hold-duration[data-order-opened-ms]").forEach(el=>{
const ms = Number(el.getAttribute("data-order-opened-ms"));
if(!Number.isFinite(ms) || ms <= 0) return;
el.textContent = formatLiveHoldDurationFromMs(ms, now);
});
}
setInterval(tickOrderHoldDurations, 1000);
tickOrderHoldDurations();
(function startInstanceDataRefresh(){
const embedShell = document.body && document.body.getAttribute("data-embed-shell") === "1";
const balanceMs = Number(document.body.dataset.balanceRefreshMs || 30000);
const priceMs = Number(document.body.dataset.priceRefreshMs || 5000);
if(embedShell){
setInterval(function(){
if(window.InstanceLive && InstanceLive.isConnected && InstanceLive.isConnected()) return;
refreshAccountSnapshot();
}, balanceMs);
setInterval(function(){
if(window.InstanceLive && InstanceLive.isConnected && InstanceLive.isConnected()) return;
refreshPriceSnapshotConditional();
}, priceMs);
return;
}
setInterval(refreshAccountSnapshot, balanceMs);
setInterval(refreshPriceSnapshotConditional, priceMs);
})();
</script>