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
- 若用户明确说「先别部署 / 只提交不推 / 只推不部署」,则按用户当次要求覆盖本条
## 例外(可不推送部署)
- 「先聊」或只读审计、解释、方案讨论
- 仅改本地未跟踪的临时探测脚本且用户未要求交付
- 推送/部署失败时说明原因与已完成步骤,不要假装成功
+99 -30
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,10 +126,9 @@ export default function SettingsPage() {
e.preventDefault(); e.preventDefault();
setStratOk(""); setStratOk("");
setErr(""); setErr("");
const isLive = (runtime?.mode ?? mode) === "LIVE";
try { try {
await apiFetch("/api/settings/strategy", { const body: Record<string, unknown> = {
method: "PUT",
body: JSON.stringify({
fee_rate: fee, fee_rate: fee,
exit_mode: exitMode, exit_mode: exitMode,
net_profit_target: netTarget, net_profit_target: netTarget,
@@ -139,12 +144,20 @@ export default function SettingsPage() {
close_bid_mark_max_pct: closeDevPct, close_bid_mark_max_pct: closeDevPct,
perp_qty_eth: perpQty, perp_qty_eth: perpQty,
option_qty_eth: optQty, option_qty_eth: optQty,
initial_equity: initialEquity,
exchange, exchange,
}), };
// LIVE 不改模拟资金,避免误重置本地账本
if (!isLive) {
body.initial_equity = initialEquity;
}
await apiFetch("/api/settings/strategy", {
method: "PUT",
body: JSON.stringify(body),
}); });
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,37 +248,64 @@ 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}>
{stratSub === "position" ? (
<section className="settings-section"> <section className="settings-section">
<h3></h3> <h3></h3>
<div className="settings-fields"> <div className="settings-fields">
<div className="field"> <div className="field">
<label htmlFor="exch">SIM </label> <label htmlFor="exch"></label>
<select <select
id="exch" id="exch"
className="mono" className="mono"
value={exchange} value={exchange}
onChange={(e) => onChange={(e) =>
setExchange(e.target.value === "binance" ? "binance" : "okx") setExchange(
e.target.value === "binance" ? "binance" : "okx",
)
} }
> >
<option value="okx">OKXUSDC </option> <option value="okx">OKXUSDC </option>
<option value="binance">USDT + </option> <option value="binance">
USDT +
</option>
</select> </select>
<p className="settings-hint"> <p className="settings-hint">
</p> </p>
</div> </div>
</div> {(runtime?.mode ?? mode) !== "LIVE" ? (
</section>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field"> <div className="field">
<label htmlFor="equity">USDT</label> <label htmlFor="equity">USDT</label>
<input <input
@@ -272,12 +315,15 @@ export default function SettingsPage() {
step="100" step="100"
min="1000" min="1000"
value={initialEquity} value={initialEquity}
onChange={(e) => setInitialEquity(Number(e.target.value))} onChange={(e) =>
setInitialEquity(Number(e.target.value))
}
/> />
<p className="settings-hint"> <p className="settings-hint">
10000 SIM 10000
</p> </p>
</div> </div>
) : null}
<div className="field"> <div className="field">
<label htmlFor="lev"></label> <label htmlFor="lev"></label>
<input <input
@@ -316,9 +362,11 @@ export default function SettingsPage() {
</div> </div>
</div> </div>
</section> </section>
) : null}
{stratSub === "select" ? (
<section className="settings-section"> <section className="settings-section">
<h3></h3> <h3></h3>
<div className="settings-fields"> <div className="settings-fields">
<div className="field"> <div className="field">
<label htmlFor="hours"></label> <label htmlFor="hours"></label>
@@ -333,7 +381,9 @@ export default function SettingsPage() {
/> />
</div> </div>
<div className="field"> <div className="field">
<label htmlFor="olev">÷</label> <label htmlFor="olev">
÷
</label>
<input <input
id="olev" id="olev"
className="mono" className="mono"
@@ -372,14 +422,16 @@ export default function SettingsPage() {
onChange={(e) => setMaxAtmOff(Number(e.target.value))} onChange={(e) => setMaxAtmOff(Number(e.target.value))}
/> />
<p className="settings-hint"> <p className="settings-hint">
3
</p> </p>
</div> </div>
</div> </div>
</section> </section>
) : null}
{stratSub === "exit" ? (
<section className="settings-section"> <section className="settings-section">
<h3></h3> <h3></h3>
<div className="settings-fields"> <div className="settings-fields">
<div className="field"> <div className="field">
<label htmlFor="exitMode"></label> <label htmlFor="exitMode"></label>
@@ -414,7 +466,9 @@ export default function SettingsPage() {
</div> </div>
) : ( ) : (
<div className="field"> <div className="field">
<label htmlFor="premMult">1 = </label> <label htmlFor="premMult">
1 =
</label>
<input <input
id="premMult" id="premMult"
className="mono" className="mono"
@@ -437,7 +491,18 @@ export default function SettingsPage() {
value={closeDevPct} value={closeDevPct}
onChange={(e) => setCloseDevPct(Number(e.target.value))} onChange={(e) => setCloseDevPct(Number(e.target.value))}
/> />
<p className="settings-hint">
SIM LIVE
</p>
</div> </div>
</div>
</section>
) : null}
{stratSub === "pace" ? (
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field"> <div className="field">
<label htmlFor="rest"></label> <label htmlFor="rest"></label>
<input <input
@@ -450,7 +515,9 @@ export default function SettingsPage() {
/> />
</div> </div>
<div className="field"> <div className="field">
<label htmlFor="orderInterval"></label> <label htmlFor="orderInterval">
</label>
<input <input
id="orderInterval" id="orderInterval"
className="mono" className="mono"
@@ -459,11 +526,12 @@ export default function SettingsPage() {
min="0.2" min="0.2"
max="30" max="30"
value={orderInterval} value={orderInterval}
onChange={(e) => setOrderInterval(Number(e.target.value))} onChange={(e) =>
setOrderInterval(Number(e.target.value))
}
/> />
<p className="settings-hint"> <p className="settings-hint">
LIVE / 1s 1 LIVE / 1s 0.230
0.230
</p> </p>
</div> </div>
<div className="field"> <div className="field">
@@ -491,6 +559,7 @@ export default function SettingsPage() {
</div> </div>
</div> </div>
</section> </section>
) : null}
<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;