Add configurable SIM equity (default 10k) and strategy docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-25 11:43:51 +08:00
parent b50c134f5b
commit 5dcec0fde0
9 changed files with 308 additions and 6 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ OPTION_INST_FAMILY=ETH-USD_UM
INDEX_INST_ID=ETH-USD INDEX_INST_ID=ETH-USD
FEE_RATE=0.0005 FEE_RATE=0.0005
INITIAL_EQUITY=100000 INITIAL_EQUITY=10000
LEVERAGE=3 LEVERAGE=3
MIN_OPTION_HOURS=12 MIN_OPTION_HOURS=12
MIN_OPTION_LEVERAGE=100 MIN_OPTION_LEVERAGE=100
+1
View File
@@ -10,6 +10,7 @@
- [开发方案](docs/开发方案.md) - [开发方案](docs/开发方案.md)
- [代码结构](docs/代码结构.md) - [代码结构](docs/代码结构.md)
- [商业化与授权方案](docs/商业化与授权方案.md) - [商业化与授权方案](docs/商业化与授权方案.md)
- [策略说明](docs/策略说明.md)
## 访问(测试机) ## 访问(测试机)
+22 -2
View File
@@ -2,12 +2,13 @@ from __future__ import annotations
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ..config import get_settings from ..config import get_settings
from ..models.db import get_db from ..models.db import get_db
from ..sim.ledger import Ledger from ..sim.ledger import Ledger
from ..sim.matcher import Matcher
from .auth import require_user from .auth import require_user
router = APIRouter(prefix="/api/settings", tags=["settings"]) router = APIRouter(prefix="/api/settings", tags=["settings"])
@@ -38,7 +39,7 @@ class StrategySettingsBody(BaseModel):
premium_exit_multiple: float | None = Field(default=None, ge=0.1, le=100) premium_exit_multiple: float | None = Field(default=None, ge=0.1, le=100)
rest_seconds: int | None = Field(default=None, ge=0, le=3600) rest_seconds: int | None = Field(default=None, ge=0, le=3600)
skip_weekends: bool | None = None skip_weekends: bool | None = None
initial_equity: float | None = Field(default=None, ge=1000) initial_equity: float | None = Field(default=None, ge=1000, le=10_000_000)
leverage: float | None = Field(default=None, ge=1, le=125) leverage: float | None = Field(default=None, ge=1, le=125)
min_option_hours: float | None = Field(default=None, ge=1, le=720) min_option_hours: float | None = Field(default=None, ge=1, le=720)
min_option_leverage: float | None = Field(default=None, ge=1, le=10000) min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
@@ -116,8 +117,27 @@ async def put_strategy_settings(
_user: Annotated[str, Depends(require_user)], _user: Annotated[str, Depends(require_user)],
) -> dict: ) -> dict:
db = get_db() db = get_db()
s = get_settings()
data = body.model_dump(exclude_none=True) data = body.model_dump(exclude_none=True)
equity_to_apply: float | None = None
if "initial_equity" in data:
new_eq = float(data["initial_equity"])
old_eq = float(
db.get_setting("initial_equity", str(s.initial_equity)) or s.initial_equity
)
if abs(new_eq - old_eq) > 1e-9:
if Matcher(db).has_open_position():
raise HTTPException(
status_code=409,
detail="有未平仓,无法重置模拟资金;请先平仓后再改",
)
equity_to_apply = new_eq
for k, v in data.items(): for k, v in data.items():
if k in KEYS: if k in KEYS:
db.set_setting(k, str(v)) db.set_setting(k, str(v))
if equity_to_apply is not None:
Ledger(db).reset_equity(
equity_to_apply,
note=f"设置模拟资金={equity_to_apply:.2f}",
)
return _read_settings() return _read_settings()
+1 -1
View File
@@ -38,7 +38,7 @@ class Settings(BaseSettings):
index_inst_id: str = "ETH-USD" index_inst_id: str = "ETH-USD"
fee_rate: float = 0.0005 fee_rate: float = 0.0005
initial_equity: float = 100_000.0 initial_equity: float = 10_000.0 # SIM 模拟初始资金(USDT),设置页可改
max_rounds: int = 3 # 已不再强管控,仅兼容旧字段 max_rounds: int = 3 # 已不再强管控,仅兼容旧字段
open_hhmm: str = "16:00" # 已废弃开仓窗 open_hhmm: str = "16:00" # 已废弃开仓窗
stop_open_hhmm: str = "08:00" # 已废弃开仓窗 stop_open_hhmm: str = "08:00" # 已废弃开仓窗
+18
View File
@@ -48,6 +48,24 @@ class Ledger:
self.db._conn.commit() self.db._conn.commit()
return equity return equity
def reset_equity(self, amount: float, *, note: str = "重置模拟资金") -> float:
"""将权益与可用资金重置为 amount(reserved 清零)。须在无持仓时调用。"""
now = int(time.time() * 1000)
amt = float(amount)
if amt < 0:
raise ValueError("模拟资金不能为负")
with self.db._lock:
self.db._conn.execute(
"UPDATE ledger_meta SET equity=?, available=?, reserved=0, updated_at_ms=? WHERE id=1",
(amt, amt, now),
)
self.db._conn.execute(
"INSERT INTO ledger_entries(group_id, kind, amount, balance_after, note, ts_ms) VALUES (?,?,?,?,?,?)",
(None, "reset", amt, amt, note, now),
)
self.db._conn.commit()
return amt
def get_setting_float(self, key: str, default: float) -> float: def get_setting_float(self, key: str, default: float) -> float:
v = self.db.get_setting(key) v = self.db.get_setting(key)
if v is None or v == "": if v is None or v == "":
+1 -1
View File
@@ -186,7 +186,7 @@ OKX_API_KEY=... # 模拟阶段只需能拉行情的权限
OKX_API_SECRET=... OKX_API_SECRET=...
OKX_API_PASSPHRASE=... OKX_API_PASSPHRASE=...
FEE_RATE=0.0005 # 示例;滑点 = 1 × FEE_RATE FEE_RATE=0.0005 # 示例;滑点 = 1 × FEE_RATE
INITIAL_EQUITY=100000 INITIAL_EQUITY=10000
TZ=Asia/Shanghai TZ=Asia/Shanghai
``` ```
+236
View File
@@ -0,0 +1,236 @@
# ETH 永续 + 期权对冲策略说明
> 依据当前代码逻辑整理(SIM 默认真值参数)。
> 关联:[开发方案](./开发方案.md)、[商业化与授权方案](./商业化与授权方案.md)
> 更新:2026-07-25
---
## 1. 策略一句话
**ATM 期权买方** 表达方向弹性,用 **反向永续** 做对冲腿;波动大时争取多轮兑现净盈利,波动小时接受权利金磨损,**到期自动全平**。
本质是 **概率与样本**:不追求每天固定轮次,而按行情吃机会。
---
## 2. 仓位结构(默认)
| 腿 | 默认数量 | 方向规则 | 成交方式 |
|----|----------|----------|----------|
| 永续 ETH-USDT-SWAP | **1 ETH** | 见下表 | 市价(吃买一/卖一 + 滑点) |
| 期权 ETH-USD_UM | **2 ETH** 名义 | 只买不卖 | 开仓吃卖一,平仓吃买一 |
永续杠杆默认 **3×**(可配)。同时最多 **1 组**仓,禁止叠仓开下一组。
### 2.1 开仓方向(卖一比价)
比较同一 ATM 行权价的 **Call 卖一****Put 卖一**
| 条件 | 期权 | 永续 |
|------|------|------|
| Call 卖一 > Put 卖一 | 买入 Call | 做空 |
| Call 卖一 < Put 卖一 | 买入 Put | 做多 |
| 相等 | 不开仓,继续等待 | — |
直觉:卖一更高的一侧往往隐含更强的短期方向/溢价偏好,买入该侧期权;永续反向以平衡部分 Delta。
---
## 3. 开仓机制
### 3.1 流程概览
```
策略运行中
→ 无持仓且不在休息期
→ 非周末跳过(若开启)
→ 选到期:剩余时长 ≥ min_option_hours(默认 12h
→ 该到期 ATM 行权价(最接近标的)
→ Call/Put 卖一比价选方向
→ 期权杠杆 = 标的价 ÷ 卖一权利金 ≥ min_option_leverage(默认 100
→ 开永续 + 开期权(一组)
→ 锁定 initial_premium = 期权成交价 × 期权名义(不含费)
```
### 3.2 选约约束
| 参数 | 默认 | 作用 |
|------|------|------|
| `min_option_hours` | 12 | 过滤过近到期,减少刚开仓就到期 |
| `min_option_leverage` | 100 | 权利金相对标的不能太贵(现价/卖一) |
| 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(策略设置可改;保存且数值变更时在无持仓下重置账本),**不代表**实盘建议入金。
---
## 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 | 选约 |
| `close_bid_mark_max_pct` | 30 | 平仓流动性 |
---
## 9. 修订记录
| 日期 | 说明 |
|------|------|
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
+21 -1
View File
@@ -34,6 +34,7 @@ export default function SettingsPage() {
const [closeDevPct, setCloseDevPct] = useState(30); const [closeDevPct, setCloseDevPct] = useState(30);
const [perpQty, setPerpQty] = useState(1); const [perpQty, setPerpQty] = useState(1);
const [optQty, setOptQty] = useState(2); const [optQty, setOptQty] = useState(2);
const [initialEquity, setInitialEquity] = useState(10000);
const [stratOk, setStratOk] = useState(""); const [stratOk, setStratOk] = useState("");
useEffect(() => { useEffect(() => {
@@ -51,6 +52,7 @@ export default function SettingsPage() {
setCloseDevPct(s.close_bid_mark_max_pct ?? 30); setCloseDevPct(s.close_bid_mark_max_pct ?? 30);
setPerpQty(s.perp_qty_eth ?? 1); setPerpQty(s.perp_qty_eth ?? 1);
setOptQty(s.option_qty_eth ?? 2); setOptQty(s.option_qty_eth ?? 2);
setInitialEquity(s.initial_equity ?? 10000);
}) })
.catch(() => undefined); .catch(() => undefined);
}, []); }, []);
@@ -106,9 +108,12 @@ export default function SettingsPage() {
close_bid_mark_max_pct: closeDevPct, close_bid_mark_max_pct: closeDevPct,
perp_qty_eth: perpQty, perp_qty_eth: perpQty,
option_qty_eth: optQty, option_qty_eth: optQty,
initial_equity: initialEquity,
}), }),
}); });
setStratOk("策略参数已保存"); setStratOk(
"策略参数已保存(模拟资金仅在数值变更且无持仓时重置账本)",
);
} catch (ex) { } catch (ex) {
setErr(ex instanceof Error ? ex.message : String(ex)); setErr(ex instanceof Error ? ex.message : String(ex));
} }
@@ -145,6 +150,21 @@ export default function SettingsPage() {
<section className="settings-section"> <section className="settings-section">
<h3></h3> <h3></h3>
<div className="settings-fields"> <div className="settings-fields">
<div className="field">
<label htmlFor="equity">USDT</label>
<input
id="equity"
className="mono"
type="number"
step="100"
min="1000"
value={initialEquity}
onChange={(e) => setInitialEquity(Number(e.target.value))}
/>
<p className="settings-hint">
10000
</p>
</div>
<div className="field"> <div className="field">
<label htmlFor="lev"></label> <label htmlFor="lev"></label>
<input <input
+7
View File
@@ -433,6 +433,13 @@ input {
margin-bottom: 12px; margin-bottom: 12px;
} }
.settings-hint {
margin: 6px 0 0;
font-size: 12px;
color: var(--muted);
line-height: 1.4;
}
.settings-actions { .settings-actions {
margin-top: 8px; margin-top: 8px;
display: flex; display: flex;