feat: 交易记录增加保证金占比与最新资金,上方展示资金曲线
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
+8
-2
@@ -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 "手动平仓",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
@@ -1,6 +1,13 @@
|
||||
{% 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">
|
||||
@@ -14,9 +21,9 @@
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
</td>
|
||||
<td>
|
||||
<span class="cell-readonly cell-edit-hide">{{ t.lots }}手 / {{ t.margin or '-' }}</span>
|
||||
<input class="cell-edit-show" type="number" step="0.01" name="margin" value="{{ t.margin or '' }}" placeholder="保证金" style="display:none">
|
||||
<input type="hidden" name="lots" value="{{ t.lots }}">
|
||||
<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">—</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">
|
||||
@@ -80,6 +94,9 @@
|
||||
{{ 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>
|
||||
@@ -107,7 +124,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="16" class="text-muted">暂无交易记录</td></tr>
|
||||
<tr><td colspan="18" class="text-muted">暂无交易记录</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -298,6 +315,9 @@
|
||||
{% 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 %}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user