Merge OKX dual APIs into one OKX_API_* account for perp and options.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-05 21:28:33 +08:00
parent 22e6b68e6d
commit 09a763e47d
26 changed files with 166 additions and 473 deletions
+11 -10
View File
@@ -79,11 +79,12 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单)
LIVE_TRADING_ENABLED=true
# OKX API Key(实盘)
# =============================================================================
# OKX 账户 API(永续 + 期权共用同一套密钥;修改后须重启 PM2)
# 旧键 OKX_OPTIONS_API_* 已废弃:若 OKX_API_* 为空,启动时会从 OPTIONS 键回填
# =============================================================================
OKX_API_KEY=REPLACE_WITH_OKX_API_KEY
# OKX API Secret(实盘)
OKX_API_SECRET=REPLACE_WITH_OKX_API_SECRET
# OKX API Passphrase(实盘)
OKX_API_PASSPHRASE=REPLACE_WITH_OKX_API_PASSPHRASE
# 保证金模式:cross=全仓,isolated=逐仓
OKX_TD_MODE=cross
@@ -99,18 +100,19 @@ OKX_POSITION_INST_TYPE=SWAP
EXCHANGE_DISPLAY_NAME=OKX
# 企业微信推送里展示的账户备注
# OKX_ACCOUNT_LABEL=
# 顶栏是否显示永续资金账户/交易账户(热更);false 时总资金仅计期权侧
# 顶栏是否显示 USDT 资金/交易账户(热更);false 时总资金仅计期权 USDC
OKX_SHOW_PERP_FUNDS=true
# =============================================================================
# 期权(主账户 API,与永续子账户 OKX_API_* 分离;修改后须重启 PM2)
# 期权模块(与上方 OKX_API_* 同源;修改启用开关后须重启 PM2)
# 详见 docs/期权方案.md 与 docs/期权用法.md
# =============================================================================
OKX_OPTIONS_ENABLED=false
OKX_OPTIONS_API_KEY=
OKX_OPTIONS_API_SECRET=
OKX_OPTIONS_API_PASSPHRASE=
OKX_OPTIONS_ACCOUNT_LABEL=主账户·期权
# 以下 OKX_OPTIONS_API_* 已废弃,请勿再配置(仅兼容旧部署回填)
# OKX_OPTIONS_API_KEY=
# OKX_OPTIONS_API_SECRET=
# OKX_OPTIONS_API_PASSPHRASE=
OKX_OPTIONS_ACCOUNT_LABEL=账户·期权
OKX_OPTIONS_TRADE_BUDGET_USDC=10
OKX_OPTIONS_BUDGET_BUFFER=0.95
# 交易模式三选一(热更):options=单独期权 / perp_options=永期对冲 / options_options=期期对冲
@@ -123,7 +125,6 @@ OKX_OPTIONS_DEFAULT_UNDERLY=ETH
OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED=true
OKX_OPTIONS_MAX_DTE_DAYS=2
OKX_OPTIONS_CHAIN_MAX_DTE_DAYS=14
OKX_SUB_ACCOUNT_NAME=
OKX_OPTIONS_ITM_MAX_DIST_USD=30
OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0
OKX_OPTIONS_POLL_SECONDS=15
+26 -13
View File
@@ -343,16 +343,29 @@ def _resolve_app_tz():
APP_TZ = _resolve_app_tz()
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
def _promote_legacy_options_api_keys() -> None:
"""1B: OKX_API_* 为空时,用废弃的 OKX_OPTIONS_API_* 回填到进程环境."""
if (os.getenv("OKX_API_KEY") or "").strip():
return
legacy_key = (os.getenv("OKX_OPTIONS_API_KEY") or "").strip()
legacy_secret = (os.getenv("OKX_OPTIONS_API_SECRET") or "").strip()
legacy_pass = (os.getenv("OKX_OPTIONS_API_PASSPHRASE") or "").strip()
if not (legacy_key and legacy_secret and legacy_pass):
return
os.environ["OKX_API_KEY"] = legacy_key
os.environ["OKX_API_SECRET"] = legacy_secret
os.environ["OKX_API_PASSPHRASE"] = legacy_pass
_promote_legacy_options_api_keys()
OKX_API_KEY = os.getenv("OKX_API_KEY", "")
OKX_API_SECRET = os.getenv("OKX_API_SECRET", "")
OKX_API_PASSPHRASE = os.getenv("OKX_API_PASSPHRASE", "")
OKX_OPTIONS_ENABLED = os.getenv("OKX_OPTIONS_ENABLED", "false").lower() in ("1", "true", "yes", "on")
OKX_OPTIONS_API_KEY = os.getenv("OKX_OPTIONS_API_KEY", "")
OKX_OPTIONS_API_SECRET = os.getenv("OKX_OPTIONS_API_SECRET", "")
OKX_OPTIONS_API_PASSPHRASE = os.getenv("OKX_OPTIONS_API_PASSPHRASE", "")
OKX_OPTIONS_TRADE_BUDGET_USDC = float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC", "10"))
OKX_OPTIONS_DEFAULT_UNDERLY = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper()
OKX_SUB_ACCOUNT_NAME = (os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip()
OKX_TD_MODE = os.getenv("OKX_TD_MODE", "cross")
OKX_POS_MODE = os.getenv("OKX_POS_MODE", "hedge")
EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "OKX").strip() or "OKX"
@@ -467,7 +480,7 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(ORDER_CHART_DIR, exist_ok=True)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
# 换成 OKX 永续
# 同一套 OKX_API_*:swap 客户端跑永续,option 客户端跑期权(身份相同,defaultType 不同)
exchange = ccxt.okx({
"enableRateLimit": True,
"options": {"defaultType": "swap"}, # OKX 用 swap 表示永续
@@ -487,10 +500,10 @@ exchange_options = ccxt.okx(
)
if OKX_CCXT_PROXIES:
exchange_options.proxies = OKX_CCXT_PROXIES
if OKX_OPTIONS_API_KEY and OKX_OPTIONS_API_SECRET and OKX_OPTIONS_API_PASSPHRASE:
exchange_options.apiKey = OKX_OPTIONS_API_KEY
exchange_options.secret = OKX_OPTIONS_API_SECRET
exchange_options.password = OKX_OPTIONS_API_PASSPHRASE
if OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE:
exchange_options.apiKey = OKX_API_KEY
exchange_options.secret = OKX_API_SECRET
exchange_options.password = OKX_API_PASSPHRASE
MARKETS_LOADED = False
ACCOUNT_BALANCE_CACHE = {
@@ -6755,8 +6768,8 @@ def render_main_page(page="trade", embed_mode=None):
current_capital if _show_perp_funds else None,
options_trading_usdc,
options_funding_usdc,
options_funding_usdt,
options_trading_usdt,
None,
None,
),
options_funding_usdc=options_funding_usdc,
options_funding_usdt=options_funding_usdt,
@@ -7097,8 +7110,8 @@ def api_account_snapshot():
current_capital if _show_perp_funds else None,
options_trading_usdc,
options_funding_usdc,
options_funding_usdt,
options_trading_usdt,
None,
None,
),
"available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None,
"unrealized_pnl": unrealized_pnl,
+4 -5
View File
@@ -61,14 +61,14 @@ Binance / Gate 无期权模块时,第三列最后一格不显示或显示「本
| 中文名 | 说明 | 重启 |
|--------|------|------|
| 开启实盘下单 | 关闭时仅走本地流程,不向交易所发单 | 需重启 |
| API Key | 永续子账户 API Key | 需重启 |
| API Secret | 永续子账户 Secret | 需重启 |
| API Key | 账户 API Key(永续+期权共用) | 需重启 |
| API Secret | 账户 API Secret | 需重启 |
| API Passphrase | 仅 OKX 显示 | 需重启 |
| 保证金模式 | 全仓 / 逐仓 | 需重启 |
| 持仓模式 | 双向 / 单向净持仓等(按所) | 需重启 |
| 仓位查询类型 | 仅 OKX:如 SWAP | 需重启 |
| 账户备注 | 企业微信推送中显示的交易所备注 | 保存即生效 |
| 显示永续资金 | 仅 OKX:关闭后顶栏隐藏永续资金账户与交易账户,总资金仅计期权侧 | 保存即生效 |
| 显示永续资金 | 仅 OKX:关闭后顶栏隐藏 USDT 资金/交易账户,总资金仅计期权 USDC 侧 | 保存即生效 |
**本卡片不包含**:网页登录账号密码,是否关闭登录校验,中控通信密钥.
@@ -170,8 +170,7 @@ AI 相关环境变量(`AI_PROVIDER`,`OPENAI_*`,`OLLAMA_*`,`AI_MODEL`,`AI_TIMEOUT
| 中文名 | 说明 |
|--------|------|
| 启用期权模块 | |
| 期权 API Key / Secret / Passphrase | 主账户,与永续子账户分离 |
| 启用期权模块 | 与永续共用 `OKX_API_*`;不再单独配置期权密钥 |
| 期权账户备注 | |
| 单笔预算(USDC) | |
| 预算缓冲比例 | |
+3 -3
View File
@@ -12,8 +12,8 @@
| 产品名 | 英文键 | 含义 |
|--------|--------|------|
| **永期对冲** | `perp_options` | 永续(子账户) + 买方期权(主账户) |
| **期期对冲** | `options_options` | 账户内两条买方期权腿 |
| **永期对冲** | `perp_options` | 同账户永续 + 买方期权 |
| **期期对冲** | `options_options` | 账户内两条买方期权腿 |
页面/导航展示用中文名;API/DB 用英文键.
@@ -217,7 +217,7 @@
| 侧 | 来源 |
|----|------|
| 永续行情/规格 | OKX 账户 ccxt:ticker + 现有 `/api/hub/market` 规格逻辑 |
| 永续行情/规格 | OKX 账户 ccxt:ticker + 现有 `/api/hub/market` 规格逻辑 |
| 期权链 | `build_option_chain` / `/api/options/chain`(本实例直连,无需中控代理) |
| 指数价 | 期权 `index_px`,左右对齐 |
+2 -2
View File
@@ -9,8 +9,8 @@
| 类型 | 账户 | 作用 |
|------|------|------|
| **永期对冲** | 永续子账户 + 期权主账户买方 | 全仓做方向,期权买保险 |
| **期期对冲** | 仅期权主账户双买方 | 目标价兑现盈利腿,亏损腿到期 |
| **永期对冲** | 同账户永续 + 期权买方 | 全仓做方向,期权买保险 |
| **期期对冲** | 账户双买方期权 | 目标价兑现盈利腿,亏损腿到期 |
### 永期结束与统计
+4 -4
View File
@@ -1,15 +1,15 @@
# 期权对冲方案分析
> 适用范围:OKX **永续子账户**(USDT 本位) + **期权主账户**(USDⓈ 本位买方).
> 适用范围:OKX **同一账户**(`OKX_API_*`):USDT 永续 + USDⓈ 期权买方.
> 本文档为 **策略与操盘说明**,非系统自动下单功能;组合须 **人工** 在永续页与期权页分别执行.
---
## 1. 前提与账户分工
| 维度 | 永续合约(子账户) | 期权(主账户) |
|------|------------------|--------------|
| API | `OKX_API_*` | `OKX_OPTIONS_API_*` |
| 维度 | 永续合约 | 期权 |
|------|----------|------|
| API | `OKX_API_*`(swap 客户端) | `OKX_API_*`(option 客户端) |
| 系统页面 | 实盘下单 / 关键位 / 策略 | 期权 |
| 保证金 | USDT | USDC / USDG |
| 本系统能力 | 开平仓、止损、关键位 | **仅买方** 开平仓,无组合单 |
+26 -99
View File
@@ -1,15 +1,15 @@
# OKX 期权模块 — 技术方案
> 适用范围:`crypto_monitor_okx` 实例;永续子账户并行,不新增 PM2 进程.
> 适用范围:`crypto_monitor_okx` 实例;永续与期权共用同一套 `OKX_API_*`,不新增 PM2 进程.
## 1. 目标
在现有 OKX 监控实例中增加 **USDⓈ 本位期权(买方)** 能力:
- 永续/关键位:继续走 **子账户 API-A**(现有 `OKX_API_*`)
- 期权:走 **主账户 API-B**(`OKX_OPTIONS_API_*`)
- 资金展示对齐 OKX:**资金账户 / 交易账户**,分币种显示 USDT,USDC,USDG
- 支持 **手动 USDT→USDC 兑换****USDC 账户划转**
- 永续/关键位与期权:**同一账户 API**(`OKX_API_*`)
- 两个 ccxt 客户端:`exchange`(defaultType=swap)与 `exchange_options`(defaultType=option),身份相同
- 顶栏资金:**资金账户/交易账户=USDT**;**期权资金/期权交易=USDC**
- 支持 **手动 USDT→USDC 兑换****账户划转**(无主↔子划转)
- **无总资金池上限**;单笔权利金上限可配置(默认 10 USDC)
## 2. 交易规则(硬约束)
@@ -32,8 +32,8 @@
```
crypto_okx(单 PM2)
├── exchange (swap) ← OKX_API_* 子账户
└── exchange_options ← OKX_OPTIONS_API_* 主账户
├── exchange (swap) ← OKX_API_*
└── exchange_options (option)← 同一套 OKX_API_*
lib/options/
├── okx_options_lib.py # 封装于 lib/exchange/
@@ -43,109 +43,36 @@ lib/options/
└── options_register.py # 路由 + 监控线程
```
**隔离:** 期权模块只调用 `exchange_options`;永续逻辑只调用 `exchange`.
**说明:** `defaultType` 分离避免错路由;密钥身份唯一.旧 `OKX_OPTIONS_API_*` 已废弃.
## 4. 资金与兑换
### 4.1 展示(期权页顶栏)
### 4.1 展示(实例顶栏)
| 账户 | 币种 |
|------|------|
| 资金账户 | USDT,USDC(若有) |
| 交易账户 | USDT,USDC,USDG(若有) |
- **资金账户 / 交易账户**:USDT(永续侧)
- **期权资金账户 / 期权交易账户**:USDC
- 总资金:USDT + USDC(1:1),同账户 USDT 不重复累加期权侧 USDT
不展示「练手池」等抽象记账名称.
### 4.2 兑换与划转
### 4.2 推荐操作流程
- 系统设置「币种兑换」:资金账户内 USDT ↔ USDC
- 「期权划转」:同账户 funding ↔ trading(USDC/USDT)
- **已移除**主↔子账户划转
```
资金账户 USDT
→ [手动兑换 USDT→USDC](OKX Convert API,资金账户内)
→ [划转到交易账户](USDC)
→ 交易账户 USDC
→ [限价买入期权]
```
### 4.3 API
| 接口 | OKX |
|------|-----|
| 余额 | `fetch_balance`(funding / trading)+ `GET /api/v5/asset/balances` |
| 询价兑换 | `POST /api/v5/asset/convert/estimate-quote` |
| 确认兑换 | `POST /api/v5/asset/convert/trade` |
| 划转 | `exchange.transfer(ccy, amt, from, to)` |
## 5. 配置项(`.env`)
## 5. 环境变量(要点)
```bash
OKX_OPTIONS_ENABLED=false
OKX_OPTIONS_API_KEY=
OKX_OPTIONS_API_SECRET=
OKX_OPTIONS_API_PASSPHRASE=
OKX_OPTIONS_ACCOUNT_LABEL=主账户·期权
OKX_API_KEY=
OKX_API_SECRET=
OKX_API_PASSPHRASE=
OKX_OPTIONS_ENABLED=true
OKX_OPTIONS_TRADE_BUDGET_USDC=10
OKX_OPTIONS_BUDGET_BUFFER=0.95
OKX_OPTIONS_DEFAULT_UNDERLY=ETH
OKX_OPTIONS_MAX_DTE_DAYS=2
OKX_OPTIONS_ITM_MAX_DIST_USD=30
OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0
OKX_OPTIONS_POLL_SECONDS=15
OKX_OPTIONS_TD_MODE=cross
# 市价平仓已在代码中硬关闭,此变量无效,可删
# OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
OKX_OPTIONS_CLOSE_RECYCLE_MULT=2
OKX_OPTIONS_CLOSE_HOLD_SECONDS=120
# 平仓限价挂单超时自动撤(秒),默认 600=10 分钟;联调可临时改 60
OKX_OPTIONS_PENDING_TTL_SECONDS=600
OKX_OPTIONS_ACCOUNT_LABEL=账户·期权
```
平仓执行:**只锁买一限价**,说明见 [期权开平仓与监控说明.md](./期权开平仓与监控说明.md);线上 `/options/guide`.
详见 [env配置说明.md](./env配置说明.md) 与 `.env.example`.
修改 `.env` 后须 `pm2 restart crypto_okx`.
## 6. 相关文档
## 6. 数据库
### `options_trades`
记录本地开仓/平仓,权利金,翻倍提醒状态.
### `options_convert_log` / `options_transfer_log`
可选记录兑换与划转操作.
## 7. HTTP 路由
| 方法 | 路径 |
|------|------|
| GET | `/options` |
| GET | `/options/guide` | 开平仓与监控说明(独立页) |
| GET | `/api/options/balances` |
| GET | `/api/options/chain` |
| GET | `/api/options/quote` |
| POST | `/api/options/open` |
| POST | `/api/options/close` |
| POST | `/api/options/convert/quote` |
| POST | `/api/options/convert/execute` |
| POST | `/api/options/transfer` |
| GET | `/api/options/positions` |
## 8. 分阶段交付
1. **基础设施**:双 API,余额,文档,设置页说明
2. **兑换 + 划转**:资金账户 USDT→USDC,划转到交易户
3. **交易**:链,报价,开平仓,持仓
4. **监控**:翻倍微信提醒
## 9. 不在一期范围
- 卖方,组合单,RFQ
- 自动 USDT↔USDC
- `manual-agent-okx` / 中控聚合
- 币本位期权
## 10. 安全
- 期权 API:**交易 + 读**,禁止提币
- 日志不输出 Secret
- 下单前校验 `client is exchange_options`
- [期权用法.md](./期权用法.md)
- [对冲计划开发方案.md](./对冲计划开发方案.md)
+23 -82
View File
@@ -2,37 +2,37 @@
## 1. 前置条件
1. OKX **主账户**已开通期权(USDⓈ 本位),且 App 中可见 `ETHUSD UM` / `BTCUSD UM`.
2.`crypto_monitor_okx/.env` 配置 **期权专用 API**(永续子账户分开):
1. OKX 账户已开通期权(USDⓈ 本位),且 App 中可见 `ETHUSD UM` / `BTCUSD UM`.
2.`crypto_monitor_okx/.env` 配置 **唯一账户 API**(永续与期权共用):
```bash
OKX_API_KEY=你的账户Key
OKX_API_SECRET=...
OKX_API_PASSPHRASE=...
OKX_OPTIONS_ENABLED=true
OKX_OPTIONS_API_KEY=你的主账户Key
OKX_OPTIONS_API_SECRET=...
OKX_OPTIONS_API_PASSPHRASE=...
```
3. 重启实例:`pm2 restart crypto_okx`
3. 重启实例:`pm2 restart crypto_okx --update-env`
> 永续仍用原有 `OKX_API_*`(子账户);期权只用 `OKX_OPTIONS_API_*`(主账户).
> 旧 `OKX_OPTIONS_API_*` 已废弃.若仅残留 OPTIONS 键而 `OKX_API_*` 为空,启动会自动回填.
## 2. 资金准备
期权权利金使用 **USDC 或 USDG**,不能直接用 USDT 买入.
期权权利金使用 **USDC**,不能直接用 USDT 买入.
### 推荐步骤
1. 打开 **期权** 页,查看顶栏:
- **资金账户**:USDT 余额
- **交易账户**:USDC 余额(买期权从这里扣)
1. 查看顶栏:
- **资金账户 / 交易账户**:USDT
- **期权资金账户 / 期权交易账户**:USDC
2. **币种兑换**(资金账户内)
- 从 USDT 兑换为 USDC
- 先点 **询价**,确认预估获得量后点 **确认兑换**
3. **账户划转**
3. **账户划转**
- 从:资金账户 → 到:交易账户
- 币种:USDC
- 将兑换得到的 USDC 划到交易账户
4. 确认 **交易账户 USDC** 足够支付本笔权利金
4. 确认 **期权交易账户 USDC** 足够支付本笔权利金
系统 **不会** 自动兑换或划转,避免误动资金.
@@ -85,82 +85,23 @@ OKX_OPTIONS_API_PASSPHRASE=...
## 6. 与永续 / 对冲计划的关系
| | 永续(子账户) | 期权(主账户) |
|--|----------------|----------------|
| API | `OKX_API_*` | `OKX_OPTIONS_API_*` |
| | 永续 | 期权 |
|--|------|------|
| API | `OKX_API_*`(同一套) | `OKX_API_*`(同一套) |
| 页面 | 实盘下单 / 关键位 | 期权 · 对冲计划 |
| 资金顶栏 | USDT 资金户+交易户 | 期权页单独显示 USDC 等 |
两套资金 **不合并** 显示.
| 顶栏 | USDT 资金户+交易户 | USDC 期权资金+期权交易 |
**期期对冲张数**(对冲计划页,与单独开期权共用预算算法):
| 模式 | 说明 |
|------|------|
| 同张数(默认) | 两腿同 `n`,总权利金 ≤ 预算 |
| 做多 | Call:Put 按主腿占比(默认 7:3) |
| 做空 | Put:Call 按主腿占比(默认 7:3) |
| 按比例 | 主腿/次腿按 `HEDGE_PLAN_OO_BIAS_*` |
拆分口径与比例见 env:`HEDGE_PLAN_OO_BIAS_SPLIT_BY`(`budget` 默认 / `sheets`=先算同张数总张数 `2n` 再拆)、`HEDGE_PLAN_OO_BIAS_RATIO`(默认 `0.7`)。细则见 [对冲计划开发方案.md](./对冲计划开发方案.md) §4.1、[系统说明.md](./系统说明.md)。
## 7. 常见问题
## 7. 配置说明
**Q:以前的期权专用密钥还要配吗?**
- 不需要.统一写到 `OKX_API_*`.
| 变量 | 默认 | 含义 |
|------|------|------|
| `OKX_OPTIONS_TRADE_BUDGET_USDC` | 10 | 单笔权利金上限 |
| `OKX_OPTIONS_BUDGET_BUFFER` | 0.95 | 算张数时预留 5% 缓冲 |
| `OKX_OPTIONS_MAX_DTE_DAYS` | 2 | 最多选几天内到期 |
| `OKX_OPTIONS_ITM_MAX_DIST_USD` | 30 | 轻度实值:价内不超过多少 USD |
| `OKX_OPTIONS_PROFIT_ALERT_RATIO` | 1.0 | 浮盈/权利金 ≥ 此值推送 |
| `HEDGE_PLAN_OO_BIAS_SPLIT_BY` | budget | 期期做多/做空:按预算或按张数拆 |
| `HEDGE_PLAN_OO_BIAS_RATIO` | 0.7 | 期期做多/做空主腿占比 |
## 8. 期权复盘(含对冲)
**OKX** 实例提供独立页 **期权复盘**(`/options/review`),与合约「交易记录与复盘」完全隔离.
### 数据来源
| 类型 | source_type | 来源 | 粒度 |
|------|-------------|------|------|
| 纯期权 | `option_spot` | 本地 `options_trades` 已平仓 | 一仓一条 |
| 永期对冲 | `perp_options` | 本地 `hedge_plans``status=closed` | **一计划一条** |
| 期期对冲 | `options_options` | 同上 | **一计划一条** |
- 打开复盘页即自动读取本地记录,**不访问交易所**.
- 对冲盈亏主口径:`realized_pnl_total`;详情另显永续/期权分项.
- 若某纯期权 `inst_id` 已出现在对冲腿中,默认标记排除,避免总盈亏双计.
- 人工复盘字段存在 `options_review_entries`,刷新本地源**不会覆盖**.
### 图片
- 目录:`static/images/options_journal/`
- 文件名:`options_journal_{draftId}_{5m|15m|1h|4h}.ext`(与合约复盘同周期槽位)
- 备份时与 `crypto.db` 一并打包即可;勿与合约 `journal_*` 截图混用.
### 页面
顶部三个 Tab:**期权交易记录** / **期期对冲记录** / **永期对冲记录**.点击列表行后在下方打开「复盘记录上传」,支持四周期即时截图与情绪标签.
### 统计
同页 KPI + 分组:类型、标的、策略标签、对冲结束原因、持有周期、Call/Put.策略维度仅统计已填策略标签的记录.
## 9. 常见问题
**Q:为什么买不了?**
- 交易账户 USDC 不足 → 先兑换再划转
- 卖一价过高,10U 预算买不到 1 张 → 选更便宜合约或提高 `OKX_OPTIONS_TRADE_BUDGET_USDC`
- 期权 API 未配置或 `OKX_OPTIONS_ENABLED=false`
**Q:报价 15 是每张 15U 吗?**
- 不是.15 是 **每 1 ETH** 的报价;每张(0.01 ETH)约 0.15 USDC.
**Q:子账户能开期权吗?**
- 本系统期权走主账户 API;子账户永续不受影响.
## 10. 风险说明
- 买方最大亏损为 **权利金**;近期实值仍会时间衰减
- 限价单可能因无流动性未成交
- 请先在小额下验证兑换,划转,开平仓全流程
**Q:还能主↔子划转吗?**
- 已移除.只保留同账户内划转与币种兑换.
+1 -1
View File
@@ -84,7 +84,7 @@
### 用途
**子账户永续** 场景下,于 **资金账户(funding)****交易账户(swap)** 之间手动划转 USDT.
**OKX 账户** 场景下,于 **资金账户(funding)****交易账户(swap)** 之间手动划转 USDT.
### 与 env 配置的关系
+1 -1
View File
@@ -284,7 +284,7 @@
setSettingsSubTabInUrl("transfer");
return "transfer";
}
if (path.indexOf("/api/options/transfer") >= 0 || path.indexOf("/api/options/cross-transfer") >= 0) {
if (path.indexOf("/api/options/transfer") >= 0) {
setSettingsSubTabInUrl("options_transfer");
return "options_transfer";
}
+2 -78
View File
@@ -6,7 +6,6 @@
const SWAP_BTNS = ["opt-set-swap-btn", "opt-set-swap-all-btn"];
const INT_BTNS = ["opt-set-int-btn", "opt-set-int-all-btn"];
const CROSS_BTNS = ["opt-set-cross-btn", "opt-set-cross-all-btn"];
async function apiJson(url, opts) {
const r = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
@@ -269,80 +268,6 @@
});
}
async function submitCrossTransfer(amount) {
setButtonsBusy(CROSS_BTNS, true, "划转中…");
setMsg("opt-set-cross-msg", "划转中…", false);
try {
const d = await apiJson("/api/options/cross-transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ccy: document.getElementById("opt-set-cross-ccy").value,
amount: amount,
from_account: document.getElementById("opt-set-cross-from").value,
to_account: document.getElementById("opt-set-cross-to").value,
direction: document.getElementById("opt-set-cross-dir").value,
}),
});
if (d.ok) {
setMsg("opt-set-cross-msg", "划转成功", false);
refreshFundsAfterMutation();
} else {
setMsg("opt-set-cross-msg", "划转失败:" + (d.msg || "未知错误"), true);
}
return d;
} catch (e) {
setMsg("opt-set-cross-msg", "划转失败:" + (e.message || "网络错误"), true);
return { ok: false };
} finally {
setButtonsBusy(CROSS_BTNS, false);
}
}
const crossBtn = document.getElementById("opt-set-cross-btn");
if (crossBtn) {
crossBtn.addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-set-cross-amount").value);
if (!amount || amount <= 0) {
setMsg("opt-set-cross-msg", "请输入有效数量", true);
return;
}
await submitCrossTransfer(amount);
});
}
const crossAllBtn = document.getElementById("opt-set-cross-all-btn");
if (crossAllBtn) {
crossAllBtn.addEventListener("click", async function () {
try {
const ccy = document.getElementById("opt-set-cross-ccy").value;
const from = document.getElementById("opt-set-cross-from").value;
const to = document.getElementById("opt-set-cross-to").value;
const direction = document.getElementById("opt-set-cross-dir").value;
const scope = direction === "sub_to_main" ? "sub" : "main";
const sideLabel = direction === "sub_to_main" ? "子账户" : "主账户";
const amount = await resolveMaxAmount(from, ccy, scope);
if (!amount) {
setMsg("opt-set-cross-msg", sideLabel + "划出账户可用余额不足", true);
return;
}
const msg =
"确认全部划转?\n\n" +
"方向:" + (direction === "main_to_sub" ? "主 → 子" : "子 → 主") + "\n" +
"币种:" + ccy + "\n" +
"划出:" + sideLabel + " · " + accountLabel(from) + "\n" +
"划入:" + (direction === "main_to_sub" ? "子账户" : "主账户") + " · " + accountLabel(to) + "\n" +
"金额:" + fmtAmt(amount, ccy) + "\n\n" +
"将划转该账户全部可用余额。";
if (!confirmOk(msg)) return;
document.getElementById("opt-set-cross-amount").value = String(amount);
await submitCrossTransfer(amount);
} catch (e) {
setMsg("opt-set-cross-msg", "划转失败:" + (e.message || "余额拉取失败"), true);
}
});
}
function hardenAmountAutofill(ids) {
ids.forEach(function (id) {
const el = document.getElementById(id);
@@ -366,7 +291,7 @@
}
// 全部划转/兑换前去掉 readonly,避免写不进数量
["opt-set-swap-all-btn", "opt-set-int-all-btn", "opt-set-cross-all-btn"].forEach(function (btnId) {
["opt-set-swap-all-btn", "opt-set-int-all-btn"].forEach(function (btnId) {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.addEventListener(
@@ -375,7 +300,6 @@
const map = {
"opt-set-swap-all-btn": "opt-set-swap-amount",
"opt-set-int-all-btn": "opt-set-int-amount",
"opt-set-cross-all-btn": "opt-set-cross-amount",
};
const input = document.getElementById(map[btnId]);
if (input) input.removeAttribute("readonly");
@@ -384,5 +308,5 @@
);
});
hardenAmountAutofill(["opt-set-swap-amount", "opt-set-int-amount", "opt-set-cross-amount"]);
hardenAmountAutofill(["opt-set-swap-amount", "opt-set-int-amount"]);
})();
+4 -7
View File
@@ -20,8 +20,8 @@ from lib.env.env_schema import (
_EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = {
"okx": [
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
("OKX_API_KEY", "API Key", "永续子账户"),
("OKX_API_SECRET", "API Secret", "永续子账户"),
("OKX_API_KEY", "API Key", "账户 API(永续+期权共用)"),
("OKX_API_SECRET", "API Secret", "账户 API(永续+期权共用)"),
("OKX_API_PASSPHRASE", "API Passphrase", "OKX 必填"),
("OKX_TD_MODE", "保证金模式", ""),
("OKX_POS_MODE", "持仓模式", ""),
@@ -30,7 +30,7 @@ _EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = {
(
"OKX_SHOW_PERP_FUNDS",
"显示永续资金",
"默认开启;关闭后顶栏隐藏永续资金账户与交易账户,总资金仅计期权侧",
"默认开启;关闭后顶栏隐藏 USDT 资金账户与交易账户,总资金仅计期权 USDC ",
),
],
"binance": [
@@ -140,10 +140,7 @@ _OPTIONS_SECTION: dict[str, Any] = {
"title": "期权账户",
"exchanges": frozenset({"okx"}),
"fields": [
("OKX_OPTIONS_ENABLED", "启用期权模块", ""),
("OKX_OPTIONS_API_KEY", "期权 API Key", "主账户,与永续子账户分离"),
("OKX_OPTIONS_API_SECRET", "期权 API Secret", ""),
("OKX_OPTIONS_API_PASSPHRASE", "期权 API Passphrase", ""),
("OKX_OPTIONS_ENABLED", "启用期权模块", "与永续共用上方 OKX_API_*;不再单独配置期权密钥"),
("OKX_OPTIONS_ACCOUNT_LABEL", "期权账户备注", ""),
("OKX_OPTIONS_TRADE_BUDGET_USDC", "单笔预算(USDC)", ""),
("OKX_OPTIONS_BUDGET_BUFFER", "预算缓冲比例", "如 0.95"),
+16 -44
View File
@@ -72,16 +72,22 @@ def td_mode_for_option_buy(configured: str | None = None) -> str:
def create_options_exchange(
api_key: str,
api_secret: str,
passphrase: str,
api_key: str = "",
api_secret: str = "",
passphrase: str = "",
proxies: dict[str, str] | None = None,
) -> ccxt.okx:
"""创建 option 客户端.未传密钥时读 OKX_API_*(与永续同源)."""
import os
key = (api_key or os.getenv("OKX_API_KEY") or "").strip()
secret = (api_secret or os.getenv("OKX_API_SECRET") or "").strip()
password = (passphrase or os.getenv("OKX_API_PASSPHRASE") or "").strip()
ex = ccxt.okx(
{
"apiKey": api_key,
"secret": api_secret,
"password": passphrase,
"apiKey": key,
"secret": secret,
"password": password,
"enableRateLimit": True,
"options": {"defaultType": "option"},
}
@@ -600,7 +606,10 @@ def options_header_balances(
*,
force: bool = False,
) -> tuple[float | None, float | None, float | None, float | None]:
"""顶栏四格:交易 USDC/USDT,资金 USDC/USDT(单次拉取 + 缓存)."""
"""顶栏期权两格用 USDC;顺带返回同账户 USDT(调用方勿再计入总资金,避免与永续栏重复).
返回:(trading_usdc, funding_usdc, funding_usdt, trading_usdt)
"""
bal = fetch_options_balances(ex, force=force)
def _round(v: Any) -> float | None:
@@ -1562,43 +1571,6 @@ def spot_market_swap_usdt_usdc(
return {"ok": False, "msg": _okx_trade_error_message(e)}
def transfer_main_sub_account(
ex: ccxt.okx,
*,
ccy: str,
amount: float,
sub_acct: str,
main_to_sub: bool,
from_account: str = "funding",
to_account: str = "funding",
) -> dict[str, Any]:
"""主账户与子账户之间划转(须主账户 API)."""
if amount <= 0:
return {"ok": False, "msg": "划转金额须大于 0"}
sub = (sub_acct or "").strip()
if not sub:
return {"ok": False, "msg": "未配置子账户名称 OKX_SUB_ACCOUNT_NAME"}
from_code = _OKX_ACCT_CODE.get((from_account or "funding").lower(), "6")
to_code = _OKX_ACCT_CODE.get((to_account or "funding").lower(), "6")
try:
resp = ex.private_post_asset_transfer(
{
"type": "1" if main_to_sub else "2",
"ccy": str(ccy).upper(),
"amt": str(amount),
"from": from_code,
"to": to_code,
"subAcct": sub,
}
)
data = (resp or {}).get("data") or []
if data and str(data[0].get("sCode", "0")) == "0":
return {"ok": True, "data": data[0], "raw": resp}
return {"ok": False, "msg": _okx_trade_error_message(resp=resp), "raw": resp}
except Exception as e:
return {"ok": False, "msg": _okx_trade_error_message(e)}
def format_position_row(
pos: dict[str, Any],
ct_mult: float = 0.01,
+8 -6
View File
@@ -104,12 +104,14 @@ def options_funding_label(
funding_usdc: float | None,
funding_usdt: float | None = None,
) -> str:
parts: list[str] = []
if funding_usdc is not None and float(funding_usdc) > 0:
parts.append(f"{float(funding_usdc):.2f} USDC")
if funding_usdt is not None and float(funding_usdt) > 0:
parts.append(f"{float(funding_usdt):.2f} USDT")
return " · ".join(parts) if parts else ""
"""期权侧顶栏仅展示 USDC(USDT 归永续资金/交易账户).funding_usdt 参数保留兼容,忽略."""
_ = funding_usdt
if funding_usdc is None:
return ""
try:
return f"{float(funding_usdc):.2f} USDC"
except (TypeError, ValueError):
return ""
def total_funds_usdt(
+5 -10
View File
@@ -158,24 +158,20 @@ def build_instance_settings_view(
)
if (exchange_key or "").strip().lower() == "okx" and _env_bool("OKX_OPTIONS_ENABLED", False):
opt_key = (os.getenv("OKX_OPTIONS_API_KEY") or "").strip()
api_key = (os.getenv("OKX_API_KEY") or "").strip()
sections.append(
{
"title": "期权设置",
"rows": [
_row("期权模块", "已启用"),
_row(
"期权 API",
f"已配置(…{opt_key[-4:]})" if len(opt_key) >= 4 else "未配置",
),
_row(
"子账户",
(os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip() or "未配置 OKX_SUB_ACCOUNT_NAME",
"主/子账户划转用",
"账户 API",
f"已配置(…{api_key[-4:]})" if len(api_key) >= 4 else "未配置 OKX_API_*",
"永续与期权共用 OKX_API_*",
),
_row(
"说明",
"币种兑换与账户划转到右侧「期权设置」卡片操作",
"币种兑换与账户划转到右侧「期权设置」卡片操作",
),
],
}
@@ -196,7 +192,6 @@ def build_instance_settings_view(
"show_transfer": (exchange_key or "").strip().lower() in ("gate", "binance", "okx"),
"options_settings_enabled": (exchange_key or "").strip().lower() == "okx"
and _env_bool("OKX_OPTIONS_ENABLED", False),
"options_sub_account": (os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip(),
"auto_transfer_enabled": auto_transfer_on,
"auto_transfer_bj_hour": _env_int("AUTO_TRANSFER_BJ_HOUR", 8),
"auto_transfer_amount": _env_float("AUTO_TRANSFER_AMOUNT", 30),
@@ -1137,10 +1137,11 @@ function paintRealtimePnlFromSnapshot(data){
}
function formatOptionsFundingLabel(usdc, usdt) {
const parts = [];
if (usdc !== null && usdc !== undefined && Number(usdc) > 0) parts.push(`${Number(usdc).toFixed(2)} USDC`);
if (usdt !== null && usdt !== undefined && Number(usdt) > 0) parts.push(`${Number(usdt).toFixed(2)} USDT`);
return parts.length ? parts.join(" · ") : "—";
// 期权侧顶栏仅 USDC;usdt 参数忽略(USDT 在永续资金/交易账户)
if (usdc === null || usdc === undefined || usdc === "") return "—";
const n = Number(usdc);
if (Number.isNaN(n)) return "—";
return `${n.toFixed(2)} USDC`;
}
function setFundsFieldText(field, text){
+5 -4
View File
@@ -1614,10 +1614,11 @@ function paintRealtimePnlFromSnapshot(data){
}
function formatOptionsFundingLabel(usdc, usdt) {
const parts = [];
if (usdc !== null && usdc !== undefined && Number(usdc) > 0) parts.push(`${Number(usdc).toFixed(2)} USDC`);
if (usdt !== null && usdt !== undefined && Number(usdt) > 0) parts.push(`${Number(usdt).toFixed(2)} USDT`);
return parts.length ? parts.join(" · ") : "—";
// 期权侧顶栏仅 USDC;usdt 参数忽略(USDT 在永续资金/交易账户)
if(usdc == null || usdc === "") return "—";
const n = Number(usdc);
if(Number.isNaN(n)) return "—";
return `${n.toFixed(2)} USDC`;
}
function setFundsFieldText(field, text){
@@ -35,11 +35,11 @@
{% if options_enabled %}
<div class="stat-strip-item">
<div class="label">期权资金账户</div>
<div class="value" id="options-funding-usdc" data-funds-field="options-funding-usdc">{{ options_funding_label(options_funding_usdc, options_funding_usdt) }}</div>
<div class="value" id="options-funding-usdc" data-funds-field="options-funding-usdc">{{ options_funding_label(options_funding_usdc) }}</div>
</div>
<div class="stat-strip-item">
<div class="label">期权交易账户</div>
<div class="value" id="options-trading-usdc" data-funds-field="options-trading-usdc">{{ options_funding_label(options_trading_usdc, options_trading_usdt) }}</div>
<div class="value" id="options-trading-usdc" data-funds-field="options-trading-usdc">{{ options_funding_label(options_trading_usdc) }}</div>
</div>
{% endif %}
<div class="stat-strip-item stat-strip-item--pnl">
+1 -1
View File
@@ -36,7 +36,7 @@
{% include 'password_settings_panel.html' %}
{% elif tab.key == 'transfer' %}
<h2>永续资金划转</h2>
<p class="settings-subcard-desc">账户永续:资金账户与交易账户之间划转 USDT.</p>
<p class="settings-subcard-desc">账户:资金账户与交易账户之间划转 USDT.</p>
{% include 'instance_transfer_panel.html' %}
{% elif tab.key == 'export' %}
<h2>数据导出</h2>
+1 -58
View File
@@ -92,12 +92,10 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
quote_option_contract,
spot_market_swap_usdt_usdc,
transfer_ccy,
transfer_main_sub_account,
)
return {
"enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
"sub_account_name": (os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip(),
"get_db": app_module.get_db,
"login_required": app_module.login_required,
"exchange_options": getattr(app_module, "exchange_options", None),
@@ -132,7 +130,6 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
"execute_convert": execute_convert,
"transfer_ccy": transfer_ccy,
"spot_market_swap_usdt_usdc": spot_market_swap_usdt_usdc,
"transfer_main_sub_account": transfer_main_sub_account,
"options_api_ready": options_api_ready,
"app_module": app_module,
}
@@ -357,13 +354,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
if ex is None:
return jsonify({"ok": False, "msg": err})
force = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes")
scope = (request.args.get("scope") or "main").strip().lower()
bal = cfg["fetch_options_balances"](
ex,
force=force,
scope=scope,
sub_acct=cfg.get("sub_account_name") or "",
)
bal = cfg["fetch_options_balances"](ex, force=force, scope="main")
return jsonify({"ok": True, **bal, "trade_budget": cfg["trade_budget"]})
@app.route("/api/options/chain")
@@ -1272,54 +1263,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
_mark_balances_stale(cfg)
return jsonify(result)
@app.route("/api/options/cross-transfer", methods=["POST"])
@lr
def api_options_cross_transfer():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
ccy = (data.get("ccy") or "USDT").upper()
direction = (data.get("direction") or "sub_to_main").strip()
from_account = (data.get("from_account") or data.get("account") or "funding").strip()
to_account = (data.get("to_account") or data.get("account") or "funding").strip()
try:
amount = float(data.get("amount"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "数量无效"})
main_to_sub = direction == "main_to_sub"
result = cfg["transfer_main_sub_account"](
ex,
ccy=ccy,
amount=amount,
sub_acct=cfg.get("sub_account_name") or "",
main_to_sub=main_to_sub,
from_account=from_account,
to_account=to_account,
)
if result.get("ok"):
conn = cfg["get_db"]()
try:
init_options_tables(conn)
conn.execute(
"""
INSERT INTO options_transfer_log (ccy, amount, from_account, to_account, status, message)
VALUES (?, ?, ?, ?, 'ok', ?)
""",
(
ccy,
amount,
("main" if main_to_sub else "sub") + ":" + from_account,
("sub" if main_to_sub else "main") + ":" + to_account,
"cross",
),
)
conn.commit()
finally:
conn.close()
_mark_balances_stale(cfg)
return jsonify(result)
@app.route("/api/options/history")
@lr
def api_options_history():
+1 -1
View File
@@ -4,7 +4,7 @@
data-trade-budget="{{ options_trade_budget | default(10) }}"
data-ask-liq-filter="{% if options_chain_ask_liq_filter is defined %}{{ '1' if options_chain_ask_liq_filter else '0' }}{% else %}1{% endif %}">
{% if not options_enabled %}
<div class="flash" style="margin-bottom:12px">期权 API 未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code>主账户 <code>OKX_OPTIONS_API_*</code>,然后 <code>pm2 restart crypto_okx --update-env</code>.</div>
<div class="flash" style="margin-bottom:12px">期权未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code><code>OKX_API_*</code>(永续与期权共用),然后 <code>pm2 restart crypto_okx --update-env</code>.</div>
{% endif %}
{% if options_enabled and options_open_allowed is defined and not options_open_allowed %}
<div class="flash" style="margin-bottom:12px">当前交易模式为对冲(永期/期期),不可单独开期权;持仓可在此查看/平仓.切换请到 env「交易模式」.</div>
@@ -1,4 +1,3 @@
{# 期权设置脚本挂载点(卡片在 settings_panel 中拆分) #}
<div id="options-settings-root" hidden
data-sub-account="{{ instance_settings.options_sub_account | default('', true) }}"></div>
<script src="/static/options_settings.js?v=9"></script>
<div id="options-settings-root" hidden></div>
<script src="/static/options_settings.js?v=10"></script>
@@ -1,5 +1,5 @@
<div class="options-settings-section">
<p class="options-settings-hint">账户资金账户:USDT ↔ USDC 现货市价单.</p>
<p class="options-settings-hint">账户资金账户:USDT ↔ USDC 现货市价单.</p>
<div class="form-row settings-transfer-form options-settings-row">
<input type="text" name="username" autocomplete="username" tabindex="-1" aria-hidden="true"
style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0" value="">
@@ -1,5 +1,5 @@
<div class="options-settings-section">
<div class="options-settings-subtitle">账户内</div>
<div class="options-settings-subtitle">账户内划转</div>
<div class="form-row settings-transfer-form options-settings-row">
<input type="text" name="username" autocomplete="username" tabindex="-1" aria-hidden="true"
style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0" value="">
@@ -22,35 +22,3 @@
</div>
<div id="opt-set-int-msg" class="options-settings-msg muted"></div>
</div>
<div class="options-settings-section">
<div class="options-settings-subtitle">
主子账户
<span class="muted">({{ instance_settings.options_sub_account or '未配置' }})</span>
</div>
<div class="form-row settings-transfer-form options-settings-row">
<input type="text" name="username" autocomplete="username" tabindex="-1" aria-hidden="true"
style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0" value="">
<select id="opt-set-cross-dir" aria-label="主子方向" autocomplete="off">
<option value="main_to_sub" selected>主 → 子</option>
<option value="sub_to_main">子 → 主</option>
</select>
<select id="opt-set-cross-ccy" aria-label="币种">
<option value="USDT" selected>USDT</option>
<option value="USDC">USDC</option>
</select>
<select id="opt-set-cross-from" aria-label="划出账户">
<option value="funding" selected>from: 资金</option>
<option value="trading">from: 交易</option>
</select>
<select id="opt-set-cross-to" aria-label="划入账户">
<option value="trading" selected>to: 交易</option>
<option value="funding">to: 资金</option>
</select>
<input type="number" id="opt-set-cross-amount" name="cm_opt_cross_amt" min="0.01" step="0.01" placeholder="数量"
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-bwignore="true" data-form-type="other" readonly>
<button type="button" class="btn-secondary btn-sm" id="opt-set-cross-all-btn">全部划转</button>
<button type="button" class="btn-primary btn-sm" id="opt-set-cross-btn">划转</button>
</div>
<div id="opt-set-cross-msg" class="options-settings-msg muted"></div>
</div>
+1 -1
View File
@@ -376,7 +376,7 @@
const hour = data && data.reset_hour != null ? data.reset_hour : 8;
if (elDescBody) {
elDescBody.textContent =
"总资金 = 各监控户(永续资金账户 + 交易账户 + 期权账户,USDC 按 1:1 计入 USDT);自 " +
"总资金 = 各监控户(同一 OKX 账户:USDT 资金/交易 + USDC 期权资金/交易,USDC 按 1:1 计入 USDT);自 " +
start +
" 起按北京时间 " +
hour +
@@ -112,6 +112,9 @@ class TestEnvSchema(unittest.TestCase):
self.assertEqual(by_key["OKX_SHOW_PERP_FUNDS"]["label"], "显示永续资金")
self.assertEqual(by_key["OKX_SHOW_PERP_FUNDS"]["type"], "bool")
self.assertTrue(by_key["OKX_SHOW_PERP_FUNDS"].get("hot_reload"))
self.assertNotIn("OKX_OPTIONS_API_KEY", by_key)
self.assertNotIn("OKX_SUB_ACCOUNT_NAME", by_key)
self.assertEqual(by_key["OKX_API_KEY"]["note"], "账户 API(永续+期权共用)")
groups_v = [{"title": "t", "fields": [by_key["TRADE_DIRECTION"]]}]
clean, errors = validate_env_updates(groups_v, {"TRADE_DIRECTION": "long_only"})
@@ -137,6 +140,13 @@ class TestShowPerpFunds(unittest.TestCase):
else:
os.environ["OKX_SHOW_PERP_FUNDS"] = old
def test_options_funding_label_usdc_only(self):
from lib.instance.instance_embed_context_lib import options_funding_label
self.assertEqual(options_funding_label(12.5, 99.0), "12.50 USDC")
self.assertEqual(options_funding_label(0.0, 50.0), "0.00 USDC")
self.assertEqual(options_funding_label(None, 10.0), "")
if __name__ == "__main__":
unittest.main()