Files
crypto_monitor/crypto_monitor_gate_bot/templates/index.html
T
2026-05-15 22:04:00 +08:00

1430 lines
79 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">
<head>
<meta charset="UTF-8">
<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}
.container{width:min(98vw,1900px);margin:0 auto}
.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:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap}
.stat-item{flex:1;min-width:150px;background:#151a2a;padding:10px;border-radius:10px;text-align:center;border:1px solid #2a3152}
.stat-item .label{font-size:.8rem;color:#aaa}
.stat-item .value{font-size:1.3rem;font-weight:600;color:#fff}
.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}
.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}
.pnl-neutral{color:#cfd3ef;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
.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}
.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:999}
.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:1000;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}
.table-wrap{overflow-x:auto}
.trade-dashboard{grid-column:1/-1;display:flex;flex-direction:column;gap:14px}
.trade-panels-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
.trade-panels-row > .card{min-height:0;height:100%;display:flex;flex-direction:column;box-sizing:border-box}
.trade-panels-row > .trend-card{gap:12px}
.trade-panels-row > .order-card .order-live-positions{margin-top:auto;flex:0 1 auto;min-height:0}
.order-live-positions{display:flex;flex-direction:column;gap:10px;margin-top:12px;padding-top:14px;border-top:1px solid #2a3150;max-height:240px;overflow:auto}
.order-live-positions .running-plans-stack{margin-top:0}
.trade-panels-row > .trend-card .trend-running-plans{margin-top:auto}
.trend-running-plans{padding-top:14px;border-top:1px solid #2a3150}
.running-plans-stack{display:flex;flex-direction:column;gap:12px;margin-top:10px}
.plan-position-card{background:#141a2a;border:1px solid #2a3150;border-radius:12px;padding:12px 14px}
.plan-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:8px}
.plan-card-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:1rem;font-weight:700;color:#f0f2ff}
.plan-card-meta{font-size:.76rem;color:#8892b0;line-height:1.55;margin-bottom:10px}
.plan-card-meta .accent{color:#6ab8ff}
.plan-card-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px 14px;margin-bottom:10px}
@media (max-width:720px){
.plan-card-grid{grid-template-columns:1fr}
}
.plan-cell{display:flex;flex-direction:column;gap:3px}
.plan-cell .lbl{font-size:.72rem;color:#8b95b8}
.plan-cell .val.pnl-profit,.plan-cell .val .pnl-profit{color:#4cd97f}
.plan-cell .val.pnl-loss,.plan-cell .val .pnl-loss{color:#ff6666}
.plan-cell .val.pnl-neutral,.plan-cell .val .pnl-neutral{color:#cfd3ef}
.btn-close-plan{padding:7px 14px;background:#5c1e2a;color:#ffb4b4;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap}
.btn-close-plan:hover{filter:brightness(1.08)}
.records-card{grid-column:1/-1}
.review-card{grid-column:1/-1}
@media (min-width: 1900px){
.container{max-width:2100px}
.order-card .order-live-positions{max-height:420px}
.records-card .table-wrap{max-height:620px;overflow:auto}
}
@media (max-width: 1400px){
.container{width:min(99vw,1600px)}
.grid{grid-template-columns:1fr}
.trade-dashboard,.records-card,.review-card{grid-column:auto}
.panel-list{grid-template-columns:1fr}
}
@media (max-width:1200px){
.trade-panels-row{grid-template-columns:1fr}
}
@media (max-width: 960px){
body{padding:10px}
.form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
.stat-item{min-width:130px}
}
.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;text-align:left;padding:10px 12px}
.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}
.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}
.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}
.stats-split-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:start}
.stats-split-col{min-width:0;background:#101522;border:1px solid #252a45;border-radius:10px;padding:10px 12px}
.stats-split-head{font-size:.88rem;font-weight:600;color:#b8c4ff;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #2a3150}
@media (max-width:900px){
.stats-split-row{grid-template-columns:1fr}
}
</style>
</head>
<body>
{% macro period_metrics_cells(s) %}
<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">{{ money_fmt(s.net_pnl_u) }}</div></div>
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ money_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 %}{{ money_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 %}{{ money_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ money_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 }}{{ money_fmt(s.worst_day_pnl) }}U{% else %}-{% endif %}</div></div>
{% endmacro %}
{% macro period_stats_dual(title, pair) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
<div class="sub">{{ pair.range_label }}</div>
<div class="stats-split-row">
<div class="stats-split-col">
<div class="stats-split-head">机器人下单监控</div>
<div class="stats-detail">{{ period_metrics_cells(pair.order) }}</div>
</div>
<div class="stats-split-col">
<div class="stats-split-head">趋势回调策略</div>
<div class="stats-detail">{{ period_metrics_cells(pair.trend) }}</div>
</div>
</div>
</div>
{% endmacro %}
<div class="container">
<div class="header">
<h1>加密货币|Gate 机器人交易监控</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
</div>
<div class="top-nav">
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录</a>
<a href="/plan_history" class="{% if page == 'plan_history' %}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="export-bar">
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSVUTF-8;交易记录含开仓类型列及交易所对齐字段):</span>
<a href="/export/trade_records">交易记录</a>
</div>
<div class="stat-box">
<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 %}{{ money_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">{{ money_fmt(current_capital) }}U</div></div>
</div>
<div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div>
<div class="grid">
{% if page == 'trade' %}
<div class="trade-dashboard">
<div class="trade-panels-row">
<div class="card order-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>
<div class="rule-tip" id="order-rule-tip">
规则:单仓;与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
{% if can_trade %}可开仓{% else %}不可开仓(有持仓、有趋势回调计划,或未到北京时间 {{ reset_hour }}:00{% endif %}
按风险比例自动计算仓位
</div>
<div class="rule-tip">
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
<div class="rule-tip">
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ money_fmt(auto_transfer_amount) }}U,来自 {{ auto_transfer_from }}
</div>
<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 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="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
<select name="trade_style" required>
<option value="trend">趋势单</option>
<option value="swing">波段单</option>
</select>
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<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-tp" name="tgt" step="any" placeholder="止盈价格" required>
<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">开仓(以损定仓)</button>
</form>
<div class="order-live-positions">
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff">实时持仓</h3>
<div class="running-plans-stack">
{% for o in order %}
{% set osym = o.exchange_symbol or o.symbol %}
<div class="plan-position-card">
<div class="plan-card-head">
<div class="plan-card-title">
<span>{{ osym }}</span>
<span class="badge {{ 'direction-long' if o.direction == 'long' else 'direction-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
</div>
<a href="/del_order/{{ o.id }}" class="btn-close-plan" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
</div>
<div class="plan-card-meta">
来源: 下单监控 风格: {{ o.trade_style or 'trend' }} 风险: {% if o.risk_percent is not none %}{{ o.risk_percent }}%{% else %}—{% endif %}≈{{ money_fmt(o.risk_amount) }}U
{% if o.breakeven_enabled %}<span class="accent">移动保本: 开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(osym, o.breakeven_price) }}</span>{% else %}移动保本: 关{% endif %}
</div>
<div class="plan-card-grid">
<div class="plan-cell">
<span class="lbl">成交价</span>
<span class="val">{{ price_fmt(osym, o.trigger_price) }}</span>
</div>
<div class="plan-cell">
<span class="lbl">止损</span>
<span class="val">{{ price_fmt(osym, o.stop_loss) }}</span>
</div>
<div class="plan-cell">
<span class="lbl">止盈</span>
<span class="val">{{ price_fmt(osym, o.take_profit) }}</span>
</div>
<div class="plan-cell">
<span class="lbl">盈亏比</span>
<span class="val"><span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%.2f'|format(o.rr_ratio) }}:1{% else %}—{% endif %}</span></span>
</div>
<div class="plan-cell">
<span class="lbl">标记价</span>
<span class="val"><span id="order-price-{{ o.id }}">-</span></span>
</div>
<div class="plan-cell">
<span class="lbl">浮盈亏</span>
<span class="val"><span id="order-pnl-{{ o.id }}">-</span></span>
</div>
</div>
<div class="plan-card-meta" style="margin-bottom:0">
保证金: <span id="order-ex-margin-{{ o.id }}">-</span>
计划基数: {{ money_fmt(o.margin_capital) }}U
杠杆: {{ o.leverage }}x
仓位占比: {{ o.position_ratio }}%
</div>
</div>
{% else %}
<div class="plan-position-card" style="color:#8892b0;text-align:center;padding:16px">暂无机器人持仓</div>
{% endfor %}
</div>
</div>
</div>
<div class="card trend-card">
<h2 style="margin-bottom:8px">趋势回调策略</h2>
<div class="rule-tip">
<strong>生成预览</strong>:读取合约 USDT <strong>可用余额快照</strong>并计算计划(不下单)。预览有效期 <strong>{{ trend_pullback_preview_ttl }} 秒</strong><br>
<strong>确认执行</strong>:市价首仓 50% + 挂交易所止损;剩余 50% 在止损与补仓上沿之间共 {{ trend_pullback_dca_legs }} 档(程序可能因最小张数自动减档)市价补仓;<strong>止盈由程序监控</strong><br>
确认执行时若当前可用余额与预览快照相对偏差 &gt; <strong>{{ trend_preview_max_drift_pct }}%</strong> 会拒绝并要求重新预览。
</div>
<form action="{{ url_for('preview_trend_pullback') }}" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 ETH/USDT" required>
<select name="direction" required>
<option value="">方向</option>
<option value="long">做多</option>
<option value="short">做空</option>
</select>
<input name="leverage" type="number" min="1" step="1" placeholder="杠杆(必填)" required>
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" placeholder="风险%相对可用快照" title="默认5:最坏亏损约≤可用余额×5%">
<input name="sl" step="any" placeholder="止损价" required>
<input name="add_upper" step="any" placeholder="补仓上沿价" required>
<input name="take_profit" step="any" placeholder="止盈价(固定)" required>
<button type="submit" {% if not can_trade %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}>生成预览</button>
</form>
{% if trend_preview %}
<div style="margin-top:14px;padding:12px;background:#141a2e;border:1px solid #2a3150;border-radius:8px">
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px;margin-bottom:8px">
<strong style="color:#dbe4ff">当前预览(剩余 <span id="trend-preview-ttl">{{ trend_pullback_preview_ttl }}</span>s</strong>
<span style="font-size:.8rem;color:#9aa" data-expires-ms="{{ preview_expires_ms }}">倒计时加载中…</span>
</div>
<div style="font-size:.82rem;color:#cfd3ef;line-height:1.55;margin-bottom:10px">
{{ trend_preview.symbol }} {{ '做多' if trend_preview.direction == 'long' else '做空' }} {{ trend_preview.leverage }}x
预览可用快照 <strong>{{ money_fmt(trend_preview.snapshot_available_usdt) }}</strong> U 参考价 {{ price_fmt(trend_preview.symbol, trend_preview.live_price_ref) }}
计划保证金≈{{ money_fmt(trend_preview.plan_margin_capital) }} U 总张≈{{ amt_fmt(trend_preview.symbol, trend_preview.target_order_amount) }}(首仓 {{ amt_fmt(trend_preview.symbol, trend_preview.first_order_amount) }} + 补仓 {{ amt_fmt(trend_preview.symbol, trend_preview.remainder_total) }}<br>
止损 {{ price_fmt(trend_preview.symbol, trend_preview.stop_loss) }} 补仓上沿 {{ price_fmt(trend_preview.symbol, trend_preview.add_upper) }} 止盈 {{ price_fmt(trend_preview.symbol, trend_preview.take_profit) }} 风险比例 {{ trend_preview.risk_percent }}%
</div>
<div class="table-wrap" style="margin-bottom:10px">
<table>
<tr><th>#</th><th>补仓触发价</th><th>该档张数</th></tr>
{% for row in trend_preview_levels %}
<tr><td>{{ row.i }}</td><td>{{ price_fmt(trend_preview.symbol, row.price) }}</td><td>{{ amt_fmt(trend_preview.symbol, row.contracts) }}</td></tr>
{% endfor %}
</table>
</div>
<div class="form-row" style="gap:10px;align-items:center">
<form action="{{ url_for('execute_trend_pullback') }}" method="post" style="display:inline">
<input type="hidden" name="preview_id" value="{{ trend_preview.id }}">
<button type="submit" onclick="return confirm('确认按预览参数实盘下单?')">确认执行(实盘)</button>
</form>
<form action="{{ url_for('cancel_trend_pullback_preview') }}" method="post" style="display:inline">
<input type="hidden" name="preview_id" value="{{ trend_preview.id }}">
<button type="submit" style="background:#2f2134;color:#ffb2b2">取消预览</button>
</form>
</div>
</div>
<script>
(function(){
const el = document.querySelector("[data-expires-ms]");
if(!el) return;
const exp = parseInt(el.getAttribute("data-expires-ms")||"0",10);
function tick(){
const left = Math.max(0, Math.floor((exp - Date.now()) / 1000));
el.innerText = left > 0 ? ("剩余 " + left + " 秒") : "已过期,请重新生成预览";
const span = document.getElementById("trend-preview-ttl");
if(span) span.innerText = String(left);
if(left <= 0) return;
setTimeout(tick, 1000);
}
tick();
})();
</script>
{% elif trend_preview_expired %}
<div class="rule-tip" style="margin-top:12px;color:#ff8f8f">该预览已过期(超过 {{ trend_pullback_preview_ttl }} 秒),请重新点击「生成预览」。</div>
{% endif %}
<div class="trend-running-plans">
<h3 style="margin:0 0 10px;font-size:.95rem;color:#b8c4ff">运行中的计划</h3>
<div class="running-plans-stack">
{% for t in trend_plans %}
{% set sym = t.exchange_symbol or t.symbol %}
{% set calc = namespace(rr=None, pnlpct=None) %}
{% if t.avg_entry_price is not none and t.stop_loss is not none and t.take_profit is not none %}
{% set e = t.avg_entry_price|float %}
{% set sl = t.stop_loss|float %}
{% set tp = t.take_profit|float %}
{% if t.direction == 'long' %}
{% set risk = e - sl %}
{% set reward = tp - e %}
{% else %}
{% set risk = sl - e %}
{% set reward = e - tp %}
{% endif %}
{% if risk > 0 %}
{% set calc.rr = reward / risk %}
{% endif %}
{% endif %}
{% if t.floating_pnl is not none and t.plan_margin_capital is not none and t.plan_margin_capital|float > 0 %}
{% set calc.pnlpct = (t.floating_pnl|float) / (t.plan_margin_capital|float) * 100 %}
{% endif %}
<div class="plan-position-card">
<div class="plan-card-head">
<div class="plan-card-title">
<span>#{{ t.id }} {{ sym }}</span>
<span class="badge {{ 'direction-long' if t.direction == 'long' else 'direction-short' }}">{{ '做多' if t.direction == 'long' else '做空' }}</span>
</div>
<a href="/stop_trend_pullback/{{ t.id }}" class="btn-close-plan" onclick="return confirm('结束计划:市价平仓并撤掉该合约全部挂单,确定?')">结束计划</a>
</div>
<div class="plan-card-meta">
来源: 趋势回调计划 风险: {% if t.risk_percent is not none %}{{ t.risk_percent }}%{% else %}—{% endif %}
<span class="accent">补仓上沿 {{ price_fmt(sym, t.add_upper) }}</span>
| 已补仓 <strong>{{ t.legs_done }}/{{ t.dca_legs }}</strong>
</div>
<div class="plan-card-grid">
<div class="plan-cell">
<span class="lbl">均价</span>
<span class="val">{% if t.avg_entry_price is not none %}{{ price_fmt(sym, t.avg_entry_price) }}{% else %}—{% endif %}</span>
</div>
<div class="plan-cell">
<span class="lbl">止损</span>
<span class="val">{{ price_fmt(sym, t.stop_loss) }}</span>
</div>
<div class="plan-cell">
<span class="lbl">止盈</span>
<span class="val">{{ price_fmt(sym, t.take_profit) }}</span>
</div>
<div class="plan-cell">
<span class="lbl">盈亏比</span>
<span class="val">{% if calc.rr is not none %}{{ '%.2f'|format(calc.rr) }}:1{% else %}—{% endif %}</span>
</div>
<div class="plan-cell">
<span class="lbl">标记价</span>
<span class="val">{% if t.floating_mark is not none %}{{ price_fmt(sym, t.floating_mark) }}{% else %}—{% endif %}</span>
</div>
<div class="plan-cell">
<span class="lbl">浮盈亏</span>
<span class="val {% if t.floating_pnl is not none %}{% if t.floating_pnl > 0 %}pnl-profit{% elif t.floating_pnl < 0 %}pnl-loss{% else %}pnl-neutral{% endif %}{% endif %}">
{% if t.floating_pnl is not none %}
{{ money_fmt(t.floating_pnl) }}U{% if calc.pnlpct is not none %} ({{ '%+.2f'|format(calc.pnlpct) }}%){% endif %}
{% else %}—{% endif %}
</span>
</div>
</div>
<div class="plan-card-meta" style="margin-bottom:0">
快照可用: {% if t.snapshot_available_usdt is not none %}{{ money_fmt(t.snapshot_available_usdt) }}U{% else %}—{% endif %}
计划保证金≈{% if t.plan_margin_capital is not none %}{{ money_fmt(t.plan_margin_capital) }}U{% else %}—{% endif %}
总张≈{{ amt_fmt(sym, t.target_order_amount) }}(首{{ amt_fmt(sym, t.first_order_amount) }} + 补{{ amt_fmt(sym, t.remainder_total) }}
杠杆: {{ t.leverage }}x
</div>
</div>
{% else %}
<div class="plan-position-card" style="color:#8892b0;text-align:center;padding:16px">暂无运行中的趋势回调计划</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if page == 'records' %}
<div class="card full records-card">
<h2>交易记录</h2>
<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 }}">
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</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.effective_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 != '' %}{{ money_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ r.display_opened_at }}</td>
<td>{{ r.display_closed_at }}</td>
{% set pnl_val = (r.display_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ money_fmt(r.display_pnl_amount) }}</span>{% if r.monitor_type == '趋势回调' and r.display_pnl_source == 'local' %}<span style="font-size:.68rem;color:#8892b0"></span>{% elif r.monitor_type == '趋势回调' and r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></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>
{% else %}<span class="badge miss">{{ effective_result }}</span>{% endif %}
</td>
<td>
<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>
{% endif %}
</div>
{% if page == 'plan_history' %}
<div class="card full" style="margin-bottom:12px">
<h2 style="margin-bottom:6px">已结束的趋势回调计划</h2>
<div class="rule-tip" style="margin-bottom:8px">删除将同时移除 <code>trend_plan_id</code> 关联的「趋势回调」交易记录及该计划对应的预览快照归档。交易所平仓同步起点(北京日期):<strong>{{ exchange_sync_from_label }}</strong><code>EXCHANGE_POSITION_SYNC_FROM_BJ</code>)。</div>
{% if plan_history and plan_history|length > 0 %}
<div class="table-wrap">
<table>
<tr><th>ID</th><th>品种</th><th>方向</th><th>杠杆</th><th>状态</th><th>结束</th><th>开仓时间</th><th>计划保证金≈</th><th>操作</th></tr>
{% for p in plan_history %}
<tr>
<td>{{ p.id }}</td>
<td>{{ p.symbol }}</td>
<td><span class="badge {{ 'direction-long' if p.direction == 'long' else 'direction-short' }}">{{ '做多' if p.direction == 'long' else '做空' }}</span></td>
<td>{{ p.leverage }}x</td>
<td>{{ p.status_label }}</td>
<td>{{ p.message or '-' }}</td>
<td>{{ (p.opened_at or '-')[:16] }}</td>
<td>{% if p.plan_margin_capital is not none %}{{ money_fmt(p.plan_margin_capital) }}{% else %}-{% endif %}</td>
<td>
<form action="{{ url_for('delete_trend_plan_history', pid=p.id) }}" method="post" style="display:inline" onsubmit="return confirm('确定删除该计划历史及关联趋势交易记录?');">
<button type="submit" class="table-del">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="rule-tip" style="color:#8892b0">暂无已结束的计划</div>
{% endif %}
</div>
<div class="card full" style="margin-bottom:12px">
<h2 style="margin-bottom:6px">预览快照(自本版本起留存)</h2>
<div class="rule-tip" style="margin-bottom:8px">每次「生成预览」自动归档;取消、过期或执行后仍可点开查看当时参数。执行后状态为「已执行」并带关联计划 ID。</div>
{% if preview_snapshots and preview_snapshots|length > 0 %}
<div class="table-wrap">
<table>
<tr><th>ID</th><th>时间</th><th>品种</th><th>方向</th><th>杠杆</th><th>状态</th><th>快照余额U</th><th>操作</th></tr>
{% for s in preview_snapshots %}
<tr>
<td>{{ s.id }}</td>
<td>{{ (s.preview_created_at or '-')[:16] }}</td>
<td>{{ s.symbol }}</td>
<td>{{ '多' if s.direction == 'long' else '空' }}</td>
<td>{{ s.leverage }}x</td>
<td>{{ s.outcome_label }}{% if s.executed_plan_id %} #{{ s.executed_plan_id }}{% endif %}</td>
<td>{{ money_fmt(s.snapshot_available_usdt) }}</td>
<td><button type="button" class="table-del" style="background:#1f3a5a;color:#8fc8ff" onclick="openPreviewSnapshotDetail({{ s.id }})">查看</button></td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="rule-tip" style="color:#8892b0">暂无预览快照(新版本生成预览后将出现在此)</div>
{% endif %}
</div>
{% 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="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>北京时间</strong>交易日;胜率按盈笔数/(盈+亏)。各策略分列统计。<br>
历史总开仓(累计):下单监控 <strong style="color:#cfd3ef">{{ stats_bundle.total_opens_order }}</strong>
| 趋势回调 <strong style="color:#cfd3ef">{{ stats_bundle.total_opens_trend }}</strong>
(合计 <strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong> 次)
</div>
{{ period_stats_dual("日统计", stats_bundle.day) }}
{{ period_stats_dual("周统计", stats_bundle.week) }}
{{ period_stats_dual("月统计", stats_bundle.month) }}
</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>
<button type="button" class="panel-close" onclick="forceCloseDetailModal()">关闭</button>
</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>
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 forceCloseDetailModal(){document.getElementById("detailModal").style.display="none";}
function fmtU2(n){
if(n === null || n === undefined || n === "") return "-";
const x = Number(n);
if(Number.isNaN(x)) return String(n);
return x.toFixed(2);
}
function openPreviewSnapshotDetail(id){
fetch(`/api/preview_snapshot/${id}`).then(r=>r.json()).then(data=>{
if(!data.ok){ alert((data && data.msg) || "加载失败"); return; }
const s = data.snapshot;
const lines = [
`预览ID${s.preview_id || "-"}`,
`归档状态:${s.outcome_label || "-"}`,
`关联计划ID${s.executed_plan_id != null ? s.executed_plan_id : "-"}`,
"",
`${s.symbol || "-"} ${s.direction === "long" ? "做多" : "做空"} ${s.leverage || "-"}x`,
`可用快照U${fmtU2(s.snapshot_available_usdt)}`,
`参考价:${s.live_price_ref != null ? s.live_price_ref : "-"}`,
`计划保证金≈U${fmtU2(s.plan_margin_capital)}`,
`总张数:${s.target_order_amount != null ? s.target_order_amount : "-"}`,
`首仓/补仓余:${s.first_order_amount != null ? s.first_order_amount : "-"} / ${s.remainder_total != null ? s.remainder_total : "-"}`,
`补仓档数:${s.dca_legs != null ? s.dca_legs : "-"}`,
`止损 / 补仓上沿 / 止盈:${s.stop_loss} / ${s.add_upper} / ${s.take_profit}`,
`风险%${s.risk_percent != null ? s.risk_percent : "-"}`,
`网格价 JSON${s.grid_prices_json || "[]"}`,
`分档张数 JSON${s.leg_amounts_json || "[]"}`,
`创建时间:${s.preview_created_at || "-"}`,
`预览过期(ms)${s.expires_at_ms != null ? s.expires_at_ms : "-"}`,
].join("\n");
document.getElementById("detailTitle").innerText = `预览快照 #${id}`;
document.getElementById("detailBody").innerText = lines;
document.getElementById("detailImage").style.display = "none";
document.getElementById("detailModal").style.display = "flex";
}).catch(()=>{ alert("网络错误"); });
}
function closeDetailModal(e){if(e.target && e.target.id==="detailModal"){forceCloseDetailModal();}}
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){
const o = journalCache[id];
if(!o){ return; }
const moodTags = (o.mood_issues || []).join(",") || "无";
const detail = [
`币种/周期:${o.coin || "-"} ${o.tf || "-"}`,
`开仓时间:${o.open_datetime || "-"}`,
`平仓时间:${o.close_datetime || "-"}`,
`持仓时长:${o.hold_duration || "-"}`,
`盈亏:${o.pnl || "-"}U`,
`开仓类型:${o.entry_reason || "无"}`,
`平仓/离场:${formatJournalExitOneLine(o)}`,
`预期RR${o.expect_rr || "-"}`,
`实际RR${o.real_rr || "-"}`,
`保本后盯盘:${o.post_breakeven_stare || "-"}`,
`占用时新开仓:${o.new_trade_while_occupied || "-"}`,
`心态标签:${moodTags}`,
`备注:${o.note || "无"}`,
].join("\n");
document.getElementById("detailTitle").innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
document.getElementById("detailBody").innerText = detail;
const imgEl = document.getElementById("detailImage");
if(o.image){
imgEl.src = `/static/images/${o.image}`;
imgEl.style.display = "block";
} else {
imgEl.src = "";
imgEl.style.display = "none";
}
document.getElementById("detailModal").style.display = "flex";
}
function openReviewDetail(id){
const r = reviewCache[id];
if(!r){ return; }
document.getElementById("detailTitle").innerText = `${r.review_type === "daily" ? "日复盘" : "周复盘"}${r.target_date || "-"}`;
document.getElementById("detailBody").innerText = r.content || "";
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
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("止损价格(核对后用于统计)", String(t.stop_loss ?? ""));
if(stopLoss === null) return;
const takeProfit = prompt("止盈价格(核对后用于统计)", String(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 loadJournals(){
fetch("/api/journals").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(){
fetch("/api/reviews").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}')">查看全文</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(){
const d = document.getElementById("day_date").value;
if(!d){alert("请选择日期");return;}
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
.then(r=>r.json()).then(data=>{
const el=document.getElementById("daily_result");
el.innerText=data.result;
el.style.display="block";
loadReviews();
});
}
function genWeekly(){
const s=document.getElementById("week_start").value;
const e=document.getElementById("week_end").value;
if(!s || !e){alert("请选择起止日期");return;}
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
.then(r=>r.json()).then(data=>{
const el=document.getElementById("weekly_result");
el.innerText=data.result;
el.style.display="block";
loadReviews();
});
}
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(["保本止盈","移动止盈","手动平仓","止损","其他"]);
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;
}
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 entryHint = document.getElementById("entry-price-hint");
if(entryHint){ entryHint.value = t.trigger_price || ""; }
const stopHint = document.getElementById("stop-loss-hint");
if(stopHint){ stopHint.value = t.stop_loss || ""; }
const dirHint = document.getElementById("direction-hint");
if(dirHint){ dirHint.value = t.direction || "long"; }
setJournalField("early_exit_trigger", "");
setJournalField("early_exit_note", "");
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 || "-"} | 入场:${t.trigger_price || "-"} 止损:${t.stop_loss || "-"} 止盈:${t.take_profit || "-"} | 类型:${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 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 ? "展开" : "折叠";
}
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();
}
}
const keyForm = document.getElementById("key-form");
if(keyForm){
keyForm.addEventListener("submit", (e)=>{
e.preventDefault();
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if(!symbol){
alert("请先输入交易对");
return;
}
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) || "日成交量排名读取失败");
return;
}
if(!data.in_top30){
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前30,已拦截。`);
return;
}
keyForm.submit();
})
.catch(()=>alert("日成交量排名检查失败,请稍后重试"));
});
}
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
setTimeout(() => {
if(document.getElementById("journal-list")) loadJournals();
if(document.getElementById("review-list")) loadReviews();
}, 300);
let latestAvailableUsdt = null;
const lastPriceMap = {};
function formatSigned(v, digits=4){
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 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 = 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); })();
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
const decimals = hasMark ? 8 : 6;
pEl.innerText = px.toFixed(decimals);
paintPriceTrend(pEl, `o-${o.id}`, 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("pnl-profit","pnl-loss","pnl-neutral");
const fp = Number(o.float_pnl);
if(fp > 0) pnlEl.classList.add("pnl-profit");
else if(fp < 0) pnlEl.classList.add("pnl-loss");
else pnlEl.classList.add("pnl-neutral");
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl){
rrEl.innerText = (typeof o.rr_ratio !== "undefined" && o.rr_ratio !== null) ? `${Number(o.rr_ratio).toFixed(2)}:1` : "-";
}
});
}).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;
}
}
}).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);
}
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00";
const tip = document.getElementById("order-rule-tip");
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : "";
if(tip){
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`;
}
}).catch(()=>{});
}
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 : "price";
const slEl = document.getElementById("order-sl");
const tpEl = document.getElementById("order-tp");
const slPctEl = document.getElementById("order-sl-pct");
const tpPctEl = document.getElementById("order-tp-pct");
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
const pct = mode === "pct";
slEl.style.display = pct ? "none" : "";
tpEl.style.display = pct ? "none" : "";
slEl.required = !pct;
tpEl.required = !pct;
slPctEl.style.display = pct ? "" : "none";
tpPctEl.style.display = pct ? "" : "none";
slPctEl.required = pct;
tpPctEl.required = pct;
}
if(sltpModeEl){
sltpModeEl.addEventListener("change", toggleSltpMode);
toggleSltpMode();
}
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();
}
refreshOrderDefaults();
refreshPriceSnapshot();
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }});
</script>
</body>
</html>