diff --git a/apps/stock/web-app/src/app/App.tsx b/apps/stock/web-app/src/app/App.tsx index 256a540..e95f970 100644 --- a/apps/stock/web-app/src/app/App.tsx +++ b/apps/stock/web-app/src/app/App.tsx @@ -4,7 +4,7 @@ import { ExchangesPage } from '@/features/exchanges'; import { MonitoringPage } from '@/features/monitoring'; import { PipelinePage } from '@/features/pipeline'; import { BacktestPage } from '@/features/backtest'; -import { ChartPage } from '@/features/charts'; +import { SymbolsPage, SymbolDetailPage } from '@/features/symbols'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; export function App() { @@ -14,7 +14,10 @@ export function App() { }> } /> } /> - } /> + + } /> + } /> + } /> { - const expanded = new Set(); - navigation.forEach(item => { - if (item.children && item.children.some(child => location.pathname === child.href)) { - expanded.add(item.name); - } - }); - return expanded; - }; - - const [expandedItems, setExpandedItems] = useState>(getInitialExpanded()); - - const toggleExpanded = (name: string) => { - const newExpanded = new Set(expandedItems); - if (newExpanded.has(name)) { - newExpanded.delete(name); - } else { - newExpanded.add(name); - } - setExpandedItems(newExpanded); - }; const isChildActive = (children: NavigationItem[]) => { return children.some(child => location.pathname === child.href); @@ -119,12 +96,11 @@ function SidebarContent() {
  • {item.children ? ( <> - - {expandedItems.has(item.name) && ( -
      - {item.children.map(child => ( -
    • - - cn( - isActive - ? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500' - : 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary', - 'group flex gap-x-2 rounded-r-md px-2 py-1 text-sm leading-tight font-medium transition-colors' - ) - } - > - {({ isActive }) => ( - <> - -
    • - ))} -
    - )} + +
      + {item.children.map(child => ( +
    • + + cn( + isActive + ? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500' + : 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary', + 'group flex gap-x-2 rounded-r-md px-2 py-1 text-sm leading-tight font-medium transition-colors' + ) + } + > + {({ isActive }) => ( + <> + +
    • + ))} +
    ) : ( ({ - symbol: 'AAPL', + symbol, interval: '1d', chartType: 'candlestick', showVolume: true, @@ -201,14 +206,16 @@ export function ChartPage() { return (
    -
    -

    Market Charts

    -

    - Real-time market data and advanced charting powered by TradingView. -

    -
    + {showHeader && ( +
    +

    Market Charts

    +

    + Real-time market data and advanced charting powered by TradingView. +

    +
    + )} -
    +
    {/* Left Sidebar - Indicator List */} {showSidebar && ( -
    - {/* Symbol Input */} -
    - onConfigChange({ symbol: e.target.value.toUpperCase() })} - placeholder="Symbol" - className="px-3 py-1.5 bg-background border border-border rounded-md text-sm font-medium text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500 w-24" - /> -
    - +
    +
    {/* Interval Selector */}
    {intervals.map(({ value, label }) => ( @@ -76,19 +65,6 @@ export function ChartToolbar({ config, onConfigChange }: ChartToolbarProps) {
    - {/* Volume Toggle */} - - - {/* Theme Toggle */} +
    +
    + ); + } + + const renderTabContent = () => { + switch (activeTab) { + case 'chart': + return ; + case 'financials': + return ( +
    +

    Financial Statements

    +

    Financial data coming soon...

    +
    + ); + case 'filings': + return ( +
    +

    SEC Filings

    +

    Filings data coming soon...

    +
    + ); + case 'insiders': + return ( +
    +

    Insider Trading

    +

    Insider trading data coming soon...

    +
    + ); + case 'shorts': + return ( +
    +

    Short Interest

    +

    Short interest data coming soon...

    +
    + ); + case 'sentiment': + return ( +
    +

    Market Sentiment

    +

    Sentiment analysis coming soon...

    +
    + ); + case 'news': + return ( +
    +

    Latest News

    +

    News feed coming soon...

    +
    + ); + case 'info': + return ( +
    +

    Company Information

    +
    +
    +
    +
    Symbol
    +
    {symbolData.symbol}
    +
    +
    +
    Name
    +
    {symbolData.name}
    +
    +
    +
    Exchange
    +
    {symbolData.exchange}
    +
    +
    +
    Country
    +
    {symbolData.country}
    +
    +
    +
    Type
    +
    {symbolData.type}
    +
    +
    +
    Currency
    +
    {symbolData.currency}
    +
    + {symbolData.sector && ( +
    +
    Sector
    +
    {symbolData.sector}
    +
    + )} + {symbolData.industry && ( +
    +
    Industry
    +
    {symbolData.industry}
    +
    + )} +
    +
    +
    + ); + default: + return null; + } + }; + + return ( +
    + {/* Header with Tabs */} +
    +
    +
    + +
    +

    + {symbolData.symbol} + + {symbolData.exchange} + +

    +

    {symbolData.name}

    +
    + + {/* Tabs */} + +
    +
    +
    + + {/* Tab Content */} +
    + {renderTabContent()} +
    +
    + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/symbols/SymbolsPage.tsx b/apps/stock/web-app/src/features/symbols/SymbolsPage.tsx new file mode 100644 index 0000000..b2abfe7 --- /dev/null +++ b/apps/stock/web-app/src/features/symbols/SymbolsPage.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react'; +import { SymbolsFilter } from './components/SymbolsFilter'; +import { SymbolsTable } from './components/SymbolsTable'; +import type { Exchange, Symbol, SymbolFilter } from './types'; + +// Mock data for demonstration +const mockExchanges: Exchange[] = [ + { code: 'TSX', name: 'Toronto Stock Exchange', country: 'CA' }, + { code: 'TSXV', name: 'TSX Venture Exchange', country: 'CA' }, + { code: 'NYSE', name: 'New York Stock Exchange', country: 'US' }, + { code: 'NASDAQ', name: 'NASDAQ', country: 'US' }, + { code: 'AMEX', name: 'NYSE American', country: 'US' }, +]; + +const mockSymbols: Symbol[] = [ + { symbol: 'AAPL', name: 'Apple Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 3.48e12, sector: 'Technology', industry: 'Consumer Electronics' }, + { symbol: 'MSFT', name: 'Microsoft Corporation', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 3.18e12, sector: 'Technology', industry: 'Software' }, + { symbol: 'GOOGL', name: 'Alphabet Inc. Class A', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 2.23e12, sector: 'Technology', industry: 'Internet Services' }, + { symbol: 'AMZN', name: 'Amazon.com Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 2.16e12, sector: 'Consumer Cyclical', industry: 'E-Commerce' }, + { symbol: 'TSLA', name: 'Tesla Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 1.25e12, sector: 'Consumer Cyclical', industry: 'Auto Manufacturers' }, + { symbol: 'META', name: 'Meta Platforms Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 1.52e12, sector: 'Technology', industry: 'Social Media' }, + { symbol: 'JPM', name: 'JPMorgan Chase & Co.', exchange: 'NYSE', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 650e9, sector: 'Financial', industry: 'Banks' }, + { symbol: 'BAC', name: 'Bank of America Corp.', exchange: 'NYSE', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 370e9, sector: 'Financial', industry: 'Banks' }, + { symbol: 'TD', name: 'Toronto-Dominion Bank', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 150e9, sector: 'Financial', industry: 'Banks' }, + { symbol: 'RY', name: 'Royal Bank of Canada', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 200e9, sector: 'Financial', industry: 'Banks' }, + { symbol: 'SHOP', name: 'Shopify Inc.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 135e9, sector: 'Technology', industry: 'E-Commerce Software' }, + { symbol: 'CP', name: 'Canadian Pacific Kansas City', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 110e9, sector: 'Industrials', industry: 'Railroads' }, + { symbol: 'CNR', name: 'Canadian National Railway', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 105e9, sector: 'Industrials', industry: 'Railroads' }, + { symbol: 'BMO', name: 'Bank of Montreal', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 125e9, sector: 'Financial', industry: 'Banks' }, + { symbol: 'BNS', name: 'Bank of Nova Scotia', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 95e9, sector: 'Financial', industry: 'Banks' }, + { symbol: 'ENB', name: 'Enbridge Inc.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 110e9, sector: 'Energy', industry: 'Oil & Gas Pipelines' }, + { symbol: 'SU', name: 'Suncor Energy Inc.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 65e9, sector: 'Energy', industry: 'Oil & Gas' }, + { symbol: 'TRP', name: 'TC Energy Corporation', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 60e9, sector: 'Energy', industry: 'Oil & Gas Pipelines' }, + { symbol: 'MFC', name: 'Manulife Financial Corp.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 55e9, sector: 'Financial', industry: 'Insurance' }, + { symbol: 'BAM', name: 'Brookfield Asset Management', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 85e9, sector: 'Financial', industry: 'Asset Management' }, +]; + +export function SymbolsPage() { + const [filters, setFilters] = useState({ + country: ['CA', 'US'], + }); + const [filteredSymbols, setFilteredSymbols] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Simulate API call + setIsLoading(true); + setTimeout(() => { + let symbols = [...mockSymbols]; + + // Apply filters + if (filters.exchange) { + symbols = symbols.filter(s => s.exchange === filters.exchange); + } + + if (filters.searchText) { + const search = filters.searchText.toLowerCase(); + symbols = symbols.filter(s => + s.symbol.toLowerCase().includes(search) || + s.name.toLowerCase().includes(search) + ); + } + + if (filters.country && filters.country.length > 0) { + symbols = symbols.filter(s => filters.country!.includes(s.country)); + } + + // Sort by market cap (descending) + symbols.sort((a, b) => (b.marketCap || 0) - (a.marketCap || 0)); + + setFilteredSymbols(symbols); + setIsLoading(false); + }, 500); + }, [filters]); + + return ( +
    +
    +
    +

    Symbols

    + +
    + {isLoading ? ( + 'Loading...' + ) : ( + `${filteredSymbols.length} found` + )} +
    +
    +
    + +
    + +
    +
    + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/symbols/components/MultiSelect.tsx b/apps/stock/web-app/src/features/symbols/components/MultiSelect.tsx new file mode 100644 index 0000000..2c1df57 --- /dev/null +++ b/apps/stock/web-app/src/features/symbols/components/MultiSelect.tsx @@ -0,0 +1,92 @@ +import { Fragment, useState } from 'react'; +import { Listbox, Transition } from '@headlessui/react'; +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline'; + +interface Option { + value: string; + label: string; +} + +interface MultiSelectProps { + options: Option[]; + selected: string[]; + onChange: (selected: string[]) => void; + placeholder?: string; + className?: string; +} + +export function MultiSelect({ options, selected, onChange, placeholder = 'Select...', className = '' }: MultiSelectProps) { + const [isOpen, setIsOpen] = useState(false); + + const handleToggle = (value: string) => { + if (selected.includes(value)) { + onChange(selected.filter(v => v !== value)); + } else { + onChange([...selected, value]); + } + }; + + const selectedLabels = selected.map(value => + options.find(opt => opt.value === value)?.label || value + ).join(', '); + + return ( +
    + {}} multiple> +
    + setIsOpen(!isOpen)} + className="relative w-full cursor-default rounded-md bg-background border border-border py-1 pl-2 pr-8 text-left text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500" + > + + {selected.length > 0 ? selectedLabels : placeholder} + + + + + + + {options.map((option) => ( + + `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ + active ? 'bg-surface-secondary text-text-primary' : 'text-text-primary' + }` + } + onClick={(e) => { + e.preventDefault(); + handleToggle(option.value); + }} + > + <> + + {option.label} + + {selected.includes(option.value) ? ( + + + ) : null} + + + ))} + + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/symbols/components/SymbolsFilter.tsx b/apps/stock/web-app/src/features/symbols/components/SymbolsFilter.tsx new file mode 100644 index 0000000..ffdf005 --- /dev/null +++ b/apps/stock/web-app/src/features/symbols/components/SymbolsFilter.tsx @@ -0,0 +1,80 @@ +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import type { Exchange, SymbolFilter } from '../types'; +import { MultiSelect } from './MultiSelect'; + +interface SymbolsFilterProps { + filters: SymbolFilter; + exchanges: Exchange[]; + onFiltersChange: (filters: SymbolFilter) => void; +} + +export function SymbolsFilter({ filters, exchanges, onFiltersChange }: SymbolsFilterProps) { + const handleExchangeChange = (exchange: string) => { + onFiltersChange({ + ...filters, + exchange: exchange === 'all' ? undefined : exchange, + }); + }; + + const handleSearchChange = (searchText: string) => { + onFiltersChange({ + ...filters, + searchText: searchText.trim() || undefined, + }); + }; + + const handleCountryChange = (countries: string[]) => { + onFiltersChange({ + ...filters, + country: countries.length > 0 ? countries : undefined, + }); + }; + + const countryOptions = [ + { value: 'CA', label: 'Canada' }, + { value: 'US', label: 'United States' }, + ]; + + return ( +
    + {/* Exchange Dropdown */} + + + {/* Symbol Search */} +
    +
    + +
    + handleSearchChange(e.target.value)} + className="w-full pl-8 pr-2 py-1 rounded-md bg-background border border-border text-sm text-text-primary placeholder-text-secondary focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500" + /> +
    + + {/* Country Filter */} +
    + +
    +
    + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/symbols/components/SymbolsTable.tsx b/apps/stock/web-app/src/features/symbols/components/SymbolsTable.tsx new file mode 100644 index 0000000..3511b88 --- /dev/null +++ b/apps/stock/web-app/src/features/symbols/components/SymbolsTable.tsx @@ -0,0 +1,103 @@ +import { useNavigate } from 'react-router-dom'; +import type { Symbol } from '../types'; +import { DataTable } from '@/components/ui/DataTable'; +import type { ColumnDef } from '@tanstack/react-table'; + +interface SymbolsTableProps { + symbols: Symbol[]; + isLoading?: boolean; +} + +export function SymbolsTable({ symbols, isLoading }: SymbolsTableProps) { + const navigate = useNavigate(); + + const formatMarketCap = (value?: number) => { + if (!value) return '-'; + if (value >= 1e12) return `$${(value / 1e12).toFixed(2)}T`; + if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`; + if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`; + return `$${value.toLocaleString()}`; + }; + + const columns: ColumnDef[] = [ + { + accessorKey: 'symbol', + header: 'Symbol', + size: 120, + cell: ({ row }) => ( +
    +
    {row.original.symbol}
    +
    {row.original.currency}
    +
    + ), + }, + { + accessorKey: 'name', + header: 'Name', + size: 250, + cell: ({ getValue }) => ( +
    {getValue() as string}
    + ), + }, + { + accessorKey: 'exchange', + header: 'Exchange', + size: 150, + cell: ({ row }) => ( +
    +
    {row.original.exchange}
    +
    {row.original.country}
    +
    + ), + }, + { + accessorKey: 'type', + header: 'Type', + size: 120, + cell: ({ getValue }) => ( + + {getValue() as string} + + ), + }, + { + accessorKey: 'marketCap', + header: 'Market Cap', + size: 120, + cell: ({ getValue }) => ( + {formatMarketCap(getValue() as number)} + ), + }, + { + accessorKey: 'sector', + header: 'Sector', + size: 200, + cell: ({ row }) => ( +
    +
    {row.original.sector || '-'}
    +
    {row.original.industry || '-'}
    +
    + ), + }, + ]; + + if (symbols.length === 0 && !isLoading) { + return ( +
    +
    + No symbols found matching your criteria +
    +
    + ); + } + + return ( + navigate(`/symbols/${symbol.symbol}`)} + className="border-0 rounded-none border-t h-full" + /> + ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/symbols/components/index.ts b/apps/stock/web-app/src/features/symbols/components/index.ts new file mode 100644 index 0000000..0857f49 --- /dev/null +++ b/apps/stock/web-app/src/features/symbols/components/index.ts @@ -0,0 +1,3 @@ +export { SymbolsFilter } from './SymbolsFilter'; +export { SymbolsTable } from './SymbolsTable'; +export { MultiSelect } from './MultiSelect'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/symbols/index.ts b/apps/stock/web-app/src/features/symbols/index.ts new file mode 100644 index 0000000..2b5669a --- /dev/null +++ b/apps/stock/web-app/src/features/symbols/index.ts @@ -0,0 +1,3 @@ +export { SymbolsPage } from './SymbolsPage'; +export { SymbolDetailPage } from './SymbolDetailPage'; +export * from './types'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/symbols/types.ts b/apps/stock/web-app/src/features/symbols/types.ts new file mode 100644 index 0000000..bc43b72 --- /dev/null +++ b/apps/stock/web-app/src/features/symbols/types.ts @@ -0,0 +1,23 @@ +export interface Symbol { + symbol: string; + name: string; + exchange: string; + country: string; + type: string; + currency: string; + marketCap?: number; + sector?: string; + industry?: string; +} + +export interface SymbolFilter { + exchange?: string; + searchText?: string; + country?: string[]; +} + +export interface Exchange { + code: string; + name: string; + country: string; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/lib/constants.ts b/apps/stock/web-app/src/lib/constants.ts index 92d68ef..03dcc99 100644 --- a/apps/stock/web-app/src/lib/constants.ts +++ b/apps/stock/web-app/src/lib/constants.ts @@ -25,8 +25,8 @@ export const navigation: NavigationItem[] = [ name: 'Market Data', icon: CurrencyDollarIcon, children: [ - { name: 'Charts', href: '/charts', icon: ChartBarIcon }, { name: 'Exchanges', href: '/exchanges', icon: BuildingLibraryIcon }, + { name: 'Symbols', href: '/symbols', icon: ChartBarIcon }, ], }, { name: 'Portfolio', href: '/portfolio', icon: ChartBarIcon },