Close residual options when premium recovers above configurable threshold.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 14:19:21 +08:00
parent 60b3743e8d
commit 62c91d4bd0
12 changed files with 603 additions and 11 deletions
+18
View File
@@ -46,6 +46,8 @@ KEYS = (
"fixed_direction_enabled",
"fixed_perp_side",
"close_bid_mark_max_pct",
"residual_min_premium_pct",
"residual_close_check_sec",
"perp_qty_eth",
"option_qty_eth",
"show_manual_trade_buttons",
@@ -82,6 +84,8 @@ class StrategySettingsBody(BaseModel):
fixed_direction_enabled: bool | None = None
fixed_perp_side: str | None = Field(default=None, pattern="^(long|short)$")
close_bid_mark_max_pct: float | None = Field(default=None, ge=1, le=100)
residual_min_premium_pct: float | None = Field(default=None, ge=1, le=100)
residual_close_check_sec: int | None = Field(default=None, ge=30, le=86400)
perp_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
option_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
show_manual_trade_buttons: bool | None = None
@@ -213,6 +217,20 @@ def _read_settings() -> dict:
db.get_setting("close_bid_mark_max_pct", str(s.close_bid_mark_max_pct))
or s.close_bid_mark_max_pct
),
"residual_min_premium_pct": float(
db.get_setting(
"residual_min_premium_pct", str(s.residual_min_premium_pct)
)
or s.residual_min_premium_pct
),
"residual_close_check_sec": int(
float(
db.get_setting(
"residual_close_check_sec", str(s.residual_close_check_sec)
)
or s.residual_close_check_sec
)
),
"perp_qty_eth": float(
db.get_setting("perp_qty_eth", str(s.perp_qty_eth)) or s.perp_qty_eth
),
+3
View File
@@ -82,6 +82,9 @@ class Settings(BaseSettings):
fixed_direction_enabled: bool = False
fixed_perp_side: str = "long" # long|shortlong→买Putshort→买Call
close_bid_mark_max_pct: float = 30.0 # 平仓:买一相对标记最大偏差%
# 残留期权中途平:当前买一权利金 ≥ 初始权利金 × 该% 才尝试卖出
residual_min_premium_pct: float = 20.0
residual_close_check_sec: int = 300 # 残留巡检间隔(秒)
perp_qty_eth: float = 1.0
option_qty_eth: float = 2.0
db_path: str = "" # empty -> backend/data/hedge.db
+63
View File
@@ -1149,6 +1149,69 @@ class BinanceLiveExecutor(Matcher):
},
)
def try_close_one_residual(self, row: dict) -> dict | None:
"""LIVE-BN:权利金达标后交易所市价卖出归档期权。"""
err = self._guard_live()
if err:
logger.warning("residual premium close blocked: %s", err)
return None
skip, close_bid, _oq = self._evaluate_residual_premium_close(row)
if skip or close_bid is None:
if skip:
logger.debug(
"residual premium close skip %s: %s",
row.get("group_id"),
skip,
)
return None
option_inst_id = str(row.get("option_inst_id") or "")
opt_contracts = float(row.get("option_qty_contracts") or 0)
opt_qty = float(row.get("option_qty_eth") or 0)
if opt_contracts <= 0:
ct = self._ct_mult(option_inst_id)
opt_contracts = float(contracts_for_eth(opt_qty, ct) or 0)
if opt_contracts <= 0:
logger.warning(
"residual premium close skip %s: bad contracts", row.get("group_id")
)
return None
client = self._client()
try:
opt_live = client.place_option_market(
symbol=option_inst_id,
side="SELL",
quantity=max(1.0, opt_contracts),
reduce_only=True,
)
except Exception as e:
logger.warning(
"residual premium close exchange sell failed %s: %s",
row.get("group_id"),
e,
)
return None
of_px = float(opt_live.avg_px)
of_fee = float(opt_live.fee)
filled_c = (
float(opt_live.sz)
if opt_live.sz and float(opt_live.sz) > 0
else opt_contracts
)
opt_qty = eth_from_contracts(filled_c, self._ct_mult(option_inst_id))
row = {**row, "option_qty_eth": opt_qty, "option_qty_contracts": filled_c}
of_notional = of_px * opt_qty
now_ms = int(time.time() * 1000)
return self._book_residual_market_close(
row,
fill_px=of_px,
fee=of_fee,
notional=of_notional,
slip=0.0,
now_ms=now_ms,
note="LIVE-BN residual mid-close by premium recovery",
exec_mode="LIVE",
)
def close_perp_abandon_option(
self, *, reason: str = "target_perp_only", require_deep_otm: bool = True
) -> CloseResult:
+64
View File
@@ -1192,6 +1192,70 @@ class OkxLiveExecutor(Matcher):
},
)
def try_close_one_residual(self, row: dict) -> dict | None:
"""LIVE:权利金达标后交易所市价卖出归档期权。"""
err = self._guard_live()
if err:
logger.warning("residual premium close blocked: %s", err)
return None
skip, close_bid, _oq = self._evaluate_residual_premium_close(row)
if skip or close_bid is None:
if skip:
logger.debug(
"residual premium close skip %s: %s",
row.get("group_id"),
skip,
)
return None
option_inst_id = str(row.get("option_inst_id") or "")
opt_contracts = float(row.get("option_qty_contracts") or 0)
opt_qty = float(row.get("option_qty_eth") or 0)
if opt_contracts <= 0:
ct = self._ct_mult(option_inst_id)
opt_contracts = float(contracts_for_eth(opt_qty, ct) or 0)
if opt_contracts <= 0:
logger.warning(
"residual premium close skip %s: bad contracts", row.get("group_id")
)
return None
client = self._client()
try:
opt_live = client.place_market(
inst_id=option_inst_id,
side="sell",
sz=str(max(1, int(round(opt_contracts)))),
td_mode="cash",
reduce_only=True,
)
except Exception as e:
logger.warning(
"residual premium close exchange sell failed %s: %s",
row.get("group_id"),
e,
)
return None
of_px = float(opt_live.avg_px)
of_fee = float(opt_live.fee)
filled_c = (
float(opt_live.sz)
if opt_live.sz and float(opt_live.sz) > 0
else opt_contracts
)
opt_qty = eth_from_contracts(filled_c, self._ct_mult(option_inst_id))
row = {**row, "option_qty_eth": opt_qty, "option_qty_contracts": filled_c}
of_notional = of_px * opt_qty
now_ms = int(time.time() * 1000)
return self._book_residual_market_close(
row,
fill_px=of_px,
fee=of_fee,
notional=of_notional,
slip=0.0,
now_ms=now_ms,
note="LIVE residual mid-close by premium recovery",
exec_mode="LIVE",
)
def close_perp_abandon_option(
self, *, reason: str = "target_perp_only", require_deep_otm: bool = True
) -> CloseResult:
+1
View File
@@ -37,6 +37,7 @@ CLOSE_REASON_ZH: dict[str, str] = {
"manual": "手动全平",
"perp_pending_retry": "续平永续",
"liquidity_retry": "等待流动性后全平",
"residual_premium_close": "残留期权·权利金回收中途平",
"unknown": "未知原因",
}
+226
View File
@@ -829,6 +829,232 @@ class Matcher:
)
return [dict(r) for r in rows]
def _residual_min_premium_pct(self) -> float:
s = get_settings()
return float(
self.ledger.get_setting_float(
"residual_min_premium_pct", s.residual_min_premium_pct
)
)
def _evaluate_residual_premium_close(
self, row: dict[str, Any]
) -> tuple[str | None, float | None, Any]:
"""
残留中途平前置:权利金比例 + 买一流动性。
返回 (skip_reason, close_bid, option_quote)skip_reason 非空则本轮不卖。
"""
s = get_settings()
initial_premium = float(row.get("initial_premium") or 0)
opt_qty = float(row.get("option_qty_eth") or 0)
if initial_premium <= 0 or opt_qty <= 0:
return ("invalid_initial_premium_or_qty", None, None)
option_inst_id = str(row.get("option_inst_id") or "")
if not option_inst_id:
return ("missing_option_inst", None, None)
oq = self._quote_held_option(option_inst_id)
if oq is None or oq.bid is None:
return ("option_bid_unavailable", None, None)
close_bid = float(oq.bid)
current_premium = close_bid * opt_qty
min_pct = self._residual_min_premium_pct()
threshold = initial_premium * (min_pct / 100.0)
if current_premium + 1e-12 < threshold:
return (
f"premium_below_threshold curr={current_premium:.4f} "
f"need>={threshold:.4f} ({min_pct:g}%)",
None,
None,
)
ct_mult = self._ct_mult(option_inst_id)
if not bid_covers_eth(
bid_sz_contracts=oq.bid_sz,
ct_mult=ct_mult,
need_eth=opt_qty,
):
return ("option_bid_liquidity_insufficient", None, None)
max_dev = self.ledger.get_setting_float(
"close_bid_mark_max_pct", s.close_bid_mark_max_pct
)
ok_dev, why = bid_mark_ok(bid=close_bid, mark=oq.mark_px, max_dev_pct=max_dev)
if not ok_dev:
return (why or "bid_mark_deviation", None, None)
strike = row.get("strike")
spot = self._close_spot_px(get_session().snapshot())
intrinsic: float | None = None
if strike is not None and spot is not None:
intrinsic = option_intrinsic(
option_side=str(row["option_side"]),
strike=float(strike),
spot=float(spot),
)
resolved = resolve_option_close_bid(
bid=close_bid,
mark=oq.mark_px,
intrinsic=intrinsic,
bypass_liquidity=False,
)
if resolved is None:
return ("option_close_px_unavailable", None, None)
return (None, float(resolved), oq)
def _book_residual_market_close(
self,
row: dict[str, Any],
*,
fill_px: float,
fee: float,
notional: float,
slip: float,
now_ms: int,
note: str,
exec_mode: str | None = None,
) -> dict[str, Any]:
"""买一卖出残留后的入账与结清(SIM/LIVE 共用)。"""
group_id = str(row["group_id"])
opt_qty = float(row["option_qty_eth"])
opt_entry = float(row["option_entry_px"])
opt_pnl = (float(fill_px) - opt_entry) * opt_qty
opt_cash = float(notional) - float(fee)
self.ledger.apply_cash(
opt_cash,
kind="close_option",
group_id=group_id,
note=note,
allow_negative=not get_settings().is_sim,
)
fill_cols = (
"group_id, leg, action, side, inst_id, qty_eth, qty_contracts, "
"base_px, fill_px, fee, slip, notional, ts_ms"
)
fill_vals: list[Any] = [
group_id,
"option",
"close",
"flat",
str(row["option_inst_id"]),
opt_qty,
float(row["option_qty_contracts"] or 0),
float(fill_px),
float(fill_px),
float(fee),
float(slip),
float(notional),
now_ms,
]
if exec_mode:
fill_cols += ", exec_mode"
fill_vals.append(exec_mode)
with self.db._lock:
self.db._conn.execute(
f"""INSERT INTO fills({fill_cols})
VALUES ({",".join("?" for _ in fill_vals)})""",
tuple(fill_vals),
)
fills = self.db._conn.execute(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
).fetchall()
from ..sim.pnl import summarize_fills_pnl
summary = summarize_fills_pnl(list(fills))
net = summary.get("net_pnl")
if net is None:
net = opt_pnl - float(fee)
g = self.db._conn.execute(
"SELECT fees, slip_cost FROM groups WHERE group_id=?", (group_id,)
).fetchone()
fees = float(g["fees"] or 0) + float(fee) if g else float(fee)
slip_total = float(g["slip_cost"] or 0) + float(slip) if g else float(slip)
self.db._conn.execute(
"""UPDATE residual_options SET status=?, settled_at_ms=?, settle_px=?, settle_pnl=?, note=?
WHERE group_id=?""",
(
"settled",
now_ms,
float(fill_px),
opt_pnl,
note,
group_id,
),
)
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=COALESCE(close_at_ms, ?),
close_reason=COALESCE(close_reason, ?), realized_pnl=?, fees=?, slip_cost=?
WHERE group_id=?""",
(
"closed",
now_ms,
"residual_premium_close",
float(net),
fees,
slip_total,
group_id,
),
)
self.db._conn.commit()
return {
"group_id": group_id,
"option_pnl": opt_pnl,
"settle_px": float(fill_px),
"net_pnl": float(net),
"reason": "residual_premium_close",
"current_premium": float(fill_px) * opt_qty,
"initial_premium": float(row.get("initial_premium") or 0),
}
def try_close_one_residual(self, row: dict[str, Any]) -> dict[str, Any] | None:
"""SIM:权利金达标且流动性通过则本地吃买一平残留。"""
skip, close_bid, oq = self._evaluate_residual_premium_close(row)
if skip or close_bid is None or oq is None:
if skip:
logger.debug(
"residual premium close skip %s: %s",
row.get("group_id"),
skip,
)
return None
of = option_fill(
action="close",
bid=float(close_bid),
ask=float(oq.ask or close_bid),
qty_eth=float(row["option_qty_eth"]),
fee_rate=self._fee_rate(),
)
now_ms = int(time.time() * 1000)
return self._book_residual_market_close(
row,
fill_px=of.fill_px,
fee=of.fee,
notional=of.notional,
slip=of.slip,
now_ms=now_ms,
note="residual mid-close by premium recovery",
)
def try_close_pending_residuals(self) -> list[dict[str, Any]]:
"""巡检全部 pending 残留,尝试权利金回收平仓。"""
out: list[dict[str, Any]] = []
for row in self.list_residual_options(pending_only=True):
try:
r = self.try_close_one_residual(row)
except Exception:
logger.exception(
"try_close_one_residual failed group=%s", row.get("group_id")
)
continue
if r:
out.append(r)
return out
def settle_due_residuals(self, *, now_ms: int | None = None) -> list[dict[str, Any]]:
"""到期结算所有 pending 残留期权(不扫描进活跃组平仓)。"""
now = int(now_ms if now_ms is not None else time.time() * 1000)
+37
View File
@@ -31,6 +31,7 @@ class StrategyEngine:
self._lock = asyncio.Lock()
self._retry_gate = LiveRetryGate()
self._extra_sleep_sec = 0.0
self._last_residual_premium_check_ms = 0
def refresh_executor(self) -> None:
"""MODE 变更后刷新执行器。"""
@@ -545,6 +546,41 @@ class StrategyEngine:
async def _settle_residuals(self) -> None:
await asyncio.to_thread(self.matcher.settle_due_residuals)
async def _maybe_close_residuals_by_premium(self) -> None:
"""残留期权:权利金回升达标时周期性尝试中途平仓。"""
s = get_settings()
interval_sec = int(
self.ledger.get_setting_int(
"residual_close_check_sec", s.residual_close_check_sec
)
)
interval_sec = max(30, interval_sec)
now_ms = int(time.time() * 1000)
if now_ms - self._last_residual_premium_check_ms < interval_sec * 1000:
return
self._last_residual_premium_check_ms = now_ms
fn = getattr(self.matcher, "try_close_pending_residuals", None)
if not callable(fn):
return
try:
closed = await asyncio.to_thread(fn)
except Exception:
logger.exception("try_close_pending_residuals failed")
return
if not closed:
return
for item in closed:
try:
from ..notify import wecom
wecom.notify_close(
reason="residual_premium_close",
detail="残留期权权利金回收中途平",
data=item if isinstance(item, dict) else {},
)
except Exception:
logger.exception("wecom notify residual premium close failed")
async def _maybe_expiry_close(self) -> bool:
"""若持仓已到期则强制全平。返回是否触发到期平仓。"""
await self._settle_residuals()
@@ -613,6 +649,7 @@ class StrategyEngine:
async def _tick_manage_positions(self) -> None:
"""有仓时的盯盘:残留结算 / 半仓修复 / 目标平 / 到期平。不新开仓。"""
await self._settle_residuals()
await self._maybe_close_residuals_by_premium()
s = get_settings()
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
@@ -0,0 +1,150 @@
"""残留期权:权利金回升达标后中途平。"""
from __future__ import annotations
from types import SimpleNamespace
from app.models.db import Database
from app.sim.matcher import Matcher
def _seed_residual(
db: Database,
*,
group_id: str = "G-res",
initial_premium: float = 100.0,
qty: float = 2.0,
entry_px: float = 50.0,
) -> None:
now = 1_700_000_000_000
with db._lock:
db._conn.execute(
"""INSERT INTO groups(
group_id, status, bias, option_side, perp_side, option_inst_id,
strike, expiry_ymd, initial_premium, open_at_ms, close_at_ms,
close_reason, realized_pnl, fees, slip_cost
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"option_residual",
"test",
"call",
"short",
"ETH-USD_UM-260801-2000-C",
2000.0,
"260801",
initial_premium,
now - 10_000,
now - 5_000,
"target_perp_only",
10.0,
1.0,
0.0,
),
)
db._conn.execute(
"""INSERT INTO residual_options(
group_id, option_inst_id, option_side, option_qty_eth, option_qty_contracts,
option_entry_px, strike, expiry_ymd, expiry_ms, entry_index_px,
initial_premium, status, created_at_ms, note
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"ETH-USD_UM-260801-2000-C",
"call",
qty,
200.0,
entry_px,
2000.0,
"260801",
now + 86_400_000,
1900.0,
initial_premium,
"pending",
now - 5_000,
"test residual",
),
)
db._conn.commit()
def test_residual_premium_below_threshold_skips(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
db = Database(tmp_path / "below.db")
db.set_setting("residual_min_premium_pct", "20")
_seed_residual(db, initial_premium=100.0, qty=2.0)
m = Matcher(db)
# bid=5 → premium=10 < 20
oq = SimpleNamespace(bid=5.0, ask=5.5, bid_sz=10_000.0, mark_px=5.0)
monkeypatch.setattr(m, "_quote_held_option", lambda _id: oq)
monkeypatch.setattr(m, "_close_spot_px", lambda _snap: 1900.0)
assert m.try_close_one_residual(m.list_residual_options()[0]) is None
row = db.fetchone(
"SELECT status FROM residual_options WHERE group_id=?", ("G-res",)
)
assert row is not None and row["status"] == "pending"
db.close()
def test_residual_premium_above_threshold_closes(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
db = Database(tmp_path / "above.db")
db.set_setting("residual_min_premium_pct", "20")
_seed_residual(db, initial_premium=100.0, qty=2.0, entry_px=50.0)
m = Matcher(db)
# bid=15 → premium=30 >= 20
oq = SimpleNamespace(bid=15.0, ask=15.5, bid_sz=10_000.0, mark_px=15.0)
monkeypatch.setattr(m, "_quote_held_option", lambda _id: oq)
monkeypatch.setattr(m, "_close_spot_px", lambda _snap: 1900.0)
monkeypatch.setattr(m, "_ct_mult", lambda _id: 0.01)
out = m.try_close_one_residual(m.list_residual_options()[0])
assert out is not None
assert out["reason"] == "residual_premium_close"
row = db.fetchone(
"SELECT status, settle_px FROM residual_options WHERE group_id=?", ("G-res",)
)
assert row is not None and row["status"] == "settled"
g = db.fetchone("SELECT status FROM groups WHERE group_id=?", ("G-res",))
assert g is not None and g["status"] == "closed"
db.close()
def test_residual_liquidity_fail_skips(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
db = Database(tmp_path / "liq.db")
db.set_setting("residual_min_premium_pct", "20")
_seed_residual(db, initial_premium=100.0, qty=2.0)
m = Matcher(db)
# premium ok but depth tiny
oq = SimpleNamespace(bid=15.0, ask=15.5, bid_sz=1.0, mark_px=15.0)
monkeypatch.setattr(m, "_quote_held_option", lambda _id: oq)
monkeypatch.setattr(m, "_close_spot_px", lambda _snap: 1900.0)
monkeypatch.setattr(m, "_ct_mult", lambda _id: 0.01)
assert m.try_close_one_residual(m.list_residual_options()[0]) is None
row = db.fetchone(
"SELECT status FROM residual_options WHERE group_id=?", ("G-res",)
)
assert row is not None and row["status"] == "pending"
db.close()
def test_settings_exposes_residual_min_premium_pct(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from app.api import settings as settings_api
from app.models.db import set_db
d = Database(tmp_path / "set.db")
set_db(d)
try:
d.set_setting("residual_min_premium_pct", "35")
payload = settings_api._read_settings()
assert float(payload["residual_min_premium_pct"]) == 35.0
finally:
set_db(None)
d.close()
+8 -7
View File
@@ -180,11 +180,10 @@ k = floor(budget / (2A + I×fee_rate×3) × 10) / 10
- 条件:净利达标,且期权已是 **虚值且远虚**(内在价值 ≈ 0)。
- 动作:
1. **只市价平掉永续**,兑现净利里永续那一截;
2. 本张期权 **不再盯盘、不再参与平仓扫描**,归档为「到期残留」
3. 因期权 **逐仓**,残留 **不占用活跃持仓****不挡住下一组开仓**
4. 下一组开平仓 **只扫当前活跃组期权**,不扫描历史残留腿
5. 残留期权到到期日再按 **内在价值** 单独结算(多半接近 0);
6. 页面:**活跃持仓区变空**;归档腿出现在「残留期权(待到期)」;下方期权盘口 **切回新 ATM**(见 4.6)。
2. 本张期权归档为「残留」:不占用活跃持仓、**不挡住下一组开仓**;下一组只扫当前活跃组期权
3. **中途回收(可配置)**:默认每 **5 分钟**巡检 pending 残留;当 **买一权利金 ≥ 初始权利金 × 比例**(默认 **20%**,系统设置「残留期权回收」可改)且通过买一流动性闸门时,**市价卖掉**该残留并结清
4. 未达比例或闸门不过 → 继续等到下次巡检,或到期按 **内在价值** 结算(多半接近 0
5. 页面:**活跃持仓区变空**;归档腿出现在「残留期权(待到期)」;下方期权盘口 **切回新 ATM**(见 4.6)。
残留到期结算口径:
@@ -213,7 +212,7 @@ k = floor(budget / (2A + I×fee_rate×3) × 10) / 10
- 买一相对标记偏差默认 ≤ **30%**`close_bid_mark_max_pct`);
- 不满足 → `liquidity_wait`,继续等待。
**4.1.B / 到期 / 紧急全平**不适用「必须卖掉期权买一」这套闸门(到期与残留按内在价值;紧急可绕过)。
**4.1.B 归档当下 / 到期 / 紧急全平**归档与到期不强制吃买一(到期/强制按内在价值;紧急可绕过)。**残留中途回收**仍走买一深度 + 偏差闸门(同 4.1.A)。
**紧急全平**:活跃组尽量双腿平掉;残留期权一并按内在价值结算;成功后进入组间休息(与手动全平相同)。
@@ -223,6 +222,7 @@ k = floor(budget / (2A + I×fee_rate×3) × 10) / 10
|--------|------|
| `fixed_usdt` / `premium_multiple` | 目标平仓 · 双腿全平(4.1.A) |
| `target_perp_only` | 目标平仓 · 只平永续,期权归档到期(4.1.B) |
| `residual_premium_close` | 残留期权 · 权利金回升达标后中途平 |
| `expiry` | 到期结算(活跃组或残留期权) |
| `emergency` | 界面紧急全平 |
| `manual` | 手动平仓 |
@@ -237,7 +237,8 @@ k = floor(budget / (2A + I×fee_rate×3) × 10) / 10
└─ 否 → 持有直到到期 → 内在价值结算(+ 若有永续则平永续)
残留期权(已归档)
仅到期结算;不参与盯盘、不参与下一组平仓扫描
周期性:买一权利金 / 初始 ≥ 设置% 且流动性过 → 市价卖出结清
└─ 否则到期内在价值结算;不挡下一组开仓
```
### 4.6 行情监控与页面展示
+2
View File
@@ -339,6 +339,8 @@ export type StrategySettings = {
fixed_direction_enabled?: boolean;
fixed_perp_side?: "long" | "short";
close_bid_mark_max_pct?: number;
residual_min_premium_pct?: number;
residual_close_check_sec?: number;
perp_qty_eth?: number;
option_qty_eth?: number;
show_manual_trade_buttons?: boolean;
+1
View File
@@ -27,6 +27,7 @@ const CLOSE_REASON_ZH: Record<string, string> = {
fixed_usdt: "固定净盈利达标·双腿全平",
premium_multiple: "权利金倍数达标·双腿全平",
target_perp_only: "净盈利达标·只平永续(期权归档)",
residual_premium_close: "残留期权·权利金回收中途平",
expiry: "到期结算",
emergency: "紧急全平",
manual: "手动平仓",
+30 -4
View File
@@ -85,6 +85,7 @@ export default function SettingsPage() {
const [fixedDirOn, setFixedDirOn] = useState(false);
const [fixedPerpSide, setFixedPerpSide] = useState<"long" | "short">("long");
const [closeDevPct, setCloseDevPct] = useState(30);
const [residualMinPremPct, setResidualMinPremPct] = useState(20);
const [perpQty, setPerpQty] = useState(1);
const [optQty, setOptQty] = useState(2);
const [showManualTrade, setShowManualTrade] = useState(false);
@@ -196,6 +197,7 @@ export default function SettingsPage() {
setFixedDirOn(s.fixed_direction_enabled === true);
setFixedPerpSide(s.fixed_perp_side === "short" ? "short" : "long");
setCloseDevPct(s.close_bid_mark_max_pct ?? 30);
setResidualMinPremPct(s.residual_min_premium_pct ?? 20);
setPerpQty(s.perp_qty_eth ?? 1);
setOptQty(s.option_qty_eth ?? 2);
setShowManualTrade(s.show_manual_trade_buttons === true);
@@ -362,6 +364,7 @@ export default function SettingsPage() {
fixed_direction_enabled: fixedDirOn,
fixed_perp_side: fixedPerpSide,
close_bid_mark_max_pct: closeDevPct,
residual_min_premium_pct: residualMinPremPct,
show_manual_trade_buttons: showManualTrade,
sizing_mode: sizingMode,
risk_leverage_basis: riskLeverageBasis,
@@ -1160,6 +1163,23 @@ export default function SettingsPage() {
onChange={(e) => setCloseDevPct(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="residualPrem">
/ %
</label>
<input
id="residualPrem"
className="mono"
type="number"
step="1"
min="1"
max="100"
value={residualMinPremPct}
onChange={(e) =>
setResidualMinPremPct(Number(e.target.value))
}
/>
</div>
</div>
</section>
) : null}
@@ -1297,10 +1317,16 @@ export default function SettingsPage() {
</>
) : null}
{stratSub === "exit" ? (
<li>
/ SIM
LIVE
</li>
<>
<li>
/ SIM
LIVE
</li>
<li>
20%
</li>
</>
) : null}
{stratSub === "pace" ? (
<>