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:
+93
-77
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { apiFetch, MarketSnapshot, PlanState } from "../api/client";
|
||||
import { fmtBidLiqEx, fmtExPx, fmtTopEx } from "../format";
|
||||
|
||||
@@ -122,10 +122,13 @@ export default function PlanPage() {
|
||||
const [semiPerpU, setSemiPerpU] = useState(1);
|
||||
const [semiOptU, setSemiOptU] = useState(4);
|
||||
const [semiDirty, setSemiDirty] = useState(false);
|
||||
const semiDirtyRef = useRef(false);
|
||||
const semiViewRef = useRef<"long" | "short">("long");
|
||||
const [planTab, setPlanTab] = useState<"semi" | "monitor">("semi");
|
||||
const [ladder, setLadder] = useState<{
|
||||
ok?: boolean;
|
||||
detail?: string;
|
||||
side?: string;
|
||||
index_px?: number | null;
|
||||
expiry_ymd?: string;
|
||||
atm_strike?: number;
|
||||
@@ -133,10 +136,9 @@ export default function PlanPage() {
|
||||
strike: number;
|
||||
offset: number;
|
||||
tag: string;
|
||||
call_ask: number | null;
|
||||
call_lev: number | null;
|
||||
put_ask: number | null;
|
||||
put_lev: number | null;
|
||||
ask: number | null;
|
||||
ask_sz: number | null;
|
||||
lev: number | null;
|
||||
}>;
|
||||
} | null>(null);
|
||||
|
||||
@@ -160,7 +162,8 @@ export default function PlanPage() {
|
||||
]);
|
||||
setSnap(m);
|
||||
setPlan(p);
|
||||
if (!semiDirty) {
|
||||
// 用 ref:interval 闭包里的 semiDirty 会过期,导致改行权类型被刷回虚值
|
||||
if (!semiDirtyRef.current) {
|
||||
setSemiView(p.semi_view_side === "short" ? "short" : "long");
|
||||
setSemiMove(Number(p.semi_option_move_points ?? 50));
|
||||
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
|
||||
@@ -177,9 +180,15 @@ export default function PlanPage() {
|
||||
String(p.hedge_mode || "") !== "option_option"
|
||||
) {
|
||||
try {
|
||||
const viewSide = semiDirtyRef.current
|
||||
? semiViewRef.current
|
||||
: p.semi_view_side === "short"
|
||||
? "short"
|
||||
: "long";
|
||||
const optSide = viewSide === "short" ? "put" : "call";
|
||||
setLadder(
|
||||
await apiFetch<NonNullable<typeof ladder>>(
|
||||
"/api/market/option-ladder?wings=4",
|
||||
`/api/market/option-ladder?wings=5&side=${optSide}`,
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
@@ -232,6 +241,14 @@ export default function PlanPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
semiDirtyRef.current = semiDirty;
|
||||
}, [semiDirty]);
|
||||
|
||||
useEffect(() => {
|
||||
semiViewRef.current = semiView;
|
||||
}, [semiView]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
const t = window.setInterval(refresh, 1500);
|
||||
@@ -309,6 +326,26 @@ export default function PlanPage() {
|
||||
!!pos?.option2_inst_id;
|
||||
const semiOn = !!plan?.semi_auto_enabled && !isOo;
|
||||
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 riskBased =
|
||||
plan?.risk_based === true || plan?.sizing_mode === "risk_based";
|
||||
@@ -847,9 +884,11 @@ export default function PlanPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card plan-t-quote" aria-label="T型报价">
|
||||
<section className="card plan-t-quote" aria-label="期权报价列表">
|
||||
<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">
|
||||
{ladder?.expiry_ymd
|
||||
? `到期 ${ladder.expiry_ymd}`
|
||||
@@ -869,79 +908,56 @@ export default function PlanPage() {
|
||||
<table className="plan-t-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Call卖一</th>
|
||||
<th>Call杠杆</th>
|
||||
<th>行权价</th>
|
||||
<th>类型</th>
|
||||
<th>Put杠杆</th>
|
||||
<th>Put卖一</th>
|
||||
<th>卖一 / 流动性</th>
|
||||
<th>杠杆</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...(ladder.rows || [])]
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((r) => {
|
||||
const tagZh =
|
||||
r.tag === "atm"
|
||||
? "平值"
|
||||
: r.tag === "otm_call"
|
||||
? "Call虚/Put实"
|
||||
: "Put虚/Call实";
|
||||
const hi =
|
||||
(semiMny === "otm" &&
|
||||
((semiView === "long" && r.tag === "otm_call") ||
|
||||
(semiView === "short" && r.tag === "otm_put")) &&
|
||||
Math.abs(r.offset) <= Number(semiOtmOff) + 1e-9) ||
|
||||
(semiMny === "atm" && r.tag === "atm") ||
|
||||
(semiMny === "itm" &&
|
||||
((semiView === "long" &&
|
||||
(r.tag === "otm_put" || r.tag === "atm")) ||
|
||||
(semiView === "short" &&
|
||||
(r.tag === "otm_call" || r.tag === "atm"))));
|
||||
return (
|
||||
<tr
|
||||
key={r.strike}
|
||||
className={
|
||||
r.tag === "atm"
|
||||
? "plan-t-atm"
|
||||
: hi
|
||||
? "plan-t-pick"
|
||||
: 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">
|
||||
{Math.round(r.strike)}
|
||||
<span className="meta">
|
||||
{" "}
|
||||
({r.offset >= 0 ? "+" : ""}
|
||||
{fmt(r.offset, 0)})
|
||||
</span>
|
||||
</td>
|
||||
<td>{tagZh}</td>
|
||||
<td className="mono">
|
||||
{r.put_lev != null
|
||||
? `${r.put_lev.toFixed(0)}x`
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="mono">
|
||||
{r.put_ask != null
|
||||
? fmtExPx("option", r.put_ask)
|
||||
: "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{(ladder.rows || []).map((r) => {
|
||||
const tagZh =
|
||||
r.tag === "atm"
|
||||
? "平值"
|
||||
: r.tag === "itm"
|
||||
? "实值"
|
||||
: "虚值";
|
||||
const hi =
|
||||
(semiMny === "otm" &&
|
||||
r.tag === "otm" &&
|
||||
Math.abs(r.offset) <= Number(semiOtmOff) + 1e-9) ||
|
||||
(semiMny === "atm" && r.tag === "atm") ||
|
||||
(semiMny === "itm" &&
|
||||
(r.tag === "itm" || r.tag === "atm"));
|
||||
return (
|
||||
<tr
|
||||
key={r.strike}
|
||||
className={
|
||||
r.tag === "atm"
|
||||
? "plan-t-atm"
|
||||
: hi
|
||||
? "plan-t-pick"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<td className="mono plan-t-k">
|
||||
{Math.round(r.strike)}
|
||||
<span className="meta">
|
||||
{" "}
|
||||
({r.offset >= 0 ? "+" : ""}
|
||||
{fmt(r.offset, 0)})
|
||||
</span>
|
||||
</td>
|
||||
<td>{tagZh}</td>
|
||||
<td className="mono">
|
||||
{fmtTopEx("option", r.ask, r.ask_sz)}
|
||||
</td>
|
||||
<td className="mono">
|
||||
{r.lev != null ? `${r.lev.toFixed(0)}x` : "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -573,10 +573,10 @@ input {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.plan-t-table th:nth-child(3),
|
||||
.plan-t-table td:nth-child(3),
|
||||
.plan-t-table th:nth-child(4),
|
||||
.plan-t-table td:nth-child(4) {
|
||||
.plan-t-table th:nth-child(1),
|
||||
.plan-t-table td:nth-child(1),
|
||||
.plan-t-table th:nth-child(2),
|
||||
.plan-t-table td:nth-child(2) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user