feat期权盈亏按交易账户快照结算:一次一仓、持仓禁划转、卖回后冻结。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
|||||||
|
# 规格:期权盈亏按交易账户结算(一次一仓)
|
||||||
|
|
||||||
|
> 状态:已定稿,按本文实现
|
||||||
|
> 适用范围:币本位期权(模拟盘优先落地;实盘同规则)
|
||||||
|
> 目标:复盘盈亏、数据统计盈亏与交易账户真实 USDT 变动一致,可对账、可冻结、可商用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目标与原则
|
||||||
|
|
||||||
|
### 1.1 要解决什么
|
||||||
|
|
||||||
|
此前单笔盈亏混用多套口径(权利金×指数、卖回−买币预算、实时指数重算),导致:
|
||||||
|
|
||||||
|
- 持仓「合计盈亏」与复盘单笔不一致
|
||||||
|
- 数字随行情跳动
|
||||||
|
- 后期账本无法审计
|
||||||
|
|
||||||
|
### 1.2 原则
|
||||||
|
|
||||||
|
| 原则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 真源 | **交易账户 USDT 快照差分** 为本单已实现盈亏真源 |
|
||||||
|
| 一次一仓 | 同一账户同一时间只允许一笔期权持仓 |
|
||||||
|
| 卖回后结算 | 未卖回标的币前,本单不进入已实现 |
|
||||||
|
| 冻结 | 结算完成后永不修改 |
|
||||||
|
| 禁划转 | 持仓期间禁止资金划转(前端灰 + 后端拒) |
|
||||||
|
| 双账清晰 | 顶栏余额 = 钱包;合计/复盘盈亏 = 冻结历史 +(持仓时)浮盈 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 产品规则(用户可见)
|
||||||
|
|
||||||
|
### 2.1 开仓前
|
||||||
|
|
||||||
|
1. **有可卖标的币(ETH/BTC)→ 拒开仓**
|
||||||
|
- 提示:先卖回 USDT(可走自动扫尾/卖回)。
|
||||||
|
- 粉尘:低于现货最小可卖量可忽略;达到可卖量一律视为有币并拒开。
|
||||||
|
2. **交易账户 USDT 快照** `trading_usdt_before`
|
||||||
|
- 取开仓前(买币前)交易账户 USDT。
|
||||||
|
- 开仓前若有过多次划转:以**开仓前最后一次划转完成后**的余额为准(即开仓那一刻的余额)。
|
||||||
|
3. 通过检查后才允许买币并开期权。
|
||||||
|
|
||||||
|
### 2.2 持仓中
|
||||||
|
|
||||||
|
1. **划转按钮灰色不可点**;API 同步拒绝。
|
||||||
|
2. **合计盈亏(动态)** =
|
||||||
|
`Σ已冻结历史盈亏` + `当前持仓浮盈`
|
||||||
|
3. 本单未卖回结算前:不计入「已冻结历史」;浮盈跟盘更新。
|
||||||
|
|
||||||
|
### 2.3 平仓与卖回
|
||||||
|
|
||||||
|
1. 期权平仓后触发**自动卖回**残留标的币。
|
||||||
|
2. **卖回未完成**:本单状态为「待结算」,已实现盈亏不写死。
|
||||||
|
3. **卖回成功后**:再拍交易账户 USDT 快照 `trading_usdt_after`。
|
||||||
|
|
||||||
|
### 2.4 结算与冻结
|
||||||
|
|
||||||
|
```
|
||||||
|
本单已实现盈亏(U) = trading_usdt_after − trading_usdt_before
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 写入本单并标记已冻结。
|
||||||
|
2. **此后不得因划转、指数变动、复盘同步而修改。**
|
||||||
|
3. 期权复盘、数据统计的已实现盈亏均读此冻结值。
|
||||||
|
|
||||||
|
### 2.5 划转与资金
|
||||||
|
|
||||||
|
| 时机 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| 无持仓、无待卖回 | 允许划转;顶栏资金立即刷新 |
|
||||||
|
| 持仓中或待卖回 | 禁止划转 |
|
||||||
|
| 划转后 | 只影响下一笔开仓前的 `usdt_before`;**不重算**已冻结历史单 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 生命周期
|
||||||
|
|
||||||
|
```text
|
||||||
|
[空仓, 无残留币]
|
||||||
|
│
|
||||||
|
├─ 有可卖币? ──是──> 拒开仓「请先卖回」
|
||||||
|
│
|
||||||
|
├─ 快照 trading_usdt_before
|
||||||
|
├─ 买币 → 开期权
|
||||||
|
│
|
||||||
|
[持仓中] 划转禁用;合计 = 冻结历史 + 浮盈
|
||||||
|
│
|
||||||
|
├─ 平仓
|
||||||
|
│
|
||||||
|
[待卖回] 已实现未结算;划转仍禁用
|
||||||
|
│
|
||||||
|
├─ 自动/重试卖回成功
|
||||||
|
├─ 快照 trading_usdt_after
|
||||||
|
├─ realized = after − before → 冻结
|
||||||
|
│
|
||||||
|
[已结算] 复盘/统计只读冻结值
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 数据字段(实现约定)
|
||||||
|
|
||||||
|
在 `options_trades` 增加(或等价持久化):
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| `trading_usdt_before` | 开仓前交易账户 USDT |
|
||||||
|
| `trading_usdt_after` | 卖回完成后交易账户 USDT |
|
||||||
|
| `realized_pnl_usdt` | 冻结已实现 = after − before(结算后写入) |
|
||||||
|
| `pnl_settled` | 0/1,是否已按账户差分结算冻结 |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `budget_usdt` 仍表示本笔买币花费,**不得**当作开仓前账户快照。
|
||||||
|
- 币数量 `realized_pnl` 可保留供期权历史展示(币),与 U 冻结分离。
|
||||||
|
- 统计/复盘的 **U 盈亏** 只认 `pnl_settled=1` 时的 `realized_pnl_usdt`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 模块行为
|
||||||
|
|
||||||
|
### 5.1 开仓 `open_coin_option_buy_full`
|
||||||
|
|
||||||
|
1. 检查一次一仓 / 未完成桥。
|
||||||
|
2. `fetch_trading_coin_available`:可卖则拒开。
|
||||||
|
3. 读取交易账户 USDT → `trading_usdt_before`。
|
||||||
|
4. 买币、下单、写库(带 before,`pnl_settled=0`)。
|
||||||
|
|
||||||
|
### 5.2 划转 `POST /api/options/transfer`
|
||||||
|
|
||||||
|
1. 若存在 `options_trades.status='open'` 或桥状态 ∈ {bought, holding, pending_sell} → **403/业务错误**。
|
||||||
|
2. 前端:持仓或待卖回时划转按钮 `disabled` + 提示「持仓中不可划转」。
|
||||||
|
|
||||||
|
### 5.3 卖回结算
|
||||||
|
|
||||||
|
在 `sell_residual_after_option_flat`(及 retry-sell)桥关闭成功后:
|
||||||
|
|
||||||
|
1. 读取交易账户 USDT → `trading_usdt_after`。
|
||||||
|
2. 对本桥关联且未结算的已平仓位:
|
||||||
|
`realized_pnl_usdt = after - before`,`pnl_settled=1`。
|
||||||
|
3. **停止**用「权利金×指数」或「卖回−预算」覆盖已结算单。
|
||||||
|
|
||||||
|
粉尘关闭桥(无法卖出):
|
||||||
|
- 仍拍 `usdt_after` 并结算(差额含粉尘损失);或
|
||||||
|
- 记 `pnl_settled` 并附 message「粉尘忽略」——实现取:拍售后余额并冻结,保证可对账。
|
||||||
|
|
||||||
|
### 5.4 数据统计
|
||||||
|
|
||||||
|
- `net_realized_pnl` / 已平相关:仅 `pnl_settled=1` 的冻结值之和。
|
||||||
|
- `total_pnl`(持仓页合计)= `net_realized_pnl` + `open_float_pnl`。
|
||||||
|
- 待结算已平单:不计入已实现,也不重复计入浮盈(浮盈仅当前期权持仓)。
|
||||||
|
|
||||||
|
### 5.5 期权复盘
|
||||||
|
|
||||||
|
- 同步本地已平:优先读 `realized_pnl_usdt`(且已结算)。
|
||||||
|
- 未结算:复盘显示「待结算」或暂不导入已实现 U。
|
||||||
|
- 禁止再用实时指数改写已冻结 U。
|
||||||
|
|
||||||
|
### 5.6 期权历史列表
|
||||||
|
|
||||||
|
- **仍按币**展示权利金/币盈亏(不强制改成 U)。
|
||||||
|
- U 口径只用于复盘与数据统计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 对账关系(商用)
|
||||||
|
|
||||||
|
```
|
||||||
|
Σ 已冻结单笔盈亏 + 当前浮盈 ≈ 合计盈亏展示
|
||||||
|
Σ 已冻结单笔盈亏 + 期间净划转 ≈ 交易账户相对「首笔 before」的累计变化
|
||||||
|
```
|
||||||
|
|
||||||
|
差异来源应可归因:粉尘、手续费计入方式、卖回滑点(已含在 after−before 内)。
|
||||||
|
禁止用展示公式回写钱包余额。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 非目标 / 不做
|
||||||
|
|
||||||
|
- 不用买币预算 `budget_usdt` 当单笔盈利。
|
||||||
|
- 不用「净币 × 单一指数」覆盖已结算账户差分。
|
||||||
|
- 持仓中不因行情重算已冻结历史。
|
||||||
|
- 不把期权历史列表强制改为 U 展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 验收清单
|
||||||
|
|
||||||
|
- [ ] 开仓前有可卖 BTC/ETH → 拒开并提示卖回
|
||||||
|
- [ ] 开仓写入 `trading_usdt_before`
|
||||||
|
- [ ] 持仓中划转按钮灰 + API 拒绝
|
||||||
|
- [ ] 平仓未卖回前,统计已实现不含本单
|
||||||
|
- [ ] 卖回后 `realized = after − before` 并 `pnl_settled=1`
|
||||||
|
- [ ] 再次同步复盘/统计,冻结值不变
|
||||||
|
- [ ] 合计盈亏 = 冻结历史 + 浮盈(有持仓时动态)
|
||||||
|
- [ ] 空仓划转后顶栏资金更新,历史冻结单不变
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 修订记录
|
||||||
|
|
||||||
|
| 日期 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-10-09 | 初版定稿:交易账户快照差分 + 一次一仓 + 持仓禁划转 + 卖回后冻结 |
|
||||||
@@ -2598,6 +2598,12 @@
|
|||||||
paintPositions(list);
|
paintPositions(list);
|
||||||
paintSpotSellBanner(d, list);
|
paintSpotSellBanner(d, list);
|
||||||
paintCondOrderLock(d);
|
paintCondOrderLock(d);
|
||||||
|
if (typeof window.OptionsFundsSetTransferBlocked === "function") {
|
||||||
|
window.OptionsFundsSetTransferBlocked(
|
||||||
|
!!(d && d.transfer_blocked),
|
||||||
|
(d && d.transfer_block_msg) || "持仓或待卖回期间不可划转"
|
||||||
|
);
|
||||||
|
}
|
||||||
const fromPos = list.reduce(function (targets, p) {
|
const fromPos = list.reduce(function (targets, p) {
|
||||||
if (!p) return targets;
|
if (!p) return targets;
|
||||||
if (p.target_index != null) {
|
if (p.target_index != null) {
|
||||||
|
|||||||
@@ -353,4 +353,32 @@
|
|||||||
intMsg: "opt-page-int-msg",
|
intMsg: "opt-page-int-msg",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 持仓/待卖回时划转按钮置灰(规格). */
|
||||||
|
window.OptionsFundsSetTransferBlocked = function (blocked, msg) {
|
||||||
|
const idsList = [
|
||||||
|
["opt-page-int-btn", "opt-page-int-all-btn", "opt-page-int-amount", "opt-page-int-msg"],
|
||||||
|
["opt-set-int-btn", "opt-set-int-all-btn", "opt-set-int-amount", "opt-set-int-msg"],
|
||||||
|
];
|
||||||
|
idsList.forEach(function (ids) {
|
||||||
|
const btn = document.getElementById(ids[0]);
|
||||||
|
const allBtn = document.getElementById(ids[1]);
|
||||||
|
const amount = document.getElementById(ids[2]);
|
||||||
|
const msgEl = document.getElementById(ids[3]);
|
||||||
|
[btn, allBtn].forEach(function (el) {
|
||||||
|
if (!el) return;
|
||||||
|
el.disabled = !!blocked;
|
||||||
|
el.style.opacity = blocked ? "0.45" : "";
|
||||||
|
el.title = blocked ? String(msg || "持仓中不可划转") : "";
|
||||||
|
});
|
||||||
|
if (amount) amount.disabled = !!blocked;
|
||||||
|
if (blocked && msgEl && msg) {
|
||||||
|
msgEl.textContent = String(msg);
|
||||||
|
msgEl.classList.add("text-warn");
|
||||||
|
} else if (!blocked && msgEl && /持仓|划转|卖回/.test(String(msgEl.textContent || ""))) {
|
||||||
|
msgEl.textContent = "";
|
||||||
|
msgEl.classList.remove("text-warn");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
"""期权已实现盈亏:交易账户 USDT 快照差分(一次一仓).
|
||||||
|
|
||||||
|
规格见 docs/规格-期权交易账户盈亏结算.md
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(v: Any) -> float | None:
|
||||||
|
if v is None or v == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_trading_usdt(cfg: dict[str, Any] | None, ex: Any) -> float | None:
|
||||||
|
"""读取交易账户 USDT(可用优先)."""
|
||||||
|
fetch = (cfg or {}).get("fetch_options_balances") if isinstance(cfg, dict) else None
|
||||||
|
if not callable(fetch):
|
||||||
|
from lib.exchange.okx_options_lib import fetch_options_balances
|
||||||
|
|
||||||
|
fetch = fetch_options_balances
|
||||||
|
try:
|
||||||
|
bal = fetch(ex, force=True) or {}
|
||||||
|
except TypeError:
|
||||||
|
try:
|
||||||
|
bal = fetch(ex) or {}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
v = _safe_float(bal.get("trading_usdt_avail"))
|
||||||
|
if v is None:
|
||||||
|
v = _safe_float(bal.get("trading_usdt"))
|
||||||
|
return round(float(v), 8) if v is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def sellable_coin_blocks_open_msg(ex: Any, underlying: str) -> 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}),请先卖回 USDT 后再开仓"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_blocked_by_position_msg(conn, ex: Any = None) -> str | None:
|
||||||
|
"""持仓中或待卖回时禁止划转."""
|
||||||
|
from lib.options.options_spot_bridge_lib import has_unfinished_bridge
|
||||||
|
|
||||||
|
try:
|
||||||
|
if has_unfinished_bridge(conn):
|
||||||
|
return "期权持仓或待卖回期间禁止划转,请先平仓并卖回 USDT"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS n FROM options_trades WHERE status='open'"
|
||||||
|
).fetchone()
|
||||||
|
n = int(row["n"] if hasattr(row, "keys") else row[0])
|
||||||
|
if n > 0:
|
||||||
|
return "期权持仓中禁止划转"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if ex is not None:
|
||||||
|
try:
|
||||||
|
from lib.exchange.okx_options_lib import fetch_option_positions
|
||||||
|
|
||||||
|
for p in fetch_option_positions(ex) or []:
|
||||||
|
try:
|
||||||
|
if abs(float(p.get("pos") or 0)) > 1e-12:
|
||||||
|
return "存在未平期权持仓,禁止划转"
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def try_settle_after_sellback(
|
||||||
|
get_db: Any,
|
||||||
|
ex: Any,
|
||||||
|
*,
|
||||||
|
bridge_id: int | None = None,
|
||||||
|
cfg: dict[str, Any] | 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:
|
||||||
|
return 0
|
||||||
|
conn = get_db() if callable(get_db) else get_db
|
||||||
|
close_conn = callable(get_db)
|
||||||
|
try:
|
||||||
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
|
init_options_tables(conn)
|
||||||
|
n = 0
|
||||||
|
if bridge_id is not None:
|
||||||
|
n = settle_closed_trades_from_trading_account(
|
||||||
|
conn, trading_usdt_after=after, bridge_id=int(bridge_id)
|
||||||
|
)
|
||||||
|
if n < 1:
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id FROM options_trades
|
||||||
|
WHERE status='closed' AND COALESCE(pnl_settled,0)=0
|
||||||
|
AND trading_usdt_before IS NOT NULL
|
||||||
|
ORDER BY id DESC LIMIT 1
|
||||||
|
"""
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
n = settle_closed_trades_from_trading_account(
|
||||||
|
conn,
|
||||||
|
trading_usdt_after=after,
|
||||||
|
trade_id=int(row["id"] if hasattr(row, "keys") else row[0]),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return n
|
||||||
|
finally:
|
||||||
|
if close_conn:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def settle_closed_trades_from_trading_account(
|
||||||
|
conn,
|
||||||
|
*,
|
||||||
|
trading_usdt_after: float,
|
||||||
|
bridge_id: int | None = None,
|
||||||
|
trade_id: int | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""卖回后:对本桥/本单未结算已平仓位冻结 after−before.
|
||||||
|
|
||||||
|
已 pnl_settled=1 的行永不改写.
|
||||||
|
"""
|
||||||
|
after = _safe_float(trading_usdt_after)
|
||||||
|
if after is None:
|
||||||
|
return 0
|
||||||
|
after = round(float(after), 8)
|
||||||
|
clauses = ["status='closed'", "COALESCE(pnl_settled, 0)=0", "trading_usdt_before IS NOT NULL"]
|
||||||
|
params: list[Any] = []
|
||||||
|
if trade_id is not None:
|
||||||
|
clauses.append("id=?")
|
||||||
|
params.append(int(trade_id))
|
||||||
|
elif bridge_id is not None:
|
||||||
|
clauses.append("bridge_id=?")
|
||||||
|
params.append(int(bridge_id))
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
rows = conn.execute(
|
||||||
|
f"""
|
||||||
|
SELECT id, trading_usdt_before, realized_pnl_usdt
|
||||||
|
FROM options_trades
|
||||||
|
WHERE {" AND ".join(clauses)}
|
||||||
|
""",
|
||||||
|
tuple(params),
|
||||||
|
).fetchall()
|
||||||
|
updated = 0
|
||||||
|
for row in rows:
|
||||||
|
before = _safe_float(row["trading_usdt_before"] if hasattr(row, "keys") else row[1])
|
||||||
|
if before is None:
|
||||||
|
continue
|
||||||
|
pnl_u = round(float(after) - float(before), 4)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE options_trades
|
||||||
|
SET trading_usdt_after = ?,
|
||||||
|
realized_pnl_usdt = ?,
|
||||||
|
pnl_settled = 1
|
||||||
|
WHERE id = ? AND COALESCE(pnl_settled, 0)=0
|
||||||
|
""",
|
||||||
|
(after, pnl_u, int(row["id"] if hasattr(row, "keys") else row[0])),
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
return updated
|
||||||
@@ -379,7 +379,7 @@ def close_option_by_bid1(
|
|||||||
"""
|
"""
|
||||||
SELECT id, premium_paid, open_fee, premium_ccy, underlying,
|
SELECT id, premium_paid, open_fee, premium_ccy, underlying,
|
||||||
exchange_ord_id, budget_usdt, open_index_px, fee_ccy,
|
exchange_ord_id, budget_usdt, open_index_px, fee_ccy,
|
||||||
eth_amount, open_quote
|
eth_amount, open_quote, trading_usdt_before
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE inst_id = ? AND status = 'open'
|
WHERE inst_id = ? AND status = 'open'
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
@@ -510,7 +510,9 @@ def close_option_by_bid1(
|
|||||||
fee_ccy=str(row["fee_ccy"] or row["premium_ccy"] or row_ccy or ""),
|
fee_ccy=str(row["fee_ccy"] or row["premium_ccy"] or row_ccy or ""),
|
||||||
)
|
)
|
||||||
pnl = settled.get("realized_pnl")
|
pnl = settled.get("realized_pnl")
|
||||||
pnl_usdt = settled.get("realized_pnl_usdt")
|
# 有交易账户 before 快照:U 盈亏等卖回后结算,平仓不写 realized_pnl_usdt
|
||||||
|
has_acct_snap = _safe_float(row["trading_usdt_before"]) is not None
|
||||||
|
pnl_usdt = None if has_acct_snap else settled.get("realized_pnl_usdt")
|
||||||
note_sql = ""
|
note_sql = ""
|
||||||
|
|
||||||
# 占位: close_quote/recv/pnl/ord/closed_at/open_fee/close_fee/fee_ccy/close_idx/open_idx/pnl_u/[note]/id
|
# 占位: close_quote/recv/pnl/ord/closed_at/open_fee/close_fee/fee_ccy/close_idx/open_idx/pnl_u/[note]/id
|
||||||
|
|||||||
@@ -216,6 +216,16 @@ def open_coin_option_buy_full(
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# 开仓前有可卖标的币 → 拒开(规格:先卖回)
|
||||||
|
from lib.options.options_account_pnl_lib import (
|
||||||
|
fetch_trading_usdt,
|
||||||
|
sellable_coin_blocks_open_msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
coin_block = sellable_coin_blocks_open_msg(ex, underlying)
|
||||||
|
if coin_block:
|
||||||
|
return {"ok": False, "msg": coin_block, "can_open": False}
|
||||||
|
trading_usdt_before = fetch_trading_usdt(cfg, ex)
|
||||||
# 1) 仅买「权利金×现货缓冲」所需 USDT,不全额兑换预算
|
# 1) 仅买「权利金×现货缓冲」所需 USDT,不全额兑换预算
|
||||||
coin_before = _spot_bridge.fetch_trading_coin_available(ex, underlying) or 0.0
|
coin_before = _spot_bridge.fetch_trading_coin_available(ex, underlying) or 0.0
|
||||||
buy = _spot_bridge.spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=buy_usdt)
|
buy = _spot_bridge.spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=buy_usdt)
|
||||||
@@ -514,6 +524,7 @@ def open_coin_option_buy_full(
|
|||||||
premium_ccy=premium_ccy,
|
premium_ccy=premium_ccy,
|
||||||
open_fee=open_fee,
|
open_fee=open_fee,
|
||||||
open_index_px=idx_open,
|
open_index_px=idx_open,
|
||||||
|
trading_usdt_before=trading_usdt_before,
|
||||||
profit_exit_enabled=profit_exit_enabled,
|
profit_exit_enabled=profit_exit_enabled,
|
||||||
profit_exit_mult=profit_exit_mult,
|
profit_exit_mult=profit_exit_mult,
|
||||||
)
|
)
|
||||||
@@ -594,15 +605,20 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
|||||||
open_index_px = None
|
open_index_px = None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
open_index_px = None
|
open_index_px = None
|
||||||
|
tusdt_before = kwargs.get("trading_usdt_before")
|
||||||
|
try:
|
||||||
|
tusdt_before = float(tusdt_before) if tusdt_before is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
tusdt_before = None
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO options_trades(
|
INSERT INTO options_trades(
|
||||||
inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount,
|
inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount,
|
||||||
open_quote, premium_paid, status, signal_note, exchange_ord_id,
|
open_quote, premium_paid, status, signal_note, exchange_ord_id,
|
||||||
margin_mode, premium_ccy, bridge_id, budget_usdt,
|
margin_mode, premium_ccy, bridge_id, budget_usdt,
|
||||||
open_fee, fee_ccy, open_index_px,
|
open_fee, fee_ccy, open_index_px, trading_usdt_before, pnl_settled,
|
||||||
profit_exit_enabled, profit_exit_mult, profit_exit_state, created_at
|
profit_exit_enabled, profit_exit_mult, profit_exit_state, created_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
kwargs["inst_id"],
|
kwargs["inst_id"],
|
||||||
@@ -622,6 +638,7 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
|||||||
open_fee,
|
open_fee,
|
||||||
fee_ccy,
|
fee_ccy,
|
||||||
open_index_px,
|
open_index_px,
|
||||||
|
tusdt_before,
|
||||||
pe,
|
pe,
|
||||||
pe_mult,
|
pe_mult,
|
||||||
pe_state,
|
pe_state,
|
||||||
@@ -668,18 +685,18 @@ def maybe_sell_spot_after_close(
|
|||||||
underlying=underlying,
|
underlying=underlying,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
)
|
)
|
||||||
# 模拟盘:卖回后按桥上真实 USDT 净额回写已实现盈亏
|
# 卖回成功:交易账户 after − 开仓 before → 冻结本单盈亏
|
||||||
try:
|
try:
|
||||||
from lib.sim.mode_lib import is_sim_mode
|
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||||
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
|
|
||||||
|
|
||||||
if is_sim_mode(cfg["get_db"]):
|
if (result or {}).get("ok"):
|
||||||
conn = cfg["get_db"]()
|
bid = (result or {}).get("bridge_id")
|
||||||
try:
|
try_settle_after_sellback(
|
||||||
sync_sim_cash_realized_pnl_usdt(conn, ex)
|
cfg["get_db"],
|
||||||
conn.commit()
|
ex,
|
||||||
finally:
|
bridge_id=int(bid) if bid is not None else None,
|
||||||
conn.close()
|
cfg=cfg,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return result
|
return result
|
||||||
@@ -749,6 +766,19 @@ def sweep_spot_after_options_flat(
|
|||||||
underlying=uly,
|
underlying=uly,
|
||||||
inst_id=None,
|
inst_id=None,
|
||||||
)
|
)
|
||||||
|
if (out or {}).get("ok"):
|
||||||
|
try:
|
||||||
|
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||||
|
|
||||||
|
bid = (out or {}).get("bridge_id")
|
||||||
|
try_settle_after_sellback(
|
||||||
|
cfg["get_db"],
|
||||||
|
exchange,
|
||||||
|
bridge_id=int(bid) if bid is not None else None,
|
||||||
|
cfg=cfg,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
sold.append({"underlying": uly, **(out or {})})
|
sold.append({"underlying": uly, **(out or {})})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sold.append({"underlying": uly, "ok": False, "msg": str(e)})
|
sold.append({"underlying": uly, "ok": False, "msg": str(e)})
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
|||||||
"ALTER TABLE options_trades ADD COLUMN close_index_px REAL",
|
"ALTER TABLE options_trades ADD COLUMN close_index_px REAL",
|
||||||
"ALTER TABLE options_trades ADD COLUMN open_index_px REAL",
|
"ALTER TABLE options_trades ADD COLUMN open_index_px REAL",
|
||||||
"ALTER TABLE options_trades ADD COLUMN realized_pnl_usdt REAL",
|
"ALTER TABLE options_trades ADD COLUMN realized_pnl_usdt REAL",
|
||||||
|
# 交易账户快照盈亏(一次一仓):before→after 差分冻结
|
||||||
|
"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",
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
conn.execute(ddl)
|
conn.execute(ddl)
|
||||||
|
|||||||
@@ -1026,6 +1026,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
conn_c.close()
|
conn_c.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
transfer_blocked = False
|
||||||
|
transfer_block_msg = ""
|
||||||
|
try:
|
||||||
|
from lib.options.options_account_pnl_lib import transfer_blocked_by_position_msg
|
||||||
|
|
||||||
|
conn_tb = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
tmsg = transfer_blocked_by_position_msg(conn_tb, ex)
|
||||||
|
if tmsg:
|
||||||
|
transfer_blocked = True
|
||||||
|
transfer_block_msg = tmsg
|
||||||
|
finally:
|
||||||
|
conn_tb.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -1034,6 +1049,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
"cond_order_locked": cond_locked,
|
"cond_order_locked": cond_locked,
|
||||||
"cond_order_msg": cond_msg,
|
"cond_order_msg": cond_msg,
|
||||||
"cond_order": cond_order,
|
"cond_order": cond_order,
|
||||||
|
"transfer_blocked": transfer_blocked,
|
||||||
|
"transfer_block_msg": transfer_block_msg,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1196,6 +1213,18 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
)
|
)
|
||||||
if out.get("ok"):
|
if out.get("ok"):
|
||||||
|
try:
|
||||||
|
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||||
|
|
||||||
|
bid = out.get("bridge_id")
|
||||||
|
try_settle_after_sellback(
|
||||||
|
cfg["get_db"],
|
||||||
|
ex,
|
||||||
|
bridge_id=int(bid) if bid is not None else None,
|
||||||
|
cfg=cfg,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
_mark_balances_stale(cfg)
|
_mark_balances_stale(cfg)
|
||||||
return jsonify(out)
|
return jsonify(out)
|
||||||
|
|
||||||
@@ -1215,6 +1244,16 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
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})
|
||||||
|
# 持仓/待卖回禁止划转(规格)
|
||||||
|
conn_blk = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
from lib.options.options_account_pnl_lib import transfer_blocked_by_position_msg
|
||||||
|
|
||||||
|
block = transfer_blocked_by_position_msg(conn_blk, ex)
|
||||||
|
if block:
|
||||||
|
return jsonify({"ok": False, "msg": block, "transfer_blocked": True})
|
||||||
|
finally:
|
||||||
|
conn_blk.close()
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
ccy = (data.get("ccy") or "USDC").upper()
|
ccy = (data.get("ccy") or "USDC").upper()
|
||||||
from_acct = (data.get("from") or "funding").strip()
|
from_acct = (data.get("from") or "funding").strip()
|
||||||
|
|||||||
@@ -316,7 +316,8 @@ def sync_options_from_local_trades(
|
|||||||
SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets,
|
SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets,
|
||||||
open_quote, close_quote, premium_paid, premium_received, realized_pnl,
|
open_quote, close_quote, premium_paid, premium_received, realized_pnl,
|
||||||
realized_pnl_usdt, close_index_px, open_index_px, budget_usdt,
|
realized_pnl_usdt, close_index_px, open_index_px, budget_usdt,
|
||||||
premium_ccy, created_at, closed_at, signal_note, status
|
premium_ccy, created_at, closed_at, signal_note, status,
|
||||||
|
trading_usdt_before, pnl_settled
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
@@ -409,6 +410,12 @@ def sync_options_from_local_trades(
|
|||||||
if not ccy:
|
if not ccy:
|
||||||
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly)
|
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly)
|
||||||
frozen_usdt = _safe_float(r["realized_pnl_usdt"])
|
frozen_usdt = _safe_float(r["realized_pnl_usdt"])
|
||||||
|
try:
|
||||||
|
settled = int(r["pnl_settled"] or 0) == 1
|
||||||
|
except Exception:
|
||||||
|
settled = False
|
||||||
|
# 账户快照路径:未结算不写 U,避免指数假数进复盘
|
||||||
|
acct_pending = _safe_float(r["trading_usdt_before"]) is not None and not settled
|
||||||
close_idx = _safe_float(r["close_index_px"])
|
close_idx = _safe_float(r["close_index_px"])
|
||||||
open_idx = _safe_float(r["open_index_px"])
|
open_idx = _safe_float(r["open_index_px"])
|
||||||
# 折算指数:平仓冻结 > 开仓 > 实时(仅无冻结时兜底)
|
# 折算指数:平仓冻结 > 开仓 > 实时(仅无冻结时兜底)
|
||||||
@@ -426,7 +433,9 @@ def sync_options_from_local_trades(
|
|||||||
if paid0 is not None and recv0 is not None:
|
if paid0 is not None and recv0 is not None:
|
||||||
pnl = recv0 - paid0
|
pnl = recv0 - paid0
|
||||||
prem = _safe_float(r["premium_paid"])
|
prem = _safe_float(r["premium_paid"])
|
||||||
if frozen_usdt is not None:
|
if acct_pending:
|
||||||
|
pnl_u = None
|
||||||
|
elif frozen_usdt is not None and (settled or _safe_float(r["trading_usdt_before"]) is None):
|
||||||
pnl_u = round(float(frozen_usdt), 4)
|
pnl_u = round(float(frozen_usdt), 4)
|
||||||
else:
|
else:
|
||||||
pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx)
|
pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx)
|
||||||
|
|||||||
@@ -303,7 +303,11 @@ _CLOSED_STATS_SELECT = """
|
|||||||
created_at, closed_at, inst_id, underlying, premium_ccy
|
created_at, closed_at, inst_id, underlying, premium_ccy
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
AND (realized_pnl IS NOT NULL OR realized_pnl_usdt IS NOT NULL)
|
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))
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -322,6 +326,7 @@ def freeze_missing_realized_pnl_usdt(conn, ex: Any = None) -> int:
|
|||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
AND realized_pnl_usdt IS NULL
|
AND realized_pnl_usdt IS NULL
|
||||||
AND realized_pnl IS NOT NULL
|
AND realized_pnl IS NOT NULL
|
||||||
|
AND trading_usdt_before IS NULL
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 300
|
LIMIT 300
|
||||||
"""
|
"""
|
||||||
@@ -512,96 +517,15 @@ def _sim_net_usdt_converts_for_trade(
|
|||||||
|
|
||||||
|
|
||||||
def sync_sim_cash_realized_pnl_usdt(conn, ex: Any = None) -> int:
|
def sync_sim_cash_realized_pnl_usdt(conn, ex: Any = None) -> int:
|
||||||
"""模拟盘:收回×卖回价 − 初始U权利金 − 手续费U.
|
"""兼容入口:已按交易账户结算(pnl_settled=1)的永不改写.
|
||||||
|
|
||||||
盈利=(权利金币+盈利币)×卖回价−开仓U权利金−费;
|
有 trading_usdt_before 的待结算单留给卖回结算;
|
||||||
亏损=剩余权利金U−费−开仓U(同一式).不用买币预算差额.
|
仅无账户快照的旧单才回退权利金×指数冻结.
|
||||||
"""
|
"""
|
||||||
|
del ex
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
from lib.options.options_pnl_settle_lib import resolve_close_index_px, settle_closed_pnl
|
# 账户快照路径由 sell_residual / maybe_sell 结算,此处不覆盖
|
||||||
|
return 0
|
||||||
rows = conn.execute(
|
|
||||||
"""
|
|
||||||
SELECT id, inst_id, underlying, premium_ccy, premium_paid, premium_received,
|
|
||||||
open_fee, close_fee, realized_pnl, realized_pnl_usdt,
|
|
||||||
close_index_px, open_index_px, close_quote, eth_amount
|
|
||||||
FROM options_trades
|
|
||||||
WHERE status = 'closed'
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT 300
|
|
||||||
"""
|
|
||||||
).fetchall()
|
|
||||||
updated = 0
|
|
||||||
for row in rows:
|
|
||||||
ccy = str(_row_get(row, "premium_ccy") or "").strip().upper()
|
|
||||||
underly = str(_row_get(row, "underlying") or "").strip().upper()
|
|
||||||
if ccy not in ("ETH", "BTC") and underly not in ("ETH", "BTC"):
|
|
||||||
continue
|
|
||||||
underly = underly or ccy
|
|
||||||
paid = _safe_float(_row_get(row, "premium_paid"))
|
|
||||||
recv = _safe_float(_row_get(row, "premium_received"))
|
|
||||||
if recv is None:
|
|
||||||
cq = _safe_float(_row_get(row, "close_quote"))
|
|
||||||
eth = _safe_float(_row_get(row, "eth_amount"))
|
|
||||||
if cq is not None and eth is not None and eth > 0:
|
|
||||||
try:
|
|
||||||
from lib.options.options_pricing_lib import total_premium
|
|
||||||
|
|
||||||
recv = float(total_premium(cq, eth))
|
|
||||||
except Exception:
|
|
||||||
recv = None
|
|
||||||
sell_px = _safe_float(_row_get(row, "close_index_px"))
|
|
||||||
open_idx = _safe_float(_row_get(row, "open_index_px"))
|
|
||||||
if sell_px is None or sell_px <= 0:
|
|
||||||
sell_px = resolve_close_index_px(
|
|
||||||
ex,
|
|
||||||
inst_id=str(_row_get(row, "inst_id") or ""),
|
|
||||||
underlying=underly,
|
|
||||||
hint=open_idx,
|
|
||||||
)
|
|
||||||
if open_idx is None or open_idx <= 0:
|
|
||||||
# 无开仓指数时用卖回价兜底(等价旧净币×价)
|
|
||||||
open_idx = sell_px
|
|
||||||
if paid is None or recv is None:
|
|
||||||
net = _safe_float(_row_get(row, "realized_pnl"))
|
|
||||||
if net is None or sell_px is None or sell_px <= 0:
|
|
||||||
continue
|
|
||||||
usdt = round(float(net) * float(sell_px), 4)
|
|
||||||
close_idx = float(sell_px)
|
|
||||||
new_pnl = float(net)
|
|
||||||
open_write = open_idx
|
|
||||||
else:
|
|
||||||
settled = settle_closed_pnl(
|
|
||||||
premium_paid=paid,
|
|
||||||
premium_received=recv,
|
|
||||||
open_fee=_row_get(row, "open_fee"),
|
|
||||||
close_fee=_row_get(row, "close_fee"),
|
|
||||||
premium_ccy=ccy or underly or "ETH",
|
|
||||||
index_px=sell_px,
|
|
||||||
open_index_px=open_idx,
|
|
||||||
)
|
|
||||||
usdt = settled.get("realized_pnl_usdt")
|
|
||||||
close_idx = settled.get("close_index_px") or sell_px
|
|
||||||
new_pnl = settled.get("realized_pnl")
|
|
||||||
open_write = settled.get("open_index_px") or open_idx
|
|
||||||
if usdt is None:
|
|
||||||
continue
|
|
||||||
old = _safe_float(_row_get(row, "realized_pnl_usdt"))
|
|
||||||
if old is not None and abs(float(old) - float(usdt)) < 1e-6:
|
|
||||||
continue
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
UPDATE options_trades
|
|
||||||
SET realized_pnl_usdt = ?,
|
|
||||||
close_index_px = COALESCE(close_index_px, ?),
|
|
||||||
open_index_px = COALESCE(open_index_px, ?),
|
|
||||||
realized_pnl = COALESCE(?, realized_pnl)
|
|
||||||
WHERE id = ?
|
|
||||||
""",
|
|
||||||
(float(usdt), close_idx, open_write, new_pnl, int(row["id"])),
|
|
||||||
)
|
|
||||||
updated += 1
|
|
||||||
return updated
|
|
||||||
|
|
||||||
|
|
||||||
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
|
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""交易账户快照盈亏结算."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class AccountPnlSettleTests(unittest.TestCase):
|
||||||
|
def test_settle_after_minus_before(self):
|
||||||
|
from lib.options.options_account_pnl_lib import (
|
||||||
|
settle_closed_trades_from_trading_account,
|
||||||
|
)
|
||||||
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
init_options_tables(conn)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO options_trades(
|
||||||
|
inst_id, underlying, opt_type, sheets, eth_amount,
|
||||||
|
premium_paid, status, bridge_id, trading_usdt_before, pnl_settled,
|
||||||
|
created_at, closed_at
|
||||||
|
) VALUES (
|
||||||
|
'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01,
|
||||||
|
0.01, 'closed', 7, 1000.0, 0,
|
||||||
|
'2026-10-05 05:11:30', '2026-10-07 10:03:53'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
n = settle_closed_trades_from_trading_account(
|
||||||
|
conn, trading_usdt_after=1947.05, bridge_id=7
|
||||||
|
)
|
||||||
|
self.assertEqual(n, 1)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT trading_usdt_after, realized_pnl_usdt, pnl_settled FROM options_trades WHERE id=1"
|
||||||
|
).fetchone()
|
||||||
|
self.assertAlmostEqual(float(row["trading_usdt_after"]), 1947.05, places=2)
|
||||||
|
self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 947.05, places=2)
|
||||||
|
self.assertEqual(int(row["pnl_settled"]), 1)
|
||||||
|
|
||||||
|
# 已冻结不再改
|
||||||
|
n2 = settle_closed_trades_from_trading_account(
|
||||||
|
conn, trading_usdt_after=9999.0, bridge_id=7
|
||||||
|
)
|
||||||
|
self.assertEqual(n2, 0)
|
||||||
|
row2 = conn.execute(
|
||||||
|
"SELECT realized_pnl_usdt FROM options_trades WHERE id=1"
|
||||||
|
).fetchone()
|
||||||
|
self.assertAlmostEqual(float(row2["realized_pnl_usdt"]), 947.05, places=2)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_stats_only_count_settled_account_trades(self):
|
||||||
|
from lib.options.options_db import init_options_tables
|
||||||
|
from lib.options.options_stats_lib import (
|
||||||
|
compute_options_stats_from_history,
|
||||||
|
history_pnl_to_usdt,
|
||||||
|
load_local_options_stats_history,
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
init_options_tables(conn)
|
||||||
|
# 待结算:不进统计
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO options_trades(
|
||||||
|
inst_id, underlying, opt_type, sheets, eth_amount,
|
||||||
|
premium_paid, premium_received, realized_pnl, premium_ccy,
|
||||||
|
status, trading_usdt_before, pnl_settled, created_at, closed_at
|
||||||
|
) VALUES (
|
||||||
|
'ETH-USD-261009-2500-P', 'ETH', 'P', 1, 0.01,
|
||||||
|
0.01, 0.02, 0.01, 'ETH',
|
||||||
|
'closed', 500.0, 0, '2026-10-06 10:00:00', '2026-10-08 14:00:00'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# 已结算
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO options_trades(
|
||||||
|
inst_id, underlying, opt_type, sheets, eth_amount,
|
||||||
|
premium_paid, premium_received, realized_pnl, premium_ccy,
|
||||||
|
status, trading_usdt_before, trading_usdt_after,
|
||||||
|
realized_pnl_usdt, pnl_settled, created_at, closed_at
|
||||||
|
) VALUES (
|
||||||
|
'ETH-USD-261009-2500-P', 'ETH', 'P', 1, 0.01,
|
||||||
|
0.01, 0.02, 0.01, 'ETH',
|
||||||
|
'closed', 1000.0, 1952.96,
|
||||||
|
952.96, 1, '2026-10-01 10:00:00', '2026-10-02 14:00:00'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
hist = load_local_options_stats_history(conn, None)
|
||||||
|
stats = compute_options_stats_from_history(history_pnl_to_usdt(hist, None))
|
||||||
|
self.assertEqual(int(stats["total_closed"]), 1)
|
||||||
|
self.assertAlmostEqual(float(stats["net_realized_pnl"]), 952.96, places=2)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -104,83 +104,38 @@ class SettleClosedPnlTests(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class SimPremiumPnlStatsTests(unittest.TestCase):
|
class SimPremiumPnlStatsTests(unittest.TestCase):
|
||||||
def test_sim_uses_premium_net_not_bridge_cash(self):
|
def test_sync_sim_no_longer_overwrites_account_settled(self):
|
||||||
"""卖回−预算会把权利金本金算进盈利;必须用(收回−已付)×平仓指数."""
|
"""账户结算后 sync 兼容入口不得改写."""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
from lib.options.options_spot_bridge_lib import ensure_bridge_table
|
|
||||||
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
|
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
|
||||||
|
|
||||||
conn = sqlite3.connect(":memory:")
|
conn = sqlite3.connect(":memory:")
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
ensure_bridge_table(conn)
|
|
||||||
# 桥上现金差额 952.96,但权利金净盈亏仅 0.01 ETH × 2500 = 25U
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO options_spot_bridge(
|
|
||||||
id, underlying, status, budget_usdt, usdt_recovered
|
|
||||||
) VALUES (1, 'ETH', 'closed', 1000.0, 1952.96)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO options_trades(
|
INSERT INTO options_trades(
|
||||||
inst_id, underlying, opt_type, sheets, eth_amount,
|
inst_id, underlying, opt_type, sheets, eth_amount,
|
||||||
premium_paid, premium_received, realized_pnl, premium_ccy,
|
premium_paid, premium_received, realized_pnl, premium_ccy,
|
||||||
status, bridge_id, budget_usdt, realized_pnl_usdt, close_index_px,
|
status, trading_usdt_before, trading_usdt_after,
|
||||||
created_at, closed_at
|
realized_pnl_usdt, pnl_settled, created_at, closed_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
'ETH-USD-261009-2500-P', 'ETH', 'P', 1, 0.01,
|
'ETH-USD-261009-2500-P', 'ETH', 'P', 1, 0.01,
|
||||||
0.01, 0.02, 0.01, 'ETH',
|
0.01, 0.02, 0.01, 'ETH',
|
||||||
'closed', 1, 1000.0, 1947.05, 2500.0,
|
'closed', 1000.0, 1952.96,
|
||||||
'2026-10-06 10:00:00', '2026-10-08 14:00:00'
|
952.96, 1, '2026-10-06 10:00:00', '2026-10-08 14:00:00'
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
n = sync_sim_cash_realized_pnl_usdt(conn, None)
|
n = sync_sim_cash_realized_pnl_usdt(conn, None)
|
||||||
self.assertEqual(n, 1)
|
self.assertEqual(n, 0)
|
||||||
row = conn.execute(
|
|
||||||
"SELECT realized_pnl_usdt, realized_pnl FROM options_trades WHERE id=1"
|
|
||||||
).fetchone()
|
|
||||||
# (0.02-0.01)*2500 = 25, 绝不是 1952.96-1000
|
|
||||||
self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 25.0, places=2)
|
|
||||||
self.assertAlmostEqual(float(row["realized_pnl"]), 0.01, places=8)
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def test_sim_deducts_fees_from_premium_pnl(self):
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
from lib.options.options_db import init_options_tables
|
|
||||||
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
|
|
||||||
|
|
||||||
conn = sqlite3.connect(":memory:")
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
init_options_tables(conn)
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO options_trades(
|
|
||||||
inst_id, underlying, opt_type, sheets, eth_amount,
|
|
||||||
premium_paid, premium_received, realized_pnl, premium_ccy,
|
|
||||||
open_fee, close_fee, status, realized_pnl_usdt, close_index_px,
|
|
||||||
created_at, closed_at
|
|
||||||
) VALUES (
|
|
||||||
'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01,
|
|
||||||
0.01, 0.02, 0.01, 'BTC',
|
|
||||||
0.0001, 0.0001, 'closed', 1947.05, 80000.0,
|
|
||||||
'2026-10-05 05:11:30', '2026-10-07 10:03:53'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
sync_sim_cash_realized_pnl_usdt(conn, None)
|
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT realized_pnl_usdt FROM options_trades WHERE id=1"
|
"SELECT realized_pnl_usdt FROM options_trades WHERE id=1"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
# (0.02-0.01-0.0002)*80000 = 784
|
self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 952.96, places=2)
|
||||||
self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 784.0, places=2)
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user