Sync OKX options closed trades into hub archive with a separate tab.

Mirror perpetual archive flow into archive_options_trade_cache for offline calendar and review.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-24 00:39:59 +08:00
parent 6f1ae14b3d
commit 54f1857fa2
8 changed files with 1058 additions and 27 deletions
+142 -5
View File
@@ -35,6 +35,7 @@
const elQuoteContent = document.getElementById("archive-quote-content");
const elQuoteSubmit = document.getElementById("archive-quote-submit");
const elContentTabs = document.getElementById("archive-content-tabs");
const elProductTabs = document.getElementById("archive-product-tabs");
const elPanelViz = document.getElementById("archive-panel-viz");
const elPanelCalendar = document.getElementById("archive-panel-calendar");
const elPanelTrades = document.getElementById("archive-panel-trades");
@@ -76,6 +77,7 @@
let selectedQuoteId = null;
let editingQuoteId = null;
let archiveContentTab = "trades";
let archiveProduct = "perp";
let quoteDayTrades = [];
let quoteDayTradesDay = "";
let quoteDayTradesReq = 0;
@@ -416,6 +418,44 @@
syncPeriodUI();
}
function isOptionsProduct() {
return archiveProduct === "options";
}
function syncProductUI() {
document.body.classList.toggle("archive-product-options", isOptionsProduct());
if (elProductTabs) {
elProductTabs.querySelectorAll(".archive-product-tab").forEach(function (btn) {
const on = btn.getAttribute("data-archive-product") === archiveProduct;
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-selected", on ? "true" : "false");
});
}
if (isOptionsProduct()) {
setChartOpen(false);
if (archiveContentTab === "viz") setArchiveContentTab("trades");
}
}
function setArchiveProduct(product) {
const next = product === "options" ? "options" : "perp";
if (next === archiveProduct) return;
archiveProduct = next;
selected = null;
selectedTradeKey = null;
syncProductUI();
void loadDailyTrades();
void loadCalendar();
}
function dailyTradesApiPath() {
return isOptionsProduct() ? "/api/archive/options/daily-trades" : "/api/archive/daily-trades";
}
function calendarApiPath() {
return isOptionsProduct() ? "/api/archive/options/calendar" : "/api/archive/calendar";
}
function queryDailyParams() {
const q = new URLSearchParams();
q.set("period", periodMode);
@@ -430,7 +470,7 @@
if (ex) q.set("exchange_key", ex);
if (elFilterProfit && elFilterProfit.checked) q.set("filter_profit", "1");
if (elFilterLoss && elFilterLoss.checked) q.set("filter_loss", "1");
if (elFilterSick && elFilterSick.checked) q.set("filter_sick", "1");
if (!isOptionsProduct() && elFilterSick && elFilterSick.checked) q.set("filter_sick", "1");
if (elSearch && elSearch.value.trim()) q.set("search", elSearch.value.trim());
return q.toString();
}
@@ -554,7 +594,7 @@
return q;
},
fetchFn: async function (q) {
const r = await apiFetch("/api/archive/calendar?" + q.toString());
const r = await apiFetch(calendarApiPath() + "?" + q.toString());
return r.json();
},
parseResponse: function (data) {
@@ -1089,7 +1129,7 @@
elQuoteDayTradesBody.innerHTML = '<p class="archive-empty">加载当日已平仓…</p>';
if (elQuoteDayTradesMeta) elQuoteDayTradesMeta.textContent = day;
try {
const r = await apiFetch("/api/archive/daily-trades?" + q.toString());
const r = await apiFetch(dailyTradesApiPath() + "?" + q.toString());
const j = await r.json();
if (req !== quoteDayTradesReq) return;
if (!r.ok) {
@@ -1827,6 +1867,80 @@
return;
}
const pageRows = pagedDailyTrades();
if (isOptionsProduct()) {
elTrades.innerHTML =
'<table class="archive-trades-table"><thead><tr>' +
"<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>" +
pageRows
.map(function (t) {
const rowKey = tradeRowKey(t);
const active = rowKey && rowKey === selectedTradeKey ? " is-active" : "";
const holdMin =
t.hold_minutes != null
? t.hold_minutes
: t.hold_seconds != null
? Number(t.hold_seconds) / 60
: null;
const optLabel =
t.source_label ||
t.source_type ||
(t.opt_type === "C" || t.opt_type === "CALL"
? "Call"
: t.opt_type === "P" || t.opt_type === "PUT"
? "Put"
: "—");
const pnl = t.pnl_amount != null ? t.pnl_amount : t.realized_pnl_total;
return (
'<tr class="archive-trade-row' +
active +
'" data-key="' +
esc(rowKey) +
'">' +
"<td>" +
esc(tradeRowExchange(t)) +
"</td>" +
'<td class="archive-symbol">' +
esc(t.underlying || "—") +
"</td>" +
"<td>" +
esc(t.inst_id || t.source_label || "—") +
"</td>" +
'<td class="archive-dt">' +
fmtDt(t.opened_at) +
"</td>" +
'<td class="archive-dt">' +
fmtDt(t.closed_at) +
"</td>" +
'<td class="archive-hold">' +
fmtDurationMinutes(holdMin) +
"</td>" +
"<td>" +
esc(optLabel) +
"</td>" +
"<td>" +
esc(t.strategy_tag || "—") +
"</td>" +
'<td class="' +
pnlClass(pnl) +
'">' +
fmtPnl(pnl) +
"</td>" +
"<td>" +
fmtVolStat(t.premium_total != null ? t.premium_total : t.premium_paid) +
"</td>" +
"<td>" +
(t.reviewed ? "已复盘" : "—") +
"</td>" +
"</tr>"
);
})
.join("") +
"</tbody></table>";
updateTradesPager();
return;
}
elTrades.innerHTML =
'<table class="archive-trades-table"><thead><tr>' +
"<th>交易所</th><th>合约</th><th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
@@ -2051,7 +2165,7 @@
async function loadDailyTrades() {
setStatus("加载交易记录…");
const r = await apiFetch("/api/archive/daily-trades?" + queryDailyParams());
const r = await apiFetch(dailyTradesApiPath() + "?" + queryDailyParams());
const j = await r.json();
if (!r.ok) {
setStatus(j.detail || "加载失败");
@@ -2080,7 +2194,8 @@
void loadCalendar();
if (archiveContentTab === "quotes") void loadQuoteDayTrades();
setStatus(
(periodLabel || tradingDay || "当日") +
(isOptionsProduct() ? "期权 · " : "永续 · ") +
(periodLabel || tradingDay || "当日") +
" · 列表 " +
dailyTrades.length +
" 笔 · " +
@@ -2105,6 +2220,7 @@
function formatSyncSummary(j) {
const results = j.results || [];
const optResults = j.options_results || [];
const okN = results.filter(function (x) {
return x.ok !== false;
}).length;
@@ -2118,6 +2234,19 @@
parts.push(line);
}
});
optResults.forEach(function (row) {
const label = (row.exchange_key || row.name || "?") + "期权";
if (row.ok === false) parts.push(label + " 失败: " + (row.msg || "未知错误"));
else {
let line =
label +
" " +
(row.trade_count != null ? row.trade_count : row.trades_upserted || 0) +
" 笔";
if (row.trades_removed > 0) line += " 清" + row.trades_removed;
parts.push(line);
}
});
return parts.join(" · ");
}
@@ -2217,6 +2346,13 @@
setArchiveContentTab(btn.getAttribute("data-archive-tab") || "trades");
});
}
if (elProductTabs) {
elProductTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-product-tab");
if (!btn) return;
setArchiveProduct(btn.getAttribute("data-archive-product") || "perp");
});
}
if (elTfTabs) {
elTfTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-tf-btn");
@@ -2249,6 +2385,7 @@
syncPeriodUI();
syncTradesLayout();
bindEvents();
syncProductUI();
setArchiveContentTab("trades");
inited = true;
}