diff --git a/api/openapi.yaml b/api/openapi.yaml index 5916b55..3dafb58 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Veyra API - version: 0.12.0 + version: 0.15.0 description: 开发身份、账户、供应商配置、候选、计划、成交账本、站内通知、研究策略、Python 回测与公开 A 股开发行情。模拟工作流仅限 local/test;正式行情授权、完整交易规则及远程推送未完成。 servers: - url: http://127.0.0.1:8080 @@ -1538,6 +1538,91 @@ paths: $ref: "#/components/responses/Error" "503": $ref: "#/components/responses/Error" + /api/admin/v1/strategy-screenings: + get: + tags: + - Admin + summary: 最近自动日线筛选记录 + description: 管理员专用。只记录已完成筛选;不满足数据时效或来源失败时不生成候选。SIGNAL 提交到待审核,不自动发布。 + security: + - adminCookie: [] + responses: + "200": &a3 + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/strategy-screenings/{id}: + get: + tags: + - Admin + summary: 查看自动筛选不可覆盖的参数与日线输入快照 + description: 管理员专用。只记录已完成筛选;不满足数据时效或来源失败时不生成候选。SIGNAL 提交到待审核,不自动发布。 + security: + - adminCookie: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": *a3 + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/strategy-screening-status: + get: + operationId: getScreeningWorkerState + summary: 当前进程的最近一批筛选状态 + description: 状态与数量仅对应当前进程最近一批;持久化结果使用 strategy-screenings。 + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: &a5 + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": &a4 + $ref: "#/components/responses/Error" + "403": *a4 + "503": *a4 + /api/admin/v1/strategy-screenings/run: + post: + operationId: requestStrategyScreening + summary: 请求立即自动筛选 + description: 同源管理员请求,异步执行。运行中或已排队时复用现有请求,不另起 Worker。每批最多 20 只,固定范围最大 100 只;不自动发布候选。 + security: + - adminCookie: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EmptyObject" + responses: + "202": + description: 已排队或已有正在执行的任务 + content: *a5 + "401": *a4 + "403": *a4 + "422": *a4 + "503": *a4 components: securitySchemes: adminCookie: @@ -2219,7 +2304,7 @@ components: code: *a2 name: *a2 description: *a2 - expected_version: &a3 + expected_version: &a6 type: integer parameters: type: object @@ -2230,11 +2315,13 @@ components: - slippage_bps - lot_size properties: - short_window: *a3 - long_window: *a3 - fee_bps: *a3 - slippage_bps: *a3 - lot_size: *a3 + short_window: *a6 + long_window: *a6 + fee_bps: *a6 + slippage_bps: *a6 + lot_size: *a6 + screening: + $ref: "#/components/schemas/ScreeningParameters" StrategyDeployment: type: object required: @@ -2248,7 +2335,7 @@ components: enum: - ACTIVE - PAUSED - expected_version: *a3 + expected_version: *a6 reason: *a2 HistoricalBar: type: object @@ -2318,3 +2405,51 @@ components: code: *a2 message: *a2 request_id: *a2 + ScreeningParameters: + type: object + additionalProperties: false + required: + - securities + - entry_band_bps + - stop_loss_bps + - target_bps + - valid_hours + properties: + securities: + type: array + minItems: 1 + maxItems: 100 + uniqueItems: true + items: + type: object + additionalProperties: false + required: + - symbol + - exchange + properties: + symbol: + type: string + pattern: ^[0-9]{6}$ + exchange: + type: string + enum: + - SSE + - SZSE + entry_band_bps: + type: integer + minimum: 0 + maximum: 1000 + stop_loss_bps: + type: integer + minimum: 1 + maximum: 5000 + description: 必须大于 entry_band_bps + target_bps: + type: integer + minimum: 1 + maximum: 10000 + description: 必须大于 entry_band_bps + valid_hours: + type: integer + minimum: 1 + maximum: 168 diff --git a/apps/mobile/app/(tabs)/opportunities.tsx b/apps/mobile/app/(tabs)/opportunities.tsx index 3e36061..50f021d 100644 --- a/apps/mobile/app/(tabs)/opportunities.tsx +++ b/apps/mobile/app/(tabs)/opportunities.tsx @@ -7,5 +7,5 @@ import type {Candidate} from "../../src/lib/candidates" export default function Opportunities(){ const {session,client}=useSession(),router=useRouter() const query=useQuery({queryKey:["candidates",session?.user.id],enabled:!!session,queryFn:({signal})=>client("/api/v1/candidates",{signal}),refetchInterval:30000}) - return 机会从证据、反证与失效条件理解一个候选。模拟研究候选 · 由管理端手工录入并审核发布,当前未接入自动行情筛选。)} -
开发环境
模拟业务流程已接通
+ navigate("总览")} className="flex items-center gap-3 px-2 text-foreground no-underline">V
见势 · Veyra

