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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -9,8 +9,8 @@
|
||||
|
||||
| 类型 | 账户 | 作用 |
|
||||
|------|------|------|
|
||||
| **永期对冲** | 永续子账户 + 期权主账户买方 | 全仓做方向,期权买保险 |
|
||||
| **期期对冲** | 仅期权主账户双买方 | 目标价兑现盈利腿,亏损腿到期 |
|
||||
| **永期对冲** | 同账户永续 + 期权买方 | 全仓做方向,期权买保险 |
|
||||
| **期期对冲** | 同账户双买方期权 | 目标价兑现盈利腿,亏损腿到期 |
|
||||
|
||||
### 永期结束与统计
|
||||
|
||||
|
||||
+4
-4
@@ -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
@@ -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
@@ -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
@@ -84,7 +84,7 @@
|
||||
|
||||
### 用途
|
||||
|
||||
在 **子账户永续** 场景下,于 **资金账户(funding)** 与 **交易账户(swap)** 之间手动划转 USDT.
|
||||
在 **OKX 账户** 场景下,于 **资金账户(funding)** 与 **交易账户(swap)** 之间手动划转 USDT.
|
||||
|
||||
### 与 env 配置的关系
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
})();
|
||||
|
||||
Vendored
+4
-7
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user