feat: 实盘配置面板 (live_config)

DB:
- live_config表(key/value/label/updated_at)
- 默认7项: 1R金额/初始本金/风险比例/最大持仓/杠杆/策略/环境

API:
- GET /api/live/config — 读取全部配置
- PUT /api/live/config — 批量更新配置

前端:
- L1.5配置面板(L1止血和L2账户之间)
- 标题栏醒目显示 1R = $X.XX
- 7列网格: 图标+标签+值
- 编辑模式: 点编辑→改值→保存/取消
- 各项格式化: $前缀/%后缀/x后缀
This commit is contained in:
root 2026-03-02 10:55:52 +00:00
parent b1731f0f79
commit b0a463a22c
2 changed files with 111 additions and 0 deletions

View File

@ -1887,3 +1887,29 @@ async def live_events(
*params
)
return {"count": len(rows), "data": rows}
# ============ Live Config (实盘配置) ============
@app.get("/api/live/config")
async def live_config_get(user: dict = Depends(get_current_user)):
"""获取实盘配置"""
rows = await async_fetch("SELECT key, value, label, updated_at FROM live_config ORDER BY key")
config = {}
for r in rows:
config[r["key"]] = {"value": r["value"], "label": r["label"], "updated_at": str(r["updated_at"])}
return config
@app.put("/api/live/config")
async def live_config_update(request: Request, user: dict = Depends(get_current_user)):
"""更新实盘配置"""
body = await request.json()
updated = []
for key, value in body.items():
await async_execute(
"UPDATE live_config SET value = $1, updated_at = NOW() WHERE key = $2",
str(value), key
)
updated.append(key)
return {"updated": updated}

View File

@ -110,6 +110,90 @@ function L1_EmergencyPanel() {
);
}
// ═══════════════════════════════════════════════════════════════
// L1.5: 实盘配置面板
// ═══════════════════════════════════════════════════════════════
function L15_LiveConfig() {
const [config, setConfig] = useState<Record<string, any>>({});
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState<Record<string, string>>({});
const [saving, setSaving] = useState(false);
useEffect(() => {
const f = async () => {
try {
const r = await authFetch("/api/live/config");
if (r.ok) { const d = await r.json(); setConfig(d); }
} catch {}
};
f();
}, []);
const configOrder = ["risk_per_trade_usd", "initial_capital", "risk_pct", "max_positions", "leverage", "enabled_strategies", "trade_env"];
const configIcons: Record<string, string> = {
risk_per_trade_usd: "🎯", initial_capital: "💰", risk_pct: "📊",
max_positions: "📦", leverage: "⚡", enabled_strategies: "🧠", trade_env: "🌐"
};
const startEdit = () => {
const d: Record<string, string> = {};
for (const k of configOrder) { d[k] = config[k]?.value || ""; }
setDraft(d); setEditing(true);
};
const saveConfig = async () => {
setSaving(true);
try {
const r = await authFetch("/api/live/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
if (r.ok) {
const r2 = await authFetch("/api/live/config");
if (r2.ok) setConfig(await r2.json());
setEditing(false);
}
} catch {} finally { setSaving(false); }
};
const riskUsd = parseFloat(config.risk_per_trade_usd?.value || "2");
return (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between">
<h3 className="font-semibold text-slate-800 text-xs flex items-center gap-2">
<span className="text-blue-600 font-bold text-sm">1R = ${riskUsd.toFixed(2)}</span>
</h3>
{!editing ? (
<button onClick={startEdit} className="px-2 py-0.5 rounded text-[10px] bg-slate-100 text-slate-600 hover:bg-slate-200"></button>
) : (
<div className="flex gap-1">
<button onClick={() => setEditing(false)} className="px-2 py-0.5 rounded text-[10px] bg-slate-100 text-slate-500"></button>
<button onClick={saveConfig} disabled={saving} className="px-2 py-0.5 rounded text-[10px] bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50">{saving ? "保存中..." : "保存"}</button>
</div>
)}
</div>
<div className="p-3 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-2">
{configOrder.map(k => {
const c = config[k];
if (!c) return null;
return (
<div key={k} className="text-center">
<div className="text-[10px] text-slate-400 mb-0.5">{configIcons[k]} {c.label}</div>
{editing ? (
<input value={draft[k] || ""} onChange={e => setDraft({...draft, [k]: e.target.value})}
className="w-full text-center text-xs font-mono border border-slate-300 rounded px-1 py-0.5 focus:border-blue-500 focus:outline-none" />
) : (
<div className="text-sm font-bold text-slate-800 font-mono">
{k === "risk_per_trade_usd" ? `$${c.value}` : k === "risk_pct" ? `${c.value}%` : k === "leverage" ? `${c.value}x` : c.value}
</div>
)}
</div>
);
})}
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L2: 账户概览8卡片
// ═══════════════════════════════════════════════════════════════
@ -655,6 +739,7 @@ export default function LiveTradingPage() {
</div>
</div>
<L1_EmergencyPanel />
<L15_LiveConfig />
<L2_AccountOverview />
<L3_Positions />
<L4_ExecutionQuality />