From c8c61d29bf3d3cda5368aebbc84fd21fc8b09eb0 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 9 Oct 2026 23:08:47 +0800 Subject: [PATCH] fix: retain stock identities in research and AI task lists --- docs/development-state.md | 3 ++- internal/integration/candidate_analysis_test.go | 5 +++++ internal/integration/strategy_backtest_test.go | 4 ++++ internal/model/backtest.go | 1 + internal/model/candidate_analysis.go | 3 +++ internal/repository/backtest.go | 7 +++++++ internal/repository/candidate_analysis.go | 4 ++-- web/admin/src/components/AIReviewPanel.tsx | 5 +++-- web/admin/src/components/BacktestsPanel.tsx | 4 ++-- 9 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/development-state.md b/docs/development-state.md index 440d902..acc1c86 100644 --- a/docs/development-state.md +++ b/docs/development-state.md @@ -19,7 +19,8 @@ - 实际 HTTPS 查询复现:名称“诺普信”无结果,但代码 002215 和名称“诺 普 信”均返回该深交所股票。数据源在名称中填充了空格,原 SQL LIKE 连续匹配漏掉无空格查询,证券并未漏同步。 - 查询统一忽略名称与输入中的空格、制表符和全角空格,保持原始源名称;转义 LIKE 通配符为字面匹配,避免输入 % 或 _ 返回全部股票。管理端无搜索结果文案与无同步数据区分。真实 PostgreSQL 隔离 schema 的 race 回归覆盖名称/代码/空格和通配符,go vet、管理端类型检查/build/embed 通过;已通过 PR #6 合并,待 CI 上线后实查。 -- 按用户补充要求,管理端行情列表/详情、候选队列、审核详情/确认、总览观察与信号卡将代码与名称并列展示,代码使用等宽字形;选股、回测、策略证券列表以及 App 候选/计划/持仓/成交原已有代码,继续保留。管理端统一 StockIdentity 组件,类型检查/build/embed 通过。 +- 按用户补充要求,管理端行情列表/详情、候选队列、审核详情/确认、总览观察与信号卡将代码与名称并列展示,代码使用等宽字形;选股、回测详情、策略证券列表以及 App 候选/计划/持仓/成交原已有代码,继续保留。另补齐回测任务列表与 AI 任务执行中/失败时的股票身份:代码从不可覆盖输入/候选快照读取,不等待模型输出。管理端统一 StockIdentity 组件,类型检查/build/embed 通过。 +- 完整 Go race 与 go vet、管理端类型检查/build/embed 通过;新增列表集成检查保留证券代码且不返回完整回测输入。真实 gpt-6.1-sol 使用中洲控股的现有日线完成分析与独立复核(约 40 秒合计),两次结构化输出及证据索引校验通过;仅只读连接验证,没有写入 AI 发布事实。 - 用户最新指示先不处理 App 安装,保留已有 IPA 产物;当前优先验证名称搜索、股票代码展示与模型运行配置。 ## App Expo Go 真机入口(2026-10-09) diff --git a/internal/integration/candidate_analysis_test.go b/internal/integration/candidate_analysis_test.go index cd11ec1..6ed5c2d 100644 --- a/internal/integration/candidate_analysis_test.go +++ b/internal/integration/candidate_analysis_test.go @@ -129,6 +129,11 @@ func testCandidateAI(t *testing.T, ctx context.Context, pool *pgxpool.Pool, admi if err != nil || len(results) != 4 { t.Fatal("durable results missing", err, len(results)) } + for _, result := range results { + if result.Name != "Fixture" || result.Symbol != "000042" || result.Exchange != "SZSE" { + t.Fatal("analysis task lost candidate identity") + } + } } type rejectModel struct{ calls int } diff --git a/internal/integration/strategy_backtest_test.go b/internal/integration/strategy_backtest_test.go index ccd3a55..480cbd1 100644 --- a/internal/integration/strategy_backtest_test.go +++ b/internal/integration/strategy_backtest_test.go @@ -95,6 +95,10 @@ func testStrategyBacktest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, if e != nil || done.Status != "COMPLETED" || done.FinishedAt == nil || len(done.Result) == 0 { t.Fatalf("completed job: %v", e) } + listed, e := repo.List(ctx) + if e != nil || len(listed) != 1 || listed[0].Symbol != bi.Symbol || listed[0].Input != nil || listed[0].Result != nil { + t.Fatal("backtest list must retain the symbol without returning large inputs") + } if _, e := pool.Exec(ctx, `UPDATE veyra_backtest_run SET result='{}'::jsonb WHERE id=$1`, job.ID); e == nil { t.Fatal("completed result can be overwritten") } diff --git a/internal/model/backtest.go b/internal/model/backtest.go index 72afa44..62e1b77 100644 --- a/internal/model/backtest.go +++ b/internal/model/backtest.go @@ -27,6 +27,7 @@ type ResearchInput struct { Parameters StrategyParameters `json:"parameters"` } type BacktestRun struct { + Symbol string `json:"symbol"` ID string `db:"id" json:"id"` StrategyVersionID string `db:"strategy_version_id" json:"strategy_version_id"` Status string `db:"status" json:"status"` diff --git a/internal/model/candidate_analysis.go b/internal/model/candidate_analysis.go index 7b4f717..6405164 100644 --- a/internal/model/candidate_analysis.go +++ b/internal/model/candidate_analysis.go @@ -23,6 +23,9 @@ type AIConclusion struct { MissingInformation []string `json:"missing_information"` } type CandidateAnalysis struct { + Name string `json:"name,omitempty"` + Symbol string `json:"symbol,omitempty"` + Exchange string `json:"exchange,omitempty"` ID string `db:"id" json:"id"` CandidateRevisionID string `db:"candidate_revision_id" json:"candidate_revision_id"` InputVersion int64 `db:"input_version" json:"input_version"` diff --git a/internal/repository/backtest.go b/internal/repository/backtest.go index f13ebbd..208b2b1 100644 --- a/internal/repository/backtest.go +++ b/internal/repository/backtest.go @@ -21,6 +21,13 @@ const backtestColumns = `id,strategy_version_id,status,input,input_hash,result,f func scanBacktest(row pgx.Row) (model.BacktestRun, error) { var v model.BacktestRun err := row.Scan(&v.ID, &v.StrategyVersionID, &v.Status, &v.Input, &v.InputHash, &v.Result, &v.Failure, &v.LeaseID, &v.CreatedAt, &v.FinishedAt) + if err == nil { + var identity struct { + Symbol string `json:"symbol"` + } + err = json.Unmarshal(v.Input, &identity) + v.Symbol = identity.Symbol + } return v, mapError(err) } func (r *BacktestRepository) Detail(ctx context.Context, id string) (model.BacktestRun, error) { diff --git a/internal/repository/candidate_analysis.go b/internal/repository/candidate_analysis.go index 28c4b37..a935855 100644 --- a/internal/repository/candidate_analysis.go +++ b/internal/repository/candidate_analysis.go @@ -183,7 +183,7 @@ func (r *CandidateAnalysisRepository) Complete(ctx context.Context, p model.AIRe return mapError(tx.Commit(ctx)) } func (r *CandidateAnalysisRepository) List(ctx context.Context) ([]model.CandidateAnalysis, error) { - rows, err := r.pool.Query(ctx, `SELECT id,candidate_revision_id,input_version,policy_version,status,attempts,primary_model,review_model,primary_version,review_version,analysis,review,message,created_at,updated_at FROM veyra_candidate_analysis ORDER BY updated_at DESC LIMIT 100`) + rows, err := r.pool.Query(ctx, `SELECT a.id,a.candidate_revision_id,a.input_version,a.policy_version,a.status,a.attempts,a.primary_model,a.review_model,a.primary_version,a.review_version,a.analysis,a.review,a.message,a.created_at,a.updated_at,r.snapshot->>'name',r.snapshot->>'symbol',r.snapshot->>'exchange' FROM veyra_candidate_analysis a JOIN veyra_candidate_revision r ON r.id=a.candidate_revision_id ORDER BY a.updated_at DESC LIMIT 100`) if err != nil { return nil, mapError(err) } @@ -192,7 +192,7 @@ func (r *CandidateAnalysisRepository) List(ctx context.Context) ([]model.Candida for rows.Next() { var v model.CandidateAnalysis var a, b []byte - if err = rows.Scan(&v.ID, &v.CandidateRevisionID, &v.InputVersion, &v.PolicyVersion, &v.Status, &v.Attempts, &v.PrimaryModel, &v.ReviewModel, &v.PrimaryVersion, &v.ReviewVersion, &a, &b, &v.Message, &v.CreatedAt, &v.UpdatedAt); err != nil { + if err = rows.Scan(&v.ID, &v.CandidateRevisionID, &v.InputVersion, &v.PolicyVersion, &v.Status, &v.Attempts, &v.PrimaryModel, &v.ReviewModel, &v.PrimaryVersion, &v.ReviewVersion, &a, &b, &v.Message, &v.CreatedAt, &v.UpdatedAt, &v.Name, &v.Symbol, &v.Exchange); err != nil { return nil, mapError(err) } if len(a) > 0 { diff --git a/web/admin/src/components/AIReviewPanel.tsx b/web/admin/src/components/AIReviewPanel.tsx index 8f6782d..669cf44 100644 --- a/web/admin/src/components/AIReviewPanel.tsx +++ b/web/admin/src/components/AIReviewPanel.tsx @@ -1,3 +1,4 @@ +import {StockIdentity} from "./StockIdentity" import {useEffect,useState} from "react" import {request,ApiError} from "@/lib/auth" import {Button} from "@/components/ui/button" @@ -5,7 +6,7 @@ import {Badge} from "@/components/ui/badge" import {Card,CardHeader,CardTitle,CardContent,CardDescription} from "@/components/ui/card" type Policy={enabled:boolean;auto_publish:boolean;version:number} type Conclusion={symbol:string;decision:string;summary:string;risks:string;missing_information:string[]} -type Analysis={id:string;status:string;attempts:number;message:string;primary_model:string;review_model:string;analysis?:Conclusion;review?:Conclusion;updated_at:string} +type Analysis={id:string;name:string;symbol:string;exchange:string;status:string;attempts:number;message:string;primary_model:string;review_model:string;analysis?:Conclusion;review?:Conclusion;updated_at:string} const states:Record={RUNNING:"分析中",FAILED:"调用失败",APPROVED:"已通过 · 等待发布",REJECTED:"AI 未通过",PUBLISHED:"已自动发布",STALE:"版本或规则已失效"} export function AIReviewPanel({onSessionExpired}:{onSessionExpired:()=>void}){ const [policy,setPolicy]=useState(null),[results,setResults]=useState([]),[enabled,setEnabled]=useState(false),[auto,setAuto]=useState(false),[error,setError]=useState(""),[busy,setBusy]=useState(false),[notice,setNotice]=useState("") @@ -13,5 +14,5 @@ export function AIReviewPanel({onSessionExpired}:{onSessionExpired:()=>void}){ useEffect(()=>{const c=new AbortController();request("/api/admin/v1/ai-review-policy",{signal:c.signal}).then(p=>{setPolicy(p);setEnabled(p.enabled);setAuto(p.auto_publish)}).catch(e=>{if(!c.signal.aborted)failure(e)});const load=()=>request("/api/admin/v1/candidate-analyses",{signal:c.signal}).then(setResults).catch(e=>{if(!c.signal.aborted)failure(e)});void load();const timer=setInterval(()=>void load(),5000);return()=>{c.abort();clearInterval(timer)}},[onSessionExpired]) async function save(){if(!policy)return;setBusy(true);setError("");try{const p=await request("/api/admin/v1/ai-review-policy",{method:"PUT",body:JSON.stringify({enabled,auto_publish:auto,expected_version:policy.version})});setPolicy(p);setNotice("审核策略已保存,后台会处理待审核候选。")}catch(e){failure(e)}finally{setBusy(false)}} async function run(){setBusy(true);setError("");try{await request("/api/admin/v1/candidate-analyses/run",{method:"POST"});setNotice("已通知后台处理;结果自动刷新。")}catch(e){failure(e)}finally{setBusy(false)}} - return AI 候选分析与自动审核策略筛选 → 主模型分析 → 审核模型复核 → 服务端规则校验 → App 发布。仅限模拟研究;不会生成计划或成交。

