diff --git a/app.py b/app.py index 87ef7e0..12a9cf5 100644 --- a/app.py +++ b/app.py @@ -1328,6 +1328,17 @@ def update_trade(tid): fee = calc["fee"] pnl_net = calc["pnl_net"] + form_pnl_raw = d.get("pnl") + if form_pnl_raw not in (None, ""): + pnl = float(form_pnl_raw) + pnl_net = round(pnl - fee, 2) + + try: + from app import holding_to_minutes + minutes = int(holding_to_minutes(open_time, close_time) or 0) + except Exception: + minutes = int(d.get("holding_minutes") or row.get("holding_minutes") or 0) + conn.execute( """UPDATE trade_logs SET symbol_name=?, monitor_type=?, direction=?, @@ -1345,7 +1356,7 @@ def update_trade(tid): close_px, lots, float(d.get("margin") or 0), - int(d.get("holding_minutes") or 0), + minutes, open_time, close_time, pnl, diff --git a/static/css/records.css b/static/css/records.css index 9d6b722..3723144 100644 --- a/static/css/records.css +++ b/static/css/records.css @@ -93,6 +93,66 @@ display: flex; } +.records-page .records-verify-toggle { + display: flex; + align-items: center; + gap: .45rem; + margin-bottom: .75rem; + font-size: .82rem; + color: var(--text-muted); + cursor: pointer; +} + +.records-page .records-verify-toggle input { + flex-shrink: 0; +} + +.records-trade-card .records-src-badge { + margin-left: .25rem; + font-size: .65rem; +} + +.records-trade-card .records-verified-inline { + margin-left: .25rem; +} + +.records-trade-card .btn-records-action, +.records-trade-table-wrap .btn-records-action { + background: #1f3a5a; + color: #8fc8ff; + border: none; + border-radius: 6px; + padding: .3rem .55rem; + font-size: .72rem; + text-decoration: none; + cursor: pointer; + white-space: nowrap; +} + +.records-trade-card .btn-records-action:disabled, +.records-trade-table-wrap .btn-records-action:disabled { + opacity: .45; + cursor: not-allowed; +} + +.records-trade-card .btn-records-del, +.records-trade-table-wrap .btn-records-del { + background: rgba(239, 68, 68, .15); + color: var(--loss); + border: none; + border-radius: 6px; + padding: .3rem .55rem; + font-size: .72rem; + text-decoration: none; + cursor: pointer; + white-space: nowrap; +} + +.records-trade-card .records-trade-actions, +.records-trade-table-wrap .records-trade-actions { + min-width: 15.5rem; +} + #trade-detail-modal .records-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -303,6 +363,11 @@ html:is([data-layout="tablet"], .layout-tablet) .records-page .records-desktop-o display: none !important; } +html:is([data-mobile="1"], .layout-phone) .records-page .records-verify-toggle, +html:is([data-layout="phone"], .layout-phone) .records-page .records-verify-toggle { + display: flex; +} + html:is([data-mobile="1"], .layout-phone) .records-page .records-trade-card .card-body, html:is([data-layout="phone"], .layout-phone) .records-page .records-trade-card .card-body { padding: 0; diff --git a/static/js/records.js b/static/js/records.js index b890061..d8aaecb 100644 --- a/static/js/records.js +++ b/static/js/records.js @@ -47,6 +47,9 @@ html += ''; html += '
'; + if (data.id) { + html += ''; + } if (data.fill_review_url) { html += '填入复盘'; } @@ -56,6 +59,29 @@ html += '
'; body.innerHTML = html; + var reviewBtn = body.querySelector('.review-edit-btn'); + if (reviewBtn && data.id) { + reviewBtn.setAttribute('data-trade-edit', JSON.stringify({ + id: data.id, + symbol_name: data.symbol_name || data.symbol, + monitor_type: data.monitor_type, + direction: data.direction_code || (data.direction === '做多' ? 'long' : 'short'), + entry_price: data.entry_price, + close_price: data.close_price, + stop_loss: data.stop_loss, + take_profit: data.take_profit, + lots: data.lots, + margin: data.margin, + holding_minutes: data.holding_minutes, + open_time: data.open_time, + close_time: data.close_time, + pnl: data.pnl, + result: data.result + })); + } + if (typeof window.toggleTradeReviewMode === 'function') { + window.toggleTradeReviewMode(); + } mask.classList.add('show'); } diff --git a/static/js/trades.js b/static/js/trades.js index b440111..eced459 100644 --- a/static/js/trades.js +++ b/static/js/trades.js @@ -1,46 +1,99 @@ /* Copyright (c) 2025-2026 马建军. All rights reserved. - * 专有软件 — 未经授权禁止复制、传播、转售。 - * 详见 LICENSE.zh-CN.txt + * 交易记录核对修改 — 与币安实例一致:开关启用后,逐条弹窗核对 */ (function () { var switchEl = document.getElementById('trade-edit-switch'); if (!switchEl) return; - function setEditMode(on) { - document.querySelectorAll('.cell-edit-hide').forEach(function (el) { - el.style.display = on ? 'none' : ''; - }); - document.querySelectorAll('.cell-edit-show').forEach(function (el) { - if (el.type === 'hidden') return; - el.style.display = on ? '' : 'none'; - }); - document.querySelectorAll('.trade-save-btn').forEach(function (btn) { + function syncReviewEditButtons() { + var on = !!switchEl.checked; + document.querySelectorAll('.review-edit-btn').forEach(function (btn) { btn.disabled = !on; }); } - switchEl.addEventListener('change', function () { - setEditMode(switchEl.checked); - }); + function normalizeDatetime(v) { + var raw = String(v || '').trim().replace('T', ' '); + var m = raw.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2}:\d{2})(:\d{2})?/); + if (!m) return raw.slice(0, 19); + return m[1] + ' ' + m[2] + ':' + (m[3] ? m[3].slice(1) : '00'); + } - document.querySelectorAll('.trade-save-btn').forEach(function (btn) { - btn.addEventListener('click', function () { - var row = btn.closest('tr[data-trade-id]'); - if (!row) return; - var id = row.getAttribute('data-trade-id'); - var form = document.createElement('form'); - form.method = 'POST'; - form.action = '/update_trade/' + id; - row.querySelectorAll('.cell-edit-show').forEach(function (el) { - if (!el.name) return; - var input = document.createElement('input'); - input.type = 'hidden'; - input.name = el.name; - input.value = el.value; - form.appendChild(input); - }); - document.body.appendChild(form); - form.submit(); + function fmtPrice(v) { + if (v === null || v === undefined || v === '') return ''; + return String(v); + } + + function editTradeRecordReview(t) { + if (!t || !t.id) return; + + var opened = prompt('开仓时间(YYYY-MM-DD HH:MM:SS)', normalizeDatetime(t.open_time)); + if (opened === null) return; + var closed = prompt('平仓时间(YYYY-MM-DD HH:MM:SS)', normalizeDatetime(t.close_time)); + if (closed === null) return; + var entry = prompt('开仓价(核对后用于统计)', fmtPrice(t.entry_price)); + if (entry === null) return; + var closePx = prompt('平仓价(跨日持仓请以柜台成交为准)', fmtPrice(t.close_price)); + if (closePx === null) return; + var stopLoss = prompt('止损价格(核对后用于统计)', fmtPrice(t.stop_loss)); + if (stopLoss === null) return; + var takeProfit = prompt('止盈价格(核对后用于统计)', fmtPrice(t.take_profit)); + if (takeProfit === null) return; + var pnl = prompt('盈亏(元,可手工核对后填写)', String(t.pnl != null ? t.pnl : '')); + if (pnl === null) return; + var result = prompt( + '结果(止盈/止损/保本止盈/移动止盈/手动平仓/CTP同步)', + String(t.result || '手动平仓') + ); + if (result === null) return; + + var form = document.createElement('form'); + form.method = 'POST'; + form.action = '/update_trade/' + t.id; + var fields = { + symbol_name: t.symbol_name || t.symbol || '', + monitor_type: t.monitor_type || '', + direction: t.direction || 'long', + entry_price: entry, + close_price: closePx, + stop_loss: stopLoss, + take_profit: takeProfit, + lots: t.lots != null ? t.lots : '', + margin: t.margin != null ? t.margin : '', + holding_minutes: t.holding_minutes != null ? t.holding_minutes : 0, + open_time: normalizeDatetime(opened), + close_time: normalizeDatetime(closed), + pnl: pnl, + result: String(result || '').trim() || '手动平仓', + }; + Object.keys(fields).forEach(function (name) { + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = fields[name]; + form.appendChild(input); }); + document.body.appendChild(form); + form.submit(); + } + + window.editTradeRecordReview = editTradeRecordReview; + window.toggleTradeReviewMode = syncReviewEditButtons; + + switchEl.addEventListener('change', syncReviewEditButtons); + switchEl.addEventListener('input', syncReviewEditButtons); + syncReviewEditButtons(); + window.addEventListener('pageshow', syncReviewEditButtons); + + document.addEventListener('click', function (e) { + var btn = e.target.closest('.review-edit-btn'); + if (!btn || btn.disabled) return; + e.preventDefault(); + e.stopPropagation(); + var raw = btn.getAttribute('data-trade-edit'); + if (!raw) return; + try { + editTradeRecordReview(JSON.parse(raw)); + } catch (err) { /* ignore */ } }); })(); diff --git a/templates/records.html b/templates/records.html index 5042226..d2a1d7d 100644 --- a/templates/records.html +++ b/templates/records.html @@ -7,11 +7,14 @@ {% block content %} {% macro trade_detail_json(t) -%} {{ { + "id": t.id, "symbol": t.symbol_name or t.symbol, "symbol_code": t.symbol, + "symbol_name": t.symbol_name or t.symbol, "monitor_type": t.monitor_type, "source": "柜台" if t.source == "ctp" else "本地", "direction": "做多" if t.direction == "long" else "做空", + "direction_code": t.direction, "entry_price": t.entry_price, "close_price": t.close_price, "stop_loss": t.stop_loss, @@ -47,31 +50,42 @@ {% macro trade_pnl_cell(v) %} {% if v is not none %}{% set n = v|float %}{{ ('+' if n > 0 else '') ~ ('%.2f'|format(n)) }} 元{% else %}—{% endif %} {% endmacro %} +{% macro trade_edit_json(t) -%} +{{ { + "id": t.id, + "symbol_name": t.symbol_name or t.symbol, + "monitor_type": t.monitor_type, + "direction": t.direction, + "entry_price": t.entry_price, + "close_price": t.close_price, + "stop_loss": t.stop_loss, + "take_profit": t.take_profit, + "lots": t.lots, + "margin": t.margin, + "holding_minutes": t.holding_minutes or 0, + "open_time": t.open_time, + "close_time": t.close_time, + "pnl": t.pnl, + "result": t.result, +}|tojson }} +{%- endmacro %} +{% macro trade_review_edit_btn(t) %} + +{% endmacro %} {% macro trade_verify_form(t) %} -
- - - - - - - - - - - - - - - -
+{{ trade_review_edit_btn(t) }} {% endmacro %} {% macro trade_row_actions(t, detail_class) %}
- 填入复盘 - {{ trade_verify_form(t) }} - 删除 + 填入复盘 + {{ trade_review_edit_btn(t) }} + 删除
{% endmacro %}
@@ -96,9 +110,9 @@

跨日持仓的盈亏以平仓价与柜台结算为准;表格中「开仓价」为程序记录,「平仓价」为成交回报,二者不一致时请以平仓价核对净盈亏。

-