From 240fbe799466a645bc2fbfb893967579764fa0e0 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 25 Jun 2026 16:17:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A4=E6=98=93=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BF=9D=E8=AF=81=E9=87=91=E5=8D=A0=E6=AF=94?= =?UTF-8?q?=E4=B8=8E=E6=9C=80=E6=96=B0=E8=B5=84=E9=87=91=EF=BC=8C=E4=B8=8A?= =?UTF-8?q?=E6=96=B9=E5=B1=95=E7=A4=BA=E8=B5=84=E9=87=91=E6=9B=B2=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.py | 25 ++++++++++++--- sl_tp_guard.py | 10 ++++-- static/js/equity_curve.js | 61 +++++++++++++++++++++++++++++++++++ templates/records.html | 34 ++++++++++++++++---- trade_log_lib.py | 67 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 static/js/equity_curve.js create mode 100644 trade_log_lib.py diff --git a/app.py b/app.py index db4f1a7..1bf39b1 100644 --- a/app.py +++ b/app.py @@ -286,6 +286,8 @@ def init_db(): "ALTER TABLE review_records ADD COLUMN sina_code TEXT", "ALTER TABLE trade_logs ADD COLUMN fee 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 pnl_net REAL", ] @@ -1082,16 +1084,21 @@ def close_position(pid): pnl_net = round(pnl - fee, 2) result = classify_close_result(direction, close_price, sl, tp) 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( """INSERT INTO trade_logs (symbol, symbol_name, market_code, sina_code, monitor_type, direction, entry_price, stop_loss, take_profit, close_price, lots, margin, - holding_minutes, open_time, close_time, pnl, fee, pnl_net, result) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + margin_pct, holding_minutes, open_time, close_time, pnl, fee, pnl_net, + equity_after, result) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( sym, row["symbol_name"], market, sina, "持仓监控", direction, 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,)) @@ -1226,6 +1233,15 @@ def records(): trade_list = conn.execute( "SELECT * FROM trade_logs ORDER BY id DESC LIMIT 500" ).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() trade_prefill_keys = ( @@ -1238,7 +1254,8 @@ def records(): return render_template( "records.html", reviews=review_list, - trades=trade_list, + trades=trades, + equity_curve=equity_curve, auto_records=auto_list, preset=preset, start=start, diff --git a/sl_tp_guard.py b/sl_tp_guard.py index 5ece4ec..4f71dd7 100644 --- a/sl_tp_guard.py +++ b/sl_tp_guard.py @@ -11,6 +11,7 @@ from zoneinfo import ZoneInfo from contract_specs import calc_position_metrics from ctp_symbol import ths_to_vnpy_symbol from fee_specs import calc_round_trip_fee +from trade_log_lib import calc_equity_after from market_sessions import is_trading_session from symbols import ths_to_codes 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, ) pnl_net = round(pnl - fee, 2) + margin_pct = metrics.get("position_pct") + equity_after = calc_equity_after(capital, pnl_net) try: from app import holding_to_minutes @@ -231,8 +234,9 @@ def write_trade_log( """INSERT INTO trade_logs (symbol, symbol_name, market_code, sina_code, monitor_type, direction, entry_price, stop_loss, take_profit, close_price, lots, margin, - holding_minutes, open_time, close_time, pnl, fee, pnl_net, result) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + margin_pct, holding_minutes, open_time, close_time, pnl, fee, pnl_net, + equity_after, result) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( sym, symbol_name, @@ -246,12 +250,14 @@ def write_trade_log( close_price, lots, metrics.get("margin"), + margin_pct, minutes, open_time, close_time, pnl, fee, pnl_net, + equity_after, result if result in TRADE_RESULTS else "手动平仓", ), ) diff --git a/static/js/equity_curve.js b/static/js/equity_curve.js new file mode 100644 index 0000000..09c4e77 --- /dev/null +++ b/static/js/equity_curve.js @@ -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 = '

暂无资金曲线数据

'; + 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 }); + }); +})(); diff --git a/templates/records.html b/templates/records.html index 9bbf536..011d1e8 100644 --- a/templates/records.html +++ b/templates/records.html @@ -1,6 +1,13 @@ {% extends "base.html" %} {% block title %}交易记录与复盘 - 国内期货监控系统{% endblock %} {% block content %} +
+

资金曲线

+
+
+
+
+

交易记录

@@ -14,9 +21,9 @@ 品种类型方向 成交止损(开仓)止盈 - 基数杠杆持仓分钟 + 手数保证金保证金占比持仓分钟 开仓时间平仓时间 - 盈亏(元)手续费净盈亏结果操作 + 盈亏(元)手续费净盈亏最新资金结果操作 @@ -49,11 +56,18 @@ - {{ t.lots }}手 / {{ t.margin or '-' }} - - + {{ t.lots }} + + + + {{ t.margin if t.margin is not none else '-' }} + + + + + {% if t.margin_pct is not none %}{{ t.margin_pct }}%{% else %}-{% endif %} + - {{ t.holding_minutes or 0 }} @@ -80,6 +94,9 @@ {{ t.pnl_net if t.pnl_net is not none else '-' }} + + {{ t.equity_after if t.equity_after is not none else '-' }} + {% if t.result == '止盈' %}{{ t.result }} @@ -107,7 +124,7 @@ {% else %} - 暂无交易记录 + 暂无交易记录 {% endfor %} @@ -298,6 +315,9 @@ {% endif %} {% endblock %} {% block extra_js %} + + + {% if prefill %} diff --git a/trade_log_lib.py b/trade_log_lib.py new file mode 100644 index 0000000..43b7e5f --- /dev/null +++ b/trade_log_lib.py @@ -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