Add always-apply rule: deploy after edits; chat-only when user says 先聊.

Also split strategy settings into subtabs and hide sim equity in LIVE.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 21:41:16 +08:00
parent dbc86a1ce6
commit b7e055e37b
3 changed files with 401 additions and 262 deletions
+38
View File
@@ -0,0 +1,38 @@
---
description: 改完必推送部署;用户说「先聊」则只讨论不改代码
alwaysApply: true
---
# 交付与对话模式(硬指令)
## 先聊模式(最高优先)
用户消息含 **「先聊」**(或明确说只讨论、不要改代码、先别动手)时:
- **禁止**改代码、写文件、提交、推送、部署
- 只做讨论、方案对比、审计结论、问答
- 用户之后明确说「改吧 / 实现 / 动手」再进入改代码
## 改完必推送 + 部署
在**本仓库**完成一轮实际代码修改后(用户未说「先聊」、且不是纯问答),必须收尾:
1. **提交**:有改动则 `git add` 相关文件并 `git commit`(勿提交 `.env`、密钥、本地临时脚本如 `scripts/_ssh_*`、`_t.py`
2. **推送**`git push origin HEAD`(或当前跟踪分支)
3. **部署**:用本机脚本更新服务器(git pull + 构建 + pm2),例如:
```powershell
$env:PYTHONIOENCODING='utf-8'
# DEPLOY_PASS 须已在环境中配置;勿把密码写进规则或提交到仓库
& .\.venv\Scripts\python.exe .\scripts\deploy_remote.py
```
- 服务器:`47.236.184.99`,目录 `/opt/eth_hedge_sim`,进程 `eth-hedge-api`
- 部署成功后简要回报:commit、push、health 是否 OK
- 若用户明确说「先别部署 / 只提交不推 / 只推不部署」,则按用户当次要求覆盖本条
## 例外(可不推送部署)
- 「先聊」或只读审计、解释、方案讨论
- 仅改本地未跟踪的临时探测脚本且用户未要求交付
- 推送/部署失败时说明原因与已完成步骤,不要假装成功
+331 -262
View File
@@ -9,9 +9,15 @@ import {
} from "../api/client"; } from "../api/client";
type Tab = "strategy" | "runtime" | "account"; type Tab = "strategy" | "runtime" | "account";
type StratSub =
| "position"
| "select"
| "exit"
| "pace";
export default function SettingsPage() { export default function SettingsPage() {
const [tab, setTab] = useState<Tab>("strategy"); const [tab, setTab] = useState<Tab>("strategy");
const [stratSub, setStratSub] = useState<StratSub>("position");
const [newUsername, setNewUsername] = useState(getUsername() || "admin"); const [newUsername, setNewUsername] = useState(getUsername() || "admin");
const [currentPassword, setCurrentPassword] = useState(""); const [currentPassword, setCurrentPassword] = useState("");
@@ -120,31 +126,38 @@ export default function SettingsPage() {
e.preventDefault(); e.preventDefault();
setStratOk(""); setStratOk("");
setErr(""); setErr("");
const isLive = (runtime?.mode ?? mode) === "LIVE";
try { try {
const body: Record<string, unknown> = {
fee_rate: fee,
exit_mode: exitMode,
net_profit_target: netTarget,
premium_exit_multiple: premMult,
rest_seconds: rest,
live_order_interval_sec: orderInterval,
skip_weekends: skipWeekends,
leverage,
min_option_hours: minHours,
min_option_leverage: minOptLev,
atm_open_offset_enabled: atmOffOn,
max_atm_open_offset: maxAtmOff,
close_bid_mark_max_pct: closeDevPct,
perp_qty_eth: perpQty,
option_qty_eth: optQty,
exchange,
};
// LIVE 不改模拟资金,避免误重置本地账本
if (!isLive) {
body.initial_equity = initialEquity;
}
await apiFetch("/api/settings/strategy", { await apiFetch("/api/settings/strategy", {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify(body),
fee_rate: fee,
exit_mode: exitMode,
net_profit_target: netTarget,
premium_exit_multiple: premMult,
rest_seconds: rest,
live_order_interval_sec: orderInterval,
skip_weekends: skipWeekends,
leverage,
min_option_hours: minHours,
min_option_leverage: minOptLev,
atm_open_offset_enabled: atmOffOn,
max_atm_open_offset: maxAtmOff,
close_bid_mark_max_pct: closeDevPct,
perp_qty_eth: perpQty,
option_qty_eth: optQty,
initial_equity: initialEquity,
exchange,
}),
}); });
setStratOk( setStratOk(
"策略参数已保存(切换交易所/改模拟资金需无持仓;切换后会重连行情)", isLive
? "策略参数已保存(实盘模式不改模拟资金)"
: "策略参数已保存(切换交易所/改模拟资金需无持仓;切换后会重连行情)",
); );
} catch (ex) { } catch (ex) {
setErr(ex instanceof Error ? ex.message : String(ex)); setErr(ex instanceof Error ? ex.message : String(ex));
@@ -206,7 +219,10 @@ export default function SettingsPage() {
<button <button
type="button" type="button"
className={tab === "strategy" ? "tab active" : "tab"} className={tab === "strategy" ? "tab active" : "tab"}
onClick={() => setTab("strategy")} onClick={() => {
setTab("strategy");
loadRuntime();
}}
> >
</button> </button>
@@ -232,265 +248,318 @@ export default function SettingsPage() {
{tab === "strategy" ? ( {tab === "strategy" ? (
<div className="card settings-card"> <div className="card settings-card">
<p className="settings-lead"> <p className="settings-lead">
OKX / SIM {(runtime?.mode ?? mode) === "LIVE"
? "当前 LIVE:仓位/选约/出场影响真下单;模拟资金已隐藏。"
: "当前 SIM:本地撮合;可改模拟资金与交易所行情源。"}
</p> </p>
{stratOk ? <div className="settings-ok">{stratOk}</div> : null} {stratOk ? <div className="settings-ok">{stratOk}</div> : null}
{err && tab === "strategy" ? <div className="err">{err}</div> : null} {err && tab === "strategy" ? <div className="err">{err}</div> : null}
<div className="settings-subtabs" role="tablist" aria-label="策略子分类">
{(
[
["position", "仓位"],
["select", "选约"],
["exit", "出场"],
["pace", "节奏"],
] as const
).map(([id, label]) => (
<button
key={id}
type="button"
role="tab"
aria-selected={stratSub === id}
className={
stratSub === id ? "settings-subtab active" : "settings-subtab"
}
onClick={() => setStratSub(id)}
>
{label}
</button>
))}
</div>
<form onSubmit={onSaveStrategy}> <form onSubmit={onSaveStrategy}>
<section className="settings-section"> {stratSub === "position" ? (
<h3></h3> <section className="settings-section">
<div className="settings-fields"> <h3></h3>
<div className="field"> <div className="settings-fields">
<label htmlFor="exch">SIM </label>
<select
id="exch"
className="mono"
value={exchange}
onChange={(e) =>
setExchange(e.target.value === "binance" ? "binance" : "okx")
}
>
<option value="okx">OKXUSDC </option>
<option value="binance">USDT + </option>
</select>
<p className="settings-hint">
</p>
</div>
</div>
</section>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="equity">USDT</label>
<input
id="equity"
className="mono"
type="number"
step="100"
min="1000"
value={initialEquity}
onChange={(e) => setInitialEquity(Number(e.target.value))}
/>
<p className="settings-hint">
10000
</p>
</div>
<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>
<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 className="field">
<label htmlFor="atmoffOn"> ATM </label>
<select
id="atmoffOn"
className="mono"
value={atmOffOn ? "1" : "0"}
onChange={(e) => setAtmOffOn(e.target.value === "1")}
>
<option value="0"></option>
<option value="1"></option>
</select>
<p className="settings-hint">
|ATM |
</p>
</div>
<div className="field">
<label htmlFor="atmoff"> ATM </label>
<input
id="atmoff"
className="mono"
type="number"
step="0.5"
min="0"
disabled={!atmOffOn}
value={maxAtmOff}
onChange={(e) => setMaxAtmOff(Number(e.target.value))}
/>
<p className="settings-hint">
3
</p>
</div>
</div>
</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"> <div className="field">
<label htmlFor="netTarget">USDT</label> <label htmlFor="exch"></label>
<select
id="exch"
className="mono"
value={exchange}
onChange={(e) =>
setExchange(
e.target.value === "binance" ? "binance" : "okx",
)
}
>
<option value="okx">OKXUSDC </option>
<option value="binance">
USDT +
</option>
</select>
<p className="settings-hint">
</p>
</div>
{(runtime?.mode ?? mode) !== "LIVE" ? (
<div className="field">
<label htmlFor="equity">USDT</label>
<input
id="equity"
className="mono"
type="number"
step="100"
min="1000"
value={initialEquity}
onChange={(e) =>
setInitialEquity(Number(e.target.value))
}
/>
<p className="settings-hint">
SIM 10000
</p>
</div>
) : null}
<div className="field">
<label htmlFor="lev"></label>
<input <input
id="netTarget" 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>
) : null}
{stratSub === "select" ? (
<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 className="field">
<label htmlFor="atmoffOn"> ATM </label>
<select
id="atmoffOn"
className="mono"
value={atmOffOn ? "1" : "0"}
onChange={(e) => setAtmOffOn(e.target.value === "1")}
>
<option value="0"></option>
<option value="1"></option>
</select>
<p className="settings-hint">
|ATM |
</p>
</div>
<div className="field">
<label htmlFor="atmoff"> ATM </label>
<input
id="atmoff"
className="mono"
type="number"
step="0.5"
min="0"
disabled={!atmOffOn}
value={maxAtmOff}
onChange={(e) => setMaxAtmOff(Number(e.target.value))}
/>
<p className="settings-hint">
</p>
</div>
</div>
</section>
) : null}
{stratSub === "exit" ? (
<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))}
/>
<p className="settings-hint">
SIM LIVE
</p>
</div>
</div>
</section>
) : null}
{stratSub === "pace" ? (
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<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="orderInterval">
</label>
<input
id="orderInterval"
className="mono" className="mono"
type="number" type="number"
step="0.1" step="0.1"
min="0.1" min="0.2"
value={netTarget} max="30"
onChange={(e) => setNetTarget(Number(e.target.value))} value={orderInterval}
onChange={(e) =>
setOrderInterval(Number(e.target.value))
}
/> />
<p className="settings-hint">
LIVE / 1s 0.230
</p>
</div> </div>
) : (
<div className="field"> <div className="field">
<label htmlFor="premMult">1 = </label> <label htmlFor="skipWe"></label>
<select
id="skipWe"
className="mono"
value={skipWeekends ? "1" : "0"}
onChange={(e) => setSkipWeekends(e.target.value === "1")}
>
<option value="1"></option>
<option value="0"></option>
</select>
</div>
<div className="field">
<label htmlFor="fee"></label>
<input <input
id="premMult" id="fee"
className="mono" className="mono"
type="number" type="number"
step="0.1" step="0.0001"
min="0.1" value={fee}
value={premMult} onChange={(e) => setFee(Number(e.target.value))}
onChange={(e) => setPremMult(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))}
/>
</div> </div>
<div className="field"> </section>
<label htmlFor="rest"></label> ) : null}
<input
id="rest"
className="mono"
type="number"
step="1"
value={rest}
onChange={(e) => setRest(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="orderInterval"></label>
<input
id="orderInterval"
className="mono"
type="number"
step="0.1"
min="0.2"
max="30"
value={orderInterval}
onChange={(e) => setOrderInterval(Number(e.target.value))}
/>
<p className="settings-hint">
LIVE / 1s 1
0.230
</p>
</div>
<div className="field">
<label htmlFor="skipWe"></label>
<select
id="skipWe"
className="mono"
value={skipWeekends ? "1" : "0"}
onChange={(e) => setSkipWeekends(e.target.value === "1")}
>
<option value="1"></option>
<option value="0"></option>
</select>
</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>
</section>
<div className="settings-actions"> <div className="settings-actions">
<button className="btn" type="submit"> <button className="btn" type="submit">
+32
View File
@@ -399,6 +399,38 @@ input {
font-size: 13px; font-size: 13px;
} }
.settings-subtabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0 0 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--line);
}
.settings-subtab {
appearance: none;
border: 1px solid var(--line);
background: var(--input);
color: var(--muted);
border-radius: 6px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
}
.settings-subtab:hover {
color: var(--text);
border-color: #2a3441;
}
.settings-subtab.active {
color: #111;
background: var(--accent);
border-color: var(--accent);
font-weight: 600;
}
.settings-ok { .settings-ok {
color: var(--up); color: var(--up);
margin-bottom: 12px; margin-bottom: 12px;