Add tabbed period stats with lightweight charts on instance pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-10 13:27:02 +08:00
parent 0ce14c8822
commit 6aaa328a24
8 changed files with 826 additions and 535 deletions
+31
View File
@@ -166,6 +166,37 @@
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px} .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{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px} .stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
.stats-period-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
.stats-period-tab{background:#151a2a;color:#9aa3bf;border:1px solid #304164;border-radius:8px;padding:7px 14px;font-size:.84rem;cursor:pointer;transition:background .15s,border-color .15s,color .15s}
.stats-period-tab:hover{background:#1c2438;color:#cfd3ef}
.stats-period-tab.active{background:#1f3a5a;color:#8fc8ff;border-color:#3d5f8a;font-weight:600}
.stats-period-pane[hidden]{display:none!important}
.stats-period-range{font-size:.78rem;color:#8892b0;margin-bottom:12px;line-height:1.45}
.inst-stats-viz{display:flex;flex-direction:column;gap:14px;margin-bottom:14px}
.inst-stats-kpis{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
.inst-stats-kpi{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:12px 10px;background:#151a2a;border:1px solid #2a3152;border-radius:10px;text-align:center;min-height:88px}
.inst-stats-kpi-val{font-size:1.15rem;font-weight:700;font-variant-numeric:tabular-nums;line-height:1.2}
.inst-stats-kpi-lbl{font-size:.72rem;color:#8892b0;line-height:1.3}
.inst-stats-ring{--win-pct:0;width:56px;height:56px;border-radius:50%;background:conic-gradient(#4cd97f 0 calc(var(--win-pct) * 1%),#ff6b6b calc(var(--win-pct) * 1%) 100%);display:flex;align-items:center;justify-content:center;position:relative}
.inst-stats-ring::before{content:"";position:absolute;inset:7px;border-radius:50%;background:#151a2a}
.inst-stats-ring-label{position:relative;z-index:1;font-size:.78rem;font-weight:700;font-variant-numeric:tabular-nums}
.inst-stats-block{padding:12px;background:#141923;border:1px solid #2a3150;border-radius:10px}
.inst-stats-block-title{font-size:.72rem;color:#8892b0;margin-bottom:8px}
.inst-stats-stacked-bar{display:flex;height:10px;border-radius:6px;overflow:hidden;background:#1e2438}
.inst-stats-stacked-fill{height:100%;min-width:0;transition:width .2s ease}
.inst-stats-stacked-fill--profit{background:#4cd97f}
.inst-stats-stacked-fill--loss{background:#ff6b6b}
.inst-stats-bar-labels{display:flex;justify-content:space-between;gap:10px;margin-top:8px;font-size:.76rem;font-variant-numeric:tabular-nums}
.inst-stats-risk-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px}
.inst-stats-risk-item{display:flex;flex-direction:column;gap:3px;min-width:0}
.inst-stats-risk-item .k{font-size:.7rem;color:#8892b0}
.inst-stats-risk-item .v{font-size:.84rem;font-weight:600;font-variant-numeric:tabular-nums;color:#e8ecf4;word-break:break-word}
.inst-stats-empty{margin:0;padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
.inst-stats-details{margin-top:4px}
.inst-stats-details>summary{cursor:pointer;font-size:.84rem;color:#9aa3bf;padding:8px 0;user-select:none;list-style-position:inside}
.inst-stats-details>summary::-webkit-details-marker{color:#6d7689}
.inst-stats-details[open]>summary{margin-bottom:6px;color:#cfd3ef}
@media (max-width:640px){.inst-stats-kpis{grid-template-columns:1fr}.inst-stats-risk-grid{grid-template-columns:1fr}}
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150} .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 h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px} .key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
+96
View File
@@ -0,0 +1,96 @@
(function (global) {
"use strict";
var PERIODS = ["day", "week", "month"];
function statsSegmentSelect() {
return document.getElementById("stats-segment-select");
}
function activeSegmentPanel() {
var sel = statsSegmentSelect();
if (!sel) return null;
var key = sel.value;
return document.querySelector(
'.stats-segment-panel[data-stats-segment="' + key + '"]'
);
}
function replaceStatsUrl(params) {
var q = new URLSearchParams(global.location.search);
Object.keys(params).forEach(function (k) {
if (params[k] == null || params[k] === "") q.delete(k);
else q.set(k, params[k]);
});
var qs = q.toString();
global.history.replaceState(
null,
"",
qs ? global.location.pathname + "?" + qs : global.location.pathname
);
}
function switchStatsPeriod(periodKey) {
var panel = activeSegmentPanel();
if (!panel) return;
var key = PERIODS.indexOf(periodKey) >= 0 ? periodKey : "day";
panel.querySelectorAll(".stats-period-pane").forEach(function (pane) {
var match = pane.getAttribute("data-stats-period") === key;
if (match) pane.removeAttribute("hidden");
else pane.setAttribute("hidden", "");
});
panel.querySelectorAll(".stats-period-tab").forEach(function (btn) {
var on = btn.getAttribute("data-stats-period") === key;
btn.classList.toggle("active", on);
btn.setAttribute("aria-selected", on ? "true" : "false");
});
replaceStatsUrl({ stats_period: key });
}
function switchStatsSegment() {
var sel = statsSegmentSelect();
if (!sel) return;
var key = sel.value;
document.querySelectorAll(".stats-segment-panel").forEach(function (p) {
p.style.display =
p.getAttribute("data-stats-segment") === key ? "block" : "none";
});
replaceStatsUrl({ stats_segment: key });
var period =
new URLSearchParams(global.location.search).get("stats_period") || "day";
switchStatsPeriod(period);
}
function bindPeriodTabs() {
document.querySelectorAll(".stats-period-tab").forEach(function (btn) {
if (btn.getAttribute("data-stats-bound") === "1") return;
btn.setAttribute("data-stats-bound", "1");
btn.addEventListener("click", function () {
switchStatsPeriod(btn.getAttribute("data-stats-period") || "day");
});
});
}
function initStatsFromUrl() {
var sel = statsSegmentSelect();
if (!sel) return;
bindPeriodTabs();
var url = new URLSearchParams(global.location.search);
var segKey = url.get("stats_segment");
if (
segKey &&
sel.querySelector('option[value="' + segKey.replace(/"/g, "") + '"]')
) {
sel.value = segKey;
}
switchStatsSegment();
var period = url.get("stats_period") || "day";
if (PERIODS.indexOf(period) < 0) period = "day";
switchStatsPeriod(period);
}
global.switchStatsSegment = switchStatsSegment;
global.switchStatsPeriod = switchStatsPeriod;
global.initStatsFromUrl = initStatsFromUrl;
global.initStatsSegmentFromUrl = initStatsFromUrl;
})(window);
+40
View File
@@ -819,6 +819,46 @@ html[data-theme="light"] .stats-period-block {
border-bottom-color: #d0dae4 !important; border-bottom-color: #d0dae4 !important;
} }
html[data-theme="light"] .stats-period-tab {
background: #f4f7fb !important;
color: #4a6078 !important;
border-color: #c8d4e0 !important;
}
html[data-theme="light"] .stats-period-tab:hover {
background: #e8eef5 !important;
color: #142232 !important;
}
html[data-theme="light"] .stats-period-tab.active {
background: #dce8f5 !important;
color: #0d4a7a !important;
border-color: #7eb0d8 !important;
}
html[data-theme="light"] .stats-period-range,
html[data-theme="light"] .inst-stats-kpi-lbl,
html[data-theme="light"] .inst-stats-block-title,
html[data-theme="light"] .inst-stats-risk-item .k,
html[data-theme="light"] .inst-stats-empty {
color: #4a6078 !important;
}
html[data-theme="light"] .inst-stats-kpi,
html[data-theme="light"] .inst-stats-block {
background: #f8fafc !important;
border-color: #c8d4e0 !important;
}
html[data-theme="light"] .inst-stats-ring::before {
background: #f8fafc !important;
}
html[data-theme="light"] .inst-stats-stacked-bar {
background: #e2e8f0 !important;
}
html[data-theme="light"] .inst-stats-risk-item .v,
html[data-theme="light"] .inst-stats-details > summary {
color: #142232 !important;
}
html[data-theme="light"] .inst-stats-details[open] > summary {
color: #0d4a7a !important;
}
html[data-theme="light"] .key-history { html[data-theme="light"] .key-history {
border-top-color: #d0dae4 !important; border-top-color: #d0dae4 !important;
} }
@@ -597,29 +597,6 @@ function recomputeJournalRealRr(){
} }
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(){ function toggleStatsCard(){
const card = document.getElementById("stats-card"); const card = document.getElementById("stats-card");
const btn = document.getElementById("stats-toggle-btn"); const btn = document.getElementById("stats-toggle-btn");
+541 -442
View File
@@ -1,442 +1,541 @@
{# Hub iframe tab fragment — shared via embed_templates #} {# Hub iframe tab fragment — shared via embed_templates #}
{% macro period_stats(title, s) %} {% macro period_stats_pane(period_key, s) %}
<div class="stats-period-block"> {% set win_pct = s.win_rate_pct if s.win_rate_pct is not none else 0 %}
<h3>{{ title }}</h3> {% set profit_sum = (s.net_pnl_u + s.loss_sum_u) if s.closed_count else 0 %}
<div class="sub">{{ s.range_label }}</div> {% set loss_sum = s.loss_sum_u %}
<div class="stats-detail"> {% set pnl_total = profit_sum + loss_sum %}
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div> {% set profit_bar_w = (profit_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div> {% set loss_bar_w = (loss_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
<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> {% set net_cls = 'pos-pnl-profit' if s.net_pnl_u > 0 else ('pos-pnl-loss' if s.net_pnl_u < 0 else '') %}
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div> <div class="stats-period-pane" data-stats-period="{{ period_key }}" role="tabpanel"{% if period_key != 'day' %} hidden{% endif %}>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div> <div class="stats-period-range">{{ s.range_label }}</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="inst-stats-viz">
<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> {% if s.closed_count %}
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div> <div class="inst-stats-kpis">
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div> <div class="inst-stats-kpi inst-stats-kpi--pnl">
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }}</div></div> <span class="inst-stats-kpi-val {{ net_cls }}">{% if s.net_pnl_u > 0 %}+{% endif %}{{ funds_fmt(s.net_pnl_u) }}U</span>
<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> <span class="inst-stats-kpi-lbl">净盈亏</span>
</div> </div>
</div> <div class="inst-stats-kpi inst-stats-kpi--win">
{% endmacro %} <div class="inst-stats-ring" style="--win-pct: {{ win_pct }}">
<div class="grid"> <span class="inst-stats-ring-label">{% if s.win_rate_pct is not none %}{{ win_pct|round(0)|int }}%{% else %}—{% endif %}</span>
{% if page == 'key_monitor' %} </div>
{% include 'key_monitor_panel.html' %} <span class="inst-stats-kpi-lbl">{{ s.win_count }}胜 {{ s.loss_count }}负</span>
{% elif page == 'trade' %} </div>
<div class="dual-panel-grid" style="grid-column:1/-1"> <div class="inst-stats-kpi inst-stats-kpi--trades">
<div class="card"> <span class="inst-stats-kpi-val">{{ s.opens_count }} / {{ s.closed_count }}</span>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px"> <span class="inst-stats-kpi-lbl">开单 / 平仓</span>
<h2 style="margin-bottom:0">实盘下单监控</h2> </div>
{% if focus_order_id %} </div>
<a href="/order_focus?order_id={{ focus_order_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(100根)</a> <div class="inst-stats-block">
{% else %} <div class="inst-stats-block-title">盈亏构成</div>
<span class="btn-del" style="background:#2f2f44;color:#9aa;cursor:not-allowed">暂无持仓可放大</span> <div class="inst-stats-stacked-bar">
{% endif %} <div class="inst-stats-stacked-fill inst-stats-stacked-fill--profit" style="width: {{ '%.1f'|format(profit_bar_w) }}%"></div>
</div> <div class="inst-stats-stacked-fill inst-stats-stacked-fill--loss" style="width: {{ '%.1f'|format(loss_bar_w) }}%"></div>
{% include order_rule_tips_tpl %} </div>
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}"> <div class="inst-stats-bar-labels">
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %} <span class="pos-pnl-profit">盈利 {{ funds_fmt(profit_sum) }}U</span>
{{ trade_policy_symbol('symbol', 'order-symbol') }} <span class="pos-pnl-loss">亏损 {{ funds_fmt(loss_sum) }}U</span>
{{ trade_policy_direction('direction', 'order-direction') }} </div>
<select id="sltp-mode" name="sltp_mode"> </div>
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option> <div class="inst-stats-block inst-stats-block--risk">
<option value="price">止盈止损:价格模式</option> <div class="inst-stats-risk-grid">
<option value="pct">止盈止损:百分比模式</option> <div class="inst-stats-risk-item">
</select> <span class="k">最大回撤</span>
{% from 'order_entry_model_fields.html' import order_entry_type_fields with context %} <span class="v pos-pnl-loss">{{ funds_fmt(s.max_drawdown_u) }}U</span>
{{ order_entry_type_fields() }} </div>
{% from 'order_leverage_fields.html' import order_leverage_fields with context %} <div class="inst-stats-risk-item">
{{ order_leverage_fields() }} <span class="k">连续亏损</span>
{% if not intraday_discipline %} <span class="v">{{ s.consecutive_losses }} 笔</span>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef"> </div>
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单) <div class="inst-stats-risk-item">
</label> <span class="k">最长连亏日</span>
<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"> <span class="v">{{ s.max_loss_streak_days }} 天</span>
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer"> </div>
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓 <div class="inst-stats-risk-item">
</label> <span class="k">最大亏损日</span>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓"> <span class="v">{% if s.worst_day %}{{ s.worst_day }} ({{ funds_fmt(s.worst_day_pnl) }}U){% else %}—{% endif %}</span>
<option value="1">1h</option> </div>
<option value="2">2h</option> </div>
<option value="4" selected>4h</option> </div>
</select> {% else %}
</span> <p class="inst-stats-empty">当前区间暂无平仓数据</p>
{% else %} {% endif %}
<input type="hidden" name="breakeven_enabled" value="0"> </div>
{% endif %} <details class="inst-stats-details" open>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef"> <summary>详细指标</summary>
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记) <div class="stats-detail">
</label> <div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %} <div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }} <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>
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span> <div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required> <div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比"> <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>
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none"> <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>
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none"> <div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none"> <div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
<button type="submit">{{ open_position_button_label }}</button> <div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }}</div></div>
</form> <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>
{% include 'order_plan_preview_bar.html' %} </div>
</div> </details>
<div class="card"> </div>
<h2 style="margin-bottom:8px">实时持仓</h2> {% endmacro %}
<div class="panel-scroll pos-list pos-list-live"> <div class="grid">
{% for o in order %} </div>
<div class="pos-card" id="order-row-{{ o.id }}" <a href="/env_config" class="{% if page == 'env_config' %}active{% endif %}">env配置</a>
data-monitor-id="{{ o.id }}" {% endif %}
data-symbol="{{ o.symbol }}" <a href="/settings" class="{% if page == 'settings' %}active{% endif %}">系统设置</a>
data-direction="{{ o.direction }}" </div>
data-plan-sl="{% if o.stop_loss %}{{ price_fmt(o.symbol, o.stop_loss) }}{% endif %}" {% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
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 %}"> {% include 'instance_header_panel.html' %}
<div class="pos-card-head"> {% if page not in ('settings', 'risk_policy', 'env_config', 'options') %}
<div class="pos-card-symbol"> {% include 'instance_top_bar.html' %}
<strong>{{ o.exchange_symbol or o.symbol }}</strong> {% endif %}
{% 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 }}" <div class="grid">
data-close-at-ms="{{ o.time_close_at_ms or '' }}"> {% if page == 'key_monitor' %}
<span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span> {% include 'key_monitor_panel.html' %}
· <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span> {% elif page == 'trade' %}
</span> <div class="dual-panel-grid" style="grid-column:1/-1">
{% endif %} <div class="card">
{% include 'force_close_order_badge.html' %} <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span> <h2 style="margin-bottom:0">实盘下单监控</h2>
</div> {% if focus_order_id %}
<div class="pos-head-actions"> <a href="/order_focus?order_id={{ focus_order_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(100根)</a>
{% if not intraday_discipline %} {% else %}
<button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button> <span class="btn-del" style="background:#2f2f44;color:#9aa;cursor:not-allowed">暂无持仓可放大</span>
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a> {% endif %}
{% endif %} </div>
</div> {% include order_rule_tips_tpl %}
</div> <form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
<div class="pos-meta"> {% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span> {{ trade_policy_symbol('symbol', 'order-symbol') }}
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% elif intraday_discipline %}开仓: —{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span> {{ trade_policy_direction('direction', 'order-direction') }}
<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> <select id="sltp-mode" name="sltp_mode">
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span> <option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}"> <option value="price">止盈止损:价格模式</option>
{% if intraday_discipline %} <option value="pct">止盈止损:百分比模式</option>
{% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %} </select>
</span> {% from 'order_entry_model_fields.html' import order_entry_type_fields with context %}
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span> {{ order_entry_type_fields() }}
</div> {% from 'order_leverage_fields.html' import order_leverage_fields with context %}
<div class="pos-grid"> {{ order_leverage_fields() }}
<div class="pos-cell"> {% if not intraday_discipline %}
<span class="pos-label">成交价</span> <label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span> <input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</div> </label>
<div class="pos-cell"> <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">
<span class="pos-label">止损</span> <label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span> <input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</div> </label>
<div class="pos-cell"> <select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<span class="pos-label">止盈</span> <option value="1">1h</option>
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span> <option value="2">2h</option>
</div> <option value="4" selected>4h</option>
<div class="pos-cell"> </select>
<span class="pos-label">盈亏比</span> </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> {% else %}
</div> <input type="hidden" name="breakeven_enabled" value="0">
<div class="pos-cell"> {% endif %}
<span class="pos-label">张数</span> <label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %}</span> <input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</div> </label>
<div class="pos-cell"> {% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
<span class="pos-label">盈利金额</span> {{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="pos-value pos-tp-profit" id="order-tp-profit-{{ o.id }}"></span> <span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
</div> <input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<div class="pos-cell"> <input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span class="pos-label">标记价</span> <input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
<span class="pos-value" id="order-price-{{ o.id }}">-</span> <input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
</div> <input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
<div class="pos-cell"> <button type="submit">{{ open_position_button_label }}</button>
<span class="pos-label">浮盈亏</span> </form>
<span class="pos-value" id="order-pnl-{{ o.id }}">-</span> {% include 'order_plan_preview_bar.html' %}
</div> </div>
</div> <div class="card">
<div class="pos-footer"> <h2 style="margin-bottom:8px">实时持仓</h2>
<span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span> {% if ui_orphan_recovery_enabled %}
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span> {% if not order and orphan_live_positions %}
<span>杠杆: {{ o.leverage or '-' }}x</span> {% set o = orphan_live_positions[0] %}
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span> <div id="orphan-position-recover" class="orphan-recover-banner" style="display:block;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8">
<span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span> 检测到交易所仍有 <strong>{{ o.symbol }}</strong> {{ '空' if o.direction == 'short' else '多' }}仓,但本地监控已中断(误同步时可能无交易记录).
<span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}"></span></span> {% if o.recoverable_monitor_id %}
</div> <button type="button" class="pos-entrust-btn" onclick="recoverLivePosition({{ o.recoverable_monitor_id }})">恢复监控{% if o.plan_stop_loss and o.plan_take_profit %}并挂止盈止损{% endif %}</button>
<div class="pos-ex-orders"> {% else %}
<div class="pos-ex-orders-title">交易所止盈止损</div> 未找到可恢复的监控记录,需在服务器数据库处理.
<div class="pos-ex-order-row"> {% endif %}
<span class="pos-ex-order-main" id="ex-sl-text-{{ o.id }}">止损:加载中…</span> </div>
{% if not intraday_discipline %} {% else %}
<button type="button" class="pos-ex-cancel-btn" id="ex-sl-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'sl')">撤单</button> <div id="orphan-position-recover" class="orphan-recover-banner" style="display:none;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8"></div>
{% endif %} {% endif %}
</div> {% endif %}
<div class="pos-ex-order-row"> <div class="panel-scroll pos-list pos-list-live">
<span class="pos-ex-order-main" id="ex-tp-text-{{ o.id }}">止盈:加载中…</span> {% for o in order %}
{% if not intraday_discipline %} <div class="pos-card" id="order-row-{{ o.id }}"
<button type="button" class="pos-ex-cancel-btn" id="ex-tp-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'tp')">撤单</button> data-monitor-id="{{ o.id }}"
{% endif %} data-symbol="{{ o.symbol }}"
</div> data-direction="{{ o.direction }}"
</div> data-plan-sl="{% if o.stop_loss %}{{ price_fmt(o.symbol, o.stop_loss) }}{% endif %}"
</div> data-plan-tp="{% if o.take_profit %}{{ price_fmt(o.symbol, o.take_profit) }}{% endif %}"
{% else %} data-entry="{% if o.trigger_price %}{{ price_fmt(o.symbol, o.trigger_price) }}{% endif %}">
<div class="pos-empty">暂无持仓</div> <div class="pos-card-head">
{% endfor %} <div class="pos-card-symbol">
</div> <strong>{{ o.exchange_symbol or o.symbol }}</strong>
</div> {% 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 }}"
<div id="tpsl-modal" class="tpsl-modal-backdrop" onclick="if(event.target===this)closeTpslEntrustModal()"> data-close-at-ms="{{ o.time_close_at_ms or '' }}">
<div class="tpsl-modal" onclick="event.stopPropagation()"> <span class="pos-time-close-label">时间平仓 {{ o.time_close_hours or '' }}h</span>
<h3 id="tpsl-modal-title">挂止盈止损</h3> · <span class="pos-time-close-cd" id="order-time-close-cd-{{ o.id }}">--:--:--</span>
<p style="font-size:.78rem;color:#8892b0;margin:0 0 10px">将先撤销该合约已有 TP/SL,再按下列价格重挂.</p> </span>
<div class="form-row"> {% endif %}
<select id="tpsl-modal-mode" onchange="toggleTpslModalMode()"> {% include 'force_close_order_badge.html' %}
<option value="price">价格模式</option> <span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
<option value="pct">百分比模式</option> </div>
</select> <div class="pos-head-actions">
</div> {% if not intraday_discipline %}
<div class="form-row"> <button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button>
<input id="tpsl-modal-sl" step="any" placeholder="止损价格"> <a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
<input id="tpsl-modal-tp" step="any" placeholder="止盈价格"> {% endif %}
</div> </div>
<div class="form-row"> </div>
<input id="tpsl-modal-sl-pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none"> <div class="pos-meta">
<input id="tpsl-modal-tp-pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none"> <span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
</div> <span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% elif intraday_discipline %}开仓: —{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<div class="tpsl-modal-actions"> <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>
<button type="button" class="tpsl-modal-cancel" onclick="closeTpslEntrustModal()">取消</button> <span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<button type="button" class="tpsl-modal-submit" onclick="submitTpslEntrust()">先撤后挂</button> <span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}">
</div> {% if intraday_discipline %}
</div> {% elif o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</div> </span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div> </div>
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %} <div class="pos-grid">
{% include 'strategy_trading_page.html' %} <div class="pos-cell">
{% elif page == 'strategy_records' %} <span class="pos-label">成交价</span>
{% include 'strategy_records_page.html' %} <span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span>
{% elif page == 'options' %} </div>
{% include 'options_panel.html' %} <div class="pos-cell">
{% endif %} <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">
{% if page == 'records' %} <span class="pos-label">止盈</span>
<div class="card full records-card"> <span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
<h2>交易记录</h2> </div>
<div class="form-row" style="margin-bottom:10px;gap:8px"> <div class="pos-cell">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef"> <span class="pos-label">盈亏比</span>
<input id="review-mode-toggle" type="checkbox"> <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>
</label> <div class="pos-cell">
</div> <span class="pos-label">张数</span>
<div class="table-wrap"> <span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %}</span>
<table> </div>
<tr><th>品种</th><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> <div class="pos-cell">
{% for r in record %} <span class="pos-label">盈利金额</span>
<tr id="trade-row-{{ r.id }}"> <span class="pos-value pos-tp-profit" id="order-tp-profit-{{ o.id }}"></span>
{% set pnl_val = (r.pnl_amount or 0)|float %} </div>
<td>{{ r.symbol }}</td> <div class="pos-cell">
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td> <span class="pos-label">标记价</span>
<td>{{ r.effective_entry_reason or '-' }}</td> <span class="pos-value" id="order-price-{{ o.id }}">-</span>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td> </div>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td> <div class="pos-cell">
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %} <span class="pos-label">浮盈亏</span>
{% set tp_show = r.effective_take_profit or r.take_profit %} <span class="pos-value" id="order-pnl-{{ o.id }}">-</span>
<td>{{ price_fmt(r.symbol, stop_show) }}</td> </div>
<td>{{ price_fmt(r.symbol, tp_show) }}</td> </div>
<td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td> <div class="pos-footer">
<td>{{ r.leverage or '-' }}</td> <span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span>
<td>{{ r.effective_hold_minutes or 0 }}</td> <span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td> <span>杠杆: {{ o.leverage or '-' }}x</span>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td> <span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %} <span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span>
<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> <span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}"></span></span>
<td> </div>
{% set effective_result = r.effective_result %} <div class="pos-ex-orders">
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span> <div class="pos-ex-orders-title">交易所止盈止损</div>
{% elif effective_result in ["止损","强制清仓","手动平仓"] %}<span class="badge loss">{{ effective_result }}</span> <div class="pos-ex-order-row">
{% elif effective_result == "时间平仓" %}<span class="badge miss">{{ effective_result }}</span> <span class="pos-ex-order-main" id="ex-sl-text-{{ o.id }}">止损:加载中…</span>
{% else %}<span class="badge">{{ effective_result or '-' }}</span>{% endif %} {% if not intraday_discipline %}
</td> <button type="button" class="pos-ex-cancel-btn" id="ex-sl-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'sl')">撤单</button>
<td> {% endif %}
<button </div>
type="button" <div class="pos-ex-order-row">
class="table-del" <span class="pos-ex-order-main" id="ex-tp-text-{{ o.id }}">止盈:加载中…</span>
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px" {% if not intraday_discipline %}
onclick='fillJournalFromTrade({{ { <button type="button" class="pos-ex-cancel-btn" id="ex-tp-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'tp')">撤单</button>
"symbol": r.symbol, {% endif %}
"monitor_type": r.monitor_type, </div>
"key_signal_type": r.key_signal_type or "", </div>
"direction": r.direction, </div>
"trigger_price": r.trigger_price, {% else %}
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss, <div class="pos-empty">暂无持仓</div>
"take_profit": r.effective_take_profit or r.take_profit, {% endfor %}
"opened_at": r.effective_opened_at, </div>
"closed_at": r.effective_closed_at, </div>
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result, <div id="tpsl-modal" class="tpsl-modal-backdrop" onclick="if(event.target===this)closeTpslEntrustModal()">
"risk_amount": r.risk_amount, <div class="tpsl-modal" onclick="event.stopPropagation()">
"effective_entry_reason": r.effective_entry_reason or "" <h3 id="tpsl-modal-title">挂止盈止损</h3>
}|tojson|safe }})' <p style="font-size:.78rem;color:#8892b0;margin:0 0 10px">将先撤销该合约已有 TP/SL,再按下列价格重挂.</p>
>填入复盘</button> <div class="form-row">
<button <select id="tpsl-modal-mode" onchange="toggleTpslModalMode()">
type="button" <option value="price">价格模式</option>
class="table-del review-edit-btn" <option value="pct">百分比模式</option>
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px" </select>
onclick='editTradeRecordReview({{ { </div>
"id": r.id, <div class="form-row">
"opened_at": r.effective_opened_at, <input id="tpsl-modal-sl" step="any" placeholder="止损价格">
"closed_at": r.effective_closed_at, <input id="tpsl-modal-tp" step="any" placeholder="止盈价格">
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss, </div>
"take_profit": r.effective_take_profit or r.take_profit, <div class="form-row">
"pnl_amount": r.effective_pnl_amount, <input id="tpsl-modal-sl-pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
"result": r.effective_result, <input id="tpsl-modal-tp-pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
"miss_reason": r.effective_miss_reason, </div>
"effective_entry_reason": r.effective_entry_reason or "" <div class="tpsl-modal-actions">
}|tojson|safe }})' <button type="button" class="tpsl-modal-cancel" onclick="closeTpslEntrustModal()">取消</button>
disabled <button type="button" class="tpsl-modal-submit" onclick="submitTpslEntrust()">先撤后挂</button>
>核对修改</button> </div>
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button> </div>
</td> </div>
</tr>
{% endfor %} </div>
</table> {% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
</div> {% include 'strategy_trading_page.html' %}
</div> {% elif page == 'strategy_records' %}
{% include 'strategy_records_page.html' %}
<div class="card full journal-card"> {% elif page == 'options' %}
<h2>交易复盘记录上传(含截图)</h2> {% include 'options_panel.html' %}
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data"> {% endif %}
<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"> {% if page == 'records' %}
<input type="hidden" name="direction_hint" id="direction-hint"> <div class="card full records-card">
{% from 'journal_form_fields.html' import journal_form_fields %} <h2>交易记录</h2>
{{ journal_form_fields(entry_reason_options, order_type_options) }} <div class="form-row" style="margin-bottom:10px;gap:8px">
{% from 'journal_upload_slots.html' import journal_upload_slots %} <label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
{{ journal_upload_slots() }} <input id="review-mode-toggle" type="checkbox">
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center"> 修改/核对开关(开启后可编辑关键字段)
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef"> </label>
<input type="checkbox" name="journal_exchange_chart" value="true"> </div>
保存时自动生成 K 线图并作为截图 <div class="table-wrap">
</label> <table>
<label style="font-size:.82rem;color:#9aa">周期1</label> <tr><th>品种</th><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>
<select name="journal_chart_tf1" style="min-width:72px"> {% for r in record %}
{% for tf in journal_chart_tf_choices %} <tr id="trade-row-{{ r.id }}">
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option> {% set pnl_val = (r.pnl_amount or 0)|float %}
{% endfor %} <td>{{ r.symbol }}</td>
</select> <td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<label style="font-size:.82rem;color:#9aa">周期2</label> <td>{{ r.effective_entry_reason or '-' }}</td>
<select name="journal_chart_tf2" style="min-width:72px"> <td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
{% for tf in journal_chart_tf_choices %} <td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option> {% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% endfor %} {% set tp_show = r.effective_take_profit or r.take_profit %}
</select> <td>{{ price_fmt(r.symbol, stop_show) }}</td>
<label style="font-size:.82rem;color:#9aa">K线数</label> <td>{{ price_fmt(r.symbol, tp_show) }}</td>
<select name="journal_chart_limit" style="min-width:72px"> <td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
{% for n in [100, 150, 200, 250, 300, 400, 500] %} <td>{{ r.leverage or '-' }}</td>
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option> <td>{{ r.effective_hold_minutes or 0 }}</td>
{% endfor %} <td>{{ (r.effective_opened_at or '-')[:16] }}</td>
</select> <td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
<label style="font-size:.82rem;color:#9aa">K线截止</label> {% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间"> <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>
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option> <td>
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option> {% set effective_result = r.effective_result %}
</select> {% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
</div> {% elif effective_result in ["止损","强制清仓","手动平仓"] %}<span class="badge loss">{{ effective_result }}</span>
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:2px;margin-bottom:0">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓,平仓与止损位</div> {% elif effective_result == "时间平仓" %}<span class="badge miss">{{ effective_result }}</span>
<div class="mood-grid"> {% else %}<span class="badge">{{ effective_result or '-' }}</span>{% endif %}
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label> </td>
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label> <td>
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label> <button
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label> type="button"
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label> class="table-del"
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label> style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
</div> onclick='fillJournalFromTrade({{ {
<textarea name="note" rows="2" placeholder="备注"></textarea> "symbol": r.symbol,
<button type="submit" style="margin-top:8px">保存复盘记录</button> "monitor_type": r.monitor_type,
</form> "key_signal_type": r.key_signal_type or "",
</div> "direction": r.direction,
"trigger_price": r.trigger_price,
<div class="card full review-card" id="review-card"> "stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
<div class="review-card-head"> "take_profit": r.effective_take_profit or r.take_profit,
<h2>AI复盘(按交易记录)</h2> "opened_at": r.effective_opened_at,
<button type="button" class="review-card-fs-btn" id="review-card-fs-btn" onclick="toggleReviewCardFullscreen()">全屏</button> "closed_at": r.effective_closed_at,
</div> "pnl_amount": r.effective_pnl_amount,
<div class="form-row"> "result": r.effective_result,
<input type="date" id="day_date"> "risk_amount": r.risk_amount,
<button type="button" id="gen-daily-btn" onclick="genDaily()">生成日复盘</button> "effective_entry_reason": r.effective_entry_reason or ""
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button> }|tojson|safe }})'
<input type="date" id="week_start"> >填入复盘</button>
<input type="date" id="week_end"> <button
<button type="button" id="gen-weekly-btn" onclick="genWeekly()">生成周复盘</button> type="button"
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button> class="table-del review-edit-btn"
</div> style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
<div class="ai-result-wrap" id="daily_result_wrap" style="display:none"> onclick='editTradeRecordReview({{ {
<div id="daily_result" class="ai-result"></div> "id": r.id,
<div class="ai-result-toolbar"> "opened_at": r.effective_opened_at,
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('日复盘结果', 'daily_result')">全屏查看</button> "closed_at": r.effective_closed_at,
</div> "stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
</div> "take_profit": r.effective_take_profit or r.take_profit,
<div class="ai-result-wrap" id="weekly_result_wrap" style="display:none"> "pnl_amount": r.effective_pnl_amount,
<div id="weekly_result" class="ai-result"></div> "result": r.effective_result,
<div class="ai-result-toolbar"> "miss_reason": r.effective_miss_reason,
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('周复盘结果', 'weekly_result')">全屏查看</button> "effective_entry_reason": r.effective_entry_reason or ""
</div> }|tojson|safe }})'
</div> disabled
<div class="panel-list" style="margin-top:10px"> >核对修改</button>
<div class="panel-item"> <button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
<strong>交易复盘记录</strong> </td>
<div id="journal-list"></div> </tr>
</div> {% endfor %}
<div class="panel-item"> </table>
<strong>AI历史复盘</strong> </div>
<div id="review-list"></div> </div>
</div>
</div> <div class="card full journal-card">
</div> <h2>交易复盘记录上传(含截图)</h2>
</div> <form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
</div> <input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
{% endif %} <input type="hidden" name="entry_price_hint" id="entry-price-hint">
</div> <input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
{% if page == 'env_config' %} <input type="hidden" name="exit_price_hint" id="exit-price-hint">
{% include 'env_config_panel.html' %} <input type="hidden" name="direction_hint" id="direction-hint">
{% endif %} {% from 'journal_form_fields.html' import journal_form_fields %}
{% if page == 'risk_policy' %} {{ journal_form_fields(entry_reason_options, order_type_options) }}
{% include 'risk_policy_panel.html' %} {% from 'journal_upload_slots.html' import journal_upload_slots %}
{% endif %} {{ journal_upload_slots() }}
{% if page == 'settings' %} <div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
{% include 'settings_panel.html' %} <label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
{% endif %} <input type="checkbox" name="journal_exchange_chart" value="true">
{% if page == 'stats' %} 保存时自动生成 K 线图并作为截图
<div class="card stats-card full" id="stats-card"> </label>
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap"> <label style="font-size:.82rem;color:#9aa">周期1</label>
<h2 style="margin-bottom:0">数据统计</h2> <select name="journal_chart_tf1" style="min-width:72px">
<button type="button" class="stats-toggle" id="stats-toggle-btn" onclick="toggleStatsCard()">折叠</button> {% for tf in journal_chart_tf_choices %}
</div> <option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
<div class="stats-content" id="stats-content"> {% endfor %}
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem"> </select>
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入(与顶栏 UTC 列表窗无关).历史总开仓(累计): <label style="font-size:.82rem;color:#9aa">周期2</label>
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong> <select name="journal_chart_tf2" style="min-width:72px">
</div> {% for tf in journal_chart_tf_choices %}
<div class="form-row" style="margin-bottom:14px;align-items:center"> <option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
<label style="display:flex;align-items:center;gap:8px;font-size:.88rem;color:#cfd3ef"> {% endfor %}
统计品类 </select>
<select id="stats-segment-select" onchange="switchStatsSegment()" style="min-width:200px"> <label style="font-size:.82rem;color:#9aa">K线数</label>
{% for seg in stats_bundle.segments %} <select name="journal_chart_limit" style="min-width:72px">
<option value="{{ seg.key }}">{{ seg.title }}</option> {% for n in [100, 150, 200, 250, 300, 400, 500] %}
{% endfor %} <option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
</select> {% endfor %}
</label> </select>
</div> <label style="font-size:.82rem;color:#9aa">K线截止</label>
{% for seg in stats_bundle.segments %} <select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}> <option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
{{ period_stats("日统计", seg.day) }} <option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
{{ period_stats("周统计", seg.week) }} </select>
{{ period_stats("月统计", seg.month) }} </div>
</div> <div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:2px;margin-bottom:0">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓,平仓与止损位</div>
{% endfor %} <div class="mood-grid">
</div> <label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
</div> <label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
{% endif %} <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" 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 == 'env_config' %}
{% include 'env_config_panel.html' %}
{% endif %}
{% if page == 'risk_policy' %}
{% include 'risk_policy_panel.html' %}
{% endif %}
{% if page == 'settings' %}
{% include 'settings_panel.html' %}
{% endif %}
{% 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="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 %}>
<div class="stats-period-tabs" role="tablist" aria-label="统计周期">
<button type="button" class="stats-period-tab active" data-stats-period="day" role="tab" aria-selected="true">日统计</button>
<button type="button" class="stats-period-tab" data-stats-period="week" role="tab" aria-selected="false">周统计</button>
<button type="button" class="stats-period-tab" data-stats-period="month" role="tab" aria-selected="false">月统计</button>
</div>
{{ period_stats_pane("day", seg.day) }}
{{ period_stats_pane("week", seg.week) }}
{{ period_stats_pane("month", seg.month) }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
+3 -2
View File
@@ -6,8 +6,8 @@
<script src="/static/instance_theme.js?v=50"></script> <script src="/static/instance_theme.js?v=50"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<link rel="stylesheet" href="/static/instance_page.css?v=4"> <link rel="stylesheet" href="/static/instance_page.css?v=5">
<link rel="stylesheet" href="/static/instance_theme.css?v=77"> <link rel="stylesheet" href="/static/instance_theme.css?v=78">
<script src="/static/account_risk_badge.js?v=4"></script> <script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title> <title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
@@ -100,6 +100,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
<script src="/static/symbol_live_price.js?v=2"></script> <script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script> <script src="/static/strategy_roll.js?v=6"></script>
<script src="/static/key_monitor_form.js?v=2"></script> <script src="/static/key_monitor_form.js?v=2"></script>
<script src="/static/instance_stats.js?v=1"></script>
{% include 'embed_boot_scripts.html' %} {% include 'embed_boot_scripts.html' %}
<script> <script>
window.__INSTANCE_DISPLAY__ = {{ display | tojson }}; window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
+89 -44
View File
@@ -16,8 +16,8 @@
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
<link rel="manifest" href="/static/icons/manifest.webmanifest"> <link rel="manifest" href="/static/icons/manifest.webmanifest">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title> <title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<link rel="stylesheet" href="/static/instance_page.css?v=1"> <link rel="stylesheet" href="/static/instance_page.css?v=2">
<link rel="stylesheet" href="/static/instance_theme.css?v=77"> <link rel="stylesheet" href="/static/instance_theme.css?v=78">
</head> </head>
<body <body
@@ -30,23 +30,85 @@
data-full-margin-buffer="{{ full_margin_buffer_ratio }}" data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}" data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
> >
{% macro period_stats(title, s) %} {% macro period_stats_pane(period_key, s) %}
<div class="stats-period-block"> {% set win_pct = s.win_rate_pct if s.win_rate_pct is not none else 0 %}
<h3>{{ title }}</h3> {% set profit_sum = (s.net_pnl_u + s.loss_sum_u) if s.closed_count else 0 %}
<div class="sub">{{ s.range_label }}</div> {% set loss_sum = s.loss_sum_u %}
<div class="stats-detail"> {% set pnl_total = profit_sum + loss_sum %}
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div> {% set profit_bar_w = (profit_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div> {% set loss_bar_w = (loss_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
<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> {% set net_cls = 'pos-pnl-profit' if s.net_pnl_u > 0 else ('pos-pnl-loss' if s.net_pnl_u < 0 else '') %}
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div> <div class="stats-period-pane" data-stats-period="{{ period_key }}" role="tabpanel"{% if period_key != 'day' %} hidden{% endif %}>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div> <div class="stats-period-range">{{ s.range_label }}</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="inst-stats-viz">
<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> {% if s.closed_count %}
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div> <div class="inst-stats-kpis">
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div> <div class="inst-stats-kpi inst-stats-kpi--pnl">
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }}</div></div> <span class="inst-stats-kpi-val {{ net_cls }}">{% if s.net_pnl_u > 0 %}+{% endif %}{{ funds_fmt(s.net_pnl_u) }}U</span>
<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> <span class="inst-stats-kpi-lbl">净盈亏</span>
</div>
<div class="inst-stats-kpi inst-stats-kpi--win">
<div class="inst-stats-ring" style="--win-pct: {{ win_pct }}">
<span class="inst-stats-ring-label">{% if s.win_rate_pct is not none %}{{ win_pct|round(0)|int }}%{% else %}—{% endif %}</span>
</div>
<span class="inst-stats-kpi-lbl">{{ s.win_count }}胜 {{ s.loss_count }}负</span>
</div>
<div class="inst-stats-kpi inst-stats-kpi--trades">
<span class="inst-stats-kpi-val">{{ s.opens_count }} / {{ s.closed_count }}</span>
<span class="inst-stats-kpi-lbl">开单 / 平仓</span>
</div>
</div>
<div class="inst-stats-block">
<div class="inst-stats-block-title">盈亏构成</div>
<div class="inst-stats-stacked-bar">
<div class="inst-stats-stacked-fill inst-stats-stacked-fill--profit" style="width: {{ '%.1f'|format(profit_bar_w) }}%"></div>
<div class="inst-stats-stacked-fill inst-stats-stacked-fill--loss" style="width: {{ '%.1f'|format(loss_bar_w) }}%"></div>
</div>
<div class="inst-stats-bar-labels">
<span class="pos-pnl-profit">盈利 {{ funds_fmt(profit_sum) }}U</span>
<span class="pos-pnl-loss">亏损 {{ funds_fmt(loss_sum) }}U</span>
</div>
</div>
<div class="inst-stats-block inst-stats-block--risk">
<div class="inst-stats-risk-grid">
<div class="inst-stats-risk-item">
<span class="k">最大回撤</span>
<span class="v pos-pnl-loss">{{ funds_fmt(s.max_drawdown_u) }}U</span>
</div>
<div class="inst-stats-risk-item">
<span class="k">连续亏损</span>
<span class="v">{{ s.consecutive_losses }} 笔</span>
</div>
<div class="inst-stats-risk-item">
<span class="k">最长连亏日</span>
<span class="v">{{ s.max_loss_streak_days }} 天</span>
</div>
<div class="inst-stats-risk-item">
<span class="k">最大亏损日</span>
<span class="v">{% if s.worst_day %}{{ s.worst_day }} ({{ funds_fmt(s.worst_day_pnl) }}U){% else %}—{% endif %}</span>
</div>
</div>
</div>
{% else %}
<p class="inst-stats-empty">当前区间暂无平仓数据</p>
{% endif %}
</div> </div>
<details class="inst-stats-details" open>
<summary>详细指标</summary>
<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>
</details>
</div> </div>
{% endmacro %} {% endmacro %}
<div class="container"> <div class="container">
@@ -519,9 +581,14 @@
</div> </div>
{% for seg in stats_bundle.segments %} {% 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 %}> <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) }} <div class="stats-period-tabs" role="tablist" aria-label="统计周期">
{{ period_stats("周统计", seg.week) }} <button type="button" class="stats-period-tab active" data-stats-period="day" role="tab" aria-selected="true">日统计</button>
{{ period_stats("月统计", seg.month) }} <button type="button" class="stats-period-tab" data-stats-period="week" role="tab" aria-selected="false">周统计</button>
<button type="button" class="stats-period-tab" data-stats-period="month" role="tab" aria-selected="false">月统计</button>
</div>
{{ period_stats_pane("day", seg.day) }}
{{ period_stats_pane("week", seg.week) }}
{{ period_stats_pane("month", seg.month) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -562,6 +629,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script> <script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script> <script src="/static/strategy_roll.js?v=6"></script>
<script src="/static/instance_stats.js?v=1"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ORDER_TYPE_OPTIONS = {{ order_type_options | tojson }}; const JOURNAL_ORDER_TYPE_OPTIONS = {{ order_type_options | tojson }};
@@ -1153,29 +1221,6 @@ function recomputeJournalRealRr(){
} }
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(){ function toggleStatsCard(){
const card = document.getElementById("stats-card"); const card = document.getElementById("stats-card");
const btn = document.getElementById("stats-toggle-btn"); const btn = document.getElementById("stats-toggle-btn");
+26 -24
View File
@@ -7,39 +7,40 @@ ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "lib" / "instance" / "templates" / "index.html" SRC = ROOT / "lib" / "instance" / "templates" / "index.html"
OUT = ROOT / "lib" / "instance" / "templates" / "embed_page_fragment.html" OUT = ROOT / "lib" / "instance" / "templates" / "embed_page_fragment.html"
GRID_START = ' <div class="grid">' GRID_START = " <div class=\"grid\">"
STATS_START = ' <div class="card full stats-card'
def _slice_between(lines: list[str], start: str, end: str | None) -> list[str]: def _find_line(lines: list[str], predicate, *, start: int = 0) -> int:
try: for idx in range(start, len(lines)):
i = next(idx for idx, line in enumerate(lines) if line == start) if predicate(lines[idx]):
except StopIteration: return idx
raise SystemExit(f"marker not found: {start!r}") raise SystemExit("marker not found")
if end is None:
return lines[i:]
try:
j = next(idx for idx, line in enumerate(lines[i + 1 :], i + 1) if line.startswith(end))
except StopIteration:
raise SystemExit(f"end marker not found: {end!r}")
return lines[i:j]
def main() -> None: def main() -> None:
lines = SRC.read_text(encoding="utf-8").splitlines() lines = SRC.read_text(encoding="utf-8").splitlines()
macro_start = next(i for i, l in enumerate(lines) if l.startswith("{% macro period_stats")) macro_start = _find_line(lines, lambda l: "macro period_stats_pane" in l)
macro_end = next(i for i, l in enumerate(lines) if l.strip() == "{% endmacro %}") macro_end = _find_line(lines, lambda l: l.strip() == "{% endmacro %}", start=macro_start)
macro_body = lines[macro_start : macro_end + 1] macro_body = lines[macro_start : macro_end + 1]
grid_block = _slice_between(lines, GRID_START, STATS_START) grid_start = _find_line(lines, lambda l: l == GRID_START)
# strip outer .grid wrapper; fragment adds its own panel_start = _find_line(lines, lambda l: "{% if page == 'env_config' %}" in l)
if grid_block and grid_block[0] == GRID_START: stats_card_line = _find_line(lines, lambda l: 'id="stats-card"' in l)
grid_block = grid_block[1:] stats_start = stats_card_line
if grid_block and grid_block[-1].strip() == "</div>": while stats_start > 0 and lines[stats_start].strip() != "{% if page == 'stats' %}":
# only remove closing div if it closes .grid (heuristic: last line before stats) stats_start -= 1
pass if lines[stats_start].strip() != "{% if page == 'stats' %}":
raise SystemExit("stats if-block not found")
stats_end = _find_line(lines, lambda l: l.strip() == "{% endif %}", start=stats_start + 1)
stats_block = _slice_between(lines, STATS_START, " </div>") grid_block = lines[grid_start + 1 : panel_start]
while grid_block and not grid_block[-1].strip():
grid_block.pop()
if grid_block and grid_block[-1].strip() == "</div>":
grid_block.pop()
panel_block = lines[panel_start:stats_start]
stats_block = lines[stats_start : stats_end + 1]
out_lines = [ out_lines = [
"{# Hub iframe tab fragment — shared via embed_templates #}", "{# Hub iframe tab fragment — shared via embed_templates #}",
@@ -47,6 +48,7 @@ def main() -> None:
'<div class="grid">', '<div class="grid">',
*grid_block, *grid_block,
"</div>", "</div>",
*panel_block,
*stats_block, *stats_block,
] ]
text = "\n".join(out_lines).rstrip() + "\n" text = "\n".join(out_lines).rstrip() + "\n"