import {useEffect,useState} from "react" import {ApiError,request} from "@/lib/auth" import {Button} from "@/components/ui/button" import {Card,CardContent,CardHeader,CardTitle} from "@/components/ui/card" import type {Strategy,StrategyDetail} from "./StrategiesPanel" type Result={engine_version:string;final_equity:string;cash:string;remaining_quantity:string;total_return_bps:number;max_drawdown_bps:number;trades:{date:string;side:string;quantity:string;price:string;fee:string;realized_pnl:string|null}[];equity_curve:{date:string;equity:string}[]} type Run={id:string;strategy_version_id:string;status:string;input_hash:string;failure:string;created_at:string;finished_at:string|null;result:Result|null;input?:{dataset_name:string;symbol:string;initial_cash:string;parameters:unknown;bars:unknown[]}} const labels:Record={PENDING:"排队中",RUNNING:"执行中",COMPLETED:"已完成",FAILED:"失败"} const field="mt-2 w-full rounded-lg border bg-background px-3 py-2 text-sm" export function BacktestsPanel({onSessionExpired}:{onSessionExpired:()=>void}){ const [strategies,setStrategies]=useState([]),[strategy,setStrategy]=useState(null),[version,setVersion]=useState(""),[list,setList]=useState([]),[detail,setDetail]=useState(null),[error,setError]=useState(""),[busy,setBusy]=useState(false),[form,setForm]=useState({dataset_name:"",symbol:"",initial_cash:"",csv:"",exchange:"SSE",start:"",end:""}),[mode,setMode]=useState("market") function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"请求失败")} useEffect(()=>{const c=new AbortController();request("/api/admin/v1/strategies",{signal:c.signal}).then(setStrategies).catch(e=>{if(!c.signal.aborted)fail(e)});return()=>c.abort()},[]) useEffect(()=>{const c=new AbortController();let running=false;async function load(){if(running)return;running=true;try{const runs=await request("/api/admin/v1/backtests",{signal:c.signal});if(!c.signal.aborted)setList(runs);if(detail&&(detail.status==="PENDING"||detail.status==="RUNNING")){const r=await request(`/api/admin/v1/backtests/${detail.id}`,{signal:c.signal});if(!c.signal.aborted)setDetail(r)}}catch(e){if(!c.signal.aborted)fail(e)}finally{running=false}}void load();const timer=setInterval(()=>void load(),3000);return()=>{c.abort();clearInterval(timer)}},[detail?.id,detail?.status]) async function select(id:string){setVersion("");setStrategy(null);if(!id)return;try{const s=await request(`/api/admin/v1/strategies/${id}`);setStrategy(s)}catch(e){fail(e)}} async function submit(){setBusy(true);setError("");try{if(mode==="market"){const result=await request("/api/admin/v1/backtests/from-market",{method:"POST",body:JSON.stringify({strategy_version_id:version,symbol:form.symbol,exchange:form.exchange,start:form.start,end:form.end,initial_cash:form.initial_cash})});setDetail(result);setList(await request("/api/admin/v1/backtests"));return} const lines=form.csv.trim().split(/\r?\n/).filter(v=>v.trim());if(lines[0]?.trim()!=="date,open,high,low,close")throw new Error("CSV 首行须为 date,open,high,low,close。");const bars=lines.slice(1).map((line,index)=>{const v=line.split(",").map(s=>s.trim());if(v.length!==5)throw new Error(`CSV 第 ${index+2} 行应有五列。`);return{date:v[0],open:v[1],high:v[2],low:v[3],close:v[4]}});const result=await request("/api/admin/v1/backtests",{method:"POST",body:JSON.stringify({strategy_version_id:version,dataset_name:form.dataset_name,symbol:form.symbol,initial_cash:form.initial_cash,bars})});setDetail(result);setList(await request("/api/admin/v1/backtests"))}catch(e){fail(e)}finally{setBusy(false)}} async function open(id:string){setError("");try{setDetail(await request(`/api/admin/v1/backtests/${id}`))}catch(e){fail(e)}} const result=detail?.result return

自动获取所选股票的日线行情,执行均线交叉历史模拟。也保留手工 CSV 用于研究复验。信号使用之前已完成的日线,次日开盘模拟成交;计入费率与滑点。数据来源、复权、停牌及涨跌停须由输入数据说明,当前引擎没有模拟这些成交限制,结果不能代表可实现收益。

{error&&

{error}

}创建研究任务
{e.preventDefault();void submit()}} className="space-y-4">
{([["dataset_name","数据来源与处理说明"],["symbol","六位股票代码"],["initial_cash","初始模拟现金(元)"]] as const).map(([key,label])=>)}
{mode==="market"?
{(["start","end"] as const).map(k=>)}
: