Move settings hints into rules; add OO amplitude filter toggle default off.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 17:58:50 +08:00
parent 0daac05ef5
commit ab612df818
10 changed files with 201 additions and 118 deletions
+13
View File
@@ -322,6 +322,19 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di
"oo_amplitude_hours": _pick(
"oo_amplitude_hours", float(settings.oo_amplitude_hours)
),
"oo_amplitude_filter_enabled": (
str(
st.get("oo_amplitude_filter_enabled")
if "oo_amplitude_filter_enabled" in st
else db.get_setting(
"oo_amplitude_filter_enabled",
str(settings.oo_amplitude_filter_enabled),
)
)
.strip()
.lower()
in ("1", "true", "yes", "on")
),
"oo_min_option_hours": _pick(
"oo_min_option_hours", float(settings.oo_min_option_hours)
),
+10
View File
@@ -67,6 +67,7 @@ KEYS = (
"hedge_mode",
"oo_amplitude_pct",
"oo_amplitude_hours",
"oo_amplitude_filter_enabled",
"oo_min_option_hours",
"oo_min_leverage",
"oo_reward_ratio",
@@ -123,6 +124,7 @@ class StrategySettingsBody(BaseModel):
)
oo_amplitude_pct: float | None = Field(default=None, ge=0.1, le=50)
oo_amplitude_hours: float | None = Field(default=None, ge=1, le=168)
oo_amplitude_filter_enabled: bool | None = None
oo_min_option_hours: float | None = Field(default=None, ge=1, le=720)
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)
@@ -365,6 +367,13 @@ def _read_settings() -> dict:
db.get_setting("oo_amplitude_hours", str(s.oo_amplitude_hours))
or s.oo_amplitude_hours
),
"oo_amplitude_filter_enabled": _as_bool(
db.get_setting(
"oo_amplitude_filter_enabled",
str(s.oo_amplitude_filter_enabled),
),
s.oo_amplitude_filter_enabled,
),
"oo_min_option_hours": float(
db.get_setting("oo_min_option_hours", str(s.oo_min_option_hours))
or s.oo_min_option_hours
@@ -476,6 +485,7 @@ async def put_strategy_settings(
"hedge_mode",
"oo_amplitude_pct",
"oo_amplitude_hours",
"oo_amplitude_filter_enabled",
"oo_min_option_hours",
"oo_min_leverage",
"oo_reward_ratio",
+3 -2
View File
@@ -82,8 +82,9 @@ class Settings(BaseSettings):
martingale_max_doubles: int = 3 # 最多翻倍次数(如 2→4→8→16 为 3 次)
# 对冲模式:perp_option=永期(默认)| option_option=期期
hedge_mode: str = "perp_option"
oo_amplitude_pct: float = 1.5 # 振幅最大 %回看窗内高低;超过则不开)
oo_amplitude_hours: float = 12.0 # 振幅回看小时
oo_amplitude_pct: float = 1.5 # 振幅最大 %过滤开启时:超过则不开)
oo_amplitude_hours: float = 12.0 # 振幅回看小时(选约高低点;过滤关也用)
oo_amplitude_filter_enabled: bool = False # 振幅过滤开关(默认关)
oo_min_option_hours: float = 24.0 # 期期:最短剩余到期小时
oo_min_leverage: float = 200.0 # 期期:单腿最低杠杆
oo_reward_ratio: float = 2.0 # 盈亏比:出场目标 = 预算 ×
+10
View File
@@ -279,6 +279,16 @@ class StrategyEngine:
)
or s.oo_amplitude_hours
),
"oo_amplitude_filter_enabled": str(
self.ledger.get_setting_str(
"oo_amplitude_filter_enabled",
str(s.oo_amplitude_filter_enabled),
)
or s.oo_amplitude_filter_enabled
)
.strip()
.lower()
in ("1", "true", "yes", "on"),
"oo_min_option_hours": float(
self.ledger.get_setting_float(
"oo_min_option_hours", s.oo_min_option_hours
+3 -2
View File
@@ -144,12 +144,13 @@ def build_oo_pick_core(
skip_expiry_ymds: set[str] | None = None,
now: datetime | None = None,
max_dev_pct: float = 1.0,
amplitude_filter_enabled: bool = True,
) -> OoPickCore | None:
"""完整期期选约:振幅门 + 虚值双腿(贴高低≤max_dev% + 杠杆。"""
"""完整期期选约:振幅门(可关)+ 虚值双腿(贴高低≤max_dev% + 杠杆。"""
amp = amplitude or fetch_amplitude_hl_for_runtime(amplitude_hours)
if amp is None:
return None
if float(amp.range_pct) > float(amplitude_pct) + 1e-12:
if amplitude_filter_enabled and float(amp.range_pct) > float(amplitude_pct) + 1e-12:
return None
if spot <= 0:
spot = float(amp.mid)
+38 -10
View File
@@ -192,13 +192,24 @@ def _hedge_mode() -> str:
return "perp_option"
def _oo_settings() -> tuple[float, float, float, float, float]:
"""amplitude_pct, amplitude_hours, min_option_hours, min_leverage, strike_max_dev_pct"""
def _oo_settings() -> tuple[float, float, float, float, float, bool]:
"""amplitude_pct, amplitude_hours, min_option_hours, min_leverage, strike_max_dev_pct, amp_filter_on"""
s = get_settings()
try:
from ..models.db import get_db
db = get_db()
filt_raw = db.get_setting(
"oo_amplitude_filter_enabled", str(s.oo_amplitude_filter_enabled)
)
filt_on = str(filt_raw or "").strip().lower() in (
"1",
"true",
"yes",
"on",
)
if filt_raw is None or filt_raw == "":
filt_on = bool(s.oo_amplitude_filter_enabled)
return (
float(db.get_setting("oo_amplitude_pct", str(s.oo_amplitude_pct)) or s.oo_amplitude_pct),
float(
@@ -216,6 +227,7 @@ def _oo_settings() -> tuple[float, float, float, float, float]:
)
or s.oo_strike_max_dev_pct
),
filt_on,
)
except Exception:
return (
@@ -224,6 +236,7 @@ def _oo_settings() -> tuple[float, float, float, float, float]:
s.oo_min_option_hours,
s.oo_min_leverage,
s.oo_strike_max_dev_pct,
bool(s.oo_amplitude_filter_enabled),
)
@@ -457,7 +470,7 @@ class StrategySession:
try:
from ..exchange.candles import fetch_amplitude_hl_for_runtime
amp_pct, amp_hours, _, _, _ = _oo_settings()
amp_pct, amp_hours, _, _, _, amp_filt = _oo_settings()
amp = fetch_amplitude_hl_for_runtime(amp_hours)
if amp is None:
return self._oo_amp
@@ -468,7 +481,12 @@ class StrategySession:
"range_pct": float(amp.range_pct),
"hours": float(amp_hours),
"max_pct": float(amp_pct),
"ok": float(amp.range_pct) <= float(amp_pct) + 1e-12,
"filter_enabled": bool(amp_filt),
"ok": (
True
if not amp_filt
else float(amp.range_pct) <= float(amp_pct) + 1e-12
),
}
return self._oo_amp
except Exception:
@@ -484,7 +502,7 @@ class StrategySession:
if _has_open_position():
return self.align_to_held_position()
s = self.settings
amp_pct, amp_hours, min_hours, _min_lev, max_dev = _oo_settings()
amp_pct, amp_hours, min_hours, _min_lev, max_dev, _amp_filt = _oo_settings()
idx = self.ex.fetch_index(s.index_inst_id)
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
if mark is None or mark <= 0:
@@ -535,7 +553,7 @@ class StrategySession:
from .selection import _complete_by_expiry, option_leverage
s = self.settings
amp_pct, amp_hours, min_hours, min_lev, max_dev = _oo_settings()
amp_pct, amp_hours, min_hours, min_lev, max_dev, amp_filt = _oo_settings()
idx = self.ex.fetch_index(s.index_inst_id)
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
if mark is None or mark <= 0:
@@ -553,9 +571,14 @@ class StrategySession:
"range_pct": float(amp.range_pct),
"hours": float(amp_hours),
"max_pct": float(amp_pct),
"ok": float(amp.range_pct) <= float(amp_pct) + 1e-12,
"filter_enabled": bool(amp_filt),
"ok": (
True
if not amp_filt
else float(amp.range_pct) <= float(amp_pct) + 1e-12
),
}
if float(amp.range_pct) > float(amp_pct) + 1e-12:
if amp_filt and float(amp.range_pct) > float(amp_pct) + 1e-12:
logger.info(
"oo: amplitude %.3f%% > max %.3f%% (H=%.2f L=%.2f)",
amp.range_pct,
@@ -852,7 +875,7 @@ class StrategySession:
def oo_needs_realign(self) -> bool:
if self._pair is None:
return True
_amp_pct, amp_hours, min_hours, _, max_dev = _oo_settings()
_amp_pct, amp_hours, min_hours, _, max_dev, amp_filt = _oo_settings()
if (
hours_until_expiry(self._pair.expiry_ymd, expiry_ms=self._pair.expiry_ms)
+ 1e-9
@@ -879,7 +902,12 @@ class StrategySession:
"range_pct": float(amp.range_pct),
"hours": float(amp_hours),
"max_pct": float(_amp_pct),
"ok": float(amp.range_pct) <= float(_amp_pct) + 1e-12,
"filter_enabled": bool(amp_filt),
"ok": (
True
if not amp_filt
else float(amp.range_pct) <= float(_amp_pct) + 1e-12
),
}
contracts = self.ex.list_option_contracts(self.settings.option_inst_family)
picked = select_oo_pair(
+15
View File
@@ -175,3 +175,18 @@ def test_amplitude_max_gate() -> None:
assert ok is not None
assert ok.call.strike == 2025.0
assert ok.put.strike == 1975.0
# 过滤关闭:振幅 3% 超过 1.5% 上限也可过
ok_off = 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,
max_dev_pct=1.0,
amplitude_filter_enabled=False,
)
assert ok_off is not None
+2 -1
View File
@@ -370,6 +370,7 @@ export type PlanState = {
hedge_mode?: "perp_option" | "option_option";
oo_amplitude_pct?: number;
oo_amplitude_hours?: number;
oo_amplitude_filter_enabled?: boolean;
oo_min_option_hours?: number;
oo_min_leverage?: number;
oo_reward_ratio?: number;
@@ -425,12 +426,12 @@ export type StrategySettings = {
hedge_mode?: "perp_option" | "option_option";
oo_amplitude_pct?: number;
oo_amplitude_hours?: number;
oo_amplitude_filter_enabled?: boolean;
oo_min_option_hours?: number;
oo_min_leverage?: number;
oo_reward_ratio?: number;
oo_budget_cushion?: number;
oo_strike_max_dev_pct?: number;
oo_strike_max_dev_pct?: number;
exchange?: string;
};
+3 -1
View File
@@ -264,7 +264,9 @@ export default function PlanPage() {
? `ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}`
: "ATM偏差关";
const ooSelectLabel = [
`振幅≤${fmt(plan?.oo_amplitude_pct ?? 1.5, 1)}%/${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h`,
plan?.oo_amplitude_filter_enabled
? `振幅≤${fmt(plan?.oo_amplitude_pct ?? 1.5, 1)}%/${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h`
: `振幅回看${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h(过滤关)`,
`贴高低≤${fmt(plan?.oo_strike_max_dev_pct ?? 1, 1)}%`,
"虚值Call@高·Put@低",
`剩余≥${fmt(plan?.oo_min_option_hours ?? 24, 0)}h`,
+104 -102
View File
@@ -113,6 +113,7 @@ export default function SettingsPage() {
);
const [ooAmpPct, setOoAmpPct] = useState(1.5);
const [ooAmpHours, setOoAmpHours] = useState(12);
const [ooAmpFilterOn, setOoAmpFilterOn] = useState(false);
const [ooMinHours, setOoMinHours] = useState(24);
const [ooMinLev, setOoMinLev] = useState(200);
const [ooStrikeDev, setOoStrikeDev] = useState(1);
@@ -235,6 +236,7 @@ export default function SettingsPage() {
);
setOoAmpPct(s.oo_amplitude_pct ?? 1.5);
setOoAmpHours(s.oo_amplitude_hours ?? 12);
setOoAmpFilterOn(Boolean(s.oo_amplitude_filter_enabled));
setOoMinHours(s.oo_min_option_hours ?? 24);
setOoMinLev(s.oo_min_leverage ?? 200);
setOoStrikeDev(s.oo_strike_max_dev_pct ?? 1);
@@ -412,6 +414,7 @@ export default function SettingsPage() {
hedge_mode: hedgeMode,
oo_amplitude_pct: ooAmpPct,
oo_amplitude_hours: ooAmpHours,
oo_amplitude_filter_enabled: ooAmpFilterOn,
oo_min_option_hours: ooMinHours,
oo_min_leverage: ooMinLev,
oo_strike_max_dev_pct: ooStrikeDev,
@@ -739,29 +742,68 @@ export default function SettingsPage() {
<option value="perp_option">+</option>
<option value="option_option"></option>
</select>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
N Call/Put
</p>
</div>
{isOo ? (
<div className="field">
<label htmlFor="ooRR"></label>
<input
id="ooRR"
className="mono"
type="number"
step="0.1"
min="0.5"
value={ooRewardRatio}
onChange={(e) =>
setOoRewardRatio(Number(e.target.value))
}
/>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
= × 100U 2
200U Call/Put
</p>
</div>
<>
<div className="field">
<label htmlFor="ooAmpFilt"></label>
<select
id="ooAmpFilt"
className="mono"
value={ooAmpFilterOn ? "on" : "off"}
onChange={(e) =>
setOoAmpFilterOn(e.target.value === "on")
}
>
<option value="off"></option>
<option value="on"></option>
</select>
</div>
<div className="field">
<label htmlFor="ooAmpH"></label>
<input
id="ooAmpH"
className="mono"
type="number"
step="1"
min="1"
value={ooAmpHours}
onChange={(e) =>
setOoAmpHours(Number(e.target.value))
}
/>
</div>
{ooAmpFilterOn ? (
<div className="field">
<label htmlFor="ooAmp">%</label>
<input
id="ooAmp"
className="mono"
type="number"
step="0.1"
min="0.1"
value={ooAmpPct}
onChange={(e) =>
setOoAmpPct(Number(e.target.value))
}
/>
</div>
) : null}
<div className="field">
<label htmlFor="ooRR"></label>
<input
id="ooRR"
className="mono"
type="number"
step="0.1"
min="0.5"
value={ooRewardRatio}
onChange={(e) =>
setOoRewardRatio(Number(e.target.value))
}
/>
</div>
</>
) : null}
{!isOo ? (
<div className="field">
@@ -816,11 +858,6 @@ export default function SettingsPage() {
</option>
<option value="actual"></option>
</select>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
÷
k便
便 k
</p>
</div>
) : null}
{!isOo ? (
@@ -914,11 +951,6 @@ export default function SettingsPage() {
<option value="off"></option>
<option value="on"></option>
</select>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
{riskLossPct > 3
? "亏损幅度超过 3% 时不可启用倍投。"
: "连续亏损日达阈值后翻倍。到期结算无论盈亏(含小盈利)都按亏损日计;达标平仓盈利才打断连亏。"}
</p>
</div>
{martingaleOn && riskLossPct <= 3 ? (
<>
@@ -957,26 +989,6 @@ export default function SettingsPage() {
)
}
/>
<p
className="hint"
style={{ margin: "0.35rem 0 0" }}
>
{riskLossPct}%{" "}
{martingaleStartAfter} {" "}
{martingaleMaxDoubles} {" "}
{[0, 1, 2, 3]
.filter((d) => d <= martingaleMaxDoubles)
.map(
(d) =>
`${Number(
(
riskLossPct *
2 ** d
).toPrecision(6),
)}%`,
)
.join(" → ")}
</p>
</div>
</>
) : null}
@@ -1230,35 +1242,6 @@ export default function SettingsPage() {
<div className="settings-fields">
{isOo ? (
<>
<div className="field">
<label htmlFor="ooAmp">%</label>
<input
id="ooAmp"
className="mono"
type="number"
step="0.1"
min="0.1"
value={ooAmpPct}
onChange={(e) => setOoAmpPct(Number(e.target.value))}
/>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
</p>
</div>
<div className="field">
<label htmlFor="ooAmpH"></label>
<input
id="ooAmpH"
className="mono"
type="number"
step="1"
min="1"
value={ooAmpHours}
onChange={(e) =>
setOoAmpHours(Number(e.target.value))
}
/>
</div>
<div className="field">
<label htmlFor="ooMinH"></label>
<input
@@ -1299,10 +1282,6 @@ export default function SettingsPage() {
setOoStrikeDev(Number(e.target.value))
}
/>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
Call/Put / |KHL|/HL
1%
</p>
</div>
</>
) : (
@@ -1412,9 +1391,6 @@ export default function SettingsPage() {
setOoRewardRatio(Number(e.target.value))
}
/>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
×
</p>
</div>
) : (
<>
@@ -1585,9 +1561,6 @@ export default function SettingsPage() {
<option value="1"></option>
<option value="0"></option>
</select>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
0803/ 0804
</p>
</div>
<div className="field">
<label htmlFor="fee"></label>
@@ -1637,24 +1610,50 @@ export default function SettingsPage() {
10000
</li>
) : null}
<li>
=+= N
Call/Put
</li>
{isOo ? (
<li>
+ % 1:1 使//k
</li>
<>
<li>
+ %
Call/Put = ×
</li>
<li>
%
</li>
</>
) : (
<li>
/OKX
/cash
</li>
<>
<li>
/OKX
/cash
</li>
<li>
÷
k便便
k
</li>
</>
)}
<li>
3%
</li>
</>
) : null}
{stratSub === "select" ? (
isOo ? (
<li>
Call
Put
</li>
<>
<li>
Call@高 /
Put@低
</li>
<li>
/ |KHL|/HL
1%
</li>
</>
) : (
<>
<li>
@@ -1679,7 +1678,7 @@ export default function SettingsPage() {
</li>
<li>
{isOo
? "残留回收:达标只平盈利腿后,亏损腿权利金回升到初始比例及以上时可尝试平仓;否则到期结算。"
? "期期出场:净盈 ≥ 预算×盈亏比时只平盈利腿亏损腿进残留,按回收比例或到期结算。"
: "残留期权回收比例:只平永续后,当买一权利金回升到初始权利金的该比例及以上时,按最新买一 IOC 限价卖掉归档期权(不扫市价);默认 20%。未达标或未完全成交则等到下次巡检或到期结算。"}
</li>
</>
@@ -1690,6 +1689,9 @@ export default function SettingsPage() {
LIVE / 1s
0.230
</li>
<li>
</li>
<li>
/