fix期权复盘与数据统计双口径盈亏,并修复卖回封顶导致盈利单冻成亏损。
平仓卖光可卖币后再按账户差分结算复盘;数据统计改为历史币盈亏×卖回价;复盘展示初始权利金与盈利率,并附历史修复脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+400
-401
@@ -1,401 +1,400 @@
|
|||||||
# 系统分析:期权主链条
|
# 系统分析:期权主链条
|
||||||
|
|
||||||
> 范围:币本位期权(`OKX_OPTIONS_MARGIN_MODE=coin`)
|
> 范围:币本位期权(`OKX_OPTIONS_MARGIN_MODE=coin`)
|
||||||
> 配套规格:[规格-期权交易账户盈亏结算.md](./规格-期权交易账户盈亏结算.md)、[实盘说明.md](./实盘说明.md)
|
> 配套规格:[规格-期权交易账户盈亏结算.md](./规格-期权交易账户盈亏结算.md)、[实盘说明.md](./实盘说明.md)
|
||||||
> 目的:从架构到开仓 / 平仓 / 展示 / 复盘 / 统计 / 对账 / 条件单,说明数据流、关键模块与边界
|
> 目的:从架构到开仓 / 平仓 / 展示 / 复盘 / 统计 / 对账 / 条件单,说明数据流、关键模块与边界
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. 总览
|
## 0. 总览
|
||||||
|
|
||||||
### 0.1 三条控制面
|
### 0.1 三条控制面
|
||||||
|
|
||||||
| 控制面 | 入口 | 职责 |
|
| 控制面 | 入口 | 职责 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| 面板 API | `/options` → `/api/options/*` | 人工开平仓、链/报价、持仓、划转、统计 |
|
| 面板 API | `/options` → `/api/options/*` | 人工开平仓、链/报价、持仓、划转、统计 |
|
||||||
| 监控线程 | `options_monitor_lib`(安装时拉起) | 目标位平仓、止盈平仓、条件单、持仓同步、卖回扫尾 |
|
| 监控线程 | `options_monitor_lib`(安装时拉起) | 目标位平仓、止盈平仓、条件单、持仓同步、卖回扫尾 |
|
||||||
| 中控 Hub | `/api/hub/options/*` | 快照、统计、远程平仓、条件单 CRUD |
|
| 中控 Hub | `/api/hub/options/*` | 快照、统计、远程平仓、条件单 CRUD |
|
||||||
|
|
||||||
### 0.2 资金主链(商业真源)
|
### 0.2 资金主链(双口径)
|
||||||
|
|
||||||
```text
|
```text
|
||||||
USDT(交易户)
|
USDT(交易户)
|
||||||
│ 开仓前快照 trading_usdt_before
|
│ 开仓前快照 trading_usdt_before;initial_premium = 开仓指数×权利金币量
|
||||||
▼
|
▼
|
||||||
买 ETH/BTC(现货桥)
|
买 ETH/BTC(现货桥)
|
||||||
▼
|
▼
|
||||||
买期权(IOC @ 卖一)
|
买期权(IOC @ 卖一)
|
||||||
▼
|
▼
|
||||||
持仓中(禁划转;浮盈跟盘)
|
持仓中(禁划转;浮盈跟盘)
|
||||||
▼
|
▼
|
||||||
卖期权(买一 / 深度)
|
卖期权(可多次部分平;以全部平完为准)
|
||||||
▼
|
▼
|
||||||
卖回残留币 → 快照 trading_usdt_after
|
卖光可卖标的币(禁止按开仓权利金封顶)→ 快照 trading_usdt_after
|
||||||
▼
|
▼
|
||||||
realized_pnl_usdt = after − before,pnl_settled=1(冻结,永不改)
|
口径 A(复盘): realized_pnl_usdt = after − before,pnl_settled=1
|
||||||
▼
|
口径 B(数据统计): 历史币盈亏 × 平仓卖回价(与 A 独立)
|
||||||
复盘 / 统计只读冻结值
|
```
|
||||||
```
|
|
||||||
|
### 0.3 启动装配
|
||||||
### 0.3 启动装配
|
|
||||||
|
```text
|
||||||
```text
|
app.py
|
||||||
app.py
|
→ install_options_trading # options_register
|
||||||
→ install_options_trading # options_register
|
→ install_options_review # options_review_register
|
||||||
→ install_options_review # options_review_register
|
→ register_hub_bridge_routes # hub_bridge_register
|
||||||
→ register_hub_bridge_routes # hub_bridge_register
|
→ 监控守护线程 # options_monitor_lib
|
||||||
→ 监控守护线程 # options_monitor_lib
|
```
|
||||||
```
|
|
||||||
|
核心配置包:`_build_cfg`(交易所、DB、预算、报价/下单回调)。
|
||||||
核心配置包:`_build_cfg`(交易所、DB、预算、报价/下单回调)。
|
UI:`lib/options/templates/options_panel.html` + `lib/common/static/options_panel.js`。
|
||||||
UI:`lib/options/templates/options_panel.html` + `lib/common/static/options_panel.js`。
|
|
||||||
|
### 0.4 关键表(SQLite)
|
||||||
### 0.4 关键表(SQLite)
|
|
||||||
|
| 表 | 用途 |
|
||||||
| 表 | 用途 |
|
|----|------|
|
||||||
|----|------|
|
| `options_trades` | 本地开平流水;含 `trading_usdt_before/after`、`realized_pnl_usdt`、`pnl_settled` |
|
||||||
| `options_trades` | 本地开平流水;含 `trading_usdt_before/after`、`realized_pnl_usdt`、`pnl_settled` |
|
| `options_spot_bridge` | 买币/卖回桥状态 |
|
||||||
| `options_spot_bridge` | 买币/卖回桥状态 |
|
| `options_target_monitors` | 目标位监控 |
|
||||||
| `options_target_monitors` | 目标位监控 |
|
| `options_cond_orders` | 条件单 |
|
||||||
| `options_cond_orders` | 条件单 |
|
| `options_exchange_pnl_freeze` | 实盘历史盈亏冻结辅助 |
|
||||||
| `options_exchange_pnl_freeze` | 实盘历史盈亏冻结辅助 |
|
| `options_review_*` | 复盘成交 / 笔记 / 隐藏 / 图 |
|
||||||
| `options_review_*` | 复盘成交 / 笔记 / 隐藏 / 图 |
|
| `options_transfer_log` 等 | 划转等资金侧日志 |
|
||||||
| `options_transfer_log` 等 | 划转等资金侧日志 |
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 1. 主链条(架构)
|
||||||
## 1. 主链条(架构)
|
|
||||||
|
### 1.1 模块分层
|
||||||
### 1.1 模块分层
|
|
||||||
|
| 层 | 代表文件 | 说明 |
|
||||||
| 层 | 代表文件 | 说明 |
|
|----|----------|------|
|
||||||
|----|----------|------|
|
| 路由装配 | `lib/options/options_register.py` | HTTP、尺寸归一、同步持仓 |
|
||||||
| 路由装配 | `lib/options/options_register.py` | HTTP、尺寸归一、同步持仓 |
|
| 开仓编排 | `options_coin_open_lib.py` | 预算→买币→开期权→写库 |
|
||||||
| 开仓编排 | `options_coin_open_lib.py` | 预算→买币→开期权→写库 |
|
| 平仓执行 | `options_close_exec_lib.py` | 买一/深度卖出 |
|
||||||
| 平仓执行 | `options_close_exec_lib.py` | 买一/深度卖出 |
|
| 账户结算 | `options_account_pnl_lib.py` | USDT 快照、卖回后冻结 |
|
||||||
| 账户结算 | `options_account_pnl_lib.py` | USDT 快照、卖回后冻结 |
|
| 公式结算 | `options_pnl_settle_lib.py` | 权利金侧辅助结算(有账户快照时 U 以账户为准) |
|
||||||
| 公式结算 | `options_pnl_settle_lib.py` | 权利金侧辅助结算(有账户快照时 U 以账户为准) |
|
| 交易所 | `lib/exchange/okx_options_lib.py` | 链、报价、下单、持仓 |
|
||||||
| 交易所 | `lib/exchange/okx_options_lib.py` | 链、报价、下单、持仓 |
|
| 模式/预算 | `options_margin_mode_lib.py` | 币本位、预算、张数规划 |
|
||||||
| 模式/预算 | `options_margin_mode_lib.py` | 币本位、预算、张数规划 |
|
| 展示 enrichment | `options_positions_lib.py` / `options_history_lib.py` | 浮盈、平仓预览 |
|
||||||
| 展示 enrichment | `options_positions_lib.py` / `options_history_lib.py` | 浮盈、平仓预览 |
|
| Env UI | `lib/env/env_ui_manifest.py` | 期权相关默认与文案 |
|
||||||
| Env UI | `lib/env/env_ui_manifest.py` | 期权相关默认与文案 |
|
|
||||||
|
### 1.2 运行模式
|
||||||
### 1.2 运行模式
|
|
||||||
|
- `OKX_TRADE_MODE=options`:主站偏期权。
|
||||||
- `OKX_TRADE_MODE=options`:主站偏期权。
|
- `OKX_OPTIONS_ENABLED`:模块开关。
|
||||||
- `OKX_OPTIONS_ENABLED`:模块开关。
|
- 模拟 / 实盘由顶栏 `trading.mode` + `LIVE_TRADING_ENABLED` 等控制;实盘要求现货+期权权限(见实盘说明)。
|
||||||
- 模拟 / 实盘由顶栏 `trading.mode` + `LIVE_TRADING_ENABLED` 等控制;实盘要求现货+期权权限(见实盘说明)。
|
|
||||||
|
### 1.3 与盈亏规格的关系
|
||||||
### 1.3 与盈亏规格的关系
|
|
||||||
|
规格文档定义**产品规则**;本分析描述**代码落点**。
|
||||||
规格文档定义**产品规则**;本分析描述**代码落点**。
|
凡「已实现盈亏」对外展示(复盘 / 统计 / 顶栏已实现),在新规则下均以 `pnl_settled=1` 的账户差分为准;旧单无快照字段时走遗留口径,互不回写。
|
||||||
凡「已实现盈亏」对外展示(复盘 / 统计 / 顶栏已实现),在新规则下均以 `pnl_settled=1` 的账户差分为准;旧单无快照字段时走遗留口径,互不回写。
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 2. 开仓
|
||||||
## 2. 开仓
|
|
||||||
|
### 2.1 入口
|
||||||
### 2.1 入口
|
|
||||||
|
| 来源 | 路径 / 调用 |
|
||||||
| 来源 | 路径 / 调用 |
|
|------|-------------|
|
||||||
|------|-------------|
|
| 面板 | `POST /api/options/open` |
|
||||||
| 面板 | `POST /api/options/open` |
|
| 条件单触发 | `options_cond_order_lib` → 同一套 `open_coin_option_buy_full` |
|
||||||
| 条件单触发 | `options_cond_order_lib` → 同一套 `open_coin_option_buy_full` |
|
|
||||||
|
前端默认尺寸模式:**按可用余额打满**(`budget_full`);复利开时 UI 切 `compound_full`。
|
||||||
前端默认尺寸模式:**按可用余额打满**(`budget_full`);复利开时 UI 切 `compound_full`。
|
后端 `_normalize_size_mode`:币本位保留 `budget_full` / `compound_full`;USDC 遗留路径才落到 `sheets`。
|
||||||
后端 `_normalize_size_mode`:币本位保留 `budget_full` / `compound_full`;USDC 遗留路径才落到 `sheets`。
|
|
||||||
|
### 2.2 尺寸模式
|
||||||
### 2.2 尺寸模式
|
|
||||||
|
| mode | 含义 |
|
||||||
| mode | 含义 |
|
|------|------|
|
||||||
|------|------|
|
| `budget_full` | min(可用 USDT, 单笔预算) × 预算缓冲 → 张数(默认) |
|
||||||
| `budget_full` | min(可用 USDT, 单笔预算) × 预算缓冲 → 张数(默认) |
|
| `compound_full` | 交易户可用 × 缓冲(受全仓上限 env 约束) |
|
||||||
| `compound_full` | 交易户可用 × 缓冲(受全仓上限 env 约束) |
|
| `sheets` | 指定张数 |
|
||||||
| `sheets` | 指定张数 |
|
| `eth` / `eth_amount` | 指定币数量反推张数 |
|
||||||
| `eth` / `eth_amount` | 指定币数量反推张数 |
|
|
||||||
|
预算计算:`compute_coin_budget_usdt` / `plan_coin_open_by_budget`(`options_margin_mode_lib.py`)。
|
||||||
预算计算:`compute_coin_budget_usdt` / `plan_coin_open_by_budget`(`options_margin_mode_lib.py`)。
|
|
||||||
|
### 2.3 流程
|
||||||
### 2.3 流程
|
|
||||||
|
```text
|
||||||
```text
|
请求 {inst_id, mode, sheets?, eth?, target_index?, profit_exit_*}
|
||||||
请求 {inst_id, mode, sheets?, eth?, target_index?, profit_exit_*}
|
→ 交易模式门禁 / 条件单占用锁 / 持仓笔数上限
|
||||||
→ 交易模式门禁 / 条件单占用锁 / 持仓笔数上限
|
→ 有可卖标的币 → 拒开(须先卖回)
|
||||||
→ 有可卖标的币 → 拒开(须先卖回)
|
→ coin_budget_preview → 规划张数与买币量
|
||||||
→ coin_budget_preview → 规划张数与买币量
|
→ fetch_trading_usdt → 写入 trading_usdt_before
|
||||||
→ fetch_trading_usdt → 写入 trading_usdt_before
|
→ 现货市价买币(× SPOT_BUY_BUFFER)→ options_spot_bridge=bought
|
||||||
→ 现货市价买币(× SPOT_BUY_BUFFER)→ options_spot_bridge=bought
|
→ 期权限价 IOC @ 卖一 → 等待成交
|
||||||
→ 期权限价 IOC @ 卖一 → 等待成交
|
失败 → 回滚卖币
|
||||||
失败 → 回滚卖币
|
成功 → bridge=holding,_insert_coin_trade(open, pnl_settled=0)
|
||||||
成功 → bridge=holding,_insert_coin_trade(open, pnl_settled=0)
|
→ 可选:目标位 / 止盈倍数 / 微信通知
|
||||||
→ 可选:目标位 / 止盈倍数 / 微信通知
|
```
|
||||||
```
|
|
||||||
|
### 2.4 关键 env
|
||||||
### 2.4 关键 env
|
|
||||||
|
| 变量 | 默认(代码/清单) | 作用 |
|
||||||
| 变量 | 默认(代码/清单) | 作用 |
|
|------|-------------------|------|
|
||||||
|------|-------------------|------|
|
| `OKX_OPTIONS_COIN_COMPOUND` | true | 是否按交易户复利 |
|
||||||
| `OKX_OPTIONS_COIN_COMPOUND` | true | 是否按交易户复利 |
|
| `OKX_OPTIONS_COIN_BUDGET_USDT` | (单笔预算) | 打满时的封顶 |
|
||||||
| `OKX_OPTIONS_COIN_BUDGET_USDT` | (单笔预算) | 打满时的封顶 |
|
| `OKX_OPTIONS_BUDGET_BUFFER` | 1 | 预算缓冲 |
|
||||||
| `OKX_OPTIONS_BUDGET_BUFFER` | 1 | 预算缓冲 |
|
| `OKX_OPTIONS_COIN_SPOT_BUY_BUFFER` | 1.05 | 现货多买缓冲 |
|
||||||
| `OKX_OPTIONS_COIN_SPOT_BUY_BUFFER` | 1.05 | 现货多买缓冲 |
|
| `OKX_OPTIONS_MAX_ACTIVE_POSITIONS` | 1 | 同时持仓笔数;0=不限 |
|
||||||
| `OKX_OPTIONS_MAX_ACTIVE_POSITIONS` | 1 | 同时持仓笔数;0=不限 |
|
| `OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC` | — | 开仓等待成交超时 |
|
||||||
| `OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC` | — | 开仓等待成交超时 |
|
|
||||||
|
### 2.5 边界
|
||||||
### 2.5 边界
|
|
||||||
|
- 开仓**只快照 before**,不结算盈亏。
|
||||||
- 开仓**只快照 before**,不结算盈亏。
|
- 持仓或桥未完成期间:`transfer_blocked_by_position_msg` 禁止划转。
|
||||||
- 持仓或桥未完成期间:`transfer_blocked_by_position_msg` 禁止划转。
|
- 一次一仓是对账规格前提;上限默认 1。
|
||||||
- 一次一仓是对账规格前提;上限默认 1。
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 3. 平仓
|
||||||
## 3. 平仓
|
|
||||||
|
### 3.1 三条平仓通道
|
||||||
### 3.1 三条平仓通道
|
|
||||||
|
| 通道 | 触发 | 门控 | 执行 |
|
||||||
| 通道 | 触发 | 门控 | 执行 |
|
|------|------|------|------|
|
||||||
|------|------|------|------|
|
| 手动 / Hub | 用户或中控 | 一般只校验流动性 | `close_option_by_bid1(require_recycle_gate=False)` |
|
||||||
| 手动 / Hub | 用户或中控 | 一般只校验流动性 | `close_option_by_bid1(require_recycle_gate=False)` |
|
| 目标位 | 指数触达目标 | 回收门控(权利金倍数 / 净盈亏 + 持续秒数) | `close_option_by_bid_depth` |
|
||||||
| 目标位 | 指数触达目标 | 回收门控(权利金倍数 / 净盈亏 + 持续秒数) | `close_option_by_bid_depth` |
|
| 止盈倍数 | 买一回收 ≥ 权利金×(1+mult) | 自有回收条件,不走 close_gate | `close_option_by_bid_profit_exit` |
|
||||||
| 止盈倍数 | 买一回收 ≥ 权利金×(1+mult) | 自有回收条件,不走 close_gate | `close_option_by_bid_profit_exit` |
|
|
||||||
|
监控循环:`run_options_target_closes` / `run_options_profit_exits`。
|
||||||
监控循环:`run_options_target_closes` / `run_options_profit_exits`。
|
|
||||||
|
### 3.2 流程
|
||||||
### 3.2 流程
|
|
||||||
|
```text
|
||||||
```text
|
卖期权(买一或深度,reduce-only)
|
||||||
卖期权(买一或深度,reduce-only)
|
→ 部分成交:累计平仓张数
|
||||||
→ 部分成交:累计平仓张数
|
→ 全平:
|
||||||
→ 全平:
|
settle_closed_pnl(币侧/费用;有账户快照时 U 不以公式定稿)
|
||||||
settle_closed_pnl(币侧/费用;有账户快照时 U 不以公式定稿)
|
→ maybe_sell_spot_after_close / sell_residual
|
||||||
→ maybe_sell_spot_after_close / sell_residual
|
→ try_settle_after_sellback
|
||||||
→ try_settle_after_sellback
|
after − before → realized_pnl_usdt,pnl_settled=1
|
||||||
after − before → realized_pnl_usdt,pnl_settled=1
|
```
|
||||||
```
|
|
||||||
|
卖回失败:本单「待结算」,已实现不进统计;可 `POST /api/options/spot-bridge/retry-sell` 或监控扫尾。
|
||||||
卖回失败:本单「待结算」,已实现不进统计;可 `POST /api/options/spot-bridge/retry-sell` 或监控扫尾。
|
|
||||||
|
### 3.3 回收门控 env
|
||||||
### 3.3 回收门控 env
|
|
||||||
|
| 变量 | 默认 | 作用 |
|
||||||
| 变量 | 默认 | 作用 |
|
|------|------|------|
|
||||||
|------|------|------|
|
| `OKX_OPTIONS_CLOSE_GATE_MODE` | premium | `premium` / `net_pnl` |
|
||||||
| `OKX_OPTIONS_CLOSE_GATE_MODE` | premium | `premium` / `net_pnl` |
|
| `OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN` | 1.05 | 币本位回收倍数 |
|
||||||
| `OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN` | 1.05 | 币本位回收倍数 |
|
| `OKX_OPTIONS_CLOSE_HOLD_SECONDS` | 20 | 条件持续秒数 |
|
||||||
| `OKX_OPTIONS_CLOSE_HOLD_SECONDS` | 20 | 条件持续秒数 |
|
| `OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT` | 0 | net_pnl 模式下限 |
|
||||||
| `OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT` | 0 | net_pnl 模式下限 |
|
|
||||||
|
### 3.4 边界
|
||||||
### 3.4 边界
|
|
||||||
|
- 手动平仓可绕过回收门控;自动目标位不可。
|
||||||
- 手动平仓可绕过回收门控;自动目标位不可。
|
- 冻结后任何划转、指数、复盘同步**不得改写** `realized_pnl_usdt`。
|
||||||
- 冻结后任何划转、指数、复盘同步**不得改写** `realized_pnl_usdt`。
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 4. 期权显示
|
||||||
## 4. 期权显示
|
|
||||||
|
### 4.1 页面与 API
|
||||||
### 4.1 页面与 API
|
|
||||||
|
| UI | API | 数据源 |
|
||||||
| UI | API | 数据源 |
|
|----|-----|--------|
|
||||||
|----|-----|--------|
|
| 期权链 / 到期 / 虚实度过滤 | `GET /api/options/chain` | `build_option_chain` |
|
||||||
| 期权链 / 到期 / 虚实度过滤 | `GET /api/options/chain` | `build_option_chain` |
|
| 下单弹窗报价与估张 | `GET /api/options/quote` | `quote_option_contract` + 币本位规划 |
|
||||||
| 下单弹窗报价与估张 | `GET /api/options/quote` | `quote_option_contract` + 币本位规划 |
|
| 持仓卡片 | `GET /api/options/positions` | 交易所持仓 + 本地 trade enrichment |
|
||||||
| 持仓卡片 | `GET /api/options/positions` | 交易所持仓 + 本地 trade enrichment |
|
| 余额 / 顶栏 | `GET /api/options/balances` + header pack | 交易户 / 资金户 |
|
||||||
| 余额 / 顶栏 | `GET /api/options/balances` + header pack | 交易户 / 资金户 |
|
| 挂单 / 目标位 | `.../orders/pending`、`.../targets` | 本地 + 交易所 |
|
||||||
| 挂单 / 目标位 | `.../orders/pending`、`.../targets` | 本地 + 交易所 |
|
| 中控快照 | `GET /api/hub/options/snapshot` | `build_options_hub_snapshot` |
|
||||||
| 中控快照 | `GET /api/hub/options/snapshot` | `build_options_hub_snapshot` |
|
|
||||||
|
前端主文件:`options_panel.js`、`options_position_cards.js`、`options_expiry_countdown.js`。
|
||||||
前端主文件:`options_panel.js`、`options_position_cards.js`、`options_expiry_countdown.js`。
|
|
||||||
|
### 4.2 展示口径(勿与冻结混淆)
|
||||||
### 4.2 展示口径(勿与冻结混淆)
|
|
||||||
|
| 指标 | 口径 |
|
||||||
| 指标 | 口径 |
|
|------|------|
|
||||||
|------|------|
|
| 持仓浮盈 | 买一回收估算(`display_pnl_*`),跟盘变 |
|
||||||
| 持仓浮盈 | 买一回收估算(`display_pnl_*`),跟盘变 |
|
| 合计盈亏 | Σ已冻结 + 当前浮盈 |
|
||||||
| 合计盈亏 | Σ已冻结 + 当前浮盈 |
|
| 顶栏钱包 | 交易所余额,不是盈亏账本 |
|
||||||
| 顶栏钱包 | 交易所余额,不是盈亏账本 |
|
| 下单估权利金 / 杠杆 / 打满提示 | 报价瞬间估算,非成交结果 |
|
||||||
| 下单估权利金 / 杠杆 / 打满提示 | 报价瞬间估算,非成交结果 |
|
|
||||||
|
### 4.3 链过滤 env
|
||||||
### 4.3 链过滤 env
|
|
||||||
|
`OKX_OPTIONS_CHAIN_MAX_DTE_DAYS`、`OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED`、`OKX_OPTIONS_ITM_MAX_DIST_USD`、`OKX_OPTIONS_DEFAULT_UNDERLY`。
|
||||||
`OKX_OPTIONS_CHAIN_MAX_DTE_DAYS`、`OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED`、`OKX_OPTIONS_ITM_MAX_DIST_USD`、`OKX_OPTIONS_DEFAULT_UNDERLY`。
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 5. 复盘
|
||||||
## 5. 复盘
|
|
||||||
|
### 5.1 入口
|
||||||
### 5.1 入口
|
|
||||||
|
- 页:`/options/review`
|
||||||
- 页:`/options/review`
|
- 注册:`options_review_register.py`
|
||||||
- 注册:`options_review_register.py`
|
- 逻辑:`options_review_lib.py` + `options_review_db.py`
|
||||||
- 逻辑:`options_review_lib.py` + `options_review_db.py`
|
- 前端:`options_review_panel.html`、`options_review.js`
|
||||||
- 前端:`options_review_panel.html`、`options_review.js`
|
|
||||||
|
### 5.2 数据流
|
||||||
### 5.2 数据流
|
|
||||||
|
```text
|
||||||
```text
|
options_trades(已平)
|
||||||
options_trades(已平)
|
→ sync_options_from_local_trades / ensure_local_review_synced
|
||||||
→ sync_options_from_local_trades / ensure_local_review_synced
|
→ options_review_trades(history_key ≈ local_opt:{id})
|
||||||
→ options_review_trades(history_key ≈ local_opt:{id})
|
→ 笔记 options_review_entries + 上传图 / 异步图表
|
||||||
→ 笔记 options_review_entries + 上传图 / 异步图表
|
→ 隐藏 options_review_hidden
|
||||||
→ 隐藏 options_review_hidden
|
```
|
||||||
```
|
|
||||||
|
主要 API:`POST /api/options/review/sync`,`GET/DELETE .../trades`,`POST .../entry`,`POST .../upload_slot`,`GET .../stats`。
|
||||||
主要 API:`POST /api/options/review/sync`,`GET/DELETE .../trades`,`POST .../entry`,`POST .../upload_slot`,`GET .../stats`。
|
|
||||||
|
### 5.3 盈亏读取规则
|
||||||
### 5.3 盈亏读取规则
|
|
||||||
|
| 本地单状态 | 复盘展示 |
|
||||||
| 本地单状态 | 复盘展示 |
|
|------------|----------|
|
||||||
|------------|----------|
|
| 有 `trading_usdt_before` 且未 `pnl_settled` | 已实现 U 待结算(不展示假冻结) |
|
||||||
| 有 `trading_usdt_before` 且未 `pnl_settled` | 已实现 U 待结算(不展示假冻结) |
|
| `pnl_settled=1` | 只读冻结 `realized_pnl_usdt` |
|
||||||
| `pnl_settled=1` | 只读冻结 `realized_pnl_usdt` |
|
| 无账户快照的历史单 | 遗留口径,不强制改写成账户差分 |
|
||||||
| 无账户快照的历史单 | 遗留口径,不强制改写成账户差分 |
|
|
||||||
|
复盘是**消费者**,不负责改写结算真源。
|
||||||
复盘是**消费者**,不负责改写结算真源。
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 6. 统计
|
||||||
## 6. 统计
|
|
||||||
|
### 6.1 入口
|
||||||
### 6.1 入口
|
|
||||||
|
| 场景 | API / 函数 |
|
||||||
| 场景 | API / 函数 |
|
|------|------------|
|
||||||
|------|------------|
|
| 期权面板 KPI | `GET /api/options/stats` → `compute_options_stats_auto` |
|
||||||
| 期权面板 KPI | `GET /api/options/stats` → `compute_options_stats_auto` |
|
| 复盘页统计 | `GET /api/options/review/stats` |
|
||||||
| 复盘页统计 | `GET /api/options/review/stats` |
|
| 顶栏窗口统计 | `header_options_stats_for_window` |
|
||||||
| 顶栏窗口统计 | `header_options_stats_for_window` |
|
| 中控 | `GET /api/hub/options/stats` |
|
||||||
| 中控 | `GET /api/hub/options/stats` |
|
|
||||||
|
核心库:`options_stats_lib.py`。
|
||||||
核心库:`options_stats_lib.py`。
|
|
||||||
|
### 6.2 窗口与构成
|
||||||
### 6.2 窗口与构成
|
|
||||||
|
- 窗口:`today` / `7d` / `30d` / `all`(`normalize_stats_window`)。
|
||||||
- 窗口:`today` / `7d` / `30d` / `all`(`normalize_stats_window`)。
|
- 模拟:本地已平且已结算样本。
|
||||||
- 模拟:本地已平且已结算样本。
|
- 实盘:交易所历史 + `options_exchange_pnl_freeze` 对齐后同一套 KPI。
|
||||||
- 实盘:交易所历史 + `options_exchange_pnl_freeze` 对齐后同一套 KPI。
|
- 面板合计:`净已实现(仅 settled) + 持仓浮盈`。
|
||||||
- 面板合计:`净已实现(仅 settled) + 持仓浮盈`。
|
|
||||||
|
### 6.3 边界
|
||||||
### 6.3 边界
|
|
||||||
|
- 待卖回、未冻结单**不计入**已实现。
|
||||||
- 待卖回、未冻结单**不计入**已实现。
|
- 统计不重新推导账户差分;最多做冻结补齐 / 修复类运维函数(如 `freeze_missing_realized_pnl_usdt`),与规格「冻结后不改」一致的运维路径需谨慎使用。
|
||||||
- 统计不重新推导账户差分;最多做冻结补齐 / 修复类运维函数(如 `freeze_missing_realized_pnl_usdt`),与规格「冻结后不改」一致的运维路径需谨慎使用。
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 7. 对账
|
||||||
## 7. 对账
|
|
||||||
|
此处「对账」= **交易账户快照结算 + 本地/交易所持仓对齐**,不是已删除的永续 `reconcile_flat`。
|
||||||
此处「对账」= **交易账户快照结算 + 本地/交易所持仓对齐**,不是已删除的永续 `reconcile_flat`。
|
|
||||||
|
### 7.1 结算真源(账户账)
|
||||||
### 7.1 结算真源(账户账)
|
|
||||||
|
| 步骤 | 函数 | 结果 |
|
||||||
| 步骤 | 函数 | 结果 |
|
|------|------|------|
|
||||||
|------|------|------|
|
| 开仓前 | `fetch_trading_usdt` | `trading_usdt_before` |
|
||||||
| 开仓前 | `fetch_trading_usdt` | `trading_usdt_before` |
|
| 卖回后 | `try_settle_after_sellback` / `settle_closed_trades_from_trading_account` | `after`、`realized_pnl_usdt`、`pnl_settled=1` |
|
||||||
| 卖回后 | `try_settle_after_sellback` / `settle_closed_trades_from_trading_account` | `after`、`realized_pnl_usdt`、`pnl_settled=1` |
|
| 持仓禁划转 | `transfer_blocked_by_position_msg` | 前端灰 + API 拒 |
|
||||||
| 持仓禁划转 | `transfer_blocked_by_position_msg` | 前端灰 + API 拒 |
|
|
||||||
|
公式身份(规格 §6 思路):
|
||||||
公式身份(规格 §6 思路):
|
|
||||||
|
```text
|
||||||
```text
|
Σ 冻结已实现 + 当前浮盈 ≈ 面板合计盈亏
|
||||||
Σ 冻结已实现 + 当前浮盈 ≈ 面板合计盈亏
|
Σ 冻结已实现 + 区间净划转 ≈ 钱包 USDT 变动(在一次一仓、无并行污染前提下)
|
||||||
Σ 冻结已实现 + 区间净划转 ≈ 钱包 USDT 变动(在一次一仓、无并行污染前提下)
|
```
|
||||||
```
|
|
||||||
|
### 7.2 持仓对齐(仓位账)
|
||||||
### 7.2 持仓对齐(仓位账)
|
|
||||||
|
监控 / `_sync_options_trades`:
|
||||||
监控 / `_sync_options_trades`:
|
|
||||||
|
| 动作 | 含义 |
|
||||||
| 动作 | 含义 |
|
|------|------|
|
||||||
|------|------|
|
| `reconcile_live_open_trades` / `sync_open_options_trades` | 交易所仍有仓而本地已关 → 纠偏重开等 |
|
||||||
| `reconcile_live_open_trades` / `sync_open_options_trades` | 交易所仍有仓而本地已关 → 纠偏重开等 |
|
| `backfill_closed_options_realized_pnl_from_history` | 历史回填;**跳过已冻结 U** |
|
||||||
| `backfill_closed_options_realized_pnl_from_history` | 历史回填;**跳过已冻结 U** |
|
| `sell_spot_after_options_sync` / `sweep_spot_after_options_flat` | 平仓后残留币扫尾,推动账户结算 |
|
||||||
| `sell_spot_after_options_sync` / `sweep_spot_after_options_flat` | 平仓后残留币扫尾,推动账户结算 |
|
|
||||||
|
### 7.3 双账清晰
|
||||||
### 7.3 双账清晰
|
|
||||||
|
| 账本 | 看见什么 |
|
||||||
| 账本 | 看见什么 |
|
|------|----------|
|
||||||
|------|----------|
|
| 钱包(顶栏) | 交易所余额,随划转变 |
|
||||||
| 钱包(顶栏) | 交易所余额,随划转变 |
|
| 盈亏账 | 冻结历史 + 浮盈;划转不改已冻结单 |
|
||||||
| 盈亏账 | 冻结历史 + 浮盈;划转不改已冻结单 |
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 8. 条件单
|
||||||
## 8. 条件单
|
|
||||||
|
### 8.1 定位
|
||||||
### 8.1 定位
|
|
||||||
|
中控下发「价格带触发 → 自动开仓 → 挂目标/止盈 → 等平仓结束」的自动化;与面板互斥:有活跃条件单时**锁定手动开仓**(`cond_order_blocks_manual_open_msg`)。
|
||||||
中控下发「价格带触发 → 自动开仓 → 挂目标/止盈 → 等平仓结束」的自动化;与面板互斥:有活跃条件单时**锁定手动开仓**(`cond_order_blocks_manual_open_msg`)。
|
|
||||||
|
### 8.2 入口
|
||||||
### 8.2 入口
|
|
||||||
|
| API | 作用 |
|
||||||
| API | 作用 |
|
|-----|------|
|
||||||
|-----|------|
|
| `GET/POST /api/hub/options/cond_orders` | 列表 / 创建 |
|
||||||
| `GET/POST /api/hub/options/cond_orders` | 列表 / 创建 |
|
| `POST .../<hub_order_id>/cancel` | 取消 |
|
||||||
| `POST .../<hub_order_id>/cancel` | 取消 |
|
| `POST .../exit` | 更新出场参数 |
|
||||||
| `POST .../exit` | 更新出场参数 |
|
|
||||||
|
库:`options_cond_order_lib.py`。
|
||||||
库:`options_cond_order_lib.py`。
|
Tick:监控循环 `run_cond_orders_tick` → `_tick_one`。
|
||||||
Tick:监控循环 `run_cond_orders_tick` → `_tick_one`。
|
|
||||||
|
### 8.3 生命周期
|
||||||
### 8.3 生命周期
|
|
||||||
|
```text
|
||||||
```text
|
Hub 创建(trigger_px, deviation_pct, 认购/认沽, 出场)
|
||||||
Hub 创建(trigger_px, deviation_pct, 认购/认沽, 出场)
|
→ options_cond_orders = armed
|
||||||
→ options_cond_orders = armed
|
→ 指数落入 band_bounds
|
||||||
→ 指数落入 band_bounds
|
→ pick_nearest_itm_contract
|
||||||
→ pick_nearest_itm_contract
|
→ 走 open_coin_option_buy_full(同人工开仓:before 快照、买币、IOC)
|
||||||
→ 走 open_coin_option_buy_full(同人工开仓:before 快照、买币、IOC)
|
→ 挂目标位 / 止盈
|
||||||
→ 挂目标位 / 止盈
|
→ 等待仓位平坦 → _end_order
|
||||||
→ 等待仓位平坦 → _end_order
|
→ 周四 23:00 等策略可 expire 未触发单
|
||||||
→ 周四 23:00 等策略可 expire 未触发单
|
```
|
||||||
```
|
|
||||||
|
### 8.4 与主链关系
|
||||||
### 8.4 与主链关系
|
|
||||||
|
- **不另建盈亏账本**;触发开仓后的卖回冻结与人工单相同。
|
||||||
- **不另建盈亏账本**;触发开仓后的卖回冻结与人工单相同。
|
- 占用期间面板开仓按钮应不可用,避免双开破坏「一次一仓」。
|
||||||
- 占用期间面板开仓按钮应不可用,避免双开破坏「一次一仓」。
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 9. 端到端对照表
|
||||||
## 9. 端到端对照表
|
|
||||||
|
| 阶段 | 写库 / 状态 | 用户可见 | 消费方 |
|
||||||
| 阶段 | 写库 / 状态 | 用户可见 | 消费方 |
|
|------|-------------|----------|--------|
|
||||||
|------|-------------|----------|--------|
|
| 开仓成功 | trade open + before + bridge holding | 持仓卡、浮盈 | 监控、禁划转 |
|
||||||
| 开仓成功 | trade open + before + bridge holding | 持仓卡、浮盈 | 监控、禁划转 |
|
| 期权已平、币未卖回 | 待结算 | 提示待卖回 | 统计不含已实现 |
|
||||||
| 期权已平、币未卖回 | 待结算 | 提示待卖回 | 统计不含已实现 |
|
| 卖回成功 | after、realized、settled=1 | 复盘/统计定格 | 对账完成 |
|
||||||
| 卖回成功 | after、realized、settled=1 | 复盘/统计定格 | 对账完成 |
|
| 条件单 armed | cond_orders | 横幅锁定手动开仓 | 监控 tick |
|
||||||
| 条件单 armed | cond_orders | 横幅锁定手动开仓 | 监控 tick |
|
| 目标位/止盈触发 | 平仓执行 | 持仓消失→卖回→冻结 | 同平仓链 |
|
||||||
| 目标位/止盈触发 | 平仓执行 | 持仓消失→卖回→冻结 | 同平仓链 |
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 10. 关键文件索引
|
||||||
## 10. 关键文件索引
|
|
||||||
|
| 主题 | 路径 |
|
||||||
| 主题 | 路径 |
|
|------|------|
|
||||||
|------|------|
|
| 路由 / 开平仓 API | `lib/options/options_register.py` |
|
||||||
| 路由 / 开平仓 API | `lib/options/options_register.py` |
|
| 币本位开仓 | `lib/options/options_coin_open_lib.py` |
|
||||||
| 币本位开仓 | `lib/options/options_coin_open_lib.py` |
|
| 现货桥 | `lib/options/options_spot_bridge_lib.py` |
|
||||||
| 现货桥 | `lib/options/options_spot_bridge_lib.py` |
|
| 平仓执行 | `lib/options/options_close_exec_lib.py` |
|
||||||
| 平仓执行 | `lib/options/options_close_exec_lib.py` |
|
| 回收门控 | `lib/options/options_close_gate_lib.py` |
|
||||||
| 回收门控 | `lib/options/options_close_gate_lib.py` |
|
| 目标位 | `lib/options/options_target_lib.py` |
|
||||||
| 目标位 | `lib/options/options_target_lib.py` |
|
| 止盈 | `lib/options/options_profit_exit_lib.py` |
|
||||||
| 止盈 | `lib/options/options_profit_exit_lib.py` |
|
| 账户盈亏 | `lib/options/options_account_pnl_lib.py` |
|
||||||
| 账户盈亏 | `lib/options/options_account_pnl_lib.py` |
|
| 统计 | `lib/options/options_stats_lib.py` |
|
||||||
| 统计 | `lib/options/options_stats_lib.py` |
|
| 复盘 | `lib/options/options_review_*.py` |
|
||||||
| 复盘 | `lib/options/options_review_*.py` |
|
| 条件单 | `lib/options/options_cond_order_lib.py` |
|
||||||
| 条件单 | `lib/options/options_cond_order_lib.py` |
|
| 监控 | `lib/options/options_monitor_lib.py` |
|
||||||
| 监控 | `lib/options/options_monitor_lib.py` |
|
| Hub 快照 | `lib/options/options_hub_lib.py` |
|
||||||
| Hub 快照 | `lib/options/options_hub_lib.py` |
|
| Hub 路由 | `lib/hub/hub_bridge_register.py` |
|
||||||
| Hub 路由 | `lib/hub/hub_bridge_register.py` |
|
| Schema | `lib/options/options_db.py` |
|
||||||
| Schema | `lib/options/options_db.py` |
|
| 面板 | `lib/options/templates/options_panel.html`、`lib/common/static/options_panel.js` |
|
||||||
| 面板 | `lib/options/templates/options_panel.html`、`lib/common/static/options_panel.js` |
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## 11. 维护时注意
|
||||||
## 11. 维护时注意
|
|
||||||
|
1. **改盈亏口径**:先改规格文档,再改 `options_account_pnl_lib` 与复盘/统计读取,避免三处漂移。
|
||||||
1. **改盈亏口径**:先改规格文档,再改 `options_account_pnl_lib` 与复盘/统计读取,避免三处漂移。
|
2. **改开仓默认尺寸**:同时看前端 `currentSizeMode`、HTML `checked`、后端 `_normalize_size_mode`(币本位勿再压成 `sheets`)。
|
||||||
2. **改开仓默认尺寸**:同时看前端 `currentSizeMode`、HTML `checked`、后端 `_normalize_size_mode`(币本位勿再压成 `sheets`)。
|
3. **改门控秒数/缓冲**:代码默认、`env_ui_manifest` 文案与 `_RUNTIME_ENV_DEFAULTS` 一并对齐;已有 `.env` 旧值不会自动覆盖。
|
||||||
3. **改门控秒数/缓冲**:代码默认、`env_ui_manifest` 文案与 `_RUNTIME_ENV_DEFAULTS` 一并对齐;已有 `.env` 旧值不会自动覆盖。
|
4. **条件单与手动开仓**:任何新开仓入口都要接 `cond_order_blocks_manual_open_msg`(或等价锁)。
|
||||||
4. **条件单与手动开仓**:任何新开仓入口都要接 `cond_order_blocks_manual_open_msg`(或等价锁)。
|
5. **对账前提**:一次一仓 + 持仓禁划转 + 卖回后冻结;破坏任一前提,钱包差分将无法与单笔盈亏对齐。
|
||||||
5. **对账前提**:一次一仓 + 持仓禁划转 + 卖回后冻结;破坏任一前提,钱包差分将无法与单笔盈亏对齐。
|
|
||||||
|
|||||||
+263
-206
@@ -1,206 +1,263 @@
|
|||||||
# 规格:期权盈亏按交易账户结算(一次一仓)
|
# 规格:期权盈亏双口径结算(一次一仓)
|
||||||
|
|
||||||
> 状态:已定稿,按本文实现
|
> 状态:已定稿(2026-10-13 修订)
|
||||||
> 适用范围:币本位期权(模拟盘优先落地;实盘同规则)
|
> 适用范围:币本位期权(模拟盘优先;实盘同规则)
|
||||||
> 目标:复盘盈亏、数据统计盈亏与交易账户真实 USDT 变动一致,可对账、可冻结、可商用
|
> 目标:复盘可对账、数据统计跟期权历史一致;初始权利金 / 盈利率可展示;错账可脚本修复
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 目标与原则
|
## 1. 目标与原则
|
||||||
|
|
||||||
### 1.1 要解决什么
|
### 1.1 要解决什么
|
||||||
|
|
||||||
此前单笔盈亏混用多套口径(权利金×指数、卖回−买币预算、实时指数重算),导致:
|
此前单笔盈亏混用多套口径,且卖回按开仓权利金封顶,导致:
|
||||||
|
|
||||||
- 持仓「合计盈亏」与复盘单笔不一致
|
- 盈利单(如约 1.5 倍)在复盘被冻成亏损(账户差分在盈利币未卖回时结算)
|
||||||
- 数字随行情跳动
|
- 复盘与数据统计共用账户冻结,与「历史币盈亏 × 卖回价」不一致
|
||||||
- 后期账本无法审计
|
- 缺少初始权利金(U)、盈利率展示
|
||||||
|
|
||||||
### 1.2 原则
|
### 1.2 原则
|
||||||
|
|
||||||
| 原则 | 说明 |
|
| 原则 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 真源 | **交易账户 USDT 快照差分** 为本单已实现盈亏真源 |
|
| 双口径 | **A 复盘** = 交易账户 USDT 快照差分;**B 数据统计** = 期权历史币盈亏 × 平仓卖回价 |
|
||||||
| 一次一仓 | 同一账户同一时间只允许一笔期权持仓 |
|
| 一次一仓 | 同一账户同一时间只允许一笔期权持仓 |
|
||||||
| 卖回后结算 | 未卖回标的币前,本单不进入已实现 |
|
| 卖光再结算 | A 口径:未卖光可卖标的币前,本单不冻结 |
|
||||||
| 冻结 | 结算完成后永不修改 |
|
| 冻结 | A 结算完成后永不修改;B 按平仓卖回价冻结后不跟盘 |
|
||||||
| 禁划转 | 持仓期间禁止资金划转(前端灰 + 后端拒) |
|
| 禁划转 | 持仓 / 待卖回期间禁止资金划转 |
|
||||||
| 双账清晰 | 顶栏余额 = 钱包;合计/复盘盈亏 = 冻结历史 +(持仓时)浮盈 |
|
| 权利金不含多买 | 初始权利金 = 开仓指数 × 权利金币量(不含现货缓冲多买) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 产品规则(用户可见)
|
## 2. 双口径定义
|
||||||
|
|
||||||
### 2.1 开仓前
|
### 2.1 口径 A — 期权复盘(交易账户)
|
||||||
|
|
||||||
1. **有可卖标的币(ETH/BTC)→ 拒开仓**
|
```
|
||||||
- 提示:先卖回 USDT(可走自动扫尾/卖回)。
|
本单已实现盈亏(U) = trading_usdt_after − trading_usdt_before
|
||||||
- 粉尘:低于现货最小可卖量可忽略;达到可卖量一律视为有币并拒开。
|
```
|
||||||
2. **交易账户 USDT 快照** `trading_usdt_before`
|
|
||||||
- 取开仓前(买币前)交易账户 USDT。
|
- **消费者**:期权复盘「交易记录」「复盘记录」、复盘 KPI
|
||||||
- 开仓前若有过多次划转:以**开仓前最后一次划转完成后**的余额为准(即开仓那一刻的余额)。
|
- **刻意含多买缓冲 / 滑点 / 手续费**:便于交易账户对账
|
||||||
3. 通过检查后才允许买币并开期权。
|
- **门禁**:全部期权仓位已平 **且** 标的币无可卖量(粉尘可忽略)后才拍 `after` 并冻结
|
||||||
|
|
||||||
### 2.2 持仓中
|
### 2.2 口径 B — 期权数据统计(历史 × 卖回价)
|
||||||
|
|
||||||
1. **划转按钮灰色不可点**;API 同步拒绝。
|
```
|
||||||
2. **合计盈亏(动态)** =
|
统计已实现盈亏(U) = 期权历史.realized_pnl(币) × 平仓卖回价
|
||||||
`Σ已冻结历史盈亏` + `当前持仓浮盈`
|
```
|
||||||
3. 本单未卖回结算前:不计入「已冻结历史」;浮盈跟盘更新。
|
|
||||||
|
- **平仓卖回价**:优先卖回成交均价 / 冻结的 `close_index_px`;缺失时再用当时指数一次性冻结
|
||||||
### 2.3 平仓与卖回
|
- **消费者**:期权面板「数据统计」合计/胜率/盈亏比、数据统计页 KPI、顶栏期权窗口统计
|
||||||
|
- **不含**账户多买缓冲差分;与复盘 A **独立**,禁止混用
|
||||||
1. 期权平仓后触发**自动卖回**残留标的币。
|
|
||||||
2. **卖回未完成**:本单状态为「待结算」,已实现盈亏不写死。
|
### 2.3 初始权利金与盈利率
|
||||||
3. **卖回成功后**:再拍交易账户 USDT 快照 `trading_usdt_after`。
|
|
||||||
|
| 字段 | 定义 |
|
||||||
### 2.4 结算与冻结
|
|------|------|
|
||||||
|
| 初始权利金(U) | `open_index_px × premium_paid`(`premium_paid` 为开仓权利金币量;**不含** `budget_usdt` 多买) |
|
||||||
```
|
| 盈利率(复盘) | `realized_pnl_usdt(A) / initial_premium_usdt` |
|
||||||
本单已实现盈亏(U) = trading_usdt_after − trading_usdt_before
|
| 盈利率(统计) | `统计盈亏(B) / initial_premium_usdt`(展示层可算,不强制落库) |
|
||||||
```
|
|
||||||
|
复盘列表与复盘详情须展示:初始权利金(U)、盈利率。
|
||||||
1. 写入本单并标记已冻结。
|
|
||||||
2. **此后不得因划转、指数变动、复盘同步而修改。**
|
---
|
||||||
3. 期权复盘、数据统计的已实现盈亏均读此冻结值。
|
|
||||||
|
## 3. 产品规则
|
||||||
### 2.5 划转与资金
|
|
||||||
|
### 3.1 开仓前
|
||||||
| 时机 | 行为 |
|
|
||||||
|------|------|
|
1. 有可卖标的币(ETH/BTC)→ 拒开仓(粉尘低于现货最小可卖可忽略)。
|
||||||
| 无持仓、无待卖回 | 允许划转;顶栏资金立即刷新 |
|
2. 拍交易账户 USDT → `trading_usdt_before`。
|
||||||
| 持仓中或待卖回 | 禁止划转 |
|
3. 开仓前若有多次划转:以**开仓前最后一次划转完成后**的余额为准(即开仓瞬间余额)。
|
||||||
| 划转后 | 只影响下一笔开仓前的 `usdt_before`;**不重算**已冻结历史单 |
|
4. 开仓失败:回滚卖币,并拍**最新**资金快照(不留下脏 before / 半成品桥阻挡后续)。
|
||||||
|
5. 写入 `initial_premium_usdt = open_index_px × premium_paid`(开仓成功后)。
|
||||||
---
|
|
||||||
|
### 3.2 持仓中
|
||||||
## 3. 生命周期
|
|
||||||
|
1. 划转按钮灰 + API 拒绝。
|
||||||
```text
|
2. 面板合计盈亏(动态)= `Σ口径B已冻结` + `当前持仓浮盈`。
|
||||||
[空仓, 无残留币]
|
3. 本单未完成 A 结算前:复盘显示「待结算」,不写假 U。
|
||||||
│
|
|
||||||
├─ 有可卖币? ──是──> 拒开仓「请先卖回」
|
### 3.3 平仓与卖回
|
||||||
│
|
|
||||||
├─ 快照 trading_usdt_before
|
1. 系统可多次部分平仓;**以最后一次全部平仓完成**为准进入卖回。
|
||||||
├─ 买币 → 开期权
|
2. 期权空仓后:**卖光**交易账户该标的可卖币(**禁止**用开仓权利金 `coin_bought` 封顶)。
|
||||||
│
|
3. `coin_bought` 仅用于开仓阶段清理过量买币,不作平仓卖回上限。
|
||||||
[持仓中] 划转禁用;合计 = 冻结历史 + 浮盈
|
4. 卖回未完成(仍有可卖币):桥保持待卖回,**不**进入 A 结算。
|
||||||
│
|
5. 真粉尘(低于现货最小):可关桥并进入 A 结算(差额含粉尘损失)。
|
||||||
├─ 平仓
|
|
||||||
│
|
### 3.4 结算与冻结(A)
|
||||||
[待卖回] 已实现未结算;划转仍禁用
|
|
||||||
│
|
1. `realized_pnl_usdt = after − before`,`pnl_settled=1`。
|
||||||
├─ 自动/重试卖回成功
|
2. 此后不得因划转、指数、复盘同步、监控公式而修改。
|
||||||
├─ 快照 trading_usdt_after
|
3. 仅复盘读此冻结值。
|
||||||
├─ realized = after − before → 冻结
|
|
||||||
│
|
### 3.5 划转
|
||||||
[已结算] 复盘/统计只读冻结值
|
|
||||||
```
|
| 时机 | 行为 |
|
||||||
|
|------|------|
|
||||||
---
|
| 无持仓、无待卖回 | 允许划转 |
|
||||||
|
| 持仓中或待卖回 | 禁止划转 |
|
||||||
## 4. 数据字段(实现约定)
|
| 划转后 | 只影响下一笔开仓前的 `usdt_before`;不重算已冻结 A/B |
|
||||||
|
|
||||||
在 `options_trades` 增加(或等价持久化):
|
---
|
||||||
|
|
||||||
| 字段 | 含义 |
|
## 4. 状态机
|
||||||
|------|------|
|
|
||||||
| `trading_usdt_before` | 开仓前交易账户 USDT |
|
```text
|
||||||
| `trading_usdt_after` | 卖回完成后交易账户 USDT |
|
IDLE
|
||||||
| `realized_pnl_usdt` | 冻结已实现 = after − before(结算后写入) |
|
│ 有可卖币? ──是──> 拒开仓
|
||||||
| `pnl_settled` | 0/1,是否已按账户差分结算冻结 |
|
│ 快照 trading_usdt_before(划转取最后一次后余额)
|
||||||
|
▼
|
||||||
说明:
|
OPENING ──失败──> 回滚卖币 → 最新资金快照 → IDLE
|
||||||
|
│ 成功(一次一仓);写 initial_premium_usdt
|
||||||
- `budget_usdt` 仍表示本笔买币花费,**不得**当作开仓前账户快照。
|
▼
|
||||||
- 币数量 `realized_pnl` 可保留供期权历史展示(币),与 U 冻结分离。
|
OPEN(禁划转)
|
||||||
- 统计/复盘的 **U 盈亏** 只认 `pnl_settled=1` 时的 `realized_pnl_usdt`。
|
│ 可多次部分平仓
|
||||||
|
▼
|
||||||
---
|
CLOSING(交易所仓位未清零则停留)
|
||||||
|
│ 仓位 = 0
|
||||||
## 5. 模块行为
|
▼
|
||||||
|
SELLING_SPOT(卖光可卖标的币;禁止权利金封顶)
|
||||||
### 5.1 开仓 `open_coin_option_buy_full`
|
│ 可卖币 < minSz(真粉尘)或已空
|
||||||
|
▼
|
||||||
1. 检查一次一仓 / 未完成桥。
|
SETTLE_A(after − before → 冻结;仅此态可写 pnl_settled=1)
|
||||||
2. `fetch_trading_coin_available`:可卖则拒开。
|
▼
|
||||||
3. 读取交易账户 USDT → `trading_usdt_before`。
|
IDLE
|
||||||
4. 买币、下单、写库(带 before,`pnl_settled=0`)。
|
```
|
||||||
|
|
||||||
### 5.2 划转 `POST /api/options/transfer`
|
**硬门禁**
|
||||||
|
|
||||||
1. 若存在 `options_trades.status='open'` 或桥状态 ∈ {bought, holding, pending_sell} → **403/业务错误**。
|
| 条件 | 行为 |
|
||||||
2. 前端:持仓或待卖回时划转按钮 `disabled` + 提示「持仓中不可划转」。
|
|------|------|
|
||||||
|
| OPEN / CLOSING / SELLING_SPOT | 禁止划转 |
|
||||||
### 5.3 卖回结算
|
| 未进 SETTLE_A | 复盘 U 为待结算 |
|
||||||
|
| pnl_settled=1 | 禁止覆盖 A |
|
||||||
在 `sell_residual_after_option_flat`(及 retry-sell)桥关闭成功后:
|
| SETTLE_A | 要求 open_positions=0 且无可卖标的币 |
|
||||||
|
|
||||||
1. 读取交易账户 USDT → `trading_usdt_after`。
|
实现上可为显式状态字段,或由 `options_trades.status` + 桥状态 + `pnl_settled` 等价表达,但**结算入口必须统一校验门禁**。
|
||||||
2. 对本桥关联且未结算的已平仓位:
|
|
||||||
`realized_pnl_usdt = after - before`,`pnl_settled=1`。
|
---
|
||||||
3. **停止**用「权利金×指数」或「卖回−预算」覆盖已结算单。
|
|
||||||
|
## 5. 数据字段
|
||||||
粉尘关闭桥(无法卖出):
|
|
||||||
- 仍拍 `usdt_after` 并结算(差额含粉尘损失);或
|
### 5.1 `options_trades`
|
||||||
- 记 `pnl_settled` 并附 message「粉尘忽略」——实现取:拍售后余额并冻结,保证可对账。
|
|
||||||
|
| 字段 | 含义 |
|
||||||
### 5.4 数据统计
|
|------|------|
|
||||||
|
| `trading_usdt_before` | 开仓前交易账户 USDT |
|
||||||
- `net_realized_pnl` / 已平相关:仅 `pnl_settled=1` 的冻结值之和。
|
| `trading_usdt_after` | 卖光后交易账户 USDT |
|
||||||
- `total_pnl`(持仓页合计)= `net_realized_pnl` + `open_float_pnl`。
|
| `realized_pnl_usdt` | **口径 A** 冻结值 |
|
||||||
- 待结算已平单:不计入已实现,也不重复计入浮盈(浮盈仅当前期权持仓)。
|
| `pnl_settled` | 0/1,A 是否已冻结 |
|
||||||
- **顶栏三项**(总交易 / 胜率 / 盈亏比):与上同一过滤口径(`header_options_stats_for_window`)。
|
| `realized_pnl` | 币数量净盈亏(期权历史展示) |
|
||||||
|
| `close_index_px` | 平仓/卖回价(口径 B 用) |
|
||||||
### 5.5 期权复盘
|
| `open_index_px` | 开仓指数 |
|
||||||
|
| `premium_paid` | 开仓权利金币量 |
|
||||||
- 同步本地已平:优先读 `realized_pnl_usdt`(且已结算)。
|
| `initial_premium_usdt` | 初始权利金 U = open_index × premium_paid |
|
||||||
- 未结算:复盘显示「待结算」或暂不导入已实现 U。
|
| `budget_usdt` | 买币花费(含缓冲),**不得**当权利金或 before |
|
||||||
- 禁止再用实时指数改写已冻结 U。
|
|
||||||
|
### 5.2 `options_review_trades`
|
||||||
### 5.6 期权历史列表
|
|
||||||
|
| 字段 | 含义 |
|
||||||
- **仍按币**展示权利金/币盈亏(不强制改成 U)。
|
|------|------|
|
||||||
- U 口径只用于复盘与数据统计。
|
| `realized_pnl_total` | **仅口径 A**(或待结算空) |
|
||||||
|
| `initial_premium_usdt` | 初始权利金 U |
|
||||||
---
|
| `profit_rate` | A / 初始权利金(小数,如 1.5 = 1.5 倍) |
|
||||||
|
|
||||||
## 6. 对账关系(商用)
|
### 5.3 `options_exchange_pnl_freeze`
|
||||||
|
|
||||||
```
|
实盘统计辅助:历史 key → 口径 B 的 U 冻结。
|
||||||
Σ 已冻结单笔盈亏 + 当前浮盈 ≈ 合计盈亏展示
|
|
||||||
Σ 已冻结单笔盈亏 + 期间净划转 ≈ 交易账户相对「首笔 before」的累计变化
|
---
|
||||||
```
|
|
||||||
|
## 6. 模块行为
|
||||||
差异来源应可归因:粉尘、手续费计入方式、卖回滑点(已含在 after−before 内)。
|
|
||||||
禁止用展示公式回写钱包余额。
|
### 6.1 开仓 `open_coin_option_buy_full`
|
||||||
|
|
||||||
---
|
1. 一次一仓 / 未完成桥检查;可卖币拒开。
|
||||||
|
2. `trading_usdt_before`;买币、下单、写库(`pnl_settled=0`,`initial_premium_usdt`)。
|
||||||
## 7. 非目标 / 不做
|
3. 桥 `coin_bought` 可记为权利金币量(供开仓清过量),**平仓卖回不得用其封顶**。
|
||||||
|
|
||||||
- 不用买币预算 `budget_usdt` 当单笔盈利。
|
### 6.2 划转 `POST /api/options/transfer`
|
||||||
- 不用「净币 × 单一指数」覆盖已结算账户差分。
|
|
||||||
- 持仓中不因行情重算已冻结历史。
|
持仓或桥 ∈ {bought, holding, pending_sell} → 拒绝。
|
||||||
- 不把期权历史列表强制改为 U 展示。
|
|
||||||
|
### 6.3 卖回与 A 结算
|
||||||
---
|
|
||||||
|
`sell_residual_after_option_flat`(及 retry / sweep):
|
||||||
## 8. 验收清单
|
|
||||||
|
1. 空仓后卖光可用标的币(`coin_amount=None`)。
|
||||||
- [ ] 开仓前有可卖 BTC/ETH → 拒开并提示卖回
|
2. 仍有可卖 → 桥 `pending_sell`,返回未完成,**不结算**。
|
||||||
- [ ] 开仓写入 `trading_usdt_before`
|
3. 仅粉尘/已空 → 关桥 → `try_settle_after_sellback`(内再校验无可卖币)→ A 冻结。
|
||||||
- [ ] 持仓中划转按钮灰 + API 拒绝
|
|
||||||
- [ ] 平仓未卖回前,统计已实现不含本单
|
### 6.4 数据统计(口径 B)
|
||||||
- [ ] 卖回后 `realized = after − before` 并 `pnl_settled=1`
|
|
||||||
- [ ] 再次同步复盘/统计,冻结值不变
|
- `net_realized_pnl` / 胜率 / 盈亏比:历史币盈亏 × 卖回价。
|
||||||
- [ ] 合计盈亏 = 冻结历史 + 浮盈(有持仓时动态)
|
- **不读** `pnl_settled` 账户差分。
|
||||||
- [ ] 空仓划转后顶栏资金更新,历史冻结单不变
|
- 数据统计页不得再打复盘 stats API 作为盈亏真源。
|
||||||
|
|
||||||
---
|
### 6.5 期权复盘(口径 A)
|
||||||
|
|
||||||
## 9. 修订记录
|
- 同步本地已平:优先 `pnl_settled=1` 的 `realized_pnl_usdt`。
|
||||||
|
- 未结算:显示待结算,不导入假 U。
|
||||||
| 日期 | 说明 |
|
- 同步 `initial_premium_usdt`、`profit_rate`。
|
||||||
|------|------|
|
|
||||||
| 2026-10-09 | 初版定稿:交易账户快照差分 + 一次一仓 + 持仓禁划转 + 卖回后冻结 |
|
### 6.6 期权历史列表
|
||||||
|
|
||||||
|
仍按**币**展示权利金/币盈亏;U 只用于复盘(A)与数据统计(B)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 对账关系
|
||||||
|
|
||||||
|
```
|
||||||
|
口径 A: Σ 已冻结单笔 A + 期间净划转 ≈ 交易账户相对首笔 before 的累计变化
|
||||||
|
口径 B: Σ (历史币盈亏 × 卖回价) ≈ 面板数据统计已实现
|
||||||
|
```
|
||||||
|
|
||||||
|
差异应可归因:粉尘、滑点、手续费(A 内已含);B 不含多买缓冲。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 非目标 / 不做
|
||||||
|
|
||||||
|
- 不用 `budget_usdt` 当单笔盈利或初始权利金。
|
||||||
|
- 不用口径 A 覆盖数据统计,也不用口径 B 覆盖复盘。
|
||||||
|
- 持仓中不因行情重算已冻结 A/B。
|
||||||
|
- 不把期权历史列表强制改为 U。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 历史数据修复
|
||||||
|
|
||||||
|
对已错误冻结的账户差分(典型:币盈亏>0 且 A 为负,且 `coin_sold≈premium_paid≪premium_received`):
|
||||||
|
|
||||||
|
1. 回填 `initial_premium_usdt = open_index_px × premium_paid`。
|
||||||
|
2. 口径 B:按币盈亏 × 卖回价重算统计侧(含 `options_exchange_pnl_freeze`)。
|
||||||
|
3. 口径 A:卖回完整可重建则重算;否则 `pnl_settled=0` 标 `needs_resettle` / 异常,勿盲信错账。
|
||||||
|
4. 重刷 `options_review_trades.realized_pnl_total` / `profit_rate`。
|
||||||
|
|
||||||
|
脚本:`scripts/repair_options_dual_pnl.py`(dry-run → CSV → apply)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 验收清单
|
||||||
|
|
||||||
|
- [ ] 盈利约 1.5 倍单:卖回含盈利币;复盘 A 为正,约等于账户差分
|
||||||
|
- [ ] 卖回不得按开仓权利金封顶
|
||||||
|
- [ ] 仍有可卖币时不 `pnl_settled=1`
|
||||||
|
- [ ] 复盘展示初始权利金、盈利率
|
||||||
|
- [ ] 数据统计 = 历史币盈亏 × 卖回价,与复盘 A 可不同
|
||||||
|
- [ ] 开仓写入 before + initial_premium;失败有最新快照/回滚
|
||||||
|
- [ ] 持仓禁划转;空仓划转不影响已冻结
|
||||||
|
- [ ] 历史错账可用脚本修复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 修订记录
|
||||||
|
|
||||||
|
| 日期 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-10-09 | 初版:交易账户快照差分 + 一次一仓 + 持仓禁划转 + 卖回后冻结 |
|
||||||
|
| 2026-10-13 | 修订:双口径 A/B;卖光再结算;初始权利金/盈利率;状态机;历史修复脚本 |
|
||||||
|
|||||||
@@ -102,12 +102,8 @@
|
|||||||
var kpi = $("ods-kpi");
|
var kpi = $("ods-kpi");
|
||||||
var groups = $("ods-stats-groups");
|
var groups = $("ods-stats-groups");
|
||||||
if (!kpi || !groups) return Promise.resolve();
|
if (!kpi || !groups) return Promise.resolve();
|
||||||
var qs = new URLSearchParams();
|
// 口径 B:期权历史币盈亏×卖回价(/api/options/stats),与复盘账户差分独立
|
||||||
qs.set("source_type", sourceType());
|
return fetch("/api/options/stats", { credentials: "same-origin" })
|
||||||
qs.set("sync", "0");
|
|
||||||
return fetch("/api/options/review/stats?" + qs.toString(), {
|
|
||||||
credentials: "same-origin",
|
|
||||||
})
|
|
||||||
.then(function (r) {
|
.then(function (r) {
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
@@ -117,19 +113,26 @@
|
|||||||
groups.innerHTML = "";
|
groups.innerHTML = "";
|
||||||
return;
|
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 = [
|
kpi.innerHTML = [
|
||||||
["笔数", k.total],
|
["已平笔数", data.total_closed || 0],
|
||||||
["已复盘率", (k.review_rate || 0) + "%"],
|
["胜率", (data.win_rate || 0) + "%"],
|
||||||
["胜率", (k.win_rate || 0) + "%"],
|
["盈亏比", data.profit_loss_ratio != null ? data.profit_loss_ratio : "—"],
|
||||||
["累计盈亏", fmtPnl(k.pnl_sum)],
|
["累计盈亏", fmtPnl(net)],
|
||||||
["平均盈亏", fmtPnl(k.avg_pnl)],
|
["平均盈利", data.avg_win != null ? fmtPnl(data.avg_win) : "—"],
|
||||||
["平均持有", fmtHold(k.avg_hold_sec)],
|
["平均亏损", data.avg_loss != null ? fmtPnl(-Math.abs(Number(data.avg_loss))) : "—"],
|
||||||
|
["平均持有", fmtHold(data.avg_hold_sec)],
|
||||||
]
|
]
|
||||||
.map(function (pair) {
|
.map(function (pair) {
|
||||||
var cls = "";
|
var cls = "";
|
||||||
if (pair[0] === "累计盈亏") cls = pnlClass(k.pnl_sum);
|
if (pair[0] === "累计盈亏") cls = pnlClass(net);
|
||||||
if (pair[0] === "平均盈亏") cls = pnlClass(k.avg_pnl);
|
if (pair[0] === "平均盈利") cls = "pnl-pos";
|
||||||
|
if (pair[0] === "平均亏损") cls = "pnl-neg";
|
||||||
return (
|
return (
|
||||||
'<div class="ods-kpi-tile"><div class="ods-kpi-label">' +
|
'<div class="ods-kpi-tile"><div class="ods-kpi-label">' +
|
||||||
pair[0] +
|
pair[0] +
|
||||||
@@ -141,18 +144,11 @@
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.join("");
|
.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 =
|
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 () {
|
.catch(function () {
|
||||||
kpi.innerHTML = '<div class="muted" style="font-size:.76rem">统计加载失败</div>';
|
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
|
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(
|
def try_settle_after_sellback(
|
||||||
get_db: Any,
|
get_db: Any,
|
||||||
ex: Any,
|
ex: Any,
|
||||||
*,
|
*,
|
||||||
bridge_id: int | None = None,
|
bridge_id: int | None = None,
|
||||||
cfg: dict[str, Any] | None = None,
|
cfg: dict[str, Any] | None = None,
|
||||||
|
underlying: str | None = None,
|
||||||
|
sell_complete: bool | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""卖回路径通用结算:读交易账户 after 并冻结未结算单."""
|
"""卖回路径通用结算:读交易账户 after 并冻结未结算单.
|
||||||
pack = cfg if isinstance(cfg, dict) else {"fetch_options_balances": None}
|
|
||||||
after = fetch_trading_usdt(pack, ex)
|
门禁:sell_complete 显式 False 时不结算;仍有可卖标的币时不结算.
|
||||||
if after is None:
|
"""
|
||||||
|
if sell_complete is False:
|
||||||
return 0
|
return 0
|
||||||
conn = get_db() if callable(get_db) else get_db
|
pack = cfg if isinstance(cfg, dict) else {"fetch_options_balances": None}
|
||||||
close_conn = callable(get_db)
|
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:
|
try:
|
||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
init_options_tables(conn)
|
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
|
n = 0
|
||||||
if bridge_id is not None:
|
if bridge_id is not None:
|
||||||
n = settle_closed_trades_from_trading_account(
|
n = settle_closed_trades_from_trading_account(
|
||||||
@@ -125,10 +208,16 @@ def try_settle_after_sellback(
|
|||||||
"""
|
"""
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row:
|
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(
|
n = settle_closed_trades_from_trading_account(
|
||||||
conn,
|
conn,
|
||||||
trading_usdt_after=after,
|
trading_usdt_after=after,
|
||||||
trade_id=int(row["id"] if hasattr(row, "keys") else row[0]),
|
trade_id=tid,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
conn.commit()
|
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
|
tusdt_before = float(tusdt_before) if tusdt_before is not None else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
tusdt_before = None
|
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(
|
cur = conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO options_trades(
|
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,
|
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, trading_usdt_before, pnl_settled,
|
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
|
profit_exit_enabled, profit_exit_mult, profit_exit_state, created_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
kwargs["inst_id"],
|
kwargs["inst_id"],
|
||||||
@@ -639,6 +648,7 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
|||||||
fee_ccy,
|
fee_ccy,
|
||||||
open_index_px,
|
open_index_px,
|
||||||
tusdt_before,
|
tusdt_before,
|
||||||
|
initial_premium_usdt,
|
||||||
pe,
|
pe,
|
||||||
pe_mult,
|
pe_mult,
|
||||||
pe_state,
|
pe_state,
|
||||||
@@ -685,17 +695,20 @@ def maybe_sell_spot_after_close(
|
|||||||
underlying=underlying,
|
underlying=underlying,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
)
|
)
|
||||||
# 卖回成功:交易账户 after − 开仓 before → 冻结本单盈亏
|
# 卖回完成(sell_complete):交易账户 after − 开仓 before → 冻结本单盈亏
|
||||||
try:
|
try:
|
||||||
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||||
|
|
||||||
if (result or {}).get("ok"):
|
res = result or {}
|
||||||
bid = (result or {}).get("bridge_id")
|
if res.get("ok") and res.get("sell_complete", True) and not res.get("incomplete"):
|
||||||
|
bid = res.get("bridge_id")
|
||||||
try_settle_after_sellback(
|
try_settle_after_sellback(
|
||||||
cfg["get_db"],
|
cfg["get_db"],
|
||||||
ex,
|
ex,
|
||||||
bridge_id=int(bid) if bid is not None else None,
|
bridge_id=int(bid) if bid is not None else None,
|
||||||
cfg=cfg,
|
cfg=cfg,
|
||||||
|
underlying=underlying,
|
||||||
|
sell_complete=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -766,20 +779,23 @@ def sweep_spot_after_options_flat(
|
|||||||
underlying=uly,
|
underlying=uly,
|
||||||
inst_id=None,
|
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:
|
try:
|
||||||
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
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(
|
try_settle_after_sellback(
|
||||||
cfg["get_db"],
|
cfg["get_db"],
|
||||||
exchange,
|
exchange,
|
||||||
bridge_id=int(bid) if bid is not None else None,
|
bridge_id=int(bid) if bid is not None else None,
|
||||||
cfg=cfg,
|
cfg=cfg,
|
||||||
|
underlying=uly,
|
||||||
|
sell_complete=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
sold.append({"underlying": uly, **(out or {})})
|
sold.append({"underlying": uly, **out})
|
||||||
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)})
|
||||||
return {"ok": True, "results": sold, "skipped": not sold}
|
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_before REAL",
|
||||||
"ALTER TABLE options_trades ADD COLUMN trading_usdt_after REAL",
|
"ALTER TABLE options_trades ADD COLUMN trading_usdt_after REAL",
|
||||||
"ALTER TABLE options_trades ADD COLUMN pnl_settled INTEGER DEFAULT 0",
|
"ALTER TABLE options_trades ADD COLUMN pnl_settled INTEGER DEFAULT 0",
|
||||||
|
# 初始权利金(U)=开仓指数×权利金币量,不含多买缓冲
|
||||||
|
"ALTER TABLE options_trades ADD COLUMN initial_premium_usdt REAL",
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
conn.execute(ddl)
|
conn.execute(ddl)
|
||||||
|
|||||||
@@ -1225,7 +1225,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
underlying=underlying,
|
underlying=underlying,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
)
|
)
|
||||||
if out.get("ok"):
|
if out.get("ok") and out.get("sell_complete", True) and not out.get("incomplete"):
|
||||||
try:
|
try:
|
||||||
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
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,
|
ex,
|
||||||
bridge_id=int(bid) if bid is not None else None,
|
bridge_id=int(bid) if bid is not None else None,
|
||||||
cfg=cfg,
|
cfg=cfg,
|
||||||
|
underlying=underlying,
|
||||||
|
sell_complete=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
_mark_balances_stale(cfg)
|
_mark_balances_stale(cfg)
|
||||||
|
elif out.get("ok"):
|
||||||
|
_mark_balances_stale(cfg)
|
||||||
return jsonify(out)
|
return jsonify(out)
|
||||||
|
|
||||||
@app.route("/api/options/convert/quote", methods=["POST"])
|
@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_up", "REAL")
|
||||||
_ensure_column(conn, "options_review_trades", "target_price_down", "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", "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")
|
_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")),
|
"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")),
|
"premium_paid": _safe_float(row.get("premium_paid")),
|
||||||
"realized_pnl": pnl,
|
"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())
|
cols = list(fields.keys())
|
||||||
if existing:
|
if existing:
|
||||||
@@ -317,7 +319,7 @@ def sync_options_from_local_trades(
|
|||||||
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
|
trading_usdt_before, pnl_settled, initial_premium_usdt
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
@@ -439,12 +441,19 @@ def sync_options_from_local_trades(
|
|||||||
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)
|
||||||
# 权利金用平仓/开仓冻结指数折算,不用实时价
|
# 初始权利金(U)=开仓指数×权利金币量;不含多买
|
||||||
prem_u = _to_usdt(prem, ccy=ccy, idx=idx)
|
stored_initial = _safe_float(r["initial_premium_usdt"])
|
||||||
if ccy in ("ETH", "BTC") and pnl_u is None:
|
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
|
pnl_u = pnl
|
||||||
if ccy in ("ETH", "BTC") and prem_u is None:
|
if ccy in ("ETH", "BTC") and prem_u is None:
|
||||||
prem_u = prem
|
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"]
|
opened_at = r["created_at"]
|
||||||
closed_at = r["closed_at"]
|
closed_at = r["closed_at"]
|
||||||
action = upsert_option_history_row(
|
action = upsert_option_history_row(
|
||||||
@@ -461,10 +470,12 @@ def sync_options_from_local_trades(
|
|||||||
"open_avg_px": r["open_quote"],
|
"open_avg_px": r["open_quote"],
|
||||||
"close_avg_px": r["close_quote"],
|
"close_avg_px": r["close_quote"],
|
||||||
"premium_paid": prem_u if prem_u is not None else prem,
|
"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,
|
"created_at": opened_at,
|
||||||
"closed_at": closed_at,
|
"closed_at": closed_at,
|
||||||
"status_label": "已平",
|
"status_label": "待结算" if acct_pending else "已平",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if action == "inserted":
|
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]:
|
def enrich_trade_row(row: dict[str, Any], entry: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
out = dict(row)
|
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["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)
|
out["is_hedge"] = str(out.get("source_type") or "") in (SOURCE_PERP_OPTIONS, SOURCE_OPTIONS_OPTIONS)
|
||||||
legs = []
|
legs = []
|
||||||
|
|||||||
@@ -451,14 +451,14 @@ def sell_residual_after_option_flat(
|
|||||||
underlying: str,
|
underlying: str,
|
||||||
inst_id: str | None = None,
|
inst_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""期权已平:卖掉本桥残留标的币;优先关闭 matching holding/pending 桥.
|
"""期权已平:卖光交易户该标的可卖币(一次一仓,含盈利币).
|
||||||
|
|
||||||
|
规格:平仓卖回禁止按开仓权利金 coin_bought 封顶;仍有可卖则不关桥、不结算.
|
||||||
可传 get_db 可调用对象(推荐)或已打开的 Connection。
|
可传 get_db 可调用对象(推荐)或已打开的 Connection。
|
||||||
卖币(尤其模拟盘写钱包)期间绝不握着业务连接,否则会 database is locked。
|
卖币(尤其模拟盘写钱包)期间绝不握着业务连接,否则会 database is locked。
|
||||||
"""
|
"""
|
||||||
get_db = conn_or_get_db if callable(conn_or_get_db) else None
|
get_db = conn_or_get_db if callable(conn_or_get_db) else None
|
||||||
bid: int | None = None
|
bid: int | None = None
|
||||||
sell_cap: float | None = None
|
|
||||||
want_inst = str(inst_id or "").strip()
|
want_inst = str(inst_id or "").strip()
|
||||||
|
|
||||||
def _pick_bridge(bridges: list[dict[str, Any]]) -> dict[str, Any] | None:
|
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))
|
target = _pick_bridge(list_open_bridges(conn))
|
||||||
if target is not None:
|
if target is not None:
|
||||||
bid = int(target["id"])
|
bid = int(target["id"])
|
||||||
sell_cap = _safe_float(target.get("coin_bought"))
|
|
||||||
try:
|
try:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -501,7 +500,6 @@ def sell_residual_after_option_flat(
|
|||||||
target = _pick_bridge(list_open_bridges(conn))
|
target = _pick_bridge(list_open_bridges(conn))
|
||||||
if target is not None:
|
if target is not None:
|
||||||
bid = int(target["id"])
|
bid = int(target["id"])
|
||||||
sell_cap = _safe_float(target.get("coin_bought"))
|
|
||||||
try:
|
try:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -512,13 +510,10 @@ def sell_residual_after_option_flat(
|
|||||||
avail_chk = fetch_trading_coin_available(ex, underlying)
|
avail_chk = fetch_trading_coin_available(ex, underlying)
|
||||||
except Exception:
|
except Exception:
|
||||||
avail_chk = None
|
avail_chk = None
|
||||||
dust_amt = avail_chk
|
if avail_chk is not None and not is_sellable_spot_coin(ex, underlying, 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):
|
|
||||||
dust_msg = "粉尘低于现货最小可卖,已忽略"
|
dust_msg = "粉尘低于现货最小可卖,已忽略"
|
||||||
if bid is None:
|
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]:
|
def _close_dust(c: sqlite3.Connection) -> dict[str, Any]:
|
||||||
update_bridge(
|
update_bridge(
|
||||||
c,
|
c,
|
||||||
@@ -533,6 +528,7 @@ def sell_residual_after_option_flat(
|
|||||||
"msg": dust_msg,
|
"msg": dust_msg,
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
"dust": True,
|
"dust": True,
|
||||||
|
"sell_complete": True,
|
||||||
"bridge_id": bid,
|
"bridge_id": bid,
|
||||||
"bridge_status": BRIDGE_CLOSED,
|
"bridge_status": BRIDGE_CLOSED,
|
||||||
}
|
}
|
||||||
@@ -547,20 +543,33 @@ def sell_residual_after_option_flat(
|
|||||||
pass
|
pass
|
||||||
return _close_dust(conn_or_get_db)
|
return _close_dust(conn_or_get_db)
|
||||||
|
|
||||||
# 有桥时按 coin_bought 封顶,避免误卖无关余额;无桥空仓扫尾仍卖光可用
|
# 一次一仓空仓后卖光可用(含盈利币);禁止用 coin_bought/权利金封顶
|
||||||
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
|
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
|
||||||
sell = spot_market_sell_coin_to_usdt(
|
sell = spot_market_sell_coin_to_usdt(
|
||||||
ex,
|
ex,
|
||||||
underlying=underlying,
|
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 bid is None:
|
||||||
if not sell.get("ok"):
|
if not sell.get("ok"):
|
||||||
msg = str(sell.get("msg") or "")
|
msg = str(sell.get("msg") or "")
|
||||||
if "无可用" in msg or "过小" in msg:
|
if "无可用" in msg or "过小" in msg:
|
||||||
return {"ok": True, "msg": "无残留币需卖回", "skipped": True}
|
return {"ok": True, "msg": "无残留币需卖回", "skipped": True, "sell_complete": True}
|
||||||
return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL}
|
return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL, "sell_complete": False}
|
||||||
return {"ok": True, "sell": sell, "bridge_status": None}
|
# 再确认无可卖残留
|
||||||
|
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]:
|
def _apply_bridge(c: sqlite3.Connection) -> dict[str, Any]:
|
||||||
if not sell.get("ok"):
|
if not sell.get("ok"):
|
||||||
@@ -580,6 +589,7 @@ def sell_residual_after_option_flat(
|
|||||||
"msg": "粉尘低于现货最小可卖,已忽略",
|
"msg": "粉尘低于现货最小可卖,已忽略",
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
"dust": True,
|
"dust": True,
|
||||||
|
"sell_complete": True,
|
||||||
"bridge_id": bid,
|
"bridge_id": bid,
|
||||||
"bridge_status": BRIDGE_CLOSED,
|
"bridge_status": BRIDGE_CLOSED,
|
||||||
}
|
}
|
||||||
@@ -595,10 +605,36 @@ def sell_residual_after_option_flat(
|
|||||||
"msg": msg,
|
"msg": msg,
|
||||||
"bridge_id": bid,
|
"bridge_id": bid,
|
||||||
"bridge_status": BRIDGE_PENDING_SELL,
|
"bridge_status": BRIDGE_PENDING_SELL,
|
||||||
|
"sell_complete": False,
|
||||||
}
|
}
|
||||||
recovered = _safe_float(sell.get("usdt_recovered"))
|
recovered = _safe_float(sell.get("usdt_recovered"))
|
||||||
if recovered is not None and recovered > 0:
|
if recovered is not None and recovered > 0:
|
||||||
add_bridge_usdt_recovered(c, bid, recovered)
|
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(
|
update_bridge(
|
||||||
c,
|
c,
|
||||||
bid,
|
bid,
|
||||||
@@ -609,7 +645,13 @@ def sell_residual_after_option_flat(
|
|||||||
message="期权已平,币已卖回 USDT",
|
message="期权已平,币已卖回 USDT",
|
||||||
close=True,
|
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:
|
if get_db is not None:
|
||||||
conn2 = get_db()
|
conn2 = get_db()
|
||||||
|
|||||||
@@ -59,11 +59,16 @@ def _underlying_index_usdt(ex: Any, underly: str) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
|
def history_pnl_to_usdt(
|
||||||
"""
|
history: list[dict[str, Any]],
|
||||||
统计用:优先已冻结的 realized_pnl_usdt(平仓当时口径,含手续费后净额);
|
ex: Any = None,
|
||||||
否则币本位用 close_index_px(再否则实时指数)折 U;USDC 原样.
|
*,
|
||||||
折算失败的币仓剔除盈亏字段,避免把「币数量」当成 U.
|
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
|
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)
|
out.append(r)
|
||||||
continue
|
continue
|
||||||
frozen = _safe_float(r.get("realized_pnl_usdt"))
|
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)
|
r["realized_pnl"] = round(float(frozen), 4)
|
||||||
out.append(r)
|
out.append(r)
|
||||||
continue
|
continue
|
||||||
@@ -104,6 +109,11 @@ def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[d
|
|||||||
return out
|
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:
|
def _parse_ts(raw: Any) -> datetime | None:
|
||||||
if raw is None or raw == "":
|
if raw is None or raw == "":
|
||||||
return None
|
return None
|
||||||
@@ -286,7 +296,8 @@ def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"status": "closed",
|
"status": "closed",
|
||||||
"realized_pnl": _row_get(row, "realized_pnl"),
|
"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"),
|
"close_index_px": _row_get(row, "close_index_px"),
|
||||||
"created_at": _row_get(row, "created_at"),
|
"created_at": _row_get(row, "created_at"),
|
||||||
"closed_at": _row_get(row, "closed_at"),
|
"closed_at": _row_get(row, "closed_at"),
|
||||||
@@ -298,16 +309,13 @@ def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# 口径 B:凡有币盈亏的已平均计入;U 由 history_pnl_to_usdt_stats 按卖回价折算
|
||||||
_CLOSED_STATS_SELECT = """
|
_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
|
created_at, closed_at, inst_id, underlying, premium_ccy
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
AND (
|
AND realized_pnl IS NOT NULL
|
||||||
(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))
|
|
||||||
)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -634,7 +642,7 @@ def apply_exchange_pnl_freeze(
|
|||||||
|
|
||||||
|
|
||||||
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
||||||
"""模拟盘:本地交易结算.实盘请用 compute_options_stats_live_exchange."""
|
"""模拟盘数据统计:口径 B(历史币盈亏×卖回价)."""
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
history = load_local_options_stats_history(conn, ex)
|
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()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
finally:
|
||||||
conn.close()
|
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]:
|
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)
|
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(
|
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)
|
history = _closed_trade_dicts(closed_rows)
|
||||||
for row in open_rows or []:
|
for row in open_rows or []:
|
||||||
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
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(
|
def header_options_stats_for_window(
|
||||||
@@ -821,32 +831,20 @@ def header_options_stats_for_window(
|
|||||||
app_tz,
|
app_tz,
|
||||||
ex: Any = None,
|
ex: Any = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""顶栏总交易/胜率/盈亏比:与期权统计同一结算口径.
|
"""顶栏总交易/胜率/盈亏比:口径 B(历史币盈亏×卖回价)."""
|
||||||
|
|
||||||
新单(有 trading_usdt_before):仅 pnl_settled=1 的账户差分计入;
|
|
||||||
旧单(无 before):仍按原 realized 兼容.币数量须折 U 后再算盈亏比.
|
|
||||||
"""
|
|
||||||
from lib.common.history_window_lib import utc_window_to_bj_sql_strings
|
from lib.common.history_window_lib import utc_window_to_bj_sql_strings
|
||||||
|
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
start_bj, end_bj = utc_window_to_bj_sql_strings(
|
start_bj, end_bj = utc_window_to_bj_sql_strings(
|
||||||
list_window["start_utc"], list_window["end_utc"], app_tz
|
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(
|
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
|
created_at, closed_at, inst_id, underlying, premium_ccy
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
AND (
|
AND realized_pnl IS NOT NULL
|
||||||
(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 COALESCE(closed_at, created_at) >= ?
|
AND COALESCE(closed_at, created_at) >= ?
|
||||||
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)
|
history = _closed_trade_dicts(closed_rows)
|
||||||
for row in open_rows:
|
for row in open_rows:
|
||||||
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
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 {
|
return {
|
||||||
"total": int(stats.get("total_closed") or 0),
|
"total": int(stats.get("total_closed") or 0),
|
||||||
"rate": float(stats.get("win_rate") 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-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-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{
|
.or-kpi-row{
|
||||||
display:grid;grid-template-columns:repeat(6,minmax(0,1fr));
|
display:grid;grid-template-columns:repeat(6,minmax(0,1fr));
|
||||||
gap:8px;margin-bottom:12px;
|
gap:8px;margin-bottom:12px;
|
||||||
@@ -237,7 +238,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>类型</th>
|
<th>类型</th>
|
||||||
<th>标的/合约</th>
|
<th>标的/合约</th>
|
||||||
|
<th>初始权利金</th>
|
||||||
<th>盈亏</th>
|
<th>盈亏</th>
|
||||||
|
<th>盈利率</th>
|
||||||
<th>开仓时间</th>
|
<th>开仓时间</th>
|
||||||
<th>平仓时间</th>
|
<th>平仓时间</th>
|
||||||
<th>持有</th>
|
<th>持有</th>
|
||||||
@@ -245,7 +248,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="or-trades-tbody">
|
<tbody id="or-trades-tbody">
|
||||||
<tr><td colspan="7" class="muted">加载中…</td></tr>
|
<tr><td colspan="9" class="muted">加载中…</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,6 +283,8 @@
|
|||||||
data-lpignore="true" data-1p-ignore="true" data-form-type="other">
|
data-lpignore="true" data-1p-ignore="true" data-form-type="other">
|
||||||
<input type="text" id="or-f-pnl" name="or_f_pnl" placeholder="盈亏(U)"
|
<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">
|
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">
|
<input type="text" id="or-f-hold" name="or_f_hold" placeholder="持有时长" readonly autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="or-form-grid2">
|
<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>
|
||||||
|
<th>盈利率</th>
|
||||||
<th>开仓时间</th>
|
<th>开仓时间</th>
|
||||||
<th>平仓时间</th>
|
<th>平仓时间</th>
|
||||||
<th>持仓时长</th>
|
<th>持仓时长</th>
|
||||||
@@ -414,7 +421,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="or-reviewed-tbody">
|
<tbody id="or-reviewed-tbody">
|
||||||
<tr><td colspan="11" class="muted">加载中…</td></tr>
|
<tr><td colspan="13" class="muted">加载中…</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""修复期权双口径历史数据(规格 docs/规格-期权交易账户盈亏结算.md §9).
|
||||||
|
|
||||||
|
默认 dry-run,打印将要修改的行;加 --apply 才写库.
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/repair_options_dual_pnl.py --db path/to/instance.db
|
||||||
|
python scripts/repair_options_dual_pnl.py --db path/to/instance.db --apply
|
||||||
|
python scripts/repair_options_dual_pnl.py --db path/to/instance.db --csv out.csv --apply
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
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 _connect(db_path: Path) -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_columns(conn: sqlite3.Connection) -> None:
|
||||||
|
# 与业务库迁移一致,可重复执行
|
||||||
|
for ddl in (
|
||||||
|
"ALTER TABLE options_trades ADD COLUMN initial_premium_usdt REAL",
|
||||||
|
"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",
|
||||||
|
"ALTER TABLE options_review_trades ADD COLUMN initial_premium_usdt REAL",
|
||||||
|
"ALTER TABLE options_review_trades ADD COLUMN profit_rate REAL",
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
conn.execute(ddl)
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]:
|
||||||
|
_ensure_columns(conn)
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, inst_id, premium_paid, premium_received, realized_pnl,
|
||||||
|
open_index_px, close_index_px, budget_usdt,
|
||||||
|
trading_usdt_before, trading_usdt_after,
|
||||||
|
realized_pnl_usdt, pnl_settled, bridge_id, initial_premium_usdt
|
||||||
|
FROM options_trades
|
||||||
|
WHERE status='closed'
|
||||||
|
ORDER BY id
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
report: list[dict[str, Any]] = []
|
||||||
|
for r in rows:
|
||||||
|
tid = int(r["id"])
|
||||||
|
paid = _safe_float(r["premium_paid"])
|
||||||
|
recv = _safe_float(r["premium_received"])
|
||||||
|
coin_pnl = _safe_float(r["realized_pnl"])
|
||||||
|
open_idx = _safe_float(r["open_index_px"])
|
||||||
|
close_idx = _safe_float(r["close_index_px"])
|
||||||
|
before = _safe_float(r["trading_usdt_before"])
|
||||||
|
after = _safe_float(r["trading_usdt_after"])
|
||||||
|
settled_u = _safe_float(r["realized_pnl_usdt"])
|
||||||
|
settled = int(r["pnl_settled"] or 0) == 1
|
||||||
|
initial = _safe_float(r["initial_premium_usdt"])
|
||||||
|
|
||||||
|
new_initial = initial
|
||||||
|
if new_initial is None and open_idx and paid and open_idx > 0 and paid > 0:
|
||||||
|
new_initial = round(open_idx * paid, 4)
|
||||||
|
|
||||||
|
stats_u = None
|
||||||
|
if coin_pnl is not None and close_idx is not None and close_idx > 0:
|
||||||
|
stats_u = round(coin_pnl * close_idx, 4)
|
||||||
|
elif coin_pnl is not None and open_idx is not None and open_idx > 0:
|
||||||
|
stats_u = round(coin_pnl * open_idx, 4)
|
||||||
|
|
||||||
|
# 错账:币盈亏为正、账户冻结为负(典型卖回封顶)
|
||||||
|
suspicious = bool(
|
||||||
|
settled
|
||||||
|
and coin_pnl is not None
|
||||||
|
and coin_pnl > 0
|
||||||
|
and settled_u is not None
|
||||||
|
and settled_u < 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# 桥侧:coin_sold ≈ premium 且远小于 recv → 未卖盈利币
|
||||||
|
bridge_hint = ""
|
||||||
|
bid = r["bridge_id"]
|
||||||
|
coin_bought = coin_sold = None
|
||||||
|
if bid is not None:
|
||||||
|
try:
|
||||||
|
b = conn.execute(
|
||||||
|
"SELECT coin_bought, coin_sold FROM options_spot_bridge WHERE id=?",
|
||||||
|
(int(bid),),
|
||||||
|
).fetchone()
|
||||||
|
if b:
|
||||||
|
coin_bought = _safe_float(b["coin_bought"])
|
||||||
|
coin_sold = _safe_float(b["coin_sold"])
|
||||||
|
if (
|
||||||
|
paid
|
||||||
|
and coin_sold is not None
|
||||||
|
and recv
|
||||||
|
and coin_sold <= paid * 1.05
|
||||||
|
and recv > paid * 1.2
|
||||||
|
):
|
||||||
|
bridge_hint = "sell_capped_at_premium"
|
||||||
|
suspicious = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
action = "ok"
|
||||||
|
new_settled_u = settled_u
|
||||||
|
new_pnl_settled = 1 if settled else 0
|
||||||
|
if suspicious:
|
||||||
|
# 无法可靠重建 after 时解除错误冻结,待卖回后重结算
|
||||||
|
action = "unsettle_bad_account_freeze"
|
||||||
|
new_pnl_settled = 0
|
||||||
|
new_settled_u = None
|
||||||
|
elif before is not None and after is not None and not settled:
|
||||||
|
action = "settle_from_snapshots"
|
||||||
|
new_settled_u = round(after - before, 4)
|
||||||
|
new_pnl_settled = 1
|
||||||
|
|
||||||
|
profit_rate = None
|
||||||
|
a_for_rate = new_settled_u if new_pnl_settled == 1 else None
|
||||||
|
if a_for_rate is not None and new_initial and abs(new_initial) > 1e-12:
|
||||||
|
profit_rate = round(a_for_rate / new_initial, 6)
|
||||||
|
|
||||||
|
rec = {
|
||||||
|
"trade_id": tid,
|
||||||
|
"inst_id": r["inst_id"],
|
||||||
|
"action": action,
|
||||||
|
"suspicious": suspicious,
|
||||||
|
"bridge_hint": bridge_hint,
|
||||||
|
"coin_pnl": coin_pnl,
|
||||||
|
"stats_pnl_usdt_B": stats_u,
|
||||||
|
"old_realized_pnl_usdt_A": settled_u,
|
||||||
|
"new_realized_pnl_usdt_A": new_settled_u,
|
||||||
|
"old_pnl_settled": int(r["pnl_settled"] or 0),
|
||||||
|
"new_pnl_settled": new_pnl_settled,
|
||||||
|
"initial_premium_usdt": new_initial,
|
||||||
|
"profit_rate": profit_rate,
|
||||||
|
"coin_bought": coin_bought,
|
||||||
|
"coin_sold": coin_sold,
|
||||||
|
"premium_paid": paid,
|
||||||
|
"premium_received": recv,
|
||||||
|
}
|
||||||
|
report.append(rec)
|
||||||
|
|
||||||
|
if not apply:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if new_initial is not None and initial != new_initial:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE options_trades SET initial_premium_usdt=? WHERE id=?",
|
||||||
|
(new_initial, tid),
|
||||||
|
)
|
||||||
|
if action == "unsettle_bad_account_freeze":
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE options_trades
|
||||||
|
SET pnl_settled=0, realized_pnl_usdt=NULL, trading_usdt_after=NULL
|
||||||
|
WHERE id=? AND COALESCE(pnl_settled,0)=1
|
||||||
|
""",
|
||||||
|
(tid,),
|
||||||
|
)
|
||||||
|
elif action == "settle_from_snapshots" and new_settled_u is not None:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE options_trades
|
||||||
|
SET realized_pnl_usdt=?, pnl_settled=1
|
||||||
|
WHERE id=? AND COALESCE(pnl_settled,0)=0
|
||||||
|
""",
|
||||||
|
(new_settled_u, tid),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 复盘表
|
||||||
|
hk = f"local_opt:{tid}"
|
||||||
|
rev = conn.execute(
|
||||||
|
"SELECT id FROM options_review_trades WHERE history_key=?", (hk,)
|
||||||
|
).fetchone()
|
||||||
|
if rev:
|
||||||
|
pnl_total = new_settled_u if new_pnl_settled == 1 else None
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE options_review_trades
|
||||||
|
SET initial_premium_usdt=COALESCE(?, initial_premium_usdt),
|
||||||
|
realized_pnl_total=?,
|
||||||
|
realized_pnl=?,
|
||||||
|
profit_rate=?,
|
||||||
|
status_raw=?
|
||||||
|
WHERE history_key=?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
new_initial,
|
||||||
|
pnl_total,
|
||||||
|
pnl_total,
|
||||||
|
profit_rate,
|
||||||
|
"待结算" if new_pnl_settled == 0 and before is not None else "已平",
|
||||||
|
hk,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 实盘/统计冻结表:口径 B
|
||||||
|
if stats_u is not None:
|
||||||
|
try:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO options_exchange_pnl_freeze(
|
||||||
|
history_key, realized_pnl, realized_pnl_usdt, close_index_px,
|
||||||
|
premium_ccy, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, datetime('now'))
|
||||||
|
ON CONFLICT(history_key) DO UPDATE SET
|
||||||
|
realized_pnl=excluded.realized_pnl,
|
||||||
|
realized_pnl_usdt=excluded.realized_pnl_usdt,
|
||||||
|
close_index_px=excluded.close_index_px,
|
||||||
|
updated_at=excluded.updated_at
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
hk,
|
||||||
|
coin_pnl,
|
||||||
|
stats_u,
|
||||||
|
close_idx or open_idx,
|
||||||
|
"BTC" if "BTC" in str(r["inst_id"] or "") else "ETH",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if apply:
|
||||||
|
conn.commit()
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description="修复期权双口径历史盈亏")
|
||||||
|
ap.add_argument("--db", required=True, help="实例 SQLite 路径")
|
||||||
|
ap.add_argument("--apply", action="store_true", help="实际写库(默认 dry-run)")
|
||||||
|
ap.add_argument("--csv", default="", help="导出报告 CSV 路径")
|
||||||
|
args = ap.parse_args()
|
||||||
|
db_path = Path(args.db)
|
||||||
|
if not db_path.is_file():
|
||||||
|
print(f"db not found: {db_path}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
conn = _connect(db_path)
|
||||||
|
try:
|
||||||
|
report = repair(conn, apply=bool(args.apply))
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
bad = [x for x in report if x.get("suspicious") or x.get("action") != "ok"]
|
||||||
|
print(f"scanned={len(report)} need_attention={len(bad)} apply={bool(args.apply)}")
|
||||||
|
for x in bad[:50]:
|
||||||
|
print(
|
||||||
|
f" #{x['trade_id']} {x['inst_id']} action={x['action']} "
|
||||||
|
f"A:{x['old_realized_pnl_usdt_A']}->{x['new_realized_pnl_usdt_A']} "
|
||||||
|
f"B={x['stats_pnl_usdt_B']} hint={x['bridge_hint']}"
|
||||||
|
)
|
||||||
|
if args.csv:
|
||||||
|
out = Path(args.csv)
|
||||||
|
if report:
|
||||||
|
with out.open("w", newline="", encoding="utf-8-sig") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=list(report[0].keys()))
|
||||||
|
w.writeheader()
|
||||||
|
w.writerows(report)
|
||||||
|
print(f"wrote {out}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -51,55 +51,59 @@ class AccountPnlSettleTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(float(row2["realized_pnl_usdt"]), 947.05, places=2)
|
self.assertAlmostEqual(float(row2["realized_pnl_usdt"]), 947.05, places=2)
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_stats_only_count_settled_account_trades(self):
|
def test_stats_use_coin_times_sell_px_not_account(self):
|
||||||
|
"""口径 B:有币盈亏即计入,U=币×卖回价;不读账户差分."""
|
||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
from lib.options.options_stats_lib import (
|
from lib.options.options_stats_lib import (
|
||||||
compute_options_stats_from_history,
|
compute_options_stats_from_history,
|
||||||
history_pnl_to_usdt,
|
history_pnl_to_usdt_stats,
|
||||||
load_local_options_stats_history,
|
load_local_options_stats_history,
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
# 待结算:不进统计
|
# 账户未结算但有币盈亏+卖回价 → 进统计 B
|
||||||
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, trading_usdt_before, pnl_settled, created_at, closed_at
|
close_index_px, status, trading_usdt_before, 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', 500.0, 0, '2026-10-06 10:00:00', '2026-10-08 14:00:00'
|
3000.0, 'closed', 500.0, 0,
|
||||||
|
'2026-10-06 10:00:00', '2026-10-08 14:00:00'
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
# 已结算
|
# 账户冻结为负、币盈亏为正 → 统计仍用币×价
|
||||||
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, trading_usdt_before, trading_usdt_after,
|
close_index_px, status, trading_usdt_before, trading_usdt_after,
|
||||||
realized_pnl_usdt, pnl_settled, 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,
|
'BTC-USD-261015-84000-P', 'BTC', 'P', 1, 0.01,
|
||||||
0.01, 0.02, 0.01, 'ETH',
|
0.01, 0.025, 0.015, 'BTC',
|
||||||
'closed', 1000.0, 1952.96,
|
100000.0, 'closed', 1000.0, 948.82,
|
||||||
952.96, 1, '2026-10-01 10:00:00', '2026-10-02 14:00:00'
|
-51.18, 1, '2026-10-01 10:00:00', '2026-10-02 14:00:00'
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
hist = load_local_options_stats_history(conn, None)
|
hist = load_local_options_stats_history(conn, None)
|
||||||
stats = compute_options_stats_from_history(history_pnl_to_usdt(hist, None))
|
stats = compute_options_stats_from_history(history_pnl_to_usdt_stats(hist, None))
|
||||||
self.assertEqual(int(stats["total_closed"]), 1)
|
self.assertEqual(int(stats["total_closed"]), 2)
|
||||||
self.assertAlmostEqual(float(stats["net_realized_pnl"]), 952.96, places=2)
|
# 0.01*3000 + 0.015*100000 = 30 + 1500 = 1530
|
||||||
|
self.assertAlmostEqual(float(stats["net_realized_pnl"]), 1530.0, places=2)
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def test_header_stats_skip_unsettled_account_trades(self):
|
def test_header_stats_count_coin_pnl_rows(self):
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
@@ -112,11 +116,11 @@ class AccountPnlSettleTests(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
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, realized_pnl, premium_ccy, status,
|
premium_paid, realized_pnl, premium_ccy, close_index_px, status,
|
||||||
trading_usdt_before, pnl_settled, created_at, closed_at
|
trading_usdt_before, 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.01, 'ETH', 'closed',
|
0.01, 0.01, 'ETH', 3000.0, 'closed',
|
||||||
500.0, 0, '2026-10-08 10:00:00', '2026-10-08 12:00:00'
|
500.0, 0, '2026-10-08 10:00:00', '2026-10-08 12:00:00'
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
@@ -125,12 +129,12 @@ class AccountPnlSettleTests(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
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, realized_pnl, premium_ccy, status,
|
premium_paid, realized_pnl, premium_ccy, close_index_px, status,
|
||||||
trading_usdt_before, trading_usdt_after, realized_pnl_usdt,
|
trading_usdt_before, trading_usdt_after, realized_pnl_usdt,
|
||||||
pnl_settled, created_at, closed_at
|
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.01, 'ETH', 'closed',
|
0.01, 0.01, 'ETH', 3000.0, 'closed',
|
||||||
1000.0, 1100.0, 100.0,
|
1000.0, 1100.0, 100.0,
|
||||||
1, '2026-10-08 08:00:00', '2026-10-08 09:00:00'
|
1, '2026-10-08 08:00:00', '2026-10-08 09:00:00'
|
||||||
)
|
)
|
||||||
@@ -142,7 +146,50 @@ class AccountPnlSettleTests(unittest.TestCase):
|
|||||||
"end_utc": datetime(2026, 10, 31, 23, 59, 59, tzinfo=timezone.utc),
|
"end_utc": datetime(2026, 10, 31, 23, 59, 59, tzinfo=timezone.utc),
|
||||||
}
|
}
|
||||||
out = header_options_stats_for_window(conn, win, timezone.utc, None)
|
out = header_options_stats_for_window(conn, win, timezone.utc, None)
|
||||||
self.assertEqual(int(out["total"]), 1)
|
self.assertEqual(int(out["total"]), 2)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_settle_blocked_when_sellable_coin(self):
|
||||||
|
from lib.options.options_account_pnl_lib import try_settle_after_sellback
|
||||||
|
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-261015-84000-P', 'BTC', 'P', 1, 0.01,
|
||||||
|
0.01, 'closed', 7, 1000.0, 0,
|
||||||
|
'2026-10-12 01:46:50', '2026-10-13 08:07:35'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
class _Ex:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# monkey: 仍有可卖 BTC
|
||||||
|
import lib.options.options_account_pnl_lib as mod
|
||||||
|
|
||||||
|
old_block = mod.sellable_coin_blocks_settle
|
||||||
|
mod.sellable_coin_blocks_settle = lambda ex, uly: "仍有可卖 BTC"
|
||||||
|
try:
|
||||||
|
n = try_settle_after_sellback(
|
||||||
|
conn, _Ex(), bridge_id=7, sell_complete=True
|
||||||
|
)
|
||||||
|
self.assertEqual(n, 0)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT pnl_settled FROM options_trades WHERE id=1"
|
||||||
|
).fetchone()
|
||||||
|
self.assertEqual(int(row["pnl_settled"]), 0)
|
||||||
|
finally:
|
||||||
|
mod.sellable_coin_blocks_settle = old_block
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,22 @@ class TestSpotDust(unittest.TestCase):
|
|||||||
|
|
||||||
class TestSellCapLogic(unittest.TestCase):
|
class TestSellCapLogic(unittest.TestCase):
|
||||||
def test_excess_cap_math(self):
|
def test_excess_cap_math(self):
|
||||||
|
"""开仓清过量仍按 coin_bought 封顶;平仓卖回不再用权利金封顶."""
|
||||||
bought, keep = 0.01, 0.004
|
bought, keep = 0.01, 0.004
|
||||||
excess = max(0.0, bought - keep)
|
excess = max(0.0, bought - keep)
|
||||||
self.assertAlmostEqual(excess, 0.006)
|
self.assertAlmostEqual(excess, 0.006)
|
||||||
sell_amt = min(0.02, excess)
|
sell_amt = min(0.02, excess)
|
||||||
self.assertAlmostEqual(sell_amt, 0.006)
|
self.assertAlmostEqual(sell_amt, 0.006)
|
||||||
|
|
||||||
|
def test_flat_sell_does_not_use_premium_cap(self):
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from lib.options import options_spot_bridge_lib as m
|
||||||
|
|
||||||
|
src = inspect.getsource(m.sell_residual_after_option_flat)
|
||||||
|
self.assertIn("coin_amount=None", src)
|
||||||
|
self.assertNotIn("sell_cap if", src)
|
||||||
|
|
||||||
|
|
||||||
class TestMaybeSellAfterCloseGuard(unittest.TestCase):
|
class TestMaybeSellAfterCloseGuard(unittest.TestCase):
|
||||||
"""平仓后卖回:持仓查询失败/空响应绝不卖币."""
|
"""平仓后卖回:持仓查询失败/空响应绝不卖币."""
|
||||||
|
|||||||
Reference in New Issue
Block a user