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 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
@@ -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 "手动平仓",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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" %}
|
{% 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 %}
|
||||||
|
|||||||
@@ -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