统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import {Tabs} from "expo-router"
|
||||
import Ionicons from "@expo/vector-icons/Ionicons"
|
||||
import {colors} from "../../src/components/ui"
|
||||
export default function TabLayout(){
|
||||
return <Tabs screenOptions={{headerShown:false,tabBarActiveTintColor:colors.accent,tabBarInactiveTintColor:colors.muted,tabBarStyle:{backgroundColor:colors.background,borderTopColor:colors.border},tabBarLabelStyle:{fontSize:11},tabBarItemStyle:{paddingTop:5}}}>
|
||||
<Tabs.Screen name="index" options={{title:"首页",tabBarIcon:({color,size})=><Ionicons name="home-outline" color={color} size={size}/>}}/>
|
||||
<Tabs.Screen name="opportunities" options={{title:"机会",tabBarIcon:({color,size})=><Ionicons name="compass-outline" color={color} size={size}/>}}/>
|
||||
<Tabs.Screen name="plans" options={{title:"计划",tabBarIcon:({color,size})=><Ionicons name="list-outline" color={color} size={size}/>}}/>
|
||||
<Tabs.Screen name="portfolio" options={{title:"持仓",tabBarIcon:({color,size})=><Ionicons name="pie-chart-outline" color={color} size={size}/>}}/>
|
||||
<Tabs.Screen name="profile" options={{title:"我的",tabBarIcon:({color,size})=><Ionicons name="person-outline" color={color} size={size}/>}}/>
|
||||
</Tabs>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {router} from "expo-router"
|
||||
import {Text,View} from "react-native"
|
||||
import {AccountSummary} from "../../src/components/AccountSummary"
|
||||
import {Body,Button,Panel,Screen,Title,styles} from "../../src/components/ui"
|
||||
import {useSession} from "../../src/state/session"
|
||||
export default function Home(){const {session}=useSession();return <Screen><View style={styles.row}><Title>见势 <Text style={{fontSize:18,fontWeight:"400"}}>· Veyra</Text></Title><Button title="提醒" secondary onPress={()=>router.push("/notifications")}/></View><Body>{session?.user.display_name},让判断建立在依据之上。</Body><Panel accent><Text style={styles.badge}>AI 观察</Text><Text style={styles.subtitle}>先理解,再行动。</Text><Body>研究与行情服务尚未接入,当前没有可供展示的在线分析。供应商由管理员在后台配置。</Body><Button title="发现机会" onPress={()=>router.push("/(tabs)/opportunities")}/></Panel><AccountSummary/><View style={styles.divider}/><Text style={styles.subtitle}>每一步,都可追溯</Text><Body>观察候选 → 查看证据 → 制订计划 → 条件提醒 → 记录成交 → 复盘。当前已开放模拟候选、计划预算、站内提醒与成交账本;自动行情和 AI 分析仍待接入。</Body></Screen>}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {Text,View} from "react-native"
|
||||
import {useQuery} from "@tanstack/react-query"
|
||||
import {useRouter} from "expo-router"
|
||||
import {useSession} from "../../src/state/session"
|
||||
import {Body,Button,EmptyState,ErrorMessage,Panel,Screen,Title,styles} from "../../src/components/ui"
|
||||
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<Candidate[]>("/api/v1/candidates",{signal}),refetchInterval:30000})
|
||||
return <Screen><Title>机会</Title><Body>从证据、反证与失效条件理解一个候选。</Body><Panel accent><Body>模拟研究候选 · 由管理端手工录入并审核发布,当前未接入自动行情筛选。</Body></Panel><Button secondary title="查看自动同步的 A 股行情" onPress={()=>router.push("/market")}/><Button secondary title={query.isFetching?"刷新中…":"刷新候选"} disabled={query.isFetching} onPress={()=>void query.refetch()}/>{query.isPending&&<Body>正在读取已发布候选…</Body>}{query.error&&<ErrorMessage message={query.error.message}/>} {query.data?.length===0&&<EmptyState title="当前没有有效候选" detail="审核发布且未过期的模拟候选会显示在这里。"/>}{query.data?.map(c=><Panel key={c.id}><View style={styles.row}><Text style={styles.subtitle}>{c.content.name}</Text><Text style={styles.badge}>模拟候选</Text></View><Body>{c.content.exchange}:{c.content.symbol} · 修订 {c.revision}</Body><Text style={styles.subtitle}>{c.content.title}</Text><Body>{c.content.thesis}</Body><Body>有效截止 {new Date(c.valid_until).toLocaleString("zh-CN")}</Body><Button title="查看证据与风险" onPress={()=>router.push({pathname:"/candidates/[id]",params:{id:c.id}})}/></Panel>)}</Screen>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {useState} from "react"
|
||||
import {Text} from "react-native"
|
||||
import {useQuery,useQueryClient} from "@tanstack/react-query"
|
||||
import {useRouter} from "expo-router"
|
||||
import {useSession} from "../../src/state/session"
|
||||
import {useAccounts} from "../../src/state/accounts"
|
||||
import {Body,Button,EmptyState,ErrorMessage,Panel,Screen,Title,styles} from "../../src/components/ui"
|
||||
import {planStates,type Plan} from "../../src/lib/plans"
|
||||
export default function Plans(){
|
||||
const {session,client}=useSession(),{selected}=useAccounts(),router=useRouter(),cache=useQueryClient()
|
||||
const [confirming,setConfirming]=useState<Plan|null>(null),[busy,setBusy]=useState(false),[error,setError]=useState("")
|
||||
const query=useQuery({queryKey:["plans",session?.user.id,selected?.id],enabled:!!session,queryFn:({signal})=>client<Plan[]>("/api/v1/plans",{signal}),refetchInterval:15000})
|
||||
const plans=query.data?.filter(p=>p.account_id===selected?.id)
|
||||
async function cancel(){if(!confirming)return;setBusy(true);setError("");try{await client(`/api/v1/plans/${confirming.id}/cancel`,{method:"POST",body:{expected_version:confirming.version}});setConfirming(null);await query.refetch();await cache.invalidateQueries({queryKey:["budget"]})}catch(e){setError(e instanceof Error?e.message:"取消失败")}finally{setBusy(false)}}
|
||||
return <Screen><Title>计划</Title><Body>{selected?`当前账户:${selected.name}`:"先选择账户,再从候选创建计划。"}计划保留预算,成交需另行记录。</Body><Button secondary title={query.isFetching?"刷新中…":"刷新计划"} disabled={query.isFetching||busy} onPress={()=>void query.refetch()}/>{query.error&&<ErrorMessage message={query.error.message}/>}<ErrorMessage message={error}/>{plans?.length===0&&<><EmptyState title="暂无交易计划" detail="从已发布候选查看依据,再预览数量与资金预算。"/><Button title="查看候选" onPress={()=>router.push("/(tabs)/opportunities")}/></>}{plans?.map(p=><Panel key={p.id}><Text style={styles.badge}>{planStates[p.status]??p.status} · 模拟计划</Text><Text style={styles.subtitle}>{p.candidate_content.name} · {p.candidate_content.symbol}</Text><Body>数量 {p.quantity} 股 · 已记录 {p.filled_quantity} 股</Body><Body>保留预算 ¥{p.reserved_amount} · 有效截止 {new Date(p.valid_until).toLocaleString("zh-CN")}</Body>{p.status!=="COMPLETED"&&p.status!=="CANCELLED"&&<Button title="记录此计划的实际模拟买入" onPress={()=>router.push({pathname:"/fills/create",params:{plan_id:p.id,symbol:p.candidate_content.symbol,exchange:p.candidate_content.exchange,name:p.candidate_content.name}})}/>}<Button secondary title="查看原始候选依据" onPress={()=>router.push({pathname:"/candidates/[id]",params:{id:p.candidate_revision_id}})}/>{["ACTIVE","PART_FILLED"].includes(p.status)&&<Button secondary title="取消计划" disabled={busy} onPress={()=>{setConfirming(p);setError("")}}/>}{confirming?.id===p.id&&<><Body>确认取消 {p.candidate_content.name} 的计划?保留额度将释放;已记录成交保留。</Body><Button title="确认取消" busy={busy} onPress={()=>void cancel()}/><Button secondary title="保留计划" disabled={busy} onPress={()=>setConfirming(null)}/></>}</Panel>)}</Screen>
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {useState} from "react"
|
||||
import {Text} from "react-native"
|
||||
import {useQuery,useQueryClient} from "@tanstack/react-query"
|
||||
import {useRouter} from "expo-router"
|
||||
import {useSession} from "../../src/state/session"
|
||||
import {useAccounts} from "../../src/state/accounts"
|
||||
import {Body,Button,EmptyState,ErrorMessage,Field,Panel,Screen,Title,styles} from "../../src/components/ui"
|
||||
import type {Fill,Portfolio} from "../../src/lib/fills"
|
||||
export default function PortfolioScreen(){
|
||||
const {session,client}=useSession(),{selected:account}=useAccounts(),router=useRouter(),cache=useQueryClient(),enabled=!!session&&account?.mode==="simulation"
|
||||
const [reversing,setReversing]=useState<Fill|null>(null),[reason,setReason]=useState(""),[busy,setBusy]=useState(false),[error,setError]=useState("")
|
||||
const portfolio=useQuery({queryKey:["portfolio",session?.user.id,account?.id],enabled,queryFn:({signal})=>client<Portfolio>(`/api/v1/accounts/${account!.id}/portfolio`,{signal})})
|
||||
const fills=useQuery({queryKey:["fills",session?.user.id,account?.id],enabled,queryFn:({signal})=>client<Fill[]>(`/api/v1/accounts/${account!.id}/fills`,{signal})})
|
||||
const latest=fills.data?.find(f=>!f.reversed)
|
||||
async function reverse(){if(!reversing)return;setBusy(true);setError("");try{await client(`/api/v1/fills/${reversing.id}/reverse`,{method:"POST",body:{reason}});setReversing(null);await Promise.all(["portfolio","fills","accounts","budget","plans"].map(key=>cache.invalidateQueries({queryKey:[key]})))}catch(e){setError(e instanceof Error?e.message:"冲正失败")}finally{setBusy(false)}}
|
||||
return <Screen><Title>持仓与账本</Title><Body>当前账户:{account?.name??"未选择"}。现金与已实现盈亏来自记录的成交;行情未接入,市值和浮盈亏未知。</Body>{!account&&<Button title="选择账户" onPress={()=>router.push("/settings/accounts")}/>} {account?.mode==="manual_real"&&<EmptyState title="真实记录账本尚未接通" detail="当前成交与持仓闭环仅用于模拟账户,不展示预设资产或盈亏。"/>}{enabled&&<><Button title="记录模拟成交" onPress={()=>router.push("/fills/create")}/><Button secondary title="刷新账本" disabled={portfolio.isFetching||fills.isFetching||busy} onPress={()=>{void portfolio.refetch();void fills.refetch()}}/>{portfolio.error&&<ErrorMessage message={portfolio.error.message}/>} {portfolio.data&&<><Panel accent><Body>账本现金</Body><Text style={styles.money}>¥{portfolio.data.cash_balance}</Text><Body>累计已实现盈亏(含录入费用) ¥{portfolio.data.realized_pnl}</Body></Panel><Panel><Text style={styles.subtitle}>账本复盘 · 累计</Text><Body>未冲正成交 {portfolio.data.review.trade_count} 笔 · 卖出 {portfolio.data.review.sell_count} 笔</Body><Body>买入现金支出 ¥{portfolio.data.review.buy_outflow} · 卖出现金收入 ¥{portfolio.data.review.sell_inflow}</Body><Body>录入费用合计 ¥{portfolio.data.review.total_fees}</Body><Body>盈利卖出 {portfolio.data.review.profitable_sell_count} 笔 · 亏损卖出 {portfolio.data.review.losing_sell_count} 笔</Body><Body>按卖出记录统计,包含已录入费用;不等于独立策略胜率,也不包含未实现盈亏或尚未录入的费用。</Body></Panel>{portfolio.data.positions.length===0&&<EmptyState title="当前没有记录的持仓" detail="记录买入后建立持仓批次,卖出与冲正同步更新。"/>}{portfolio.data.positions.map(p=><Panel key={p.exchange+p.symbol}><Text style={styles.subtitle}>{p.name} · {p.symbol}</Text><Body>持仓 {p.quantity} 股 · 按模拟可卖时间可卖 {p.sellable_quantity} 股</Body><Body>剩余成本 ¥{p.cost_basis} · 市值与浮盈亏未知</Body><Button secondary title="记录卖出成交" disabled={p.sellable_quantity==="0"} onPress={()=>router.push({pathname:"/fills/create",params:{side:"SELL",symbol:p.symbol,exchange:p.exchange,name:p.name}})}/></Panel>)}</>}<Text style={styles.subtitle}>成交记录</Text><Body>最近 100 笔。仅允许冲正账户最新一笔未冲正成交,避免破坏后续成本分摊;冲正不自动恢复原计划。</Body>{fills.error&&<ErrorMessage message={fills.error.message}/>}<ErrorMessage message={error}/>{fills.data?.map(f=><Panel key={f.id}><Text style={styles.badge}>{f.reversed?"已冲正":f.side==="BUY"?"买入":"卖出"}</Text><Text style={styles.subtitle}>{f.name} · {f.symbol}</Text><Body>{f.quantity} 股 · ¥{f.price} / 股 · 费用 ¥{f.fee}</Body><Body>现金变动 ¥{f.cash_delta}{f.side==="SELL"?` · 已实现盈亏 ¥${f.realized_pnl}`:""}</Body><Body>{new Date(f.occurred_at).toLocaleString("zh-CN")}{f.note?` · ${f.note}`:""}</Body>{latest?.id===f.id&&!f.reversed&&<Button secondary title="纠错:冲正此记录" disabled={busy} onPress={()=>{setReversing(f);setReason("");setError("")}}/>}{reversing?.id===f.id&&<><Body>确认冲正此笔 {f.name} {f.quantity} 股成交?现金和持仓将反向恢复,原成交记录保留。</Body><Field label="冲正原因" value={reason} editable={!busy} onChangeText={setReason} multiline/><Button title="确认冲正" disabled={!reason.trim()} busy={busy} onPress={()=>void reverse()}/><Button secondary title="取消" disabled={busy} onPress={()=>setReversing(null)}/></>}</Panel>)}</>}</Screen>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {useState} from "react"
|
||||
import {router} from "expo-router"
|
||||
import {Text,View} from "react-native"
|
||||
import {useSession} from "../../src/state/session"
|
||||
import {Body,Button,ErrorMessage,Panel,Screen,Title,styles} from "../../src/components/ui"
|
||||
export default function Profile(){const {session,logout}=useSession();const [busy,setBusy]=useState(false);const [error,setError]=useState("");async function signOut(){setBusy(true);setError("");try{await logout()}catch(e){setError(e instanceof Error?e.message:"退出失败,请重试。")}finally{setBusy(false)}}return <Screen><Title>我的</Title><Panel><View style={styles.row}><Text style={styles.subtitle}>{session?.user.display_name}</Text><Text style={styles.badge}>开发测试身份</Text></View><Body>会话有效期至 {session?new Date(session.expires_at).toLocaleString("zh-CN"):"—"}</Body></Panel><Button title="账户管理与切换" secondary onPress={()=>router.push("/settings/accounts")}/><Button title="提醒中心" secondary onPress={()=>router.push("/notifications")}/><View style={styles.divider}/><Text style={styles.subtitle}>关于见势</Text><Body>见势 · Veyra 帮助理解依据与记录决策。投资偏好、通知设置和复盘功能将随对应服务接入开放。</Body><ErrorMessage message={error}/><Button title="退出登录" busy={busy} onPress={()=>void signOut()}/></Screen>}
|
||||
Reference in New Issue
Block a user