A-SHARE RESEARCH

+

股票研究与审核

+ +
开发环境
公开行情 · 模拟业务
自动筛选与人工审核
工作空间 {page}
{user.display_name}LOCAL · 开发环境
-
+
{logoutError &&

{logoutError}

} -

VEYRA / {overview ? "SYSTEM OVERVIEW" : "WORKSPACE"}

{page === "总览" ? "把每一步,建立在依据之上。" : page}

{overview ? "查看服务连接与建设进度,从可靠的基础开始。" : page === "模型配置" || page === "系统设置" ? "配置供应商连接信息,按需启用。" : page === "审计日志" ? "查看后台配置的操作记录。" : page === "通知任务" ? "查看持久化事件与站内通知处理状态。" : page === "候选审核" ? "录入证据、审核修订并发布模拟候选。" : page === "策略中心" ? "管理研究参数与版本历史。" : page === "回测研究" ? "提交历史数据,查看可追溯的 Python 回测结果。" : "该工作区的业务功能正在建设中。"}

{overview && }
+

VEYRA / {overview ? "SYSTEM OVERVIEW" : "WORKSPACE"}

{page === "总览" ? "A 股研究总览" : page}

{page === "总览" ? "从真实股票行情,到策略信号与候选审核。" : page === "股票行情" ? "搜索沪深股票,查看报价、数据时间与真实日线走势。" : overview ? "检查行情链路、存储与任务依赖。" : page === "模型配置" || page === "系统设置" ? "配置供应商连接信息,按需启用。" : page === "审计日志" ? "查看后台配置的操作记录。" : page === "通知任务" ? "查看持久化事件与站内通知处理状态。" : page === "候选审核" ? "审核自动生成的股票信号、证据与参考价格,发布到 App。" : page === "策略中心" ? "管理研究参数与版本历史。" : page === "回测研究" ? "提交历史数据,查看可追溯的 Python 回测结果。" : "该工作区的业务功能正在建设中。"}

