467f4f092f
Co-authored-by: Cursor <cursoragent@cursor.com>
1642 lines
72 KiB
HTML
1642 lines
72 KiB
HTML
<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(r.status === 504
|
||
? "HTTP 504(网关超时:Nginx 等反代切断,通常 AI 仍在生成;已加长超时后请重试)"
|
||
: ("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(r.status === 504
|
||
? "HTTP 504(网关超时:Nginx 等反代切断,通常 AI 仍在生成;已加长超时后请重试)"
|
||
: ("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}`);
|
||
if(slText) slText.innerText = formatExTpslLine('sl', data.sl);
|
||
if(tpText) tpText.innerText = formatExTpslLine('tp', data.tp);
|
||
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, unit, spotPx){
|
||
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 u = String(unit || lastRealtimePnlUnit || "U").toUpperCase() || "U";
|
||
lastRealtimePnlUnit = u;
|
||
if (spotPx != null && Number.isFinite(Number(spotPx)) && Number(spotPx) > 0) {
|
||
lastRealtimePnlSpotPx = Number(spotPx);
|
||
}
|
||
const sign = n > 0 ? "+" : "";
|
||
let text;
|
||
let tone = n;
|
||
if (u === "ETH" || u === "BTC") {
|
||
// 顶栏实时盈亏只显示 U(按指数/现货换算),不展示币数量
|
||
const px = Number(spotPx != null ? spotPx : lastRealtimePnlSpotPx);
|
||
if (Number.isFinite(px) && px > 0) {
|
||
const uu = n * px;
|
||
tone = uu;
|
||
const uAbs = Math.abs(uu).toFixed(2);
|
||
const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
|
||
text = `${uSign}${uAbs}U`;
|
||
} else {
|
||
text = "—";
|
||
tone = 0;
|
||
}
|
||
} else {
|
||
text = `${sign}${n.toFixed(2)}U`;
|
||
}
|
||
nodes.forEach((pnlEl) => {
|
||
pnlEl.innerText = text;
|
||
pnlEl.classList.toggle("pnl-pos", tone > 0);
|
||
pnlEl.classList.toggle("pnl-neg", tone < 0);
|
||
});
|
||
}
|
||
let lastRealtimePnl = null;
|
||
let lastRealtimePnlUnit = "U";
|
||
let lastRealtimePnlSpotPx = null;
|
||
function updateRealtimePnl(v, unit, spotPx){
|
||
if(v != null && !Number.isNaN(Number(v))){
|
||
lastRealtimePnl = Number(v);
|
||
if (unit) lastRealtimePnlUnit = String(unit).toUpperCase();
|
||
paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
|
||
return;
|
||
}
|
||
if(lastRealtimePnl != null) return;
|
||
paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
|
||
}
|
||
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 opt = data.options_unrealized_pnl;
|
||
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
|
||
const underly = String(data.options_underly || "ETH").toUpperCase() || "ETH";
|
||
const spotPx = data.options_index_px != null ? Number(data.options_index_px) : null;
|
||
if (coinMode) {
|
||
if (opt != null && !Number.isNaN(Number(opt))) {
|
||
if (perp != null && Math.abs(Number(perp)) >= 0.005) {
|
||
// 顶栏只显示合计 U:永续 U + 期权币盈亏×指数
|
||
let totalU = Number(perp);
|
||
if (Number.isFinite(spotPx) && spotPx > 0) {
|
||
totalU += Number(opt) * spotPx;
|
||
}
|
||
const uAbs = Math.abs(totalU).toFixed(2);
|
||
const uSign = totalU < 0 ? "-" : totalU > 0 ? "+" : "";
|
||
const text = `${uSign}${uAbs}U`;
|
||
lastRealtimePnl = Number(opt);
|
||
lastRealtimePnlUnit = underly;
|
||
lastRealtimePnlSpotPx = spotPx;
|
||
document.querySelectorAll('[data-funds-field="realtime-pnl"]').forEach((pnlEl) => {
|
||
pnlEl.innerText = text;
|
||
pnlEl.classList.toggle("pnl-pos", totalU > 0);
|
||
pnlEl.classList.toggle("pnl-neg", totalU < 0);
|
||
});
|
||
return;
|
||
}
|
||
paintRealtimePnl(opt, underly, spotPx);
|
||
return;
|
||
}
|
||
// 期权盈亏拉取失败时保留上次有效值,避免顶栏闪成 0/—
|
||
if (lastRealtimePnl != null && (lastRealtimePnlUnit === "ETH" || lastRealtimePnlUnit === "BTC")) {
|
||
return;
|
||
}
|
||
}
|
||
const combined = combineRealtimeFloatPnl(perp, opt);
|
||
if(combined !== null || perp !== null || opt != null){
|
||
paintRealtimePnl(combined, "U");
|
||
}
|
||
}
|
||
|
||
function formatOptionsFundingLabel(usdc, usdt, eth, marginMode, underly) {
|
||
if (usdc === null || usdc === undefined || usdc === "") return "—";
|
||
const n = Number(usdc);
|
||
if (Number.isNaN(n)) return "—";
|
||
return `${n.toFixed(2)} USDC`;
|
||
}
|
||
function formatTradingAccountLabel(usdt, eth, btc, marginMode) {
|
||
// 缺省按 usdc(xx.xxU):Gate/Binance 快照无 options_margin_mode;OKX 会显式下发.
|
||
const mode = String(marginMode || "usdc").toLowerCase();
|
||
if (mode !== "coin") {
|
||
if (usdt === null || usdt === undefined || usdt === "") return "—";
|
||
const n = Number(usdt);
|
||
if (Number.isNaN(n)) return "—";
|
||
return `${n.toFixed(2)}U`;
|
||
}
|
||
const parts = [];
|
||
if (usdt !== null && usdt !== undefined && usdt !== "") {
|
||
const n = Number(usdt);
|
||
if (!Number.isNaN(n)) parts.push(`${n.toFixed(2)} USDT`);
|
||
}
|
||
const pushCoin = (v, ccy) => {
|
||
if (v === null || v === undefined || v === "") return;
|
||
const n = Number(v);
|
||
if (Number.isNaN(n) || !(n >= (ccy === "BTC" ? 1e-7 : 1e-6))) return;
|
||
const txt = String(n.toFixed(6)).replace(/\.?0+$/, "");
|
||
parts.push(`${txt || "0"} ${ccy}`);
|
||
};
|
||
pushCoin(eth, "ETH");
|
||
pushCoin(btc, "BTC");
|
||
return parts.length ? parts.join("\n") : "—";
|
||
}
|
||
|
||
function setFundsFieldText(field, text){
|
||
if(text == null || text === "") return;
|
||
document.querySelectorAll(`[data-funds-field="${field}"]`).forEach((el) => {
|
||
el.innerText = text;
|
||
});
|
||
}
|
||
function applyPerpFundsVisibility(show){
|
||
const on = show !== false;
|
||
document.querySelectorAll("[data-perp-funds='1']").forEach((el) => {
|
||
el.style.display = on ? "" : "none";
|
||
});
|
||
}
|
||
function applyOptionsFundsVisibility(show){
|
||
document.querySelectorAll("[data-options-funds='1']").forEach((el) => {
|
||
el.style.display = show ? "" : "none";
|
||
});
|
||
}
|
||
function accountSnapshotFundingMissing(data){
|
||
if(!data || typeof data !== "object") return true;
|
||
if(data.show_perp_funds === false){
|
||
const hasTotal = data.total_funds != null && data.total_funds !== "";
|
||
const hasOpt =
|
||
data.options_funding_usdc != null ||
|
||
data.options_funding_usdt != null ||
|
||
data.options_trading_usdc != null ||
|
||
data.options_trading_usdt != null;
|
||
return !hasTotal && !hasOpt;
|
||
}
|
||
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;
|
||
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
|
||
if(typeof data.show_perp_funds !== "undefined"){
|
||
applyPerpFundsVisibility(data.show_perp_funds !== false || coinMode);
|
||
} else if (coinMode) {
|
||
applyPerpFundsVisibility(true);
|
||
}
|
||
applyOptionsFundsVisibility(!coinMode);
|
||
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",
|
||
formatTradingAccountLabel(
|
||
data.current_capital,
|
||
data.options_trading_eth,
|
||
data.options_trading_btc,
|
||
data.options_margin_mode
|
||
)
|
||
);
|
||
}
|
||
if(!coinMode && (data.options_funding_usdc != null || data.options_funding_usdt != null || data.options_funding_eth != null)){
|
||
const optFunding = formatOptionsFundingLabel(
|
||
data.options_funding_usdc,
|
||
data.options_funding_usdt,
|
||
data.options_funding_eth,
|
||
data.options_margin_mode,
|
||
data.options_underly
|
||
);
|
||
setFundsFieldText("options-funding-usdc", optFunding);
|
||
}
|
||
if(!coinMode && (data.options_trading_usdc != null || data.options_trading_usdt != null || data.options_trading_eth != null)){
|
||
const optTrading = formatOptionsFundingLabel(
|
||
data.options_trading_usdc,
|
||
data.options_trading_usdt,
|
||
data.options_trading_eth,
|
||
data.options_margin_mode,
|
||
data.options_underly
|
||
);
|
||
setFundsFieldText("options-trading-usdc", optTrading);
|
||
}
|
||
if(typeof data.unrealized_pnl !== "undefined" || typeof data.options_unrealized_pnl !== "undefined"){
|
||
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
|
||
if (coinMode && data.options_unrealized_pnl != null && !Number.isNaN(Number(data.options_unrealized_pnl))) {
|
||
paintRealtimePnl(
|
||
data.options_unrealized_pnl,
|
||
String(data.options_underly || "ETH").toUpperCase() || "ETH",
|
||
data.options_index_px
|
||
);
|
||
} else if (typeof data.unrealized_pnl !== "undefined") {
|
||
updateRealtimePnl(data.unrealized_pnl, "U");
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
if(window.OpenSubmitGate) OpenSubmitGate.apply(data);
|
||
let canTradeText = "可开仓";
|
||
if(!data.can_trade){
|
||
const parts = [];
|
||
if(data.open_block_note) parts.push(data.open_block_note);
|
||
if(data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason){
|
||
if(!data.open_block_note || data.open_block_note.indexOf(data.risk_status.reason) < 0){
|
||
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(data.open_guard_blocks_now) parts.push(`未到北京时间 ${data.reset_hour||{{ reset_hour }}}:00`);
|
||
canTradeText = parts.length ? `不可开仓(${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 setOmFieldVisible(inputEl, show){
|
||
if(!inputEl) return;
|
||
inputEl.style.display = show ? "" : "none";
|
||
const wrap = inputEl.closest(".om-field");
|
||
if(wrap) wrap.style.display = show ? "" : "none";
|
||
}
|
||
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";
|
||
setOmFieldVisible(slEl, !pct);
|
||
setOmFieldVisible(tpEl, !(pct || fixed));
|
||
setOmFieldVisible(fixedRrEl, fixed);
|
||
slEl.required = !pct;
|
||
tpEl.required = !pct && !fixed;
|
||
if(fixedRrEl) fixedRrEl.required = fixed;
|
||
setOmFieldVisible(slPctEl, pct);
|
||
setOmFieldVisible(tpPctEl, pct);
|
||
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 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>
|