Skip to content

Commit 8ed5dc2

Browse files
Ruicursoragent
andcommitted
feat(market-dynamics): keep A-share dashboard and offline yfinance
Remove HK/US market dynamics tabs from the UI and unregister yfinance from the builtin provider stack while retaining source for later re-enable. Hub HK helpers fall back to Tickflow; yfinance client hardening stays in tree. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 74671b4 commit 8ed5dc2

21 files changed

Lines changed: 477 additions & 119 deletions

client-ui/src/api/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export const research = {
303303
marketRegime: (scope: 'cn' | 'us' = 'cn') =>
304304
apiCall<import('../types/schemas').MarketRegimeData>('market_regime', { profile_scope: scope }),
305305

306-
marketDynamics: (opts?: { market?: 'cn' | 'us' }) =>
306+
marketDynamics: (opts?: { market?: 'cn' | 'us' | 'hk' }) =>
307307
apiCall<import('../types/schemas').MarketDynamicsData>(
308308
'market_dynamics',
309309
{ market: opts?.market ?? 'cn' },

client-ui/src/desktop/DesktopWindowChrome.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
desktopTitleLeft,
4545
desktopTitleMaxWidth,
4646
desktopToolbarLeft,
47+
desktopChromeToolbarReserve,
4748
type DesktopViewMode,
4849
} from './layout'
4950
import ChromeToolButton from './ChromeToolButton'
@@ -357,6 +358,8 @@ export default function DesktopWindowChrome({
357358
rightPanelWidth,
358359
)
359360

361+
const standaloneDragLeft = dragLeftInset
362+
360363
const handleSidebarPointer = () => {
361364
if (sidebarHoverReveal) {
362365
if (!sidebarOpen) onRevealSidebar?.()
@@ -411,7 +414,7 @@ export default function DesktopWindowChrome({
411414
<div
412415
className={s.drag}
413416
style={{
414-
left: `${dragLeftInset}px`,
417+
left: `${standaloneDragLeft}px`,
415418
right: 0,
416419
...dragRightClip,
417420
...(dragRightClip.right != null

client-ui/src/pages/market-dynamics/CnInsightSplitView.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useMemo } from 'react'
22
import { makeStyles, mergeClasses } from '@fluentui/react-components'
33
import TradingViewChart from '../../market/TradingViewChart'
44
import { opptrixCssVars } from '../../theme/tokens'
5+
import type { InstrumentRef } from '../../types/instrument'
56
import { CnInsightStockSelectProvider } from './cnInsightStockContext'
67
import type { CnInsightStockPick } from './cnInsightStockUtils'
78
import { cnInsightChartInputCode, cnInsightInstrumentFromCode } from './cnInsightStockUtils'
@@ -68,23 +69,29 @@ type Props = {
6869
selected: CnInsightStockPick | null
6970
onSelect: (pick: CnInsightStockPick | null) => void
7071
children: React.ReactNode
72+
instrumentFromCode?: (code: string) => InstrumentRef
73+
chartInputCode?: (ref: InstrumentRef) => string
7174
}
7275

7376
function ChartPane({
7477
stock,
7578
onClose,
79+
instrumentFromCode,
80+
chartInputCodeFn,
7681
}: {
7782
stock: CnInsightStockPick
7883
onClose: () => void
84+
instrumentFromCode: (code: string) => InstrumentRef
85+
chartInputCodeFn: (ref: InstrumentRef) => string
7986
}) {
8087
const s = useStyles()
8188
const instrument = useMemo(
82-
() => cnInsightInstrumentFromCode(stock.code),
83-
[stock.code],
89+
() => instrumentFromCode(stock.code),
90+
[instrumentFromCode, stock.code],
8491
)
8592
const chartInputCode = useMemo(
86-
() => cnInsightChartInputCode(instrument),
87-
[instrument],
93+
() => chartInputCodeFn(instrument),
94+
[chartInputCodeFn, instrument],
8895
)
8996

9097
return (
@@ -109,6 +116,8 @@ export default function CnInsightSplitView({
109116
selected,
110117
onSelect,
111118
children,
119+
instrumentFromCode = cnInsightInstrumentFromCode,
120+
chartInputCode: chartInputCodeFn = cnInsightChartInputCode,
112121
}: Props) {
113122
const s = useStyles()
114123
const split = selected != null
@@ -121,7 +130,12 @@ export default function CnInsightSplitView({
121130
</div>
122131
<div className={mergeClasses(s.chartPane, split && s.chartPaneOpen)}>
123132
{selected ? (
124-
<ChartPane stock={selected} onClose={() => onSelect(null)} />
133+
<ChartPane
134+
stock={selected}
135+
onClose={() => onSelect(null)}
136+
instrumentFromCode={instrumentFromCode}
137+
chartInputCodeFn={chartInputCodeFn}
138+
/>
125139
) : null}
126140
</div>
127141
</div>

client-ui/src/pages/market-dynamics/MarketDynamicsHeader.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,19 @@ const useStyles = makeStyles({
3636
height: '40px',
3737
padding: '0 12px',
3838
},
39+
titleTabs: {
40+
flexShrink: 0,
41+
display: 'flex',
42+
alignItems: 'center',
43+
minHeight: '28px',
44+
position: 'relative',
45+
zIndex: 1,
46+
},
3947
title: {
40-
paddingLeft: '3px',
41-
fontSize: 'var(--opptrix-font-sm)',
48+
fontSize: 'var(--opptrix-font-md)',
4249
fontWeight: 650,
4350
color: opptrixCssVars.textPrimary,
44-
flexShrink: 0,
51+
lineHeight: 1.2,
4552
whiteSpace: 'nowrap',
4653
},
4754
spacer: {
@@ -133,7 +140,9 @@ export default function MarketDynamicsHeader({
133140
paddingRight: electronChrome ? `${paddingRight}px` : undefined,
134141
}}
135142
>
136-
<Text className={s.title}>A股</Text>
143+
<div className={mergeClasses(s.titleTabs, 'opptrix-panel-title-no-drag')}>
144+
<Text className={s.title}>市场动态</Text>
145+
</div>
137146
<div
138147
className={mergeClasses(s.spacer, electronChrome && dragRegionClassName)}
139148
aria-hidden

client-ui/src/pages/market-dynamics/MarketDynamicsPage.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,20 @@ type Props = {
4545

4646
function MarketDynamicsContent({ electronChrome = false, chromeToolbarReserve = 0 }: Props) {
4747
const s = useStyles()
48-
const { data, loading, refreshing, error, refreshedAt, refresh } = useMarketDynamics('cn')
49-
const insights = useMarketInsights('cn')
48+
const { data, loading, refreshing, error, refresh } = useMarketDynamics()
49+
const insights = useMarketInsights()
5050

51-
const updatedLabel = refreshedAt ? formatCnDateTime(refreshedAt) : null
51+
const panelData = data?.market === 'cn' ? data : null
52+
const panelLoading = loading || (data != null && data.market !== 'cn')
53+
54+
const updatedLabel = panelData?.refreshed_at ? formatCnDateTime(panelData.refreshed_at) : null
5255
const statusLabel = refreshing
5356
? '刷新中…'
5457
: updatedLabel
5558
? `更新 ${updatedLabel}`
5659
: '尚未刷新'
5760

58-
const hasData = Boolean(data?.sections.length || data?.us_indices?.length)
61+
const hasData = Boolean(panelData?.sections.length)
5962

6063
const handleRefresh = () => {
6164
void refresh()
@@ -75,8 +78,8 @@ function MarketDynamicsContent({ electronChrome = false, chromeToolbarReserve =
7578
{error && <div className={s.errorBanner}>{error}</div>}
7679
<div className={s.content}>
7780
<CnMarketDynamicsView
78-
data={data}
79-
loading={loading && !hasData}
81+
data={panelData}
82+
loading={panelLoading && !hasData}
8083
articles={insights.articles}
8184
insightsLoading={insights.loading}
8285
/>

client-ui/src/pages/market-dynamics/marketBoardUtils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,18 @@ export function indexInstrumentFromQuote(
6666
const chartSym = item.chart_symbol?.trim()
6767

6868
if (chartSym && (mkt === 'HK' || mkt === 'US')) {
69-
return { market: mkt as 'HK' | 'US', assetClass: 'INDEX', symbol: chartSym }
69+
const isHkIndex = mkt === 'HK' && (
70+
chartSym.startsWith('^')
71+
|| chartSym.toUpperCase() === 'HSTECH.HK'
72+
|| ['HSI', 'HSCE', 'HSTECH'].includes((item.code ?? '').trim().toUpperCase())
73+
)
74+
return {
75+
market: mkt as 'HK' | 'US',
76+
assetClass: mkt === 'HK'
77+
? (isHkIndex ? 'INDEX' : 'EQUITY')
78+
: 'INDEX',
79+
symbol: chartSym,
80+
}
7081
}
7182

7283
const symbol = indexChartCodeFromQuote(item)
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1+
/** A 股市场动态 — 港股/美股面板已暂时下线 */
12
export type MarketDynamicsTab = 'cn'
23

4+
const STORAGE_KEY = 'opptrix-market-dynamics-tab'
5+
36
export function readMarketDynamicsTab(): MarketDynamicsTab {
7+
if (typeof window === 'undefined') return 'cn'
8+
try {
9+
const raw = localStorage.getItem(STORAGE_KEY)?.trim().toLowerCase()
10+
if (raw === 'hk' || raw === 'us') {
11+
localStorage.setItem(STORAGE_KEY, 'cn')
12+
}
13+
} catch {
14+
/* ignore */
15+
}
416
return 'cn'
517
}
618

719
export function writeMarketDynamicsTab(_tab: MarketDynamicsTab): void {
8-
/* 暂仅 A 股看板;忽略历史 localStorage */
20+
if (typeof window === 'undefined') return
21+
try {
22+
localStorage.setItem(STORAGE_KEY, 'cn')
23+
} catch {
24+
/* ignore */
25+
}
926
}

client-ui/src/pages/market-dynamics/useMarketDynamics.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { useCallback, useEffect, useRef, useState } from 'react'
22
import { news, research } from '../../api/client'
33
import type { FeedArticle, MarketDynamicsData } from '../../types/schemas'
4-
import type { MarketDynamicsTab } from './marketDynamicsStorage'
54

65
const NEWS_REFRESH_MS = 60_000
76
const MARKET_REFRESH_MS = 30_000
87

9-
export function useMarketInsights(market: MarketDynamicsTab = 'cn') {
8+
function filterCnArticles(articles: FeedArticle[]): FeedArticle[] {
9+
return articles.filter(article => {
10+
const text = `${article.title} ${article.source_title ?? ''}`.toLowerCase()
11+
return /a|||||||cn\b|china/i.test(text)
12+
|| !/|nasdaq|nyse|||crypto|btc/i.test(text)
13+
})
14+
}
15+
16+
export function useMarketInsights() {
1017
const [articles, setArticles] = useState<FeedArticle[]>([])
1118
const [loading, setLoading] = useState(true)
1219
const [error, setError] = useState('')
@@ -19,20 +26,15 @@ export function useMarketInsights(market: MarketDynamicsTab = 'cn') {
1926
const feedResp = await news.getFeed({ limit: 24 }).catch(() => null)
2027
if (!mountedRef.current) return
2128
if (feedResp?.articles) {
22-
const filtered = feedResp.articles.filter(article => {
23-
const text = `${article.title} ${article.source_title ?? ''}`.toLowerCase()
24-
return /a|||||||cn\b|china/i.test(text)
25-
|| !/|nasdaq|nyse|||crypto|btc/i.test(text)
26-
})
27-
setArticles(filtered.slice(0, 12))
29+
setArticles(filterCnArticles(feedResp.articles).slice(0, 12))
2830
}
2931
} catch (e) {
3032
if (!mountedRef.current) return
3133
setError(e instanceof Error ? e.message : '资讯加载失败')
3234
} finally {
3335
if (mountedRef.current) setLoading(false)
3436
}
35-
}, [market])
37+
}, [])
3638

3739
useEffect(() => {
3840
mountedRef.current = true
@@ -47,7 +49,7 @@ export function useMarketInsights(market: MarketDynamicsTab = 'cn') {
4749
return { articles, loading, error, refresh: () => load({ silent: true }) }
4850
}
4951

50-
export function useMarketDynamics(market: MarketDynamicsTab = 'cn') {
52+
export function useMarketDynamics() {
5153
const [data, setData] = useState<MarketDynamicsData | null>(null)
5254
const [loading, setLoading] = useState(true)
5355
const [refreshing, setRefreshing] = useState(false)
@@ -60,10 +62,13 @@ export function useMarketDynamics(market: MarketDynamicsTab = 'cn') {
6062
else setRefreshing(true)
6163
setError('')
6264
try {
63-
const resp = await research.marketDynamics({ market })
65+
const resp = await research.marketDynamics({ market: 'cn' })
6466
if (!mountedRef.current) return
6567
if (resp.success && resp.data) {
66-
setData(resp.data)
68+
setData({
69+
...resp.data,
70+
market: resp.data.market ?? 'cn',
71+
})
6772
} else {
6873
setError(resp.message || '暂时无法获取市场数据')
6974
}
@@ -76,7 +81,7 @@ export function useMarketDynamics(market: MarketDynamicsTab = 'cn') {
7681
setRefreshing(false)
7782
}
7883
}
79-
}, [market])
84+
}, [])
8085

8186
useEffect(() => {
8287
mountedRef.current = true

client-ui/src/types/schemas.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ export interface MarketStockMover {
400400
price: number | null
401401
change_pct: number | null
402402
change_amt?: number | null
403+
rank?: number | null
403404
}
404405

405406
export interface MarketDragonTigerItem {
@@ -461,7 +462,7 @@ export interface MarketAnomalyItem {
461462
}
462463

463464
export interface MarketDynamicsData {
464-
market?: 'cn' | 'us'
465+
market?: 'cn' | 'us' | 'hk'
465466
refreshed_at: string
466467
sections: MarketDynamicsSection[]
467468
cn_gainers?: MarketStockMover[]
@@ -482,6 +483,12 @@ export interface MarketDynamicsData {
482483
us_losers?: MarketStockMover[]
483484
us_trending?: MarketStockMover[]
484485
us_trending_jp?: MarketStockMover[]
486+
hk_indices?: MarketIndexQuote[]
487+
hk_gainers?: MarketStockMover[]
488+
hk_losers?: MarketStockMover[]
489+
hk_trending?: MarketStockMover[]
490+
hk_sector_status?: 'ok' | 'empty' | 'error'
491+
hk_sector_hint?: string
485492
}
486493

487494
export interface StockSearchItem {

packages/a-stock-layer/src/engine.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,13 +1220,20 @@ export class MarketDataEngine {
12201220
const normalized = market === 'US'
12211221
? symbols.map(s => normalizeUsSymbol(s))
12221222
: symbols
1223+
const marketsMap = Object.fromEntries(
1224+
normalized.flatMap(code => {
1225+
const key = code.trim()
1226+
const padded = market === 'HK' ? key.padStart(5, '0') : key
1227+
return [[key, market], [padded, market], [key.toUpperCase(), market]]
1228+
}),
1229+
)
12231230
return this.qScoped(
12241231
market,
12251232
'EQUITY',
12261233
Capability.STOCK_REALTIME,
12271234
'batchRealtime',
12281235
false,
1229-
normalized,
1236+
[normalized, marketsMap],
12301237
)
12311238
}
12321239

0 commit comments

Comments
 (0)