{overview && }
{overview ? <> {error &&
无法读取当前服务状态。请检查 API 是否启动。{error}
}
{[ @@ -61,9 +65,8 @@ export default function App({user,onLogout,onSessionExpired}:{user:User;onLogout {title:"PostgreSQL",subtitle:"业务事实与版本化迁移",icon:Database,value:loading ? "检查中" : health ? label[health.components.postgresql.status] : "未知"}, {title:"Redis",subtitle:"缓存与订阅索引",icon:Activity,value:loading ? "检查中" : health ? label[health.components.redis.status] : "未知"}, ].map(({title,subtitle,icon:Icon,value})=>
{title}

{value}

{subtitle}

)}
-
第一条产品闭环模拟流程已接通
先验证完整流程,再扩大策略与数据范围。
    {["管理端审核并发布观察候选","用户查看证据,确认买入计划","条件满足后发送提醒","记录实际成交,建立持仓监控","记录退出成交,生成成本后复盘"].map((v,i)=>
  1. 0{i+1}{v}已接通模拟流程
  2. )}
数据接入后,开始观察。候选数量、策略表现与账户指标将在业务功能与数据接入完成后展示。
当前状态来源:Go API 实时探测
最近检查:{health ? new Date(health.checked_at).toLocaleString("zh-CN") : "尚未取得状态"}
- {page==="数据健康"&&<>} : page==="候选审核" ? : page==="回测研究" ? : page==="策略中心" ? : page==="通知任务" ? : page==="用户管理" ? : page==="模型配置" ? : page==="系统设置" ? : page==="审计日志" ? :

{page}尚未开放

该模块完成业务接口、权限与验证后开放。

} -
见势 · Veyra / 可追溯的投资决策模拟业务闭环 v0.8
+
开发验证:模拟价格回放
: page==="总览" ? : page==="股票行情" ? : page==="候选审核" ? : page==="回测研究" ? : page==="策略中心" ? : page==="通知任务" ? : page==="用户管理" ? : page==="模型配置" ? : page==="系统设置" ? : page==="审计日志" ? :

{page}尚未开放

该模块完成业务接口、权限与验证后开放。

} +
见势 · Veyra / 可追溯的投资决策A 股研究工作台 · 开发版
diff --git a/web/admin/src/components/CandidatesPanel.tsx b/web/admin/src/components/CandidatesPanel.tsx index d119925..c27aa26 100644 --- a/web/admin/src/components/CandidatesPanel.tsx +++ b/web/admin/src/components/CandidatesPanel.tsx @@ -1,4 +1,6 @@ import {useEffect,useRef,useState,type FormEvent} from "react" +import {ScreeningRunControl} from "./ScreeningRunControl" +import {StockHistoryChart} from "./StockHistoryChart" import {Plus,RefreshCw} from "lucide-react" import {Button} from "./ui/button" import {Input} from "./ui/input" @@ -10,7 +12,7 @@ const control="w-full rounded-md border bg-background px-3 py-2 text-sm focus:ou const actions:Record={submit:"提交审核",publish:"审核发布",return:"退回修订",withdraw:"撤回发布"} const localTime=(date:Date)=>new Date(date.getTime()-date.getTimezoneOffset()*60000).toISOString().slice(0,16) function blank():CandidateContent{return {symbol:"",exchange:"SSE",name:"",strategy_code:"trend_following",title:"",thesis:"",evidence:[{source_url:"",locator:"",summary:""}],counter_evidence:"",invalidation:"",reference_plan:{entry_low:"",entry_high:"",stop_loss:"",target_price:""}}} -export function CandidatesPanel({onSessionExpired}:{onSessionExpired:()=>void}){ +export function CandidatesPanel({onSessionExpired,initialCandidateID}:{onSessionExpired:()=>void;initialCandidateID?:string}){ const [list,setList]=useState([]),[selected,setSelected]=useState(null),[status,setStatus]=useState("") const [error,setError]=useState(""),[busy,setBusy]=useState(false),[loading,setLoading]=useState(false),[editing,setEditing]=useState(false),[source,setSource]=useState() const [content,setContent]=useState(blank),[observed,setObserved]=useState(()=>localTime(new Date())),[until,setUntil]=useState(()=>localTime(new Date(Date.now()+86400000))) @@ -19,20 +21,22 @@ export function CandidatesPanel({onSessionExpired}:{onSessionExpired:()=>void}){ function failed(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"操作失败,请重试。")} async function refresh(){controller.current?.abort();const next=new AbortController();controller.current=next;setLoading(true);try{const result=await getCandidates(next.signal,status);if(!next.signal.aborted){setList(result);setError("")}}catch(e){if(!next.signal.aborted)failed(e)}finally{if(!next.signal.aborted)setLoading(false)}} useEffect(()=>{void refresh();return()=>{controller.current?.abort();detailController.current?.abort()}},[status]) + useEffect(()=>{if(!initialCandidateID)return;const c=new AbortController();getCandidate(initialCandidateID,c.signal).then(result=>{if(!c.signal.aborted)setSelected(result)}).catch(e=>{if(!c.signal.aborted)failed(e)});return()=>c.abort()},[initialCandidateID]) async function select(c:Candidate){detailController.current?.abort();const next=new AbortController();detailController.current=next;setSelected(null);setAction("");setEditing(false);try{const detail=await getCandidate(c.id,next.signal);if(!next.signal.aborted)setSelected(detail)}catch(e){if(!next.signal.aborted)failed(e)}} function edit(c?:Candidate){setSource(c);setContent(c?structuredClone(c.content):blank());setObserved(localTime(new Date()));setUntil(localTime(new Date(Date.now()+86400000)));setEditing(true);setAction("");setError("")} async function save(e:FormEvent){e.preventDefault();setBusy(true);setError("");try{const result=await saveDraft({content,observed_at:new Date(observed).toISOString(),valid_until:new Date(until).toISOString()},source);setEditing(false);await refresh();await select(result)}catch(e){failed(e)}finally{setBusy(false)}} async function confirm(e:FormEvent){e.preventDefault();if(!selected)return;setBusy(true);setError("");try{const result=await actOnCandidate(selected,action,reason);setAction("");setReason("");await refresh();await select(result)}catch(e){failed(e)}finally{setBusy(false)}} const set=(key:keyof CandidateContent,value:string)=>setContent(c=>({...c,[key]:value})) return
-
模拟候选

手工录入来源,保存修订后再提交审核。

+ void refresh()}/> +
模拟候选

自动筛选形成队列,审核股票依据后发布。

{error&&

{error}

} - {editing&&{source?`创建第 ${source.revision+1} 次修订` : "录入观察候选"}原修订保留。证据、反证与参考价格需完整填写;当前不会自动获取或核验行情。
void save(e)} className="space-y-5">
{([["证券代码","symbol"],["证券名称","name"],["候选标题","title"]] as const).map(([label,key])=>)}
+ {editing&&{source?`创建第 ${source.revision+1} 次修订` : "录入观察候选"}原修订保留。证据、反证与参考价格需完整填写;此表单为手工录入;自动筛选结果从策略中心进入待审核。void save(e)} className="space-y-5">
{([["证券代码","symbol"],["证券名称","name"],["候选标题","title"]] as const).map(([label,key])=>)}
{([["观察逻辑","thesis"],["反证与风险","counter_evidence"],["失效条件","invalidation"]] as const).map(([label,key])=>