fix期权复盘与数据统计双口径盈亏,并修复卖回封顶导致盈利单冻成亏损。
平仓卖光可卖币后再按账户差分结算复盘;数据统计改为历史币盈亏×卖回价;复盘展示初始权利金与盈利率,并附历史修复脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -102,12 +102,8 @@
|
||||
var kpi = $("ods-kpi");
|
||||
var groups = $("ods-stats-groups");
|
||||
if (!kpi || !groups) return Promise.resolve();
|
||||
var qs = new URLSearchParams();
|
||||
qs.set("source_type", sourceType());
|
||||
qs.set("sync", "0");
|
||||
return fetch("/api/options/review/stats?" + qs.toString(), {
|
||||
credentials: "same-origin",
|
||||
})
|
||||
// 口径 B:期权历史币盈亏×卖回价(/api/options/stats),与复盘账户差分独立
|
||||
return fetch("/api/options/stats", { credentials: "same-origin" })
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
})
|
||||
@@ -117,19 +113,26 @@
|
||||
groups.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
var k = data.kpi || {};
|
||||
var unit = data.pnl_unit || "U";
|
||||
var net = data.net_realized_pnl;
|
||||
var avg =
|
||||
data.total_closed > 0 && net != null
|
||||
? Number(net) / Number(data.total_closed)
|
||||
: null;
|
||||
kpi.innerHTML = [
|
||||
["笔数", k.total],
|
||||
["已复盘率", (k.review_rate || 0) + "%"],
|
||||
["胜率", (k.win_rate || 0) + "%"],
|
||||
["累计盈亏", fmtPnl(k.pnl_sum)],
|
||||
["平均盈亏", fmtPnl(k.avg_pnl)],
|
||||
["平均持有", fmtHold(k.avg_hold_sec)],
|
||||
["已平笔数", data.total_closed || 0],
|
||||
["胜率", (data.win_rate || 0) + "%"],
|
||||
["盈亏比", data.profit_loss_ratio != null ? data.profit_loss_ratio : "—"],
|
||||
["累计盈亏", fmtPnl(net)],
|
||||
["平均盈利", data.avg_win != null ? fmtPnl(data.avg_win) : "—"],
|
||||
["平均亏损", data.avg_loss != null ? fmtPnl(-Math.abs(Number(data.avg_loss))) : "—"],
|
||||
["平均持有", fmtHold(data.avg_hold_sec)],
|
||||
]
|
||||
.map(function (pair) {
|
||||
var cls = "";
|
||||
if (pair[0] === "累计盈亏") cls = pnlClass(k.pnl_sum);
|
||||
if (pair[0] === "平均盈亏") cls = pnlClass(k.avg_pnl);
|
||||
if (pair[0] === "累计盈亏") cls = pnlClass(net);
|
||||
if (pair[0] === "平均盈利") cls = "pnl-pos";
|
||||
if (pair[0] === "平均亏损") cls = "pnl-neg";
|
||||
return (
|
||||
'<div class="ods-kpi-tile"><div class="ods-kpi-label">' +
|
||||
pair[0] +
|
||||
@@ -141,18 +144,11 @@
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
var html = [
|
||||
renderGroup("按类型", data.by_source_type),
|
||||
renderGroup("按标的", data.by_underlying),
|
||||
renderGroup("按策略", data.by_strategy),
|
||||
renderGroup("对冲结束原因", data.by_close_reason),
|
||||
renderGroup("持有周期", data.by_hold_bucket),
|
||||
renderGroup("Call/Put", data.by_opt_type),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
groups.innerHTML =
|
||||
html || '<div class="muted" style="font-size:.76rem">暂无分组数据</div>';
|
||||
'<div class="muted" style="font-size:.76rem">数据统计口径:期权历史盈亏×平仓卖回价(与复盘账户差分独立)。单位 ' +
|
||||
unit +
|
||||
(avg != null ? " · 笔均 " + fmtPnl(avg) : "") +
|
||||
"</div>";
|
||||
})
|
||||
.catch(function () {
|
||||
kpi.innerHTML = '<div class="muted" style="font-size:.76rem">统计加载失败</div>';
|
||||
|
||||
+1561
-1516
File diff suppressed because it is too large
Load Diff
@@ -92,24 +92,107 @@ def transfer_blocked_by_position_msg(conn, ex: Any = None) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def sellable_coin_blocks_settle(
|
||||
ex: Any,
|
||||
underlying: str | None,
|
||||
) -> str | None:
|
||||
"""仍有可卖标的币则禁止账户差分结算(规格:卖光再冻结)."""
|
||||
from lib.options.options_spot_bridge_lib import (
|
||||
fetch_trading_coin_available,
|
||||
is_sellable_spot_coin,
|
||||
)
|
||||
|
||||
uly = (underlying or "").strip().upper()
|
||||
if uly not in ("ETH", "BTC"):
|
||||
return None
|
||||
try:
|
||||
avail = fetch_trading_coin_available(ex, uly)
|
||||
except Exception:
|
||||
return None
|
||||
if avail is None:
|
||||
return None
|
||||
if is_sellable_spot_coin(ex, uly, avail):
|
||||
return f"仍有可卖 {uly}({float(avail):g}),暂不结算账户盈亏"
|
||||
return None
|
||||
|
||||
|
||||
def _underlying_for_settle(
|
||||
conn,
|
||||
*,
|
||||
bridge_id: int | None = None,
|
||||
trade_id: int | None = None,
|
||||
) -> str | None:
|
||||
try:
|
||||
if trade_id is not None:
|
||||
row = conn.execute(
|
||||
"SELECT underlying FROM options_trades WHERE id=?",
|
||||
(int(trade_id),),
|
||||
).fetchone()
|
||||
if row:
|
||||
return str(row["underlying"] if hasattr(row, "keys") else row[0] or "").strip().upper() or None
|
||||
if bridge_id is not None:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT underlying FROM options_trades
|
||||
WHERE bridge_id=? AND status='closed'
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""",
|
||||
(int(bridge_id),),
|
||||
).fetchone()
|
||||
if row:
|
||||
return str(row["underlying"] if hasattr(row, "keys") else row[0] or "").strip().upper() or None
|
||||
brow = conn.execute(
|
||||
"SELECT underlying FROM options_spot_bridge WHERE id=?",
|
||||
(int(bridge_id),),
|
||||
).fetchone()
|
||||
if brow:
|
||||
return str(brow["underlying"] if hasattr(brow, "keys") else brow[0] or "").strip().upper() or None
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def try_settle_after_sellback(
|
||||
get_db: Any,
|
||||
ex: Any,
|
||||
*,
|
||||
bridge_id: int | None = None,
|
||||
cfg: dict[str, Any] | None = None,
|
||||
underlying: str | None = None,
|
||||
sell_complete: bool | None = None,
|
||||
) -> int:
|
||||
"""卖回路径通用结算:读交易账户 after 并冻结未结算单."""
|
||||
pack = cfg if isinstance(cfg, dict) else {"fetch_options_balances": None}
|
||||
after = fetch_trading_usdt(pack, ex)
|
||||
if after is None:
|
||||
"""卖回路径通用结算:读交易账户 after 并冻结未结算单.
|
||||
|
||||
门禁:sell_complete 显式 False 时不结算;仍有可卖标的币时不结算.
|
||||
"""
|
||||
if sell_complete is False:
|
||||
return 0
|
||||
conn = get_db() if callable(get_db) else get_db
|
||||
close_conn = callable(get_db)
|
||||
pack = cfg if isinstance(cfg, dict) else {"fetch_options_balances": None}
|
||||
import sqlite3
|
||||
|
||||
# Windows 上 sqlite3.Connection 的 callable() 可能为 True,不能据此判断 get_db
|
||||
if isinstance(get_db, sqlite3.Connection):
|
||||
conn = get_db
|
||||
close_conn = False
|
||||
elif callable(get_db):
|
||||
conn = get_db()
|
||||
close_conn = True
|
||||
else:
|
||||
conn = get_db
|
||||
close_conn = False
|
||||
try:
|
||||
from lib.options.options_db import init_options_tables
|
||||
|
||||
init_options_tables(conn)
|
||||
uly = (underlying or "").strip().upper() or None
|
||||
if not uly:
|
||||
uly = _underlying_for_settle(conn, bridge_id=bridge_id)
|
||||
block = sellable_coin_blocks_settle(ex, uly)
|
||||
if block:
|
||||
return 0
|
||||
after = fetch_trading_usdt(pack, ex)
|
||||
if after is None:
|
||||
return 0
|
||||
n = 0
|
||||
if bridge_id is not None:
|
||||
n = settle_closed_trades_from_trading_account(
|
||||
@@ -125,10 +208,16 @@ def try_settle_after_sellback(
|
||||
"""
|
||||
).fetchone()
|
||||
if row:
|
||||
tid = int(row["id"] if hasattr(row, "keys") else row[0])
|
||||
if not uly:
|
||||
uly = _underlying_for_settle(conn, trade_id=tid)
|
||||
block2 = sellable_coin_blocks_settle(ex, uly)
|
||||
if block2:
|
||||
return 0
|
||||
n = settle_closed_trades_from_trading_account(
|
||||
conn,
|
||||
trading_usdt_after=after,
|
||||
trade_id=int(row["id"] if hasattr(row, "keys") else row[0]),
|
||||
trade_id=tid,
|
||||
)
|
||||
try:
|
||||
conn.commit()
|
||||
|
||||
@@ -610,6 +610,14 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
||||
tusdt_before = float(tusdt_before) if tusdt_before is not None else None
|
||||
except (TypeError, ValueError):
|
||||
tusdt_before = None
|
||||
paid = kwargs.get("premium_paid")
|
||||
try:
|
||||
paid_f = float(paid) if paid is not None else None
|
||||
except (TypeError, ValueError):
|
||||
paid_f = None
|
||||
initial_premium_usdt = None
|
||||
if open_index_px is not None and paid_f is not None and paid_f > 0:
|
||||
initial_premium_usdt = round(float(open_index_px) * float(paid_f), 4)
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO options_trades(
|
||||
@@ -617,8 +625,9 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
||||
open_quote, premium_paid, status, signal_note, exchange_ord_id,
|
||||
margin_mode, premium_ccy, bridge_id, budget_usdt,
|
||||
open_fee, fee_ccy, open_index_px, trading_usdt_before, pnl_settled,
|
||||
initial_premium_usdt,
|
||||
profit_exit_enabled, profit_exit_mult, profit_exit_state, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
kwargs["inst_id"],
|
||||
@@ -639,6 +648,7 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
||||
fee_ccy,
|
||||
open_index_px,
|
||||
tusdt_before,
|
||||
initial_premium_usdt,
|
||||
pe,
|
||||
pe_mult,
|
||||
pe_state,
|
||||
@@ -685,17 +695,20 @@ def maybe_sell_spot_after_close(
|
||||
underlying=underlying,
|
||||
inst_id=inst_id,
|
||||
)
|
||||
# 卖回成功:交易账户 after − 开仓 before → 冻结本单盈亏
|
||||
# 卖回完成(sell_complete):交易账户 after − 开仓 before → 冻结本单盈亏
|
||||
try:
|
||||
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||
|
||||
if (result or {}).get("ok"):
|
||||
bid = (result or {}).get("bridge_id")
|
||||
res = result or {}
|
||||
if res.get("ok") and res.get("sell_complete", True) and not res.get("incomplete"):
|
||||
bid = res.get("bridge_id")
|
||||
try_settle_after_sellback(
|
||||
cfg["get_db"],
|
||||
ex,
|
||||
bridge_id=int(bid) if bid is not None else None,
|
||||
cfg=cfg,
|
||||
underlying=underlying,
|
||||
sell_complete=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -766,20 +779,23 @@ def sweep_spot_after_options_flat(
|
||||
underlying=uly,
|
||||
inst_id=None,
|
||||
)
|
||||
if (out or {}).get("ok"):
|
||||
out = out or {}
|
||||
if out.get("ok") and out.get("sell_complete", True) and not out.get("incomplete"):
|
||||
try:
|
||||
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||
|
||||
bid = (out or {}).get("bridge_id")
|
||||
bid = out.get("bridge_id")
|
||||
try_settle_after_sellback(
|
||||
cfg["get_db"],
|
||||
exchange,
|
||||
bridge_id=int(bid) if bid is not None else None,
|
||||
cfg=cfg,
|
||||
underlying=uly,
|
||||
sell_complete=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
sold.append({"underlying": uly, **(out or {})})
|
||||
sold.append({"underlying": uly, **out})
|
||||
except Exception as e:
|
||||
sold.append({"underlying": uly, "ok": False, "msg": str(e)})
|
||||
return {"ok": True, "results": sold, "skipped": not sold}
|
||||
|
||||
@@ -133,6 +133,8 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
||||
"ALTER TABLE options_trades ADD COLUMN trading_usdt_before REAL",
|
||||
"ALTER TABLE options_trades ADD COLUMN trading_usdt_after REAL",
|
||||
"ALTER TABLE options_trades ADD COLUMN pnl_settled INTEGER DEFAULT 0",
|
||||
# 初始权利金(U)=开仓指数×权利金币量,不含多买缓冲
|
||||
"ALTER TABLE options_trades ADD COLUMN initial_premium_usdt REAL",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
|
||||
@@ -1225,7 +1225,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
underlying=underlying,
|
||||
inst_id=inst_id,
|
||||
)
|
||||
if out.get("ok"):
|
||||
if out.get("ok") and out.get("sell_complete", True) and not out.get("incomplete"):
|
||||
try:
|
||||
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||
|
||||
@@ -1235,10 +1235,14 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
ex,
|
||||
bridge_id=int(bid) if bid is not None else None,
|
||||
cfg=cfg,
|
||||
underlying=underlying,
|
||||
sell_complete=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_mark_balances_stale(cfg)
|
||||
elif out.get("ok"):
|
||||
_mark_balances_stale(cfg)
|
||||
return jsonify(out)
|
||||
|
||||
@app.route("/api/options/convert/quote", methods=["POST"])
|
||||
|
||||
@@ -131,6 +131,8 @@ def init_options_review_tables(conn: sqlite3.Connection) -> None:
|
||||
_ensure_column(conn, "options_review_trades", "target_price_up", "REAL")
|
||||
_ensure_column(conn, "options_review_trades", "target_price_down", "REAL")
|
||||
_ensure_column(conn, "options_review_trades", "profit_rr", "REAL")
|
||||
_ensure_column(conn, "options_review_trades", "initial_premium_usdt", "REAL")
|
||||
_ensure_column(conn, "options_review_trades", "profit_rate", "REAL")
|
||||
_ensure_column(conn, "options_review_entries", "prev_entry_tf", "TEXT")
|
||||
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) ->
|
||||
"close_avg": _safe_float(row.get("close_avg_px") if row.get("close_avg_px") is not None else row.get("close_avg")),
|
||||
"premium_paid": _safe_float(row.get("premium_paid")),
|
||||
"realized_pnl": pnl,
|
||||
"initial_premium_usdt": _safe_float(row.get("initial_premium_usdt")),
|
||||
"profit_rate": _safe_float(row.get("profit_rate")),
|
||||
}
|
||||
cols = list(fields.keys())
|
||||
if existing:
|
||||
@@ -317,7 +319,7 @@ def sync_options_from_local_trades(
|
||||
open_quote, close_quote, premium_paid, premium_received, realized_pnl,
|
||||
realized_pnl_usdt, close_index_px, open_index_px, budget_usdt,
|
||||
premium_ccy, created_at, closed_at, signal_note, status,
|
||||
trading_usdt_before, pnl_settled
|
||||
trading_usdt_before, pnl_settled, initial_premium_usdt
|
||||
FROM options_trades
|
||||
WHERE status = 'closed'
|
||||
ORDER BY id DESC
|
||||
@@ -439,12 +441,19 @@ def sync_options_from_local_trades(
|
||||
pnl_u = round(float(frozen_usdt), 4)
|
||||
else:
|
||||
pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx)
|
||||
# 权利金用平仓/开仓冻结指数折算,不用实时价
|
||||
prem_u = _to_usdt(prem, ccy=ccy, idx=idx)
|
||||
if ccy in ("ETH", "BTC") and pnl_u is None:
|
||||
# 初始权利金(U)=开仓指数×权利金币量;不含多买
|
||||
stored_initial = _safe_float(r["initial_premium_usdt"])
|
||||
open_idx_for_prem = open_idx if (open_idx is not None and open_idx > 0) else idx
|
||||
prem_u = stored_initial
|
||||
if prem_u is None:
|
||||
prem_u = _to_usdt(prem, ccy=ccy, idx=open_idx_for_prem)
|
||||
if ccy in ("ETH", "BTC") and pnl_u is None and not acct_pending:
|
||||
pnl_u = pnl
|
||||
if ccy in ("ETH", "BTC") and prem_u is None:
|
||||
prem_u = prem
|
||||
profit_rate = None
|
||||
if pnl_u is not None and prem_u is not None and abs(float(prem_u)) > 1e-12:
|
||||
profit_rate = round(float(pnl_u) / float(prem_u), 6)
|
||||
opened_at = r["created_at"]
|
||||
closed_at = r["closed_at"]
|
||||
action = upsert_option_history_row(
|
||||
@@ -461,10 +470,12 @@ def sync_options_from_local_trades(
|
||||
"open_avg_px": r["open_quote"],
|
||||
"close_avg_px": r["close_quote"],
|
||||
"premium_paid": prem_u if prem_u is not None else prem,
|
||||
"realized_pnl": pnl_u if pnl_u is not None else pnl,
|
||||
"realized_pnl": pnl_u if pnl_u is not None else (None if acct_pending else pnl),
|
||||
"initial_premium_usdt": prem_u,
|
||||
"profit_rate": profit_rate,
|
||||
"created_at": opened_at,
|
||||
"closed_at": closed_at,
|
||||
"status_label": "已平",
|
||||
"status_label": "待结算" if acct_pending else "已平",
|
||||
},
|
||||
)
|
||||
if action == "inserted":
|
||||
@@ -759,6 +770,12 @@ def _row_to_dict(row: Any) -> dict[str, Any]:
|
||||
|
||||
def enrich_trade_row(row: dict[str, Any], entry: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
out = dict(row)
|
||||
# 盈利率缺失时按 复盘盈亏/初始权利金 补算
|
||||
if _safe_float(out.get("profit_rate")) is None:
|
||||
pnl = _safe_float(out.get("realized_pnl_total"))
|
||||
prem = _safe_float(out.get("initial_premium_usdt"))
|
||||
if pnl is not None and prem is not None and abs(prem) > 1e-12:
|
||||
out["profit_rate"] = round(pnl / prem, 6)
|
||||
out["source_label"] = SOURCE_LABELS.get(str(out.get("source_type") or ""), out.get("source_type"))
|
||||
out["is_hedge"] = str(out.get("source_type") or "") in (SOURCE_PERP_OPTIONS, SOURCE_OPTIONS_OPTIONS)
|
||||
legs = []
|
||||
|
||||
@@ -451,14 +451,14 @@ def sell_residual_after_option_flat(
|
||||
underlying: str,
|
||||
inst_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""期权已平:卖掉本桥残留标的币;优先关闭 matching holding/pending 桥.
|
||||
"""期权已平:卖光交易户该标的可卖币(一次一仓,含盈利币).
|
||||
|
||||
规格:平仓卖回禁止按开仓权利金 coin_bought 封顶;仍有可卖则不关桥、不结算.
|
||||
可传 get_db 可调用对象(推荐)或已打开的 Connection。
|
||||
卖币(尤其模拟盘写钱包)期间绝不握着业务连接,否则会 database is locked。
|
||||
"""
|
||||
get_db = conn_or_get_db if callable(conn_or_get_db) else None
|
||||
bid: int | None = None
|
||||
sell_cap: float | None = None
|
||||
want_inst = str(inst_id or "").strip()
|
||||
|
||||
def _pick_bridge(bridges: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||||
@@ -485,7 +485,6 @@ def sell_residual_after_option_flat(
|
||||
target = _pick_bridge(list_open_bridges(conn))
|
||||
if target is not None:
|
||||
bid = int(target["id"])
|
||||
sell_cap = _safe_float(target.get("coin_bought"))
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
@@ -501,7 +500,6 @@ def sell_residual_after_option_flat(
|
||||
target = _pick_bridge(list_open_bridges(conn))
|
||||
if target is not None:
|
||||
bid = int(target["id"])
|
||||
sell_cap = _safe_float(target.get("coin_bought"))
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
@@ -512,13 +510,10 @@ def sell_residual_after_option_flat(
|
||||
avail_chk = fetch_trading_coin_available(ex, underlying)
|
||||
except Exception:
|
||||
avail_chk = None
|
||||
dust_amt = avail_chk
|
||||
if dust_amt is None and sell_cap is not None:
|
||||
dust_amt = sell_cap
|
||||
if dust_amt is not None and not is_sellable_spot_coin(ex, underlying, dust_amt):
|
||||
if avail_chk is not None and not is_sellable_spot_coin(ex, underlying, avail_chk):
|
||||
dust_msg = "粉尘低于现货最小可卖,已忽略"
|
||||
if bid is None:
|
||||
return {"ok": True, "msg": dust_msg, "skipped": True, "dust": True}
|
||||
return {"ok": True, "msg": dust_msg, "skipped": True, "dust": True, "sell_complete": True}
|
||||
def _close_dust(c: sqlite3.Connection) -> dict[str, Any]:
|
||||
update_bridge(
|
||||
c,
|
||||
@@ -533,6 +528,7 @@ def sell_residual_after_option_flat(
|
||||
"msg": dust_msg,
|
||||
"skipped": True,
|
||||
"dust": True,
|
||||
"sell_complete": True,
|
||||
"bridge_id": bid,
|
||||
"bridge_status": BRIDGE_CLOSED,
|
||||
}
|
||||
@@ -547,20 +543,33 @@ def sell_residual_after_option_flat(
|
||||
pass
|
||||
return _close_dust(conn_or_get_db)
|
||||
|
||||
# 有桥时按 coin_bought 封顶,避免误卖无关余额;无桥空仓扫尾仍卖光可用
|
||||
# 一次一仓空仓后卖光可用(含盈利币);禁止用 coin_bought/权利金封顶
|
||||
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
|
||||
sell = spot_market_sell_coin_to_usdt(
|
||||
ex,
|
||||
underlying=underlying,
|
||||
coin_amount=sell_cap if (bid is not None and sell_cap is not None and sell_cap > 0) else None,
|
||||
coin_amount=None,
|
||||
)
|
||||
if bid is None:
|
||||
if not sell.get("ok"):
|
||||
msg = str(sell.get("msg") or "")
|
||||
if "无可用" in msg or "过小" in msg:
|
||||
return {"ok": True, "msg": "无残留币需卖回", "skipped": True}
|
||||
return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL}
|
||||
return {"ok": True, "sell": sell, "bridge_status": None}
|
||||
return {"ok": True, "msg": "无残留币需卖回", "skipped": True, "sell_complete": True}
|
||||
return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL, "sell_complete": False}
|
||||
# 再确认无可卖残留
|
||||
try:
|
||||
left = fetch_trading_coin_available(ex, underlying)
|
||||
except Exception:
|
||||
left = None
|
||||
if left is not None and is_sellable_spot_coin(ex, underlying, left):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": f"卖回未完成,仍有可卖 {str(underlying or '').upper()}",
|
||||
"incomplete": True,
|
||||
"sell_complete": False,
|
||||
"sell": sell,
|
||||
}
|
||||
return {"ok": True, "sell": sell, "bridge_status": None, "sell_complete": True}
|
||||
|
||||
def _apply_bridge(c: sqlite3.Connection) -> dict[str, Any]:
|
||||
if not sell.get("ok"):
|
||||
@@ -580,6 +589,7 @@ def sell_residual_after_option_flat(
|
||||
"msg": "粉尘低于现货最小可卖,已忽略",
|
||||
"skipped": True,
|
||||
"dust": True,
|
||||
"sell_complete": True,
|
||||
"bridge_id": bid,
|
||||
"bridge_status": BRIDGE_CLOSED,
|
||||
}
|
||||
@@ -595,10 +605,36 @@ def sell_residual_after_option_flat(
|
||||
"msg": msg,
|
||||
"bridge_id": bid,
|
||||
"bridge_status": BRIDGE_PENDING_SELL,
|
||||
"sell_complete": False,
|
||||
}
|
||||
recovered = _safe_float(sell.get("usdt_recovered"))
|
||||
if recovered is not None and recovered > 0:
|
||||
add_bridge_usdt_recovered(c, bid, recovered)
|
||||
# 卖后仍有可卖 → 不关桥、不结算(盈利币未卖回时曾误冻亏损)
|
||||
try:
|
||||
left = fetch_trading_coin_available(ex, underlying)
|
||||
except Exception:
|
||||
left = None
|
||||
if left is not None and is_sellable_spot_coin(ex, underlying, left):
|
||||
left_msg = f"卖回未完成,仍有可卖 {str(underlying or '').upper()}({float(left):g})"
|
||||
update_bridge(
|
||||
c,
|
||||
bid,
|
||||
status=BRIDGE_PENDING_SELL,
|
||||
sell_ord_id=str(sell.get("ord_id") or ""),
|
||||
coin_sold=_safe_float(sell.get("coin_sold")),
|
||||
inst_id=inst_id,
|
||||
message=left_msg,
|
||||
)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": left_msg,
|
||||
"incomplete": True,
|
||||
"sell_complete": False,
|
||||
"sell": sell,
|
||||
"bridge_id": bid,
|
||||
"bridge_status": BRIDGE_PENDING_SELL,
|
||||
}
|
||||
update_bridge(
|
||||
c,
|
||||
bid,
|
||||
@@ -609,7 +645,13 @@ def sell_residual_after_option_flat(
|
||||
message="期权已平,币已卖回 USDT",
|
||||
close=True,
|
||||
)
|
||||
return {"ok": True, "sell": sell, "bridge_id": bid, "bridge_status": BRIDGE_CLOSED}
|
||||
return {
|
||||
"ok": True,
|
||||
"sell": sell,
|
||||
"bridge_id": bid,
|
||||
"bridge_status": BRIDGE_CLOSED,
|
||||
"sell_complete": True,
|
||||
}
|
||||
|
||||
if get_db is not None:
|
||||
conn2 = get_db()
|
||||
|
||||
@@ -59,11 +59,16 @@ def _underlying_index_usdt(ex: Any, underly: str) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
统计用:优先已冻结的 realized_pnl_usdt(平仓当时口径,含手续费后净额);
|
||||
否则币本位用 close_index_px(再否则实时指数)折 U;USDC 原样.
|
||||
折算失败的币仓剔除盈亏字段,避免把「币数量」当成 U.
|
||||
def history_pnl_to_usdt(
|
||||
history: list[dict[str, Any]],
|
||||
ex: Any = None,
|
||||
*,
|
||||
prefer_frozen_usdt: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""折 U.
|
||||
|
||||
prefer_frozen_usdt=True:优先行内 realized_pnl_usdt(实盘 exchange freeze / 旧兼容).
|
||||
prefer_frozen_usdt=False:口径 B — 币盈亏 × 平仓卖回价(close_index_px),忽略账户差分 A.
|
||||
"""
|
||||
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
|
||||
|
||||
@@ -75,7 +80,7 @@ def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[d
|
||||
out.append(r)
|
||||
continue
|
||||
frozen = _safe_float(r.get("realized_pnl_usdt"))
|
||||
if frozen is not None:
|
||||
if prefer_frozen_usdt and frozen is not None:
|
||||
r["realized_pnl"] = round(float(frozen), 4)
|
||||
out.append(r)
|
||||
continue
|
||||
@@ -104,6 +109,11 @@ def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[d
|
||||
return out
|
||||
|
||||
|
||||
def history_pnl_to_usdt_stats(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
|
||||
"""数据统计口径 B:期权历史币盈亏 × 平仓卖回价."""
|
||||
return history_pnl_to_usdt(history, ex, prefer_frozen_usdt=False)
|
||||
|
||||
|
||||
def _parse_ts(raw: Any) -> datetime | None:
|
||||
if raw is None or raw == "":
|
||||
return None
|
||||
@@ -286,7 +296,8 @@ def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
|
||||
{
|
||||
"status": "closed",
|
||||
"realized_pnl": _row_get(row, "realized_pnl"),
|
||||
"realized_pnl_usdt": _row_get(row, "realized_pnl_usdt"),
|
||||
# 口径 B 不读账户 realized_pnl_usdt;保留键位兼容旧调用
|
||||
"realized_pnl_usdt": None,
|
||||
"close_index_px": _row_get(row, "close_index_px"),
|
||||
"created_at": _row_get(row, "created_at"),
|
||||
"closed_at": _row_get(row, "closed_at"),
|
||||
@@ -298,16 +309,13 @@ def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
# 口径 B:凡有币盈亏的已平均计入;U 由 history_pnl_to_usdt_stats 按卖回价折算
|
||||
_CLOSED_STATS_SELECT = """
|
||||
SELECT realized_pnl, realized_pnl_usdt, close_index_px,
|
||||
SELECT realized_pnl, close_index_px,
|
||||
created_at, closed_at, inst_id, underlying, premium_ccy
|
||||
FROM options_trades
|
||||
WHERE status = 'closed'
|
||||
AND (
|
||||
(COALESCE(pnl_settled, 0) = 1 AND realized_pnl_usdt IS NOT NULL)
|
||||
OR (trading_usdt_before IS NULL
|
||||
AND (realized_pnl IS NOT NULL OR realized_pnl_usdt IS NOT NULL))
|
||||
)
|
||||
AND realized_pnl IS NOT NULL
|
||||
"""
|
||||
|
||||
|
||||
@@ -634,7 +642,7 @@ def apply_exchange_pnl_freeze(
|
||||
|
||||
|
||||
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
||||
"""模拟盘:本地交易结算.实盘请用 compute_options_stats_live_exchange."""
|
||||
"""模拟盘数据统计:口径 B(历史币盈亏×卖回价)."""
|
||||
conn = get_db()
|
||||
try:
|
||||
history = load_local_options_stats_history(conn, ex)
|
||||
@@ -642,7 +650,7 @@ def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||
return compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -717,9 +725,11 @@ def load_live_exchange_stats_history(cfg: dict[str, Any], ex: Any) -> list[dict[
|
||||
|
||||
|
||||
def compute_options_stats_live_exchange(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
|
||||
"""实盘:与「期权历史」同一数据源(交易所 positions-history),盈亏以交易所为准."""
|
||||
"""实盘:与「期权历史」同一数据源;口径 B=币盈亏×卖回价(freeze)."""
|
||||
history = load_live_exchange_stats_history(cfg, ex)
|
||||
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||
return compute_options_stats_from_history(
|
||||
history_pnl_to_usdt(history, ex, prefer_frozen_usdt=True)
|
||||
)
|
||||
|
||||
|
||||
def _apply_exchange_pnl_freeze_with_hints(
|
||||
@@ -812,7 +822,7 @@ def _stats_from_option_trade_rows(closed_rows, open_rows, *, ex: Any = None) ->
|
||||
history = _closed_trade_dicts(closed_rows)
|
||||
for row in open_rows or []:
|
||||
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
||||
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||
return compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
|
||||
|
||||
|
||||
def header_options_stats_for_window(
|
||||
@@ -821,32 +831,20 @@ def header_options_stats_for_window(
|
||||
app_tz,
|
||||
ex: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""顶栏总交易/胜率/盈亏比:与期权统计同一结算口径.
|
||||
|
||||
新单(有 trading_usdt_before):仅 pnl_settled=1 的账户差分计入;
|
||||
旧单(无 before):仍按原 realized 兼容.币数量须折 U 后再算盈亏比.
|
||||
"""
|
||||
"""顶栏总交易/胜率/盈亏比:口径 B(历史币盈亏×卖回价)."""
|
||||
from lib.common.history_window_lib import utc_window_to_bj_sql_strings
|
||||
|
||||
init_options_tables(conn)
|
||||
start_bj, end_bj = utc_window_to_bj_sql_strings(
|
||||
list_window["start_utc"], list_window["end_utc"], app_tz
|
||||
)
|
||||
try:
|
||||
freeze_missing_realized_pnl_usdt(conn, ex)
|
||||
except Exception:
|
||||
pass
|
||||
closed_rows = conn.execute(
|
||||
"""
|
||||
SELECT realized_pnl, realized_pnl_usdt, close_index_px,
|
||||
SELECT realized_pnl, close_index_px,
|
||||
created_at, closed_at, inst_id, underlying, premium_ccy
|
||||
FROM options_trades
|
||||
WHERE status = 'closed'
|
||||
AND (
|
||||
(COALESCE(pnl_settled, 0) = 1 AND realized_pnl_usdt IS NOT NULL)
|
||||
OR (trading_usdt_before IS NULL
|
||||
AND (realized_pnl IS NOT NULL OR realized_pnl_usdt IS NOT NULL))
|
||||
)
|
||||
AND realized_pnl IS NOT NULL
|
||||
AND COALESCE(closed_at, created_at) >= ?
|
||||
AND COALESCE(closed_at, created_at) <= ?
|
||||
""",
|
||||
@@ -858,7 +856,7 @@ def header_options_stats_for_window(
|
||||
history = _closed_trade_dicts(closed_rows)
|
||||
for row in open_rows:
|
||||
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
||||
stats = compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||
stats = compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
|
||||
return {
|
||||
"total": int(stats.get("total_closed") or 0),
|
||||
"rate": float(stats.get("win_rate") or 0),
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
}
|
||||
.or-list-loading{opacity:.55;pointer-events:none;transition:opacity .12s ease}
|
||||
.or-trades-table-wrap,.or-reviewed-table-wrap{min-height:9.5rem;overflow-x:auto}
|
||||
.or-reviewed-table{min-width:980px}
|
||||
.or-reviewed-table{min-width:1180px}
|
||||
.or-trades-table{min-width:960px}
|
||||
.or-kpi-row{
|
||||
display:grid;grid-template-columns:repeat(6,minmax(0,1fr));
|
||||
gap:8px;margin-bottom:12px;
|
||||
@@ -237,7 +238,9 @@
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>标的/合约</th>
|
||||
<th>初始权利金</th>
|
||||
<th>盈亏</th>
|
||||
<th>盈利率</th>
|
||||
<th>开仓时间</th>
|
||||
<th>平仓时间</th>
|
||||
<th>持有</th>
|
||||
@@ -245,7 +248,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="or-trades-tbody">
|
||||
<tr><td colspan="7" class="muted">加载中…</td></tr>
|
||||
<tr><td colspan="9" class="muted">加载中…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -280,6 +283,8 @@
|
||||
data-lpignore="true" data-1p-ignore="true" data-form-type="other">
|
||||
<input type="text" id="or-f-pnl" name="or_f_pnl" placeholder="盈亏(U)"
|
||||
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other">
|
||||
<input type="text" id="or-f-initial-prem" name="or_f_initial_prem" placeholder="初始权利金(U)" readonly autocomplete="off">
|
||||
<input type="text" id="or-f-profit-rate" name="or_f_profit_rate" placeholder="盈利率" readonly autocomplete="off">
|
||||
<input type="text" id="or-f-hold" name="or_f_hold" placeholder="持有时长" readonly autocomplete="off">
|
||||
</div>
|
||||
<div class="or-form-grid2">
|
||||
@@ -403,7 +408,9 @@
|
||||
<th>类型</th>
|
||||
<th>标的/合约</th>
|
||||
<th>方向</th>
|
||||
<th>初始权利金</th>
|
||||
<th>盈亏</th>
|
||||
<th>盈利率</th>
|
||||
<th>开仓时间</th>
|
||||
<th>平仓时间</th>
|
||||
<th>持仓时长</th>
|
||||
@@ -414,7 +421,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="or-reviewed-tbody">
|
||||
<tr><td colspan="11" class="muted">加载中…</td></tr>
|
||||
<tr><td colspan="13" class="muted">加载中…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user