Load options review from local records with compact journal UI.
Drop exchange sync for the review page, auto-import closed local options and hedge plans, and match contract-style multi-timeframe upload styling with smaller fonts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-4
@@ -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`,刷新本地源**不会覆盖**.
|
||||
|
||||
### 图片
|
||||
|
||||
|
||||
@@ -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 = '<tr><td colspan="7" class="muted">加载失败</td></tr>';
|
||||
return;
|
||||
@@ -136,7 +108,8 @@
|
||||
var rows = data.trades || [];
|
||||
tradesCache = {};
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="muted">暂无记录,请先同步</td></tr>';
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="7" class="muted">暂无本地已平记录(期权页平仓后或对冲计划结束后会出现在此)</td></tr>';
|
||||
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);
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,67 +1,75 @@
|
||||
{# OKX 期权复盘:三类 Tab + 常驻复盘上传(同合约多周期截图) + 列表 #}
|
||||
{# OKX 期权复盘:读本地记录;复盘上传区字体缩小,截图槽位对齐合约 #}
|
||||
<div class="options-review-wrap" id="options-review-root" style="grid-column:1/-1">
|
||||
{% if not options_enabled %}
|
||||
<div class="flash" style="margin-bottom:12px">期权未启用:请设置 <code>OKX_OPTIONS_ENABLED=true</code> 后重启.对冲计划仍可单独同步(若本地有已结束计划).</div>
|
||||
<div class="flash" style="margin-bottom:12px;font-size:.82rem">期权未启用:请设置 <code>OKX_OPTIONS_ENABLED=true</code> 后重启.</div>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
.or-tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
|
||||
.or-tab{border:1px solid rgba(127,127,127,.35);background:transparent;color:inherit;padding:8px 14px;border-radius:8px;cursor:pointer}
|
||||
.options-review-wrap{font-size:.82rem}
|
||||
.options-review-wrap h2{font-size:1rem;margin:0 0 8px}
|
||||
.options-review-wrap h3{font-size:.9rem}
|
||||
.or-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.or-tab{border:1px solid rgba(127,127,127,.35);background:transparent;color:inherit;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:.78rem}
|
||||
.or-tab.active{background:rgba(59,130,246,.25);border-color:rgba(59,130,246,.55)}
|
||||
.or-badge{display:inline-block;padding:2px 8px;border-radius:999px;background:rgba(127,127,127,.2);font-size:12px}
|
||||
.or-stat-card{border:1px solid rgba(127,127,127,.25);border-radius:8px;padding:10px}
|
||||
.or-badge{display:inline-block;padding:1px 6px;border-radius:999px;background:rgba(127,127,127,.2);font-size:.7rem}
|
||||
.or-stat-card{border:1px solid rgba(127,127,127,.25);border-radius:8px;padding:8px;font-size:.78rem}
|
||||
.or-trades-table{font-size:.78rem}
|
||||
.or-trades-table tr.or-row-active{outline:1px solid rgba(59,130,246,.55);background:rgba(59,130,246,.08)}
|
||||
.or-trades-table tbody tr{cursor:pointer}
|
||||
.or-upload-slots{display:flex;flex-direction:column;gap:8px;margin:10px 0}
|
||||
.or-upload-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.or-upload-slot-label{min-width:36px;font-weight:600}
|
||||
.or-mood-grid{display:flex;flex-wrap:wrap;gap:10px 14px;margin:10px 0}
|
||||
.or-form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:8px}
|
||||
.or-form-grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;margin-bottom:8px}
|
||||
.or-journal-idle .or-journal-body{opacity:.72}
|
||||
.or-journal-card{font-size:.78rem}
|
||||
.or-journal-card h2{font-size:.92rem}
|
||||
.or-journal-card input,
|
||||
.or-journal-card select,
|
||||
.or-journal-card textarea,
|
||||
.or-journal-card button{font-size:.76rem}
|
||||
.or-journal-card .or-form-grid,
|
||||
.or-journal-card .or-form-grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px;margin-bottom:6px}
|
||||
.or-journal-card .or-mood-grid{display:flex;flex-wrap:wrap;gap:6px 12px;margin:8px 0;font-size:.74rem}
|
||||
.or-journal-card .muted,
|
||||
.or-journal-card .sub{font-size:.7rem}
|
||||
.or-journal-idle .or-journal-body{opacity:.78}
|
||||
</style>
|
||||
|
||||
<div class="card" style="margin-bottom:12px">
|
||||
<div class="form-row" style="flex-wrap:wrap;align-items:center;gap:8px">
|
||||
<div class="card" style="margin-bottom:10px">
|
||||
<div class="form-row" style="flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<h2 style="margin:0;margin-right:auto">期权复盘</h2>
|
||||
<button type="button" class="btn-secondary" id="or-sync-btn">从 OKX / 对冲计划同步</button>
|
||||
<span class="muted" id="or-sync-status"></span>
|
||||
<span class="muted" id="or-sync-status" style="font-size:.72rem"></span>
|
||||
<button type="button" class="btn-secondary" id="or-reload-btn" style="font-size:.76rem;padding:4px 10px">刷新</button>
|
||||
</div>
|
||||
<div class="or-tabs" role="tablist" aria-label="复盘分类">
|
||||
<button type="button" class="or-tab active" data-source="option_spot" role="tab">期权交易记录</button>
|
||||
<button type="button" class="or-tab" data-source="options_options" role="tab">期期对冲记录</button>
|
||||
<button type="button" class="or-tab" data-source="perp_options" role="tab">永期对冲记录</button>
|
||||
</div>
|
||||
<div class="form-row" style="flex-wrap:wrap;gap:8px;margin-top:10px">
|
||||
<select id="or-filter-uly">
|
||||
<p class="muted" style="margin:0 0 8px;font-size:.72rem">直接读取本地已平期权与已结束对冲计划,无需同步交易所.点下方记录可填入复盘表单.</p>
|
||||
<div class="form-row" style="flex-wrap:wrap;gap:6px">
|
||||
<select id="or-filter-uly" style="font-size:.76rem">
|
||||
<option value="">标的:全部</option>
|
||||
<option value="ETH">ETH</option>
|
||||
<option value="BTC">BTC</option>
|
||||
</select>
|
||||
<select id="or-filter-opt">
|
||||
<select id="or-filter-opt" style="font-size:.76rem">
|
||||
<option value="">Call/Put:全部</option>
|
||||
<option value="C">Call</option>
|
||||
<option value="P">Put</option>
|
||||
</select>
|
||||
<select id="or-filter-reviewed">
|
||||
<select id="or-filter-reviewed" style="font-size:.76rem">
|
||||
<option value="">复盘:全部</option>
|
||||
<option value="0">待复盘</option>
|
||||
<option value="1">已复盘</option>
|
||||
</select>
|
||||
<input type="text" id="or-filter-strategy" placeholder="策略标签" style="max-width:120px">
|
||||
<input type="datetime-local" id="or-filter-from" title="平仓起">
|
||||
<input type="datetime-local" id="or-filter-to" title="平仓止">
|
||||
<label class="muted" style="display:flex;align-items:center;gap:4px">
|
||||
<input type="text" id="or-filter-strategy" placeholder="策略标签" style="max-width:110px;font-size:.76rem">
|
||||
<input type="datetime-local" id="or-filter-from" title="平仓起" style="font-size:.76rem">
|
||||
<input type="datetime-local" id="or-filter-to" title="平仓止" style="font-size:.76rem">
|
||||
<label class="muted" style="display:flex;align-items:center;gap:4px;font-size:.72rem">
|
||||
<input type="checkbox" id="or-include-hedge-legs"> 含已归属对冲的期权腿
|
||||
</label>
|
||||
<button type="button" class="btn-secondary" id="or-reload-btn">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 与合约一致:复盘上传区常驻,不依赖先点列表 #}
|
||||
<div class="card or-journal-idle" id="or-journal-card" style="margin-bottom:12px">
|
||||
<h2 style="margin-top:0">复盘记录上传(含截图)</h2>
|
||||
<p class="muted" id="or-journal-summary" style="margin-top:0">先点「同步」拉记录,再在下方列表点选一笔,即可自动填入开平时间与盈亏;也可先上传截图再选记录保存.</p>
|
||||
<div class="card journal-card or-journal-card or-journal-idle" id="or-journal-card" style="margin-bottom:10px">
|
||||
<h2>复盘记录上传(含截图)</h2>
|
||||
<p class="muted" id="or-journal-summary" style="margin-top:0">点下方列表一笔记录自动填入;截图槽位与合约复盘相同(5m / 15m / 1h / 4h).</p>
|
||||
<div class="or-journal-body">
|
||||
<form id="or-journal-form" onsubmit="return false;">
|
||||
<input type="hidden" id="or-trade-id" value="">
|
||||
@@ -94,21 +102,22 @@
|
||||
</select>
|
||||
<input type="text" id="or-f-result" placeholder="结果标签">
|
||||
</div>
|
||||
<textarea id="or-f-entry" rows="2" placeholder="入场逻辑" style="width:100%;margin-bottom:8px"></textarea>
|
||||
<textarea id="or-f-entry" rows="2" placeholder="入场逻辑" style="width:100%;margin-bottom:6px;box-sizing:border-box"></textarea>
|
||||
|
||||
<div class="or-upload-slots" id="or-upload-slots">
|
||||
<input type="hidden" id="journal-draft-id" value="">
|
||||
<div class="journal-upload-slots" id="or-upload-slots">
|
||||
{% for tf in ['5m', '15m', '1h', '4h'] %}
|
||||
<div class="or-upload-row journal-upload-row" data-tf="{{ tf }}">
|
||||
<span class="or-upload-slot-label">{{ tf }}</span>
|
||||
<input type="file" accept="image/*" class="or-upload-input" data-tf="{{ tf }}">
|
||||
<input type="hidden" class="or-upload-hidden" data-tf="{{ tf }}" value="">
|
||||
<span class="or-upload-status muted" data-tf="{{ tf }}" aria-live="polite"></span>
|
||||
<div class="journal-upload-row" data-tf="{{ tf }}">
|
||||
<span class="journal-upload-slot-label">{{ tf }}</span>
|
||||
<input type="file" accept="image/*" class="journal-upload-slot-input or-upload-input" data-tf="{{ tf }}">
|
||||
<input type="hidden" class="journal-upload-hidden-file or-upload-hidden" data-tf="{{ tf }}" value="">
|
||||
<span class="journal-upload-status or-upload-status" data-tf="{{ tf }}" aria-live="polite"></span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="sub muted" style="margin-top:0">可只传部分周期;选文件后即时上传(与合约复盘相同 5m/15m/1h/4h)</p>
|
||||
<p class="sub journal-upload-hint">可只传部分周期;选文件后即时上传,保存后详情页四宫格查看</p>
|
||||
|
||||
<div class="or-mood-grid">
|
||||
<div class="or-mood-grid mood-grid">
|
||||
<label><input type="checkbox" class="or-mood" value="怕踏空">怕踏空</label>
|
||||
<label><input type="checkbox" class="or-mood" value="报复开仓">报复开仓</label>
|
||||
<label><input type="checkbox" class="or-mood" value="盈利飘了">盈利飘了</label>
|
||||
@@ -116,27 +125,27 @@
|
||||
<label><input type="checkbox" class="or-mood" value="扛单">扛单</label>
|
||||
<label><input type="checkbox" class="or-mood" value="重仓违规">重仓违规</label>
|
||||
</div>
|
||||
<textarea id="or-f-note" rows="2" placeholder="备注" style="width:100%"></textarea>
|
||||
<div class="form-row" style="margin-top:10px;gap:8px">
|
||||
<textarea id="or-f-note" rows="2" placeholder="备注" style="width:100%;box-sizing:border-box"></textarea>
|
||||
<div class="form-row" style="margin-top:8px;gap:6px">
|
||||
<button type="button" class="btn" id="or-save-btn">保存复盘记录</button>
|
||||
<button type="button" class="btn-secondary" id="or-clear-btn">清空表单</button>
|
||||
<button type="button" class="btn-secondary" id="or-del-btn">删除复盘</button>
|
||||
<span class="muted" id="or-save-status"></span>
|
||||
</div>
|
||||
<div id="or-legs-host" style="margin-top:12px"></div>
|
||||
<div id="or-legs-host" style="margin-top:10px;font-size:.74rem"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:12px">
|
||||
<div class="card" style="margin-bottom:10px">
|
||||
<h3 style="margin-top:0">统计</h3>
|
||||
<div id="or-kpi" class="form-row" style="flex-wrap:wrap;gap:12px"></div>
|
||||
<div id="or-stats-groups" style="margin-top:12px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px"></div>
|
||||
<div id="or-kpi" class="form-row" style="flex-wrap:wrap;gap:10px"></div>
|
||||
<div id="or-stats-groups" style="margin-top:10px;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0" id="or-list-title">期权交易记录</h3>
|
||||
<p class="muted" style="margin-top:0">点选一行,上方复盘表单会自动填入该笔数据.</p>
|
||||
<p class="muted" style="margin-top:0;font-size:.72rem">点选一行,上方复盘表单自动填入.</p>
|
||||
<div class="options-strike-table-wrap">
|
||||
<table class="options-strike-table or-trades-table" id="or-trades-table">
|
||||
<thead>
|
||||
@@ -158,4 +167,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/options_review.js?v=3"></script>
|
||||
<script src="/static/options_review.js?v=4"></script>
|
||||
|
||||
@@ -226,6 +226,32 @@ class OptionsReviewTests(unittest.TestCase):
|
||||
).fetchone()
|
||||
self.assertEqual(float(row["realized_pnl_total"]), 1.23)
|
||||
|
||||
def test_local_options_trades_import(self):
|
||||
conn = _conn()
|
||||
from lib.options.options_db import init_options_tables
|
||||
|
||||
init_options_tables(conn)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO options_trades
|
||||
(inst_id, underlying, opt_type, strike, sheets, eth_amount,
|
||||
open_quote, premium_paid, status, realized_pnl, created_at, closed_at)
|
||||
VALUES ('ETH-USD-260328-2000-C','ETH','C',2000,2,0.02,0.01,0.5,'closed',3.2,
|
||||
'2026-03-01 10:00:00','2026-03-01 12:00:00')
|
||||
"""
|
||||
)
|
||||
from lib.options.options_review_lib import sync_options_from_local_trades
|
||||
|
||||
out = sync_options_from_local_trades(conn)
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertEqual(out["inserted"], 1)
|
||||
row = conn.execute(
|
||||
"SELECT history_key, realized_pnl_total, source_type FROM options_review_trades"
|
||||
).fetchone()
|
||||
self.assertTrue(str(row["history_key"]).startswith("local_opt:"))
|
||||
self.assertEqual(float(row["realized_pnl_total"]), 3.2)
|
||||
self.assertEqual(row["source_type"], SOURCE_OPTION)
|
||||
|
||||
def test_image_namespace(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
folder = options_review_upload_dir(tmp)
|
||||
|
||||
Reference in New Issue
Block a user