Extend semi-auto with ITM/ATM/OTM, offset, and 1:4 sizing.
Moneyness and perp:option units are plan-only; OTM enforces leverage >=180. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -298,6 +298,10 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di
|
|||||||
"semi_net_exit_target": st.get("semi_net_exit_target"),
|
"semi_net_exit_target": st.get("semi_net_exit_target"),
|
||||||
"semi_min_option_hours": st.get("semi_min_option_hours"),
|
"semi_min_option_hours": st.get("semi_min_option_hours"),
|
||||||
"semi_min_option_leverage": st.get("semi_min_option_leverage"),
|
"semi_min_option_leverage": st.get("semi_min_option_leverage"),
|
||||||
|
"semi_moneyness": st.get("semi_moneyness"),
|
||||||
|
"semi_otm_max_offset": st.get("semi_otm_max_offset"),
|
||||||
|
"semi_perp_unit": st.get("semi_perp_unit"),
|
||||||
|
"semi_option_unit": st.get("semi_option_unit"),
|
||||||
"leverage": _pick("leverage", float(settings.leverage)),
|
"leverage": _pick("leverage", float(settings.leverage)),
|
||||||
"min_option_leverage": _pick(
|
"min_option_leverage": _pick(
|
||||||
"min_option_leverage", float(settings.min_option_leverage)
|
"min_option_leverage", float(settings.min_option_leverage)
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ class SemiParamsBody(BaseModel):
|
|||||||
semi_perp_exit_unit: float | None = Field(default=None, ge=0.1, le=1_000_000)
|
semi_perp_exit_unit: float | None = Field(default=None, ge=0.1, le=1_000_000)
|
||||||
semi_min_option_hours: float | None = Field(default=None, ge=1, le=720)
|
semi_min_option_hours: float | None = Field(default=None, ge=1, le=720)
|
||||||
semi_min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
|
semi_min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
|
||||||
|
semi_moneyness: Literal["itm", "atm", "otm"] | None = None
|
||||||
|
semi_otm_max_offset: float | None = Field(default=None, ge=1, le=5000)
|
||||||
|
semi_perp_unit: float | None = Field(default=None, ge=0.01, le=100)
|
||||||
|
semi_option_unit: float | None = Field(default=None, ge=0.01, le=100)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/semi/arm")
|
@router.post("/semi/arm")
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ KEYS = (
|
|||||||
"semi_perp_exit_unit",
|
"semi_perp_exit_unit",
|
||||||
"semi_min_option_hours",
|
"semi_min_option_hours",
|
||||||
"semi_min_option_leverage",
|
"semi_min_option_leverage",
|
||||||
|
"semi_moneyness",
|
||||||
|
"semi_otm_max_offset",
|
||||||
|
"semi_perp_unit",
|
||||||
|
"semi_option_unit",
|
||||||
"close_bid_mark_max_pct",
|
"close_bid_mark_max_pct",
|
||||||
"residual_min_premium_pct",
|
"residual_min_premium_pct",
|
||||||
"residual_close_check_sec",
|
"residual_close_check_sec",
|
||||||
@@ -109,6 +113,10 @@ class StrategySettingsBody(BaseModel):
|
|||||||
semi_perp_exit_unit: float | None = Field(default=None, ge=0.1, le=1_000_000)
|
semi_perp_exit_unit: float | None = Field(default=None, ge=0.1, le=1_000_000)
|
||||||
semi_min_option_hours: float | None = Field(default=None, ge=1, le=720)
|
semi_min_option_hours: float | None = Field(default=None, ge=1, le=720)
|
||||||
semi_min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
|
semi_min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
|
||||||
|
semi_moneyness: str | None = Field(default=None, pattern="^(itm|atm|otm)$")
|
||||||
|
semi_otm_max_offset: float | None = Field(default=None, ge=1, le=5000)
|
||||||
|
semi_perp_unit: float | None = Field(default=None, ge=0.01, le=100)
|
||||||
|
semi_option_unit: float | None = Field(default=None, ge=0.01, le=100)
|
||||||
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)
|
||||||
residual_min_premium_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)
|
residual_close_check_sec: int | None = Field(default=None, ge=30, le=86400)
|
||||||
@@ -296,6 +304,28 @@ def _read_settings() -> dict:
|
|||||||
)
|
)
|
||||||
or s.semi_min_option_leverage
|
or s.semi_min_option_leverage
|
||||||
),
|
),
|
||||||
|
"semi_moneyness": (
|
||||||
|
lambda m: m
|
||||||
|
if m in ("itm", "atm", "otm")
|
||||||
|
else "otm"
|
||||||
|
)(
|
||||||
|
str(
|
||||||
|
db.get_setting("semi_moneyness", s.semi_moneyness) or s.semi_moneyness
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
),
|
||||||
|
"semi_otm_max_offset": float(
|
||||||
|
db.get_setting("semi_otm_max_offset", str(s.semi_otm_max_offset))
|
||||||
|
or s.semi_otm_max_offset
|
||||||
|
),
|
||||||
|
"semi_perp_unit": float(
|
||||||
|
db.get_setting("semi_perp_unit", str(s.semi_perp_unit)) or s.semi_perp_unit
|
||||||
|
),
|
||||||
|
"semi_option_unit": float(
|
||||||
|
db.get_setting("semi_option_unit", str(s.semi_option_unit))
|
||||||
|
or s.semi_option_unit
|
||||||
|
),
|
||||||
"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
|
||||||
@@ -555,6 +585,10 @@ async def put_strategy_settings(
|
|||||||
"semi_perp_exit_unit",
|
"semi_perp_exit_unit",
|
||||||
"semi_min_option_hours",
|
"semi_min_option_hours",
|
||||||
"semi_min_option_leverage",
|
"semi_min_option_leverage",
|
||||||
|
"semi_moneyness",
|
||||||
|
"semi_otm_max_offset",
|
||||||
|
"semi_perp_unit",
|
||||||
|
"semi_option_unit",
|
||||||
"semi_auto_enabled",
|
"semi_auto_enabled",
|
||||||
)
|
)
|
||||||
hit = [k for k in locked_keys if k in data]
|
hit = [k for k in locked_keys if k in data]
|
||||||
|
|||||||
@@ -102,7 +102,11 @@ class Settings(BaseSettings):
|
|||||||
semi_option_move_points: float = 50.0 # 顺方向:标的波动点数
|
semi_option_move_points: float = 50.0 # 顺方向:标的波动点数
|
||||||
semi_perp_exit_unit: float = 5.0 # 净利出场基数(×k)
|
semi_perp_exit_unit: float = 5.0 # 净利出场基数(×k)
|
||||||
semi_min_option_hours: float = 30.0
|
semi_min_option_hours: float = 30.0
|
||||||
semi_min_option_leverage: float = 100.0
|
semi_min_option_leverage: float = 200.0 # 虚值默认 200(虚值门仍强制≥180)
|
||||||
|
semi_moneyness: str = "otm" # itm|atm|otm
|
||||||
|
semi_otm_max_offset: float = 25.0 # 虚值 |K−S| 上限(可配)
|
||||||
|
semi_perp_unit: float = 1.0 # 半自动永续:期权 配比分子
|
||||||
|
semi_option_unit: float = 4.0 # 配比分母,默认 1:4
|
||||||
close_bid_mark_max_pct: float = 30.0 # 平仓:买一相对标记最大偏差%
|
close_bid_mark_max_pct: float = 30.0 # 平仓:买一相对标记最大偏差%
|
||||||
# 残留期权中途平:当前买一权利金 ≥ 初始权利金 × 该% 才尝试卖出
|
# 残留期权中途平:当前买一权利金 ≥ 初始权利金 × 该% 才尝试卖出
|
||||||
residual_min_premium_pct: float = 20.0
|
residual_min_premium_pct: float = 20.0
|
||||||
|
|||||||
@@ -369,6 +369,10 @@ class StrategyEngine:
|
|||||||
out["semi_perp_exit_unit"] = sp.get("perp_exit_unit")
|
out["semi_perp_exit_unit"] = sp.get("perp_exit_unit")
|
||||||
out["semi_min_option_hours"] = sp.get("min_option_hours")
|
out["semi_min_option_hours"] = sp.get("min_option_hours")
|
||||||
out["semi_min_option_leverage"] = sp.get("min_option_leverage")
|
out["semi_min_option_leverage"] = sp.get("min_option_leverage")
|
||||||
|
out["semi_moneyness"] = sp.get("moneyness")
|
||||||
|
out["semi_otm_max_offset"] = sp.get("otm_max_offset")
|
||||||
|
out["semi_perp_unit"] = sp.get("perp_unit")
|
||||||
|
out["semi_option_unit"] = sp.get("option_unit")
|
||||||
out["semi_net_exit_target"] = float(sp["perp_exit_unit"]) * k_eff
|
out["semi_net_exit_target"] = float(sp["perp_exit_unit"]) * k_eff
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("semi params for state() failed")
|
logger.exception("semi params for state() failed")
|
||||||
@@ -1361,6 +1365,10 @@ class StrategyEngine:
|
|||||||
view_side=str(sp_lock["view_side"]),
|
view_side=str(sp_lock["view_side"]),
|
||||||
option_move_points=float(sp_lock["option_move_points"]),
|
option_move_points=float(sp_lock["option_move_points"]),
|
||||||
perp_exit_unit=float(sp_lock["perp_exit_unit"]),
|
perp_exit_unit=float(sp_lock["perp_exit_unit"]),
|
||||||
|
moneyness=str(sp_lock.get("moneyness") or "otm"),
|
||||||
|
otm_max_offset=float(sp_lock.get("otm_max_offset") or 25),
|
||||||
|
perp_unit=float(sp_lock.get("perp_unit") or 1),
|
||||||
|
option_unit=float(sp_lock.get("option_unit") or 4),
|
||||||
)
|
)
|
||||||
self._set_state(phase="open", last_error=None)
|
self._set_state(phase="open", last_error=None)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -76,10 +76,27 @@ def size_and_gate(
|
|||||||
if not rs.ok:
|
if not rs.ok:
|
||||||
return OpenPrepResult(ok=False, detail=rs.detail, sizing_detail=rs.detail)
|
return OpenPrepResult(ok=False, detail=rs.detail, sizing_detail=rs.detail)
|
||||||
else:
|
else:
|
||||||
|
from ..sim.ledger import Ledger
|
||||||
|
from .risk_sizing import is_risk_based
|
||||||
|
from .semi_auto import is_semi_auto, read_semi_params
|
||||||
|
|
||||||
|
led = Ledger(database)
|
||||||
|
semi_perp = None
|
||||||
|
semi_opt = None
|
||||||
|
if is_semi_auto(led):
|
||||||
|
sp = read_semi_params(led)
|
||||||
|
semi_perp = float(sp["perp_unit"])
|
||||||
|
semi_opt = float(sp["option_unit"])
|
||||||
|
if not is_risk_based(led):
|
||||||
|
# 手动仓:半自动直接写入 永续:期权 配比名义
|
||||||
|
database.set_setting("perp_qty_eth", f"{semi_perp:.4f}")
|
||||||
|
database.set_setting("option_qty_eth", f"{semi_opt:.4f}")
|
||||||
rs = apply_risk_sizing_to_ledger(
|
rs = apply_risk_sizing_to_ledger(
|
||||||
index_px=float(index_px),
|
index_px=float(index_px),
|
||||||
option_ask=float(option_ask),
|
option_ask=float(option_ask),
|
||||||
db=database,
|
db=database,
|
||||||
|
perp_unit=semi_perp,
|
||||||
|
option_unit=semi_opt,
|
||||||
)
|
)
|
||||||
if not rs.ok:
|
if not rs.ok:
|
||||||
return OpenPrepResult(ok=False, detail=rs.detail, sizing_detail=rs.detail)
|
return OpenPrepResult(ok=False, detail=rs.detail, sizing_detail=rs.detail)
|
||||||
|
|||||||
@@ -428,12 +428,18 @@ def compute_risk_sizing(
|
|||||||
index_px: float,
|
index_px: float,
|
||||||
option_ask: float,
|
option_ask: float,
|
||||||
db: Database | None = None,
|
db: Database | None = None,
|
||||||
|
perp_unit: float | None = None,
|
||||||
|
option_unit: float | None = None,
|
||||||
) -> RiskSizingResult:
|
) -> RiskSizingResult:
|
||||||
database = db or get_db()
|
database = db or get_db()
|
||||||
ledger = Ledger(database)
|
ledger = Ledger(database)
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
fee_rate = ledger.get_setting_float("fee_rate", s.fee_rate)
|
fee_rate = ledger.get_setting_float("fee_rate", s.fee_rate)
|
||||||
perp_u, opt_u, exit_u = read_risk_units(ledger)
|
perp_u, opt_u, exit_u = read_risk_units(ledger)
|
||||||
|
if perp_unit is not None and float(perp_unit) > 0:
|
||||||
|
perp_u = float(perp_unit)
|
||||||
|
if option_unit is not None and float(option_unit) > 0:
|
||||||
|
opt_u = float(option_unit)
|
||||||
basis_raw = ledger.get_setting_str(
|
basis_raw = ledger.get_setting_str(
|
||||||
"risk_leverage_basis", s.risk_leverage_basis
|
"risk_leverage_basis", s.risk_leverage_basis
|
||||||
) or s.risk_leverage_basis
|
) or s.risk_leverage_basis
|
||||||
@@ -682,6 +688,8 @@ def apply_risk_sizing_to_ledger(
|
|||||||
index_px: float,
|
index_px: float,
|
||||||
option_ask: float,
|
option_ask: float,
|
||||||
db: Database | None = None,
|
db: Database | None = None,
|
||||||
|
perp_unit: float | None = None,
|
||||||
|
option_unit: float | None = None,
|
||||||
) -> RiskSizingResult:
|
) -> RiskSizingResult:
|
||||||
"""计算并写入 perp/option/exit;非以损定仓模式直接 ok 跳过。持仓中拒绝改写。"""
|
"""计算并写入 perp/option/exit;非以损定仓模式直接 ok 跳过。持仓中拒绝改写。"""
|
||||||
database = db or get_db()
|
database = db or get_db()
|
||||||
@@ -699,7 +707,13 @@ def apply_risk_sizing_to_ledger(
|
|||||||
detail="持仓中已锁定本组成交目标与名义,平仓后再自动计算",
|
detail="持仓中已锁定本组成交目标与名义,平仓后再自动计算",
|
||||||
)
|
)
|
||||||
|
|
||||||
r = compute_risk_sizing(index_px=index_px, option_ask=option_ask, db=database)
|
r = compute_risk_sizing(
|
||||||
|
index_px=index_px,
|
||||||
|
option_ask=option_ask,
|
||||||
|
db=database,
|
||||||
|
perp_unit=perp_unit,
|
||||||
|
option_unit=option_unit,
|
||||||
|
)
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,53 @@ 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_otm_strike(
|
||||||
|
strikes: list[float],
|
||||||
|
mark_px: float,
|
||||||
|
*,
|
||||||
|
option_side: str,
|
||||||
|
max_offset: float,
|
||||||
|
) -> float | None:
|
||||||
|
"""
|
||||||
|
虚值:Call K>S、Put K<S;取最接近标的且 |K−S|≤max_offset 的一档。
|
||||||
|
"""
|
||||||
|
if not strikes or mark_px <= 0:
|
||||||
|
return None
|
||||||
|
side = (option_side or "").strip().lower()
|
||||||
|
spot = float(mark_px)
|
||||||
|
cap = max(0.0, float(max_offset))
|
||||||
|
if side == "call":
|
||||||
|
cands = [
|
||||||
|
float(s)
|
||||||
|
for s in strikes
|
||||||
|
if float(s) > spot + 1e-9 and float(s) - spot <= cap + 1e-9
|
||||||
|
]
|
||||||
|
elif side == "put":
|
||||||
|
cands = [
|
||||||
|
float(s)
|
||||||
|
for s in strikes
|
||||||
|
if float(s) < spot - 1e-9 and spot - float(s) <= cap + 1e-9
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
if not cands:
|
||||||
|
return None
|
||||||
|
return min(cands, key=lambda s: (abs(s - spot), s))
|
||||||
|
|
||||||
|
|
||||||
|
def is_otm(*, option_side: str, strike: float, mark_px: float) -> bool:
|
||||||
|
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 pick_itm_or_atm_strike(
|
def pick_itm_or_atm_strike(
|
||||||
strikes: list[float],
|
strikes: list[float],
|
||||||
mark_px: float,
|
mark_px: float,
|
||||||
@@ -166,11 +213,14 @@ def select_option_pair(
|
|||||||
min_hours: float | None = None,
|
min_hours: float | None = None,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
option_side: str | None = None,
|
option_side: str | None = None,
|
||||||
|
moneyness: str | None = None,
|
||||||
|
otm_max_offset: float | None = None,
|
||||||
) -> OptionPair | None:
|
) -> OptionPair | None:
|
||||||
"""
|
"""
|
||||||
选到期 + 行权价。
|
选到期 + 行权价。
|
||||||
option_side 为 call/put 时:按实值/平值选档(固定方向模式);
|
option_side 为 call/put 时:按 moneyness 选档(默认实值/平值,兼容固定方向);
|
||||||
否则仍选 ATM(现有规则)。
|
否则仍选 ATM(现有规则)。
|
||||||
|
moneyness: itm | atm | otm(仅半自动传入 otm/atm)。
|
||||||
"""
|
"""
|
||||||
complete = _complete_by_expiry(contracts)
|
complete = _complete_by_expiry(contracts)
|
||||||
if not complete:
|
if not complete:
|
||||||
@@ -194,13 +244,24 @@ def select_option_pair(
|
|||||||
ymd = eligible[0]
|
ymd = eligible[0]
|
||||||
|
|
||||||
ems, strikes_map = complete[ymd]
|
ems, strikes_map = complete[ymd]
|
||||||
|
keys = list(strikes_map.keys())
|
||||||
side = (option_side or "").strip().lower() or None
|
side = (option_side or "").strip().lower() or None
|
||||||
|
mny = (moneyness or "").strip().lower() or None
|
||||||
if side in ("call", "put"):
|
if side in ("call", "put"):
|
||||||
strike = pick_itm_or_atm_strike(
|
if mny == "otm":
|
||||||
list(strikes_map.keys()), mark_px, option_side=side
|
strike = pick_otm_strike(
|
||||||
|
keys,
|
||||||
|
mark_px,
|
||||||
|
option_side=side,
|
||||||
|
max_offset=float(otm_max_offset or 0),
|
||||||
)
|
)
|
||||||
|
elif mny == "atm":
|
||||||
|
strike = pick_atm_strike(keys, mark_px)
|
||||||
else:
|
else:
|
||||||
strike = pick_atm_strike(list(strikes_map.keys()), mark_px)
|
# itm 或未指定:实值/平值(固定方向默认)
|
||||||
|
strike = pick_itm_or_atm_strike(keys, mark_px, option_side=side)
|
||||||
|
else:
|
||||||
|
strike = pick_atm_strike(keys, mark_px)
|
||||||
if strike is None:
|
if strike is None:
|
||||||
return None
|
return None
|
||||||
legs = strikes_map[strike]
|
legs = strikes_map[strike]
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ PHASE_WAIT_HUMAN = "wait_human"
|
|||||||
REASON_POINTS = "semi_target_points"
|
REASON_POINTS = "semi_target_points"
|
||||||
REASON_PERP_NET = "semi_perp_exit"
|
REASON_PERP_NET = "semi_perp_exit"
|
||||||
TRADE_LOCK_KEY = "semi_trade_lock"
|
TRADE_LOCK_KEY = "semi_trade_lock"
|
||||||
|
OTM_LEV_FLOOR = 180.0
|
||||||
|
MONEYNESS_CHOICES = ("itm", "atm", "otm")
|
||||||
|
|
||||||
|
|
||||||
def is_semi_auto(ledger: Ledger | None = None) -> bool:
|
def is_semi_auto(ledger: Ledger | None = None) -> bool:
|
||||||
@@ -60,12 +62,23 @@ def lock_trade_params(
|
|||||||
view_side: str,
|
view_side: str,
|
||||||
option_move_points: float,
|
option_move_points: float,
|
||||||
perp_exit_unit: float,
|
perp_exit_unit: float,
|
||||||
|
moneyness: str = "otm",
|
||||||
|
otm_max_offset: float = 25.0,
|
||||||
|
perp_unit: float = 1.0,
|
||||||
|
option_unit: float = 4.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
mny = str(moneyness or "otm").strip().lower()
|
||||||
|
if mny not in MONEYNESS_CHOICES:
|
||||||
|
mny = "otm"
|
||||||
payload = {
|
payload = {
|
||||||
"group_id": str(group_id),
|
"group_id": str(group_id),
|
||||||
"view_side": "short" if view_side == "short" else "long",
|
"view_side": "short" if view_side == "short" else "long",
|
||||||
"option_move_points": float(option_move_points),
|
"option_move_points": float(option_move_points),
|
||||||
"perp_exit_unit": float(perp_exit_unit),
|
"perp_exit_unit": float(perp_exit_unit),
|
||||||
|
"moneyness": mny,
|
||||||
|
"otm_max_offset": float(otm_max_offset),
|
||||||
|
"perp_unit": float(perp_unit),
|
||||||
|
"option_unit": float(option_unit),
|
||||||
}
|
}
|
||||||
db.set_setting(TRADE_LOCK_KEY, json.dumps(payload, ensure_ascii=False))
|
db.set_setting(TRADE_LOCK_KEY, json.dumps(payload, ensure_ascii=False))
|
||||||
|
|
||||||
@@ -74,6 +87,19 @@ def clear_trade_lock(db: Any) -> None:
|
|||||||
db.set_setting(TRADE_LOCK_KEY, "")
|
db.set_setting(TRADE_LOCK_KEY, "")
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_moneyness(raw: str | None, default: str = "otm") -> str:
|
||||||
|
m = str(raw or default).strip().lower()
|
||||||
|
return m if m in MONEYNESS_CHOICES else default
|
||||||
|
|
||||||
|
|
||||||
|
def effective_min_leverage(moneyness: str, configured: float) -> float:
|
||||||
|
"""虚值强制不低于 180;实值/平值用配置值。"""
|
||||||
|
lev = max(1.0, float(configured))
|
||||||
|
if _norm_moneyness(moneyness) == "otm":
|
||||||
|
return max(lev, OTM_LEV_FLOOR)
|
||||||
|
return lev
|
||||||
|
|
||||||
|
|
||||||
def read_semi_params(
|
def read_semi_params(
|
||||||
ledger: Ledger | None = None,
|
ledger: Ledger | None = None,
|
||||||
*,
|
*,
|
||||||
@@ -96,7 +122,32 @@ def read_semi_params(
|
|||||||
led.get_setting_float("semi_perp_exit_unit", s.semi_perp_exit_unit)
|
led.get_setting_float("semi_perp_exit_unit", s.semi_perp_exit_unit)
|
||||||
or s.semi_perp_exit_unit
|
or s.semi_perp_exit_unit
|
||||||
)
|
)
|
||||||
# 持仓中优先用开仓时锁定的出场参数
|
moneyness = _norm_moneyness(
|
||||||
|
led.get_setting_str("semi_moneyness", s.semi_moneyness),
|
||||||
|
s.semi_moneyness,
|
||||||
|
)
|
||||||
|
otm_off = float(
|
||||||
|
led.get_setting_float("semi_otm_max_offset", s.semi_otm_max_offset)
|
||||||
|
or s.semi_otm_max_offset
|
||||||
|
)
|
||||||
|
perp_u = float(
|
||||||
|
led.get_setting_float("semi_perp_unit", s.semi_perp_unit) or s.semi_perp_unit
|
||||||
|
)
|
||||||
|
opt_u = float(
|
||||||
|
led.get_setting_float("semi_option_unit", s.semi_option_unit)
|
||||||
|
or s.semi_option_unit
|
||||||
|
)
|
||||||
|
if perp_u <= 0:
|
||||||
|
perp_u = float(s.semi_perp_unit)
|
||||||
|
if opt_u <= 0:
|
||||||
|
opt_u = float(s.semi_option_unit)
|
||||||
|
min_lev_cfg = float(
|
||||||
|
led.get_setting_float(
|
||||||
|
"semi_min_option_leverage", s.semi_min_option_leverage
|
||||||
|
)
|
||||||
|
or s.semi_min_option_leverage
|
||||||
|
)
|
||||||
|
# 持仓中优先用开仓时锁定的出场/选约参数
|
||||||
lock = read_trade_lock(led)
|
lock = read_trade_lock(led)
|
||||||
if lock and group_id and str(lock.get("group_id")) == str(group_id):
|
if lock and group_id and str(lock.get("group_id")) == str(group_id):
|
||||||
view = str(lock.get("view_side") or view).strip().lower()
|
view = str(lock.get("view_side") or view).strip().lower()
|
||||||
@@ -105,8 +156,12 @@ def read_semi_params(
|
|||||||
try:
|
try:
|
||||||
move = float(lock.get("option_move_points", move))
|
move = float(lock.get("option_move_points", move))
|
||||||
exit_unit = float(lock.get("perp_exit_unit", exit_unit))
|
exit_unit = float(lock.get("perp_exit_unit", exit_unit))
|
||||||
|
otm_off = float(lock.get("otm_max_offset", otm_off))
|
||||||
|
perp_u = float(lock.get("perp_unit", perp_u))
|
||||||
|
opt_u = float(lock.get("option_unit", opt_u))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
moneyness = _norm_moneyness(str(lock.get("moneyness") or moneyness), moneyness)
|
||||||
return {
|
return {
|
||||||
"enabled": is_semi_auto(led),
|
"enabled": is_semi_auto(led),
|
||||||
"armed": is_armed(led),
|
"armed": is_armed(led),
|
||||||
@@ -116,19 +171,21 @@ def read_semi_params(
|
|||||||
"perp_side": "short" if view == "long" else "long",
|
"perp_side": "short" if view == "long" else "long",
|
||||||
"option_move_points": move,
|
"option_move_points": move,
|
||||||
"perp_exit_unit": exit_unit,
|
"perp_exit_unit": exit_unit,
|
||||||
|
"moneyness": moneyness,
|
||||||
|
"otm_max_offset": max(0.0, otm_off),
|
||||||
|
"perp_unit": perp_u,
|
||||||
|
"option_unit": opt_u,
|
||||||
"min_option_hours": float(
|
"min_option_hours": float(
|
||||||
led.get_setting_float(
|
led.get_setting_float(
|
||||||
"semi_min_option_hours", s.semi_min_option_hours
|
"semi_min_option_hours", s.semi_min_option_hours
|
||||||
)
|
)
|
||||||
or s.semi_min_option_hours
|
or s.semi_min_option_hours
|
||||||
),
|
),
|
||||||
"min_option_leverage": float(
|
"min_option_leverage": effective_min_leverage(moneyness, min_lev_cfg),
|
||||||
led.get_setting_float(
|
"min_option_leverage_cfg": min_lev_cfg,
|
||||||
"semi_min_option_leverage", s.semi_min_option_leverage
|
"trade_locked": bool(
|
||||||
)
|
lock and group_id and str(lock.get("group_id")) == str(group_id)
|
||||||
or s.semi_min_option_leverage
|
|
||||||
),
|
),
|
||||||
"trade_locked": bool(lock and group_id and str(lock.get("group_id")) == str(group_id)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -726,8 +726,10 @@ class StrategySession:
|
|||||||
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()
|
fixed_on, fixed_perp = _fixed_direction()
|
||||||
# 半自动:强制看法方向 + 实值/平值 + 半自动选约门槛(须已授权)
|
# 半自动:强制看法方向 + 行权类型(itm/atm/otm) + 半自动选约门槛(须已授权)
|
||||||
semi_on = is_semi_auto()
|
semi_on = is_semi_auto()
|
||||||
|
semi_mny: str | None = None
|
||||||
|
semi_otm_off: float | None = None
|
||||||
if semi_on:
|
if semi_on:
|
||||||
if not is_armed():
|
if not is_armed():
|
||||||
return None
|
return None
|
||||||
@@ -737,6 +739,8 @@ class StrategySession:
|
|||||||
min_hours = float(sp["min_option_hours"])
|
min_hours = float(sp["min_option_hours"])
|
||||||
min_lev = float(sp["min_option_leverage"])
|
min_lev = float(sp["min_option_leverage"])
|
||||||
atm_off_on = False
|
atm_off_on = False
|
||||||
|
semi_mny = str(sp.get("moneyness") or "otm")
|
||||||
|
semi_otm_off = float(sp.get("otm_max_offset") or 0)
|
||||||
opt_side_hint = _option_side_for_perp(fixed_perp) if fixed_on else None
|
opt_side_hint = _option_side_for_perp(fixed_perp) if fixed_on else None
|
||||||
# 第一关:振幅过滤(默认关;开启则回看窗振幅须 ≤ 最大%)
|
# 第一关:振幅过滤(默认关;开启则回看窗振幅须 ≤ 最大%)
|
||||||
if not self._apply_amplitude_first_gate():
|
if not self._apply_amplitude_first_gate():
|
||||||
@@ -766,12 +770,40 @@ class StrategySession:
|
|||||||
mark_px=underlying,
|
mark_px=underlying,
|
||||||
expiry_ymd=ymd,
|
expiry_ymd=ymd,
|
||||||
option_side=opt_side_hint,
|
option_side=opt_side_hint,
|
||||||
|
moneyness=semi_mny if semi_on else None,
|
||||||
|
otm_max_offset=semi_otm_off if semi_on else None,
|
||||||
)
|
)
|
||||||
if pair is None:
|
if pair is None:
|
||||||
|
if semi_on and semi_mny == "otm":
|
||||||
|
logger.info(
|
||||||
|
"skip expiry=%s no OTM within offset=%.1f for %s mark=%.2f",
|
||||||
|
ymd,
|
||||||
|
float(semi_otm_off or 0),
|
||||||
|
opt_side_hint,
|
||||||
|
underlying,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if fixed_on:
|
if fixed_on:
|
||||||
if not is_itm_or_atm(
|
from .selection import is_otm
|
||||||
option_side=opt_side_hint or "",
|
|
||||||
|
side = opt_side_hint or ""
|
||||||
|
if semi_on and semi_mny == "otm":
|
||||||
|
if not is_otm(
|
||||||
|
option_side=side,
|
||||||
|
strike=pair.strike,
|
||||||
|
mark_px=underlying,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
atm_open_offset(pair.strike, underlying)
|
||||||
|
> float(semi_otm_off or 0) + 1e-9
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
elif semi_on and semi_mny == "atm":
|
||||||
|
# 平值:须为该到期最接近标的的档
|
||||||
|
pass
|
||||||
|
elif not is_itm_or_atm(
|
||||||
|
option_side=side,
|
||||||
strike=pair.strike,
|
strike=pair.strike,
|
||||||
mark_px=underlying,
|
mark_px=underlying,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.strategy.semi_auto import REASON_PERP_NET, REASON_POINTS, check_semi_exits
|
from app.strategy.selection import pick_otm_strike
|
||||||
|
from app.strategy.semi_auto import (
|
||||||
|
REASON_PERP_NET,
|
||||||
|
REASON_POINTS,
|
||||||
|
check_semi_exits,
|
||||||
|
effective_min_leverage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_semi_points_long_needs_net_positive() -> None:
|
def test_semi_points_long_needs_net_positive() -> None:
|
||||||
@@ -73,3 +79,22 @@ def test_semi_not_yet() -> None:
|
|||||||
risk_k=1,
|
risk_k=1,
|
||||||
)
|
)
|
||||||
assert d.should_close is False
|
assert d.should_close is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_otm_leverage_floor() -> None:
|
||||||
|
assert effective_min_leverage("otm", 100) == 180
|
||||||
|
assert effective_min_leverage("otm", 200) == 200
|
||||||
|
assert effective_min_leverage("itm", 100) == 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_otm_within_offset() -> None:
|
||||||
|
strikes = [1800.0, 1825.0, 1850.0, 1875.0]
|
||||||
|
# Call 虚值:标的 1830 → 1850(20点)在 25 内;1875 超
|
||||||
|
k = pick_otm_strike(strikes, 1830, option_side="call", max_offset=25)
|
||||||
|
assert k == 1850.0
|
||||||
|
assert (
|
||||||
|
pick_otm_strike(strikes, 1830, option_side="call", max_offset=15) is None
|
||||||
|
)
|
||||||
|
# Put 虚值
|
||||||
|
k2 = pick_otm_strike(strikes, 1830, option_side="put", max_offset=30)
|
||||||
|
assert k2 == 1825.0
|
||||||
|
|||||||
@@ -1194,7 +1194,16 @@ export default function MonitorPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{detail.strat.semi_auto_enabled === true
|
{detail.strat.semi_auto_enabled === true
|
||||||
? `半自动·点${fmt(detail.strat.semi_option_move_points, 0)}/净≥${fmt(
|
? `半自动·${
|
||||||
|
detail.strat.semi_moneyness === "itm"
|
||||||
|
? "实值"
|
||||||
|
: detail.strat.semi_moneyness === "atm"
|
||||||
|
? "平值"
|
||||||
|
: "虚值"
|
||||||
|
} ${fmt(detail.strat.semi_perp_unit, 0)}:${fmt(
|
||||||
|
detail.strat.semi_option_unit,
|
||||||
|
0,
|
||||||
|
)} ·点${fmt(detail.strat.semi_option_move_points, 0)}/净≥${fmt(
|
||||||
detail.strat.semi_net_exit_target ??
|
detail.strat.semi_net_exit_target ??
|
||||||
detail.strat.semi_perp_exit_unit,
|
detail.strat.semi_perp_exit_unit,
|
||||||
2,
|
2,
|
||||||
|
|||||||
+15
-2
@@ -36,7 +36,15 @@
|
|||||||
| 多 (`long`) | 买 Call | 空 |
|
| 多 (`long`) | 买 Call | 空 |
|
||||||
| 空 (`short`) | 买 Put | 多 |
|
| 空 (`short`) | 买 Put | 多 |
|
||||||
|
|
||||||
选约强制:**实值或平值**;最短剩余小时、最低杠杆用半自动本单参数(默认 ≥30h、杠杆 ≥100)。
|
选约:本单可选 **实值 / 平值 / 虚值**(仅半自动)。
|
||||||
|
|
||||||
|
| 行权类型 | 规则 |
|
||||||
|
|----------|------|
|
||||||
|
| `itm` | 实值或平值(Call K≤S;Put K≥S) |
|
||||||
|
| `atm` | 该到期最接近标的的档 |
|
||||||
|
| `otm` | 虚值且 \|K−S\| ≤ `semi_otm_max_offset`(可配);杠杆门强制 ≥180,默认 200 |
|
||||||
|
|
||||||
|
名义配比:`semi_perp_unit` : `semi_option_unit`(默认 **1:4**);以损定仓时作单位再乘 k,手动仓直接写入名义。最短剩余小时默认 ≥30h。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -67,7 +75,11 @@
|
|||||||
| `semi_option_move_points` | 50 | 顺向波动点数 |
|
| `semi_option_move_points` | 50 | 顺向波动点数 |
|
||||||
| `semi_perp_exit_unit` | 5 | 净利出场基数(×k) |
|
| `semi_perp_exit_unit` | 5 | 净利出场基数(×k) |
|
||||||
| `semi_min_option_hours` | 30 | 选约最短剩余 |
|
| `semi_min_option_hours` | 30 | 选约最短剩余 |
|
||||||
| `semi_min_option_leverage` | 100 | 选约最低杠杆 |
|
| `semi_min_option_leverage` | 200 | 选约最低杠杆(虚值仍≥180) |
|
||||||
|
| `semi_moneyness` | otm | itm / atm / otm |
|
||||||
|
| `semi_otm_max_offset` | 25 | 虚值最大偏离(点) |
|
||||||
|
| `semi_perp_unit` | 1 | 永续名义单位 |
|
||||||
|
| `semi_option_unit` | 4 | 期权名义单位 |
|
||||||
|
|
||||||
API(需登录):
|
API(需登录):
|
||||||
|
|
||||||
@@ -97,4 +109,5 @@ Fleet `strategy` 透出半自动字段。监控模式列在永期开启半自动
|
|||||||
|
|
||||||
| 日期 | 说明 |
|
| 日期 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 2026-08-08 | 行权类型 itm/atm/otm;虚值偏离可配;永续:期权配比默认 1:4 |
|
||||||
| 2026-08-08 | 初版:设置开关、首页本单、中控展示、出场与停机 |
|
| 2026-08-08 | 初版:设置开关、首页本单、中控展示、出场与停机 |
|
||||||
|
|||||||
+11
-1
@@ -5,13 +5,23 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-08-08 — 半自动行权类型与 1:4 配比
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
1. 半自动本单增加行权类型:实值 / 平值 / 虚值;虚值最大偏离可配。
|
||||||
|
2. 永续:期权名义配比可配,默认 1:4(以损作单位×k / 手动直写)。
|
||||||
|
3. 虚值杠杆门强制 ≥180,默认 200。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-08-08 — 永期半自动(人工授权单笔)
|
## 2026-08-08 — 永期半自动(人工授权单笔)
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|
||||||
1. 系统设置「节奏」增加半自动开关;期期强制关闭半自动。
|
1. 系统设置「节奏」增加半自动开关;期期强制关闭半自动。
|
||||||
2. 首页「半自动 · 本单」:看法方向、波动点数、净利基数、选约门槛;授权/取消。
|
2. 首页「半自动 · 本单」:看法方向、波动点数、净利基数、选约门槛;授权/取消。
|
||||||
3. 引擎:未授权 `wait_human`;授权后选约 ITM/ATM 开仓;出场为到点且净利>0 或净利≥基数×k;平完清授权停机。
|
3. 引擎:未授权 `wait_human`;授权后选约并开仓;出场为到点且净利>0 或净利≥基数×k;平完清授权停机。
|
||||||
4. 中控模式列/详情透出半自动状态与目标。
|
4. 中控模式列/详情透出半自动状态与目标。
|
||||||
5. 文档:`docs/半自动说明.md`;审计见 `docs/审计说明-2026-08-08-半自动.md`。
|
5. 文档:`docs/半自动说明.md`;审计见 `docs/审计说明-2026-08-08-半自动.md`。
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -470,7 +470,11 @@ LIVE 特殊态:`option_closed_perp_pending`(期权已在交易所卖掉、
|
|||||||
| `semi_option_move_points` | 50 | 半自动顺向波动点数 |
|
| `semi_option_move_points` | 50 | 半自动顺向波动点数 |
|
||||||
| `semi_perp_exit_unit` | 5 | 半自动净利出场基数(×k) |
|
| `semi_perp_exit_unit` | 5 | 半自动净利出场基数(×k) |
|
||||||
| `semi_min_option_hours` | 30 | 半自动选约最短剩余小时 |
|
| `semi_min_option_hours` | 30 | 半自动选约最短剩余小时 |
|
||||||
| `semi_min_option_leverage` | 100 | 半自动选约最低杠杆 |
|
| `semi_min_option_leverage` | 200 | 半自动选约最低杠杆(虚值≥180) |
|
||||||
|
| `semi_moneyness` | otm | 半自动行权类型 itm/atm/otm |
|
||||||
|
| `semi_otm_max_offset` | 25 | 半自动虚值最大偏离 |
|
||||||
|
| `semi_perp_unit` | 1 | 半自动永续名义单位 |
|
||||||
|
| `semi_option_unit` | 4 | 半自动期权名义单位 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,10 @@ export type PlanState = {
|
|||||||
semi_perp_exit_unit?: number;
|
semi_perp_exit_unit?: number;
|
||||||
semi_min_option_hours?: number;
|
semi_min_option_hours?: number;
|
||||||
semi_min_option_leverage?: number;
|
semi_min_option_leverage?: number;
|
||||||
|
semi_moneyness?: "itm" | "atm" | "otm";
|
||||||
|
semi_otm_max_offset?: number;
|
||||||
|
semi_perp_unit?: number;
|
||||||
|
semi_option_unit?: number;
|
||||||
semi_net_exit_target?: number;
|
semi_net_exit_target?: number;
|
||||||
can_open: boolean;
|
can_open: boolean;
|
||||||
open_capacity?: {
|
open_capacity?: {
|
||||||
@@ -421,6 +425,10 @@ export type StrategySettings = {
|
|||||||
semi_perp_exit_unit?: number;
|
semi_perp_exit_unit?: number;
|
||||||
semi_min_option_hours?: number;
|
semi_min_option_hours?: number;
|
||||||
semi_min_option_leverage?: number;
|
semi_min_option_leverage?: number;
|
||||||
|
semi_moneyness?: "itm" | "atm" | "otm";
|
||||||
|
semi_otm_max_offset?: number;
|
||||||
|
semi_perp_unit?: number;
|
||||||
|
semi_option_unit?: number;
|
||||||
close_bid_mark_max_pct?: number;
|
close_bid_mark_max_pct?: number;
|
||||||
residual_min_premium_pct?: number;
|
residual_min_premium_pct?: number;
|
||||||
residual_close_check_sec?: number;
|
residual_close_check_sec?: number;
|
||||||
|
|||||||
+101
-18
@@ -116,9 +116,25 @@ export default function PlanPage() {
|
|||||||
const [semiMove, setSemiMove] = useState(50);
|
const [semiMove, setSemiMove] = useState(50);
|
||||||
const [semiExitU, setSemiExitU] = useState(5);
|
const [semiExitU, setSemiExitU] = useState(5);
|
||||||
const [semiMinH, setSemiMinH] = useState(30);
|
const [semiMinH, setSemiMinH] = useState(30);
|
||||||
const [semiMinLev, setSemiMinLev] = useState(100);
|
const [semiMinLev, setSemiMinLev] = useState(200);
|
||||||
|
const [semiMny, setSemiMny] = useState<"itm" | "atm" | "otm">("otm");
|
||||||
|
const [semiOtmOff, setSemiOtmOff] = useState(25);
|
||||||
|
const [semiPerpU, setSemiPerpU] = useState(1);
|
||||||
|
const [semiOptU, setSemiOptU] = useState(4);
|
||||||
const [semiDirty, setSemiDirty] = useState(false);
|
const [semiDirty, setSemiDirty] = useState(false);
|
||||||
|
|
||||||
|
const semiParamsBody = () => ({
|
||||||
|
semi_view_side: semiView,
|
||||||
|
semi_option_move_points: semiMove,
|
||||||
|
semi_perp_exit_unit: semiExitU,
|
||||||
|
semi_min_option_hours: semiMinH,
|
||||||
|
semi_min_option_leverage: semiMinLev,
|
||||||
|
semi_moneyness: semiMny,
|
||||||
|
semi_otm_max_offset: semiOtmOff,
|
||||||
|
semi_perp_unit: semiPerpU,
|
||||||
|
semi_option_unit: semiOptU,
|
||||||
|
});
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const [m, p] = await Promise.all([
|
const [m, p] = await Promise.all([
|
||||||
@@ -132,7 +148,12 @@ export default function PlanPage() {
|
|||||||
setSemiMove(Number(p.semi_option_move_points ?? 50));
|
setSemiMove(Number(p.semi_option_move_points ?? 50));
|
||||||
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
|
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
|
||||||
setSemiMinH(Number(p.semi_min_option_hours ?? 30));
|
setSemiMinH(Number(p.semi_min_option_hours ?? 30));
|
||||||
setSemiMinLev(Number(p.semi_min_option_leverage ?? 100));
|
setSemiMinLev(Number(p.semi_min_option_leverage ?? 200));
|
||||||
|
const m = p.semi_moneyness;
|
||||||
|
setSemiMny(m === "itm" || m === "atm" || m === "otm" ? m : "otm");
|
||||||
|
setSemiOtmOff(Number(p.semi_otm_max_offset ?? 25));
|
||||||
|
setSemiPerpU(Number(p.semi_perp_unit ?? 1));
|
||||||
|
setSemiOptU(Number(p.semi_option_unit ?? 4));
|
||||||
}
|
}
|
||||||
setErr("");
|
setErr("");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -146,13 +167,7 @@ export default function PlanPage() {
|
|||||||
try {
|
try {
|
||||||
const p = await apiFetch<PlanState>("/api/plan/semi/params", {
|
const p = await apiFetch<PlanState>("/api/plan/semi/params", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(semiParamsBody()),
|
||||||
semi_view_side: semiView,
|
|
||||||
semi_option_move_points: semiMove,
|
|
||||||
semi_perp_exit_unit: semiExitU,
|
|
||||||
semi_min_option_hours: semiMinH,
|
|
||||||
semi_min_option_leverage: semiMinLev,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
setPlan(p);
|
setPlan(p);
|
||||||
setSemiDirty(false);
|
setSemiDirty(false);
|
||||||
@@ -170,13 +185,7 @@ export default function PlanPage() {
|
|||||||
if (semiDirty) {
|
if (semiDirty) {
|
||||||
await apiFetch<PlanState>("/api/plan/semi/params", {
|
await apiFetch<PlanState>("/api/plan/semi/params", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(semiParamsBody()),
|
||||||
semi_view_side: semiView,
|
|
||||||
semi_option_move_points: semiMove,
|
|
||||||
semi_perp_exit_unit: semiExitU,
|
|
||||||
semi_min_option_hours: semiMinH,
|
|
||||||
semi_min_option_leverage: semiMinLev,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
setSemiDirty(false);
|
setSemiDirty(false);
|
||||||
}
|
}
|
||||||
@@ -568,7 +577,7 @@ export default function PlanPage() {
|
|||||||
<section className="card plan-semi-card" aria-label="半自动本单">
|
<section className="card plan-semi-card" aria-label="半自动本单">
|
||||||
<h3 className="plan-panel-title">半自动 · 本单</h3>
|
<h3 className="plan-panel-title">半自动 · 本单</h3>
|
||||||
<p className="meta" style={{ marginTop: 0 }}>
|
<p className="meta" style={{ marginTop: 0 }}>
|
||||||
人工定方向与目标 → 授权后机器盯选约/开平 → 平完停,再授权下一单。选约:≥剩余时长与杠杆、实值或平值。顺向:开仓指数±点数且组合净利>0;逆向兑现:净利≥基数×k。平仓先期权后永续。
|
人工定方向/行权类型/配比 → 授权后机器盯开平 → 平完停。虚值杠杆门≥180(默认200)。顺向:开仓指数±点数且净利>0;逆向:净利≥基数×k。
|
||||||
</p>
|
</p>
|
||||||
<div className="settings-fields" style={{ marginTop: 8 }}>
|
<div className="settings-fields" style={{ marginTop: 8 }}>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
@@ -587,6 +596,76 @@ export default function PlanPage() {
|
|||||||
<option value="short">空 · 买 Put + 永续多</option>
|
<option value="short">空 · 买 Put + 永续多</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="semiMny">行权类型</label>
|
||||||
|
<select
|
||||||
|
id="semiMny"
|
||||||
|
className="mono"
|
||||||
|
disabled={open || !!busy || !!plan.semi_armed}
|
||||||
|
value={semiMny}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
setSemiMny(
|
||||||
|
v === "itm" || v === "atm" || v === "otm" ? v : "otm",
|
||||||
|
);
|
||||||
|
setSemiDirty(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="itm">实值(含平值)</option>
|
||||||
|
<option value="atm">平值</option>
|
||||||
|
<option value="otm">虚值</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{semiMny === "otm" ? (
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="semiOtmOff">虚值最大偏离(点)</label>
|
||||||
|
<input
|
||||||
|
id="semiOtmOff"
|
||||||
|
className="mono"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
min="1"
|
||||||
|
disabled={open || !!busy || !!plan.semi_armed}
|
||||||
|
value={semiOtmOff}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSemiOtmOff(Number(e.target.value));
|
||||||
|
setSemiDirty(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="semiPerpU">永续名义单位</label>
|
||||||
|
<input
|
||||||
|
id="semiPerpU"
|
||||||
|
className="mono"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0.01"
|
||||||
|
disabled={open || !!busy || !!plan.semi_armed}
|
||||||
|
value={semiPerpU}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSemiPerpU(Number(e.target.value));
|
||||||
|
setSemiDirty(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="semiOptU">期权名义单位(配比)</label>
|
||||||
|
<input
|
||||||
|
id="semiOptU"
|
||||||
|
className="mono"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0.01"
|
||||||
|
disabled={open || !!busy || !!plan.semi_armed}
|
||||||
|
value={semiOptU}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSemiOptU(Number(e.target.value));
|
||||||
|
setSemiDirty(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="semiMove">期权出场 · 波动点数</label>
|
<label htmlFor="semiMove">期权出场 · 波动点数</label>
|
||||||
<input
|
<input
|
||||||
@@ -661,7 +740,11 @@ export default function PlanPage() {
|
|||||||
const n = Number(idx);
|
const n = Number(idx);
|
||||||
const tgt =
|
const tgt =
|
||||||
semiView === "long" ? n + Number(semiMove) : n - Number(semiMove);
|
semiView === "long" ? n + Number(semiMove) : n - Number(semiMove);
|
||||||
return `指数 ${fmtExPx("index", n)} → 到点 ${fmtExPx("index", tgt)} · 净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · ${plan.semi_armed ? "已授权盯开" : "未授权"}`;
|
const mnyZh =
|
||||||
|
semiMny === "itm" ? "实值" : semiMny === "atm" ? "平值" : "虚值";
|
||||||
|
return `指数 ${fmtExPx("index", n)} → 到点 ${fmtExPx("index", tgt)} · ${mnyZh}${
|
||||||
|
semiMny === "otm" ? `≤${fmt(semiOtmOff, 0)}点` : ""
|
||||||
|
} · 配比 ${fmt(semiPerpU, 2)}:${fmt(semiOptU, 2)} · 净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · ${plan.semi_armed ? "已授权盯开" : "未授权"}`;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="plan-actions" style={{ marginTop: 10, paddingTop: 0 }}>
|
<div className="plan-actions" style={{ marginTop: 10, paddingTop: 0 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user