Add fixed-direction switch for perp/option open side.
When enabled, lock perp long→buy Put or short→buy Call with ITM/ATM only; off keeps ATM/ask rules. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -41,6 +41,8 @@ KEYS = (
|
|||||||
"min_option_leverage",
|
"min_option_leverage",
|
||||||
"atm_open_offset_enabled",
|
"atm_open_offset_enabled",
|
||||||
"max_atm_open_offset",
|
"max_atm_open_offset",
|
||||||
|
"fixed_direction_enabled",
|
||||||
|
"fixed_perp_side",
|
||||||
"close_bid_mark_max_pct",
|
"close_bid_mark_max_pct",
|
||||||
"perp_qty_eth",
|
"perp_qty_eth",
|
||||||
"option_qty_eth",
|
"option_qty_eth",
|
||||||
@@ -63,6 +65,8 @@ class StrategySettingsBody(BaseModel):
|
|||||||
min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
|
min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
|
||||||
atm_open_offset_enabled: bool | None = None
|
atm_open_offset_enabled: bool | None = None
|
||||||
max_atm_open_offset: float | None = Field(default=None, ge=0, le=100)
|
max_atm_open_offset: float | None = Field(default=None, ge=0, le=100)
|
||||||
|
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)
|
close_bid_mark_max_pct: float | None = Field(default=None, ge=1, le=100)
|
||||||
perp_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
|
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)
|
option_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
|
||||||
@@ -131,6 +135,25 @@ def _read_settings() -> dict:
|
|||||||
db.get_setting("max_atm_open_offset", str(s.max_atm_open_offset))
|
db.get_setting("max_atm_open_offset", str(s.max_atm_open_offset))
|
||||||
or s.max_atm_open_offset
|
or s.max_atm_open_offset
|
||||||
),
|
),
|
||||||
|
"fixed_direction_enabled": _as_bool(
|
||||||
|
db.get_setting(
|
||||||
|
"fixed_direction_enabled", str(s.fixed_direction_enabled)
|
||||||
|
),
|
||||||
|
s.fixed_direction_enabled,
|
||||||
|
),
|
||||||
|
"fixed_perp_side": (
|
||||||
|
side
|
||||||
|
if (
|
||||||
|
side := str(
|
||||||
|
db.get_setting("fixed_perp_side", s.fixed_perp_side)
|
||||||
|
or s.fixed_perp_side
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
in ("long", "short")
|
||||||
|
else "long"
|
||||||
|
),
|
||||||
"close_bid_mark_max_pct": float(
|
"close_bid_mark_max_pct": float(
|
||||||
db.get_setting("close_bid_mark_max_pct", str(s.close_bid_mark_max_pct))
|
db.get_setting("close_bid_mark_max_pct", str(s.close_bid_mark_max_pct))
|
||||||
or s.close_bid_mark_max_pct
|
or s.close_bid_mark_max_pct
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ class Settings(BaseSettings):
|
|||||||
min_option_leverage: float = 100.0 # 现价/卖一权利金 下限
|
min_option_leverage: float = 100.0 # 现价/卖一权利金 下限
|
||||||
atm_open_offset_enabled: bool = False # 开仓 ATM 偏差限制开关(默认关)
|
atm_open_offset_enabled: bool = False # 开仓 ATM 偏差限制开关(默认关)
|
||||||
max_atm_open_offset: float = 3.0 # 开启后:|ATM行权价−标的| 上限(点)
|
max_atm_open_offset: float = 3.0 # 开启后:|ATM行权价−标的| 上限(点)
|
||||||
|
# 固定方向:关=现有 ATM/比价规则;开=指定永续多/空,期权 Put/Call 且须实值或平值
|
||||||
|
fixed_direction_enabled: bool = False
|
||||||
|
fixed_perp_side: str = "long" # long|short;long→买Put,short→买Call
|
||||||
close_bid_mark_max_pct: float = 30.0 # 平仓:买一相对标记最大偏差%
|
close_bid_mark_max_pct: float = 30.0 # 平仓:买一相对标记最大偏差%
|
||||||
perp_qty_eth: float = 1.0
|
perp_qty_eth: float = 1.0
|
||||||
option_qty_eth: float = 2.0
|
option_qty_eth: float = 2.0
|
||||||
|
|||||||
@@ -88,7 +88,26 @@ def _open_bias(
|
|||||||
call: Quote | None,
|
call: Quote | None,
|
||||||
put: Quote | None,
|
put: Quote | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""与开仓 decide 一致:先按 ATM 相对现价,贴平时再卖一比价。"""
|
"""与开仓 decide 一致;固定方向开启时显示 fixed_*。"""
|
||||||
|
try:
|
||||||
|
from ..config import get_settings
|
||||||
|
from ..models.db import get_db
|
||||||
|
|
||||||
|
s = get_settings()
|
||||||
|
db = get_db()
|
||||||
|
raw = db.get_setting(
|
||||||
|
"fixed_direction_enabled", str(s.fixed_direction_enabled)
|
||||||
|
)
|
||||||
|
on = str(raw or "").strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
if on:
|
||||||
|
side = str(
|
||||||
|
db.get_setting("fixed_perp_side", s.fixed_perp_side)
|
||||||
|
or s.fixed_perp_side
|
||||||
|
or "long"
|
||||||
|
).strip().lower()
|
||||||
|
return "fixed_long_put" if side == "long" else "fixed_short_call"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
mark = None
|
mark = None
|
||||||
if index_px is not None and index_px > 0:
|
if index_px is not None and index_px > 0:
|
||||||
mark = float(index_px)
|
mark = float(index_px)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ def select_option_pair(
|
|||||||
expiry_ymd: str | None = None,
|
expiry_ymd: str | None = None,
|
||||||
min_hours: float | None = None,
|
min_hours: float | None = None,
|
||||||
now=None,
|
now=None,
|
||||||
|
option_side: str | None = None,
|
||||||
) -> OptionPair | None:
|
) -> OptionPair | None:
|
||||||
contracts = normalize_contracts(instruments)
|
contracts = normalize_contracts(instruments)
|
||||||
return _select_pair(
|
return _select_pair(
|
||||||
@@ -48,6 +49,7 @@ def select_option_pair(
|
|||||||
expiry_ymd=expiry_ymd,
|
expiry_ymd=expiry_ymd,
|
||||||
min_hours=min_hours,
|
min_hours=min_hours,
|
||||||
now=now,
|
now=now,
|
||||||
|
option_side=option_side,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,8 @@ class Database:
|
|||||||
"min_option_leverage": str(s.min_option_leverage),
|
"min_option_leverage": str(s.min_option_leverage),
|
||||||
"atm_open_offset_enabled": str(s.atm_open_offset_enabled),
|
"atm_open_offset_enabled": str(s.atm_open_offset_enabled),
|
||||||
"max_atm_open_offset": str(s.max_atm_open_offset),
|
"max_atm_open_offset": str(s.max_atm_open_offset),
|
||||||
|
"fixed_direction_enabled": str(s.fixed_direction_enabled),
|
||||||
|
"fixed_perp_side": str(s.fixed_perp_side),
|
||||||
"close_bid_mark_max_pct": str(s.close_bid_mark_max_pct),
|
"close_bid_mark_max_pct": str(s.close_bid_mark_max_pct),
|
||||||
"perp_qty_eth": str(s.perp_qty_eth),
|
"perp_qty_eth": str(s.perp_qty_eth),
|
||||||
"option_qty_eth": str(s.option_qty_eth),
|
"option_qty_eth": str(s.option_qty_eth),
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ class StrategyEngine:
|
|||||||
max_atm_off = self.ledger.get_setting_float(
|
max_atm_off = self.ledger.get_setting_float(
|
||||||
"max_atm_open_offset", s.max_atm_open_offset
|
"max_atm_open_offset", s.max_atm_open_offset
|
||||||
)
|
)
|
||||||
|
fixed_dir_on = self.ledger.get_setting_bool(
|
||||||
|
"fixed_direction_enabled", s.fixed_direction_enabled
|
||||||
|
)
|
||||||
|
fixed_perp = str(
|
||||||
|
self.ledger.get_setting_str("fixed_perp_side", s.fixed_perp_side)
|
||||||
|
or s.fixed_perp_side
|
||||||
|
or "long"
|
||||||
|
).strip().lower()
|
||||||
|
if fixed_perp not in ("long", "short"):
|
||||||
|
fixed_perp = "long"
|
||||||
perp_qty = self.ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth)
|
perp_qty = self.ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth)
|
||||||
opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
|
opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
|
||||||
rest_until = row["rest_until_ms"]
|
rest_until = row["rest_until_ms"]
|
||||||
@@ -136,6 +146,8 @@ class StrategyEngine:
|
|||||||
"min_option_leverage": min_opt_lev,
|
"min_option_leverage": min_opt_lev,
|
||||||
"atm_open_offset_enabled": atm_off_on,
|
"atm_open_offset_enabled": atm_off_on,
|
||||||
"max_atm_open_offset": max_atm_off,
|
"max_atm_open_offset": max_atm_off,
|
||||||
|
"fixed_direction_enabled": fixed_dir_on,
|
||||||
|
"fixed_perp_side": fixed_perp,
|
||||||
"can_open": allow_open,
|
"can_open": allow_open,
|
||||||
"open_capacity": open_cap,
|
"open_capacity": open_cap,
|
||||||
"last_error": last_error,
|
"last_error": last_error,
|
||||||
|
|||||||
@@ -46,6 +46,46 @@ def pick_atm_strike(strikes: list[float], mark_px: float) -> float | None:
|
|||||||
return min(strikes, key=lambda s: (abs(s - mark_px), s))
|
return min(strikes, key=lambda s: (abs(s - mark_px), s))
|
||||||
|
|
||||||
|
|
||||||
|
def pick_itm_or_atm_strike(
|
||||||
|
strikes: list[float],
|
||||||
|
mark_px: float,
|
||||||
|
*,
|
||||||
|
option_side: str,
|
||||||
|
) -> float | None:
|
||||||
|
"""
|
||||||
|
固定方向选约:只要实值或平值,不要虚值。
|
||||||
|
- Call:行权价 ≤ 标的(平值/实值)
|
||||||
|
- Put:行权价 ≥ 标的(平值/实值)
|
||||||
|
在合格档中取最接近标的者(优先平值)。
|
||||||
|
"""
|
||||||
|
if not strikes or mark_px <= 0:
|
||||||
|
return None
|
||||||
|
side = (option_side or "").strip().lower()
|
||||||
|
if side == "call":
|
||||||
|
cands = [float(s) for s in strikes if float(s) <= float(mark_px) + 1e-9]
|
||||||
|
elif side == "put":
|
||||||
|
cands = [float(s) for s in strikes if float(s) >= float(mark_px) - 1e-9]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
if not cands:
|
||||||
|
return None
|
||||||
|
return min(cands, key=lambda s: (abs(s - float(mark_px)), s))
|
||||||
|
|
||||||
|
|
||||||
|
def is_itm_or_atm(*, option_side: str, strike: float, mark_px: float) -> bool:
|
||||||
|
"""Call: K≤S;Put: K≥S。"""
|
||||||
|
if mark_px <= 0:
|
||||||
|
return False
|
||||||
|
side = (option_side or "").strip().lower()
|
||||||
|
k = float(strike)
|
||||||
|
s = float(mark_px)
|
||||||
|
if side == "call":
|
||||||
|
return k <= s + 1e-9
|
||||||
|
if side == "put":
|
||||||
|
return k >= s - 1e-9
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def atm_open_offset(strike: float, mark_px: float) -> float:
|
def atm_open_offset(strike: float, mark_px: float) -> float:
|
||||||
"""开仓用:ATM 行权价相对标的的绝对点差。"""
|
"""开仓用:ATM 行权价相对标的的绝对点差。"""
|
||||||
return abs(float(strike) - float(mark_px))
|
return abs(float(strike) - float(mark_px))
|
||||||
@@ -125,7 +165,13 @@ def select_option_pair(
|
|||||||
expiry_ymd: str | None = None,
|
expiry_ymd: str | None = None,
|
||||||
min_hours: float | None = None,
|
min_hours: float | None = None,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
|
option_side: str | None = None,
|
||||||
) -> OptionPair | None:
|
) -> OptionPair | None:
|
||||||
|
"""
|
||||||
|
选到期 + 行权价。
|
||||||
|
option_side 为 call/put 时:按实值/平值选档(固定方向模式);
|
||||||
|
否则仍选 ATM(现有规则)。
|
||||||
|
"""
|
||||||
complete = _complete_by_expiry(contracts)
|
complete = _complete_by_expiry(contracts)
|
||||||
if not complete:
|
if not complete:
|
||||||
return None
|
return None
|
||||||
@@ -148,14 +194,20 @@ def select_option_pair(
|
|||||||
ymd = eligible[0]
|
ymd = eligible[0]
|
||||||
|
|
||||||
ems, strikes_map = complete[ymd]
|
ems, strikes_map = complete[ymd]
|
||||||
atm = pick_atm_strike(list(strikes_map.keys()), mark_px)
|
side = (option_side or "").strip().lower() or None
|
||||||
if atm is None:
|
if side in ("call", "put"):
|
||||||
|
strike = pick_itm_or_atm_strike(
|
||||||
|
list(strikes_map.keys()), mark_px, option_side=side
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
strike = pick_atm_strike(list(strikes_map.keys()), mark_px)
|
||||||
|
if strike is None:
|
||||||
return None
|
return None
|
||||||
legs = strikes_map[atm]
|
legs = strikes_map[strike]
|
||||||
return OptionPair(
|
return OptionPair(
|
||||||
expiry_ymd=ymd,
|
expiry_ymd=ymd,
|
||||||
expiry_ms=ems,
|
expiry_ms=ems,
|
||||||
strike=atm,
|
strike=strike,
|
||||||
call_inst_id=legs["C"],
|
call_inst_id=legs["C"],
|
||||||
put_inst_id=legs["P"],
|
put_inst_id=legs["P"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .selection import (
|
|||||||
atm_allows_open,
|
atm_allows_open,
|
||||||
atm_open_offset,
|
atm_open_offset,
|
||||||
hours_until_expiry,
|
hours_until_expiry,
|
||||||
|
is_itm_or_atm,
|
||||||
list_eligible_expiry_ymds,
|
list_eligible_expiry_ymds,
|
||||||
option_leverage,
|
option_leverage,
|
||||||
select_option_pair,
|
select_option_pair,
|
||||||
@@ -101,6 +102,36 @@ def _strategy_floats() -> tuple[float, float, float, bool]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _fixed_direction() -> tuple[bool, str]:
|
||||||
|
"""(enabled, perp_side long|short)。默认关。"""
|
||||||
|
s = get_settings()
|
||||||
|
try:
|
||||||
|
from ..models.db import get_db
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
enabled = _as_bool_setting(
|
||||||
|
db.get_setting(
|
||||||
|
"fixed_direction_enabled", str(s.fixed_direction_enabled)
|
||||||
|
),
|
||||||
|
s.fixed_direction_enabled,
|
||||||
|
)
|
||||||
|
side = str(
|
||||||
|
db.get_setting("fixed_perp_side", s.fixed_perp_side) or s.fixed_perp_side
|
||||||
|
).strip().lower()
|
||||||
|
if side not in ("long", "short"):
|
||||||
|
side = "long"
|
||||||
|
return enabled, side
|
||||||
|
except Exception:
|
||||||
|
side = str(s.fixed_perp_side or "long").strip().lower()
|
||||||
|
if side not in ("long", "short"):
|
||||||
|
side = "long"
|
||||||
|
return bool(s.fixed_direction_enabled), side
|
||||||
|
|
||||||
|
|
||||||
|
def _option_side_for_perp(perp_side: str) -> str:
|
||||||
|
return "put" if (perp_side or "").strip().lower() == "long" else "call"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class OpenPick:
|
class OpenPick:
|
||||||
pair: OptionPair
|
pair: OptionPair
|
||||||
@@ -230,19 +261,29 @@ class StrategySession:
|
|||||||
if mark is None or mark <= 0:
|
if mark is None or mark <= 0:
|
||||||
raise RuntimeError("无法获取标的标记/指数价格,无法选 ATM")
|
raise RuntimeError("无法获取标的标记/指数价格,无法选 ATM")
|
||||||
min_hours, _, _, _ = _strategy_floats()
|
min_hours, _, _, _ = _strategy_floats()
|
||||||
|
fixed_on, fixed_perp = _fixed_direction()
|
||||||
|
opt_side = _option_side_for_perp(fixed_perp) if fixed_on else None
|
||||||
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
||||||
pair = select_option_pair(contracts, mark_px=float(mark), min_hours=min_hours)
|
pair = select_option_pair(
|
||||||
|
contracts,
|
||||||
|
mark_px=float(mark),
|
||||||
|
min_hours=min_hours,
|
||||||
|
option_side=opt_side,
|
||||||
|
)
|
||||||
if pair is None:
|
if pair is None:
|
||||||
|
kind = f"实值/平值 {opt_side}" if opt_side else "ATM"
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"未找到剩余≥{min_hours}h 的 ATM Call/Put (family={s.option_inst_family})"
|
f"未找到剩余≥{min_hours}h 的 {kind} Call/Put (family={s.option_inst_family})"
|
||||||
)
|
)
|
||||||
return self._apply_pair(pair, mark=float(mark), idx=idx)
|
return self._apply_pair(pair, mark=float(mark), idx=idx)
|
||||||
|
|
||||||
def pick_for_open(self) -> OpenPick | None:
|
def pick_for_open(self) -> OpenPick | None:
|
||||||
from .signal import decide
|
from .signal import decide, decide_fixed
|
||||||
|
|
||||||
s = self.settings
|
s = self.settings
|
||||||
min_hours, min_lev, max_atm_off, atm_off_on = _strategy_floats()
|
min_hours, min_lev, max_atm_off, atm_off_on = _strategy_floats()
|
||||||
|
fixed_on, fixed_perp = _fixed_direction()
|
||||||
|
opt_side_hint = _option_side_for_perp(fixed_perp) if fixed_on else None
|
||||||
idx = self.ex.fetch_index(s.index_inst_id)
|
idx = self.ex.fetch_index(s.index_inst_id)
|
||||||
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
|
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
|
||||||
if mark is None or mark <= 0:
|
if mark is None or mark <= 0:
|
||||||
@@ -255,9 +296,29 @@ class StrategySession:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
for ymd in eligible:
|
for ymd in eligible:
|
||||||
pair = select_option_pair(contracts, mark_px=underlying, expiry_ymd=ymd)
|
pair = select_option_pair(
|
||||||
|
contracts,
|
||||||
|
mark_px=underlying,
|
||||||
|
expiry_ymd=ymd,
|
||||||
|
option_side=opt_side_hint,
|
||||||
|
)
|
||||||
if pair is None:
|
if pair is None:
|
||||||
continue
|
continue
|
||||||
|
if fixed_on:
|
||||||
|
if not is_itm_or_atm(
|
||||||
|
option_side=opt_side_hint or "",
|
||||||
|
strike=pair.strike,
|
||||||
|
mark_px=underlying,
|
||||||
|
):
|
||||||
|
logger.info(
|
||||||
|
"skip expiry=%s strike=%.0f not ITM/ATM for %s mark=%.2f",
|
||||||
|
ymd,
|
||||||
|
pair.strike,
|
||||||
|
opt_side_hint,
|
||||||
|
underlying,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
offset = atm_open_offset(pair.strike, underlying)
|
offset = atm_open_offset(pair.strike, underlying)
|
||||||
if not atm_allows_open(
|
if not atm_allows_open(
|
||||||
pair.strike,
|
pair.strike,
|
||||||
@@ -284,6 +345,9 @@ class StrategySession:
|
|||||||
if put_ask is None:
|
if put_ask is None:
|
||||||
pq = self.ex.quote(pair.put_inst_id)
|
pq = self.ex.quote(pair.put_inst_id)
|
||||||
put_ask = pq.ask if pq else None
|
put_ask = pq.ask if pq else None
|
||||||
|
if fixed_on:
|
||||||
|
sig = decide_fixed(call_ask, put_ask, perp_side=fixed_perp)
|
||||||
|
else:
|
||||||
sig = decide(
|
sig = decide(
|
||||||
call_ask,
|
call_ask,
|
||||||
put_ask,
|
put_ask,
|
||||||
@@ -376,6 +440,13 @@ class StrategySession:
|
|||||||
mark = mark_px if mark_px is not None else self._mark_for_atm()
|
mark = mark_px if mark_px is not None else self._mark_for_atm()
|
||||||
if mark is None or mark <= 0:
|
if mark is None or mark <= 0:
|
||||||
return False
|
return False
|
||||||
|
fixed_on, fixed_perp = _fixed_direction()
|
||||||
|
if fixed_on:
|
||||||
|
opt = _option_side_for_perp(fixed_perp)
|
||||||
|
if not is_itm_or_atm(
|
||||||
|
option_side=opt, strike=float(self._pair.strike), mark_px=float(mark)
|
||||||
|
):
|
||||||
|
return True
|
||||||
return abs(float(self._pair.strike) - float(mark)) >= _ATM_DRIFT_POINTS
|
return abs(float(self._pair.strike) - float(mark)) >= _ATM_DRIFT_POINTS
|
||||||
|
|
||||||
async def ensure_atm_async(self, *, force: bool = False) -> OptionPair | None:
|
async def ensure_atm_async(self, *, force: bool = False) -> OptionPair | None:
|
||||||
|
|||||||
@@ -66,3 +66,38 @@ def decide(
|
|||||||
put_ask=pa,
|
put_ask=pa,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def decide_fixed(
|
||||||
|
call_ask: float | None,
|
||||||
|
put_ask: float | None,
|
||||||
|
*,
|
||||||
|
perp_side: str,
|
||||||
|
) -> Signal | None:
|
||||||
|
"""
|
||||||
|
固定方向:
|
||||||
|
- 永续多 → 买 Put
|
||||||
|
- 永续空 → 买 Call
|
||||||
|
"""
|
||||||
|
if call_ask is None or put_ask is None:
|
||||||
|
return None
|
||||||
|
side = (perp_side or "").strip().lower()
|
||||||
|
ca = float(call_ask)
|
||||||
|
pa = float(put_ask)
|
||||||
|
if side == "long":
|
||||||
|
return Signal(
|
||||||
|
bias="fixed_long_put",
|
||||||
|
option_side="put",
|
||||||
|
perp_side="long",
|
||||||
|
call_ask=ca,
|
||||||
|
put_ask=pa,
|
||||||
|
)
|
||||||
|
if side == "short":
|
||||||
|
return Signal(
|
||||||
|
bias="fixed_short_call",
|
||||||
|
option_side="call",
|
||||||
|
perp_side="short",
|
||||||
|
call_ask=ca,
|
||||||
|
put_ask=pa,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|||||||
@@ -27,6 +27,43 @@ def test_pick_atm_strike() -> None:
|
|||||||
assert pick_atm_strike([3400, 3500, 3600], 3510) == 3500
|
assert pick_atm_strike([3400, 3500, 3600], 3510) == 3500
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_itm_or_atm_strike() -> None:
|
||||||
|
from app.strategy.selection import is_itm_or_atm, pick_itm_or_atm_strike
|
||||||
|
|
||||||
|
strikes = [3400, 3500, 3600]
|
||||||
|
# Call:K≤S,现价 3510 → 3500(平值侧最近)
|
||||||
|
assert pick_itm_or_atm_strike(strikes, 3510, option_side="call") == 3500
|
||||||
|
# Put:K≥S,现价 3510 → 3600(实值最近;无 3510 档)
|
||||||
|
assert pick_itm_or_atm_strike(strikes, 3510, option_side="put") == 3600
|
||||||
|
# Put 现价正好 3500 → 平值 3500
|
||||||
|
assert pick_itm_or_atm_strike(strikes, 3500, option_side="put") == 3500
|
||||||
|
assert is_itm_or_atm(option_side="call", strike=3500, mark_px=3510)
|
||||||
|
assert not is_itm_or_atm(option_side="call", strike=3600, mark_px=3510)
|
||||||
|
assert is_itm_or_atm(option_side="put", strike=3600, mark_px=3510)
|
||||||
|
assert not is_itm_or_atm(option_side="put", strike=3400, mark_px=3510)
|
||||||
|
|
||||||
|
|
||||||
|
def test_select_option_pair_itm_put() -> None:
|
||||||
|
rows = [
|
||||||
|
{"instId": "ETH-USD_UM-260725-3490-C", "state": "live"},
|
||||||
|
{"instId": "ETH-USD_UM-260725-3490-P", "state": "live"},
|
||||||
|
{"instId": "ETH-USD_UM-260725-3500-C", "state": "live"},
|
||||||
|
{"instId": "ETH-USD_UM-260725-3500-P", "state": "live"},
|
||||||
|
{"instId": "ETH-USD_UM-260725-3510-C", "state": "live"},
|
||||||
|
{"instId": "ETH-USD_UM-260725-3510-P", "state": "live"},
|
||||||
|
]
|
||||||
|
# 标的 3502:Put 实/平 → 3510(≥3502 最近)
|
||||||
|
pair = select_option_pair(rows, mark_px=3502, expiry_ymd="260725", option_side="put")
|
||||||
|
assert pair is not None
|
||||||
|
assert pair.strike == 3510
|
||||||
|
# Call 实/平 → 3500(≤3502 最近)
|
||||||
|
pair_c = select_option_pair(
|
||||||
|
rows, mark_px=3502, expiry_ymd="260725", option_side="call"
|
||||||
|
)
|
||||||
|
assert pair_c is not None
|
||||||
|
assert pair_c.strike == 3500
|
||||||
|
|
||||||
|
|
||||||
def test_next_session_expiry_before_open() -> None:
|
def test_next_session_expiry_before_open() -> None:
|
||||||
now = datetime(2026, 7, 24, 15, 0, tzinfo=_SH)
|
now = datetime(2026, 7, 24, 15, 0, tzinfo=_SH)
|
||||||
assert next_session_expiry_ymd(now) == "260724"
|
assert next_session_expiry_ymd(now) == "260724"
|
||||||
|
|||||||
@@ -28,6 +28,26 @@ def test_signal_equal() -> None:
|
|||||||
assert decide(10.0, 10.0) is None
|
assert decide(10.0, 10.0) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_decide_fixed_long_put() -> None:
|
||||||
|
from app.strategy.signal import decide_fixed
|
||||||
|
|
||||||
|
s = decide_fixed(20.0, 15.0, perp_side="long")
|
||||||
|
assert s is not None
|
||||||
|
assert s.option_side == "put"
|
||||||
|
assert s.perp_side == "long"
|
||||||
|
assert s.bias == "fixed_long_put"
|
||||||
|
|
||||||
|
|
||||||
|
def test_decide_fixed_short_call() -> None:
|
||||||
|
from app.strategy.signal import decide_fixed
|
||||||
|
|
||||||
|
s = decide_fixed(20.0, 15.0, perp_side="short")
|
||||||
|
assert s is not None
|
||||||
|
assert s.option_side == "call"
|
||||||
|
assert s.perp_side == "short"
|
||||||
|
assert s.bias == "fixed_short_call"
|
||||||
|
|
||||||
|
|
||||||
def test_signal_strike_below_spot_call_short() -> None:
|
def test_signal_strike_below_spot_call_short() -> None:
|
||||||
# 现价 1859、ATM 1850:即使 Put 卖一更高,也走 Call+空
|
# 现价 1859、ATM 1850:即使 Put 卖一更高,也走 Call+空
|
||||||
s = decide(10.0, 20.0, strike=1850, mark_px=1859)
|
s = decide(10.0, 20.0, strike=1850, mark_px=1859)
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-24 — 策略设置:固定方向开关
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
1. 选约增加 **固定方向**(默认关):开则固定永续多→买 Put,或永续空→买 Call。
|
||||||
|
2. 固定方向开启时,期权只选 **实值或平值**(Call:K≤标的;Put:K≥标的),不买虚值;ATM 偏差限制不适用。
|
||||||
|
3. 关闭时仍按现有 ATM / 卖一比价规则开仓。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-29 — 企微不标模拟盘;已完成组数按已平仓统计
|
## 2026-07-29 — 企微不标模拟盘;已完成组数按已平仓统计
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
+24
-6
@@ -39,7 +39,9 @@
|
|||||||
|
|
||||||
永续杠杆默认 **3×**(可配)。同时最多 **1 组**仓,禁止叠仓开下一组。
|
永续杠杆默认 **3×**(可配)。同时最多 **1 组**仓,禁止叠仓开下一组。
|
||||||
|
|
||||||
### 2.1 开仓方向(ATM 相对现价优先)
|
### 2.1 开仓方向
|
||||||
|
|
||||||
|
#### 默认(`fixed_direction_enabled=false`):ATM 相对现价优先
|
||||||
|
|
||||||
行权价相对标的有偏离时(币安粗档常见),**先按 ATM 偏上/偏下选向**;仅当行权价与标的贴平(≈)时,才回退卖一比价。
|
行权价相对标的有偏离时(币安粗档常见),**先按 ATM 偏上/偏下选向**;仅当行权价与标的贴平(≈)时,才回退卖一比价。
|
||||||
|
|
||||||
@@ -53,6 +55,17 @@
|
|||||||
|
|
||||||
直觉:偏下行权价用 Call+空,偏上行权价用 Put+多;贴平时再按权利金溢价侧选向。
|
直觉:偏下行权价用 Call+空,偏上行权价用 Put+多;贴平时再按权利金溢价侧选向。
|
||||||
|
|
||||||
|
#### 固定方向(`fixed_direction_enabled=true`)
|
||||||
|
|
||||||
|
设置里可开「固定方向」(默认关),并指定永续多或空:
|
||||||
|
|
||||||
|
| `fixed_perp_side` | 永续 | 期权 | 行权价约束 | bias |
|
||||||
|
|-------------------|------|------|------------|------|
|
||||||
|
| `long` | 做多 | 买入 Put | 实值或平值(K ≥ 标的),优先贴近标的 | `fixed_long_put` |
|
||||||
|
| `short` | 做空 | 买入 Call | 实值或平值(K ≤ 标的),优先贴近标的 | `fixed_short_call` |
|
||||||
|
|
||||||
|
固定方向开启时:**不买虚值**;ATM 偏差限制不适用(由实值/平值规则替代)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 开仓机制
|
## 3. 开仓机制
|
||||||
@@ -64,9 +77,9 @@
|
|||||||
→ 无持仓且不在休息期
|
→ 无持仓且不在休息期
|
||||||
→ 非周末跳过(若开启)
|
→ 非周末跳过(若开启)
|
||||||
→ 选到期:剩余时长 ≥ min_option_hours(默认 12h)
|
→ 选到期:剩余时长 ≥ min_option_hours(默认 12h)
|
||||||
→ 该到期 ATM 行权价(最接近标的)
|
→ 若固定方向关:该到期 ATM;若开:按 Put/Call 选实值或平值档
|
||||||
→ 若开启 atm_open_offset_enabled:|ATM − 标的| ≤ max_atm_open_offset(默认 3)否则跳过
|
→ 若未开固定方向且开启 atm_open_offset_enabled:|ATM − 标的| ≤ max_atm_open_offset(默认 3)否则跳过
|
||||||
→ 选向:ATM 偏下→Call+空;偏上→Put+多;贴平→卖一比价
|
→ 选向:固定方向按设置;否则 ATM 偏下→Call+空;偏上→Put+多;贴平→卖一比价
|
||||||
→ 期权杠杆 = 标的价 ÷ 卖一权利金 ≥ min_option_leverage(默认 100)
|
→ 期权杠杆 = 标的价 ÷ 卖一权利金 ≥ min_option_leverage(默认 100)
|
||||||
→ 先开期权(吃卖一)→ 再市价开永续(一组)
|
→ 先开期权(吃卖一)→ 再市价开永续(一组)
|
||||||
→ 锁定 initial_premium = 期权成交价 × 期权名义(不含费)
|
→ 锁定 initial_premium = 期权成交价 × 期权名义(不含费)
|
||||||
@@ -78,9 +91,11 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `min_option_hours` | 12 | 过滤过近到期,减少刚开仓就到期 |
|
| `min_option_hours` | 12 | 过滤过近到期,减少刚开仓就到期 |
|
||||||
| `min_option_leverage` | 100 | 权利金相对标的不能太贵(现价/卖一) |
|
| `min_option_leverage` | 100 | 权利金相对标的不能太贵(现价/卖一) |
|
||||||
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差限制开关(默认关) |
|
| `fixed_direction_enabled` | false | 固定方向开关(默认关) |
|
||||||
|
| `fixed_perp_side` | long | 开启后:long=多+Put / short=空+Call |
|
||||||
|
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差限制开关(默认关;固定方向开启时忽略) |
|
||||||
| `max_atm_open_offset` | 3 | 开关开启后:\|ATM − 标的\| 超过则不开 |
|
| `max_atm_open_offset` | 3 | 开关开启后:\|ATM − 标的\| 超过则不开 |
|
||||||
| ATM | — | 同到期、最接近指数/标记价的行权价 |
|
| ATM / 实值平值 | — | 默认 ATM;固定方向时同到期实值或平值、最贴近标的 |
|
||||||
|
|
||||||
无合格合约时:状态停留等待,记录「无合格期权…」,不硬开。
|
无合格合约时:状态停留等待,记录「无合格期权…」,不硬开。
|
||||||
|
|
||||||
@@ -324,6 +339,8 @@
|
|||||||
| `skip_weekends` | true | 时间 |
|
| `skip_weekends` | true | 时间 |
|
||||||
| `min_option_hours` | 12 | 选约 |
|
| `min_option_hours` | 12 | 选约 |
|
||||||
| `min_option_leverage` | 100 | 选约 |
|
| `min_option_leverage` | 100 | 选约 |
|
||||||
|
| `fixed_direction_enabled` | false | 固定方向开关 |
|
||||||
|
| `fixed_perp_side` | long | 固定永续方向 |
|
||||||
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差开关 |
|
| `atm_open_offset_enabled` | false | 开仓 ATM 偏差开关 |
|
||||||
| `max_atm_open_offset` | 3 | 开仓 ATM 偏差上限 |
|
| `max_atm_open_offset` | 3 | 开仓 ATM 偏差上限 |
|
||||||
| `close_bid_mark_max_pct` | 30 | 平仓流动性 |
|
| `close_bid_mark_max_pct` | 30 | 平仓流动性 |
|
||||||
@@ -334,6 +351,7 @@
|
|||||||
|
|
||||||
| 日期 | 说明 |
|
| 日期 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 2026-07-24 | 选约增加固定方向开关:永续多→Put / 空→Call,期权仅实值或平值;关则沿用 ATM/比价 |
|
||||||
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
|
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
|
||||||
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
|
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
|
||||||
| 2026-07-26 | 到期按内在价值结算(对齐实盘);紧急平仓仍用 max(买一,标记,内在价值) |
|
| 2026-07-26 | 到期按内在价值结算(对齐实盘);紧急平仓仍用 max(买一,标记,内在价值) |
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ export type PlanState = {
|
|||||||
min_option_leverage: number;
|
min_option_leverage: number;
|
||||||
atm_open_offset_enabled?: boolean;
|
atm_open_offset_enabled?: boolean;
|
||||||
max_atm_open_offset?: number;
|
max_atm_open_offset?: number;
|
||||||
|
fixed_direction_enabled?: boolean;
|
||||||
|
fixed_perp_side?: "long" | "short";
|
||||||
can_open: boolean;
|
can_open: boolean;
|
||||||
open_capacity?: {
|
open_capacity?: {
|
||||||
leverage?: number;
|
leverage?: number;
|
||||||
@@ -299,6 +301,29 @@ export type PlanState = {
|
|||||||
live_ready_reason?: string;
|
live_ready_reason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StrategySettings = {
|
||||||
|
fee_rate: number;
|
||||||
|
exit_mode: "fixed_usdt" | "premium_multiple";
|
||||||
|
net_profit_target: number;
|
||||||
|
premium_exit_multiple: number;
|
||||||
|
rest_seconds: number;
|
||||||
|
live_order_interval_sec?: number;
|
||||||
|
skip_weekends?: boolean;
|
||||||
|
initial_equity?: number;
|
||||||
|
leverage?: number;
|
||||||
|
min_option_hours?: number;
|
||||||
|
min_option_leverage?: number;
|
||||||
|
atm_open_offset_enabled?: boolean;
|
||||||
|
max_atm_open_offset?: number;
|
||||||
|
fixed_direction_enabled?: boolean;
|
||||||
|
fixed_perp_side?: "long" | "short";
|
||||||
|
close_bid_mark_max_pct?: number;
|
||||||
|
perp_qty_eth?: number;
|
||||||
|
option_qty_eth?: number;
|
||||||
|
show_manual_trade_buttons?: boolean;
|
||||||
|
exchange?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type RuntimeSettings = {
|
export type RuntimeSettings = {
|
||||||
mode: "SIM" | "LIVE";
|
mode: "SIM" | "LIVE";
|
||||||
exchange: string;
|
exchange: string;
|
||||||
|
|||||||
@@ -130,9 +130,13 @@ export default function PlanPage() {
|
|||||||
|
|
||||||
const bias = snap?.ask_compare?.bias;
|
const bias = snap?.ask_compare?.bias;
|
||||||
const biasTag =
|
const biasTag =
|
||||||
bias === "strike_below_spot" || bias === "call_ask_gt_put" ? (
|
bias === "strike_below_spot" ||
|
||||||
|
bias === "call_ask_gt_put" ||
|
||||||
|
bias === "fixed_short_call" ? (
|
||||||
<span className="tag up">买 Call + 永续空</span>
|
<span className="tag up">买 Call + 永续空</span>
|
||||||
) : bias === "strike_above_spot" || bias === "put_ask_gt_call" ? (
|
) : bias === "strike_above_spot" ||
|
||||||
|
bias === "put_ask_gt_call" ||
|
||||||
|
bias === "fixed_long_put" ? (
|
||||||
<span className="tag down">买 Put + 永续多</span>
|
<span className="tag down">买 Put + 永续多</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="tag">等待 / 相等</span>
|
<span className="tag">等待 / 相等</span>
|
||||||
@@ -149,7 +153,11 @@ export default function PlanPage() {
|
|||||||
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`
|
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`
|
||||||
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
|
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
|
||||||
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
|
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
|
||||||
const atmRule = plan?.atm_open_offset_enabled
|
const atmRule = plan?.fixed_direction_enabled
|
||||||
|
? plan?.fixed_perp_side === "short"
|
||||||
|
? "固定:永续空+买Call(实/平)"
|
||||||
|
: "固定:永续多+买Put(实/平)"
|
||||||
|
: plan?.atm_open_offset_enabled
|
||||||
? `ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}`
|
? `ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}`
|
||||||
: "ATM偏差关";
|
: "ATM偏差关";
|
||||||
const exitDetail =
|
const exitDetail =
|
||||||
@@ -337,7 +345,11 @@ export default function PlanPage() {
|
|||||||
<span className="mono">
|
<span className="mono">
|
||||||
剩余≥{fmt(plan?.min_option_hours, 0)}h · 期权杠杆≥
|
剩余≥{fmt(plan?.min_option_hours, 0)}h · 期权杠杆≥
|
||||||
{fmt(plan?.min_option_leverage, 0)}x
|
{fmt(plan?.min_option_leverage, 0)}x
|
||||||
{plan?.atm_open_offset_enabled
|
{plan?.fixed_direction_enabled
|
||||||
|
? plan?.fixed_perp_side === "short"
|
||||||
|
? " · 固定空+Call(实/平)"
|
||||||
|
: " · 固定多+Put(实/平)"
|
||||||
|
: plan?.atm_open_offset_enabled
|
||||||
? ` · ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}`
|
? ` · ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}`
|
||||||
: " · ATM偏差关"}
|
: " · ATM偏差关"}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export default function SettingsPage() {
|
|||||||
const [minOptLev, setMinOptLev] = useState(100);
|
const [minOptLev, setMinOptLev] = useState(100);
|
||||||
const [atmOffOn, setAtmOffOn] = useState(false);
|
const [atmOffOn, setAtmOffOn] = useState(false);
|
||||||
const [maxAtmOff, setMaxAtmOff] = useState(3);
|
const [maxAtmOff, setMaxAtmOff] = useState(3);
|
||||||
|
const [fixedDirOn, setFixedDirOn] = useState(false);
|
||||||
|
const [fixedPerpSide, setFixedPerpSide] = useState<"long" | "short">("long");
|
||||||
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);
|
||||||
@@ -158,6 +160,8 @@ export default function SettingsPage() {
|
|||||||
setMinOptLev(s.min_option_leverage ?? 100);
|
setMinOptLev(s.min_option_leverage ?? 100);
|
||||||
setAtmOffOn(s.atm_open_offset_enabled === true);
|
setAtmOffOn(s.atm_open_offset_enabled === true);
|
||||||
setMaxAtmOff(s.max_atm_open_offset ?? 3);
|
setMaxAtmOff(s.max_atm_open_offset ?? 3);
|
||||||
|
setFixedDirOn(s.fixed_direction_enabled === true);
|
||||||
|
setFixedPerpSide(s.fixed_perp_side === "short" ? "short" : "long");
|
||||||
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);
|
||||||
@@ -302,6 +306,8 @@ export default function SettingsPage() {
|
|||||||
min_option_leverage: minOptLev,
|
min_option_leverage: minOptLev,
|
||||||
atm_open_offset_enabled: atmOffOn,
|
atm_open_offset_enabled: atmOffOn,
|
||||||
max_atm_open_offset: maxAtmOff,
|
max_atm_open_offset: maxAtmOff,
|
||||||
|
fixed_direction_enabled: fixedDirOn,
|
||||||
|
fixed_perp_side: fixedPerpSide,
|
||||||
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,
|
||||||
@@ -610,11 +616,41 @@ export default function SettingsPage() {
|
|||||||
onChange={(e) => setMinOptLev(Number(e.target.value))}
|
onChange={(e) => setMinOptLev(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="fixedDirOn">固定方向</label>
|
||||||
|
<select
|
||||||
|
id="fixedDirOn"
|
||||||
|
className="mono"
|
||||||
|
value={fixedDirOn ? "1" : "0"}
|
||||||
|
onChange={(e) => setFixedDirOn(e.target.value === "1")}
|
||||||
|
>
|
||||||
|
<option value="0">关闭(默认,按现有规则)</option>
|
||||||
|
<option value="1">开启</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="fixedPerp">固定永续方向</label>
|
||||||
|
<select
|
||||||
|
id="fixedPerp"
|
||||||
|
className="mono"
|
||||||
|
disabled={!fixedDirOn}
|
||||||
|
value={fixedPerpSide}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFixedPerpSide(
|
||||||
|
e.target.value === "short" ? "short" : "long",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="long">永续多 · 买 Put(实值/平值)</option>
|
||||||
|
<option value="short">永续空 · 买 Call(实值/平值)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="atmoffOn">开仓 ATM 偏差限制</label>
|
<label htmlFor="atmoffOn">开仓 ATM 偏差限制</label>
|
||||||
<select
|
<select
|
||||||
id="atmoffOn"
|
id="atmoffOn"
|
||||||
className="mono"
|
className="mono"
|
||||||
|
disabled={fixedDirOn}
|
||||||
value={atmOffOn ? "1" : "0"}
|
value={atmOffOn ? "1" : "0"}
|
||||||
onChange={(e) => setAtmOffOn(e.target.value === "1")}
|
onChange={(e) => setAtmOffOn(e.target.value === "1")}
|
||||||
>
|
>
|
||||||
@@ -630,7 +666,7 @@ export default function SettingsPage() {
|
|||||||
type="number"
|
type="number"
|
||||||
step="0.5"
|
step="0.5"
|
||||||
min="0"
|
min="0"
|
||||||
disabled={!atmOffOn}
|
disabled={!atmOffOn || fixedDirOn}
|
||||||
value={maxAtmOff}
|
value={maxAtmOff}
|
||||||
onChange={(e) => setMaxAtmOff(Number(e.target.value))}
|
onChange={(e) => setMaxAtmOff(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
@@ -803,11 +839,15 @@ export default function SettingsPage() {
|
|||||||
{stratSub === "select" ? (
|
{stratSub === "select" ? (
|
||||||
<>
|
<>
|
||||||
<li>
|
<li>
|
||||||
开仓 ATM 偏差限制:关闭时不限制 |ATM −
|
固定方向:默认关。开启后固定永续多→买 Put,或永续空→买
|
||||||
|
Call;期权只选实值或平值,不买虚值。关闭时仍按 ATM/卖一比价规则。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
开仓 ATM 偏差限制:固定方向开启时不适用。关闭时不限制 |ATM −
|
||||||
现价|;开启后超过下方点数则不开仓。
|
现价|;开启后超过下方点数则不开仓。
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
开仓 ATM 最大偏差:仅开关开启时生效。币安行权价较粗时可能长时间无开仓机会。
|
开仓 ATM 最大偏差:仅开关开启且未开固定方向时生效。币安行权价较粗时可能长时间无开仓机会。
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user