Files
finance-talk/ft-app/app/templates/trades.html
T
fish e1dc4595a2 持仓列表增加品种子tab切换
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:08:34 +08:00

405 lines
20 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.
{% extends "base.html" %}
{% block title %}交易记录{% endblock %}
{% block heading %}交易记录{% endblock %}
{% block breadcrumb %}开平仓记录{% endblock %}
{% block content %}
{% set view = request.query_params.get('view', '') %}
{% set product_tab = request.query_params.get('tab', '全部') %}
{% set product_codes = product_contracts.keys()|list %}
{% set grouped_open = {} %}
{% for t in open_trades %}
{% set _ = grouped_open.setdefault(t.product_code, []).append(t) %}
{% endfor %}
<style>
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
.tab-btn {
padding: 10px 20px; border: none; background: none;
font-size: 0.88rem; font-weight: 500; color: var(--sub);
cursor: pointer; text-decoration: none; border-bottom: 2px solid transparent;
margin-bottom: -2px; transition: color .12s, border-color .12s;
}
.tab-btn:hover { color: var(--fg); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.sub-tabs { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
.sub-tab-btn {
padding: 4px 14px; border: 1px solid var(--border); background: var(--surface);
font-size: 0.78rem; color: var(--sub); cursor: pointer; text-decoration: none;
border-radius: 4px; transition: color .12s, border-color .12s;
}
.sub-tab-btn:hover { color: var(--fg); border-color: var(--accent); }
.sub-tab-btn.active { color: var(--accent); border-color: var(--accent); font-weight: 600; background: var(--accent-light); }
</style>
<div class="tabs">
<a href="/trades/" class="tab-btn{% if view != 'closed' %} active{% endif %}">📋 持仓</a>
<a href="/trades/?view=closed" class="tab-btn{% if view == 'closed' %} active{% endif %}">📊 已平仓</a>
</div>
{% if view != 'closed' %}
<div class="tab-panel active">
{# ── 新建开仓 ── #}
<div class="section-title">新建开仓</div>
<div class="form-card" style="margin-bottom:28px;max-width:100%;">
<form method="post" action="/trades/open">
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
<label>品种</label>
<select id="productSelect" required onchange="filterContracts()">
<option value="">--</option>
{% for pcode in product_contracts %}
<option value="{{ pcode }}">{{ pcode }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:120px;">
<label>合约</label>
<select name="contract_code" id="contractSelect" required disabled>
<option value="">-- 先选品种 --</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:70px;">
<label>方向</label>
<select name="direction" required>
{% for v, label in directions %}
<option value="{{ v }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
<label>开仓日期</label>
<input type="date" name="open_date" value="{{ today }}" required style="font-size:0.85rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
<label>开仓价</label>
<input type="number" step="any" name="open_price" required placeholder="1300" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
<label>手续费</label>
<input type="number" step="any" name="open_fee" value="0" style="font-size:0.9rem;">
</div>
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">开仓</button>
</div>
</form>
</div>
{# ── 持仓列表 ── #}
<div class="section-title">持仓中 · {{ open_trades|length }} 笔</div>
<div class="sub-tabs">
<a href="/trades/?tab=全部" class="sub-tab-btn{% if product_tab == '全部' %} active{% endif %}">全部</a>
{% for pc in product_codes %}
{% set count = grouped_open.get(pc, [])|length %}
<a href="/trades/?tab={{ pc }}" class="sub-tab-btn{% if product_tab == pc %} active{% endif %}">{{ pc }}{% if count %} · {{ count }}{% endif %}</a>
{% endfor %}
</div>
{% if open_trades %}
{% for pc in product_codes %}
{% if product_tab == '全部' or product_tab == pc %}
{% set trades = grouped_open.get(pc, []) %}
{% if trades %}
<div class="table-wrap" style="margin-bottom:24px;">
{% if product_tab == '全部' %}
<div class="section-title" style="font-size:0.88rem;">{{ pc }} · {{ trades|length }} 笔</div>
{% endif %}
<table>
<tr><th>合约</th><th>方向</th><th>开仓日期</th><th>开仓价</th><th>手续费</th><th>操作</th></tr>
{% for t in trades %}
<tr>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td>
{% if t.direction == 'short' %}
<span class="badge badge-down"></span>
{% else %}
<span class="badge badge-up"></span>
{% endif %}
</td>
<td>{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td>{{ t.open_price }}</td>
<td>{{ t.open_fee or 0 }}</td>
<td>
<button style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
onclick="showCloseForm({{ t.id }})">平仓</button>
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', null, null, null, '{{ t.status }}')">编辑</button>
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% endif %}
{% endfor %}
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div>
{% endif %}
{# ── 平仓弹窗 ── #}
<div id="closeFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hideCloseForm()"></div>
<div id="closeFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:360px;max-width:90vw;">
<h3 style="margin-bottom:16px;">平仓</h3>
<form method="post" id="closeForm">
<div class="form-group">
<label>平仓日期</label>
<input type="date" name="close_date" value="{{ today }}" required style="font-size:0.85rem;">
</div>
<div class="form-group">
<label>平仓价格</label>
<input type="number" step="any" name="close_price" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>手续费</label>
<input type="number" step="any" name="close_fee" value="0" style="font-size:0.9rem;">
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideCloseForm()">取消</button>
<button type="submit" class="btn btn-primary">确认平仓</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="tab-panel active">
{# ═══════════════ 已平仓 ═══════════════ #}
{% if closed_trades %}
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
<div class="table-wrap">
<table>
<tr><th>品种</th><th>合约</th><th>方向</th><th>开仓</th><th>平仓</th><th>持仓</th><th>开仓价</th><th>平仓价</th><th>开仓费</th><th>平仓费</th><th>盈亏</th><th>操作</th></tr>
{% for t in closed_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td>
{% if t.direction == 'short' %}
<span class="badge badge-down"></span>
{% else %}
<span class="badge badge-up"></span>
{% endif %}
</td>
<td style="font-size:0.8rem;">{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td style="font-size:0.8rem;">{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}</td>
<td>{{ (t.close_date - t.open_date).days }}天</td>
<td>{{ t.open_price }}</td>
<td>{{ t.close_price }}</td>
<td>{{ t.open_fee or 0 }}</td>
<td>{{ t.close_fee or 0 }}</td>
<td>
{% set p = t.pnl %}
{% if p is not none %}
<span style="font-weight:700;{% if p > 0 %}color:var(--danger-fg);{% elif p < 0 %}color:var(--success-fg);{% else %}color:var(--sub);{% endif %}">
{% if p > 0 %}+{% endif %}{{ p }}
</span>
<span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;"
onclick="showPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.close_price }}, {{ t.open_fee or 0 }}, {{ t.close_fee or 0 }}, {{ p }})" title="计算过程">&#9432;</span>
{% endif %}
</td>
<td>
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', {{ t.close_price }}, {{ t.close_fee or 0 }}, '{{ t.close_date }}', '{{ t.status }}')">编辑</button>
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% set ns = namespace(total_pnl=0, total_open_fee=0, total_close_fee=0) %}
{% for t in closed_trades %}
{% set ns.total_pnl = ns.total_pnl + (t.pnl or 0) %}
{% set ns.total_open_fee = ns.total_open_fee + (t.open_fee or 0) %}
{% set ns.total_close_fee = ns.total_close_fee + (t.close_fee or 0) %}
{% endfor %}
<div style="display:flex;gap:24px;margin-bottom:24px;padding:12px 18px;background:var(--surface);border:1px solid var(--border);border-radius:8px;font-size:0.88rem;flex-wrap:wrap;">
<span>盈亏合计 <b style="{% if ns.total_pnl > 0 %}color:var(--danger-fg);{% elif ns.total_pnl < 0 %}color:var(--success-fg);{% endif %}">{% if ns.total_pnl > 0 %}+{% endif %}{{ '%.2f'|format(ns.total_pnl) }}</b></span>
<span style="color:var(--sub);">|</span>
<span>开仓手续费 <b>{{ '%.2f'|format(ns.total_open_fee) }}</b></span>
<span style="color:var(--sub);">|</span>
<span>平仓手续费 <b>{{ '%.2f'|format(ns.total_close_fee) }}</b></span>
<span style="color:var(--sub);">|</span>
<span>手续费合计 <b>{{ '%.2f'|format(ns.total_open_fee + ns.total_close_fee) }}</b></span>
</div>
{% else %}
<div style="text-align:center;padding:60px;color:var(--sub);">暂无已平仓记录</div>
{% endif %}
</div>
{% endif %}
{% if view != 'closed' %}
<script>
var productContracts = {{ product_contracts | tojson }};
function filterContracts() {
var prod = document.getElementById('productSelect').value;
var sel = document.getElementById('contractSelect');
sel.innerHTML = '<option value="">-- 选择合约 --</option>';
sel.disabled = !prod;
if (prod && productContracts[prod]) {
productContracts[prod].forEach(function(c) {
var display = c.startsWith(prod) ? c.substring(prod.length) : c;
sel.innerHTML += '<option value="' + c + '">' + display + '</option>';
});
}
}
function showCloseForm(tradeId) {
document.getElementById('closeForm').action = '/trades/' + tradeId + '/close';
document.getElementById('closeFormOverlay').style.display = 'block';
document.getElementById('closeFormBox').style.display = 'block';
}
function hideCloseForm() {
document.getElementById('closeFormOverlay').style.display = 'none';
document.getElementById('closeFormBox').style.display = 'none';
}
</script>
{% endif %}
<div id="editFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:98;" onclick="hideEditForm()"></div>
<div id="editFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:99;width:400px;max-width:90vw;max-height:90vh;overflow-y:auto;">
<h3 style="margin-bottom:16px;">编辑记录</h3>
<form method="post" id="editForm">
<div class="form-group">
<label>合约</label>
<input type="text" name="contract_code" id="editContract" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>方向</label>
<select name="direction" id="editDirection" required>
{% for v, label in directions %}
<option value="{{ v }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>开仓日期</label>
<input type="date" name="open_date" id="editOpenDate" required style="font-size:0.85rem;">
</div>
<div class="form-group">
<label>开仓价</label>
<input type="number" step="any" name="open_price" id="editOpenPrice" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>开仓手续费</label>
<input type="number" step="any" name="open_fee" id="editOpenFee" value="0" style="font-size:0.9rem;">
</div>
<div id="editCloseFields" style="display:none;">
<div class="form-group">
<label>平仓日期</label>
<input type="date" name="close_date" id="editCloseDate" style="font-size:0.85rem;">
</div>
<div class="form-group">
<label>平仓价格</label>
<input type="number" step="any" name="close_price" id="editClosePrice" style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>平仓手续费</label>
<input type="number" step="any" name="close_fee" id="editCloseFee" value="0" style="font-size:0.9rem;">
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideEditForm()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
<div id="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hidePnlDrawer()"></div>
<div id="pnlBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:380px;max-width:90vw;">
<h3 style="margin-bottom:4px;" id="pnlTitle"></h3>
<p style="font-size:0.8rem;color:var(--sub);margin-bottom:14px;" id="pnlFormula"></p>
<table class="calc-table" style="width:100%;border-collapse:collapse;font-size:0.84rem;margin-bottom:12px;" id="pnlTable"></table>
<div style="font-size:1rem;" id="pnlResult"></div>
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hidePnlDrawer()">&times;</button>
</div>
<script>
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
document.getElementById('pnlTitle').textContent = title;
var dirLabel = direction === 'short' ? '空' : '多';
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
var diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice);
var gross = diff * 20;
var fees = openFee + closeFee;
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
html += '<tr><td>价差</td><td>' + (direction === 'short' ? openPrice + ' - ' + closePrice : closePrice + ' - ' + openPrice) + '</td><td style="text-align:right;">' + diff.toFixed(2) + ' 点</td></tr>';
html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × 20</td><td style="text-align:right;">' + gross.toFixed(2) + '</td></tr>';
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>';
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>';
document.getElementById('pnlTable').innerHTML = html;
document.getElementById('pnlResult').innerHTML = '<b>盈亏 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--danger-fg)' : result < 0 ? 'var(--success-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>';
document.getElementById('pnlOverlay').style.display = 'block';
document.getElementById('pnlBox').style.display = 'block';
}
function hidePnlDrawer() {
document.getElementById('pnlOverlay').style.display = 'none';
document.getElementById('pnlBox').style.display = 'none';
}
function showEditForm(type, id, productCode, contractCode, direction, openPrice, openFee, openDate, closePrice, closeFee, closeDate, status) {
document.getElementById('editForm').action = '/' + type + 's/' + id + '/edit';
document.getElementById('editContract').value = contractCode;
document.getElementById('editDirection').value = direction;
document.getElementById('editOpenDate').value = openDate;
document.getElementById('editOpenPrice').value = openPrice;
document.getElementById('editOpenFee').value = openFee || 0;
if (status === 'closed' && closeDate) {
document.getElementById('editCloseFields').style.display = 'block';
document.getElementById('editCloseDate').value = closeDate;
document.getElementById('editClosePrice').value = closePrice;
document.getElementById('editCloseFee').value = closeFee || 0;
} else {
document.getElementById('editCloseFields').style.display = 'none';
}
document.getElementById('editFormOverlay').style.display = 'block';
document.getElementById('editFormBox').style.display = 'block';
}
function hideEditForm() {
document.getElementById('editFormOverlay').style.display = 'none';
document.getElementById('editFormBox').style.display = 'none';
}
</script>
<div id="confirmOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:199;" onclick="hideConfirm()"></div>
<div id="confirmBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:28px 32px;z-index:200;text-align:center;max-width:90vw;">
<p id="confirmMsg" style="font-size:0.95rem;margin-bottom:20px;"></p>
<div style="display:flex;gap:10px;justify-content:center;">
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideConfirm()">取消</button>
<button class="btn" style="background:var(--danger);color:#fff;" id="confirmBtn">确认删除</button>
</div>
</div>
<script>
function confirmDelete(e, msg, url) {
e.preventDefault();
document.getElementById('confirmMsg').textContent = msg;
document.getElementById('confirmBtn').onclick = function() {
var f = document.createElement('form');
f.method = 'POST';
f.action = url;
document.body.appendChild(f);
f.submit();
};
document.getElementById('confirmOverlay').style.display = 'block';
document.getElementById('confirmBox').style.display = 'block';
}
function hideConfirm() {
document.getElementById('confirmOverlay').style.display = 'none';
document.getElementById('confirmBox').style.display = 'none';
}
</script>
{% endblock %}