Files
qihuo/templates/records.html
T
dekun 9f48f22d16 Gate order cancel to trading hours and sync trade logs from CTP.
Disable cancel UI outside sessions, query exchange fills for records, and label local vs counterparty rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 00:35:51 +08:00

363 lines
23 KiB
HTML

{% extends "base.html" %}
{% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %}
{% block content %}
<div class="card records-equity-card" style="margin-bottom:1.25rem">
<h2>资金曲线</h2>
<div class="card-body">
<div id="equity-curve-chart" style="width:100%;min-height:220px;border-radius:6px;overflow:hidden"></div>
</div>
</div>
<div class="card records-trade-card" style="margin-bottom:1.25rem">
<h2>交易记录</h2>
<div class="card-body">
{% if ctp_sync_info and ctp_sync_info.connected %}
<p class="hint" style="margin-top:0">
已连接 CTP,本页已自动同步柜台成交(新增 {{ ctp_sync_info.synced or 0 }} 条,更新 {{ ctp_sync_info.updated or 0 }} 条)。
带来源「柜台」的记录以交易所成交为准;「本地」为程序写入,可手动删除错误项。
</p>
{% else %}
<p class="hint" style="margin-top:0">CTP 未连接时仅显示本地数据库记录;连接后打开本页会自动同步柜台成交。</p>
{% endif %}
<label class="trade-switch-label">
<input type="checkbox" id="trade-edit-switch">
<span>修改/核对开关(开启后可编辑关键字段)</span>
</label>
<div class="trade-table-wrap">
<table class="trade-table">
<thead>
<tr>
<th>品种</th><th>类型</th><th>方向</th>
<th>成交</th><th>止损(开仓)</th><th>止盈</th>
<th>手数</th><th>保证金</th><th>保证金占比</th><th>持仓分钟</th>
<th>开仓时间</th><th>平仓时间</th>
<th>盈亏(元)</th><th>手续费</th><th>净盈亏</th><th>最新资金</th><th>结果</th><th>操作</th>
</tr>
</thead>
<tbody>
{% for t in trades %}
<tr data-trade-id="{{ t.id }}">
<td><span class="cell-readonly">{{ t.symbol_name or t.symbol }}</span></td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.monitor_type }}</span>
{% if t.source == 'ctp' %}
<span class="badge" style="margin-left:.25rem;font-size:.65rem">柜台</span>
{% else %}
<span class="text-muted" style="margin-left:.25rem;font-size:.65rem">本地</span>
{% endif %}
<input class="cell-edit-show" type="hidden" name="monitor_type" value="{{ t.monitor_type }}">
</td>
<td>
<span class="cell-readonly cell-edit-hide">
<span class="badge dir">{{ '做多' if t.direction == 'long' else '做空' }}</span>
</span>
<select class="cell-edit-show" name="direction" style="display:none">
<option value="long" {% if t.direction=='long' %}selected{% endif %}>做多</option>
<option value="short" {% if t.direction=='short' %}selected{% endif %}>做空</option>
</select>
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.entry_price }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="entry_price" value="{{ t.entry_price }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.stop_loss }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="stop_loss" value="{{ t.stop_loss }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.take_profit }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="take_profit" value="{{ t.take_profit }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.lots }}</span>
<input class="cell-edit-show" type="number" step="0.01" name="lots" value="{{ t.lots }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.margin if t.margin is not none else '-' }}</span>
<input class="cell-edit-show" type="number" step="0.01" name="margin" value="{{ t.margin or '' }}" placeholder="保证金" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">
{% if t.margin_pct is not none %}{{ t.margin_pct }}%{% else %}-{% endif %}
</span>
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.holding_minutes or 0 }}</span>
<input class="cell-edit-show" type="number" name="holding_minutes" value="{{ t.holding_minutes or 0 }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ (t.open_time or '')[:16].replace('T',' ') }}</span>
<input class="cell-edit-show" type="text" name="open_time" value="{{ t.open_time or '' }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide">{{ (t.close_time or '')[:16].replace('T',' ') }}</span>
<input class="cell-edit-show" type="text" name="close_time" value="{{ t.close_time or '' }}" style="display:none">
</td>
<td>
<span class="cell-readonly cell-edit-hide {% if t.pnl and t.pnl > 0 %}text-profit{% elif t.pnl and t.pnl < 0 %}text-loss{% endif %}">
{{ t.pnl if t.pnl is not none else '-' }}
</span>
<input class="cell-edit-show" type="number" step="0.01" name="pnl" value="{{ t.pnl or '' }}" style="display:none">
<input type="hidden" name="close_price" value="{{ t.close_price or '' }}">
<input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}">
</td>
<td><span class="cell-readonly text-muted">{{ t.fee if t.fee is not none else '-' }}</span></td>
<td>
<span class="cell-readonly {% if t.pnl_net and t.pnl_net > 0 %}text-profit{% elif t.pnl_net and t.pnl_net < 0 %}text-loss{% endif %}">
{{ t.pnl_net if t.pnl_net is not none else '-' }}
</span>
</td>
<td>
<span class="cell-readonly">{{ t.equity_after if t.equity_after is not none else '-' }}</span>
</td>
<td>
<span class="cell-readonly cell-edit-hide">
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '止损' %}<span class="badge loss">{{ t.result }}</span>
{% elif t.result == '移动止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '保本止盈' %}<span class="badge profit">{{ t.result }}</span>
{% elif t.result == '手动平仓' %}<span class="badge result-manual">{{ t.result }}</span>
{% else %}<span class="badge result-external">{{ t.result }}</span>{% endif %}
{% if t.verified %}<span class="badge active" style="margin-left:.25rem">已核对</span>{% endif %}
</span>
<select class="cell-edit-show" name="result" style="display:none">
<option value="手动平仓" {% if t.result=='手动平仓' %}selected{% endif %}>手动平仓</option>
<option value="止盈" {% if t.result=='止盈' %}selected{% endif %}>止盈</option>
<option value="止损" {% if t.result=='止损' %}selected{% endif %}>止损</option>
<option value="移动止盈" {% if t.result=='移动止盈' %}selected{% endif %}>移动止盈</option>
<option value="保本止盈" {% if t.result=='保本止盈' %}selected{% endif %}>保本止盈</option>
</select>
</td>
<td>
<div class="trade-actions">
<a href="{{ url_for('fill_review_from_trade', tid=t.id) }}" class="btn-fill">填入复盘</a>
<button type="button" class="btn-verify trade-save-btn" disabled>核对修改</button>
<a href="{{ url_for('del_trade', tid=t.id) }}" class="btn-del" onclick="return confirm('删除?')">删除</a>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="18" class="text-muted">暂无交易记录</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="split-grid records-split">
<div class="card" id="review-panel">
<h2>复盘上传</h2>
<div class="card-body">
<form id="review-form" action="{{ url_for('add_review') }}" method="post" enctype="multipart/form-data" class="form-compact form-compact-review line-tight">
<div class="form-line line-4">
<div class="symbol-wrap">
<input type="text" class="symbol-input" placeholder="品种" autocomplete="off" required>
<input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required>
<input type="hidden" name="sina_code">
<div class="symbol-dropdown"></div>
<div class="symbol-selected"></div>
</div>
<select name="direction" required>
<option value="">方向</option>
<option value="long">做多</option>
<option value="short">做空</option>
</select>
<input name="lots" type="number" step="1" min="1" value="1" placeholder="张数" required>
<input name="timeframe" value="5m" placeholder="周期">
</div>
<div class="form-line line-4">
<input name="entry_price" type="number" step="0.0001" placeholder="成交价" required>
<input name="stop_loss" type="number" step="0.0001" placeholder="止损" required>
<input name="take_profit" type="number" step="0.0001" placeholder="止盈" required>
<input name="close_price" type="number" step="0.0001" placeholder="平仓价" required>
</div>
<div class="form-line line-4">
<div class="mini-field"><span>开仓时间</span><input type="datetime-local" name="open_time" required></div>
<div class="mini-field"><span>平仓时间</span><input type="datetime-local" name="close_time" required></div>
<input id="holding_duration" type="text" readonly class="calc-readonly" placeholder="持仓时长(自动)">
<input name="pnl" type="number" step="0.01" placeholder="盈亏金额(手动)">
</div>
<div class="form-line line-2">
<input id="initial_rr" type="text" readonly class="calc-readonly" placeholder="初始盈亏比(自动)">
<input id="actual_rr" type="text" readonly class="calc-readonly" placeholder="实际盈亏比(自动)">
</div>
<div class="form-line line-4">
<select name="open_type" required>
<option value="">开仓类型</option>
{% for t in open_types %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
</select>
<select name="exit_trigger" required>
<option value="">离场触发</option>
{% for t in exit_triggers %}<option value="{{ t }}">{{ t }}</option>{% endfor %}
</select>
<input name="exit_supplement" placeholder="离场补充">
<select name="watch_after_breakeven" title="保本后盯盘">
<option value="否">保本盯盘:否</option>
<option value="是">保本盯盘:是</option>
</select>
</div>
<div class="form-line line-4">
<select name="new_position_while_occupied" title="占用时新开仓">
<option value="否">占用新开:否</option>
<option value="是">占用新开:是</option>
</select>
<input type="file" name="screenshot" accept="image/*" title="截图上传">
<textarea name="notes" placeholder="备注" rows="1"></textarea>
<button type="submit" class="btn-primary">保存</button>
</div>
<div class="kline-row">
<label><input type="checkbox" name="auto_kline" value="1" checked> 自动K线</label>
<select name="kline_period1" title="周期1">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='15m' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
<select name="kline_period2" title="周期2">{% for p in kline_periods %}<option value="{{ p }}" {% if p=='1h' %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
<input name="kline_count" type="number" value="300" placeholder="K线数" title="K线数">
<select name="kline_cutoff" title="K线截止">{% for c in kline_cutoffs %}<option value="{{ c }}">{{ c }}</option>{% endfor %}</select>
</div>
<p class="section-hint">勾选行为标签即为情绪单</p>
<div class="tag-grid">
{% for tag in behavior_tags %}
<label><input type="checkbox" name="tag_{{ tag }}" value="1"> {{ tag }}</label>
{% endfor %}
</div>
</form>
</div>
</div>
<div class="card">
<h2>复盘历史</h2>
<div class="card-body">
<div class="preset-tabs">
<a href="{{ url_for('records', preset='today') }}" class="{% if preset=='today' %}active{% endif %}">本日</a>
<a href="{{ url_for('records', preset='week') }}" class="{% if preset=='week' %}active{% endif %}">本周</a>
<a href="{{ url_for('records', preset='month') }}" class="{% if preset=='month' %}active{% endif %}">本月</a>
<a href="{{ url_for('records', preset='custom') }}" class="{% if preset=='custom' %}active{% endif %}">自定义</a>
</div>
{% if preset == 'custom' or start or end %}
<form method="get" class="filter-row">
<input type="hidden" name="preset" value="custom">
<div class="field"><label>开始</label><input type="date" name="start" value="{{ start }}"></div>
<div class="field"><label>结束</label><input type="date" name="end" value="{{ end }}"></div>
<button type="submit" class="btn-primary">筛选</button>
</form>
{% endif %}
<div class="card-scroll">
<table>
<thead>
<tr>
<th>平仓</th><th>品种</th><th>方向</th><th>盈亏</th><th>手续费</th><th>净盈亏</th><th>情绪单</th><th>详情</th><th></th>
</tr>
</thead>
<tbody>
{% for r in reviews %}
<tr class="{% if r.is_emotion %}row-emotion{% endif %}">
<td>{{ r.close_time[:16] if r.close_time else '' }}</td>
<td>{{ r.symbol_name or r.symbol }}</td>
<td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td>
<td>
{% if r.pnl and r.pnl > 0 %}<span class="badge profit">{{ r.pnl }}</span>
{% elif r.pnl and r.pnl < 0 %}<span class="badge loss">{{ r.pnl }}</span>
{% else %}{{ r.actual_pnl or '-' }}{% endif %}
</td>
<td class="text-muted">{{ r.fee if r.fee is not none else '-' }}</td>
<td>
{% if r.pnl_net and r.pnl_net > 0 %}<span class="badge profit">{{ r.pnl_net }}</span>
{% elif r.pnl_net and r.pnl_net < 0 %}<span class="badge loss">{{ r.pnl_net }}</span>
{% else %}-{% endif %}
</td>
<td>{% if r.is_emotion %}<span class="badge emotion">情绪单</span>{% else %}-{% endif %}</td>
<td>
<button type="button" class="btn-link review-view-btn" data-review='{{ {
"symbol": r.symbol_name or r.symbol, "symbol_code": r.symbol,
"direction": "做多" if r.direction=="long" else "做空",
"lots": r.lots, "timeframe": r.timeframe,
"entry_price": r.entry_price, "stop_loss": r.stop_loss, "take_profit": r.take_profit,
"close_price": r.close_price,
"open_time": r.open_time, "close_time": r.close_time,
"holding_duration": r.holding_duration,
"initial_pnl": r.initial_pnl, "actual_pnl": r.actual_pnl, "pnl": r.pnl,
"fee": r.fee, "pnl_net": r.pnl_net,
"open_type": r.open_type,
"exit_trigger": r.exit_trigger, "exit_supplement": r.exit_supplement,
"watch_after_breakeven": r.watch_after_breakeven,
"new_position_while_occupied": r.new_position_while_occupied,
"is_emotion": r.is_emotion, "behavior_tags": r.behavior_tags,
"notes": r.notes, "screenshot": r.screenshot
} | tojson }}'>放大查看</button>
</td>
<td><a href="{{ url_for('del_review', rid=r.id) }}" class="btn-del" onclick="return confirm('删除?')"></a></td>
</tr>
{% else %}
<tr><td colspan="9" class="text-muted">暂无复盘记录</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div id="review-modal" class="modal-mask">
<div class="modal-box review-modal-fullscreen">
<span class="modal-close"></span>
<h3>复盘详情</h3>
<div id="review-modal-body" class="review-modal-body"></div>
</div>
</div>
{% if auto_records %}
<div class="card" style="margin-top:1rem">
<h2>系统自动记录(止盈/止损)</h2>
<table>
<thead><tr><th>品种</th><th>类型</th><th>方向</th><th>触发价</th><th>结果</th><th>时间</th></tr></thead>
<tbody>
{% for r in auto_records %}
<tr>
<td>{{ r.symbol_name or r.symbol }}</td>
<td>{{ r.monitor_type }}</td>
<td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td>
<td>{{ r.trigger_price }}</td>
<td>{% if r.result == '止盈' %}<span class="badge profit">止盈</span>{% else %}<span class="badge loss">止损</span>{% endif %}</td>
<td>{{ r.created_at[:16] if r.created_at else '' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script>window.__EQUITY_CURVE__ = {{ equity_curve | default([]) | tojson }};</script>
<script src="{{ url_for('static', filename='js/equity_curve.js') }}"></script>
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
<script src="{{ url_for('static', filename='js/trades.js') }}"></script>
{% if prefill %}
<script>
document.addEventListener('DOMContentLoaded', function () {
var form = document.getElementById('review-form');
var panel = document.getElementById('review-panel');
if (!form) return;
var map = {{ prefill | tojson }};
Object.keys(map).forEach(function (k) {
var el = form.querySelector('[name="' + k + '"]');
if (el && map[k] != null && map[k] !== '') el.value = map[k];
});
var symInput = form.querySelector('.symbol-input');
if (symInput && map.symbol_name) symInput.value = map.symbol_name;
if (typeof recalc === 'function') recalc();
if (panel) panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
</script>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function () {
if (window.location.hash === '#review-panel') {
var panel = document.getElementById('review-panel');
if (panel) panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
</script>
{% endblock %}