Treat option-option amplitude as a maximum cap.
Reject opens when range exceeds the setting; update UI and docs labels. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -82,7 +82,7 @@ class Settings(BaseSettings):
|
|||||||
martingale_max_doubles: int = 3 # 最多翻倍次数(如 2→4→8→16 为 3 次)
|
martingale_max_doubles: int = 3 # 最多翻倍次数(如 2→4→8→16 为 3 次)
|
||||||
# 对冲模式:perp_option=永期(默认)| option_option=期期
|
# 对冲模式:perp_option=永期(默认)| option_option=期期
|
||||||
hedge_mode: str = "perp_option"
|
hedge_mode: str = "perp_option"
|
||||||
oo_amplitude_pct: float = 1.5 # 振幅最小 %(回看窗内高低)
|
oo_amplitude_pct: float = 1.5 # 振幅最大 %(回看窗内高低;超过则不开)
|
||||||
oo_amplitude_hours: float = 12.0 # 振幅回看小时
|
oo_amplitude_hours: float = 12.0 # 振幅回看小时
|
||||||
oo_min_option_hours: float = 24.0 # 期期:最短剩余到期小时
|
oo_min_option_hours: float = 24.0 # 期期:最短剩余到期小时
|
||||||
oo_min_leverage: float = 200.0 # 期期:单腿最低杠杆
|
oo_min_leverage: float = 200.0 # 期期:单腿最低杠杆
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ def build_oo_pick_core(
|
|||||||
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
|
||||||
if float(amp.range_pct) + 1e-12 < float(amplitude_pct):
|
if float(amp.range_pct) > float(amplitude_pct) + 1e-12:
|
||||||
return None
|
return None
|
||||||
if spot <= 0:
|
if spot <= 0:
|
||||||
spot = float(amp.mid)
|
spot = float(amp.mid)
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ class StrategySession:
|
|||||||
return self._apply_pair(pair, mark=float(mark), idx=idx)
|
return self._apply_pair(pair, mark=float(mark), idx=idx)
|
||||||
|
|
||||||
def align_oo_instruments(self) -> OptionPair | None:
|
def align_oo_instruments(self) -> OptionPair | None:
|
||||||
"""期期监控:按振幅高低点选虚值 Call/Put(展示用;振幅不足仍对齐候选)。"""
|
"""期期监控:按振幅高低点选虚值 Call/Put(展示用;振幅超限仍对齐候选)。"""
|
||||||
from ..exchange.candles import fetch_amplitude_hl_for_runtime
|
from ..exchange.candles import fetch_amplitude_hl_for_runtime
|
||||||
from .oo_selection import select_oo_pair
|
from .oo_selection import select_oo_pair
|
||||||
|
|
||||||
@@ -465,8 +465,8 @@ class StrategySession:
|
|||||||
"mid": float(amp.mid),
|
"mid": float(amp.mid),
|
||||||
"range_pct": float(amp.range_pct),
|
"range_pct": float(amp.range_pct),
|
||||||
"hours": float(amp_hours),
|
"hours": float(amp_hours),
|
||||||
"min_pct": float(amp_pct),
|
"max_pct": float(amp_pct),
|
||||||
"ok": float(amp.range_pct) + 1e-12 >= float(amp_pct),
|
"ok": float(amp.range_pct) <= float(amp_pct) + 1e-12,
|
||||||
}
|
}
|
||||||
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
||||||
skip = _skip_expiry_ymds_for_next()
|
skip = _skip_expiry_ymds_for_next()
|
||||||
@@ -518,9 +518,9 @@ class StrategySession:
|
|||||||
if amp is None:
|
if amp is None:
|
||||||
logger.info("oo: amplitude candles unavailable")
|
logger.info("oo: amplitude candles unavailable")
|
||||||
return None
|
return None
|
||||||
if float(amp.range_pct) + 1e-12 < float(amp_pct):
|
if float(amp.range_pct) > float(amp_pct) + 1e-12:
|
||||||
logger.info(
|
logger.info(
|
||||||
"oo: amplitude %.3f%% < need %.3f%% (H=%.2f L=%.2f)",
|
"oo: amplitude %.3f%% > max %.3f%% (H=%.2f L=%.2f)",
|
||||||
amp.range_pct,
|
amp.range_pct,
|
||||||
amp_pct,
|
amp_pct,
|
||||||
amp.high,
|
amp.high,
|
||||||
@@ -584,7 +584,7 @@ class StrategySession:
|
|||||||
"mid": float(amp.mid),
|
"mid": float(amp.mid),
|
||||||
"range_pct": float(amp.range_pct),
|
"range_pct": float(amp.range_pct),
|
||||||
"hours": float(amp_hours),
|
"hours": float(amp_hours),
|
||||||
"min_pct": float(amp_pct),
|
"max_pct": float(amp_pct),
|
||||||
"ok": True,
|
"ok": True,
|
||||||
}
|
}
|
||||||
self._apply_pair(pair, mark=underlying, idx=idx)
|
self._apply_pair(pair, mark=underlying, idx=idx)
|
||||||
@@ -837,8 +837,8 @@ class StrategySession:
|
|||||||
"mid": float(amp.mid),
|
"mid": float(amp.mid),
|
||||||
"range_pct": float(amp.range_pct),
|
"range_pct": float(amp.range_pct),
|
||||||
"hours": float(amp_hours),
|
"hours": float(amp_hours),
|
||||||
"min_pct": float(_amp_pct),
|
"max_pct": float(_amp_pct),
|
||||||
"ok": float(amp.range_pct) + 1e-12 >= float(_amp_pct),
|
"ok": float(amp.range_pct) <= float(_amp_pct) + 1e-12,
|
||||||
}
|
}
|
||||||
contracts = self.ex.list_option_contracts(self.settings.option_inst_family)
|
contracts = self.ex.list_option_contracts(self.settings.option_inst_family)
|
||||||
picked = select_oo_pair(
|
picked = select_oo_pair(
|
||||||
|
|||||||
@@ -64,3 +64,49 @@ def test_compute_oo_sizing_1_1_and_reward() -> None:
|
|||||||
def test_amplitude_range_pct() -> None:
|
def test_amplitude_range_pct() -> None:
|
||||||
a = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12)
|
a = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12)
|
||||||
assert abs(a.range_pct - 3.0) < 1e-9
|
assert abs(a.range_pct - 3.0) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_amplitude_max_gate() -> None:
|
||||||
|
from app.strategy.oo_selection import build_oo_pick_core
|
||||||
|
|
||||||
|
contracts = []
|
||||||
|
for k in (1900, 2000, 2100):
|
||||||
|
for side, letter in (("call", "C"), ("put", "P")):
|
||||||
|
contracts.append(
|
||||||
|
{
|
||||||
|
"expiry_ymd": "260810",
|
||||||
|
"expiry_ms": 1_786_320_000_000,
|
||||||
|
"strike": float(k),
|
||||||
|
"side": letter,
|
||||||
|
"inst_id": f"ETH-{k}-{letter}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
amp = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12)
|
||||||
|
# 3% > 上限 1.5% → 拒
|
||||||
|
assert (
|
||||||
|
build_oo_pick_core(
|
||||||
|
contracts=contracts,
|
||||||
|
spot=2000,
|
||||||
|
call_ask=5,
|
||||||
|
put_ask=5,
|
||||||
|
min_hours=1,
|
||||||
|
min_leverage=1,
|
||||||
|
amplitude_hours=12,
|
||||||
|
amplitude_pct=1.5,
|
||||||
|
amplitude=amp,
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
# 3% ≤ 上限 3.5% → 可过振幅门(杠杆/卖一足够)
|
||||||
|
ok = build_oo_pick_core(
|
||||||
|
contracts=contracts,
|
||||||
|
spot=2000,
|
||||||
|
call_ask=5,
|
||||||
|
put_ask=5,
|
||||||
|
min_hours=1,
|
||||||
|
min_leverage=1,
|
||||||
|
amplitude_hours=12,
|
||||||
|
amplitude_pct=3.5,
|
||||||
|
amplitude=amp,
|
||||||
|
)
|
||||||
|
assert ok is not None
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
| 参数 | 默认 | 含义 |
|
| 参数 | 默认 | 含义 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 振幅最小 % | 1.5 | 回看窗内 `(高-低)/中价` 须 ≥ 该值 |
|
| 振幅最大 % | 1.5 | 回看窗内 `(高-低)/中价` 须 ≤ 该值,超过不开 |
|
||||||
| 振幅回看小时 | 12 | 用 1H K 线取真实高低点 |
|
| 振幅回看小时 | 12 | 用 1H K 线取真实高低点 |
|
||||||
| 最短剩余到期 | 24 | 期权剩余小时 |
|
| 最短剩余到期 | 24 | 期权剩余小时 |
|
||||||
| 单腿最低杠杆 | 200 | `指数 / 卖一` |
|
| 单腿最低杠杆 | 200 | `指数 / 卖一` |
|
||||||
|
|||||||
+2
-2
@@ -458,7 +458,7 @@ LIVE 特殊态:`option_closed_perp_pending`(期权已在交易所卖掉、
|
|||||||
| `residual_min_premium_pct` | 20 | 残留权利金回收门槛% |
|
| `residual_min_premium_pct` | 20 | 残留权利金回收门槛% |
|
||||||
| `residual_close_check_sec` | 300 | 残留巡检间隔秒 |
|
| `residual_close_check_sec` | 300 | 残留巡检间隔秒 |
|
||||||
| `hedge_mode` | perp_option | 永期 / 期期二选一 |
|
| `hedge_mode` | perp_option | 永期 / 期期二选一 |
|
||||||
| `oo_amplitude_pct` | 1.5 | 期期振幅最小% |
|
| `oo_amplitude_pct` | 1.5 | 期期振幅最大%(超过不开) |
|
||||||
| `oo_amplitude_hours` | 12 | 期期振幅回看小时 |
|
| `oo_amplitude_hours` | 12 | 期期振幅回看小时 |
|
||||||
| `oo_min_option_hours` | 24 | 期期最短剩余到期 |
|
| `oo_min_option_hours` | 24 | 期期最短剩余到期 |
|
||||||
| `oo_min_leverage` | 200 | 期期单腿最低杠杆 |
|
| `oo_min_leverage` | 200 | 期期单腿最低杠杆 |
|
||||||
@@ -471,7 +471,7 @@ LIVE 特殊态:`option_closed_perp_pending`(期权已在交易所卖掉、
|
|||||||
|
|
||||||
详见 [期期对冲说明](./期期对冲说明.md)。摘要:
|
详见 [期期对冲说明](./期期对冲说明.md)。摘要:
|
||||||
|
|
||||||
- 回看 `oo_amplitude_hours` 的真实高低;振幅不足不开。
|
- 回看 `oo_amplitude_hours` 的真实高低;振幅超过 `oo_amplitude_pct` 不开。
|
||||||
- 虚值 Call 贴高、Put 贴低;同到期;杠杆与剩余小时门槛。
|
- 虚值 Call 贴高、Put 贴低;同到期;杠杆与剩余小时门槛。
|
||||||
- 预算 1:1、qty 一位小数、目标 = B × `oo_reward_ratio`。
|
- 预算 1:1、qty 一位小数、目标 = B × `oo_reward_ratio`。
|
||||||
- 达标只平盈利腿;亏损腿 residual(20% 回升或到期)。
|
- 达标只平盈利腿;亏损腿 residual(20% 回升或到期)。
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ export type MarketSnapshot = {
|
|||||||
mid?: number;
|
mid?: number;
|
||||||
range_pct?: number;
|
range_pct?: number;
|
||||||
hours?: number;
|
hours?: number;
|
||||||
|
max_pct?: number;
|
||||||
|
/** @deprecated 兼容旧字段,等同 max_pct */
|
||||||
min_pct?: number;
|
min_pct?: number;
|
||||||
ok?: boolean;
|
ok?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export default function PlanPage() {
|
|||||||
? `ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}`
|
? `ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}`
|
||||||
: "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`,
|
||||||
"虚值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`,
|
||||||
@@ -897,13 +897,20 @@ export default function PlanPage() {
|
|||||||
{snap?.oo_amplitude?.range_pct != null
|
{snap?.oo_amplitude?.range_pct != null
|
||||||
? `${fmt(snap.oo_amplitude.range_pct, 2)}%`
|
? `${fmt(snap.oo_amplitude.range_pct, 2)}%`
|
||||||
: "—"}
|
: "—"}
|
||||||
{snap?.oo_amplitude?.min_pct != null
|
{(snap?.oo_amplitude?.max_pct ?? snap?.oo_amplitude?.min_pct) !=
|
||||||
? ` · 门限≥${fmt(snap.oo_amplitude.min_pct, 1)}%`
|
null
|
||||||
|
? ` · 上限≤${fmt(
|
||||||
|
Number(
|
||||||
|
snap.oo_amplitude?.max_pct ??
|
||||||
|
snap.oo_amplitude?.min_pct,
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
)}%`
|
||||||
: ""}
|
: ""}
|
||||||
{snap?.oo_amplitude?.ok === false
|
{snap?.oo_amplitude?.ok === false
|
||||||
? " · 不足"
|
? " · 超限"
|
||||||
: snap?.oo_amplitude?.ok === true
|
: snap?.oo_amplitude?.ok === true
|
||||||
? " · 达标"
|
? " · 可开"
|
||||||
: ""}
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1220,7 +1220,7 @@ export default function SettingsPage() {
|
|||||||
{isOo ? (
|
{isOo ? (
|
||||||
<>
|
<>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="ooAmp">振幅最小(%)</label>
|
<label htmlFor="ooAmp">振幅最大(%)</label>
|
||||||
<input
|
<input
|
||||||
id="ooAmp"
|
id="ooAmp"
|
||||||
className="mono"
|
className="mono"
|
||||||
@@ -1230,6 +1230,9 @@ export default function SettingsPage() {
|
|||||||
value={ooAmpPct}
|
value={ooAmpPct}
|
||||||
onChange={(e) => setOoAmpPct(Number(e.target.value))}
|
onChange={(e) => setOoAmpPct(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
|
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
|
||||||
|
回看窗内振幅超过该上限则不开仓。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="ooAmpH">振幅回看(小时)</label>
|
<label htmlFor="ooAmpH">振幅回看(小时)</label>
|
||||||
@@ -1622,7 +1625,7 @@ export default function SettingsPage() {
|
|||||||
{stratSub === "select" ? (
|
{stratSub === "select" ? (
|
||||||
isOo ? (
|
isOo ? (
|
||||||
<li>
|
<li>
|
||||||
期期选约:回看窗内真实高低点振幅达标后,同到期虚值 Call
|
期期选约:回看窗内真实高低点振幅不超过上限后,同到期虚值 Call
|
||||||
贴高、Put 贴低;剩余到期与单腿杠杆门限见本页。
|
贴高、Put 贴低;剩余到期与单腿杠杆门限见本页。
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user