feat: options stats profit/loss totals and history delete
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2597,6 +2597,11 @@ html[data-theme="light"] .settings-export-link {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.options-history-table-wrap .opt-history-del {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
.options-pos-history-card {
|
.options-pos-history-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -383,10 +383,12 @@
|
|||||||
const winEl = document.getElementById("opt-stats-winrate");
|
const winEl = document.getElementById("opt-stats-winrate");
|
||||||
const plrEl = document.getElementById("opt-stats-plr");
|
const plrEl = document.getElementById("opt-stats-plr");
|
||||||
const closedEl = document.getElementById("opt-stats-closed");
|
const closedEl = document.getElementById("opt-stats-closed");
|
||||||
|
const profitEl = document.getElementById("opt-stats-profit");
|
||||||
|
const lossEl = document.getElementById("opt-stats-loss");
|
||||||
if (!d.ok) {
|
if (!d.ok) {
|
||||||
if (winEl) winEl.textContent = "—";
|
[winEl, plrEl, closedEl, profitEl, lossEl].forEach(function (el) {
|
||||||
if (plrEl) plrEl.textContent = "—";
|
if (el) el.textContent = "—";
|
||||||
if (closedEl) closedEl.textContent = "—";
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (winEl) winEl.textContent = d.total_closed ? d.win_rate + "%" : "0%";
|
if (winEl) winEl.textContent = d.total_closed ? d.win_rate + "%" : "0%";
|
||||||
@@ -394,6 +396,27 @@
|
|||||||
plrEl.textContent = d.profit_loss_ratio != null ? String(d.profit_loss_ratio) : "—";
|
plrEl.textContent = d.profit_loss_ratio != null ? String(d.profit_loss_ratio) : "—";
|
||||||
}
|
}
|
||||||
if (closedEl) closedEl.textContent = String(d.total_closed || 0);
|
if (closedEl) closedEl.textContent = String(d.total_closed || 0);
|
||||||
|
if (profitEl) {
|
||||||
|
profitEl.textContent = d.total_profit != null && d.total_profit > 0
|
||||||
|
? fmt(d.total_profit, 4) + " USDC" : (d.total_closed ? "0 USDC" : "—");
|
||||||
|
}
|
||||||
|
if (lossEl) {
|
||||||
|
lossEl.textContent = d.total_loss != null && d.total_loss > 0
|
||||||
|
? fmt(d.total_loss, 4) + " USDC" : (d.total_closed ? "0 USDC" : "—");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteHistoryRow(id, status) {
|
||||||
|
const warn = status === "open"
|
||||||
|
? "该记录仍为持仓中,仅删除本地记录,不影响交易所持仓。确认删除?"
|
||||||
|
: "确认删除该条期权历史记录?";
|
||||||
|
if (!confirm(warn)) return;
|
||||||
|
const r = await apiJson("/api/options/history/" + encodeURIComponent(id), { method: "DELETE" });
|
||||||
|
if (!r.ok) {
|
||||||
|
alert(r.msg || "删除失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshAllPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshHistory() {
|
async function refreshHistory() {
|
||||||
@@ -402,7 +425,7 @@
|
|||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
const list = (d.ok && d.history) || [];
|
const list = (d.ok && d.history) || [];
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">暂无历史记录</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7" class="muted">暂无历史记录</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.forEach(function (h) {
|
list.forEach(function (h) {
|
||||||
@@ -410,15 +433,22 @@
|
|||||||
const prem = h.status === "closed" ? h.premium_received : h.premium_paid;
|
const prem = h.status === "closed" ? h.premium_received : h.premium_paid;
|
||||||
const pnl = h.realized_pnl;
|
const pnl = h.realized_pnl;
|
||||||
const pnlTxt = pnl != null ? fmt(pnl, 4) : "—";
|
const pnlTxt = pnl != null ? fmt(pnl, 4) : "—";
|
||||||
|
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
"<td><code>" + (h.inst_id || "") + "</code></td>" +
|
"<td><code>" + (h.inst_id || "") + "</code></td>" +
|
||||||
"<td>" + fmt(h.sheets, 0) + "</td>" +
|
"<td>" + fmt(h.sheets, 0) + "</td>" +
|
||||||
"<td>" + fmt(prem, 4) + "</td>" +
|
"<td>" + fmt(prem, 4) + "</td>" +
|
||||||
"<td>" + (h.status === "closed" ? "已平" : "持仓中") + "</td>" +
|
"<td>" + (h.status === "closed" ? "已平" : "持仓中") + "</td>" +
|
||||||
"<td>" + pnlTxt + "</td>" +
|
'<td class="' + pnlCls + '">' + pnlTxt + "</td>" +
|
||||||
"<td>" + (h.closed_at || h.created_at || "—") + "</td>";
|
"<td>" + (h.closed_at || h.created_at || "—") + "</td>" +
|
||||||
|
'<td><button type="button" class="btn-secondary btn-sm opt-history-del" data-id="' + h.id + '" data-status="' + (h.status || "") + '">删除</button></td>';
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
tbody.querySelectorAll(".opt-history-del").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
deleteHistoryRow(btn.getAttribute("data-id"), btn.getAttribute("data-status"));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshAllPositions() {
|
function refreshAllPositions() {
|
||||||
|
|||||||
@@ -573,15 +573,40 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0
|
win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0
|
||||||
avg_win = sum(wins) / len(wins) if wins else None
|
avg_win = sum(wins) / len(wins) if wins else None
|
||||||
avg_loss = sum(losses) / len(losses) if losses else None
|
avg_loss = sum(losses) / len(losses) if losses else None
|
||||||
|
total_profit = round(sum(wins), 4) if wins else 0.0
|
||||||
|
total_loss = round(abs(sum(losses)), 4) if losses else 0.0
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"total_closed": total_closed,
|
"total_closed": total_closed,
|
||||||
"win_rate": win_rate,
|
"win_rate": win_rate,
|
||||||
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
|
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
|
||||||
|
"total_profit": total_profit,
|
||||||
|
"total_loss": total_loss,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route("/api/options/history/<int:trade_id>", methods=["DELETE"])
|
||||||
|
@lr
|
||||||
|
def api_options_history_delete(trade_id: int):
|
||||||
|
ex, err = _require_options_ex(cfg)
|
||||||
|
if ex is None:
|
||||||
|
return jsonify({"ok": False, "msg": err})
|
||||||
|
conn = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
init_options_tables(conn)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id, status FROM options_trades WHERE id = ?",
|
||||||
|
(trade_id,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return jsonify({"ok": False, "msg": "记录不存在"})
|
||||||
|
conn.execute("DELETE FROM options_trades WHERE id = ?", (trade_id,))
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
|
def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
|
||||||
if app.extensions.get("options_monitor_started"):
|
if app.extensions.get("options_monitor_started"):
|
||||||
|
|||||||
@@ -86,6 +86,14 @@
|
|||||||
<span class="k">已平笔数</span>
|
<span class="k">已平笔数</span>
|
||||||
<span class="v" id="opt-stats-closed">—</span>
|
<span class="v" id="opt-stats-closed">—</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="options-stat-item">
|
||||||
|
<span class="k">盈利</span>
|
||||||
|
<span class="v pos-pnl-profit" id="opt-stats-profit">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="options-stat-item">
|
||||||
|
<span class="k">亏损</span>
|
||||||
|
<span class="v pos-pnl-loss" id="opt-stats-loss">—</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-nested options-pos-subcard options-pos-history-card">
|
<div class="card-nested options-pos-subcard options-pos-history-card">
|
||||||
@@ -100,10 +108,11 @@
|
|||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>盈亏</th>
|
<th>盈亏</th>
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="opt-history-tbody">
|
<tbody id="opt-history-tbody">
|
||||||
<tr><td colspan="6" class="muted">加载中…</td></tr>
|
<tr><td colspan="7" class="muted">加载中…</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,4 +121,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_panel.js?v=6"></script>
|
<script src="/static/options_panel.js?v=7"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user