Reject OO strikes more than 1% from amplitude high/low.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -71,6 +71,7 @@ KEYS = (
|
|||||||
"oo_min_leverage",
|
"oo_min_leverage",
|
||||||
"oo_reward_ratio",
|
"oo_reward_ratio",
|
||||||
"oo_budget_cushion",
|
"oo_budget_cushion",
|
||||||
|
"oo_strike_max_dev_pct",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -126,6 +127,7 @@ class StrategySettingsBody(BaseModel):
|
|||||||
oo_min_leverage: float | None = Field(default=None, ge=1, le=10000)
|
oo_min_leverage: float | None = Field(default=None, ge=1, le=10000)
|
||||||
oo_reward_ratio: float | None = Field(default=None, ge=0.5, le=20)
|
oo_reward_ratio: float | None = Field(default=None, ge=0.5, le=20)
|
||||||
oo_budget_cushion: float | None = Field(default=None, ge=0.5, le=1.0)
|
oo_budget_cushion: float | None = Field(default=None, ge=0.5, le=1.0)
|
||||||
|
oo_strike_max_dev_pct: float | None = Field(default=None, ge=0.1, le=10)
|
||||||
|
|
||||||
|
|
||||||
def _as_bool(raw: str | None, default: bool) -> bool:
|
def _as_bool(raw: str | None, default: bool) -> bool:
|
||||||
@@ -379,6 +381,12 @@ def _read_settings() -> dict:
|
|||||||
db.get_setting("oo_budget_cushion", str(s.oo_budget_cushion))
|
db.get_setting("oo_budget_cushion", str(s.oo_budget_cushion))
|
||||||
or s.oo_budget_cushion
|
or s.oo_budget_cushion
|
||||||
),
|
),
|
||||||
|
"oo_strike_max_dev_pct": float(
|
||||||
|
db.get_setting(
|
||||||
|
"oo_strike_max_dev_pct", str(s.oo_strike_max_dev_pct)
|
||||||
|
)
|
||||||
|
or s.oo_strike_max_dev_pct
|
||||||
|
),
|
||||||
"risk_sizing_preview": _risk_preview_safe(),
|
"risk_sizing_preview": _risk_preview_safe(),
|
||||||
"exchange": rt.exchange,
|
"exchange": rt.exchange,
|
||||||
"perp_inst_id": rt.perp_inst_id,
|
"perp_inst_id": rt.perp_inst_id,
|
||||||
@@ -472,6 +480,7 @@ async def put_strategy_settings(
|
|||||||
"oo_min_leverage",
|
"oo_min_leverage",
|
||||||
"oo_reward_ratio",
|
"oo_reward_ratio",
|
||||||
"oo_budget_cushion",
|
"oo_budget_cushion",
|
||||||
|
"oo_strike_max_dev_pct",
|
||||||
)
|
)
|
||||||
hit = [k for k in locked_keys if k in data]
|
hit = [k for k in locked_keys if k in data]
|
||||||
if hit:
|
if hit:
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class Settings(BaseSettings):
|
|||||||
oo_min_leverage: float = 200.0 # 期期:单腿最低杠杆
|
oo_min_leverage: float = 200.0 # 期期:单腿最低杠杆
|
||||||
oo_reward_ratio: float = 2.0 # 盈亏比:出场目标 = 预算 × 比
|
oo_reward_ratio: float = 2.0 # 盈亏比:出场目标 = 预算 × 比
|
||||||
oo_budget_cushion: float = 0.92 # 定仓预留余地(用于权利金的预算比例)
|
oo_budget_cushion: float = 0.92 # 定仓预留余地(用于权利金的预算比例)
|
||||||
|
oo_strike_max_dev_pct: float = 1.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 且须实值或平值
|
# 固定方向:关=现有 ATM/比价规则;开=指定永续多/空,期权 Put/Call 且须实值或平值
|
||||||
|
|||||||
@@ -297,6 +297,12 @@ class StrategyEngine:
|
|||||||
)
|
)
|
||||||
or s.oo_reward_ratio
|
or s.oo_reward_ratio
|
||||||
),
|
),
|
||||||
|
"oo_strike_max_dev_pct": float(
|
||||||
|
self.ledger.get_setting_float(
|
||||||
|
"oo_strike_max_dev_pct", s.oo_strike_max_dev_pct
|
||||||
|
)
|
||||||
|
or s.oo_strike_max_dev_pct
|
||||||
|
),
|
||||||
"risk_perp_unit": risk_perp_unit,
|
"risk_perp_unit": risk_perp_unit,
|
||||||
"risk_option_unit": risk_option_unit,
|
"risk_option_unit": risk_option_unit,
|
||||||
"risk_exit_unit": risk_exit_unit,
|
"risk_exit_unit": risk_exit_unit,
|
||||||
|
|||||||
@@ -36,17 +36,42 @@ class OoPickCore:
|
|||||||
detail: str = "ok"
|
detail: str = "ok"
|
||||||
|
|
||||||
|
|
||||||
def pick_otm_call_strike(strikes: list[float], *, spot: float, high: float) -> float | None:
|
def _within_ref_pct(strike: float, ref: float, max_dev_pct: float) -> bool:
|
||||||
"""虚值 Call:K > spot,优先贴近振幅高点。"""
|
"""|K−ref|/ref ≤ max_dev_pct%。"""
|
||||||
|
if ref <= 0 or max_dev_pct < 0:
|
||||||
|
return False
|
||||||
|
return abs(float(strike) - float(ref)) / float(ref) * 100.0 <= float(
|
||||||
|
max_dev_pct
|
||||||
|
) + 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
def pick_otm_call_strike(
|
||||||
|
strikes: list[float],
|
||||||
|
*,
|
||||||
|
spot: float,
|
||||||
|
high: float,
|
||||||
|
max_dev_pct: float = 1.0,
|
||||||
|
) -> float | None:
|
||||||
|
"""虚值 Call:K > spot,贴近振幅高点,且 |K−高|/高 ≤ max_dev_pct%。"""
|
||||||
cands = [float(s) for s in strikes if float(s) > float(spot) + 1e-9]
|
cands = [float(s) for s in strikes if float(s) > float(spot) + 1e-9]
|
||||||
|
if max_dev_pct >= 0 and high > 0:
|
||||||
|
cands = [s for s in cands if _within_ref_pct(s, high, max_dev_pct)]
|
||||||
if not cands:
|
if not cands:
|
||||||
return None
|
return None
|
||||||
return min(cands, key=lambda s: (abs(s - float(high)), s))
|
return min(cands, key=lambda s: (abs(s - float(high)), s))
|
||||||
|
|
||||||
|
|
||||||
def pick_otm_put_strike(strikes: list[float], *, spot: float, low: float) -> float | None:
|
def pick_otm_put_strike(
|
||||||
"""虚值 Put:K < spot,优先贴近振幅低点。"""
|
strikes: list[float],
|
||||||
|
*,
|
||||||
|
spot: float,
|
||||||
|
low: float,
|
||||||
|
max_dev_pct: float = 1.0,
|
||||||
|
) -> float | None:
|
||||||
|
"""虚值 Put:K < spot,贴近振幅低点,且 |K−低|/低 ≤ max_dev_pct%。"""
|
||||||
cands = [float(s) for s in strikes if float(s) < float(spot) - 1e-9]
|
cands = [float(s) for s in strikes if float(s) < float(spot) - 1e-9]
|
||||||
|
if max_dev_pct >= 0 and low > 0:
|
||||||
|
cands = [s for s in cands if _within_ref_pct(s, low, max_dev_pct)]
|
||||||
if not cands:
|
if not cands:
|
||||||
return None
|
return None
|
||||||
return min(cands, key=lambda s: (abs(s - float(low)), s))
|
return min(cands, key=lambda s: (abs(s - float(low)), s))
|
||||||
@@ -61,10 +86,11 @@ def select_oo_pair(
|
|||||||
min_hours: float,
|
min_hours: float,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
skip_expiry_ymds: set[str] | None = None,
|
skip_expiry_ymds: set[str] | None = None,
|
||||||
|
max_dev_pct: float = 1.0,
|
||||||
) -> tuple[str, int, float, float, str, str] | None:
|
) -> tuple[str, int, float, float, str, str] | None:
|
||||||
"""
|
"""
|
||||||
返回 (expiry_ymd, expiry_ms, call_strike, put_strike, call_inst, put_inst)。
|
返回 (expiry_ymd, expiry_ms, call_strike, put_strike, call_inst, put_inst)。
|
||||||
Call/Put 可不同行权价;须同到期且均为虚值。
|
Call/Put 可不同行权价;须同到期、均为虚值,且相对高低点偏离不超过 max_dev_pct%。
|
||||||
"""
|
"""
|
||||||
if spot <= 0 or high <= 0 or low <= 0 or high < low:
|
if spot <= 0 or high <= 0 or low <= 0 or high < low:
|
||||||
return None
|
return None
|
||||||
@@ -80,8 +106,12 @@ def select_oo_pair(
|
|||||||
for ymd in eligible:
|
for ymd in eligible:
|
||||||
ems, strikes_map = complete[ymd]
|
ems, strikes_map = complete[ymd]
|
||||||
strikes = list(strikes_map.keys())
|
strikes = list(strikes_map.keys())
|
||||||
ck = pick_otm_call_strike(strikes, spot=spot, high=high)
|
ck = pick_otm_call_strike(
|
||||||
pk = pick_otm_put_strike(strikes, spot=spot, low=low)
|
strikes, spot=spot, high=high, max_dev_pct=max_dev_pct
|
||||||
|
)
|
||||||
|
pk = pick_otm_put_strike(
|
||||||
|
strikes, spot=spot, low=low, max_dev_pct=max_dev_pct
|
||||||
|
)
|
||||||
if ck is None or pk is None:
|
if ck is None or pk is None:
|
||||||
continue
|
continue
|
||||||
call_inst = strikes_map[ck].get("C")
|
call_inst = strikes_map[ck].get("C")
|
||||||
@@ -113,8 +143,9 @@ def build_oo_pick_core(
|
|||||||
amplitude: AmplitudeHL | None = None,
|
amplitude: AmplitudeHL | None = None,
|
||||||
skip_expiry_ymds: set[str] | None = None,
|
skip_expiry_ymds: set[str] | None = None,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
|
max_dev_pct: float = 1.0,
|
||||||
) -> OoPickCore | None:
|
) -> OoPickCore | None:
|
||||||
"""完整期期选约:振幅门 + 虚值双腿 + 杠杆。"""
|
"""完整期期选约:振幅门 + 虚值双腿(贴高低≤max_dev%) + 杠杆。"""
|
||||||
amp = amplitude or fetch_amplitude_hl_for_runtime(amplitude_hours)
|
amp = amplitude or fetch_amplitude_hl_for_runtime(amplitude_hours)
|
||||||
if amp is None:
|
if amp is None:
|
||||||
return None
|
return None
|
||||||
@@ -130,6 +161,7 @@ def build_oo_pick_core(
|
|||||||
min_hours=float(min_hours),
|
min_hours=float(min_hours),
|
||||||
now=now,
|
now=now,
|
||||||
skip_expiry_ymds=skip_expiry_ymds,
|
skip_expiry_ymds=skip_expiry_ymds,
|
||||||
|
max_dev_pct=float(max_dev_pct),
|
||||||
)
|
)
|
||||||
if picked is None:
|
if picked is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ def _hedge_mode() -> str:
|
|||||||
return "perp_option"
|
return "perp_option"
|
||||||
|
|
||||||
|
|
||||||
def _oo_settings() -> tuple[float, float, float, float]:
|
def _oo_settings() -> tuple[float, float, float, float, float]:
|
||||||
"""amplitude_pct, amplitude_hours, min_option_hours, min_leverage"""
|
"""amplitude_pct, amplitude_hours, min_option_hours, min_leverage, strike_max_dev_pct"""
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
try:
|
try:
|
||||||
from ..models.db import get_db
|
from ..models.db import get_db
|
||||||
@@ -210,6 +210,12 @@ def _oo_settings() -> tuple[float, float, float, float]:
|
|||||||
or s.oo_min_option_hours
|
or s.oo_min_option_hours
|
||||||
),
|
),
|
||||||
float(db.get_setting("oo_min_leverage", str(s.oo_min_leverage)) or s.oo_min_leverage),
|
float(db.get_setting("oo_min_leverage", str(s.oo_min_leverage)) or s.oo_min_leverage),
|
||||||
|
float(
|
||||||
|
db.get_setting(
|
||||||
|
"oo_strike_max_dev_pct", str(s.oo_strike_max_dev_pct)
|
||||||
|
)
|
||||||
|
or s.oo_strike_max_dev_pct
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return (
|
return (
|
||||||
@@ -217,6 +223,7 @@ def _oo_settings() -> tuple[float, float, float, float]:
|
|||||||
s.oo_amplitude_hours,
|
s.oo_amplitude_hours,
|
||||||
s.oo_min_option_hours,
|
s.oo_min_option_hours,
|
||||||
s.oo_min_leverage,
|
s.oo_min_leverage,
|
||||||
|
s.oo_strike_max_dev_pct,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -450,7 +457,7 @@ class StrategySession:
|
|||||||
try:
|
try:
|
||||||
from ..exchange.candles import fetch_amplitude_hl_for_runtime
|
from ..exchange.candles import fetch_amplitude_hl_for_runtime
|
||||||
|
|
||||||
amp_pct, amp_hours, _, _ = _oo_settings()
|
amp_pct, amp_hours, _, _, _ = _oo_settings()
|
||||||
amp = fetch_amplitude_hl_for_runtime(amp_hours)
|
amp = fetch_amplitude_hl_for_runtime(amp_hours)
|
||||||
if amp is None:
|
if amp is None:
|
||||||
return self._oo_amp
|
return self._oo_amp
|
||||||
@@ -477,7 +484,7 @@ class StrategySession:
|
|||||||
if _has_open_position():
|
if _has_open_position():
|
||||||
return self.align_to_held_position()
|
return self.align_to_held_position()
|
||||||
s = self.settings
|
s = self.settings
|
||||||
amp_pct, amp_hours, min_hours, _min_lev = _oo_settings()
|
amp_pct, amp_hours, min_hours, _min_lev, max_dev = _oo_settings()
|
||||||
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:
|
||||||
@@ -496,10 +503,11 @@ class StrategySession:
|
|||||||
low=amp_low,
|
low=amp_low,
|
||||||
min_hours=float(min_hours),
|
min_hours=float(min_hours),
|
||||||
skip_expiry_ymds=skip,
|
skip_expiry_ymds=skip,
|
||||||
|
max_dev_pct=float(max_dev),
|
||||||
)
|
)
|
||||||
if picked is None:
|
if picked is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"未找到剩余≥{min_hours}h 的虚值 Call@高/Put@低"
|
f"未找到剩余≥{min_hours}h 且贴高低≤{max_dev:g}% 的虚值 Call/Put"
|
||||||
)
|
)
|
||||||
ymd, ems, ck, pk, call_inst, put_inst = picked
|
ymd, ems, ck, pk, call_inst, put_inst = picked
|
||||||
pair = OptionPair(
|
pair = OptionPair(
|
||||||
@@ -527,7 +535,7 @@ class StrategySession:
|
|||||||
from .selection import _complete_by_expiry, option_leverage
|
from .selection import _complete_by_expiry, option_leverage
|
||||||
|
|
||||||
s = self.settings
|
s = self.settings
|
||||||
amp_pct, amp_hours, min_hours, min_lev = _oo_settings()
|
amp_pct, amp_hours, min_hours, min_lev, max_dev = _oo_settings()
|
||||||
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:
|
||||||
@@ -565,9 +573,13 @@ class StrategySession:
|
|||||||
low=float(amp.low),
|
low=float(amp.low),
|
||||||
min_hours=float(min_hours),
|
min_hours=float(min_hours),
|
||||||
skip_expiry_ymds=skip,
|
skip_expiry_ymds=skip,
|
||||||
|
max_dev_pct=float(max_dev),
|
||||||
)
|
)
|
||||||
if picked is None:
|
if picked is None:
|
||||||
logger.info("oo: no OTM call/put pair for amplitude HL")
|
logger.info(
|
||||||
|
"oo: no OTM call/put within %.2f%% of amplitude HL",
|
||||||
|
max_dev,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
ymd, ems, ck, pk, call_inst, put_inst = picked
|
ymd, ems, ck, pk, call_inst, put_inst = picked
|
||||||
call_bids, call_asks, _ = self.ex.fetch_book(call_inst, depth=5)
|
call_bids, call_asks, _ = self.ex.fetch_book(call_inst, depth=5)
|
||||||
@@ -840,7 +852,7 @@ class StrategySession:
|
|||||||
def oo_needs_realign(self) -> bool:
|
def oo_needs_realign(self) -> bool:
|
||||||
if self._pair is None:
|
if self._pair is None:
|
||||||
return True
|
return True
|
||||||
_amp_pct, amp_hours, min_hours, _ = _oo_settings()
|
_amp_pct, amp_hours, min_hours, _, max_dev = _oo_settings()
|
||||||
if (
|
if (
|
||||||
hours_until_expiry(self._pair.expiry_ymd, expiry_ms=self._pair.expiry_ms)
|
hours_until_expiry(self._pair.expiry_ymd, expiry_ms=self._pair.expiry_ms)
|
||||||
+ 1e-9
|
+ 1e-9
|
||||||
@@ -877,6 +889,7 @@ class StrategySession:
|
|||||||
low=float(amp.low),
|
low=float(amp.low),
|
||||||
min_hours=float(min_hours),
|
min_hours=float(min_hours),
|
||||||
skip_expiry_ymds=skip,
|
skip_expiry_ymds=skip,
|
||||||
|
max_dev_pct=float(max_dev),
|
||||||
)
|
)
|
||||||
if picked is None:
|
if picked is None:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ def test_otm_strikes_near_amplitude() -> None:
|
|||||||
assert pick_otm_put_strike(strikes, spot=1950, low=1860) == 1850.0
|
assert pick_otm_put_strike(strikes, spot=1950, low=1860) == 1850.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_otm_strikes_reject_beyond_1pct() -> None:
|
||||||
|
# 高点 2000,最近虚值 Call 仅 2100(偏离 5%)→ 拒绝
|
||||||
|
strikes = [1900.0, 1950.0, 2100.0]
|
||||||
|
assert pick_otm_call_strike(strikes, spot=1950, high=2000, max_dev_pct=1.0) is None
|
||||||
|
# 低点 1900,最近虚值 Put 仅 1800(偏离 ~5.3%)→ 拒绝
|
||||||
|
assert (
|
||||||
|
pick_otm_put_strike(
|
||||||
|
[1800.0, 1950.0, 2000.0], spot=1950, low=1900, max_dev_pct=1.0
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
# 高点 2095,Call 2100 偏离约 0.24% → 通过
|
||||||
|
assert (
|
||||||
|
pick_otm_call_strike(strikes, spot=1950, high=2095, max_dev_pct=1.0) == 2100.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_select_oo_pair_same_expiry(tmp_path=None) -> None:
|
def test_select_oo_pair_same_expiry(tmp_path=None) -> None:
|
||||||
contracts = []
|
contracts = []
|
||||||
for k in (1900, 2000, 2100):
|
for k in (1900, 2000, 2100):
|
||||||
@@ -36,6 +53,7 @@ def test_select_oo_pair_same_expiry(tmp_path=None) -> None:
|
|||||||
high=2105.0,
|
high=2105.0,
|
||||||
low=1890.0,
|
low=1890.0,
|
||||||
min_hours=1.0,
|
min_hours=1.0,
|
||||||
|
max_dev_pct=1.0,
|
||||||
)
|
)
|
||||||
assert picked is not None
|
assert picked is not None
|
||||||
ymd, _ems, ck, pk, call_i, put_i = picked
|
ymd, _ems, ck, pk, call_i, put_i = picked
|
||||||
@@ -114,7 +132,7 @@ def test_amplitude_max_gate() -> None:
|
|||||||
from app.strategy.oo_selection import build_oo_pick_core
|
from app.strategy.oo_selection import build_oo_pick_core
|
||||||
|
|
||||||
contracts = []
|
contracts = []
|
||||||
for k in (1900, 2000, 2100):
|
for k in (1900, 1975, 2000, 2025, 2100):
|
||||||
for side, letter in (("call", "C"), ("put", "P")):
|
for side, letter in (("call", "C"), ("put", "P")):
|
||||||
contracts.append(
|
contracts.append(
|
||||||
{
|
{
|
||||||
@@ -141,7 +159,7 @@ def test_amplitude_max_gate() -> None:
|
|||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
# 3% ≤ 上限 3.5% → 可过振幅门(杠杆/卖一足够)
|
# 3% ≤ 上限 3.5%,且 2025/1975 贴高低 ≤1% → 通过
|
||||||
ok = build_oo_pick_core(
|
ok = build_oo_pick_core(
|
||||||
contracts=contracts,
|
contracts=contracts,
|
||||||
spot=2000,
|
spot=2000,
|
||||||
@@ -152,5 +170,8 @@ def test_amplitude_max_gate() -> None:
|
|||||||
amplitude_hours=12,
|
amplitude_hours=12,
|
||||||
amplitude_pct=3.5,
|
amplitude_pct=3.5,
|
||||||
amplitude=amp,
|
amplitude=amp,
|
||||||
|
max_dev_pct=1.0,
|
||||||
)
|
)
|
||||||
assert ok is not None
|
assert ok is not None
|
||||||
|
assert ok.call.strike == 2025.0
|
||||||
|
assert ok.put.strike == 1975.0
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ export type PlanState = {
|
|||||||
oo_min_option_hours?: number;
|
oo_min_option_hours?: number;
|
||||||
oo_min_leverage?: number;
|
oo_min_leverage?: number;
|
||||||
oo_reward_ratio?: number;
|
oo_reward_ratio?: number;
|
||||||
|
oo_strike_max_dev_pct?: number;
|
||||||
oo_put_qty_eth?: number;
|
oo_put_qty_eth?: number;
|
||||||
sizing_mode?: "manual" | "risk_based";
|
sizing_mode?: "manual" | "risk_based";
|
||||||
risk_based?: boolean;
|
risk_based?: boolean;
|
||||||
@@ -428,6 +429,8 @@ export type StrategySettings = {
|
|||||||
oo_min_leverage?: number;
|
oo_min_leverage?: number;
|
||||||
oo_reward_ratio?: number;
|
oo_reward_ratio?: number;
|
||||||
oo_budget_cushion?: number;
|
oo_budget_cushion?: number;
|
||||||
|
oo_strike_max_dev_pct?: number;
|
||||||
|
oo_strike_max_dev_pct?: number;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ export default function PlanPage() {
|
|||||||
: "ATM偏差关";
|
: "ATM偏差关";
|
||||||
const ooSelectLabel = [
|
const ooSelectLabel = [
|
||||||
`振幅≤${fmt(plan?.oo_amplitude_pct ?? 1.5, 1)}%/${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h`,
|
`振幅≤${fmt(plan?.oo_amplitude_pct ?? 1.5, 1)}%/${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h`,
|
||||||
|
`贴高低≤${fmt(plan?.oo_strike_max_dev_pct ?? 1, 1)}%`,
|
||||||
"虚值Call@高·Put@低",
|
"虚值Call@高·Put@低",
|
||||||
`剩余≥${fmt(plan?.oo_min_option_hours ?? 24, 0)}h`,
|
`剩余≥${fmt(plan?.oo_min_option_hours ?? 24, 0)}h`,
|
||||||
`杠杆≥${fmt(plan?.oo_min_leverage ?? 200, 0)}x`,
|
`杠杆≥${fmt(plan?.oo_min_leverage ?? 200, 0)}x`,
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export default function SettingsPage() {
|
|||||||
const [ooAmpHours, setOoAmpHours] = useState(12);
|
const [ooAmpHours, setOoAmpHours] = useState(12);
|
||||||
const [ooMinHours, setOoMinHours] = useState(24);
|
const [ooMinHours, setOoMinHours] = useState(24);
|
||||||
const [ooMinLev, setOoMinLev] = useState(200);
|
const [ooMinLev, setOoMinLev] = useState(200);
|
||||||
|
const [ooStrikeDev, setOoStrikeDev] = useState(1);
|
||||||
const [ooRewardRatio, setOoRewardRatio] = useState(2);
|
const [ooRewardRatio, setOoRewardRatio] = useState(2);
|
||||||
const [riskPreview, setRiskPreview] = useState<Record<string, unknown> | null>(
|
const [riskPreview, setRiskPreview] = useState<Record<string, unknown> | null>(
|
||||||
null,
|
null,
|
||||||
@@ -236,6 +237,7 @@ export default function SettingsPage() {
|
|||||||
setOoAmpHours(s.oo_amplitude_hours ?? 12);
|
setOoAmpHours(s.oo_amplitude_hours ?? 12);
|
||||||
setOoMinHours(s.oo_min_option_hours ?? 24);
|
setOoMinHours(s.oo_min_option_hours ?? 24);
|
||||||
setOoMinLev(s.oo_min_leverage ?? 200);
|
setOoMinLev(s.oo_min_leverage ?? 200);
|
||||||
|
setOoStrikeDev(s.oo_strike_max_dev_pct ?? 1);
|
||||||
setOoRewardRatio(s.oo_reward_ratio ?? 2);
|
setOoRewardRatio(s.oo_reward_ratio ?? 2);
|
||||||
setRiskPreview(
|
setRiskPreview(
|
||||||
s.risk_sizing_preview && typeof s.risk_sizing_preview === "object"
|
s.risk_sizing_preview && typeof s.risk_sizing_preview === "object"
|
||||||
@@ -412,6 +414,7 @@ export default function SettingsPage() {
|
|||||||
oo_amplitude_hours: ooAmpHours,
|
oo_amplitude_hours: ooAmpHours,
|
||||||
oo_min_option_hours: ooMinHours,
|
oo_min_option_hours: ooMinHours,
|
||||||
oo_min_leverage: ooMinLev,
|
oo_min_leverage: ooMinLev,
|
||||||
|
oo_strike_max_dev_pct: ooStrikeDev,
|
||||||
oo_reward_ratio: ooRewardRatio,
|
oo_reward_ratio: ooRewardRatio,
|
||||||
exchange,
|
exchange,
|
||||||
};
|
};
|
||||||
@@ -1281,8 +1284,24 @@ export default function SettingsPage() {
|
|||||||
value={ooMinLev}
|
value={ooMinLev}
|
||||||
onChange={(e) => setOoMinLev(Number(e.target.value))}
|
onChange={(e) => setOoMinLev(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="ooStrikeDev">贴高低最大偏离(%)</label>
|
||||||
|
<input
|
||||||
|
id="ooStrikeDev"
|
||||||
|
className="mono"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0.1"
|
||||||
|
max="10"
|
||||||
|
value={ooStrikeDev}
|
||||||
|
onChange={(e) =>
|
||||||
|
setOoStrikeDev(Number(e.target.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
|
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
|
||||||
期期:同到期虚值 Call 贴高点、Put 贴低点;不使用 ATM/固定方向。
|
虚值 Call/Put 行权价相对振幅高/低点 |K−HL|/HL
|
||||||
|
不超过该比例(默认 1%)。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user