Files
Blizzard 92329ddce2
Checks / verify (push) Failing after 9m52s
feat: 完成模拟交易闭环、App 与 A 股行情接入
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
2026-10-08 22:58:52 +08:00

24 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
}