Fix semi moneyness reset and show Call/Put ladder by view.
Use dirty ref so refresh cannot overwrite unsaved params; ladder fetches books for the selected side. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,14 +21,15 @@ async def market_snapshot(_user: Annotated[str, Depends(require_user)]) -> dict:
|
|||||||
@router.get("/option-ladder")
|
@router.get("/option-ladder")
|
||||||
async def market_option_ladder(
|
async def market_option_ladder(
|
||||||
_user: Annotated[str, Depends(require_user)],
|
_user: Annotated[str, Depends(require_user)],
|
||||||
wings: int = Query(default=4, ge=1, le=12),
|
wings: int = Query(default=5, ge=1, le=12),
|
||||||
|
side: str = Query(default="call", pattern="^(call|put)$"),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""半自动页 T 型报价:ATM 上下各 wings 档。"""
|
"""半自动页单边报价列表:ATM 上下各 wings 档;side=call|put。"""
|
||||||
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)
|
return ladder(wings=wings, side=side)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/realign")
|
@router.post("/realign")
|
||||||
|
|||||||
@@ -1058,17 +1058,24 @@ class StrategySession:
|
|||||||
d["ask_compare"] = ac
|
d["ask_compare"] = ac
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def option_ladder(self, *, wings: int = 4) -> dict[str, Any]:
|
def option_ladder(self, *, wings: int = 5, side: str = "call") -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
T 型报价:当前监控到期附近若干档 Call/Put 盘口。
|
半自动单边报价:ATM 上下若干档。
|
||||||
行:虚值 Call(上行)/ ATM / 虚值 Put(下行);列:Call 卖一·杠杆 | 行权价 | Put 卖一·杠杆。
|
side=call(看多)或 put(看空);每档含卖一/流动性/杠杆与实值|平值|虚值。
|
||||||
"""
|
"""
|
||||||
s = self.settings
|
s = self.settings
|
||||||
wings = max(1, min(12, int(wings)))
|
wings = max(1, min(12, int(wings)))
|
||||||
|
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
|
||||||
if mark is None or float(mark) <= 0:
|
if mark is None or float(mark) <= 0:
|
||||||
return {"ok": False, "detail": "无标的价", "rows": [], "index_px": None}
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"detail": "无标的价",
|
||||||
|
"rows": [],
|
||||||
|
"index_px": None,
|
||||||
|
"side": opt_side,
|
||||||
|
}
|
||||||
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
|
||||||
@@ -1085,6 +1092,7 @@ class StrategySession:
|
|||||||
"detail": "无合格到期",
|
"detail": "无合格到期",
|
||||||
"rows": [],
|
"rows": [],
|
||||||
"index_px": underlying,
|
"index_px": underlying,
|
||||||
|
"side": opt_side,
|
||||||
}
|
}
|
||||||
complete = _complete_by_expiry(contracts)
|
complete = _complete_by_expiry(contracts)
|
||||||
if ymd not in complete:
|
if ymd not in complete:
|
||||||
@@ -1094,6 +1102,7 @@ class StrategySession:
|
|||||||
"rows": [],
|
"rows": [],
|
||||||
"index_px": underlying,
|
"index_px": underlying,
|
||||||
"expiry_ymd": ymd,
|
"expiry_ymd": ymd,
|
||||||
|
"side": opt_side,
|
||||||
}
|
}
|
||||||
_ems, strikes_map = complete[ymd]
|
_ems, strikes_map = complete[ymd]
|
||||||
strikes = sorted(float(k) for k in strikes_map.keys())
|
strikes = sorted(float(k) for k in strikes_map.keys())
|
||||||
@@ -1105,46 +1114,62 @@ class StrategySession:
|
|||||||
"rows": [],
|
"rows": [],
|
||||||
"index_px": underlying,
|
"index_px": underlying,
|
||||||
"expiry_ymd": ymd,
|
"expiry_ymd": ymd,
|
||||||
|
"side": opt_side,
|
||||||
}
|
}
|
||||||
atm_i = min(range(len(strikes)), key=lambda i: abs(strikes[i] - float(atm)))
|
atm_i = min(range(len(strikes)), key=lambda i: abs(strikes[i] - float(atm)))
|
||||||
lo = max(0, atm_i - wings)
|
lo = max(0, atm_i - wings)
|
||||||
hi = min(len(strikes), atm_i + wings + 1)
|
hi = min(len(strikes), atm_i + wings + 1)
|
||||||
|
|
||||||
|
def _quote_side(inst_id: str | None) -> tuple[float | None, float | None]:
|
||||||
|
if not inst_id:
|
||||||
|
return None, None
|
||||||
|
q = self.ex.quote(str(inst_id))
|
||||||
|
ask = float(q.ask) if q and q.ask is not None else None
|
||||||
|
ask_sz = float(q.ask_sz) if q and q.ask_sz is not None else None
|
||||||
|
if ask is not None:
|
||||||
|
return ask, ask_sz
|
||||||
|
try:
|
||||||
|
_bids, asks, _ = self.ex.fetch_book(str(inst_id), depth=1)
|
||||||
|
if asks:
|
||||||
|
return float(asks[0].px), (
|
||||||
|
float(asks[0].sz) if asks[0].sz is not None else None
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.debug("ladder fetch_book failed inst=%s", inst_id, exc_info=True)
|
||||||
|
return None, None
|
||||||
|
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
for k in strikes[lo:hi]:
|
for k in strikes[lo:hi]:
|
||||||
legs = strikes_map[k]
|
legs = strikes_map[k]
|
||||||
call_id = legs.get("C")
|
inst = legs.get("C" if opt_side == "call" else "P")
|
||||||
put_id = legs.get("P")
|
ask, ask_sz = _quote_side(inst)
|
||||||
cq = self.ex.quote(str(call_id)) if call_id else None
|
|
||||||
pq = self.ex.quote(str(put_id)) if put_id else None
|
|
||||||
c_ask = float(cq.ask) if cq and cq.ask is not None else None
|
|
||||||
p_ask = float(pq.ask) if pq and pq.ask is not None else None
|
|
||||||
c_bid = float(cq.bid) if cq and cq.bid is not None else None
|
|
||||||
p_bid = float(pq.bid) if pq and pq.bid is not None else None
|
|
||||||
off = float(k) - underlying
|
off = float(k) - underlying
|
||||||
if abs(float(k) - float(atm)) < 1e-9:
|
if abs(float(k) - float(atm)) < 1e-9:
|
||||||
tag = "atm"
|
tag = "atm"
|
||||||
elif float(k) > underlying + 1e-9:
|
elif opt_side == "call":
|
||||||
tag = "otm_call" # Call 虚值 / Put 实值
|
tag = "itm" if float(k) < underlying - 1e-9 else "otm"
|
||||||
else:
|
else:
|
||||||
tag = "otm_put" # Put 虚值 / Call 实值
|
tag = "itm" if float(k) > underlying + 1e-9 else "otm"
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"strike": float(k),
|
"strike": float(k),
|
||||||
"offset": round(off, 2),
|
"offset": round(off, 2),
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
"call_ask": c_ask,
|
"ask": ask,
|
||||||
"call_bid": c_bid,
|
"ask_sz": ask_sz,
|
||||||
"call_lev": option_leverage(underlying, c_ask),
|
"lev": option_leverage(underlying, ask) if ask else None,
|
||||||
"put_ask": p_ask,
|
"inst_id": inst,
|
||||||
"put_bid": p_bid,
|
|
||||||
"put_lev": option_leverage(underlying, p_ask),
|
|
||||||
"call_inst_id": call_id,
|
|
||||||
"put_inst_id": put_id,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# Call:高行权价在上(虚值在上);Put:低行权价在上(虚值在上)
|
||||||
|
if opt_side == "call":
|
||||||
|
rows.sort(key=lambda r: -float(r["strike"]))
|
||||||
|
else:
|
||||||
|
rows.sort(key=lambda r: float(r["strike"]))
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"detail": "",
|
"detail": "",
|
||||||
|
"side": opt_side,
|
||||||
"index_px": underlying,
|
"index_px": underlying,
|
||||||
"expiry_ymd": ymd,
|
"expiry_ymd": ymd,
|
||||||
"atm_strike": float(atm),
|
"atm_strike": float(atm),
|
||||||
|
|||||||
+58
-42
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { apiFetch, MarketSnapshot, PlanState } from "../api/client";
|
import { apiFetch, MarketSnapshot, PlanState } from "../api/client";
|
||||||
import { fmtBidLiqEx, fmtExPx, fmtTopEx } from "../format";
|
import { fmtBidLiqEx, fmtExPx, fmtTopEx } from "../format";
|
||||||
|
|
||||||
@@ -122,10 +122,13 @@ export default function PlanPage() {
|
|||||||
const [semiPerpU, setSemiPerpU] = useState(1);
|
const [semiPerpU, setSemiPerpU] = useState(1);
|
||||||
const [semiOptU, setSemiOptU] = useState(4);
|
const [semiOptU, setSemiOptU] = useState(4);
|
||||||
const [semiDirty, setSemiDirty] = useState(false);
|
const [semiDirty, setSemiDirty] = useState(false);
|
||||||
|
const semiDirtyRef = useRef(false);
|
||||||
|
const semiViewRef = useRef<"long" | "short">("long");
|
||||||
const [planTab, setPlanTab] = useState<"semi" | "monitor">("semi");
|
const [planTab, setPlanTab] = useState<"semi" | "monitor">("semi");
|
||||||
const [ladder, setLadder] = useState<{
|
const [ladder, setLadder] = useState<{
|
||||||
ok?: boolean;
|
ok?: boolean;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
|
side?: string;
|
||||||
index_px?: number | null;
|
index_px?: number | null;
|
||||||
expiry_ymd?: string;
|
expiry_ymd?: string;
|
||||||
atm_strike?: number;
|
atm_strike?: number;
|
||||||
@@ -133,10 +136,9 @@ export default function PlanPage() {
|
|||||||
strike: number;
|
strike: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
tag: string;
|
tag: string;
|
||||||
call_ask: number | null;
|
ask: number | null;
|
||||||
call_lev: number | null;
|
ask_sz: number | null;
|
||||||
put_ask: number | null;
|
lev: number | null;
|
||||||
put_lev: number | null;
|
|
||||||
}>;
|
}>;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
@@ -160,7 +162,8 @@ export default function PlanPage() {
|
|||||||
]);
|
]);
|
||||||
setSnap(m);
|
setSnap(m);
|
||||||
setPlan(p);
|
setPlan(p);
|
||||||
if (!semiDirty) {
|
// 用 ref:interval 闭包里的 semiDirty 会过期,导致改行权类型被刷回虚值
|
||||||
|
if (!semiDirtyRef.current) {
|
||||||
setSemiView(p.semi_view_side === "short" ? "short" : "long");
|
setSemiView(p.semi_view_side === "short" ? "short" : "long");
|
||||||
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));
|
||||||
@@ -177,9 +180,15 @@ export default function PlanPage() {
|
|||||||
String(p.hedge_mode || "") !== "option_option"
|
String(p.hedge_mode || "") !== "option_option"
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const viewSide = semiDirtyRef.current
|
||||||
|
? semiViewRef.current
|
||||||
|
: p.semi_view_side === "short"
|
||||||
|
? "short"
|
||||||
|
: "long";
|
||||||
|
const optSide = viewSide === "short" ? "put" : "call";
|
||||||
setLadder(
|
setLadder(
|
||||||
await apiFetch<NonNullable<typeof ladder>>(
|
await apiFetch<NonNullable<typeof ladder>>(
|
||||||
"/api/market/option-ladder?wings=4",
|
`/api/market/option-ladder?wings=5&side=${optSide}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -232,6 +241,14 @@ export default function PlanPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
semiDirtyRef.current = semiDirty;
|
||||||
|
}, [semiDirty]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
semiViewRef.current = semiView;
|
||||||
|
}, [semiView]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
const t = window.setInterval(refresh, 1500);
|
const t = window.setInterval(refresh, 1500);
|
||||||
@@ -309,6 +326,26 @@ export default function PlanPage() {
|
|||||||
!!pos?.option2_inst_id;
|
!!pos?.option2_inst_id;
|
||||||
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(() => {
|
||||||
|
if (!semiOn || planTab !== "semi") return;
|
||||||
|
const optSide = semiView === "short" ? "put" : "call";
|
||||||
|
let cancelled = false;
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const lad = await apiFetch<NonNullable<typeof ladder>>(
|
||||||
|
`/api/market/option-ladder?wings=5&side=${optSide}`,
|
||||||
|
);
|
||||||
|
if (!cancelled) setLadder(lad);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [semiView, 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";
|
||||||
@@ -847,9 +884,11 @@ export default function PlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="card plan-t-quote" aria-label="T型报价">
|
<section className="card plan-t-quote" aria-label="期权报价列表">
|
||||||
<div className="plan-semi-head">
|
<div className="plan-semi-head">
|
||||||
<h3 className="plan-panel-title">T 型报价 · 实值 / 平值 / 虚值</h3>
|
<h3 className="plan-panel-title">
|
||||||
|
{semiView === "short" ? "Put" : "Call"} 报价 · 实值 / 平值 / 虚值
|
||||||
|
</h3>
|
||||||
<span className="mono meta">
|
<span className="mono meta">
|
||||||
{ladder?.expiry_ymd
|
{ladder?.expiry_ymd
|
||||||
? `到期 ${ladder.expiry_ymd}`
|
? `到期 ${ladder.expiry_ymd}`
|
||||||
@@ -869,36 +908,27 @@ export default function PlanPage() {
|
|||||||
<table className="plan-t-table">
|
<table className="plan-t-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Call卖一</th>
|
|
||||||
<th>Call杠杆</th>
|
|
||||||
<th>行权价</th>
|
<th>行权价</th>
|
||||||
<th>类型</th>
|
<th>类型</th>
|
||||||
<th>Put杠杆</th>
|
<th>卖一 / 流动性</th>
|
||||||
<th>Put卖一</th>
|
<th>杠杆</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[...(ladder.rows || [])]
|
{(ladder.rows || []).map((r) => {
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((r) => {
|
|
||||||
const tagZh =
|
const tagZh =
|
||||||
r.tag === "atm"
|
r.tag === "atm"
|
||||||
? "平值"
|
? "平值"
|
||||||
: r.tag === "otm_call"
|
: r.tag === "itm"
|
||||||
? "Call虚/Put实"
|
? "实值"
|
||||||
: "Put虚/Call实";
|
: "虚值";
|
||||||
const hi =
|
const hi =
|
||||||
(semiMny === "otm" &&
|
(semiMny === "otm" &&
|
||||||
((semiView === "long" && r.tag === "otm_call") ||
|
r.tag === "otm" &&
|
||||||
(semiView === "short" && r.tag === "otm_put")) &&
|
|
||||||
Math.abs(r.offset) <= Number(semiOtmOff) + 1e-9) ||
|
Math.abs(r.offset) <= Number(semiOtmOff) + 1e-9) ||
|
||||||
(semiMny === "atm" && r.tag === "atm") ||
|
(semiMny === "atm" && r.tag === "atm") ||
|
||||||
(semiMny === "itm" &&
|
(semiMny === "itm" &&
|
||||||
((semiView === "long" &&
|
(r.tag === "itm" || r.tag === "atm"));
|
||||||
(r.tag === "otm_put" || r.tag === "atm")) ||
|
|
||||||
(semiView === "short" &&
|
|
||||||
(r.tag === "otm_call" || r.tag === "atm"))));
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={r.strike}
|
key={r.strike}
|
||||||
@@ -910,16 +940,6 @@ export default function PlanPage() {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<td className="mono">
|
|
||||||
{r.call_ask != null
|
|
||||||
? fmtExPx("option", r.call_ask)
|
|
||||||
: "—"}
|
|
||||||
</td>
|
|
||||||
<td className="mono">
|
|
||||||
{r.call_lev != null
|
|
||||||
? `${r.call_lev.toFixed(0)}x`
|
|
||||||
: "—"}
|
|
||||||
</td>
|
|
||||||
<td className="mono plan-t-k">
|
<td className="mono plan-t-k">
|
||||||
{Math.round(r.strike)}
|
{Math.round(r.strike)}
|
||||||
<span className="meta">
|
<span className="meta">
|
||||||
@@ -930,14 +950,10 @@ export default function PlanPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td>{tagZh}</td>
|
<td>{tagZh}</td>
|
||||||
<td className="mono">
|
<td className="mono">
|
||||||
{r.put_lev != null
|
{fmtTopEx("option", r.ask, r.ask_sz)}
|
||||||
? `${r.put_lev.toFixed(0)}x`
|
|
||||||
: "—"}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="mono">
|
<td className="mono">
|
||||||
{r.put_ask != null
|
{r.lev != null ? `${r.lev.toFixed(0)}x` : "—"}
|
||||||
? fmtExPx("option", r.put_ask)
|
|
||||||
: "—"}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -573,10 +573,10 @@ input {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-t-table th:nth-child(3),
|
.plan-t-table th:nth-child(1),
|
||||||
.plan-t-table td:nth-child(3),
|
.plan-t-table td:nth-child(1),
|
||||||
.plan-t-table th:nth-child(4),
|
.plan-t-table th:nth-child(2),
|
||||||
.plan-t-table td:nth-child(4) {
|
.plan-t-table td:nth-child(2) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user