535 lines
22 KiB
TypeScript
535 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { authFetch } from "@/lib/auth";
|
||
import { useAuth } from "@/lib/auth";
|
||
import Link from "next/link";
|
||
import { ArrowLeft, Save, Info } from "lucide-react";
|
||
|
||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||
export interface StrategyFormData {
|
||
display_name: string;
|
||
symbol: string;
|
||
direction: string;
|
||
initial_balance: number;
|
||
cvd_fast_window: string;
|
||
cvd_slow_window: string;
|
||
weight_direction: number;
|
||
weight_env: number;
|
||
weight_aux: number;
|
||
weight_momentum: number;
|
||
entry_score: number;
|
||
// 门1 波动率
|
||
gate_vol_enabled: boolean;
|
||
vol_atr_pct_min: number;
|
||
// 门2 CVD共振
|
||
gate_cvd_enabled: boolean;
|
||
// 门3 鲸鱼否决
|
||
gate_whale_enabled: boolean;
|
||
whale_usd_threshold: number;
|
||
whale_flow_pct: number;
|
||
// 门4 OBI否决
|
||
gate_obi_enabled: boolean;
|
||
obi_threshold: number;
|
||
// 门5 期现背离
|
||
gate_spot_perp_enabled: boolean;
|
||
spot_perp_threshold: number;
|
||
sl_atr_multiplier: number;
|
||
tp1_ratio: number;
|
||
tp2_ratio: number;
|
||
timeout_minutes: number;
|
||
flip_threshold: number;
|
||
description: string;
|
||
}
|
||
|
||
export const DEFAULT_FORM: StrategyFormData = {
|
||
display_name: "",
|
||
symbol: "BTCUSDT",
|
||
direction: "both",
|
||
initial_balance: 10000,
|
||
cvd_fast_window: "30m",
|
||
cvd_slow_window: "4h",
|
||
weight_direction: 55,
|
||
weight_env: 25,
|
||
weight_aux: 15,
|
||
weight_momentum: 5,
|
||
entry_score: 75,
|
||
gate_vol_enabled: true,
|
||
vol_atr_pct_min: 0.002,
|
||
gate_cvd_enabled: true,
|
||
gate_whale_enabled: true,
|
||
whale_usd_threshold: 50000,
|
||
whale_flow_pct: 0.5,
|
||
gate_obi_enabled: true,
|
||
obi_threshold: 0.35,
|
||
gate_spot_perp_enabled: false,
|
||
spot_perp_threshold: 0.005,
|
||
sl_atr_multiplier: 1.5,
|
||
tp1_ratio: 0.75,
|
||
tp2_ratio: 1.5,
|
||
timeout_minutes: 240,
|
||
flip_threshold: 80,
|
||
description: "",
|
||
};
|
||
|
||
// ─── Per-symbol 推荐值 ────────────────────────────────────────────────────────
|
||
// 来自 v53.json symbol_gates,与 signal_engine.py 默认值保持一致
|
||
export const SYMBOL_RECOMMENDED: Record<string, Partial<StrategyFormData>> = {
|
||
BTCUSDT: {
|
||
vol_atr_pct_min: 0.002, // ATR需>价格0.2%
|
||
whale_usd_threshold: 100000, // 鲸鱼单>10万USD
|
||
whale_flow_pct: 0.5, // BTC鲸鱼流量>50%才否决
|
||
obi_threshold: 0.30, // OBI阈值宽松(BTC流动性好)
|
||
spot_perp_threshold: 0.003, // 期现溢价<0.3%
|
||
},
|
||
ETHUSDT: {
|
||
vol_atr_pct_min: 0.003, // ETH波动需更大
|
||
whale_usd_threshold: 50000,
|
||
whale_flow_pct: 0.5,
|
||
obi_threshold: 0.35,
|
||
spot_perp_threshold: 0.005,
|
||
},
|
||
SOLUSDT: {
|
||
vol_atr_pct_min: 0.004, // SOL波动更剧烈,需更高阈值
|
||
whale_usd_threshold: 20000,
|
||
whale_flow_pct: 0.5,
|
||
obi_threshold: 0.45, // SOL OBI噪音多,需更严
|
||
spot_perp_threshold: 0.008,
|
||
},
|
||
XRPUSDT: {
|
||
vol_atr_pct_min: 0.0025,
|
||
whale_usd_threshold: 30000,
|
||
whale_flow_pct: 0.5,
|
||
obi_threshold: 0.40,
|
||
spot_perp_threshold: 0.006,
|
||
},
|
||
};
|
||
|
||
export function applySymbolDefaults(form: StrategyFormData, symbol: string): StrategyFormData {
|
||
const rec = SYMBOL_RECOMMENDED[symbol] || SYMBOL_RECOMMENDED["BTCUSDT"];
|
||
return { ...form, symbol, ...rec };
|
||
}
|
||
|
||
// ─── Helper Components ────────────────────────────────────────────────────────
|
||
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||
return (
|
||
<div className="flex items-center gap-1 mb-1">
|
||
<span className="text-xs font-medium text-slate-600">{label}</span>
|
||
{hint && (
|
||
<span className="group relative">
|
||
<Info size={11} className="text-slate-400 cursor-help" />
|
||
<span className="hidden group-hover:block absolute left-4 top-0 z-10 w-48 text-[10px] bg-slate-800 text-white rounded px-2 py-1">
|
||
{hint}
|
||
</span>
|
||
</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function NumberInput({
|
||
value, onChange, min, max, step = 1, disabled = false
|
||
}: {
|
||
value: number; onChange: (v: number) => void;
|
||
min: number; max: number; step?: number; disabled?: boolean;
|
||
}) {
|
||
return (
|
||
<input
|
||
type="number"
|
||
value={value}
|
||
min={min}
|
||
max={max}
|
||
step={step}
|
||
disabled={disabled}
|
||
onChange={(e) => {
|
||
const v = parseFloat(e.target.value);
|
||
if (!isNaN(v)) onChange(Math.min(max, Math.max(min, v)));
|
||
}}
|
||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300 disabled:bg-slate-50 disabled:text-slate-400"
|
||
/>
|
||
);
|
||
}
|
||
|
||
function SelectInput({
|
||
value, onChange, options
|
||
}: {
|
||
value: string; onChange: (v: string) => void;
|
||
options: { label: string; value: string }[];
|
||
}) {
|
||
return (
|
||
<select
|
||
value={value}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300 bg-white"
|
||
>
|
||
{options.map((o) => (
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
))}
|
||
</select>
|
||
);
|
||
}
|
||
|
||
function GateRow({
|
||
label, hint, enabled, onToggle, children
|
||
}: {
|
||
label: string; hint: string; enabled: boolean; onToggle: () => void; children?: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<div className={`border rounded-lg p-3 transition-colors ${enabled ? "border-blue-200 bg-blue-50/30" : "border-slate-200 bg-slate-50/50"}`}>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-xs font-medium text-slate-700">{label}</span>
|
||
<span className="group relative">
|
||
<Info size={11} className="text-slate-400 cursor-help" />
|
||
<span className="hidden group-hover:block absolute left-4 top-0 z-10 w-52 text-[10px] bg-slate-800 text-white rounded px-2 py-1">
|
||
{hint}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onToggle}
|
||
className={`relative inline-flex items-center w-10 h-5 rounded-full transition-colors flex-shrink-0 ${enabled ? "bg-blue-500" : "bg-slate-300"}`}
|
||
>
|
||
<span className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${enabled ? "translate-x-5" : "translate-x-0"}`} />
|
||
</button>
|
||
</div>
|
||
{enabled && <div className="mt-2">{children}</div>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Main Form Component ──────────────────────────────────────────────────────
|
||
interface StrategyFormProps {
|
||
mode: "create" | "edit";
|
||
initialData: StrategyFormData;
|
||
strategyId?: string;
|
||
onSuccess?: (id: string) => void;
|
||
isBalanceEditable?: boolean;
|
||
}
|
||
|
||
export default function StrategyForm({ mode, initialData, strategyId, onSuccess, isBalanceEditable = true }: StrategyFormProps) {
|
||
useAuth();
|
||
const router = useRouter();
|
||
const [form, setForm] = useState<StrategyFormData>(initialData);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [error, setError] = useState("");
|
||
|
||
const set = <K extends keyof StrategyFormData>(key: K, value: StrategyFormData[K]) => {
|
||
setForm((prev) => ({ ...prev, [key]: value }));
|
||
};
|
||
|
||
const weightsTotal = form.weight_direction + form.weight_env + form.weight_aux + form.weight_momentum;
|
||
const weightsOk = weightsTotal === 100;
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!weightsOk) {
|
||
setError(`权重合计必须等于 100,当前为 ${weightsTotal}`);
|
||
return;
|
||
}
|
||
if (!form.display_name.trim()) {
|
||
setError("策略名称不能为空");
|
||
return;
|
||
}
|
||
setError("");
|
||
setSubmitting(true);
|
||
|
||
try {
|
||
const payload: Record<string, unknown> = { ...form };
|
||
if (!payload.description) delete payload.description;
|
||
|
||
let res: Response;
|
||
if (mode === "create") {
|
||
res = await authFetch("/api/strategies", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
} else {
|
||
// Edit: only send changed fields (excluding symbol & initial_balance)
|
||
const { symbol: _s, initial_balance: _b, ...editPayload } = payload;
|
||
void _s; void _b;
|
||
res = await authFetch(`/api/strategies/${strategyId}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(editPayload),
|
||
});
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.[0]?.msg || err.detail || "请求失败");
|
||
}
|
||
const data = await res.json();
|
||
const newId = data.strategy?.strategy_id || strategyId || "";
|
||
if (onSuccess) onSuccess(newId);
|
||
else router.push(`/strategy-plaza/${newId}`);
|
||
} catch (err: unknown) {
|
||
setError(err instanceof Error ? err.message : "未知错误");
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
{/* ── 基础信息 ── */}
|
||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||
<h3 className="text-sm font-semibold text-slate-700 mb-3">基础信息</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<FieldLabel label="策略名称" hint="自由命名,最多50字符" />
|
||
<input
|
||
type="text"
|
||
value={form.display_name}
|
||
onChange={(e) => set("display_name", e.target.value)}
|
||
placeholder="例如:我的BTC激进策略"
|
||
maxLength={50}
|
||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="交易对" />
|
||
<SelectInput
|
||
value={form.symbol}
|
||
onChange={(v) => {
|
||
if (mode === "create") {
|
||
// 新建时切换币种自动填入推荐值
|
||
setForm((prev) => applySymbolDefaults(prev, v));
|
||
} else {
|
||
set("symbol", v);
|
||
}
|
||
}}
|
||
options={[
|
||
{ label: "BTC/USDT", value: "BTCUSDT" },
|
||
{ label: "ETH/USDT", value: "ETHUSDT" },
|
||
{ label: "SOL/USDT", value: "SOLUSDT" },
|
||
{ label: "XRP/USDT", value: "XRPUSDT" },
|
||
]}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="交易方向" />
|
||
<SelectInput
|
||
value={form.direction}
|
||
onChange={(v) => set("direction", v)}
|
||
options={[
|
||
{ label: "多空双向", value: "both" },
|
||
{ label: "只做多", value: "long_only" },
|
||
{ label: "只做空", value: "short_only" },
|
||
]}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="初始资金 (USDT)" hint="最少 1,000 USDT" />
|
||
<NumberInput
|
||
value={form.initial_balance}
|
||
onChange={(v) => set("initial_balance", v)}
|
||
min={1000}
|
||
max={1000000}
|
||
step={1000}
|
||
disabled={mode === "edit" && !isBalanceEditable}
|
||
/>
|
||
</div>
|
||
<div className="md:col-span-2">
|
||
<FieldLabel label="策略描述(可选)" />
|
||
<input
|
||
type="text"
|
||
value={form.description}
|
||
onChange={(e) => set("description", e.target.value)}
|
||
placeholder="简短描述这个策略的思路"
|
||
maxLength={200}
|
||
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── CVD 窗口 ── */}
|
||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||
<h3 className="text-sm font-semibold text-slate-700 mb-3">CVD 窗口</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<FieldLabel label="快线 CVD" hint="短周期CVD,捕捉近期买卖力量" />
|
||
<SelectInput
|
||
value={form.cvd_fast_window}
|
||
onChange={(v) => set("cvd_fast_window", v)}
|
||
options={[
|
||
{ label: "5m(超短线)", value: "5m" },
|
||
{ label: "15m(短线)", value: "15m" },
|
||
{ label: "30m(中短线)", value: "30m" },
|
||
]}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="慢线 CVD" hint="长周期CVD,反映趋势方向" />
|
||
<SelectInput
|
||
value={form.cvd_slow_window}
|
||
onChange={(v) => set("cvd_slow_window", v)}
|
||
options={[
|
||
{ label: "30m", value: "30m" },
|
||
{ label: "1h(推荐)", value: "1h" },
|
||
{ label: "4h(趋势)", value: "4h" },
|
||
]}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 四层权重 ── */}
|
||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h3 className="text-sm font-semibold text-slate-700">四层评分权重</h3>
|
||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${weightsOk ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-600"}`}>
|
||
合计: {weightsTotal}/100
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<FieldLabel label="方向得分权重" hint="CVD方向信号的权重,建议50-65" />
|
||
<NumberInput value={form.weight_direction} onChange={(v) => set("weight_direction", v)} min={10} max={80} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="环境得分权重" hint="市场环境(OI/FR/资金费率)的权重" />
|
||
<NumberInput value={form.weight_env} onChange={(v) => set("weight_env", v)} min={5} max={60} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="辅助因子权重" hint="清算/现货溢价等辅助信号的权重" />
|
||
<NumberInput value={form.weight_aux} onChange={(v) => set("weight_aux", v)} min={0} max={40} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="动量权重" hint="短期价格动量信号的权重" />
|
||
<NumberInput value={form.weight_momentum} onChange={(v) => set("weight_momentum", v)} min={0} max={20} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 入场阈值 ── */}
|
||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||
<h3 className="text-sm font-semibold text-slate-700 mb-3">入场阈值</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<FieldLabel label="入场最低总分" hint="信号总分超过此值才开仓,默认75,越高越严格" />
|
||
<NumberInput value={form.entry_score} onChange={(v) => set("entry_score", v)} min={60} max={95} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 五道 Gate ── */}
|
||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||
<h3 className="text-sm font-semibold text-slate-700 mb-3">过滤门控(Gate)</h3>
|
||
<div className="space-y-3">
|
||
{/* 推荐值提示 */}
|
||
{(() => {
|
||
const rec = SYMBOL_RECOMMENDED[form.symbol];
|
||
if (!rec) return null;
|
||
return (
|
||
<div className="text-xs text-slate-500 bg-slate-50 rounded-lg px-3 py-2">
|
||
📌 {form.symbol.replace("USDT","")} 推荐值:ATR%≥{((rec.vol_atr_pct_min??0)*100).toFixed(2)}%,鲸鱼≥${((rec.whale_usd_threshold??50000)/1000).toFixed(0)}k,OBI阈值{rec.obi_threshold}
|
||
</div>
|
||
);
|
||
})()}
|
||
<GateRow
|
||
label="门1:波动率门 (ATR%价格)"
|
||
hint="ATR占价格比例低于阈值时不开仓,过滤低波动时段"
|
||
enabled={form.gate_vol_enabled}
|
||
onToggle={() => set("gate_vol_enabled", !form.gate_vol_enabled)}
|
||
>
|
||
<FieldLabel label="ATR% 最低阈值" hint={`推荐:BTC=0.002, ETH=0.003, SOL=0.004, XRP=0.0025(当前${form.symbol.replace("USDT","")}推荐${SYMBOL_RECOMMENDED[form.symbol]?.vol_atr_pct_min??0.002})`} />
|
||
<NumberInput value={form.vol_atr_pct_min} onChange={(v) => set("vol_atr_pct_min", v)} min={0.0001} max={0.02} step={0.0005} />
|
||
</GateRow>
|
||
<GateRow
|
||
label="门2:CVD共振方向门"
|
||
hint="要求快慢两条CVD同向(双线共振),否则视为无方向不开仓"
|
||
enabled={form.gate_cvd_enabled}
|
||
onToggle={() => set("gate_cvd_enabled", !form.gate_cvd_enabled)}
|
||
/>
|
||
<GateRow
|
||
label="门3:鲸鱼否决门"
|
||
hint="检测大单方向:ALT用USD金额阈值,BTC用鲸鱼CVD流量比例"
|
||
enabled={form.gate_whale_enabled}
|
||
onToggle={() => set("gate_whale_enabled", !form.gate_whale_enabled)}
|
||
>
|
||
<FieldLabel label="大单USD阈值 (ALT)" hint={`推荐:BTC=10万, ETH=5万, SOL=2万, XRP=3万(当前推荐$${((SYMBOL_RECOMMENDED[form.symbol]?.whale_usd_threshold??50000)/1000).toFixed(0)}k)`} />
|
||
<NumberInput value={form.whale_usd_threshold} onChange={(v) => set("whale_usd_threshold", v)} min={1000} max={1000000} step={5000} />
|
||
<FieldLabel label="鲸鱼CVD流量阈值 (BTC)" hint="0~1,BTC鲸鱼净方向比例超过此值才否决,推荐0.5" />
|
||
<NumberInput value={form.whale_flow_pct} onChange={(v) => set("whale_flow_pct", v)} min={0} max={1} step={0.05} />
|
||
</GateRow>
|
||
<GateRow
|
||
label="门4:订单簿失衡门 (OBI)"
|
||
hint="要求订单簿方向与信号一致,OBI绝对值超过阈值才否决反向信号"
|
||
enabled={form.gate_obi_enabled}
|
||
onToggle={() => set("gate_obi_enabled", !form.gate_obi_enabled)}
|
||
>
|
||
<FieldLabel label="OBI 否决阈值" hint={`推荐:BTC=0.30(宽松), ETH=0.35, SOL=0.45(严格)(当前推荐${SYMBOL_RECOMMENDED[form.symbol]?.obi_threshold??0.35})`} />
|
||
<NumberInput value={form.obi_threshold} onChange={(v) => set("obi_threshold", v)} min={0.1} max={0.9} step={0.05} />
|
||
</GateRow>
|
||
<GateRow
|
||
label="门5:期现背离门"
|
||
hint="要求现货与永续溢价低于阈值,过滤套利异常时段(默认关闭)"
|
||
enabled={form.gate_spot_perp_enabled}
|
||
onToggle={() => set("gate_spot_perp_enabled", !form.gate_spot_perp_enabled)}
|
||
>
|
||
<FieldLabel label="溢价率阈值" hint={`推荐:BTC=0.003, ETH=0.005, SOL=0.008(当前推荐${SYMBOL_RECOMMENDED[form.symbol]?.spot_perp_threshold??0.005})`} />
|
||
<NumberInput value={form.spot_perp_threshold} onChange={(v) => set("spot_perp_threshold", v)} min={0.0005} max={0.01} step={0.0005} />
|
||
</GateRow>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 风控参数 ── */}
|
||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||
<h3 className="text-sm font-semibold text-slate-700 mb-3">风控参数</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<FieldLabel label="SL 宽度 (× ATR)" hint="止损距离 = SL倍数 × ATR,默认1.5" />
|
||
<NumberInput value={form.sl_atr_multiplier} onChange={(v) => set("sl_atr_multiplier", v)} min={0.5} max={3.0} step={0.1} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="TP1 目标 (× RD)" hint="第一止盈 = TP1倍数 × 风险距离,默认0.75" />
|
||
<NumberInput value={form.tp1_ratio} onChange={(v) => set("tp1_ratio", v)} min={0.3} max={2.0} step={0.05} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="TP2 目标 (× RD)" hint="第二止盈 = TP2倍数 × 风险距离,默认1.5" />
|
||
<NumberInput value={form.tp2_ratio} onChange={(v) => set("tp2_ratio", v)} min={0.5} max={4.0} step={0.1} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="超时时间 (分钟)" hint="持仓超过此时间自动平仓,默认240min" />
|
||
<NumberInput value={form.timeout_minutes} onChange={(v) => set("timeout_minutes", v)} min={30} max={1440} step={30} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel label="反转平仓阈值 (分)" hint="对手方向信号分≥此值时平仓,默认80" />
|
||
<NumberInput value={form.flip_threshold} onChange={(v) => set("flip_threshold", v)} min={60} max={95} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Error & Submit ── */}
|
||
{error && (
|
||
<div className="bg-red-50 border border-red-200 rounded-lg px-4 py-2 text-sm text-red-600">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-3">
|
||
<Link
|
||
href="/strategy-plaza"
|
||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-slate-200 text-sm text-slate-600 hover:bg-slate-50 transition-colors"
|
||
>
|
||
<ArrowLeft size={15} />
|
||
返回
|
||
</Link>
|
||
<button
|
||
type="submit"
|
||
disabled={submitting || !weightsOk}
|
||
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
<Save size={15} />
|
||
{submitting ? "保存中..." : mode === "create" ? "创建并启动" : "保存参数"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
);
|
||
}
|