feat: 交易记录增加保证金占比与最新资金,上方展示资金曲线

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 16:17:22 +08:00
parent 649c064c2f
commit 240fbe7994
5 changed files with 184 additions and 13 deletions
+21 -4
View File
@@ -286,6 +286,8 @@ def init_db():
"ALTER TABLE review_records ADD COLUMN sina_code TEXT", "ALTER TABLE review_records ADD COLUMN sina_code TEXT",
"ALTER TABLE trade_logs ADD COLUMN fee REAL", "ALTER TABLE trade_logs ADD COLUMN fee REAL",
"ALTER TABLE trade_logs ADD COLUMN pnl_net REAL", "ALTER TABLE trade_logs ADD COLUMN pnl_net REAL",
"ALTER TABLE trade_logs ADD COLUMN margin_pct REAL",
"ALTER TABLE trade_logs ADD COLUMN equity_after REAL",
"ALTER TABLE review_records ADD COLUMN fee REAL", "ALTER TABLE review_records ADD COLUMN fee REAL",
"ALTER TABLE review_records ADD COLUMN pnl_net REAL", "ALTER TABLE review_records ADD COLUMN pnl_net REAL",
] ]
@@ -1082,16 +1084,21 @@ def close_position(pid):
pnl_net = round(pnl - fee, 2) pnl_net = round(pnl - fee, 2)
result = classify_close_result(direction, close_price, sl, tp) result = classify_close_result(direction, close_price, sl, tp)
minutes = holding_to_minutes(open_time, close_time) minutes = holding_to_minutes(open_time, close_time)
margin_pct = metrics.get("position_pct")
from trade_log_lib import calc_equity_after
equity_after = calc_equity_after(capital, pnl_net)
conn.execute( conn.execute(
"""INSERT INTO trade_logs """INSERT INTO trade_logs
(symbol, symbol_name, market_code, sina_code, monitor_type, direction, (symbol, symbol_name, market_code, sina_code, monitor_type, direction,
entry_price, stop_loss, take_profit, close_price, lots, margin, entry_price, stop_loss, take_profit, close_price, lots, margin,
holding_minutes, open_time, close_time, pnl, fee, pnl_net, result) margin_pct, holding_minutes, open_time, close_time, pnl, fee, pnl_net,
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", equity_after, result)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
( (
sym, row["symbol_name"], market, sina, "持仓监控", direction, sym, row["symbol_name"], market, sina, "持仓监控", direction,
entry, sl, tp, close_price, lots, metrics["margin"], entry, sl, tp, close_price, lots, metrics["margin"],
minutes, open_time, close_time, pnl, fee, pnl_net, result, margin_pct,
minutes, open_time, close_time, pnl, fee, pnl_net, equity_after, result,
), ),
) )
conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,)) conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,))
@@ -1226,6 +1233,15 @@ def records():
trade_list = conn.execute( trade_list = conn.execute(
"SELECT * FROM trade_logs ORDER BY id DESC LIMIT 500" "SELECT * FROM trade_logs ORDER BY id DESC LIMIT 500"
).fetchall() ).fetchall()
from trade_log_lib import enrich_trades_for_records
try:
initial_capital = float(get_setting("live_capital", "0") or 0)
except (TypeError, ValueError):
initial_capital = 0.0
trades, equity_curve = enrich_trades_for_records(
[dict(r) for r in trade_list],
initial_capital=initial_capital,
)
conn.close() conn.close()
trade_prefill_keys = ( trade_prefill_keys = (
@@ -1238,7 +1254,8 @@ def records():
return render_template( return render_template(
"records.html", "records.html",
reviews=review_list, reviews=review_list,
trades=trade_list, trades=trades,
equity_curve=equity_curve,
auto_records=auto_list, auto_records=auto_list,
preset=preset, preset=preset,
start=start, start=start,
+8 -2
View File
@@ -11,6 +11,7 @@ from zoneinfo import ZoneInfo
from contract_specs import calc_position_metrics from contract_specs import calc_position_metrics
from ctp_symbol import ths_to_vnpy_symbol from ctp_symbol import ths_to_vnpy_symbol
from fee_specs import calc_round_trip_fee from fee_specs import calc_round_trip_fee
from trade_log_lib import calc_equity_after
from market_sessions import is_trading_session from market_sessions import is_trading_session
from symbols import ths_to_codes from symbols import ths_to_codes
from vnpy_bridge import ( from vnpy_bridge import (
@@ -220,6 +221,8 @@ def write_trade_log(
sym, entry, close_price, lots, open_time, close_time, trading_mode=trading_mode, sym, entry, close_price, lots, open_time, close_time, trading_mode=trading_mode,
) )
pnl_net = round(pnl - fee, 2) pnl_net = round(pnl - fee, 2)
margin_pct = metrics.get("position_pct")
equity_after = calc_equity_after(capital, pnl_net)
try: try:
from app import holding_to_minutes from app import holding_to_minutes
@@ -231,8 +234,9 @@ def write_trade_log(
"""INSERT INTO trade_logs """INSERT INTO trade_logs
(symbol, symbol_name, market_code, sina_code, monitor_type, direction, (symbol, symbol_name, market_code, sina_code, monitor_type, direction,
entry_price, stop_loss, take_profit, close_price, lots, margin, entry_price, stop_loss, take_profit, close_price, lots, margin,
holding_minutes, open_time, close_time, pnl, fee, pnl_net, result) margin_pct, holding_minutes, open_time, close_time, pnl, fee, pnl_net,
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", equity_after, result)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
( (
sym, sym,
symbol_name, symbol_name,
@@ -246,12 +250,14 @@ def write_trade_log(
close_price, close_price,
lots, lots,
metrics.get("margin"), metrics.get("margin"),
margin_pct,
minutes, minutes,
open_time, open_time,
close_time, close_time,
pnl, pnl,
fee, fee,
pnl_net, pnl_net,
equity_after,
result if result in TRADE_RESULTS else "手动平仓", result if result in TRADE_RESULTS else "手动平仓",
), ),
) )
+61
View File
@@ -0,0 +1,61 @@
(function () {
var el = document.getElementById('equity-curve-chart');
var raw = window.__EQUITY_CURVE__;
if (!el || !raw || !raw.length || !window.LightweightCharts) return;
function parseTime(s) {
if (!s) return null;
var t = String(s).trim().replace(' ', 'T');
if (t.length === 16) t += ':00';
var d = new Date(t);
if (isNaN(d.getTime())) return null;
return Math.floor(d.getTime() / 1000);
}
var data = [];
var lastTs = 0;
raw.forEach(function (p) {
var ts = parseTime(p.time);
if (ts == null) return;
if (ts <= lastTs) ts = lastTs + 1;
lastTs = ts;
data.push({ time: ts, value: Number(p.value) });
});
if (!data.length) {
el.innerHTML = '<p class="text-muted" style="padding:1rem">暂无资金曲线数据</p>';
return;
}
var c = {
bg: '#1a1d24',
text: '#9ca3af',
grid: '#2d3139',
line: '#6366f1',
};
var chart = LightweightCharts.createChart(el, {
width: el.clientWidth || 800,
height: 220,
layout: {
background: { type: 'solid', color: c.bg },
textColor: c.text,
fontSize: 11,
},
grid: {
vertLines: { color: c.grid },
horzLines: { color: c.grid },
},
rightPriceScale: { borderColor: c.grid },
timeScale: { borderColor: c.grid, timeVisible: true, secondsVisible: false },
});
var series = chart.addLineSeries({
color: c.line,
lineWidth: 2,
priceFormat: { type: 'price', precision: 2, minMove: 0.01 },
});
series.setData(data);
chart.timeScale().fitContent();
window.addEventListener('resize', function () {
chart.applyOptions({ width: el.clientWidth || 800 });
});
})();
+27 -7
View File
@@ -1,6 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %} {% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %}
{% block content %} {% 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"> <div class="card records-trade-card" style="margin-bottom:1.25rem">
<h2>交易记录</h2> <h2>交易记录</h2>
<div class="card-body"> <div class="card-body">
@@ -14,9 +21,9 @@
<tr> <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><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> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -49,11 +56,18 @@
<input class="cell-edit-show" type="number" step="0.0001" name="take_profit" value="{{ t.take_profit }}" style="display:none"> <input class="cell-edit-show" type="number" step="0.0001" name="take_profit" value="{{ t.take_profit }}" style="display:none">
</td> </td>
<td> <td>
<span class="cell-readonly cell-edit-hide">{{ t.lots }}手 / {{ t.margin or '-' }}</span> <span class="cell-readonly cell-edit-hide">{{ t.lots }}</span>
<input class="cell-edit-show" type="number" step="0.01" name="margin" value="{{ t.margin or '' }}" placeholder="保证金" style="display:none"> <input class="cell-edit-show" type="number" step="0.01" name="lots" value="{{ t.lots }}" style="display:none">
<input type="hidden" name="lots" value="{{ t.lots }}"> </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>
<td><span class="cell-readonly"></span></td>
<td> <td>
<span class="cell-readonly cell-edit-hide">{{ t.holding_minutes or 0 }}</span> <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"> <input class="cell-edit-show" type="number" name="holding_minutes" value="{{ t.holding_minutes or 0 }}" style="display:none">
@@ -80,6 +94,9 @@
{{ t.pnl_net if t.pnl_net is not none else '-' }} {{ t.pnl_net if t.pnl_net is not none else '-' }}
</span> </span>
</td> </td>
<td>
<span class="cell-readonly">{{ t.equity_after if t.equity_after is not none else '-' }}</span>
</td>
<td> <td>
<span class="cell-readonly cell-edit-hide"> <span class="cell-readonly cell-edit-hide">
{% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span> {% if t.result == '止盈' %}<span class="badge profit">{{ t.result }}</span>
@@ -107,7 +124,7 @@
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="16" class="text-muted">暂无交易记录</td></tr> <tr><td colspan="18" class="text-muted">暂无交易记录</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -298,6 +315,9 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_js %} {% 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/review.js') }}"></script>
<script src="{{ url_for('static', filename='js/trades.js') }}"></script> <script src="{{ url_for('static', filename='js/trades.js') }}"></script>
{% if prefill %} {% if prefill %}
+67
View File
@@ -0,0 +1,67 @@
"""交易记录:字段补全、资金曲线数据。"""
from __future__ import annotations
from typing import Any
TRADE_LOG_EXTRA_COLUMNS = (
"ALTER TABLE trade_logs ADD COLUMN margin_pct REAL",
"ALTER TABLE trade_logs ADD COLUMN equity_after REAL",
)
def ensure_trade_log_columns(conn) -> None:
for sql in TRADE_LOG_EXTRA_COLUMNS:
try:
conn.execute(sql)
except Exception:
pass
def calc_equity_after(capital: float, pnl_net: float) -> float | None:
cap = float(capital or 0)
if cap <= 0:
return None
return round(cap + float(pnl_net or 0), 2)
def enrich_trades_for_records(
trades: list[dict[str, Any]],
*,
initial_capital: float = 0.0,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""表格仍按 id 降序;资金曲线按平仓时间升序用最新资金绘制。"""
rows = [dict(t) for t in trades]
chrono = sorted(
rows,
key=lambda t: ((t.get("close_time") or ""), int(t.get("id") or 0)),
)
running = float(initial_capital or 0)
curve: list[dict[str, Any]] = []
for t in chrono:
pnl_net = float(t.get("pnl_net") or 0)
eq = t.get("equity_after")
if eq is None:
if running > 0:
eq = round(running + pnl_net, 2)
else:
eq = None
t["equity_after"] = eq
if eq is not None:
running = float(eq)
if t.get("margin_pct") is None:
margin = float(t.get("margin") or 0)
cap_before = float(eq or 0) - pnl_net if eq is not None else 0.0
if margin > 0 and cap_before > 0:
t["margin_pct"] = round(margin / cap_before * 100, 2)
if eq is not None:
curve.append({
"time": (t.get("close_time") or "")[:19],
"value": float(eq),
"id": int(t.get("id") or 0),
})
return rows, curve