diff --git a/.env.example b/.env.example index 738cace..77e9c83 100644 --- a/.env.example +++ b/.env.example @@ -31,7 +31,7 @@ OPTION_INST_FAMILY=ETH-USD_UM INDEX_INST_ID=ETH-USD FEE_RATE=0.0005 -INITIAL_EQUITY=100000 +INITIAL_EQUITY=10000 LEVERAGE=3 MIN_OPTION_HOURS=12 MIN_OPTION_LEVERAGE=100 diff --git a/README.md b/README.md index 83a579c8..518dc52 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - [开发方案](docs/开发方案.md) - [代码结构](docs/代码结构.md) - [商业化与授权方案](docs/商业化与授权方案.md) +- [策略说明](docs/策略说明.md) ## 访问(测试机) diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index b3bd254..bd7e519 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -2,12 +2,13 @@ from __future__ import annotations from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from ..config import get_settings from ..models.db import get_db from ..sim.ledger import Ledger +from ..sim.matcher import Matcher from .auth import require_user 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) rest_seconds: int | None = Field(default=None, ge=0, le=3600) 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) min_option_hours: float | None = Field(default=None, ge=1, le=720) 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)], ) -> dict: db = get_db() + s = get_settings() 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(): if k in KEYS: 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() diff --git a/backend/app/config.py b/backend/app/config.py index d9fb416..03fb51d 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -38,7 +38,7 @@ class Settings(BaseSettings): index_inst_id: str = "ETH-USD" fee_rate: float = 0.0005 - initial_equity: float = 100_000.0 + initial_equity: float = 10_000.0 # SIM 模拟初始资金(USDT),设置页可改 max_rounds: int = 3 # 已不再强管控,仅兼容旧字段 open_hhmm: str = "16:00" # 已废弃开仓窗 stop_open_hhmm: str = "08:00" # 已废弃开仓窗 diff --git a/backend/app/sim/ledger.py b/backend/app/sim/ledger.py index 8d47ca7..6aca95b 100644 --- a/backend/app/sim/ledger.py +++ b/backend/app/sim/ledger.py @@ -48,6 +48,24 @@ class Ledger: self.db._conn.commit() 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: v = self.db.get_setting(key) if v is None or v == "": diff --git a/docs/开发方案.md b/docs/开发方案.md index 9569761..3003820 100644 --- a/docs/开发方案.md +++ b/docs/开发方案.md @@ -186,7 +186,7 @@ OKX_API_KEY=... # 模拟阶段只需能拉行情的权限 OKX_API_SECRET=... OKX_API_PASSPHRASE=... FEE_RATE=0.0005 # 示例;滑点 = 1 × FEE_RATE -INITIAL_EQUITY=100000 +INITIAL_EQUITY=10000 TZ=Asia/Shanghai ``` diff --git a/docs/策略说明.md b/docs/策略说明.md new file mode 100644 index 0000000..78276f4 --- /dev/null +++ b/docs/策略说明.md @@ -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 到期自动全平 + +- 到达期权到期时刻(OKX:UTC 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,800–2,200 USDT** 量级。 + +### 7.1 单组占用粗算 + +| 项目 | 约略金额(USDT) | +|------|------------------| +| 永续保证金(名义÷杠杆) | ≈ 1,800–2,200 ÷ 3 ≈ **600–750** | +| 期权权利金(ATM、短期限,2 ETH) | 常见约 **15–60**(行情波动大时更高) | +| 开平手续费 / 滑点缓冲 | **20–50** | +| 逆向波动缓冲(避免过紧) | **300–800**(经验缓冲,非精确 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 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 | diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index f913ac0..7ce9925 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -34,6 +34,7 @@ export default function SettingsPage() { const [closeDevPct, setCloseDevPct] = useState(30); const [perpQty, setPerpQty] = useState(1); const [optQty, setOptQty] = useState(2); + const [initialEquity, setInitialEquity] = useState(10000); const [stratOk, setStratOk] = useState(""); useEffect(() => { @@ -51,6 +52,7 @@ export default function SettingsPage() { setCloseDevPct(s.close_bid_mark_max_pct ?? 30); setPerpQty(s.perp_qty_eth ?? 1); setOptQty(s.option_qty_eth ?? 2); + setInitialEquity(s.initial_equity ?? 10000); }) .catch(() => undefined); }, []); @@ -106,9 +108,12 @@ export default function SettingsPage() { close_bid_mark_max_pct: closeDevPct, perp_qty_eth: perpQty, option_qty_eth: optQty, + initial_equity: initialEquity, }), }); - setStratOk("策略参数已保存"); + setStratOk( + "策略参数已保存(模拟资金仅在数值变更且无持仓时重置账本)", + ); } catch (ex) { setErr(ex instanceof Error ? ex.message : String(ex)); } @@ -145,6 +150,21 @@ export default function SettingsPage() {

资金

+
+ + setInitialEquity(Number(e.target.value))} + /> +

+ 保存后将权益重置为该金额(须无持仓)。默认 10000。 +

+