Floor option close at intrinsic to fix expiry garbage quotes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 16:07:20 +08:00
parent b0dca59681
commit a58d97938c
5 changed files with 588 additions and 266 deletions
+96 -7
View File
@@ -12,7 +12,12 @@ from ..models.db import Database, get_db
from ..strategy.session import get_session
from .ledger import Ledger
from .liquidity import bid_covers_eth, bid_mark_ok, contracts_for_eth
from .pricing import option_fill, perp_fill
from .pricing import (
option_fill,
option_intrinsic,
perp_fill,
resolve_option_close_bid,
)
@dataclass(slots=True)
@@ -71,6 +76,61 @@ class Matcher:
)
return CloseResult(ok=False, detail=detail, liquidity_wait=True)
def _group_strike(self, group_id: str, option_inst_id: str) -> float | None:
g = self.db.fetchone(
"SELECT strike FROM groups WHERE group_id=?", (group_id,)
)
if g is not None and g["strike"] is not None:
try:
return float(g["strike"])
except (TypeError, ValueError):
pass
try:
from ..exchange.okx.parse import parse_option_inst_id
_, stk, _ = parse_option_inst_id(option_inst_id)
if stk is not None:
return float(stk)
except Exception:
pass
try:
from ..exchange.binance.parse import parse_option_symbol
_, stk, _ = parse_option_symbol(option_inst_id)
if stk is not None:
return float(stk)
except Exception:
pass
return None
def _close_spot_px(self, snap: Any) -> float | None:
if getattr(snap, "index_px", None) is not None:
try:
px = float(snap.index_px)
if px > 0:
return px
except (TypeError, ValueError):
pass
perp = getattr(snap, "perp", None)
if not perp:
return None
for attr in ("mark_px", "last"):
v = getattr(perp, attr, None)
if v is not None:
try:
px = float(v)
if px > 0:
return px
except (TypeError, ValueError):
pass
if perp.bid is not None and perp.ask is not None:
return (float(perp.bid) + float(perp.ask)) / 2.0
if perp.bid is not None:
return float(perp.bid)
if perp.ask is not None:
return float(perp.ask)
return None
def open_group(
self,
*,
@@ -269,7 +329,8 @@ class Matcher:
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
"""
全平一组。默认校验期权买一深度 + 买一/标记偏差(默认≤30%)。
bypass_liquidity=True:紧急全平可绕过(仍需有可用买一价才能成交;无买一时用标记近似)。
bypass_liquidity=True:紧急/到期可绕过深度闸门;平仓价取 max(买一, 标记, 内在价值),
避免到期垃圾盘口把实值期权按近零价卖掉。
成交顺序:先平期权(薄)→ 再瞬时平永续(对冲先留着);永续盘口失败则回滚期权入账。
"""
s = get_settings()
@@ -302,6 +363,14 @@ class Matcher:
)
close_bid = oq.bid
strike = self._group_strike(group_id, option_inst_id)
spot = self._close_spot_px(snap)
intrinsic: float | None = None
if strike is not None and spot is not None:
intrinsic = option_intrinsic(
option_side=option_side, strike=strike, spot=spot
)
if not bypass_liquidity:
if close_bid is None:
return self._liquidity_wait(group_id, "期权买一不可用")
@@ -316,12 +385,29 @@ class Matcher:
)
if not ok_dev:
return self._liquidity_wait(group_id, why)
resolved = resolve_option_close_bid(
bid=float(close_bid),
mark=oq.mark_px,
intrinsic=intrinsic,
bypass_liquidity=False,
)
if resolved is None:
return self._liquidity_wait(group_id, "期权平仓价不可用")
close_bid = resolved
else:
# 紧急:优先买一,否则用标记价近似成交(SIM)
if close_bid is None:
close_bid = oq.mark_px
if close_bid is None:
return CloseResult(ok=False, detail="紧急全平失败:无买一/标记价")
# 到期/紧急:买一/标记可能枯死,用 max(买一, 标记, 内在价值)
resolved = resolve_option_close_bid(
bid=close_bid,
mark=oq.mark_px,
intrinsic=intrinsic,
bypass_liquidity=True,
)
if resolved is None:
return CloseResult(
ok=False,
detail="紧急全平失败:无买一/标记/内在价值",
)
close_bid = resolved
fee_rate = self._fee_rate()
perp_side = str(pos["perp_side"])
@@ -455,6 +541,9 @@ class Matcher:
"net": net,
"close_sequence": ["option", "perp"],
"cash_delta": opt_cash + perp_pnl - pf.fee,
"option_close_bid": float(close_bid),
"option_intrinsic": intrinsic,
"settle_spot": spot,
},
)
+43
View File
@@ -17,6 +17,49 @@ class PriceResult:
return asdict(self)
def option_intrinsic(*, option_side: str, strike: float, spot: float) -> float:
"""多头期权内在价值(USDT/ETH)。call=max(SK,0)put=max(KS,0)。"""
s = float(spot)
k = float(strike)
side = str(option_side).lower().strip()
if side in ("call", "c"):
return max(s - k, 0.0)
if side in ("put", "p"):
return max(k - s, 0.0)
return 0.0
def resolve_option_close_bid(
*,
bid: float | None,
mark: float | None,
intrinsic: float | None,
bypass_liquidity: bool,
) -> float | None:
"""
平仓用买一价;多头卖出不得低于内在价值(SIM 防到期垃圾盘口)。
bypass 时:买一缺失可用标记/内在价值兜底。
"""
candidates: list[float] = []
if bid is not None and bid >= 0:
candidates.append(float(bid))
if bypass_liquidity and mark is not None and mark >= 0:
candidates.append(float(mark))
if intrinsic is not None and intrinsic >= 0:
candidates.append(float(intrinsic))
if not candidates:
return None
# 常规:有买一时,仍用 max(买一, 内在价值) 抬到合理底价
# bypassmax(买一, 标记, 内在价值)
if bypass_liquidity:
return max(candidates)
if bid is None:
return None
if intrinsic is not None and intrinsic >= 0:
return max(float(bid), float(intrinsic))
return float(bid)
def perp_fill(
*,
side: str,
+37
View File
@@ -142,3 +142,40 @@ def test_expiry_close() -> None:
assert d2.reason == "expiry"
d3 = check_expiry_close(expiry_ms=1_000, now_ms=1_001)
assert d3.should_close is True
def test_option_intrinsic_and_close_bid_floor() -> None:
from app.sim.pricing import option_intrinsic, resolve_option_close_bid
assert option_intrinsic(option_side="call", strike=1860, spot=1882) == 22.0
assert option_intrinsic(option_side="put", strike=1860, spot=1882) == 0.0
assert option_intrinsic(option_side="put", strike=1860, spot=1840) == 20.0
# 到期垃圾买一 0.2,内在价值 22 → 抬到 22
assert (
resolve_option_close_bid(
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=True
)
== 22.0
)
# 常规也有内在价值地板
assert (
resolve_option_close_bid(
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=False
)
== 22.0
)
# 买一高于内在价值,保留买一
assert (
resolve_option_close_bid(
bid=25.0, mark=24.0, intrinsic=22.0, bypass_liquidity=True
)
== 25.0
)
# bypass 无买一,用标记与内在价值
assert (
resolve_option_close_bid(
bid=None, mark=3.0, intrinsic=22.0, bypass_liquidity=True
)
== 22.0
)
+260 -259
View File
@@ -1,259 +1,260 @@
# ETH 永续 + 期权对冲策略说明
> 依据当前代码逻辑整理(SIM 默认真值参数)。
> 关联:[开发方案](./开发方案.md)、[商业化与授权方案](./商业化与授权方案.md)
> 更新:2026-07-25
---
## 1. 策略一句话
**ATM 期权买方** 表达方向弹性,用 **反向永续** 做对冲腿;波动大时争取多轮兑现净盈利,波动小时接受权利金磨损,**到期自动全平**。
本质是 **概率与样本**:不追求每天固定轮次,而按行情吃机会。
**行情来源可切换(SIM**:系统设置中可选 **OKX****币安** 公共行情;成交仍为本机模拟撮合,**不下真单**。
| 交易所 | 永续(默认) | 期权 | 说明 |
|--------|--------------|------|------|
| OKX | ETH-USDT-SWAP | ETH-USD_UM(偏 USDC 保证金族) | 当前默认 |
| 币安 | ETHUSDTUSDT-M | 欧洲期权 ETH-YYMMDD-行权价-C/PUSDT | 后期实盘优先候选 |
有持仓时不可切换交易所;切换后自动套用该所合约并重连行情。
---
## 2. 仓位结构(默认)
| 腿 | 默认数量 | 方向规则 | 成交方式 |
|----|----------|----------|----------|
| 永续 ETH-USDT-SWAP | **1 ETH** | 见下表 | 市价(吃买一/卖一 + 滑点) |
| 期权 ETH-USD_UM | **2 ETH** 名义 | 只买不卖 | 开仓吃卖一,平仓吃买一 |
**开仓顺序**:先成交期权 → 确认后再市价成交永续。永续盘口失败则回滚期权扣款,不留半边仓。
**平仓顺序**:同样先平期权(买一薄)→ 再瞬时平永续。平期权期间永续对冲先留着;永续盘口失败则回滚期权入账。
永续杠杆默认 **3×**(可配)。同时最多 **1 组**仓,禁止叠仓开下一组。
### 2.1 开仓方向(ATM 相对现价优先)
行权价相对标的有偏离时(币安粗档常见),**先按 ATM 偏上/偏下选向**;仅当行权价与标的贴平(≈)时,才回退卖一比价。
| 条件 | 期权 | 永续 | bias |
|------|------|------|------|
| ATM 行权价 **<** 标的(如 1850 vs 1859 | 买入 Call | 做空 | `strike_below_spot` |
| ATM 行权价 **>** 标的(如 1875 vs 1859 | 买入 Put | 做多 | `strike_above_spot` |
| ATM ≈ 标的,且 Call 卖一 > Put 卖一 | 买入 Call | 做空 | `call_ask_gt_put` |
| ATM ≈ 标的,且 Put 卖一 > Call 卖一 | 买入 Put | 做多 | `put_ask_gt_call` |
| ATM ≈ 标的且卖一相等 | 不开仓,继续等待 | — | — |
直觉:偏下行权价用 Call+空,偏上行权价用 Put+多;贴平时再按权利金溢价侧选向。
---
## 3. 开仓机制
### 3.1 流程概览
```
策略运行中
→ 无持仓且不在休息期
→ 非周末跳过(若开启)
→ 选到期:剩余时长 ≥ min_option_hours(默认 12h
→ 该到期 ATM 行权价(最接近标的)
→ 若开启 atm_open_offset_enabled|ATM 标的| ≤ max_atm_open_offset(默认 3)否则跳过
→ 选向:ATM 偏下→Call+空;偏上→Put+多;贴平→卖一比价
→ 期权杠杆 = 标的价 ÷ 卖一权利金 ≥ min_option_leverage(默认 100
→ 先开期权(吃卖一)→ 再市价开永续(一组)
→ 锁定 initial_premium = 期权成交价 × 期权名义(不含费)
```
### 3.2 选约约束
| 参数 | 默认 | 作用 |
|------|------|------|
| `min_option_hours` | 12 | 过滤过近到期,减少刚开仓就到期 |
| `min_option_leverage` | 100 | 权利金相对标的不能太贵(现价/卖一) |
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差限制开关(默认关) |
| `max_atm_open_offset` | 3 | 开关开启后:\|ATM − 标的\| 超过则不开 |
| ATM | — | 同到期、最接近指数/标记价的行权价 |
无合格合约时:状态停留等待,记录「无合格期权…」,不硬开。
### 3.3 时间与节奏
| 规则 | 说明 |
|------|------|
| 开仓时间窗 | **已取消**,全天可尝试(仍受选约约束) |
| 周六/周日 | 默认 **跳过新开仓**(上海时区,`skip_weekends`);持仓仍可平 |
| 每日轮次上限 | **不限制**;波动大可能多轮,波动小可能一轮都难 |
| 组间休息 | 全平后默认休息 **300 秒**`rest_seconds`)再开下一组 |
| 同时持仓 | 最多 1 组 |
### 3.4 费用(SIM
- 永续、期权均按可配 `fee_rate`(默认 0.0005)计费。
- 滑点按约 1 倍费率计入成交价,手续费另扣。
- 开仓现金:支付期权权利金 + 开仓手续费;永续开仓主要扣费。
---
## 4. 平仓机制
任一触发 → **该组永续 + 期权全部平掉**(先期权、后永续)。
### 4.1 净盈利达标(主出场)
**净盈利**口径(盯盘用):
```
净盈利 ≈ 永续浮盈 + 期权浮盈 − 预估平仓手续费
期权浮盈 = 当前买一 × 数量 − 初始权利金
```
| 出场模式 | 默认 | 触发条件 |
|----------|------|----------|
| `fixed_usdt` | 目标 **15 USDT** | 净盈利 ≥ `net_profit_target` |
| `premium_multiple` | 倍数 **1.0** | 净盈利 ≥ `initial_premium × premium_exit_multiple` |
说明:权利金倍数模式下,**1× = 赚回一倍权利金金额的净利**(例如权利金 20.41,目标约 20.41,不是 40.82)。
### 4.2 到期自动全平
- 到达期权到期时刻(OKXUTC 08:00 = **上海 16:00**)→ 原因 `expiry`
- **绕过**买一深度 / 买一相对标记偏差闸门(与紧急全平同类),尽量卖掉。
- 策略 **暂停时仍执行**,避免拖过期。
- 波动小、拖到到期:权利金亏损视为 **预算内成本**,可接受。
### 4.3 平仓流动性闸门(常规出场)
常规净盈利平仓时:
- 期权买一深度需覆盖平仓名义;
- 买一相对标记偏差默认 ≤ **30%**`close_bid_mark_max_pct`);
- 不满足 → `liquidity_wait`,继续等待,不改开仓。
**到期 / 紧急全平**:绕过上述闸门。
### 4.4 其它平仓入口
| 原因码 | 含义 |
|--------|------|
| `fixed_usdt` / `premium_multiple` | 净盈利达标 |
| `expiry` | 到期自动全平 |
| `emergency` | 界面紧急全平 |
| `manual` | 手动平仓 |
---
## 5. 策略侧重点
| 侧重点 | 说明 |
|--------|------|
| **吃波动,不赌横盘** | 买方期权需要标的走动;大波动利于达标出场 |
| **概率与轮次** | 波动日可能多轮;磨盘日可能零轮或一轮认亏 |
| **确定性时间规则** | 用周末跳过、到期全平,**不做**难以精准的横盘识别 |
| **费用后净利** | 出场看扣完预估平仓费后的净盈利,避免「账面赚、平完亏」 |
| **单组纪律** | 有仓不开下一组,防止重叠风险 |
| **权利金是成本预算** | 小波动拖到期亏权利金,属于策略设计内损耗,不是异常 bug |
适合对外表述:
> 适合有波动的时段;震荡磨盘是主要磨损源。不怕单边大波动本身,怕没波动却持续付时间价值。
---
## 6. 风险点评估
### 6.1 主要风险
| 风险 | 等级 | 说明 | 缓解(已有/建议) |
|------|------|------|------------------|
| **横盘 / 低波动** | 高 | Theta 消耗权利金;永续几乎不贡献利润 | 周末跳过;到期认亏全平;接受磨损日 |
| **方向选错** | 中高 | 卖一比价并非预测圣杯;错向时期权可能失利,永续端也可能承压 | 净盈利达标才兑现;到期强平止损时间 |
| **开平滑点与手续费** | 中 | 双边 taker + 滑点侵蚀小目标利润 | 目标默认 15U 需覆盖费用;勿把目标设过低 |
| **期权流动性** | 中 | 平仓买一薄、偏差大 → 等待 | 流动性闸门;到期/紧急可绕过 |
| **杠杆与保证金** | 中 | 永续 3×,标的急跌/急涨挤压保证金 | 控制名义、保留资金缓冲;实盘注意强平线 |
| **同到期 ATM 拥挤** | 低–中 | 临近到期价差与盘口恶化 | `min_option_hours`;到期强平 |
| **SIM≠实盘** | 高(若直接外推) | 本地撮合、深度与成交假设简化 | 小资金实盘验证后再放大 |
| **交易所限流 / 断连** | 中 | 行情中断导致延误开平 | 健康检查;部署侧监控 |
### 6.2 明确不做什么(边界)
- 不做卖方期权(不收权利金那套)。
- 不做横盘「智能识别」开仓过滤。
- 不做多组并行。
- 当前主路径为 **SIM**;实盘需另行授权与二次确认(见商业化方案)。
### 6.3 损益直觉
- **有利波动**:期权端弹性(2 ETH 名义)常快于永续(1 ETH)的对冲损耗 → 易达净利目标。
- **不利 / 无波动**:权利金贬值 + 费用 → 拖到到期全平,单组亏损量级约在 **权利金量级 + 双边费用**(再叠加永续期间浮亏/浮盈)。
---
## 7. 最小资金推荐
以下按 **默认仓位**(永续 1 ETH、期权 2 ETH 名义、杠杆 3×)估算,标的价按 **ETH ≈ 1,8002,200 USDT** 量级。
### 7.1 单组占用粗算
| 项目 | 约略金额(USDT) |
|------|------------------|
| 永续保证金(名义÷杠杆) | ≈ 1,800–2,200 ÷ 3 ≈ **600750** |
| 期权权利金(ATM、短期限,2 ETH) | 常见约 **1560**(行情波动大时更高) |
| 开平手续费 / 滑点缓冲 | **2050** |
| 逆向波动缓冲(避免过紧) | **300800**(经验缓冲,非精确 VaR |
### 7.2 建议档位
| 档位 | 建议权益(USDT) | 适用 |
|------|------------------|------|
| **绝对下限(不推荐)** | ≥ **1,500** | 仅能勉强覆盖保证金 + 小权利金,缓冲极薄,易心态与风控双紧 |
| **最小可用** | ≥ **3,000** | 默认参数下可跑 SIM / 极小实盘验证,仍偏紧 |
| **推荐起步** | ≥ **5,000** | 留出磨损日与波动缓冲,更符合「概率样本」玩法 |
| **较舒适** | ≥ **10,000** | 参数微调、连续多日磨损时更从容 |
> 缩放规则:若把永续改为 `0.5 ETH`、期权 `1 ETH`,资金需求大约可按比例减半,但费用占比会上升,小目标更难赚。
### 7.3 与出场目标的关系
- 默认净利目标 **15 USDT**:在费用后属于「小而频」目标,依赖波动日多次兑现。
- 单日/单组最坏常见情形更接近 **亏掉当期权利金 + 费用**(外加永续路径损益),资金规划应按 **多次磨损日** 而非「每组必赚 15」来留余量。
### 7.4 SIM 默认
系统默认虚拟权益 `initial_equity = 10,000` USDT(策略设置可改;保存且数值变更时在无持仓下重置账本),**不代表**实盘建议入金。
日后币安实盘试跑建议仓位:**永续 0.1 ETH / 期权 0.2 ETH 名义**,并同比下调净利目标。
---
## 8. 关键可配参数速查
| 参数 | 默认 | 类别 |
|------|------|------|
| `perp_qty_eth` | 1 | 仓位 |
| `option_qty_eth` | 2 | 仓位 |
| `leverage` | 3 | 永续 |
| `fee_rate` | 0.0005 | 成本 |
| `exit_mode` | fixed_usdt | 出场 |
| `net_profit_target` | 15 | 出场 |
| `premium_exit_multiple` | 1.0 | 出场 |
| `rest_seconds` | 300 | 节奏 |
| `skip_weekends` | true | 时间 |
| `min_option_hours` | 12 | 选约 |
| `min_option_leverage` | 100 | 选约 |
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差开关 |
| `max_atm_open_offset` | 3 | 开仓 ATM 偏差上限 |
| `close_bid_mark_max_pct` | 30 | 平仓流动性 |
---
## 9. 修订记录
| 日期 | 说明 |
|------|------|
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
# ETH 永续 + 期权对冲策略说明
> 依据当前代码逻辑整理(SIM 默认真值参数)。
> 关联:[开发方案](./开发方案.md)、[商业化与授权方案](./商业化与授权方案.md)
> 更新:2026-07-25
---
## 1. 策略一句话
**ATM 期权买方** 表达方向弹性,用 **反向永续** 做对冲腿;波动大时争取多轮兑现净盈利,波动小时接受权利金磨损,**到期自动全平**。
本质是 **概率与样本**:不追求每天固定轮次,而按行情吃机会。
**行情来源可切换(SIM**:系统设置中可选 **OKX****币安** 公共行情;成交仍为本机模拟撮合,**不下真单**。
| 交易所 | 永续(默认) | 期权 | 说明 |
|--------|--------------|------|------|
| OKX | ETH-USDT-SWAP | ETH-USD_UM(偏 USDC 保证金族) | 当前默认 |
| 币安 | ETHUSDTUSDT-M | 欧洲期权 ETH-YYMMDD-行权价-C/PUSDT | 后期实盘优先候选 |
有持仓时不可切换交易所;切换后自动套用该所合约并重连行情。
---
## 2. 仓位结构(默认)
| 腿 | 默认数量 | 方向规则 | 成交方式 |
|----|----------|----------|----------|
| 永续 ETH-USDT-SWAP | **1 ETH** | 见下表 | 市价(吃买一/卖一 + 滑点) |
| 期权 ETH-USD_UM | **2 ETH** 名义 | 只买不卖 | 开仓吃卖一,平仓吃买一 |
**开仓顺序**:先成交期权 → 确认后再市价成交永续。永续盘口失败则回滚期权扣款,不留半边仓。
**平仓顺序**:同样先平期权(买一薄)→ 再瞬时平永续。平期权期间永续对冲先留着;永续盘口失败则回滚期权入账。
永续杠杆默认 **3×**(可配)。同时最多 **1 组**仓,禁止叠仓开下一组。
### 2.1 开仓方向(ATM 相对现价优先)
行权价相对标的有偏离时(币安粗档常见),**先按 ATM 偏上/偏下选向**;仅当行权价与标的贴平(≈)时,才回退卖一比价。
| 条件 | 期权 | 永续 | bias |
|------|------|------|------|
| ATM 行权价 **<** 标的(如 1850 vs 1859 | 买入 Call | 做空 | `strike_below_spot` |
| ATM 行权价 **>** 标的(如 1875 vs 1859 | 买入 Put | 做多 | `strike_above_spot` |
| ATM ≈ 标的,且 Call 卖一 > Put 卖一 | 买入 Call | 做空 | `call_ask_gt_put` |
| ATM ≈ 标的,且 Put 卖一 > Call 卖一 | 买入 Put | 做多 | `put_ask_gt_call` |
| ATM ≈ 标的且卖一相等 | 不开仓,继续等待 | — | — |
直觉:偏下行权价用 Call+空,偏上行权价用 Put+多;贴平时再按权利金溢价侧选向。
---
## 3. 开仓机制
### 3.1 流程概览
```
策略运行中
→ 无持仓且不在休息期
→ 非周末跳过(若开启)
→ 选到期:剩余时长 ≥ min_option_hours(默认 12h
→ 该到期 ATM 行权价(最接近标的)
→ 若开启 atm_open_offset_enabled|ATM 标的| ≤ max_atm_open_offset(默认 3)否则跳过
→ 选向:ATM 偏下→Call+空;偏上→Put+多;贴平→卖一比价
→ 期权杠杆 = 标的价 ÷ 卖一权利金 ≥ min_option_leverage(默认 100
→ 先开期权(吃卖一)→ 再市价开永续(一组)
→ 锁定 initial_premium = 期权成交价 × 期权名义(不含费)
```
### 3.2 选约约束
| 参数 | 默认 | 作用 |
|------|------|------|
| `min_option_hours` | 12 | 过滤过近到期,减少刚开仓就到期 |
| `min_option_leverage` | 100 | 权利金相对标的不能太贵(现价/卖一) |
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差限制开关(默认关) |
| `max_atm_open_offset` | 3 | 开关开启后:\|ATM − 标的\| 超过则不开 |
| ATM | — | 同到期、最接近指数/标记价的行权价 |
无合格合约时:状态停留等待,记录「无合格期权…」,不硬开。
### 3.3 时间与节奏
| 规则 | 说明 |
|------|------|
| 开仓时间窗 | **已取消**,全天可尝试(仍受选约约束) |
| 周六/周日 | 默认 **跳过新开仓**(上海时区,`skip_weekends`);持仓仍可平 |
| 每日轮次上限 | **不限制**;波动大可能多轮,波动小可能一轮都难 |
| 组间休息 | 全平后默认休息 **300 秒**`rest_seconds`)再开下一组 |
| 同时持仓 | 最多 1 组 |
### 3.4 费用(SIM
- 永续、期权均按可配 `fee_rate`(默认 0.0005)计费。
- 滑点按约 1 倍费率计入成交价,手续费另扣。
- 开仓现金:支付期权权利金 + 开仓手续费;永续开仓主要扣费。
---
## 4. 平仓机制
任一触发 → **该组永续 + 期权全部平掉**(先期权、后永续)。
### 4.1 净盈利达标(主出场)
**净盈利**口径(盯盘用):
```
净盈利 ≈ 永续浮盈 + 期权浮盈 − 预估平仓手续费
期权浮盈 = 当前买一 × 数量 − 初始权利金
```
| 出场模式 | 默认 | 触发条件 |
|----------|------|----------|
| `fixed_usdt` | 目标 **15 USDT** | 净盈利 ≥ `net_profit_target` |
| `premium_multiple` | 倍数 **1.0** | 净盈利 ≥ `initial_premium × premium_exit_multiple` |
说明:权利金倍数模式下,**1× = 赚回一倍权利金金额的净利**(例如权利金 20.41,目标约 20.41,不是 40.82)。
### 4.2 到期自动全平
- 到达期权到期时刻(OKXUTC 08:00 = **上海 16:00**)→ 原因 `expiry`
- **绕过**买一深度 / 买一相对标记偏差闸门(与紧急全平同类),尽量卖掉。
- 策略 **暂停时仍执行**,避免拖过期。
- 波动小、拖到到期:权利金亏损视为 **预算内成本**,可接受。
### 4.3 平仓流动性闸门(常规出场)
常规净盈利平仓时:
- 期权买一深度需覆盖平仓名义;
- 买一相对标记偏差默认 ≤ **30%**`close_bid_mark_max_pct`);
- 不满足 → `liquidity_wait`,继续等待,不改开仓。
**到期 / 紧急全平**:绕过上述闸门;期权平仓价取 **max(买一, 标记, 内在价值)**,避免到期盘口枯死把实值期权按近零价结算
### 4.4 其它平仓入口
| 原因码 | 含义 |
|--------|------|
| `fixed_usdt` / `premium_multiple` | 净盈利达标 |
| `expiry` | 到期自动全平 |
| `emergency` | 界面紧急全平 |
| `manual` | 手动平仓 |
---
## 5. 策略侧重点
| 侧重点 | 说明 |
|--------|------|
| **吃波动,不赌横盘** | 买方期权需要标的走动;大波动利于达标出场 |
| **概率与轮次** | 波动日可能多轮;磨盘日可能零轮或一轮认亏 |
| **确定性时间规则** | 用周末跳过、到期全平,**不做**难以精准的横盘识别 |
| **费用后净利** | 出场看扣完预估平仓费后的净盈利,避免「账面赚、平完亏」 |
| **单组纪律** | 有仓不开下一组,防止重叠风险 |
| **权利金是成本预算** | 小波动拖到期亏权利金,属于策略设计内损耗,不是异常 bug |
适合对外表述:
> 适合有波动的时段;震荡磨盘是主要磨损源。不怕单边大波动本身,怕没波动却持续付时间价值。
---
## 6. 风险点评估
### 6.1 主要风险
| 风险 | 等级 | 说明 | 缓解(已有/建议) |
|------|------|------|------------------|
| **横盘 / 低波动** | 高 | Theta 消耗权利金;永续几乎不贡献利润 | 周末跳过;到期认亏全平;接受磨损日 |
| **方向选错** | 中高 | 卖一比价并非预测圣杯;错向时期权可能失利,永续端也可能承压 | 净盈利达标才兑现;到期强平止损时间 |
| **开平滑点与手续费** | 中 | 双边 taker + 滑点侵蚀小目标利润 | 目标默认 15U 需覆盖费用;勿把目标设过低 |
| **期权流动性** | 中 | 平仓买一薄、偏差大 → 等待 | 流动性闸门;到期/紧急可绕过 |
| **杠杆与保证金** | 中 | 永续 3×,标的急跌/急涨挤压保证金 | 控制名义、保留资金缓冲;实盘注意强平线 |
| **同到期 ATM 拥挤** | 低–中 | 临近到期价差与盘口恶化 | `min_option_hours`;到期强平 |
| **SIM≠实盘** | 高(若直接外推) | 本地撮合、深度与成交假设简化 | 小资金实盘验证后再放大 |
| **交易所限流 / 断连** | 中 | 行情中断导致延误开平 | 健康检查;部署侧监控 |
### 6.2 明确不做什么(边界)
- 不做卖方期权(不收权利金那套)。
- 不做横盘「智能识别」开仓过滤。
- 不做多组并行。
- 当前主路径为 **SIM**;实盘需另行授权与二次确认(见商业化方案)。
### 6.3 损益直觉
- **有利波动**:期权端弹性(2 ETH 名义)常快于永续(1 ETH)的对冲损耗 → 易达净利目标。
- **不利 / 无波动**:权利金贬值 + 费用 → 拖到到期全平,单组亏损量级约在 **权利金量级 + 双边费用**(再叠加永续期间浮亏/浮盈)。
---
## 7. 最小资金推荐
以下按 **默认仓位**(永续 1 ETH、期权 2 ETH 名义、杠杆 3×)估算,标的价按 **ETH ≈ 1,8002,200 USDT** 量级。
### 7.1 单组占用粗算
| 项目 | 约略金额(USDT) |
|------|------------------|
| 永续保证金(名义÷杠杆) | ≈ 1,800–2,200 ÷ 3 ≈ **600750** |
| 期权权利金(ATM、短期限,2 ETH) | 常见约 **1560**(行情波动大时更高) |
| 开平手续费 / 滑点缓冲 | **2050** |
| 逆向波动缓冲(避免过紧) | **300800**(经验缓冲,非精确 VaR |
### 7.2 建议档位
| 档位 | 建议权益(USDT) | 适用 |
|------|------------------|------|
| **绝对下限(不推荐)** | ≥ **1,500** | 仅能勉强覆盖保证金 + 小权利金,缓冲极薄,易心态与风控双紧 |
| **最小可用** | ≥ **3,000** | 默认参数下可跑 SIM / 极小实盘验证,仍偏紧 |
| **推荐起步** | ≥ **5,000** | 留出磨损日与波动缓冲,更符合「概率样本」玩法 |
| **较舒适** | ≥ **10,000** | 参数微调、连续多日磨损时更从容 |
> 缩放规则:若把永续改为 `0.5 ETH`、期权 `1 ETH`,资金需求大约可按比例减半,但费用占比会上升,小目标更难赚。
### 7.3 与出场目标的关系
- 默认净利目标 **15 USDT**:在费用后属于「小而频」目标,依赖波动日多次兑现。
- 单日/单组最坏常见情形更接近 **亏掉当期权利金 + 费用**(外加永续路径损益),资金规划应按 **多次磨损日** 而非「每组必赚 15」来留余量。
### 7.4 SIM 默认
系统默认虚拟权益 `initial_equity = 10,000` USDT(策略设置可改;保存且数值变更时在无持仓下重置账本),**不代表**实盘建议入金。
日后币安实盘试跑建议仓位:**永续 0.1 ETH / 期权 0.2 ETH 名义**,并同比下调净利目标。
---
## 8. 关键可配参数速查
| 参数 | 默认 | 类别 |
|------|------|------|
| `perp_qty_eth` | 1 | 仓位 |
| `option_qty_eth` | 2 | 仓位 |
| `leverage` | 3 | 永续 |
| `fee_rate` | 0.0005 | 成本 |
| `exit_mode` | fixed_usdt | 出场 |
| `net_profit_target` | 15 | 出场 |
| `premium_exit_multiple` | 1.0 | 出场 |
| `rest_seconds` | 300 | 节奏 |
| `skip_weekends` | true | 时间 |
| `min_option_hours` | 12 | 选约 |
| `min_option_leverage` | 100 | 选约 |
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差开关 |
| `max_atm_open_offset` | 3 | 开仓 ATM 偏差上限 |
| `close_bid_mark_max_pct` | 30 | 平仓流动性 |
---
## 9. 修订记录
| 日期 | 说明 |
|------|------|
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
| 2026-07-26 | 到期/紧急平仓:期权价不低于内在价值,修复垃圾盘口错杀实值 |
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""一键修复服务器上因到期垃圾盘口导致的期权错结算(G-20260725-01)。
用法(本机):
$env:DEPLOY_PASS='...'
python scripts/repair_expiry_settle.py
"""
from __future__ import annotations
import os
import sys
import time
HOST = os.environ.get("DEPLOY_HOST", "47.236.184.99")
USER = os.environ.get("DEPLOY_USER", "root")
PASSWORD = os.environ.get("DEPLOY_PASS", "")
ROOT = os.environ.get("DEPLOY_ROOT", "/opt/eth_hedge_sim")
DB = f"{ROOT}/backend/data/hedge.db"
GROUP = os.environ.get("REPAIR_GROUP_ID", "G-20260725-01")
REMOTE_PY = r'''
import sqlite3, time
DB = "%(db)s"
GROUP = "%(group)s"
FEE = 0.0005
con = sqlite3.connect(DB)
con.row_factory = sqlite3.Row
g = con.execute("SELECT * FROM groups WHERE group_id=?", (GROUP,)).fetchone()
if not g:
print("group missing", GROUP)
raise SystemExit(1)
if g["status"] != "closed" or g["close_reason"] != "expiry":
print("skip: not closed expiry", dict(g))
raise SystemExit(0)
fills = list(con.execute(
"SELECT * FROM fills WHERE group_id=? ORDER BY ts_ms", (GROUP,)
).fetchall())
o_open = next(f for f in fills if f["leg"]=="option" and f["action"]=="open")
o_close = next(f for f in fills if f["leg"]=="option" and f["action"]=="close")
p_open = next(f for f in fills if f["leg"]=="perp" and f["action"]=="open")
p_close = next(f for f in fills if f["leg"]=="perp" and f["action"]=="close")
strike = float(g["strike"] or 1860)
# 永续平仓为买回空头:fill≈ask*(1+f) → spot≈ask
perp_fill = float(p_close["fill_px"])
spot = perp_fill / (1.0 + FEE)
intrinsic = max(spot - strike, 0.0) if str(g["option_side"])=="call" else max(strike - spot, 0.0)
old_base = float(o_close["base_px"] or o_close["fill_px"])
new_base = max(old_base, intrinsic)
if abs(new_base - old_base) < 1e-9:
print("already ok", old_base, intrinsic)
raise SystemExit(0)
qty = float(o_close["qty_eth"])
new_fill = new_base * (1.0 - FEE)
new_notional = new_fill * qty
new_fee = new_notional * FEE
new_slip = abs(new_fill - new_base) * qty
old_cash = float(o_close["notional"]) - float(o_close["fee"])
new_cash = new_notional - new_fee
cash_delta = new_cash - old_cash
opt_entry = float(o_open["fill_px"])
opt_pnl = (new_fill - opt_entry) * qty
perp_entry = float(p_open["fill_px"])
perp_side = str(p_open["side"])
if perp_side == "long":
perp_pnl = (float(p_close["fill_px"]) - perp_entry) * float(p_open["qty_eth"])
else:
perp_pnl = (perp_entry - float(p_close["fill_px"])) * float(p_open["qty_eth"])
net = perp_pnl + opt_pnl - float(p_close["fee"]) - new_fee
# fees on group: replace option close fee contribution
old_opt_fee = float(o_close["fee"])
old_opt_slip = float(o_close["slip"] or 0)
fees = float(g["fees"] or 0) - old_opt_fee + new_fee
slip = float(g["slip_cost"] or 0) - old_opt_slip + new_slip
now = int(time.time() * 1000)
meta = con.execute("SELECT equity, available FROM ledger_meta WHERE id=1").fetchone()
eq = float(meta["equity"]) + cash_delta
av = float(meta["available"]) + cash_delta
con.execute("BEGIN")
con.execute(
"UPDATE fills SET base_px=?, fill_px=?, fee=?, slip=?, notional=? WHERE group_id=? AND leg='option' AND action='close'",
(new_base, new_fill, new_fee, new_slip, new_notional, GROUP),
)
con.execute(
"UPDATE groups SET realized_pnl=?, fees=?, slip_cost=?, note=? WHERE group_id=?",
(net, fees, slip, f"repaired_intrinsic:{intrinsic:.4f}", GROUP),
)
con.execute(
"UPDATE ledger_meta SET equity=?, available=?, updated_at_ms=? WHERE id=1",
(eq, av, now),
)
con.execute(
"INSERT INTO ledger_entries(group_id, kind, amount, balance_after, note, ts_ms) VALUES (?,?,?,?,?,?)",
(GROUP, "repair_option_intrinsic", cash_delta, eq,
f"repair {GROUP}: option close {old_base:.4f}->{new_base:.4f} intrinsic={intrinsic:.4f}", now),
)
# also fix close_option entry amount if present
row = con.execute(
"SELECT id, amount FROM ledger_entries WHERE group_id=? AND kind='close_option' ORDER BY id DESC LIMIT 1",
(GROUP,),
).fetchone()
if row:
con.execute(
"UPDATE ledger_entries SET amount=?, note=? WHERE id=?",
(new_cash, f"close option expiry (repaired intrinsic {intrinsic:.4f})", row["id"]),
)
con.commit()
print({
"group": GROUP,
"spot": spot,
"intrinsic": intrinsic,
"old_base": old_base,
"new_base": new_base,
"cash_delta": cash_delta,
"new_equity": eq,
"new_realized_pnl": net,
})
''' % {"db": DB, "group": GROUP}
def main() -> int:
if not PASSWORD:
print("Set DEPLOY_PASS", file=sys.stderr)
return 2
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(HOST, username=USER, password=PASSWORD, timeout=30)
cmd = "python3 - <<'PY'\n" + REMOTE_PY + "\nPY"
_, stdout, stderr = client.exec_command(cmd)
out = stdout.read().decode("utf-8", errors="replace")
err = stderr.read().decode("utf-8", errors="replace")
code = stdout.channel.recv_exit_status()
print(out)
if err:
print(err, file=sys.stderr)
client.close()
return code
if __name__ == "__main__":
raise SystemExit(main())