Fix option history to full closes only, 2-decimal USDC, restore hide-delete.
Filter OKX positions-history to type 2/3/6, format premium and bid recovery to 2 decimals, and allow locally hiding rows via delete button. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3727,6 +3727,11 @@ html[data-theme="light"] .options-estimate-row {
|
|||||||
.opt-history-table td:nth-child(6) {
|
.opt-history-table td:nth-child(6) {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
}
|
}
|
||||||
|
.opt-history-table th:nth-child(7),
|
||||||
|
.opt-history-table td:nth-child(7) {
|
||||||
|
width: 8%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.opt-hist-time {
|
.opt-hist-time {
|
||||||
font-size: 0.64rem;
|
font-size: 0.64rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -215,7 +215,7 @@
|
|||||||
|
|
||||||
function fmtUsdc(v) {
|
function fmtUsdc(v) {
|
||||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||||||
return Number(v).toFixed(4).replace(/\.?0+$/, "") || "0";
|
return Number(v).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtClosePreview(preview, premiumPaid) {
|
function fmtClosePreview(preview, premiumPaid) {
|
||||||
@@ -525,7 +525,7 @@
|
|||||||
const closePreview = p.close_preview || {};
|
const closePreview = p.close_preview || {};
|
||||||
const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos;
|
const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos;
|
||||||
const tickSz = p.tick_sz;
|
const tickSz = p.tick_sz;
|
||||||
const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmt(p.premium_paid, 4).replace(/\.?0+$/, "") : null);
|
const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmtUsdc(p.premium_paid) : null);
|
||||||
const avgTxt = fmtDisplay(p.avg_px_fmt, fmtOptionPx(p.avg_px, tickSz));
|
const avgTxt = fmtDisplay(p.avg_px_fmt, fmtOptionPx(p.avg_px, tickSz));
|
||||||
const markTxt = fmtDisplay(p.mark_px_fmt, fmtOptionPx(p.mark_px, tickSz));
|
const markTxt = fmtDisplay(p.mark_px_fmt, fmtOptionPx(p.mark_px, tickSz));
|
||||||
return (
|
return (
|
||||||
@@ -899,6 +899,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteHistoryRow(key, status) {
|
||||||
|
const warn = status === "open"
|
||||||
|
? "该记录仍为持仓中,仅从列表隐藏,不影响交易所持仓.确认删除?"
|
||||||
|
: "确认从列表隐藏该条历史记录?";
|
||||||
|
if (!confirm(warn)) return;
|
||||||
|
const r = await apiJson("/api/options/history/" + encodeURIComponent(key), { method: "DELETE" });
|
||||||
|
if (!r.ok) {
|
||||||
|
alert(r.msg || "删除失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshAllPositions();
|
||||||
|
}
|
||||||
|
|
||||||
function optHistoryStatus(h) {
|
function optHistoryStatus(h) {
|
||||||
if (h.status_label) return h.status_label;
|
if (h.status_label) return h.status_label;
|
||||||
if (h.status === "open") return "持仓中";
|
if (h.status === "open") return "持仓中";
|
||||||
@@ -921,26 +934,33 @@
|
|||||||
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) {
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
const premTxt = fmtDisplay(h.premium_paid_fmt, h.premium_paid != null ? fmt(h.premium_paid, 2) : null);
|
const premTxt = fmtDisplay(h.premium_paid_fmt, h.premium_paid != null ? fmtUsdc(h.premium_paid) : null);
|
||||||
const isOpen = h.status === "open";
|
const isOpen = h.status === "open";
|
||||||
const pnl = isOpen ? null : h.realized_pnl;
|
const pnl = isOpen ? null : h.realized_pnl;
|
||||||
const pnlTxt = pnl != null ? fmt(pnl, 2) : "—";
|
const pnlTxt = pnl != null ? fmt(pnl, 2) : "—";
|
||||||
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
|
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
|
||||||
const timeTxt = (h.closed_at || h.created_at || "—").replace("T", " ").slice(0, 19);
|
const timeTxt = (h.closed_at || h.created_at || "—").replace("T", " ").slice(0, 19);
|
||||||
|
const histKey = h.history_key || "";
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
'<td class="opt-hist-inst"><code>' + (h.inst_id || "") + "</code></td>" +
|
'<td class="opt-hist-inst"><code>' + (h.inst_id || "") + "</code></td>" +
|
||||||
"<td>" + fmt(h.sheets, 0) + "</td>" +
|
"<td>" + fmt(h.sheets, 0) + "</td>" +
|
||||||
"<td>" + premTxt + "</td>" +
|
"<td>" + premTxt + "</td>" +
|
||||||
"<td>" + optHistoryStatusHtml(h) + "</td>" +
|
"<td>" + optHistoryStatusHtml(h) + "</td>" +
|
||||||
'<td class="' + pnlCls + '">' + pnlTxt + "</td>" +
|
'<td class="' + pnlCls + '">' + pnlTxt + "</td>" +
|
||||||
'<td class="opt-hist-time">' + timeTxt + "</td>";
|
'<td class="opt-hist-time">' + timeTxt + "</td>" +
|
||||||
|
'<td><button type="button" class="btn-secondary btn-sm opt-history-del" data-key="' + histKey + '" 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-key"), btn.getAttribute("data-status"));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshAllPositions() {
|
function refreshAllPositions() {
|
||||||
|
|||||||
@@ -117,10 +117,32 @@ def format_option_px(px: float, tick_sz: Any) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def format_usdc_amount(v: float | None) -> str | None:
|
def format_usdc_amount(v: float | None) -> str | None:
|
||||||
"""USDC 金额展示(与交易所持仓/历史一致,最多 4 位小数)."""
|
"""USDC 金额展示(权利金/回收等,固定 2 位小数)."""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
return f"{float(v):.4f}".rstrip("0").rstrip(".") or "0"
|
return f"{float(v):.2f}"
|
||||||
|
|
||||||
|
|
||||||
|
def is_option_full_close_history(raw: dict[str, Any]) -> bool:
|
||||||
|
"""仅保留 OKX 历史仓位中的「全部平仓/强平/ADL 全平」记录,排除部分平仓."""
|
||||||
|
close_type = str(raw.get("type") or "").strip()
|
||||||
|
return close_type in ("2", "3", "6")
|
||||||
|
|
||||||
|
|
||||||
|
def option_history_row_key(
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
inst_id: str = "",
|
||||||
|
pos_id: str | None = None,
|
||||||
|
close_ms: int | None = None,
|
||||||
|
) -> str:
|
||||||
|
inst_id = (inst_id or "").strip()
|
||||||
|
pos_id = (pos_id or "").strip()
|
||||||
|
if source == "live":
|
||||||
|
return f"live:{inst_id}:{pos_id or close_ms or '0'}"
|
||||||
|
if pos_id:
|
||||||
|
return f"ex:{pos_id}"
|
||||||
|
return f"ex:{inst_id}:{close_ms or 0}"
|
||||||
|
|
||||||
|
|
||||||
def _ms_to_iso(ms: Any) -> str | None:
|
def _ms_to_iso(ms: Any) -> str | None:
|
||||||
@@ -815,6 +837,7 @@ def fetch_all_option_positions_history(
|
|||||||
if after is not None and str(oldest) == after:
|
if after is not None and str(oldest) == after:
|
||||||
break
|
break
|
||||||
after = str(oldest)
|
after = str(oldest)
|
||||||
|
out = [r for r in out if is_option_full_close_history(r)]
|
||||||
out.sort(key=lambda r: int(_safe_float(r.get("uTime")) or 0), reverse=True)
|
out.sort(key=lambda r: int(_safe_float(r.get("uTime")) or 0), reverse=True)
|
||||||
return out[:cap]
|
return out[:cap]
|
||||||
|
|
||||||
@@ -854,9 +877,17 @@ def format_option_history_row(
|
|||||||
status_label = "强平"
|
status_label = "强平"
|
||||||
else:
|
else:
|
||||||
status_label = "已平"
|
status_label = "已平"
|
||||||
|
pos_id = str(raw.get("posId") or "").strip() or None
|
||||||
|
close_ms = int(utime) if utime is not None else None
|
||||||
return {
|
return {
|
||||||
"source": "exchange",
|
"source": "exchange",
|
||||||
"pos_id": str(raw.get("posId") or "").strip() or None,
|
"history_key": option_history_row_key(
|
||||||
|
source="exchange",
|
||||||
|
inst_id=inst_id,
|
||||||
|
pos_id=pos_id,
|
||||||
|
close_ms=close_ms,
|
||||||
|
),
|
||||||
|
"pos_id": pos_id,
|
||||||
"inst_id": inst_id,
|
"inst_id": inst_id,
|
||||||
"underlying": uly,
|
"underlying": uly,
|
||||||
"opt_type": opt_type,
|
"opt_type": opt_type,
|
||||||
@@ -876,7 +907,7 @@ def format_option_history_row(
|
|||||||
"close_type": close_type,
|
"close_type": close_type,
|
||||||
"created_at": _ms_to_iso(ctime),
|
"created_at": _ms_to_iso(ctime),
|
||||||
"closed_at": _ms_to_iso(utime),
|
"closed_at": _ms_to_iso(utime),
|
||||||
"close_ms": int(utime) if utime is not None else None,
|
"close_ms": close_ms,
|
||||||
"tick_sz": tick_sz,
|
"tick_sz": tick_sz,
|
||||||
"raw": raw,
|
"raw": raw,
|
||||||
}
|
}
|
||||||
@@ -889,9 +920,17 @@ def format_live_option_history_row(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""将当前持仓格式化为历史列表中的「持仓中」行."""
|
"""将当前持仓格式化为历史列表中的「持仓中」行."""
|
||||||
inst_id = str(row.get("inst_id") or "").strip()
|
inst_id = str(row.get("inst_id") or "").strip()
|
||||||
|
pos_id = str((row.get("raw") or {}).get("posId") or "").strip() or None
|
||||||
|
close_ms = open_ms
|
||||||
return {
|
return {
|
||||||
"source": "live",
|
"source": "live",
|
||||||
"pos_id": str((row.get("raw") or {}).get("posId") or "").strip() or None,
|
"history_key": option_history_row_key(
|
||||||
|
source="live",
|
||||||
|
inst_id=inst_id,
|
||||||
|
pos_id=pos_id,
|
||||||
|
close_ms=close_ms,
|
||||||
|
),
|
||||||
|
"pos_id": pos_id,
|
||||||
"inst_id": inst_id,
|
"inst_id": inst_id,
|
||||||
"underlying": str(row.get("underlying") or inst_id.split("-")[0] or ""),
|
"underlying": str(row.get("underlying") or inst_id.split("-")[0] or ""),
|
||||||
"opt_type": row.get("opt_type"),
|
"opt_type": row.get("opt_type"),
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS options_history_hidden (
|
||||||
|
history_key TEXT PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS options_transfer_log (
|
CREATE TABLE IF NOT EXISTS options_transfer_log (
|
||||||
|
|||||||
@@ -915,6 +915,17 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
hidden_keys: set[str] = set()
|
||||||
|
conn = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
init_options_tables(conn)
|
||||||
|
hidden_keys = {
|
||||||
|
str(r["history_key"])
|
||||||
|
for r in conn.execute("SELECT history_key FROM options_history_hidden").fetchall()
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
hist_raw = fetch_all_option_positions_history(ex, limit=200)
|
hist_raw = fetch_all_option_positions_history(ex, limit=200)
|
||||||
for raw in hist_raw:
|
for raw in hist_raw:
|
||||||
inst_id = str(raw.get("instId") or "").strip()
|
inst_id = str(raw.get("instId") or "").strip()
|
||||||
@@ -925,7 +936,11 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
closed = [x for x in items if x.get("status") != "open"]
|
closed = [x for x in items if x.get("status") != "open"]
|
||||||
closed.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
|
closed.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
|
||||||
open_rows.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
|
open_rows.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
|
||||||
history = open_rows + closed
|
history = [
|
||||||
|
x
|
||||||
|
for x in (open_rows + closed)
|
||||||
|
if str(x.get("history_key") or "") not in hidden_keys
|
||||||
|
]
|
||||||
live_ids = {str(x.get("inst_id") or "") for x in open_rows}
|
live_ids = {str(x.get("inst_id") or "") for x in open_rows}
|
||||||
return jsonify({"ok": True, "history": history, "live_inst_ids": sorted(live_ids)})
|
return jsonify({"ok": True, "history": history, "live_inst_ids": sorted(live_ids)})
|
||||||
|
|
||||||
@@ -939,13 +954,26 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
return jsonify({"ok": True, **compute_options_stats(cfg["get_db"])})
|
return jsonify({"ok": True, **compute_options_stats(cfg["get_db"])})
|
||||||
|
|
||||||
@app.route("/api/options/history/<path:trade_id>", methods=["DELETE"])
|
@app.route("/api/options/history/<path:history_key>", methods=["DELETE"])
|
||||||
@lr
|
@lr
|
||||||
def api_options_history_delete(trade_id: str):
|
def api_options_history_delete(history_key: str):
|
||||||
ex, err = _require_options_ex(cfg)
|
ex, err = _require_options_ex(cfg)
|
||||||
if ex is None:
|
if ex is None:
|
||||||
return jsonify({"ok": False, "msg": err})
|
return jsonify({"ok": False, "msg": err})
|
||||||
return jsonify({"ok": False, "msg": "历史仓位来自交易所,不支持本地删除"})
|
key = (history_key or "").strip()
|
||||||
|
if not key:
|
||||||
|
return jsonify({"ok": False, "msg": "缺少 history_key"})
|
||||||
|
conn = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
init_options_tables(conn)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO options_history_hidden (history_key) VALUES (?)",
|
||||||
|
(key,),
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
|||||||
@@ -198,10 +198,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>
|
||||||
@@ -211,4 +212,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||||
<script src="/static/options_panel.js?v=24"></script>
|
<script src="/static/options_panel.js?v=25"></script>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import sqlite3
|
|||||||
from lib.exchange.okx_options_lib import (
|
from lib.exchange.okx_options_lib import (
|
||||||
format_option_history_row,
|
format_option_history_row,
|
||||||
format_usdc_amount,
|
format_usdc_amount,
|
||||||
|
is_option_full_close_history,
|
||||||
resolve_option_close_from_history,
|
resolve_option_close_from_history,
|
||||||
)
|
)
|
||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
@@ -11,9 +12,16 @@ from lib.options.options_monitor_lib import sync_open_options_trades
|
|||||||
|
|
||||||
|
|
||||||
def test_format_usdc_amount():
|
def test_format_usdc_amount():
|
||||||
assert format_usdc_amount(4.896) == "4.896"
|
assert format_usdc_amount(4.896) == "4.90"
|
||||||
assert format_usdc_amount(4.90) == "4.9"
|
assert format_usdc_amount(4.9) == "4.90"
|
||||||
assert format_usdc_amount(4.0) == "4"
|
assert format_usdc_amount(4.0) == "4.00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_option_full_close_history():
|
||||||
|
assert is_option_full_close_history({"type": "2"})
|
||||||
|
assert is_option_full_close_history({"type": "3"})
|
||||||
|
assert not is_option_full_close_history({"type": "1"})
|
||||||
|
assert not is_option_full_close_history({"type": "5"})
|
||||||
|
|
||||||
|
|
||||||
def test_format_option_history_row():
|
def test_format_option_history_row():
|
||||||
@@ -36,7 +44,8 @@ def test_format_option_history_row():
|
|||||||
assert row["realized_pnl"] == -3.99
|
assert row["realized_pnl"] == -3.99
|
||||||
assert row["status_label"] == "已平"
|
assert row["status_label"] == "已平"
|
||||||
assert row["open_avg_px_fmt"] == "380"
|
assert row["open_avg_px_fmt"] == "380"
|
||||||
assert row["premium_paid_fmt"] == "3.8"
|
assert row["premium_paid_fmt"] == "3.80"
|
||||||
|
assert row["history_key"] == "ex:pos-btc"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_option_close_from_history_picks_latest():
|
def test_resolve_option_close_from_history_picks_latest():
|
||||||
|
|||||||
Reference in New Issue
Block a user