"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; gate_obi_enabled: boolean; obi_threshold: number; gate_whale_enabled: boolean; whale_cvd_threshold: number; gate_vol_enabled: boolean; atr_percentile_min: number; 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_obi_enabled: true, obi_threshold: 0.3, gate_whale_enabled: true, whale_cvd_threshold: 0.0, gate_vol_enabled: true, atr_percentile_min: 20, gate_spot_perp_enabled: false, spot_perp_threshold: 0.002, sl_atr_multiplier: 1.5, tp1_ratio: 0.75, tp2_ratio: 1.5, timeout_minutes: 240, flip_threshold: 80, description: "", }; // ─── Helper Components ──────────────────────────────────────────────────────── function FieldLabel({ label, hint }: { label: string; hint?: string }) { return (
{label} {hint && ( {hint} )}
); } 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 ( { 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 ( ); } function GateRow({ label, hint, enabled, onToggle, children }: { label: string; hint: string; enabled: boolean; onToggle: () => void; children: React.ReactNode; }) { return (
{label} {hint}
{enabled &&
{children}
}
); } // ─── 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(initialData); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); const set = (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 = { ...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 (
{/* ── 基础信息 ── */}

基础信息

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" />
set("symbol", v)} options={[ { label: "BTC/USDT", value: "BTCUSDT" }, { label: "ETH/USDT", value: "ETHUSDT" }, { label: "SOL/USDT", value: "SOLUSDT" }, { label: "XRP/USDT", value: "XRPUSDT" }, ]} />
set("direction", v)} options={[ { label: "多空双向", value: "both" }, { label: "只做多", value: "long_only" }, { label: "只做空", value: "short_only" }, ]} />
set("initial_balance", v)} min={1000} max={1000000} step={1000} disabled={mode === "edit" && !isBalanceEditable} />
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" />
{/* ── CVD 窗口 ── */}

CVD 窗口

set("cvd_fast_window", v)} options={[ { label: "5m(超短线)", value: "5m" }, { label: "15m(短线)", value: "15m" }, { label: "30m(中短线)", value: "30m" }, ]} />
set("cvd_slow_window", v)} options={[ { label: "30m", value: "30m" }, { label: "1h(推荐)", value: "1h" }, { label: "4h(趋势)", value: "4h" }, ]} />
{/* ── 四层权重 ── */}

四层评分权重

合计: {weightsTotal}/100
set("weight_direction", v)} min={10} max={80} />
set("weight_env", v)} min={5} max={60} />
set("weight_aux", v)} min={0} max={40} />
set("weight_momentum", v)} min={0} max={20} />
{/* ── 入场阈值 ── */}

入场阈值

set("entry_score", v)} min={60} max={95} />
{/* ── 四道 Gate ── */}

过滤门控(Gate)

set("gate_obi_enabled", !form.gate_obi_enabled)} > set("obi_threshold", v)} min={0.1} max={0.9} step={0.05} /> set("gate_whale_enabled", !form.gate_whale_enabled)} > set("whale_cvd_threshold", v)} min={-1} max={1} step={0.1} /> set("gate_vol_enabled", !form.gate_vol_enabled)} > set("atr_percentile_min", v)} min={5} max={80} /> set("gate_spot_perp_enabled", !form.gate_spot_perp_enabled)} > set("spot_perp_threshold", v)} min={0.0005} max={0.01} step={0.0005} />
{/* ── 风控参数 ── */}

风控参数

set("sl_atr_multiplier", v)} min={0.5} max={3.0} step={0.1} />
set("tp1_ratio", v)} min={0.3} max={2.0} step={0.05} />
set("tp2_ratio", v)} min={0.5} max={4.0} step={0.1} />
set("timeout_minutes", v)} min={30} max={1440} step={30} />
set("flip_threshold", v)} min={60} max={95} />
{/* ── Error & Submit ── */} {error && (
{error}
)}
返回
); }