11import { useEffect , useState , useCallback } from 'react'
2- import { RefreshCw , Power , RotateCcw , X , TrendingUp , TrendingDown , Trophy , BarChart3 , Wallet , Activity , Play , Bell } from 'lucide-react'
2+ import { RefreshCw , Power , RotateCcw , X , TrendingUp , TrendingDown , Trophy , BarChart3 , Wallet , Activity , Play , Bell , SlidersHorizontal } from 'lucide-react'
33import {
44 paperTradingApi ,
55 type PaperTradingAccountResponse ,
88 type EquityCurvePoint ,
99 type StrategyPerformanceItem ,
1010 type NotifyChannelItem ,
11+ type MarketView ,
1112} from '@panwatch/api'
1213import { Button } from '@panwatch/base-ui/components/ui/button'
1314import { Switch } from '@panwatch/base-ui/components/ui/switch'
@@ -115,6 +116,15 @@ export default function PaperTradingPage() {
115116 const [ tradesPage , setTradesPage ] = useState ( 0 )
116117 const tradesPageSize = 20
117118
119+ // 市场视图(分段单选,切换即按该市场口径刷新统计)
120+ const [ marketView , setMarketView ] = useState < MarketView > ( 'ALL' )
121+
122+ // 资金配置
123+ const [ configOpen , setConfigOpen ] = useState ( false )
124+ const [ cfgTotal , setCfgTotal ] = useState ( '' )
125+ const [ cfgRatios , setCfgRatios ] = useState < { CN : string ; HK : string ; US : string } > ( { CN : '' , HK : '' , US : '' } )
126+ const [ cfgSaving , setCfgSaving ] = useState ( false )
127+
118128 // 通知设置
119129 const [ tradesOpen , setTradesOpen ] = useState ( false )
120130 const [ notifyOpen , setNotifyOpen ] = useState ( false )
@@ -130,11 +140,12 @@ export default function PaperTradingPage() {
130140 const loadData = useCallback ( async ( ) => {
131141 setLoading ( true )
132142 try {
143+ const mkt = marketView === 'ALL' ? undefined : marketView
133144 const [ acc , pos , tradeData , metrics ] = await Promise . all ( [
134- paperTradingApi . getAccount ( ) ,
135- paperTradingApi . listPositions ( 'open' ) ,
136- paperTradingApi . listTrades ( tradesPageSize , tradesPage * tradesPageSize ) ,
137- paperTradingApi . getMetrics ( ) ,
145+ paperTradingApi . getAccount ( mkt ) ,
146+ paperTradingApi . listPositions ( 'open' , mkt ) ,
147+ paperTradingApi . listTrades ( tradesPageSize , tradesPage * tradesPageSize , mkt ) ,
148+ paperTradingApi . getMetrics ( mkt ) ,
138149 ] )
139150 setAccount ( acc )
140151 setPositions ( pos )
@@ -147,7 +158,7 @@ export default function PaperTradingPage() {
147158 } finally {
148159 setLoading ( false )
149160 }
150- } , [ tradesPage ] )
161+ } , [ tradesPage , marketView ] )
151162
152163 useEffect ( ( ) => { loadData ( ) } , [ loadData ] )
153164
@@ -196,6 +207,52 @@ export default function PaperTradingPage() {
196207 }
197208 }
198209
210+ const handleOpenConfig = async ( ) => {
211+ setConfigOpen ( true )
212+ try {
213+ // 以"全部"口径取总资金与各市场比例
214+ const acc = await paperTradingApi . getAccount ( )
215+ setCfgTotal ( String ( Math . round ( acc . initial_capital ) ) )
216+ const a = acc . market_allocations || { }
217+ setCfgRatios ( {
218+ CN : String ( Math . round ( ( a . CN ?? 0 ) * 100 ) ) ,
219+ HK : String ( Math . round ( ( a . HK ?? 0 ) * 100 ) ) ,
220+ US : String ( Math . round ( ( a . US ?? 0 ) * 100 ) ) ,
221+ } )
222+ } catch {
223+ toast ( '加载配置失败' , 'error' )
224+ }
225+ }
226+
227+ const handleSaveConfig = async ( ) => {
228+ const total = Number ( cfgTotal )
229+ const cn = Number ( cfgRatios . CN ) || 0
230+ const hk = Number ( cfgRatios . HK ) || 0
231+ const us = Number ( cfgRatios . US ) || 0
232+ if ( ! ( total > 0 ) ) {
233+ toast ( '总资金需大于 0' , 'error' )
234+ return
235+ }
236+ if ( cn + hk + us > 100 ) {
237+ toast ( '比例合计不能超过 100%' , 'error' )
238+ return
239+ }
240+ setCfgSaving ( true )
241+ try {
242+ await paperTradingApi . updateSettings ( {
243+ initial_capital : total ,
244+ market_allocations : { CN : cn / 100 , HK : hk / 100 , US : us / 100 } ,
245+ } )
246+ toast ( '资金配置已保存' , 'success' )
247+ setConfigOpen ( false )
248+ loadData ( )
249+ } catch {
250+ toast ( '保存失败' , 'error' )
251+ } finally {
252+ setCfgSaving ( false )
253+ }
254+ }
255+
199256 const loadNotifySettings = async ( ) => {
200257 try {
201258 const data = await paperTradingApi . getNotifySettings ( )
@@ -260,6 +317,7 @@ export default function PaperTradingPage() {
260317 }
261318
262319 const totalPages = Math . ceil ( tradesTotal / tradesPageSize )
320+ const ratioSum = ( Number ( cfgRatios . CN ) || 0 ) + ( Number ( cfgRatios . HK ) || 0 ) + ( Number ( cfgRatios . US ) || 0 )
263321
264322 return (
265323 < div className = "space-y-5" >
@@ -308,37 +366,37 @@ export default function PaperTradingPage() {
308366 </ div >
309367 </ div >
310368
311- { /* Market Filter */ }
369+ { /* Market View Filter + 资金配置 */ }
312370 { account && (
313- < div className = "flex items-center gap-2 text-sm " >
314- < span className = "text-muted-foreground text-xs" > 交易市场: </ span >
315- { ( [ 'CN' , 'HK' , 'US' ] as const ) . map ( market => {
316- const excluded = account . excluded_markets || [ ]
317- const isEnabled = ! excluded . includes ( market )
318- const label = market === 'CN' ? 'A股' : market === 'HK' ? '港股' : '美股'
319- return (
320- < button
321- key = { market }
322- onClick = { async ( ) => {
323- const current = account . excluded_markets || [ ]
324- const next = isEnabled
325- ? [ ... current , market ]
326- : current . filter ( ( m : string ) => m !== market )
327- try {
328- const res = await paperTradingApi . updateSettings ( { excluded_markets : next } )
329- setAccount ( res )
330- } catch { /* ignore */ }
331- } }
332- className = { `px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${
333- isEnabled
334- ? 'bg-primary/10 text-primary ring-1 ring-primary/20'
335- : 'bg-muted/50 text-muted-foreground line-through'
336- } ` }
337- >
338- { label }
339- </ button >
340- )
341- } ) }
371+ < div className = "flex items-center justify-between gap-2 " >
372+ < div className = "flex items-center gap-2 text-sm" >
373+ < span className = "text-muted-foreground text-xs" > 交易市场: </ span >
374+ { ( [ 'ALL' , 'CN' , 'HK' , 'US' ] as const ) . map ( m => {
375+ const label = m === 'ALL' ? '全部' : m === 'CN' ? 'A股' : m === 'HK' ? '港股' : '美股'
376+ const active = marketView === m
377+ const ratio = m !== 'ALL' ? account . market_allocations ?. [ m ] : undefined
378+ const isOff = m !== 'ALL' && ( ratio ?? 0 ) <= 0
379+ return (
380+ < button
381+ key = { m }
382+ onClick = { ( ) => setMarketView ( m ) }
383+ className = { `px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${
384+ active
385+ ? 'bg-primary text-primary-foreground'
386+ : isOff
387+ ? 'bg-muted/50 text-muted-foreground'
388+ : 'bg-primary/10 text-primary ring-1 ring-primary/20'
389+ } ` }
390+ >
391+ { label } { m !== 'ALL' && ratio != null ? ` ${ Math . round ( ratio * 100 ) } %` : '' }
392+ </ button >
393+ )
394+ } ) }
395+ </ div >
396+ < Button variant = "outline" size = "sm" className = "h-8" onClick = { handleOpenConfig } >
397+ < SlidersHorizontal className = "w-3.5 h-3.5" / >
398+ < span className = "hidden sm:inline ml-1" > 资金配置 </ span >
399+ </ Button >
342400 </ div >
343401 ) }
344402
@@ -566,6 +624,65 @@ export default function PaperTradingPage() {
566624 </ DialogContent >
567625 </ Dialog >
568626
627+ { /* 资金配置对话框 */ }
628+ < Dialog open = { configOpen } onOpenChange = { setConfigOpen } >
629+ < DialogContent >
630+ < DialogHeader >
631+ < DialogTitle > 资金配置</ DialogTitle >
632+ < DialogDescription > 设置总资金与各市场投资比例,比例为 0 则不投入该市场(已有持仓不受影响,仅停止新建仓)</ DialogDescription >
633+ </ DialogHeader >
634+
635+ < div className = "space-y-4" >
636+ < div >
637+ < div className = "text-sm font-medium mb-1" > 总资金</ div >
638+ < input
639+ type = "number"
640+ value = { cfgTotal }
641+ onChange = { e => setCfgTotal ( e . target . value ) }
642+ className = "w-full h-9 px-3 rounded-lg border border-border bg-background text-sm"
643+ placeholder = "如 1000000"
644+ />
645+ </ div >
646+
647+ < div className = "space-y-2" >
648+ < div className = "flex items-center justify-between text-sm font-medium" >
649+ < span > 各市场投资比例</ span >
650+ < span className = { `text-xs ${ ratioSum > 100 ? 'text-destructive' : 'text-muted-foreground' } ` } >
651+ 合计 { ratioSum } %{ ratioSum > 100 ? '(超过 100%)' : '' }
652+ </ span >
653+ </ div >
654+ { ( [ 'CN' , 'HK' , 'US' ] as const ) . map ( m => {
655+ const label = m === 'CN' ? 'A股' : m === 'HK' ? '港股' : '美股'
656+ const pct = Number ( cfgRatios [ m ] ) || 0
657+ const amount = ( ( Number ( cfgTotal ) || 0 ) * pct ) / 100
658+ return (
659+ < div key = { m } className = "flex items-center gap-3" >
660+ < span className = "w-12 text-sm" > { label } </ span >
661+ < input
662+ type = "number"
663+ min = { 0 }
664+ max = { 100 }
665+ value = { cfgRatios [ m ] }
666+ onChange = { e => setCfgRatios ( prev => ( { ...prev , [ m ] : e . target . value } ) ) }
667+ className = "w-20 h-9 px-2 rounded-lg border border-border bg-background text-sm text-right"
668+ />
669+ < span className = "text-sm text-muted-foreground" > %</ span >
670+ < span className = "text-xs text-muted-foreground ml-auto" > ≈ { formatCurrency ( amount ) } </ span >
671+ </ div >
672+ )
673+ } ) }
674+ < div className = "text-xs text-muted-foreground" > 合计可小于 100%,余下为闲置不投入的资金。</ div >
675+ </ div >
676+
677+ < div className = "flex items-center gap-2 pt-1" >
678+ < Button size = "sm" onClick = { handleSaveConfig } disabled = { cfgSaving || ratioSum > 100 } >
679+ { cfgSaving ? '保存中...' : '保存' }
680+ </ Button >
681+ </ div >
682+ </ div >
683+ </ DialogContent >
684+ </ Dialog >
685+
569686 { /* 跟单通知设置对话框 */ }
570687 < Dialog open = { notifyOpen } onOpenChange = { setNotifyOpen } >
571688 < DialogContent >
0 commit comments