Files
crypto_monitor/crypto_monitor_okx/templates/index.html
T
2026-06-14 00:42:21 +08:00

2237 lines
126 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.
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=6"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1">
<meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
<link rel="icon" href="/static/icons/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
<link rel="manifest" href="/static/icons/manifest.webmanifest">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px 20px}
.container{width:100%;max-width:min(1440px,94vw);margin:0 auto;padding:0 clamp(8px,1.5vw,20px)}
.header{display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:12px}
.header h1{font-size:1.75rem;color:#dbe4ff;text-align:center;line-height:1.25}
.exchange-tag{font-size:.82rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:5px 14px;border-radius:999px;letter-spacing:.06em}
.top-nav{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:12px}
.top-nav a{padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a;color:#8fc8ff;text-decoration:none}
.top-nav a.active{background:#2a3f6c;color:#dbe4ff}
.stat-box{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-bottom:16px;align-items:stretch}
.stat-item{min-width:0;min-height:76px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:6px;background:#151a2a;padding:12px 10px;border-radius:10px;text-align:center;border:1px solid #2a3152}
.stat-item .label{font-size:.8rem;color:#aaa;line-height:1.25;max-width:100%}
.stat-item .value{font-size:1.25rem;font-weight:600;color:#fff;line-height:1.3;min-height:1.35em;display:flex;align-items:center;justify-content:center}
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150}
.full{grid-column:1/-1}
.card h2{font-size:1rem;margin-bottom:10px;color:#d4d9ff}
.form-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center}
.form-row > input:not([type=checkbox]):not([type=radio]),.form-row > select{flex:0 1 auto;width:10rem;max-width:200px;min-width:7rem}
#add-order-form #sltp-mode{min-width:12.5rem;max-width:16rem;width:auto}
.form-row > button,.form-row > label{flex:0 0 auto}
.form-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
/* 复盘表单:长下拉文案需可收缩,否则会撑破四列网格 */
.journal-card .form-grid{grid-template-columns:repeat(4,minmax(0,1fr))}
.journal-card .form-grid > input,
.journal-card .form-grid > select{
min-width:0;
width:100%;
max-width:100%;
}
.journal-card .form-grid select[name="entry_reason"]{
grid-column:1/-1;
font-size:.8rem;
line-height:1.35;
}
.journal-card .form-grid input[name="entry_reason_custom"]{
grid-column:1/-1;
font-size:.8rem;
}
input,select,button,textarea{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff;font-size:.88rem;outline:none}
button{background:linear-gradient(90deg,#4285f4,#7b42ff);border:none;cursor:pointer}
.list{display:flex;flex-direction:column;gap:8px;margin-top:8px;max-height:240px;overflow:auto}
.list-item{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:9px;background:#1a2034;border:1px solid #2a3150;border-radius:8px}
.btn-del{padding:5px 9px;background:#2f2134;color:#ff7b7b;border-radius:8px;text-decoration:none;font-size:.8rem}
.rule-tip{font-size:.8rem;color:#95a2c2;margin-bottom:8px}
table{width:100%;border-collapse:collapse}
th,td{padding:8px;text-align:left;border-bottom:1px solid #25253b;font-size:.85rem}
th{color:#a9a9ff}
.badge{padding:2px 6px;border-radius:6px;font-size:.72rem}
.profit{background:#1e332f;color:#4cd97f}
.loss{background:#331e24;color:#ff6666}
.miss{background:#29241e;color:#eac147}
.direction{background:#1e2533;color:#4cc2ff}
.direction-long{background:#1e332f;color:#4cd97f}
.direction-short{background:#331e24;color:#ff6666}
.pnl-profit{color:#4cd97f;font-weight:600}
.pnl-loss{color:#ff6666;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
form.is-form-submitting{opacity:.88;pointer-events:none}
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
.price-up{color:#4cd97f}
.price-down{color:#ff6666}
.price-flat{color:#cfd3ef}
.panel-list{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.panel-item{background:#141423;border:1px solid #24243b;border-radius:10px;padding:10px;max-height:260px;overflow:auto}
.entry{border-bottom:1px solid #2b2b43;padding:8px 0}
.entry:last-child{border-bottom:none}
.table-del{padding:4px 8px;background:#2f2134;color:#ff7b7b;border:none;border-radius:6px;cursor:pointer;font-size:.78rem}
.mood-grid{display:flex;gap:10px;flex-wrap:wrap;font-size:.82rem;color:#d7d7ea}
.mood-grid label{display:flex;align-items:center;gap:3px}
.screenshot{width:100px;border-radius:6px;cursor:pointer;margin-top:6px}
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1210}
.modal img{max-width:90%;max-height:90%;border-radius:8px}
.detail-modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1200;padding:20px}
.detail-modal .panel{width:min(92vw,980px);max-height:88vh;overflow:auto;background:#121726;border:1px solid #2a3150;border-radius:10px;padding:14px}
.detail-modal .panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}
.detail-modal .panel-title{font-size:1rem;color:#dbe4ff}
.detail-modal .panel-close{padding:6px 10px;background:#2f2134;color:#ffb2b2;border:none;border-radius:8px;cursor:pointer}
.detail-modal .panel-body{white-space:pre-wrap;line-height:1.5;font-size:.86rem;color:#e5e9ff}
.detail-modal .panel-image{margin-top:10px;max-width:min(100%,680px);border-radius:8px;cursor:pointer;border:1px solid #2a3150}
.detail-modal .panel-actions{display:flex;gap:8px;align-items:center;flex-shrink:0}
.detail-modal .panel-fs{padding:6px 10px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem}
.detail-modal.fullscreen{padding:10px}
.detail-modal.fullscreen .panel{width:100%;height:100%;max-width:none;max-height:none;display:flex;flex-direction:column;overflow:hidden}
.detail-modal.fullscreen .panel-body{flex:1;overflow:auto;min-height:0;font-size:.9rem}
.ai-result-wrap{margin-top:8px}
.ai-result-toolbar{display:flex;gap:8px;margin-top:6px}
.ai-result-toolbar .btn-fs{padding:4px 10px;font-size:.78rem;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:6px;cursor:pointer}
.table-wrap{overflow-x:auto}
.dual-panel-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
.dual-panel-grid .card{height:100%;display:flex;flex-direction:column}
.panel-scroll{flex:1;min-height:280px;max-height:420px;overflow:auto}
.records-card{grid-column:1/-1}
.review-card{grid-column:1/-1}
.review-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap}
.review-card-head h2{margin:0}
.review-card-fs-btn{padding:6px 12px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;white-space:nowrap}
.review-card-fs-btn:hover{filter:brightness(1.08)}
body.review-card-fullscreen-open{overflow:hidden}
.review-card.is-fullscreen{
position:fixed;inset:12px;z-index:1100;margin:0;
width:auto !important;max-width:none;height:auto;
overflow:auto;display:flex;flex-direction:column;
box-shadow:0 12px 48px rgba(0,0,0,.55);
}
.review-card.is-fullscreen .panel-list{flex:1;min-height:320px}
.review-card.is-fullscreen .panel-item{max-height:none;height:auto;min-height:280px}
.review-card.is-fullscreen .ai-result{max-height:min(36vh, 320px)}
@media (max-width: 1200px){
.stat-box{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}
}
@media (min-width: 1440px){
.panel-scroll,.pos-list{max-height:420px}
.records-card .table-wrap{max-height:620px;overflow:auto}
}
@media (min-width: 2200px){
.container{max-width:min(1720px,90vw)}
}
@media (min-width: 2560px){
.container{max-width:min(1860px,88vw)}
.dual-panel-grid{gap:18px}
}
@media (min-width: 3000px){
.container{max-width:min(1980px,86vw)}
.pos-grid{grid-template-columns:repeat(4,minmax(0,1fr))}
}
@media (max-width: 1100px){
.grid{grid-template-columns:1fr}
.dual-panel-grid{grid-template-columns:1fr}
.records-card,.review-card{grid-column:auto}
.panel-list{grid-template-columns:1fr}
}
@media (max-width: 960px){
body{padding:10px}
.form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
.stat-box{grid-template-columns:repeat(2,minmax(0,1fr))}
}
.stats-detail{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-top:10px}
.stats-detail .stat-item{min-width:0;min-height:0;display:block;text-align:left;padding:10px 12px;align-items:stretch;gap:4px}
.stats-detail .stat-item .value{min-height:0;display:block;font-size:1.05rem}
.stats-detail .stat-item .label{font-size:.75rem}
.stats-detail .stat-item .value{font-size:1.05rem;word-break:break-all}
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
.export-bar a:hover{background:#1f2740}
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
.key-history .list{max-height:200px}
.pos-section{margin-top:12px}
.pos-section-title{font-size:.82rem;color:#8892b0;margin-bottom:8px;font-weight:500}
.pos-list{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto}
.dual-panel-grid .pos-list-live{max-height:none;overflow:visible;flex:1 1 auto}
.dual-panel-grid .panel-scroll.pos-list-live{max-height:none;overflow:visible}
.pos-card{background:#141923;border:1px solid #2a3348;border-radius:10px;padding:12px 14px}
.pos-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.pos-meta{font-size:.74rem;color:#8b95a8;line-height:1.45;margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:4px 0}
.pos-meta-item{display:inline-flex;align-items:center}
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
.pos-meta-on{color:#6eb5ff}
.pos-meta-off{color:#7d8799}
.pos-breakeven-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:600;background:#1a3d2e;color:#4cd97f}
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
.pos-side-long{background:#253a6e;color:#6eb5ff}
.pos-side-short{background:#4a2230;color:#ff8a8a}
.pos-head-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
.pos-entrust-btn{padding:6px 12px;background:#2a4a7a;color:#8fc8ff;border:none;border-radius:8px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap}
.pos-entrust-btn:hover{background:#355d96}
.pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap;border:none;cursor:pointer;display:inline-block}
.pos-close-btn:hover{background:#d66565;color:#fff}
.pos-ex-orders{margin-top:10px;padding-top:10px;border-top:1px dashed #2a3348}
.pos-ex-orders-title{font-size:.74rem;color:#7d8799;margin-bottom:6px}
.pos-ex-order-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.78rem;color:#c5cce0;margin-top:5px}
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
.tpsl-modal-backdrop.open{display:flex}
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
.tpsl-modal h3{margin:0 0 12px;font-size:1rem;color:#fff}
.tpsl-modal .form-row{margin-bottom:10px}
.tpsl-modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
.tpsl-modal-actions button{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem}
.tpsl-modal-submit{background:#2d6a4f;color:#fff}
.tpsl-modal-cancel{background:#3a3f52;color:#ddd}
.pos-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;margin-bottom:12px}
.pos-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
.pos-label{font-size:.72rem;color:#7d8799}
.pos-value{font-size:.88rem;color:#e8ecf4;font-weight:500;line-height:1.25}
.pos-val-dash{opacity:.75;color:#8b95a8}
.pos-value.price-up{color:#4cd97f}
.pos-value.price-down{color:#ff6666}
.pos-value.price-flat{color:#e8ecf4}
.pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689}
.pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
@media (max-width:520px){.pos-grid{grid-template-columns:repeat(2,1fr)}}
.stats-card{grid-column:1/-1;margin-top:14px}
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
.stats-card.collapsed .stats-content{display:none}
.stats-period-block{margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid #2a3150}
.stats-period-block:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=14">
</head>
<body data-page="{{ page }}">
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
<div class="sub">{{ s.range_label }}</div>
<div class="stats-detail">
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}{{ funds_fmt(s.worst_day_pnl) }}U{% else %}-{% endif %}</div></div>
</div>
</div>
{% endmacro %}
<div class="container">
<div class="header">
<h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
</div>
<div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/strategy/records" class="{% if page == 'strategy_records' %}active{% endif %}">策略交易记录</a>
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
</div>
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
<div class="list-window-bar">
<span style="color:#cfd3ef">列表筛选(<strong>UTC</strong>,默认当日):{{ list_window.label }}</span>
<label>预设
<select id="win-preset-select" onchange="toggleListWindowCustom()">
<option value="utc_today" {% if list_window.preset == 'utc_today' %}selected{% endif %}>UTC 当日</option>
<option value="utc_last24h" {% if list_window.preset == 'utc_last24h' %}selected{% endif %}>近 24 小时</option>
<option value="utc_last7d" {% if list_window.preset == 'utc_last7d' %}selected{% endif %}>近 7 天</option>
<option value="custom" {% if list_window.preset == 'custom' %}selected{% endif %}>自定义</option>
</select>
</label>
<span id="win-custom-range" style="{% if list_window.preset != 'custom' %}display:none{% endif %}">
<label>起(UTC) <input type="datetime-local" id="win-from-utc" value="{{ list_window.start_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
<label>止(UTC) <input type="datetime-local" id="win-to-utc" value="{{ list_window.end_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
</span>
<button type="button" style="padding:6px 12px" onclick="applyListWindow()">应用</button>
<span style="color:#8892b0;font-size:.75rem">统计页仍按北京时间 {{ stats_bundle.stats_reset_hour|default(reset_hour) }}:00 切日</span>
</div>
<div class="export-bar instance-desktop-only">
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSVUTF-8;交易记录含开仓类型列,复盘单独导出):</span>
<a href="/export/trade_records">交易记录</a>
<a href="/export/journal_entries">复盘记录</a>
<a href="/export/key_monitors">关键位(当前)</a>
<a href="/export/key_monitor_history">关键位历史</a>
</div>
<div class="stat-box instance-desktop-only">
<div class="stat-item"><div class="label">交易所</div><div class="value">{{ exchange_display }}</div></div>
<div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div>
<div class="stat-item"><div class="label">错过次数</div><div class="value">{{ miss_count }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div>
<div class="stat-item"><div class="label">资金账户(USDT)</div><div class="value" id="total-capital">{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}</div></div>
<div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div>
<div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ funds_fmt(current_capital) }}U</div></div>
</div>
<div class="rule-tip">实时价格更新:<span id="price-last-updated">--</span>(北京时间 UTC+8</div>
<div class="rule-tip" id="open-guard-bar" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#cfd3ef">
<input type="checkbox" id="allow-open-before-reset" {% if not open_guard_enabled %}checked{% endif %}>
允许北京时间 {{ reset_hour }}:00 前开仓(斐波成交登记、人工下单)
</label>
<span id="open-guard-status" style="color:#8892b0;font-size:.75rem">
{% if open_guard_enabled %}已限制:{{ reset_hour }}:00 前不可开仓{% else %}已放开:{{ reset_hour }}:00 前允许开仓{% endif %}
</span>
</div>
<div class="grid">
{% if page == 'key_monitor' %}
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">实盘下单监控</h2>
{% if focus_order_id %}
<a href="/order_focus?order_id={{ focus_order_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(100根)</a>
{% else %}
<span class="btn-del" style="background:#2f2f44;color:#9aa;cursor:not-allowed">暂无持仓可放大</span>
{% endif %}
</div>
{% include 'order_monitor_rule_tips_okx.html' %}
<details class="tip-collapse transfer-rule-collapse">
<summary class="tip-collapse-summary">划转规则说明</summary>
<div class="tip-collapse-body rule-tip">
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;将 {{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }}<strong>持仓中不划转</strong>并微信通知)
</div>
</details>
<form action="/manual_transfer" method="post" class="form-row">
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
<select name="from_account">
<option value="funding" {% if auto_transfer_from == 'funding' %}selected{% endif %}>from: funding</option>
<option value="swap" {% if auto_transfer_from == 'swap' %}selected{% endif %}>from: swap</option>
<option value="spot" {% if auto_transfer_from == 'spot' %}selected{% endif %}>from: spot</option>
</select>
<select name="to_account">
<option value="swap" {% if auto_transfer_to == 'swap' %}selected{% endif %}>to: swap</option>
<option value="funding" {% if auto_transfer_to == 'funding' %}selected{% endif %}>to: funding</option>
<option value="spot" {% if auto_transfer_to == 'spot' %}selected{% endif %}>to: spot</option>
</select>
<button type="submit">手动划转</button>
</form>
<form id="add-order-form" action="/add_order" method="post" class="form-row">
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select id="order-direction" name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<option value="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
{% if position_sizing_mode != 'full_margin' %}
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
{% endif %}
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<span id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</span>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
<button type="submit">{{ open_position_button_label }}</button>
</form>
</div>
<div class="card">
<h2 style="margin-bottom:8px">实时持仓</h2>
<div class="panel-scroll pos-list pos-list-live">
{% for o in order %}
<div class="pos-card" id="order-row-{{ o.id }}"
data-monitor-id="{{ o.id }}"
data-symbol="{{ o.symbol }}"
data-direction="{{ o.direction }}"
data-plan-sl="{% if o.stop_loss %}{{ price_fmt(o.symbol, o.stop_loss) }}{% endif %}"
data-plan-tp="{% if o.take_profit %}{{ price_fmt(o.symbol, o.take_profit) }}{% endif %}"
data-entry="{% if o.trigger_price %}{{ price_fmt(o.symbol, o.trigger_price) }}{% endif %}">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
{% if o.time_close_enabled %}
<span class="pos-symbol-time-close pos-meta-on pos-time-close-meta" id="order-time-close-wrap-{{ o.id }}"
data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
</span>
{% endif %}
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<div class="pos-head-actions">
<button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button>
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
</div>
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
<div class="pos-cell">
<span class="pos-label">成交价</span>
<span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止损</span>
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止盈</span>
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈亏比</span>
<span class="pos-value" id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%g'|format(o.rr_ratio) }}:1{% else %}-:1{% endif %}</span>
</div>
<div class="pos-cell">
<span class="pos-label">标记价</span>
<span class="pos-value" id="order-price-{{ o.id }}">-</span>
</div>
<div class="pos-cell">
<span class="pos-label">浮盈亏</span>
<span class="pos-value" id="order-pnl-{{ o.id }}">-</span>
</div>
</div>
<div class="pos-footer">
<span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span>
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
<span>杠杆: {{ o.leverage or '-' }}x</span>
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
</div>
<div class="pos-ex-orders">
<div class="pos-ex-orders-title">交易所止盈止损</div>
<div class="pos-ex-order-row">
<span class="pos-ex-order-main" id="ex-sl-text-{{ o.id }}">止损:加载中…</span>
<button type="button" class="pos-ex-cancel-btn" id="ex-sl-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'sl')">撤单</button>
</div>
<div class="pos-ex-order-row">
<span class="pos-ex-order-main" id="ex-tp-text-{{ o.id }}">止盈:加载中…</span>
<button type="button" class="pos-ex-cancel-btn" id="ex-tp-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'tp')">撤单</button>
</div>
</div>
</div>
{% else %}
<div class="pos-empty">暂无持仓</div>
{% endfor %}
</div>
</div>
<div id="tpsl-modal" class="tpsl-modal-backdrop" onclick="if(event.target===this)closeTpslEntrustModal()">
<div class="tpsl-modal" onclick="event.stopPropagation()">
<h3 id="tpsl-modal-title">挂止盈止损</h3>
<p style="font-size:.78rem;color:#8892b0;margin:0 0 10px">将先撤销该合约已有 TP/SL,再按下列价格重挂。</p>
<div class="form-row">
<select id="tpsl-modal-mode" onchange="toggleTpslModalMode()">
<option value="price">价格模式</option>
<option value="pct">百分比模式</option>
</select>
</div>
<div class="form-row">
<input id="tpsl-modal-sl" step="any" placeholder="止损价格">
<input id="tpsl-modal-tp" step="any" placeholder="止盈价格">
</div>
<div class="form-row">
<input id="tpsl-modal-sl-pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
<input id="tpsl-modal-tp-pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
</div>
<div class="tpsl-modal-actions">
<button type="button" class="tpsl-modal-cancel" onclick="closeTpslEntrustModal()">取消</button>
<button type="button" class="tpsl-modal-submit" onclick="submitTpslEntrust()">先撤后挂</button>
</div>
</div>
</div>
</div>
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
{% include 'strategy_trading_page.html' %}
{% elif page == 'strategy_records' %}
{% include 'strategy_records_page.html' %}
{% endif %}
{% if page == 'records' %}
<div class="card full records-card">
<h2>交易记录 & 错过机会</h2>
<div class="form-row" style="margin-bottom:10px;gap:8px">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input id="review-mode-toggle" type="checkbox">
修改/核对开关(开启后可编辑关键字段)
</label>
</div>
<div class="table-wrap">
<table>
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损(开仓)</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
{% for r in record %}
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ funds_fmt(r.effective_pnl_amount or 0) }}</span>{% if r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% elif r.display_pnl_source != 'reviewed' %}<span style="font-size:.68rem;color:#8892b0"></span>{% endif %}</td>
<td>
{% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
{% elif effective_result in ["止损","强制清仓","手动平仓"] %}<span class="badge loss">{{ effective_result }}</span>
{% elif effective_result == "时间平仓" %}<span class="badge miss">{{ effective_result }}</span>
{% else %}<span class="badge miss">{{ effective_result }}</span>{% endif %}
</td>
<td>
<button
type="button"
class="table-del"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='fillJournalFromTrade({{ {
"symbol": r.symbol,
"monitor_type": r.monitor_type,
"key_signal_type": r.key_signal_type or "",
"direction": r.direction,
"trigger_price": r.trigger_price,
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"risk_amount": r.risk_amount
}|tojson|safe }})'
>填入复盘</button>
<button
type="button"
class="table-del review-edit-btn"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='editTradeRecordReview({{ {
"id": r.id,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"miss_reason": r.effective_miss_reason,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
disabled
>核对修改</button>
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card miss-card" style="opacity:.78">
<h2>记录错过机会</h2>
<form action="/add_miss" method="post" class="form-row">
<input name="symbol" placeholder="品种" required>
<select name="type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
<select name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<input name="tp" step="0.0001" placeholder="入场价" required>
<input name="sl" step="any" placeholder="止损" required>
<input name="tgt" step="any" placeholder="止盈" required>
<input name="reason" placeholder="错过原因" required>
<button type="submit">记录</button>
</form>
</div>
<div class="card journal-card">
<h2>交易复盘记录上传(含截图)</h2>
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
<input type="hidden" name="exit_price_hint" id="exit-price-hint">
<input type="hidden" name="direction_hint" id="direction-hint">
<div class="form-grid">
<input type="datetime-local" name="open_datetime" required>
<input type="datetime-local" name="close_datetime" required>
<input name="coin" placeholder="BTC" required>
<input name="tf" placeholder="5m" required>
<input name="pnl" placeholder="盈亏(U)" required>
<select name="entry_reason" id="journal-entry-reason" required title="固定五种或选其他手写">
<option value="">开仓类型(必选)</option>
{% for er in entry_reason_options %}
<option value="{{ er }}">{{ er }}</option>
{% endfor %}
<option value="{{ entry_reason_other_value }}">其他(自定义,见下方说明框)</option>
</select>
<input type="text" name="entry_reason_custom" id="journal-entry-reason-custom" maxlength="2000" placeholder="选「其他」时在此填写开仓类型说明" autocomplete="off" style="display:none">
<input name="expect_rr" placeholder="预期RR">
<input name="real_rr" placeholder="实际RR">
<select name="early_exit_trigger" required title="平仓如何触发">
<option value="">离场触发(必选)</option>
<option value="止盈">止盈</option>
<option value="保本止盈">保本止盈</option>
<option value="移动止盈">移动止盈</option>
<option value="时间平仓">时间平仓</option>
<option value="手动平仓">手动平仓</option>
<option value="止损">止损</option>
<option value="其他">其他</option>
</select>
<input name="early_exit_note" id="early-exit-note" placeholder="离场补充(仅手工平仓必填)">
<select name="post_breakeven_stare"><option value="否">保本后盯盘:否</option><option value="是">保本后盯盘:是</option></select>
<select name="new_trade_while_occupied"><option value="否">占用时新开仓:否</option><option value="是">占用时新开仓:是</option></select>
<input id="journal-screenshot" type="file" name="screenshot" accept="image/*">
</div>
<div class="form-row" style="margin-top:8px;flex-wrap:wrap;gap:10px;align-items:center">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="journal_exchange_chart" value="true" checked>
保存时自动生成 K 线图并作为截图
</label>
<label style="font-size:.82rem;color:#9aa">周期1</label>
<select name="journal_chart_tf1" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">周期2</label>
<select name="journal_chart_tf2" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线数</label>
<select name="journal_chart_limit" style="min-width:72px">
{% for n in [100, 150, 200, 250, 300, 400, 500] %}
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线截止</label>
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
</select>
</div>
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:4px">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓、平仓与止损位</div>
<div class="form-row" style="margin-top:8px">
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
</div>
<div class="mood-grid" style="margin-top:8px">
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
</div>
<textarea name="note" rows="2" style="width:100%;margin-top:8px" placeholder="备注"></textarea>
<button type="submit" style="margin-top:8px">保存复盘记录</button>
</form>
</div>
<div class="card full review-card" id="review-card">
<div class="review-card-head">
<h2>AI复盘(按交易记录)</h2>
<button type="button" class="review-card-fs-btn" id="review-card-fs-btn" onclick="toggleReviewCardFullscreen()">全屏</button>
</div>
<div class="form-row">
<input type="date" id="day_date">
<button type="button" id="gen-daily-btn" onclick="genDaily()">生成日复盘</button>
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
<input type="date" id="week_start">
<input type="date" id="week_end">
<button type="button" id="gen-weekly-btn" onclick="genWeekly()">生成周复盘</button>
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
</div>
<div class="ai-result-wrap" id="daily_result_wrap" style="display:none">
<div id="daily_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('日复盘结果', 'daily_result')">全屏查看</button>
</div>
</div>
<div class="ai-result-wrap" id="weekly_result_wrap" style="display:none">
<div id="weekly_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('周复盘结果', 'weekly_result')">全屏查看</button>
</div>
</div>
<div class="panel-list" style="margin-top:10px">
<div class="panel-item">
<strong>交易复盘记录</strong>
<div id="journal-list"></div>
</div>
<div class="panel-item">
<strong>AI历史复盘</strong>
<div id="review-list"></div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if page == 'stats' %}
<div class="card stats-card full" id="stats-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
<h2 style="margin-bottom:0">数据统计</h2>
<button type="button" class="stats-toggle" id="stats-toggle-btn" onclick="toggleStatsCard()">折叠</button>
</div>
<div class="stats-content" id="stats-content">
<div class="stat-box" style="margin-bottom:10px">
<div class="stat-item"><div class="label">持仓占用导致错过(累计)</div><div class="value">{{ occupied_miss_total }}</div></div>
</div>
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入(与顶栏 UTC 列表窗无关)。历史总开仓(累计):
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong>
</div>
<div class="form-row" style="margin-bottom:14px;align-items:center">
<label style="display:flex;align-items:center;gap:8px;font-size:.88rem;color:#cfd3ef">
统计品类
<select id="stats-segment-select" onchange="switchStatsSegment()" style="min-width:200px">
{% for seg in stats_bundle.segments %}
<option value="{{ seg.key }}">{{ seg.title }}</option>
{% endfor %}
</select>
</label>
</div>
{% for seg in stats_bundle.segments %}
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
{{ period_stats("日统计", seg.day) }}
{{ period_stats("周统计", seg.week) }}
{{ period_stats("月统计", seg.month) }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<div class="modal" id="imgModal" onclick="closeModal()">
<img id="bigImg" src="" alt="screenshot">
</div>
<div class="detail-modal" id="detailModal" onclick="closeDetailModal(event)">
<div class="panel" onclick="event.stopPropagation()">
<div class="panel-head">
<div class="panel-title" id="detailTitle">详情</div>
<div class="panel-actions">
<button type="button" class="panel-fs" onclick="expandDetailToFullscreen()">全屏</button>
<button type="button" class="panel-close" onclick="forceCloseDetailModal()">关闭</button>
</div>
</div>
<div class="panel-body" id="detailBody"></div>
<img id="detailImage" class="panel-image" src="" alt="detail-image" style="display:none" onclick="showImage(this.src)">
</div>
</div>
<script src="/static/instance_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
function syncJournalEntryReasonOtherUi(){
const form = document.getElementById("journal-form");
if(!form) return;
const sel = form.querySelector('[name="entry_reason"]');
const box = form.querySelector('[name="entry_reason_custom"]');
if(!sel || !box) return;
if(sel.value === JOURNAL_ENTRY_REASON_OTHER){
box.style.display = "";
box.required = true;
} else {
box.style.display = "none";
box.required = false;
box.value = "";
}
}
function validateJournalEntryReason(){
const form = document.getElementById("journal-form");
if(!form) return true;
const sel = form.querySelector('[name="entry_reason"]');
const box = form.querySelector('[name="entry_reason_custom"]');
if(!sel || !sel.value){
alert("请选择开仓类型");
return false;
}
if(sel.value === JOURNAL_ENTRY_REASON_OTHER){
const t = (box && box.value || "").trim();
if(!t){
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){
const body = document.getElementById("detailBody");
if(!body) return;
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 = {};
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; }
}
window.location.href = `${window.location.pathname}?_ts=${Date.now()}`;
})
.catch(()=>{ window.location.href = `${window.location.pathname}?_ts=${Date.now()}`; });
}
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(){
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 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 || "")) ?? "";
const entryHint = "开仓类型:五种固定整句、或自定义说明(2000字内;与复盘表单一致;留空=本次不改该项)";
const entryIn = prompt(entryHint, String(t.effective_entry_reason || ""));
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 ?? "-"}`);
window.location.href = `${window.location.pathname}?_ts=${Date.now()}`;
})
.catch(()=>alert("核对保存请求失败"));
}
function deleteKeyMonitor(id){
if(!confirm("删除该关键位?将写入下方历史并刷新页面。")) return;
fetch(`/delete_key_monitor/${id}`,{method:"POST"})
.then(r=>r.json())
.then(data=>{
window.location.href = `${window.location.pathname}?_ts=${Date.now()}`;
})
.catch(()=>{ window.location.href = `${window.location.pathname}?_ts=${Date.now()}`; });
}
function deleteKeyHistory(id){
if(!confirm("确定删除这条关键位历史?")) return;
fetch(`/delete_key_history/${id}`,{method:"POST"})
.then(r=>r.json())
.then(()=>{
window.location.href = `${window.location.pathname}?_ts=${Date.now()}`;
})
.catch(()=>{ window.location.href = `${window.location.pathname}?_ts=${Date.now()}`; });
}
function listWindowQueryString(){
const presetEl = document.getElementById("win-preset-select");
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
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]);
let html="";
data.forEach(o=>{
journalCache[o.id] = o;
const moodTags = (o.mood_issues || []).join(",") || "无";
html += `<div class="entry">
<div><strong>${o.coin||"-"} ${o.tf||"-"}</strong> | 盈亏:${o.pnl||"-"}U</div>
<div>开:${o.open_datetime||"-"} 平:${o.close_datetime||"-"} 持仓:${o.hold_duration||"-"}</div>
<div>心态标签:${moodTags}</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="openJournalDetail('${o.id}')">查看详情</button>
<button type="button" class="btn-del" onclick="deleteJournal('${o.id}')">删除</button>
</div>
</div>`;
});
const box = document.getElementById("journal-list");
if(box){ box.innerHTML = html || "<div class='entry'>暂无数据</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"; }
setJournalField("early_exit_trigger", "");
setJournalField("early_exit_note", "");
const kst = String(t.key_signal_type || "").trim();
const mt = String(t.monitor_type || "").trim();
if(mt === "趋势回调" && JOURNAL_ENTRY_REASON_OPTIONS.includes("趋势回调")){
setJournalField("entry_reason", "趋势回调");
} else if(mt === "顺势加仓" && JOURNAL_ENTRY_REASON_OPTIONS.includes("顺势加仓")){
setJournalField("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", "");
}
}
setJournalField("entry_reason_custom", "");
syncJournalEntryReasonOtherUi();
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 || "-"} | 类型:${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();
alert("已填入下方复盘表单,请手动补充主观原因。");
}
function prefillJournalByImage(){
const fileInput = document.getElementById("journal-screenshot");
if(!fileInput || !fileInput.files || !fileInput.files.length){
alert("请先选择截图");
return;
}
const fd = new FormData();
fd.append("screenshot", fileInput.files[0]);
fetch("/api/journal_prefill", { method: "POST", body: fd })
.then(r=>r.json())
.then(res=>{
if(!res.ok){
alert(res.msg || "AI识别失败");
return;
}
const d = res.data || {};
setJournalField("open_datetime", normalizeDatetimeLocal(d.open_datetime));
setJournalField("close_datetime", normalizeDatetimeLocal(d.close_datetime));
setJournalField("coin", d.coin || "");
setJournalField("tf", d.tf || "");
setJournalField("pnl", d.pnl || "");
setJournalField("expect_rr", d.expect_rr || "");
setJournalField("real_rr", d.real_rr || "");
let entryReason = String(d.entry_reason || "").trim();
let customEr = "";
if(JOURNAL_ENTRY_REASON_OPTIONS && JOURNAL_ENTRY_REASON_OPTIONS.includes(entryReason)){
// keep
} else if(entryReason){
customEr = entryReason;
entryReason = JOURNAL_ENTRY_REASON_OTHER;
} else {
entryReason = "";
}
setJournalField("entry_reason", entryReason);
setJournalField("entry_reason_custom", customEr);
syncJournalEntryReasonOtherUi();
const trig = (d.early_exit_trigger || "").trim();
let noteEx = (d.early_exit_note || "").trim();
const legacy = (d.early_exit_reason || "").trim();
if(!noteEx && legacy && !trig){
const sp = splitLegacyEarlyExitReason(legacy);
setJournalField("early_exit_trigger", sp.trigger);
setJournalField("early_exit_note", sp.note);
} else {
setJournalField("early_exit_trigger", trig);
setJournalField("early_exit_note", noteEx);
}
setJournalField("note", d.note || "");
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
recomputeJournalRealRr();
alert("已完成预填,请手动检查并补充原因");
})
.catch(()=>alert("AI识别请求失败"));
}
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(4);
}
}
function switchStatsSegment(){
const sel = document.getElementById("stats-segment-select");
if(!sel) return;
const key = sel.value;
document.querySelectorAll(".stats-segment-panel").forEach(p=>{
p.style.display = p.getAttribute("data-stats-segment") === key ? "block" : "none";
});
const q = new URLSearchParams(window.location.search);
q.set("stats_segment", key);
const qs = q.toString();
history.replaceState(null, "", qs ? (window.location.pathname + "?" + qs) : window.location.pathname);
}
function initStatsSegmentFromUrl(){
const sel = document.getElementById("stats-segment-select");
if(!sel) return;
const key = new URLSearchParams(window.location.search).get("stats_segment");
if(key && sel.querySelector('option[value="' + key.replace(/"/g, "") + '"]')){
sel.value = key;
}
switchStatsSegment();
}
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 ? "展开" : "折叠";
}
attachListWindowToExports();
toggleListWindowCustom();
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);
toggleReviewMode();
}
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();
}
}
function syncKeyMonitorFormFields(){
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if(!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破","收敛突破"]);
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]);
const rsTypes = new Set(["关键阻力位","关键支撑位"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !rsTypes.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if(dirEl){
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if(!showDir) dirEl.value = "";
}
if(modeEl) modeEl.style.display = showAuto ? "" : "none";
if(manualTp){
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(window.TimeCloseUI) TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if(upperEl){
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if(hideBounds) upperEl.value = "";
}
if(lowerEl){
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if(hideBounds) lowerEl.value = "";
}
if(fbPriceEl){
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if(!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder = (dirEl && dirEl.value === "short") ? "高点(阻力)" : ((dirEl && dirEl.value === "long") ? "低点(支撑)" : "做空填高点/做多填低点");
}
[teEntryEl, teSlEl, teTpEl].forEach((el)=>{
if(!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if(!showTe) el.value = "";
});
}
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if(window.TimeCloseUI){
TimeCloseUI.bindTimeCloseForm("key-time-close-cb", "key-time-close-hours", "key-time-close-wrap");
TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap");
}
const keyForm = document.getElementById("key-form");
if(keyForm){
keyForm.addEventListener("submit", (e)=>{
e.preventDefault();
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if(!symbol){
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if(typeVal === "假突破"){
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then(r=>r.json().then(d=>({status:r.status, data:d})))
.then(({status,data})=>{
if(status >= 400 || !data.ok){
alert((data && data.msg) || "日成交量排名读取失败");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
const inTop = data.in_top != null ? data.in_top : data.in_top30;
if(data.rank == null || !inTop){
alert(`${data.symbol} 当前24h成交额排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截。`);
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
})
.catch(()=>{
alert("日成交量排名检查失败,请稍后重试");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
});
});
}
// 复盘/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){
const mode = (document.getElementById("sltp-mode")||{}).value || "fixed_rr";
const preview = document.getElementById("order-tp-preview");
if(!preview) return;
if(mode !== "fixed_rr"){
preview.style.display = "none";
return;
}
preview.style.display = "";
const direction = (document.getElementById("order-direction")||{}).value || "long";
const sl = Number((document.getElementById("order-sl")||{}).value);
const rr = Number((document.getElementById("order-fixed-rr")||{}).value) || MANUAL_FIXED_RR_DEFAULT;
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
}
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);
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(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 paintBreakevenBadge(orderId, secured){
const wrap = document.getElementById(`order-be-wrap-${orderId}`);
if(!wrap) return;
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 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 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;
}
(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=>{
const pEl = document.getElementById(`order-price-${o.id}`);
if(pEl){
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
let disp = "";
if(hasMark && o.exchange_mark_price_display){
disp = o.exchange_mark_price_display;
} else if(o.price_display){
disp = o.price_display;
} else {
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
disp = Number.isFinite(px) ? px.toFixed(6) : "-";
}
pEl.innerText = disp;
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
}
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 pnlEl = document.getElementById(`order-pnl-${o.id}`);
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");
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl){
rrEl.innerText = formatRrRatio(o.rr_ratio);
}
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
paintPlanTpslDisplay(o.id, o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(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){
const levEl = document.getElementById("order-leverage");
if(levEl) levEl.value = 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);
}).catch(()=>{});
}
function refreshAccountSnapshot(){
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
if (typeof data.funding_usdt !== "undefined") {
const el = document.getElementById("total-capital");
if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${Number(data.funding_usdt).toFixed(2)}U`;
}
if (typeof data.current_capital !== "undefined") {
const el = document.getElementById("current-capital");
if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`;
}
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
latestAvailableUsdt = Number(data.available_trading_usdt);
}
let canTradeText = "可开仓";
if(!data.can_trade){
const parts = [];
if((data.active_count||0) >= (data.max_active_positions||{{ max_active_positions }})) parts.push(`持仓 ${data.active_count}/${data.max_active_positions}`);
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`;
}
const allowEl = document.getElementById("allow-open-before-reset");
const guardStatus = document.getElementById("open-guard-status");
const resetH = data.reset_hour != null ? data.reset_hour : {{ reset_hour }};
if(allowEl && typeof data.open_guard_enabled !== "undefined"){
allowEl.checked = !data.open_guard_enabled;
}
if(guardStatus && typeof data.open_guard_enabled !== "undefined"){
guardStatus.innerText = data.open_guard_enabled
? `已限制:${resetH}:00 前不可开仓`
: `已放开:${resetH}:00 前允许开仓`;
}
}).catch(()=>{});
}
const allowOpenBeforeResetEl = document.getElementById("allow-open-before-reset");
if(allowOpenBeforeResetEl){
allowOpenBeforeResetEl.addEventListener("change", function(){
const allow = !!this.checked;
fetch("/api/settings/open_guard", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({enabled: !allow}),
}).then(r=>r.json()).then(data=>{
if(!data.ok){ alert(data.msg || "保存失败"); return; }
refreshAccountSnapshot();
}).catch(()=>alert("保存失败"));
});
}
const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults);
if(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(sltpModeEl){
sltpModeEl.addEventListener("change", toggleSltpMode);
loadFixedRrPref();
toggleSltpMode();
}
["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();
const _journalFormEl = document.getElementById("journal-form");
if(_journalFormEl){
_journalFormEl.addEventListener("submit", function(ev){
if(!validateJournalEntryReason()) ev.preventDefault();
});
const _jErSel = _journalFormEl.querySelector('[name="entry_reason"]');
if(_jErSel) _jErSel.addEventListener("change", syncJournalEntryReasonOtherUi);
syncJournalEntryReasonOtherUi();
}
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);
if(!symbol){
alert("请先填写币种,以便按市价校验盈亏比");
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
fetch(`/api/order_defaults?symbol=${encodeURIComponent(symbol)}&direction=${encodeURIComponent(direction)}`)
.then(r=>r.json())
.then(data=>{
const px = data.last_price ?? data.price;
const entry = Number(px);
if(!Number.isFinite(entry) || entry <= 0){
alert("无法获取市价,请稍后重试");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
return;
}
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();
refreshPriceSnapshotConditional();
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
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(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);
});
}
if(page === "trade"){
(data.order_prices || []).forEach(o=>{
const pEl = document.getElementById(`order-price-${o.id}`);
if(pEl){
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
let disp = "";
if(hasMark && o.exchange_mark_price_display) disp = o.exchange_mark_price_display;
else if(o.price_display) disp = o.price_display;
else { const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price); disp = Number.isFinite(px) ? px.toFixed(6) : "-"; }
pEl.innerText = disp;
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
}
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 pnlEl = document.getElementById(`order-pnl-${o.id}`);
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");
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
paintPlanTpslDisplay(o.id, o);
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o);
});
}
}).catch(()=>{});
}
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
</script>
</body>
</html>