主模型和审核模型必须配置并启用。关闭自动发布时保留 AI 意见供人工复核;失败最多重试三次,候选过期不发布。

{notice&&

{notice}

}{error&&

{error}

}
{results.length===0&&

暂无 AI 任务。启用后自动处理有效待审核候选。

}{results.map(a=>
{a.analysis?.symbol??"候选分析任务"}{states[a.status]??a.status}

{a.message||"正在调用模型…"}

{a.analysis&&<>

分析:{a.analysis.summary}

风险:{a.analysis.risks}

}{a.review&&

复核:{a.review.summary}

}

{a.primary_model||"待调用"} / {a.review_model||"待调用"} · 尝试 {a.attempts} · {new Date(a.updated_at).toLocaleString("zh-CN")}

)}
+ return AI 候选分析与自动审核策略筛选 → 主模型分析 → 审核模型复核 → 服务端规则校验 → App 发布。仅限模拟研究;不会生成计划或成交。

主模型和审核模型必须配置并启用。关闭自动发布时保留 AI 意见供人工复核;失败最多重试三次,候选过期不发布。

{notice&&

{notice}

}{error&&

{error}

}
{results.length===0&&

暂无 AI 任务。启用后自动处理有效待审核候选。

}{results.map(a=>
{states[a.status]??a.status}

{a.message||"正在调用模型…"}

{a.analysis&&<>

分析:{a.analysis.summary}

风险:{a.analysis.risks}

}{a.review&&

复核:{a.review.summary}

}

{a.primary_model||"待调用"} / {a.review_model||"待调用"} · 尝试 {a.attempts} · {new Date(a.updated_at).toLocaleString("zh-CN")}

)}
} diff --git a/web/admin/src/components/BacktestsPanel.tsx b/web/admin/src/components/BacktestsPanel.tsx index b534b83..6d9430e 100644 --- a/web/admin/src/components/BacktestsPanel.tsx +++ b/web/admin/src/components/BacktestsPanel.tsx @@ -4,7 +4,7 @@ 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[]}} +type Run={id:string;symbol: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}){ @@ -16,5 +16,5 @@ export function BacktestsPanel({onSessionExpired}:{onSessionExpired:()=>void}){ 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=>)}
: