"use client"; /** * 策略说明的获取钩子。 * * 说明/公式由后端 `describe_strategy` 从 spec **真实推导**(不是前端拼字符串): * 这样「页面显示的公式」与「引擎实际执行的规则」只有一个来源, * 不会出现文案与实现漂移(本平台最怕的问题)。 * * 两个入口: * - `useStrategyDoc(params)`:未保存的参数也能实时预览(POST /strategies/describe), * 带去抖,避免每次按键都请求。 * - `useStrategyDocById(id)`:已保存策略(GET /strategies/{id}/describe)。 */ import { useEffect, useMemo, useRef, useState } from "react"; import { apiGet, apiPost } from "@/lib/api"; import type { StrategyDoc } from "@/lib/types"; import { paramsToSpec, type StrategyParams } from "@/components/StrategyParamsForm"; export interface DocState { doc: StrategyDoc | null; loading: boolean; error: string; } const EMPTY: DocState = { doc: null, loading: false, error: "" }; /** 未保存参数 → 说明(去抖 500ms) */ export function useStrategyDoc(params: StrategyParams | null, enabled = true): DocState { const [state, setState] = useState(EMPTY); const timer = useRef | null>(null); // 只依赖会改变说明的字段,避免改「初始资金」也重新请求 const key = useMemo(() => { if (!params) return ""; return JSON.stringify({ f: params.factors, a: params.priceAdjustment, n: params.topN, x: params.holdX, m: params.mMonths, y: params.yMonths, r: params.rebalance, fp: params.fillPolicy, c: params.conditions, s: params.excludeSt, l: params.minListingDays, cost: [params.commission, params.stamp, params.slippage, params.minCommission], }); }, [params]); useEffect(() => { if (!params || !enabled || !key) { setState(EMPTY); return; } let alive = true; if (timer.current) clearTimeout(timer.current); setState((s) => ({ ...s, loading: true })); timer.current = setTimeout(() => { apiPost("/strategies/describe", paramsToSpec(params)) .then((doc) => alive && setState({ doc, loading: false, error: "" })) .catch((e: Error) => alive && setState({ doc: null, loading: false, error: e.message })); }, 500); return () => { alive = false; if (timer.current) clearTimeout(timer.current); }; // key 已覆盖所有影响说明的字段 // eslint-disable-next-line react-hooks/exhaustive-deps }, [key, enabled]); return state; } /** 已保存策略 → 说明 */ export function useStrategyDocById(id: string | null): DocState { const [state, setState] = useState(EMPTY); useEffect(() => { if (!id) { setState(EMPTY); return; } let alive = true; setState({ doc: null, loading: true, error: "" }); apiGet(`/strategies/${encodeURIComponent(id)}/describe`) .then((doc) => alive && setState({ doc, loading: false, error: "" })) .catch((e: Error) => alive && setState({ doc: null, loading: false, error: e.message })); return () => { alive = false; }; }, [id]); return state; }