feat: 完成模拟交易闭环、App 与 A 股行情接入
Checks / verify (push) Failing after 9m52s

统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
2026-10-08 22:58:52 +08:00
parent bffdb3858b
commit 92329ddce2
145 changed files with 18593 additions and 587 deletions
+23
View File
@@ -0,0 +1,23 @@
import {useRef,useState} from "react"
import {Text,View} from "react-native"
import {useLocalSearchParams,useRouter} from "expo-router"
import {useQueryClient} from "@tanstack/react-query"
import * as Crypto from "expo-crypto"
import {useSession} from "../../src/state/session"
import {useAccounts} from "../../src/state/accounts"
import {Body,Button,ErrorMessage,Field,Panel,Screen,Title,styles} from "../../src/components/ui"
import type {Fill} from "../../src/lib/fills"
export default function RecordFill(){
const params=useLocalSearchParams<{plan_id?:string;symbol?:string;exchange?:string;name?:string;side?:string}>(),router=useRouter(),cache=useQueryClient(),{client}=useSession(),{selected:account}=useAccounts()
const [side,setSide]=useState(params.side==="SELL"?"SELL":"BUY"),[symbol,setSymbol]=useState(params.symbol??""),[exchange,setExchange]=useState(params.exchange??"SSE"),[name,setName]=useState(params.name??"")
const [quantity,setQuantity]=useState(""),[price,setPrice]=useState(""),[fee,setFee]=useState(""),[occurred,setOccurred]=useState(()=>new Date().toISOString()),[sellable,setSellable]=useState(""),[note,setNote]=useState("")
const [review,setReview]=useState(false),[busy,setBusy]=useState(false),[error,setError]=useState("")
const pending=useRef<{account_id:string;idempotency_key:string;[key:string]:unknown}|null>(null)
async function save(){if(!account)return;setBusy(true);setError("");try{
if(!pending.current)pending.current={account_id:account.id,plan_id:side==="BUY"&&params.plan_id?params.plan_id:null,idempotency_key:Crypto.randomUUID(),side,symbol,exchange,name,quantity,price,fee,occurred_at:new Date(occurred).toISOString(),sellable_at:side==="BUY"?new Date(sellable).toISOString():null,note}
await client<Fill>("/api/v1/fills",{method:"POST",body:pending.current});await Promise.all(["portfolio","fills","accounts","budget","plans"].map(key=>cache.invalidateQueries({queryKey:[key]})));router.replace("/(tabs)/portfolio")
}catch(e){setError(e instanceof Error?e.message:"录入失败,请重试。")}finally{setBusy(false)}}
async function check(){if(!pending.current)return;setBusy(true);setError("");try{const fills=await client<Fill[]>(`/api/v1/accounts/${pending.current.account_id}/fills`);if(fills.some(f=>f.idempotency_key===pending.current!.idempotency_key)){await cache.invalidateQueries();router.replace("/(tabs)/portfolio")}else{pending.current=null;setReview(false)}}catch(e){setError(e instanceof Error?e.message:"暂时无法核对,请重试。")}finally{setBusy(false)}}
const disabled=busy||!!pending.current
return <Screen><Title>记录模拟成交</Title><Panel accent><Body>只记录已发生的模拟成交,不提交券商订单。当前使用手工设定的模拟可卖时间,不自动推断交易日或真实 T+1。</Body></Panel><Body>当前账户:{account?.name??"未选择"}。成交录入按发生时间顺序;纠错采用冲正,原记录保留。</Body>{account?.mode!=="simulation"&&<ErrorMessage message="请先选择模拟账户;真实记录账本尚未接通。"/>}<View style={styles.row}><Button secondary={side!=="BUY"} title="买入" disabled={disabled||!!params.plan_id} onPress={()=>{setSide("BUY");setReview(false)}}/><Button secondary={side!=="SELL"} title="卖出" disabled={disabled||!!params.plan_id} onPress={()=>{setSide("SELL");setReview(false)}}/></View><View style={styles.row}><Button secondary={exchange!=="SSE"} title="上交所" disabled={disabled||!!params.plan_id} onPress={()=>{setExchange("SSE");setReview(false)}}/><Button secondary={exchange!=="SZSE"} title="深交所" disabled={disabled||!!params.plan_id} onPress={()=>{setExchange("SZSE");setReview(false)}}/></View>{([["证券代码",symbol,setSymbol],["证券名称",name,setName],["成交数量",quantity,setQuantity],["成交价格(元)",price,setPrice],["实际费用(元,允许 0)",fee,setFee],["成交时间(ISO 格式,含时区)",occurred,setOccurred]] as const).map(([label,value,set])=><Field key={label} label={label} value={value} editable={!disabled} autoCapitalize="none" onChangeText={v=>{set(v);setReview(false)}}/>)}{side==="BUY"&&<Field label="模拟可卖时间(ISO 格式,含时区)" value={sellable} editable={!disabled} autoCapitalize="none" onChangeText={v=>{setSellable(v);setReview(false)}}/>}<Field label="备注 / 偏离计划说明" value={note} editable={!disabled} multiline onChangeText={v=>{setNote(v);setReview(false)}}/><ErrorMessage message={error}/>{!review&&<Button title="核对成交记录" disabled={account?.mode!=="simulation"||!quantity||!price||!fee||!symbol||!name||side==="BUY"&&!sellable} onPress={()=>{setReview(true);setError("")}}/>}{review&&<Panel><Text style={styles.subtitle}>确认入账:{side==="BUY"?"买入":"卖出"} {name}</Text><Body>{exchange}:{symbol} · {quantity} 股 · ¥{price} / 股 · 费用 ¥{fee}</Body><Body>记录后会同步现金与持仓。买入使用现金并建立批次;卖出按 FIFO 分摊成本。已失效计划的实际成交仍可录入,需填写说明。</Body><Button title={pending.current?"重试同一笔记录":"确认成交入账"} busy={busy} onPress={()=>void save()}/>{pending.current?<Button secondary title="刷新账本核对提交结果" disabled={busy} onPress={()=>void check()}/>:<Button secondary title="返回修改" disabled={busy} onPress={()=>setReview(false)}/>}</Panel>}</Screen>
}