Drive semi quote ladder by min remaining hours input.
Picks nearest expiry with hours >= threshold and refreshes the list as the field changes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,13 +23,14 @@ async def market_option_ladder(
|
|||||||
_user: Annotated[str, Depends(require_user)],
|
_user: Annotated[str, Depends(require_user)],
|
||||||
wings: int = Query(default=5, ge=1, le=12),
|
wings: int = Query(default=5, ge=1, le=12),
|
||||||
side: str = Query(default="call", pattern="^(call|put)$"),
|
side: str = Query(default="call", pattern="^(call|put)$"),
|
||||||
|
min_hours: float = Query(default=30, ge=1, le=720),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""半自动页单边报价列表:ATM 上下各 wings 档;side=call|put。"""
|
"""半自动页单边报价:选剩余时长≥min_hours 的最近到期。"""
|
||||||
gw = get_gateway()
|
gw = get_gateway()
|
||||||
ladder = getattr(gw, "option_ladder", None)
|
ladder = getattr(gw, "option_ladder", None)
|
||||||
if not callable(ladder):
|
if not callable(ladder):
|
||||||
raise HTTPException(status_code=501, detail="当前会话不支持 option-ladder")
|
raise HTTPException(status_code=501, detail="当前会话不支持 option-ladder")
|
||||||
return ladder(wings=wings, side=side)
|
return ladder(wings=wings, side=side, min_hours=min_hours)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/realign")
|
@router.post("/realign")
|
||||||
|
|||||||
@@ -1058,13 +1058,20 @@ class StrategySession:
|
|||||||
d["ask_compare"] = ac
|
d["ask_compare"] = ac
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def option_ladder(self, *, wings: int = 5, side: str = "call") -> dict[str, Any]:
|
def option_ladder(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
wings: int = 5,
|
||||||
|
side: str = "call",
|
||||||
|
min_hours: float = 30.0,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
半自动单边报价:ATM 上下若干档。
|
半自动单边报价:ATM 上下若干档。
|
||||||
side=call(看多)或 put(看空);每档含卖一/流动性/杠杆与实值|平值|虚值。
|
到期:剩余时长 ≥ min_hours 的最近一档(与选约门槛一致)。
|
||||||
"""
|
"""
|
||||||
s = self.settings
|
s = self.settings
|
||||||
wings = max(1, min(12, int(wings)))
|
wings = max(1, min(12, int(wings)))
|
||||||
|
min_h = max(1.0, float(min_hours))
|
||||||
opt_side = "put" if str(side).strip().lower() == "put" else "call"
|
opt_side = "put" if str(side).strip().lower() == "put" else "call"
|
||||||
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
|
||||||
@@ -1075,24 +1082,23 @@ class StrategySession:
|
|||||||
"rows": [],
|
"rows": [],
|
||||||
"index_px": None,
|
"index_px": None,
|
||||||
"side": opt_side,
|
"side": opt_side,
|
||||||
|
"min_hours": min_h,
|
||||||
}
|
}
|
||||||
underlying = float(mark)
|
underlying = float(mark)
|
||||||
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
||||||
from .selection import _complete_by_expiry, pick_atm_strike
|
from .selection import _complete_by_expiry, pick_atm_strike
|
||||||
|
|
||||||
eligible = list_eligible_expiry_ymds(contracts, min_hours=1.0)
|
# 与半自动选约一致:只考虑剩余 ≥ min_hours 的到期,取最近一档
|
||||||
ymd = None
|
eligible = list_eligible_expiry_ymds(contracts, min_hours=min_h)
|
||||||
if self._pair is not None and self._pair.expiry_ymd:
|
ymd = eligible[0] if eligible else None
|
||||||
ymd = str(self._pair.expiry_ymd)
|
|
||||||
if not ymd and eligible:
|
|
||||||
ymd = eligible[0]
|
|
||||||
if not ymd:
|
if not ymd:
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"detail": "无合格到期",
|
"detail": f"无剩余≥{min_h:g}h 的到期",
|
||||||
"rows": [],
|
"rows": [],
|
||||||
"index_px": underlying,
|
"index_px": underlying,
|
||||||
"side": opt_side,
|
"side": opt_side,
|
||||||
|
"min_hours": min_h,
|
||||||
}
|
}
|
||||||
complete = _complete_by_expiry(contracts)
|
complete = _complete_by_expiry(contracts)
|
||||||
if ymd not in complete:
|
if ymd not in complete:
|
||||||
@@ -1103,8 +1109,10 @@ class StrategySession:
|
|||||||
"index_px": underlying,
|
"index_px": underlying,
|
||||||
"expiry_ymd": ymd,
|
"expiry_ymd": ymd,
|
||||||
"side": opt_side,
|
"side": opt_side,
|
||||||
|
"min_hours": min_h,
|
||||||
}
|
}
|
||||||
_ems, strikes_map = complete[ymd]
|
_ems, strikes_map = complete[ymd]
|
||||||
|
hours_left = hours_until_expiry(ymd, expiry_ms=_ems)
|
||||||
strikes = sorted(float(k) for k in strikes_map.keys())
|
strikes = sorted(float(k) for k in strikes_map.keys())
|
||||||
atm = pick_atm_strike(strikes, underlying)
|
atm = pick_atm_strike(strikes, underlying)
|
||||||
if atm is None:
|
if atm is None:
|
||||||
@@ -1172,6 +1180,8 @@ class StrategySession:
|
|||||||
"side": opt_side,
|
"side": opt_side,
|
||||||
"index_px": underlying,
|
"index_px": underlying,
|
||||||
"expiry_ymd": ymd,
|
"expiry_ymd": ymd,
|
||||||
|
"hours_left": round(float(hours_left), 1) if hours_left is not None else None,
|
||||||
|
"min_hours": min_h,
|
||||||
"atm_strike": float(atm),
|
"atm_strike": float(atm),
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-13
@@ -131,6 +131,8 @@ export default function PlanPage() {
|
|||||||
side?: string;
|
side?: string;
|
||||||
index_px?: number | null;
|
index_px?: number | null;
|
||||||
expiry_ymd?: string;
|
expiry_ymd?: string;
|
||||||
|
hours_left?: number | null;
|
||||||
|
min_hours?: number;
|
||||||
atm_strike?: number;
|
atm_strike?: number;
|
||||||
rows?: Array<{
|
rows?: Array<{
|
||||||
strike: number;
|
strike: number;
|
||||||
@@ -141,6 +143,7 @@ export default function PlanPage() {
|
|||||||
lev: number | null;
|
lev: number | null;
|
||||||
}>;
|
}>;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const semiMinHRef = useRef(30);
|
||||||
|
|
||||||
const semiParamsBody = () => ({
|
const semiParamsBody = () => ({
|
||||||
semi_view_side: semiView,
|
semi_view_side: semiView,
|
||||||
@@ -186,9 +189,10 @@ export default function PlanPage() {
|
|||||||
? "short"
|
? "short"
|
||||||
: "long";
|
: "long";
|
||||||
const optSide = viewSide === "short" ? "put" : "call";
|
const optSide = viewSide === "short" ? "put" : "call";
|
||||||
|
const minH = semiMinHRef.current;
|
||||||
setLadder(
|
setLadder(
|
||||||
await apiFetch<NonNullable<typeof ladder>>(
|
await apiFetch<NonNullable<typeof ladder>>(
|
||||||
`/api/market/option-ladder?wings=5&side=${optSide}`,
|
`/api/market/option-ladder?wings=5&side=${optSide}&min_hours=${minH}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -249,6 +253,10 @@ export default function PlanPage() {
|
|||||||
semiViewRef.current = semiView;
|
semiViewRef.current = semiView;
|
||||||
}, [semiView]);
|
}, [semiView]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
semiMinHRef.current = Number(semiMinH) > 0 ? Number(semiMinH) : 30;
|
||||||
|
}, [semiMinH]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
const t = window.setInterval(refresh, 1500);
|
const t = window.setInterval(refresh, 1500);
|
||||||
@@ -327,25 +335,29 @@ export default function PlanPage() {
|
|||||||
const semiOn = !!plan?.semi_auto_enabled && !isOo;
|
const semiOn = !!plan?.semi_auto_enabled && !isOo;
|
||||||
const showAmpCard = plan?.oo_amplitude_filter_enabled === true;
|
const showAmpCard = plan?.oo_amplitude_filter_enabled === true;
|
||||||
|
|
||||||
// 看法切换时立刻拉 Call/Put 列表
|
// 看法 / 最短小时变化时立刻刷新报价链(小时输入防抖)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!semiOn || planTab !== "semi") return;
|
if (!semiOn || planTab !== "semi") return;
|
||||||
const optSide = semiView === "short" ? "put" : "call";
|
const optSide = semiView === "short" ? "put" : "call";
|
||||||
|
const minH = Number(semiMinH) > 0 ? Number(semiMinH) : 30;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void (async () => {
|
const t = window.setTimeout(() => {
|
||||||
try {
|
void (async () => {
|
||||||
const lad = await apiFetch<NonNullable<typeof ladder>>(
|
try {
|
||||||
`/api/market/option-ladder?wings=5&side=${optSide}`,
|
const lad = await apiFetch<NonNullable<typeof ladder>>(
|
||||||
);
|
`/api/market/option-ladder?wings=5&side=${optSide}&min_hours=${minH}`,
|
||||||
if (!cancelled) setLadder(lad);
|
);
|
||||||
} catch {
|
if (!cancelled) setLadder(lad);
|
||||||
/* ignore */
|
} catch {
|
||||||
}
|
/* ignore */
|
||||||
})();
|
}
|
||||||
|
})();
|
||||||
|
}, 350);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
window.clearTimeout(t);
|
||||||
};
|
};
|
||||||
}, [semiView, semiOn, planTab]);
|
}, [semiView, semiMinH, semiOn, planTab]);
|
||||||
const exitMode = plan?.exit_mode ?? "fixed_usdt";
|
const exitMode = plan?.exit_mode ?? "fixed_usdt";
|
||||||
const riskBased =
|
const riskBased =
|
||||||
plan?.risk_based === true || plan?.sizing_mode === "risk_based";
|
plan?.risk_based === true || plan?.sizing_mode === "risk_based";
|
||||||
@@ -893,6 +905,10 @@ export default function PlanPage() {
|
|||||||
{ladder?.expiry_ymd
|
{ladder?.expiry_ymd
|
||||||
? `到期 ${ladder.expiry_ymd}`
|
? `到期 ${ladder.expiry_ymd}`
|
||||||
: "—"}
|
: "—"}
|
||||||
|
{ladder?.hours_left != null
|
||||||
|
? ` · 剩余≈${fmt(ladder.hours_left, 1)}h`
|
||||||
|
: ""}
|
||||||
|
{` · 门槛≥${fmt(Number(semiMinH) || 30, 0)}h`}
|
||||||
{ladder?.atm_strike != null
|
{ladder?.atm_strike != null
|
||||||
? ` · ATM ${Math.round(ladder.atm_strike)}`
|
? ` · ATM ${Math.round(ladder.atm_strike)}`
|
||||||
: ""}
|
: ""}
|
||||||
|
|||||||
Reference in New Issue
Block a user