1212import { useCallback , useEffect , useState } from 'react' ;
1313import { FinanceChart } from './finance-chart' ;
1414import { ReportPanel } from './report-panel' ;
15+ import { NewsSection } from '@/components/news' ;
1516// Import from the SDK-free `types` module (not the index) so the Alpaca SDK is
1617// never pulled into the client bundle.
17- import { TICKER_RANGES , type Candle , type Quote , type TickerRange } from '@/lib/finance/market-data/types' ;
18+ import { TICKER_RANGES , type AssetInfo , type Candle , type Quote , type TickerRange } from '@/lib/finance/market-data/types' ;
19+ import type { WatchlistChanges } from '@/lib/finance/performance' ;
1820
1921const RECENT_KEY = 'finance:recent' ;
2022const RECENT_MAX = 12 ;
@@ -58,6 +60,8 @@ export function TickerView({ symbol }: { symbol: string }): React.ReactElement {
5860 const [ error , setError ] = useState < string | null > ( null ) ;
5961 const [ inWatchlist , setInWatchlist ] = useState < boolean | null > ( null ) ;
6062 const [ holding , setHolding ] = useState < Holding | null > ( null ) ;
63+ const [ changes , setChanges ] = useState < WatchlistChanges | null > ( null ) ;
64+ const [ asset , setAsset ] = useState < AssetInfo | null > ( null ) ;
6165
6266 useEffect ( ( ) => {
6367 rememberRecent ( symbol ) ;
@@ -132,6 +136,36 @@ export function TickerView({ symbol }: { symbol: string }): React.ReactElement {
132136 } ;
133137 } , [ symbol ] ) ;
134138
139+ // Trailing 1/5/30-day % change (reuses the watchlist changes endpoint).
140+ useEffect ( ( ) => {
141+ let cancelled = false ;
142+ setChanges ( null ) ;
143+ fetch ( `/api/finance/watchlist/changes?symbols=${ encodeURIComponent ( symbol ) } ` , { cache : 'no-store' } )
144+ . then ( ( res ) => ( res . ok ? res . json ( ) : { changes : { } } ) )
145+ . then ( ( body : { changes ?: Record < string , WatchlistChanges > } ) => {
146+ if ( ! cancelled ) setChanges ( body . changes ?. [ symbol ] ?? null ) ;
147+ } )
148+ . catch ( ( ) => undefined ) ;
149+ return ( ) => {
150+ cancelled = true ;
151+ } ;
152+ } , [ symbol ] ) ;
153+
154+ // Company / asset metadata (Alpaca assets endpoint).
155+ useEffect ( ( ) => {
156+ let cancelled = false ;
157+ setAsset ( null ) ;
158+ fetch ( `/api/finance/asset?symbol=${ encodeURIComponent ( symbol ) } ` , { cache : 'no-store' } )
159+ . then ( ( res ) => ( res . ok ? res . json ( ) : { asset : null } ) )
160+ . then ( ( body : { asset ?: AssetInfo | null } ) => {
161+ if ( ! cancelled ) setAsset ( body . asset ?? null ) ;
162+ } )
163+ . catch ( ( ) => undefined ) ;
164+ return ( ) => {
165+ cancelled = true ;
166+ } ;
167+ } , [ symbol ] ) ;
168+
135169 const toggleWatchlist = useCallback ( async ( ) => {
136170 const next = ! inWatchlist ;
137171 setInWatchlist ( next ) ;
@@ -167,6 +201,12 @@ export function TickerView({ symbol }: { symbol: string }): React.ReactElement {
167201 { inWatchlist ? '★ In watchlist' : '☆ Add to watchlist' }
168202 </ button >
169203 </ div >
204+ { asset ?. name ? (
205+ < div className = "mt-1 text-sm text-text-secondary" >
206+ { asset . name }
207+ { asset . exchange ? < span className = "text-text-muted" > · { asset . exchange } </ span > : null }
208+ </ div >
209+ ) : null }
170210 { quote ? < div className = "mt-2 flex items-baseline gap-3" >
171211 < span className = "text-2xl font-semibold text-text-primary" >
172212 ${ formatNumber ( quote . price , { minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) }
@@ -177,6 +217,11 @@ export function TickerView({ symbol }: { symbol: string }): React.ReactElement {
177217 { formatNumber ( quote . changePercent , { maximumFractionDigits : 2 } ) } %)
178218 </ span >
179219 </ div > : null }
220+ < div className = "mt-3 flex items-center gap-5" >
221+ < TrailingChange label = "1D" value = { changes ?. d1 ?? null } />
222+ < TrailingChange label = "5D" value = { changes ?. d5 ?? null } />
223+ < TrailingChange label = "30D" value = { changes ?. d30 ?? null } />
224+ </ div >
180225 </ div >
181226 < div className = "flex flex-wrap gap-1" >
182227 { TICKER_RANGES . map ( ( r ) => (
@@ -223,8 +268,35 @@ export function TickerView({ symbol }: { symbol: string }): React.ReactElement {
223268 />
224269 </ div >
225270
271+ { /* Company info from the broker (Alpaca) assets endpoint. */ }
272+ { asset ? (
273+ < section className = "mt-8" >
274+ < h2 className = "mb-3 text-lg font-semibold text-text-primary" > About { symbol } </ h2 >
275+ < div className = "grid grid-cols-2 gap-3 sm:grid-cols-4" >
276+ { asset . name ? < Stat label = "Name" value = { asset . name } /> : null }
277+ { asset . exchange ? < Stat label = "Exchange" value = { asset . exchange } /> : null }
278+ { asset . assetClass ? < Stat label = "Class" value = { formatAssetClass ( asset . assetClass ) } /> : null }
279+ { asset . status ? < Stat label = "Status" value = { capitalize ( asset . status ) } /> : null }
280+ < Stat label = "Tradable" value = { formatBool ( asset . tradable ) } />
281+ < Stat label = "Fractionable" value = { formatBool ( asset . fractionable ) } />
282+ < Stat label = "Marginable" value = { formatBool ( asset . marginable ) } />
283+ < Stat label = "Shortable" value = { formatBool ( asset . shortable ) } />
284+ < Stat label = "Easy to borrow" value = { formatBool ( asset . easyToBorrow ) } />
285+ { asset . hasOptions !== null ? < Stat label = "Options" value = { formatBool ( asset . hasOptions ) } /> : null }
286+ </ div >
287+ </ section >
288+ ) : null }
289+
226290 { /* AI report area — never auto-runs; the Analyze button is the cost boundary. */ }
227291 < ReportPanel symbol = { symbol } />
292+
293+ { /* Ticker news — pulls from our /news API, searched by symbol. */ }
294+ < section className = "mt-8" >
295+ < h2 className = "mb-3 text-lg font-semibold text-text-primary" >
296+ News for { symbol }
297+ </ h2 >
298+ < NewsSection searchTerm = { symbol } limit = { 10 } />
299+ </ section >
228300 </ div >
229301 ) ;
230302}
@@ -237,3 +309,32 @@ function Stat({ label, value }: { label: string; value: string }): React.ReactEl
237309 </ div >
238310 ) ;
239311}
312+
313+ function TrailingChange ( { label, value } : { label : string ; value : number | null } ) : React . ReactElement {
314+ const known = value !== null && Number . isFinite ( value ) ;
315+ const up = known && ( value as number ) >= 0 ;
316+ const color = ! known ? 'text-text-muted' : up ? 'text-green-400' : 'text-red-400' ;
317+ const text = ! known ? '—' : `${ up ? '+' : '' } ${ ( value as number ) . toFixed ( 2 ) } %` ;
318+ return (
319+ < div className = "flex items-baseline gap-1.5" >
320+ < span className = "text-xs uppercase tracking-wider text-text-muted" > { label } </ span >
321+ < span className = { `text-sm font-semibold tabular-nums ${ color } ` } > { text } </ span >
322+ </ div >
323+ ) ;
324+ }
325+
326+ function formatBool ( value : boolean | null ) : string {
327+ if ( value === null ) return '—' ;
328+ return value ? 'Yes' : 'No' ;
329+ }
330+
331+ function capitalize ( value : string ) : string {
332+ return value ? value . charAt ( 0 ) . toUpperCase ( ) + value . slice ( 1 ) : value ;
333+ }
334+
335+ function formatAssetClass ( value : string ) : string {
336+ return value
337+ . split ( '_' )
338+ . map ( ( part ) => ( part === 'us' ? 'US' : capitalize ( part ) ) )
339+ . join ( ' ' ) ;
340+ }
0 commit comments