Show option expiry countdown and widen settings for ultrawide/tablet.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,28 @@ function pnlClass(n: number | null | undefined) {
|
||||
return n > 0 ? "pos-pnl-profit" : "pos-pnl-loss";
|
||||
}
|
||||
|
||||
/** OKX 期权到期:YYMMDD 当日 08:00 UTC(上海 16:00) */
|
||||
function expiryMsFromYmd(ymd: string | null | undefined): number | null {
|
||||
if (!ymd || ymd.length !== 6) return null;
|
||||
const yy = 2000 + Number(ymd.slice(0, 2));
|
||||
const mm = Number(ymd.slice(2, 4));
|
||||
const dd = Number(ymd.slice(4, 6));
|
||||
if (![yy, mm, dd].every((x) => Number.isFinite(x))) return null;
|
||||
return Date.UTC(yy, mm - 1, dd, 8, 0, 0);
|
||||
}
|
||||
|
||||
function formatCountdown(msLeft: number): string {
|
||||
if (msLeft <= 0) return "已到期";
|
||||
const s = Math.floor(msLeft / 1000);
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
if (d > 0) return `${d}天 ${h}时 ${String(m).padStart(2, "0")}分`;
|
||||
if (h > 0) return `${h}时 ${String(m).padStart(2, "0")}分 ${String(sec).padStart(2, "0")}秒`;
|
||||
return `${m}分 ${String(sec).padStart(2, "0")}秒`;
|
||||
}
|
||||
|
||||
const PHASE_ZH: Record<string, string> = {
|
||||
idle: "空闲",
|
||||
wait_signal: "等待信号",
|
||||
@@ -29,6 +51,7 @@ export default function PlanPage() {
|
||||
const [plan, setPlan] = useState<PlanState | null>(null);
|
||||
const [err, setErr] = useState("");
|
||||
const [busy, setBusy] = useState("");
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
@@ -50,6 +73,11 @@ export default function PlanPage() {
|
||||
return () => window.clearInterval(t);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const t = window.setInterval(() => setNowMs(Date.now()), 1000);
|
||||
return () => window.clearInterval(t);
|
||||
}, []);
|
||||
|
||||
async function act(path: string, label: string) {
|
||||
setBusy(label);
|
||||
setErr("");
|
||||
@@ -84,6 +112,12 @@ export default function PlanPage() {
|
||||
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`
|
||||
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
|
||||
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
|
||||
const expiryMs =
|
||||
pos?.expiry_ms ?? expiryMsFromYmd(pos?.expiry_ymd) ?? null;
|
||||
const countdown =
|
||||
open && expiryMs != null ? formatCountdown(expiryMs - nowMs) : "—";
|
||||
const countdownUrgent =
|
||||
open && expiryMs != null && expiryMs - nowMs > 0 && expiryMs - nowMs < 6 * 3600_000;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -290,6 +324,14 @@ export default function PlanPage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="pos-grid">
|
||||
<div className="pos-cell pos-cell-wide">
|
||||
<span className="pos-label">到期倒计时</span>
|
||||
<span
|
||||
className={`pos-value mono ${countdownUrgent ? "pos-countdown-urgent" : ""}`}
|
||||
>
|
||||
{countdown}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pos-cell">
|
||||
<span className="pos-label">开仓均价</span>
|
||||
<span className="pos-value mono">{fmt(pos?.option_entry_px)}</span>
|
||||
|
||||
+209
-188
@@ -112,7 +112,7 @@ export default function SettingsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 560 }}>
|
||||
<div className="settings-page">
|
||||
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
||||
<div className="tabs">
|
||||
<button
|
||||
@@ -132,207 +132,228 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{tab === "strategy" ? (
|
||||
<div className="card">
|
||||
<p style={{ color: "var(--muted)", marginTop: 0 }}>
|
||||
<div className="card settings-card">
|
||||
<p className="settings-lead">
|
||||
无开仓时间窗;期权按剩余时长选到期 → 平值 → 校验杠杆;出场可选固定金额或权利金倍数。
|
||||
</p>
|
||||
{stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null}
|
||||
{stratOk ? <div className="settings-ok">{stratOk}</div> : null}
|
||||
{err && tab === "strategy" ? <div className="err">{err}</div> : null}
|
||||
<form onSubmit={onSaveStrategy}>
|
||||
<h3 style={{ margin: "0 0 10px", fontSize: 14, color: "var(--muted)" }}>资金</h3>
|
||||
<div className="field">
|
||||
<label htmlFor="lev">永续杠杆倍数</label>
|
||||
<input
|
||||
id="lev"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={leverage}
|
||||
onChange={(e) => setLeverage(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<section className="settings-section">
|
||||
<h3>资金</h3>
|
||||
<div className="settings-fields">
|
||||
<div className="field">
|
||||
<label htmlFor="lev">永续杠杆倍数</label>
|
||||
<input
|
||||
id="lev"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={leverage}
|
||||
onChange={(e) => setLeverage(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="perp">永续 ETH 数量</label>
|
||||
<input
|
||||
id="perp"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
value={perpQty}
|
||||
onChange={(e) => setPerpQty(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="opt">期权 ETH 数量</label>
|
||||
<input
|
||||
id="opt"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
value={optQty}
|
||||
onChange={(e) => setOptQty(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3 style={{ margin: "18px 0 10px", fontSize: 14, color: "var(--muted)" }}>策略</h3>
|
||||
<div className="field">
|
||||
<label htmlFor="hours">期权最小剩余小时</label>
|
||||
<input
|
||||
id="hours"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={minHours}
|
||||
onChange={(e) => setMinHours(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="olev">期权杠杆下限(现价÷卖一权利金)</label>
|
||||
<input
|
||||
id="olev"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={minOptLev}
|
||||
onChange={(e) => setMinOptLev(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="exitMode">出场规则</label>
|
||||
<select
|
||||
id="exitMode"
|
||||
className="mono"
|
||||
value={exitMode}
|
||||
onChange={(e) =>
|
||||
setExitMode(
|
||||
e.target.value === "premium_multiple"
|
||||
? "premium_multiple"
|
||||
: "fixed_usdt",
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="fixed_usdt">固定净盈利(USDT)</option>
|
||||
<option value="premium_multiple">权利金倍数</option>
|
||||
</select>
|
||||
</div>
|
||||
{exitMode === "fixed_usdt" ? (
|
||||
<div className="field">
|
||||
<label htmlFor="netTarget">净盈利出场目标(USDT)</label>
|
||||
<input
|
||||
id="netTarget"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
value={netTarget}
|
||||
onChange={(e) => setNetTarget(Number(e.target.value))}
|
||||
/>
|
||||
<section className="settings-section">
|
||||
<h3>选约</h3>
|
||||
<div className="settings-fields">
|
||||
<div className="field">
|
||||
<label htmlFor="hours">期权最小剩余小时</label>
|
||||
<input
|
||||
id="hours"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={minHours}
|
||||
onChange={(e) => setMinHours(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="olev">期权杠杆下限(现价÷卖一权利金)</label>
|
||||
<input
|
||||
id="olev"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={minOptLev}
|
||||
onChange={(e) => setMinOptLev(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="field">
|
||||
<label htmlFor="premMult">权利金倍数(1 = 一倍权利金)</label>
|
||||
<input
|
||||
id="premMult"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
value={premMult}
|
||||
onChange={(e) => setPremMult(Number(e.target.value))}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="settings-section">
|
||||
<h3>出场与风控</h3>
|
||||
<div className="settings-fields">
|
||||
<div className="field">
|
||||
<label htmlFor="exitMode">出场规则</label>
|
||||
<select
|
||||
id="exitMode"
|
||||
className="mono"
|
||||
value={exitMode}
|
||||
onChange={(e) =>
|
||||
setExitMode(
|
||||
e.target.value === "premium_multiple"
|
||||
? "premium_multiple"
|
||||
: "fixed_usdt",
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="fixed_usdt">固定净盈利(USDT)</option>
|
||||
<option value="premium_multiple">权利金倍数</option>
|
||||
</select>
|
||||
</div>
|
||||
{exitMode === "fixed_usdt" ? (
|
||||
<div className="field">
|
||||
<label htmlFor="netTarget">净盈利出场目标(USDT)</label>
|
||||
<input
|
||||
id="netTarget"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
value={netTarget}
|
||||
onChange={(e) => setNetTarget(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="field">
|
||||
<label htmlFor="premMult">权利金倍数(1 = 一倍权利金)</label>
|
||||
<input
|
||||
id="premMult"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
value={premMult}
|
||||
onChange={(e) => setPremMult(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<label htmlFor="closeDev">平仓买一/标记最大偏差(%)</label>
|
||||
<input
|
||||
id="closeDev"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={closeDevPct}
|
||||
onChange={(e) => setCloseDevPct(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="rest">轮间休息秒数</label>
|
||||
<input
|
||||
id="rest"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
value={rest}
|
||||
onChange={(e) => setRest(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="fee">手续费率</label>
|
||||
<input
|
||||
id="fee"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
value={fee}
|
||||
onChange={(e) => setFee(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<label htmlFor="closeDev">平仓买一/标记最大偏差(%)</label>
|
||||
<input
|
||||
id="closeDev"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={closeDevPct}
|
||||
onChange={(e) => setCloseDevPct(Number(e.target.value))}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="settings-actions">
|
||||
<button className="btn" type="submit">
|
||||
保存策略
|
||||
</button>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="perp">永续 ETH 数量</label>
|
||||
<input
|
||||
id="perp"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
value={perpQty}
|
||||
onChange={(e) => setPerpQty(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="opt">期权 ETH 数量</label>
|
||||
<input
|
||||
id="opt"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
value={optQty}
|
||||
onChange={(e) => setOptQty(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="rest">轮间休息秒数</label>
|
||||
<input
|
||||
id="rest"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
value={rest}
|
||||
onChange={(e) => setRest(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="fee">手续费率</label>
|
||||
<input
|
||||
id="fee"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
value={fee}
|
||||
onChange={(e) => setFee(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit">
|
||||
保存策略
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card">
|
||||
<div className="card settings-card settings-card-narrow">
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
||||
<form onSubmit={onSaveCreds}>
|
||||
<div className="field">
|
||||
<label htmlFor="user">新用户名</label>
|
||||
<input
|
||||
id="user"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
{ok ? <div className="settings-ok">{ok}</div> : null}
|
||||
<form onSubmit={onSaveCreds} className="settings-account-form">
|
||||
<div className="settings-fields">
|
||||
<div className="field">
|
||||
<label htmlFor="user">新用户名</label>
|
||||
<input
|
||||
id="user"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cur">当前密码</label>
|
||||
<input
|
||||
id="cur"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="np">新密码</label>
|
||||
<input
|
||||
id="np"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cp">确认新密码</label>
|
||||
<input
|
||||
id="cp"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cur">当前密码</label>
|
||||
<input
|
||||
id="cur"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className="settings-actions">
|
||||
<button className="btn" type="submit" disabled={loading}>
|
||||
{loading ? "保存中…" : "保存账号"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="np">新密码</label>
|
||||
<input
|
||||
id="np"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cp">确认新密码</label>
|
||||
<input
|
||||
id="cp"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit" disabled={loading}>
|
||||
{loading ? "保存中…" : "保存账号"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user