1522 lines
84 KiB
HTML
1522 lines
84 KiB
HTML
<!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}
|
||
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
|
||
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
|
||
.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 %}
|
||
|
||
|
||
{% if page in ('records', 'plan_history') %}
|
||
<div class="list-window-bar">
|
||
<span style="color:#cfd3ef">列表筛选(<strong>UTC</strong>,默认当日):{{ list_window.label }}</span>
|
||
<label>预设
|
||
<select id="win-preset-select" onchange="toggleListWindowCustom()">
|
||
<option value="utc_today" {% if list_window.preset == 'utc_today' %}selected{% endif %}>UTC 当日</option>
|
||
<option value="utc_last24h" {% if list_window.preset == 'utc_last24h' %}selected{% endif %}>近 24 小时</option>
|
||
<option value="utc_last7d" {% if list_window.preset == 'utc_last7d' %}selected{% endif %}>近 7 天</option>
|
||
<option value="custom" {% if list_window.preset == 'custom' %}selected{% endif %}>自定义</option>
|
||
</select>
|
||
</label>
|
||
<span id="win-custom-range" style="{% if list_window.preset != 'custom' %}display:none{% endif %}">
|
||
<label>起(UTC) <input type="datetime-local" id="win-from-utc" value="{{ list_window.start_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
|
||
<label>止(UTC) <input type="datetime-local" id="win-to-utc" value="{{ list_window.end_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
|
||
</span>
|
||
<button type="button" style="padding:6px 12px" onclick="applyListWindow()">应用</button>
|
||
<span style="color:#8892b0;font-size:.75rem">统计页仍按北京时间 {{ reset_hour }}:00 切日</span>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="export-bar">
|
||
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSV,UTF-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% + 挂交易所止损;首仓后可<strong>手动保本</strong>(默认均价+{{ trend_manual_breakeven_offset_pct }}%);剩余 50% 在止损与补仓区间之间共 {{ trend_pullback_dca_legs }} 档(做多为<strong>上沿</strong>、做空为<strong>下沿</strong>;程序可能因最小张数自动减档)市价补仓;<strong>止盈由程序监控</strong>。<br>
|
||
确认执行时若当前可用余额与预览快照相对偏差 > <strong>{{ trend_preview_max_drift_pct }}%</strong> 会拒绝并要求重新预览。
|
||
</div>
|
||
<form id="trend-pullback-form" action="{{ url_for('preview_trend_pullback') }}" method="post" class="form-row">
|
||
<input name="symbol" placeholder="BTC 或 ETH/USDT" required>
|
||
<select name="direction" id="trend-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" id="trend-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>
|
||
<script>
|
||
|
||
function listWindowQueryString(){
|
||
const presetEl = document.getElementById("win-preset-select");
|
||
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
|
||
const q = new URLSearchParams(window.location.search);
|
||
q.set("win_preset", preset);
|
||
if(preset === "custom"){
|
||
const fromEl = document.getElementById("win-from-utc");
|
||
const toEl = document.getElementById("win-to-utc");
|
||
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
|
||
else q.delete("from_utc");
|
||
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
|
||
else q.delete("to_utc");
|
||
} else {
|
||
q.delete("from_utc");
|
||
q.delete("to_utc");
|
||
}
|
||
return q.toString();
|
||
}
|
||
function toggleListWindowCustom(){
|
||
const preset = document.getElementById("win-preset-select");
|
||
const box = document.getElementById("win-custom-range");
|
||
if(!preset || !box) return;
|
||
box.style.display = preset.value === "custom" ? "" : "none";
|
||
}
|
||
function applyListWindow(){
|
||
const qs = listWindowQueryString();
|
||
const path = window.location.pathname || "/records";
|
||
window.location.href = qs ? (path + "?" + qs) : path;
|
||
}
|
||
function attachListWindowToExports(){
|
||
const qs = listWindowQueryString();
|
||
if(!qs) return;
|
||
document.querySelectorAll('.export-bar a[href^="/export/trade_records"]').forEach(a=>{
|
||
const base = a.getAttribute("href").split("?")[0];
|
||
a.setAttribute("href", base + "?" + qs);
|
||
});
|
||
}
|
||
|
||
(function(){
|
||
const dirSel = document.getElementById("trend-direction");
|
||
const addInp = document.getElementById("trend-add-upper");
|
||
function syncAddUpperPlaceholder(){
|
||
if(!addInp || !dirSel) return;
|
||
const d = (dirSel.value || "long").toLowerCase();
|
||
addInp.placeholder = d === "short" ? "补仓下沿价" : "补仓上沿价";
|
||
}
|
||
if(dirSel){
|
||
dirSel.addEventListener("change", syncAddUpperPlaceholder);
|
||
syncAddUpperPlaceholder();
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
{% 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) }} | {{ trend_add_zone_label(trend_preview.direction) }} {{ 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">{{ trend_add_zone_label(t.direction) }} {{ 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-top:8px">
|
||
<form action="{{ url_for('trend_pullback_breakeven', pid=t.id) }}" method="post" class="form-row" style="margin:0;align-items:center" onsubmit="return confirm('将交易所止损移至持仓均价+偏移?仅当新止损优于当前止损时生效。');">
|
||
<label style="font-size:.78rem;color:#cfd3ef;display:flex;align-items:center;gap:6px">
|
||
手动保本 偏移%
|
||
<input name="breakeven_offset_pct" type="number" min="0" step="0.01" value="{{ trend_manual_breakeven_offset_pct }}" style="width:72px;padding:4px 8px">
|
||
(默认均价+{{ trend_manual_breakeven_offset_pct }}%)
|
||
</label>
|
||
<button type="submit" style="padding:6px 12px;background:#1f4a3a;color:#8fc8ff">应用保本止损</button>
|
||
{% if t.breakeven_applied %}<span style="color:#6ab88a;font-size:.75rem">已保本 {{ (t.breakeven_applied_at or '')[:16] }}</span>{% endif %}
|
||
{% if t.initial_stop_loss is not none and t.initial_stop_loss != t.stop_loss %}<span style="color:#8892b0;font-size:.75rem">原止损 {{ price_fmt(sym, t.initial_stop_loss) }}</span>{% endif %}
|
||
</form>
|
||
</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.direction === "long" ? "补仓上沿" : "补仓下沿"} / 止盈:${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 ? "展开" : "折叠";
|
||
}
|
||
|
||
attachListWindowToExports();
|
||
toggleListWindowCustom();
|
||
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> |