Add close price column to trade records for overnight position PnL review.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-30 21:51:53 +08:00
parent 4552f4ef9c
commit b6b7bfb248
4 changed files with 108 additions and 17 deletions
+61 -12
View File
@@ -1280,34 +1280,83 @@ def trades():
def update_trade(tid): def update_trade(tid):
d = request.form d = request.form
conn = get_db() conn = get_db()
row = conn.execute("SELECT * FROM trade_logs WHERE id=?", (tid,)).fetchone()
if not row:
conn.close()
flash("记录不存在")
return redirect(url_for("records"))
row = dict(row)
entry = float(d.get("entry_price") or 0)
close_px = float(d.get("close_price") or 0)
lots = float(d.get("lots") or 0)
sl_raw = d.get("stop_loss")
tp_raw = d.get("take_profit")
stop_loss = float(sl_raw) if sl_raw not in (None, "") else None
take_profit = float(tp_raw) if tp_raw not in (None, "") else None
open_time = (d.get("open_time") or row.get("open_time") or "").strip()
close_time = (d.get("close_time") or row.get("close_time") or "").strip()
direction = (d.get("direction") or row.get("direction") or "long").strip()
from trade_log_lib import recalc_trade_log_pnl, refresh_trade_log_equity_chain, _read_initial_capital
from trading_context import get_trading_mode
pnl = float(row.get("pnl") or 0)
fee = float(row.get("fee") or 0)
pnl_net = float(row.get("pnl_net") or 0)
old_entry = float(row.get("entry_price") or 0)
old_close = float(row.get("close_price") or 0)
old_lots = float(row.get("lots") or 0)
prices_changed = (
abs(entry - old_entry) > 0.0001
or abs(close_px - old_close) > 0.0001
or abs(lots - old_lots) > 0.0001
)
if prices_changed and close_px > 0 and entry > 0 and lots > 0:
calc = recalc_trade_log_pnl(
symbol=row.get("symbol") or "",
direction=direction,
entry_price=entry,
close_price=close_px,
lots=lots,
stop_loss=stop_loss,
take_profit=take_profit,
open_time=open_time,
close_time=close_time,
trading_mode=get_trading_mode(get_setting),
)
pnl = calc["pnl"]
fee = calc["fee"]
pnl_net = calc["pnl_net"]
conn.execute( conn.execute(
"""UPDATE trade_logs SET """UPDATE trade_logs SET
symbol_name=?, monitor_type=?, direction=?, symbol_name=?, monitor_type=?, direction=?,
entry_price=?, stop_loss=?, take_profit=?, close_price=?, entry_price=?, stop_loss=?, take_profit=?, close_price=?,
lots=?, margin=?, holding_minutes=?, open_time=?, close_time=?, lots=?, margin=?, holding_minutes=?, open_time=?, close_time=?,
pnl=?, result=?, verified=1 pnl=?, fee=?, pnl_net=?, result=?, verified=1
WHERE id=?""", WHERE id=?""",
( (
d.get("symbol_name", "").strip(), d.get("symbol_name", "").strip(),
d.get("monitor_type", "").strip(), d.get("monitor_type", "").strip(),
d.get("direction", "").strip(), direction,
float(d.get("entry_price") or 0), entry,
float(d.get("stop_loss") or 0), stop_loss,
float(d.get("take_profit") or 0), take_profit,
float(d.get("close_price") or 0), close_px,
float(d.get("lots") or 0), lots,
float(d.get("margin") or 0), float(d.get("margin") or 0),
int(d.get("holding_minutes") or 0), int(d.get("holding_minutes") or 0),
d.get("open_time", "").strip(), open_time,
d.get("close_time", "").strip(), close_time,
float(d.get("pnl") or 0), pnl,
fee,
pnl_net,
d.get("result", "").strip(), d.get("result", "").strip(),
tid, tid,
), ),
) )
try: try:
cap = float(get_setting("live_capital", "0") or 0) refresh_trade_log_equity_chain(conn, _read_initial_capital(conn))
refresh_trade_log_equity_chain(conn, cap if cap > 0 else None)
except Exception as exc: except Exception as exc:
app.logger.debug("equity chain refresh after trade edit: %s", exc) app.logger.debug("equity chain refresh after trade edit: %s", exc)
conn.commit() conn.commit()
+1 -1
View File
@@ -22,7 +22,7 @@
{ label: '合约', value: esc(data.symbol_code), wide: false }, { label: '合约', value: esc(data.symbol_code), wide: false },
{ label: '类型', value: esc(data.monitor_type) + ' · ' + esc(data.source), wide: false }, { label: '类型', value: esc(data.monitor_type) + ' · ' + esc(data.source), wide: false },
{ label: '方向', value: esc(data.direction), wide: false }, { label: '方向', value: esc(data.direction), wide: false },
{ label: '成交价', value: esc(data.entry_price), wide: false }, { label: '开仓价', value: esc(data.entry_price), wide: false },
{ label: '平仓价', value: esc(data.close_price), wide: false }, { label: '平仓价', value: esc(data.close_price), wide: false },
{ label: '手数', value: esc(data.lots), wide: false }, { label: '手数', value: esc(data.lots), wide: false },
{ label: '止损', value: esc(data.stop_loss), wide: false }, { label: '止损', value: esc(data.stop_loss), wide: false },
+10 -4
View File
@@ -93,6 +93,9 @@
{% else %} {% else %}
<p class="hint" style="margin-top:0">CTP 未连接时仅显示本地数据库记录;连接后打开本页会自动同步柜台成交。</p> <p class="hint" style="margin-top:0">CTP 未连接时仅显示本地数据库记录;连接后打开本页会自动同步柜台成交。</p>
{% endif %} {% endif %}
<p class="hint" style="margin-top:0;margin-bottom:.75rem">
跨日持仓的盈亏以<strong>平仓价</strong>与柜台结算为准;表格中「开仓价」为程序记录,「平仓价」为成交回报,二者不一致时请以平仓价核对净盈亏。
</p>
<label class="trade-switch-label records-desktop-only"> <label class="trade-switch-label records-desktop-only">
<input type="checkbox" id="trade-edit-switch"> <input type="checkbox" id="trade-edit-switch">
<span>修改/核对开关(开启后可编辑关键字段)</span> <span>修改/核对开关(开启后可编辑关键字段)</span>
@@ -170,7 +173,7 @@
<thead> <thead>
<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><th>最新资金</th><th>结果</th><th>操作</th>
@@ -199,9 +202,13 @@
</select> </select>
</td> </td>
<td> <td>
<span class="cell-readonly cell-edit-hide">{{ t.entry_price }}</span> <span class="cell-readonly cell-edit-hide">{{ t.entry_price if t.entry_price is not none else '-' }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="entry_price" value="{{ t.entry_price }}" style="display:none"> <input class="cell-edit-show" type="number" step="0.0001" name="entry_price" value="{{ t.entry_price }}" style="display:none">
</td> </td>
<td>
<span class="cell-readonly cell-edit-hide">{{ t.close_price if t.close_price is not none else '-' }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="close_price" value="{{ t.close_price or '' }}" style="display:none">
</td>
<td> <td>
<span class="cell-readonly cell-edit-hide">{{ t.stop_loss }}</span> <span class="cell-readonly cell-edit-hide">{{ t.stop_loss }}</span>
<input class="cell-edit-show" type="number" step="0.0001" name="stop_loss" value="{{ t.stop_loss }}" style="display:none"> <input class="cell-edit-show" type="number" step="0.0001" name="stop_loss" value="{{ t.stop_loss }}" style="display:none">
@@ -240,7 +247,6 @@
{{ t.pnl if t.pnl is not none else '-' }} {{ t.pnl if t.pnl is not none else '-' }}
</span> </span>
<input class="cell-edit-show" type="number" step="0.01" name="pnl" value="{{ t.pnl or '' }}" style="display:none"> <input class="cell-edit-show" type="number" step="0.01" name="pnl" value="{{ t.pnl or '' }}" style="display:none">
<input type="hidden" name="close_price" value="{{ t.close_price or '' }}">
<input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}"> <input type="hidden" name="symbol_name" value="{{ t.symbol_name or t.symbol }}">
</td> </td>
<td><span class="cell-readonly text-muted">{{ t.fee if t.fee is not none else '-' }}</span></td> <td><span class="cell-readonly text-muted">{{ t.fee if t.fee is not none else '-' }}</span></td>
@@ -279,7 +285,7 @@
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="18" class="text-muted">暂无交易记录</td></tr> <tr><td colspan="19" class="text-muted">暂无交易记录</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
+36
View File
@@ -32,6 +32,42 @@ def calc_equity_after(capital: float, pnl_net: float) -> float | None:
return round(cap + float(pnl_net or 0), 2) return round(cap + float(pnl_net or 0), 2)
def recalc_trade_log_pnl(
*,
symbol: str,
direction: str,
entry_price: float,
close_price: float,
lots: float,
stop_loss: float | None = None,
take_profit: float | None = None,
open_time: str = "",
close_time: str = "",
trading_mode: str = "simulation",
capital: float = 0.0,
) -> dict[str, float]:
"""按开/平仓价重算盈亏与手续费(跨日持仓可手动改价后核对)。"""
from contract_specs import calc_position_metrics
from fee_specs import calc_round_trip_fee
sym = (symbol or "").strip()
direction = (direction or "long").strip().lower()
entry = float(entry_price or close_price or 0)
close_px = float(close_price or 0)
lots_f = float(lots or 0)
sl = float(stop_loss) if stop_loss is not None else entry
tp = float(take_profit) if take_profit is not None else entry
metrics = calc_position_metrics(
direction, entry, sl, tp, lots_f, close_px, capital, sym,
)
pnl = round(float(metrics.get("float_pnl") or 0), 2)
fee = calc_round_trip_fee(
sym, entry, close_px, lots_f, open_time, close_time, trading_mode=trading_mode,
)
pnl_net = round(pnl - fee, 2)
return {"pnl": pnl, "fee": round(fee, 2), "pnl_net": pnl_net}
def _read_initial_capital(conn, initial_capital: float | None = None) -> float: def _read_initial_capital(conn, initial_capital: float | None = None) -> float:
if initial_capital is not None and initial_capital > 0: if initial_capital is not None and initial_capital > 0:
return float(initial_capital) return float(initial_capital)