diff --git a/docs/期权用法.md b/docs/期权用法.md
index c870737..ec25efb 100644
--- a/docs/期权用法.md
+++ b/docs/期权用法.md
@@ -106,14 +106,14 @@ OKX_OPTIONS_API_PASSPHRASE=...
| 类型 | source_type | 来源 | 粒度 |
|------|-------------|------|------|
-| 纯期权 | `option_spot` | OKX `positions-history` 已全平仓位 | 一仓一条 |
+| 纯期权 | `option_spot` | 本地 `options_trades` 已平仓 | 一仓一条 |
| 永期对冲 | `perp_options` | 本地 `hedge_plans` 且 `status=closed` | **一计划一条** |
| 期期对冲 | `options_options` | 同上 | **一计划一条** |
-- 同步按钮:`POST /api/options/review/sync`(只读导入,不下单/不平仓).
+- 打开复盘页即自动读取本地记录,**不访问交易所**.
- 对冲盈亏主口径:`realized_pnl_total`;详情另显永续/期权分项.
-- 若某纯期权 `inst_id` 已出现在对冲腿中,默认标记排除,避免总盈亏双计(可勾选「含已归属对冲的期权腿」查看).
-- 人工复盘字段(策略/备注/图片)存在 `options_review_entries`,再次同步**不会覆盖**.
+- 若某纯期权 `inst_id` 已出现在对冲腿中,默认标记排除,避免总盈亏双计.
+- 人工复盘字段存在 `options_review_entries`,刷新本地源**不会覆盖**.
### 图片
diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js
index dd8de8c..ca9a7c5 100644
--- a/lib/common/static/options_review.js
+++ b/lib/common/static/options_review.js
@@ -74,34 +74,10 @@
if (el) el.textContent = text || "";
}
- function syncNow() {
- setSyncStatus("同步中…");
- fetch("/api/options/review/sync", { method: "POST", credentials: "same-origin" })
- .then(function (r) {
- return r.json();
- })
- .then(function (data) {
- var parts = [];
- if (data.options) {
- parts.push(
- data.options.ok
- ? "期权 +" + (data.options.inserted || 0) + "/更" + (data.options.updated || 0)
- : "期权:" + (data.options.msg || "失败")
- );
- }
- if (data.hedge) {
- parts.push(
- data.hedge.ok
- ? "对冲 +" + (data.hedge.inserted || 0) + "/更" + (data.hedge.updated || 0)
- : "对冲失败"
- );
- }
- setSyncStatus(parts.join(" · ") || "完成");
- reloadAll();
- })
- .catch(function () {
- setSyncStatus("同步失败");
- });
+ function reloadAll() {
+ setSyncStatus("读取本地记录…");
+ loadTrades();
+ loadStats();
}
function setActiveTab(source) {
@@ -115,11 +91,6 @@
reloadAll();
}
- function reloadAll() {
- loadTrades();
- loadStats();
- }
-
function loadTrades() {
var tbody = $("or-trades-tbody");
if (!tbody) return;
@@ -129,6 +100,7 @@
return r.json();
})
.then(function (data) {
+ setSyncStatus("本地记录已加载");
if (!data.ok) {
tbody.innerHTML = '
| 加载失败 |
';
return;
@@ -136,7 +108,8 @@
var rows = data.trades || [];
tradesCache = {};
if (!rows.length) {
- tbody.innerHTML = '| 暂无记录,请先同步 |
';
+ tbody.innerHTML =
+ '| 暂无本地已平记录(期权页平仓后或对冲计划结束后会出现在此) |
';
return;
}
tbody.innerHTML = rows
@@ -304,11 +277,17 @@
var hidden = row && row.querySelector(".or-upload-hidden");
if (!file) {
if (hidden) hidden.value = "";
- if (status) status.textContent = "";
+ if (status) {
+ status.textContent = "";
+ status.className = "journal-upload-status or-upload-status";
+ }
return;
}
if (!draftId) draftId = newDraftId();
- if (status) status.textContent = "上传中…";
+ if (status) {
+ status.textContent = "上传中…";
+ status.className = "journal-upload-status or-upload-status journal-upload-status--pending";
+ }
var fd = new FormData();
fd.append("draft_id", draftId);
fd.append("tf", input.getAttribute("data-tf") || "");
@@ -324,12 +303,18 @@
.then(function (data) {
if (!data.ok) throw new Error(data.error || "fail");
if (hidden) hidden.value = data.file;
- if (status) status.textContent = "上传成功 " + data.file;
+ if (status) {
+ status.textContent = "上传成功 " + data.file;
+ status.className = "journal-upload-status or-upload-status journal-upload-status--ok";
+ }
input.value = "";
})
.catch(function () {
if (hidden) hidden.value = "";
- if (status) status.textContent = "上传失败";
+ if (status) {
+ status.textContent = "上传失败";
+ status.className = "journal-upload-status or-upload-status journal-upload-status--err";
+ }
});
});
});
@@ -394,7 +379,7 @@
var summary = $("or-journal-summary");
if (summary) {
summary.textContent =
- "先点「同步」拉记录,再在下方列表点选一笔填入;截图槽位 5m/15m/1h/4h 可先传.";
+ "点下方列表一笔记录自动填入;截图槽位与合约复盘相同(5m / 15m / 1h / 4h).";
}
var legsHost = $("or-legs-host");
if (legsHost) legsHost.innerHTML = "";
@@ -509,7 +494,7 @@
function saveEntry() {
var tradeId = Number(($("or-trade-id") || {}).value || 0);
if (!tradeId) {
- alert("请先在下方列表点选一笔记录(若无记录请先点同步)");
+ alert("请先在下方列表点选一笔本地记录");
return;
}
var strategy = (($("or-f-strategy") || {}).value || "").trim();
@@ -576,12 +561,10 @@
setActiveTab(btn.getAttribute("data-source"));
});
});
- var syncBtn = $("or-sync-btn");
var reloadBtn = $("or-reload-btn");
var saveBtn = $("or-save-btn");
var clearBtn = $("or-clear-btn");
var delBtn = $("or-del-btn");
- if (syncBtn) syncBtn.addEventListener("click", syncNow);
if (reloadBtn) reloadBtn.addEventListener("click", reloadAll);
if (saveBtn) saveBtn.addEventListener("click", saveEntry);
if (clearBtn) clearBtn.addEventListener("click", hideJournalForm);
diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py
index c0521a7..8da85ad 100644
--- a/lib/options/options_review_lib.py
+++ b/lib/options/options_review_lib.py
@@ -133,6 +133,69 @@ def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) ->
return "inserted"
+def sync_options_from_local_trades(conn: sqlite3.Connection) -> dict[str, Any]:
+ """从本地 options_trades 已平仓记录导入复盘快照(不访问交易所)."""
+ init_options_review_tables(conn)
+ from lib.options.options_db import init_options_tables
+
+ init_options_tables(conn)
+ rows = conn.execute(
+ """
+ SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets,
+ open_quote, close_quote, premium_paid, realized_pnl,
+ created_at, closed_at, signal_note, status
+ FROM options_trades
+ WHERE status = 'closed'
+ ORDER BY id DESC
+ LIMIT 500
+ """
+ ).fetchall()
+ inserted = updated = skipped = 0
+ for r in rows:
+ trade_id = int(r["id"])
+ history_key = f"local_opt:{trade_id}"
+ pnl = _safe_float(r["realized_pnl"])
+ opened_at = r["created_at"]
+ closed_at = r["closed_at"]
+ action = upsert_option_history_row(
+ conn,
+ {
+ "history_key": history_key,
+ "pos_id": f"local:{trade_id}",
+ "inst_id": r["inst_id"],
+ "underlying": r["underlying"],
+ "opt_type": r["opt_type"],
+ "strike": r["strike"],
+ "exp_time": r["exp_time"],
+ "sheets": r["sheets"],
+ "open_avg_px": r["open_quote"],
+ "close_avg_px": r["close_quote"],
+ "premium_paid": r["premium_paid"],
+ "realized_pnl": pnl,
+ "created_at": opened_at,
+ "closed_at": closed_at,
+ "status_label": "已平",
+ },
+ )
+ if action == "inserted":
+ inserted += 1
+ elif action == "updated":
+ updated += 1
+ else:
+ skipped += 1
+ set_sync_state(conn, "options_last_sync_at", _now_str())
+ set_sync_state(conn, "options_last_count", str(len(rows)))
+ set_sync_state(conn, "options_sync_source", "local")
+ return {
+ "ok": True,
+ "source": "local",
+ "fetched": len(rows),
+ "inserted": inserted,
+ "updated": updated,
+ "skipped": skipped,
+ }
+
+
def sync_options_from_exchange(
conn: sqlite3.Connection,
ex: Any,
@@ -141,7 +204,7 @@ def sync_options_from_exchange(
fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None,
format_fn: Optional[Callable[..., dict[str, Any]]] = None,
) -> dict[str, Any]:
- """从 OKX positions-history 导入已全平期权仓位."""
+ """从 OKX positions-history 导入已全平期权仓位(可选,默认不用)."""
init_options_review_tables(conn)
from lib.exchange.okx_options_lib import (
fetch_all_option_positions_history,
@@ -171,8 +234,10 @@ def sync_options_from_exchange(
skipped += 1
set_sync_state(conn, "options_last_sync_at", _now_str())
set_sync_state(conn, "options_last_count", str(len(raw_rows)))
+ set_sync_state(conn, "options_sync_source", "exchange")
return {
"ok": True,
+ "source": "exchange",
"fetched": len(raw_rows),
"inserted": inserted,
"updated": updated,
@@ -326,24 +391,31 @@ def sync_hedge_plans_closed(conn: sqlite3.Connection) -> dict[str, Any]:
def sync_all_review_sources(
conn: sqlite3.Connection,
- ex: Any | None,
+ ex: Any | None = None,
*,
options_limit: int = 500,
+ from_exchange: bool = False,
fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None,
format_fn: Optional[Callable[..., dict[str, Any]]] = None,
) -> dict[str, Any]:
+ """默认只读本地 options_trades + 已结束对冲计划;不访问交易所."""
init_options_review_tables(conn)
out: dict[str, Any] = {"ok": True, "options": None, "hedge": None}
- if ex is not None:
+ if from_exchange and ex is not None:
out["options"] = sync_options_from_exchange(
conn, ex, limit=options_limit, fetch_fn=fetch_fn, format_fn=format_fn
)
else:
- out["options"] = {"ok": False, "msg": "期权 exchange 未就绪"}
+ out["options"] = sync_options_from_local_trades(conn)
out["hedge"] = sync_hedge_plans_closed(conn)
return out
+def ensure_local_review_synced(conn: sqlite3.Connection) -> dict[str, Any]:
+ """列表/统计前轻量刷新本地源."""
+ return sync_all_review_sources(conn, from_exchange=False)
+
+
def _row_to_dict(row: Any) -> dict[str, Any]:
return dict(row) if row is not None else {}
diff --git a/lib/options/options_review_register.py b/lib/options/options_review_register.py
index 229bb11..2b3f4f6 100644
--- a/lib/options/options_review_register.py
+++ b/lib/options/options_review_register.py
@@ -20,6 +20,7 @@ from lib.options.options_review_lib import (
SOURCE_LABELS,
compute_review_stats,
delete_review_entry,
+ ensure_local_review_synced,
get_review_trade,
list_review_trades,
save_review_entry,
@@ -103,14 +104,11 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
@app.route("/api/options/review/sync", methods=["POST"])
@lr
def api_options_review_sync():
+ """刷新本地 options_trades + 已结束对冲计划(不访问交易所)."""
conn = cfg["get_db"]()
try:
init_options_review_tables(conn)
- ex, err = _require_ex(cfg)
- # 对冲可无交易所密钥;期权历史需要密钥
- result = sync_all_review_sources(conn, ex if ex is not None else None)
- if ex is None and result.get("options"):
- result["options"] = {"ok": False, "msg": err}
+ result = sync_all_review_sources(conn, from_exchange=False)
conn.commit()
return jsonify(result)
finally:
@@ -121,6 +119,8 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
def api_options_review_trades():
conn = cfg["get_db"]()
try:
+ ensure_local_review_synced(conn)
+ conn.commit()
items = list_review_trades(
conn,
source_type=(request.args.get("source_type") or "").strip() or None,
@@ -214,6 +214,8 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
def api_options_review_stats():
conn = cfg["get_db"]()
try:
+ ensure_local_review_synced(conn)
+ conn.commit()
stats = compute_review_stats(
conn,
source_type=(request.args.get("source_type") or "").strip() or None,
diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html
index 4138ef0..47b3634 100644
--- a/lib/options/templates/options_review_panel.html
+++ b/lib/options/templates/options_review_panel.html
@@ -1,67 +1,75 @@
-{# OKX 期权复盘:三类 Tab + 常驻复盘上传(同合约多周期截图) + 列表 #}
+{# OKX 期权复盘:读本地记录;复盘上传区字体缩小,截图槽位对齐合约 #}
{% if not options_enabled %}
-
期权未启用:请设置 OKX_OPTIONS_ENABLED=true 后重启.对冲计划仍可单独同步(若本地有已结束计划).
+
期权未启用:请设置 OKX_OPTIONS_ENABLED=true 后重启.
{% endif %}
-