@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, type MouseEvent } from 'react'
22import { ProgressBar , Spinner , Tab , TabList , Text , makeStyles , mergeClasses } from '@fluentui/react-components'
33import { AddRegular , ArrowRightRegular , DeleteRegular } from '@fluentui/react-icons'
44import { listDiscoverStrategies , getHealth } from '../api/client'
5- import type { DiscoverJobSnapshot , DiscoverStrategyOption , DiscoverStrategyPublic } from '../types/schemas'
5+ import type { DiscoverJobSnapshot , DiscoverStrategyOption , DiscoverStrategyPublic , MarketRegimeData } from '../types/schemas'
66import type { WatchlistItem } from '../types/market'
77import OpptrixButton from '../components/opptrix/OpptrixButton'
88import DiscoverStrategyPicker from './DiscoverStrategyPicker'
@@ -25,6 +25,7 @@ const CATEGORY_LABEL: Record<DiscoverStrategyPublic['category'], string> = {
2525 quality : '质量' ,
2626 momentum : '动量' ,
2727 balanced : '均衡' ,
28+ contrarian : '逆向' ,
2829}
2930
3031const useStyles = makeStyles ( {
@@ -103,6 +104,26 @@ const useStyles = makeStyles({
103104 color : opptrixTokens . textSecondary ,
104105 lineHeight : 1.45 ,
105106 } ,
107+ regimeBanner : {
108+ flexShrink : 0 ,
109+ padding : `6px ${ CONTENT_PAD } ` ,
110+ borderBottom : `1px solid ${ opptrixTokens . separator } ` ,
111+ fontSize : '10px' ,
112+ lineHeight : 1.45 ,
113+ color : opptrixTokens . textSecondary ,
114+ backgroundColor : opptrixTokens . accentSoft ,
115+ } ,
116+ regimeHeadline : {
117+ fontWeight : 650 ,
118+ color : opptrixTokens . textPrimary ,
119+ marginBottom : '2px' ,
120+ } ,
121+ regimeIndicators : {
122+ fontSize : '9px' ,
123+ color : opptrixTokens . textTertiary ,
124+ lineHeight : 1.4 ,
125+ marginBottom : '3px' ,
126+ } ,
106127 summary : {
107128 flexShrink : 0 ,
108129 padding : `8px ${ CONTENT_PAD } ` ,
@@ -363,6 +384,7 @@ export default function DiscoverTab({ session, watchlistCodes, onSelect, onAdd }
363384 const [ panelTab , setPanelTab ] = useState < DiscoverPanelTab > ( 'results' )
364385 const [ dbReady , setDbReady ] = useState < boolean | null > ( null )
365386 const [ llmReady , setLlmReady ] = useState < boolean | null > ( null )
387+ const [ marketRegime , setMarketRegime ] = useState < MarketRegimeData | null > ( null )
366388
367389 const { strategies : customStrategies } = useCustomDiscoverStrategies ( )
368390
@@ -409,6 +431,10 @@ export default function DiscoverTab({ session, watchlistCodes, onSelect, onAdd }
409431 if ( cancelled ) return
410432 setLlmReady ( Boolean ( h . llm_configured ) )
411433 } ) . catch ( ( ) => setLlmReady ( false ) )
434+ void research . marketRegime ( ) . then ( resp => {
435+ if ( cancelled || ! resp . success || ! resp . data ) return
436+ setMarketRegime ( resp . data )
437+ } ) . catch ( ( ) => { } )
412438 return ( ) => { cancelled = true }
413439 } , [ ] )
414440
@@ -427,6 +453,35 @@ export default function DiscoverTab({ session, watchlistCodes, onSelect, onAdd }
427453 const barValue = running && progressPct <= 0 ? 0.03 : Math . min ( 1 , progressPct / 100 )
428454 const historyJobs = history . filter ( h => h . status !== 'running' || h . id === job ?. id )
429455
456+ const regimeIndicators = useMemo ( ( ) => {
457+ const ind = marketRegime ?. indicators
458+ if ( ! ind ) return null
459+ const parts : string [ ] = [ ]
460+ if ( ind . marks_cycle ) parts . push ( `周期 ${ ind . marks_cycle } ` )
461+ if ( ind . valuation_anchor ) parts . push ( `估值 ${ ind . valuation_anchor } ` )
462+ if ( ind . sentiment_score != null ) parts . push ( `情绪 ${ ind . sentiment_score } ` )
463+ if ( ind . advance_pct != null ) parts . push ( `上涨 ${ ind . advance_pct . toFixed ( 0 ) } %` )
464+ if ( ind . index_pe != null ) parts . push ( `沪深300 PE ${ ind . index_pe . toFixed ( 1 ) } ` )
465+ if ( ind . northbound_net_yi != null ) {
466+ const sign = ind . northbound_net_yi >= 0 ? '+' : ''
467+ parts . push ( `北向 ${ sign } ${ ind . northbound_net_yi . toFixed ( 1 ) } 亿` )
468+ }
469+ return parts . length ? parts . join ( ' · ' ) : null
470+ } , [ marketRegime ] )
471+
472+ const regimeHint = useMemo ( ( ) => {
473+ if ( ! marketRegime ) return null
474+ if ( ! selectedId ) return marketRegime . detail
475+ const suggested = marketRegime . suggested_strategy_ids
476+ if ( ! suggested . length ) return marketRegime . detail
477+ if ( suggested . includes ( selectedId ) ) {
478+ return `当前市况与所选策略较契合。${ marketRegime . detail } `
479+ }
480+ const first = builtinList . find ( st => st . id === suggested [ 0 ] )
481+ if ( ! first ) return marketRegime . detail
482+ return `${ marketRegime . detail } 可优先考虑「${ first . name } 」。`
483+ } , [ marketRegime , selectedId , builtinList ] )
484+
430485 const handleLoadHistory = ( histJob : DiscoverJobSnapshot ) => {
431486 loadHistoryJob ( histJob )
432487 setPanelTab ( 'results' )
@@ -472,6 +527,19 @@ export default function DiscoverTab({ session, watchlistCodes, onSelect, onAdd }
472527 { llmReady === false ? ' · 需配置大模型' : '' }
473528 </ Text >
474529 </ div >
530+ { marketRegime && (
531+ < div className = { s . regimeBanner } >
532+ < Text className = { s . regimeHeadline } block >
533+ { marketRegime . headline }
534+ </ Text >
535+ { regimeIndicators && (
536+ < Text className = { s . regimeIndicators } block >
537+ { regimeIndicators }
538+ </ Text >
539+ ) }
540+ { regimeHint && < Text block > { regimeHint } </ Text > }
541+ </ div >
542+ ) }
475543 </ div >
476544
477545 < div className = { s . tabBar } >
0 commit comments