spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: client/src/pages/explore.tsx6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { useState } from "react";18import { useQuery } from "@tanstack/react-query";19import { useLocation } from "wouter";20import { Sparkles, BookOpen, Search, ArrowLeft, BarChart3, FileText, DollarSign, Users, Award, Newspaper } from "lucide-react";21import { ThemeToggle } from "@/components/theme-toggle";22import { Button } from "@/components/ui/button";23import { Input } from "@/components/ui/input";24import { Card, CardContent } from "@/components/ui/card";25import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";26import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";2728// New explore-specific components29import { MarketOverview } from "@/components/explore/market-overview";30import { StockHeader } from "@/components/explore/stock-header";31import { FinancialStatementsView } from "@/components/explore/financial-statements-view";32import { MetricsView } from "@/components/explore/metrics-view";33import { AnalystView } from "@/components/explore/analyst-view";34import { OwnershipView } from "@/components/explore/ownership-view";35import { NewsView } from "@/components/explore/news-view";36import { AdvancedPriceChart } from "@/components/financial/advanced-price-chart";37import { MarketTicker } from "@/components/market-ticker";3839export default function Explore() {40 const [, setLocation] = useLocation();41 const [searchQuery, setSearchQuery] = useState("");42 const [selectedTicker, setSelectedTicker] = useState<string | null>(null);43 const [statementPeriod, setStatementPeriod] = useState<"annual" | "quarter">("annual");4445 // Fetch market overview data - Major global indices46 const { data: marketIndices } = useQuery({47 queryKey: ['market-indices'],48 queryFn: async () => {49 // Major global indices to display50 const symbols = [51 '^GSPC', // S&P 50052 '^DJI', // Dow Jones Industrial Average53 '^IXIC', // NASDAQ Composite54 '^RUT', // Russell 200055 '^FTSE', // FTSE 100 (UK)56 '^GDAXI', // DAX (Germany)57 '^FCHI', // CAC 40 (France)58 '^N225', // Nikkei 225 (Japan)59 '^HSI', // Hang Seng (Hong Kong)60 '^STOXX50E', // EURO STOXX 5061 '^AXJO', // S&P/ASX 200 (Australia)62 '^GSPTSE' // S&P/TSX Composite (Canada)63 ];6465 const symbolsParam = symbols.join(',');66 const res = await fetch(`https://financialmodelingprep.com/stable/quote?symbol=${symbolsParam}&apikey=${import.meta.env.VITE_FMP_API_KEY || 'JeWwQMjWS3H6hBGHaVxKy1WfGyU5ZeFq'}`);6768 if (!res.ok) throw new Error('Failed to fetch market indices');69 const data = await res.json();7071 // Transform data to match expected format with validation72 return data73 .filter((index: any) => index && index.symbol && index.price != null)74 .map((index: any) => ({75 symbol: index.symbol,76 name: index.name || index.symbol,77 price: Number(index.price) || 0,78 change: Number(index.change) || 0,79 changesPercentage: Number(index.changesPercentage) || 0,80 dayLow: Number(index.dayLow) || 0,81 dayHigh: Number(index.dayHigh) || 0,82 yearHigh: Number(index.yearHigh) || 0,83 yearLow: Number(index.yearLow) || 0,84 volume: Number(index.volume) || 0,85 previousClose: Number(index.previousClose) || 086 }));87 },88 refetchInterval: 60000,89 });9091 const { data: marketStatus } = useQuery({92 queryKey: ['market-status'],93 queryFn: async () => {94 const res = await fetch('/api/fmp/market-hours');95 if (!res.ok) throw new Error('Failed to fetch market status');96 return res.json();97 },98 });99100 const { data: activeStocks } = useQuery({101 queryKey: ['active-stocks'],102 queryFn: async () => {103 const res = await fetch('/api/fmp/actives');104 if (!res.ok) throw new Error('Failed to fetch active stocks');105 return res.json();106 },107 });108109 const { data: gainers } = useQuery({110 queryKey: ['gainers'],111 queryFn: async () => {112 const res = await fetch('/api/fmp/gainers');113 if (!res.ok) throw new Error('Failed to fetch gainers');114 return res.json();115 },116 });117118 const { data: losers } = useQuery({119 queryKey: ['losers'],120 queryFn: async () => {121 const res = await fetch('/api/fmp/losers');122 if (!res.ok) throw new Error('Failed to fetch losers');123 return res.json();124 },125 });126127 // Fetch stock data when ticker is selected128 const { data: companyProfile } = useQuery({129 queryKey: ['company-profile', selectedTicker],130 queryFn: async () => {131 if (!selectedTicker) return null;132 const res = await fetch(`/api/fmp/company-profile/${selectedTicker}`);133 if (!res.ok) throw new Error('Failed to fetch company profile');134 return res.json();135 },136 enabled: !!selectedTicker,137 });138139 const { data: stockQuote } = useQuery({140 queryKey: ['stock-quote', selectedTicker],141 queryFn: async () => {142 if (!selectedTicker) return null;143 const res = await fetch(`/api/fmp/stock-quote/${selectedTicker}`);144 if (!res.ok) throw new Error('Failed to fetch stock quote');145 return res.json();146 },147 enabled: !!selectedTicker,148 refetchInterval: 5000,149 });150151 const { data: historicalPrice } = useQuery({152 queryKey: ['historical-price', selectedTicker],153 queryFn: async () => {154 if (!selectedTicker) return null;155 const res = await fetch(`/api/fmp/historical-price/${selectedTicker}`);156 if (!res.ok) throw new Error('Failed to fetch historical price');157 return res.json();158 },159 enabled: !!selectedTicker,160 });161162 const { data: incomeStatement } = useQuery({163 queryKey: ['income-statement', selectedTicker, statementPeriod],164 queryFn: async () => {165 if (!selectedTicker) return null;166 const res = await fetch(`/api/fmp/income-statement/${selectedTicker}?period=${statementPeriod}&limit=5`);167 if (!res.ok) throw new Error('Failed to fetch income statement');168 return res.json();169 },170 enabled: !!selectedTicker,171 });172173 const { data: balanceSheet } = useQuery({174 queryKey: ['balance-sheet', selectedTicker, statementPeriod],175 queryFn: async () => {176 if (!selectedTicker) return null;177 const res = await fetch(`/api/fmp/balance-sheet/${selectedTicker}?period=${statementPeriod}&limit=5`);178 if (!res.ok) throw new Error('Failed to fetch balance sheet');179 return res.json();180 },181 enabled: !!selectedTicker,182 });183184 const { data: cashFlow } = useQuery({185 queryKey: ['cash-flow', selectedTicker, statementPeriod],186 queryFn: async () => {187 if (!selectedTicker) return null;188 const res = await fetch(`/api/fmp/cash-flow/${selectedTicker}?period=${statementPeriod}&limit=5`);189 if (!res.ok) throw new Error('Failed to fetch cash flow');190 return res.json();191 },192 enabled: !!selectedTicker,193 });194195 const { data: keyMetrics } = useQuery({196 queryKey: ['key-metrics', selectedTicker],197 queryFn: async () => {198 if (!selectedTicker) return null;199 const res = await fetch(`/api/fmp/key-metrics/${selectedTicker}?period=annual&limit=5`);200 if (!res.ok) throw new Error('Failed to fetch key metrics');201 return res.json();202 },203 enabled: !!selectedTicker,204 });205206 const { data: analystEstimates } = useQuery({207 queryKey: ['analyst-estimates', selectedTicker],208 queryFn: async () => {209 if (!selectedTicker) return null;210 const res = await fetch(`/api/fmp/analyst-estimates/${selectedTicker}?period=annual`);211 if (!res.ok) throw new Error('Failed to fetch analyst estimates');212 return res.json();213 },214 enabled: !!selectedTicker,215 });216217 const { data: dividends } = useQuery({218 queryKey: ['dividends', selectedTicker],219 queryFn: async () => {220 if (!selectedTicker) return null;221 const res = await fetch(`/api/fmp/dividend-history/${selectedTicker}`);222 if (!res.ok) throw new Error('Failed to fetch dividends');223 return res.json();224 },225 enabled: !!selectedTicker,226 });227228 const { data: institutionalHolders } = useQuery({229 queryKey: ['institutional-holders', selectedTicker],230 queryFn: async () => {231 if (!selectedTicker) return null;232 const res = await fetch(`/api/fmp/institutional-holders/${selectedTicker}`);233 if (!res.ok) throw new Error('Failed to fetch institutional holders');234 return res.json();235 },236 enabled: !!selectedTicker,237 });238239 const { data: news } = useQuery({240 queryKey: ['news', selectedTicker],241 queryFn: async () => {242 if (!selectedTicker) return null;243 const res = await fetch(`/api/fmp/financial-news/${selectedTicker}?limit=20`);244 if (!res.ok) throw new Error('Failed to fetch news');245 return res.json();246 },247 enabled: !!selectedTicker,248 });249250 const handleSearch = (e: React.FormEvent) => {251 e.preventDefault();252 if (searchQuery.trim()) {253 setSelectedTicker(searchQuery.trim().toUpperCase());254 }255 };256257 const handleTickerClick = (ticker: string) => {258 setSelectedTicker(ticker);259 setSearchQuery(ticker);260 window.scrollTo({ top: 0, behavior: 'smooth' });261 };262263 const handleBack = () => {264 setSelectedTicker(null);265 setSearchQuery("");266 window.scrollTo({ top: 0, behavior: 'smooth' });267 };268269 return (270 <div className="min-h-screen bg-background" data-testid="page-explore">271 {/* Header */}272 <header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur-lg">273 <div className="container flex h-14 items-center justify-between px-4">274 <div className="flex items-center gap-3">275 <Button276 variant="ghost"277 size="sm"278 onClick={() => setLocation('/')}279 className="hover-elevate active-elevate-2 gap-2"280 >281 <ArrowLeft className="w-4 h-4" />282 <span className="hidden sm:inline">Retour</span>283 </Button>284 <button285 onClick={() => setLocation('/')}286 className="flex items-center gap-2 hover:opacity-80 transition-opacity"287 >288 <Sparkles className="w-5 h-5 text-primary" />289 <span className="hidden md:inline text-lg font-semibold bg-gradient-to-r from-primary to-purple-500 bg-clip-text text-transparent leading-tight">290 VQuant291 </span>292 </button>293 </div>294 <div className="flex items-center gap-2">295 <Tooltip>296 <TooltipTrigger asChild>297 <Button298 variant="ghost"299 size="icon"300 onClick={() => setLocation('/docs')}301 className="hover-elevate active-elevate-2"302 >303 <BookOpen className="h-5 w-5" />304 </Button>305 </TooltipTrigger>306 <TooltipContent>307 <p>Documentation</p>308 </TooltipContent>309 </Tooltip>310 <ThemeToggle />311 </div>312 </div>313 </header>314315 {/* Market Ticker Banner */}316 <MarketTicker />317318 {/* Main Content */}319 <main className="container mx-auto px-4 py-8 max-w-[1800px]">320 {/* Search Section */}321 <div className="mb-8 space-y-4">322 <div className="flex items-center gap-3">323 <div className="p-3 rounded-xl bg-primary/10">324 <BarChart3 className="w-6 h-6 text-primary" />325 </div>326 <div>327 <h1 className="text-3xl font-bold">Exploration Boursière</h1>328 <p className="text-muted-foreground">329 Analysez en profondeur n'importe quel titre sur les marchés330 </p>331 </div>332 </div>333334 {/* Search Bar */}335 <form onSubmit={handleSearch} className="relative max-w-2xl">336 <Input337 type="text"338 placeholder="Entrez un symbole boursier (ex: AAPL, TSLA, MSFT)..."339 value={searchQuery}340 onChange={(e) => setSearchQuery(e.target.value.toUpperCase())}341 className="pl-4 pr-12 h-14 text-lg rounded-2xl"342 />343 <button344 type="submit"345 className="absolute right-3 top-1/2 -translate-y-1/2 p-2 rounded-lg hover:bg-primary/10 transition-colors"346 >347 <Search className="w-5 h-5 text-primary" />348 </button>349 </form>350 </div>351352 {/* Market Overview or Stock Dashboard */}353 {!selectedTicker ? (354 <MarketOverview355 marketIndices={marketIndices}356 marketStatus={marketStatus}357 gainers={gainers}358 losers={losers}359 activeStocks={activeStocks}360 onTickerClick={handleTickerClick}361 />362 ) : (363 /* Stock Dashboard */364 <div className="space-y-8">365 {/* Stock Header */}366 <StockHeader367 ticker={selectedTicker}368 companyProfile={companyProfile?.[0]}369 stockQuote={stockQuote?.[0]}370 onBack={handleBack}371 />372373 {/* Price Chart */}374 {historicalPrice && (375 <Card>376 <CardContent className="pt-6">377 <AdvancedPriceChart data={historicalPrice} />378 </CardContent>379 </Card>380 )}381382 {/* Tabs for different sections */}383 <Tabs defaultValue="financials" className="w-full">384 <TabsList className="grid w-full grid-cols-2 lg:grid-cols-5 h-auto">385 <TabsTrigger value="financials" className="gap-2 py-3">386 <FileText className="w-4 h-4" />387 <span className="hidden sm:inline">États Financiers</span>388 <span className="sm:hidden">Financiers</span>389 </TabsTrigger>390 <TabsTrigger value="metrics" className="gap-2 py-3">391 <DollarSign className="w-4 h-4" />392 <span className="hidden sm:inline">Métriques Clés</span>393 <span className="sm:hidden">Métriques</span>394 </TabsTrigger>395 <TabsTrigger value="analysts" className="gap-2 py-3">396 <Award className="w-4 h-4" />397 <span className="hidden sm:inline">Analystes</span>398 <span className="sm:hidden">Analystes</span>399 </TabsTrigger>400 <TabsTrigger value="ownership" className="gap-2 py-3">401 <Users className="w-4 h-4" />402 <span className="hidden sm:inline">Actionnariat</span>403 <span className="sm:hidden">Actions</span>404 </TabsTrigger>405 <TabsTrigger value="news" className="gap-2 py-3">406 <Newspaper className="w-4 h-4" />407 <span className="hidden sm:inline">Actualités</span>408 <span className="sm:hidden">News</span>409 </TabsTrigger>410 </TabsList>411412 {/* Financial Statements Tab */}413 <TabsContent value="financials" className="space-y-6 mt-6">414 <div className="flex items-center justify-between mb-4">415 <div>416 <h3 className="text-2xl font-bold">États Financiers Détaillés</h3>417 <p className="text-muted-foreground">418 Compte de résultat, bilan et flux de trésorerie419 </p>420 </div>421 <div className="flex gap-2">422 <Button423 variant={statementPeriod === 'annual' ? 'default' : 'outline'}424 size="sm"425 onClick={() => setStatementPeriod('annual')}426 >427 Annuel428 </Button>429 <Button430 variant={statementPeriod === 'quarter' ? 'default' : 'outline'}431 size="sm"432 onClick={() => setStatementPeriod('quarter')}433 >434 Trimestriel435 </Button>436 </div>437 </div>438 <FinancialStatementsView439 incomeStatement={incomeStatement}440 balanceSheet={balanceSheet}441 cashFlow={cashFlow}442 period={statementPeriod}443 />444 </TabsContent>445446 {/* Key Metrics Tab */}447 <TabsContent value="metrics" className="mt-6">448 <div className="mb-6">449 <h3 className="text-2xl font-bold">Métriques et Ratios Financiers</h3>450 <p className="text-muted-foreground">451 Analyse approfondie des ratios de valorisation, rentabilité et efficacité452 </p>453 </div>454 <MetricsView keyMetrics={keyMetrics} />455 </TabsContent>456457 {/* Analysts Tab */}458 <TabsContent value="analysts" className="mt-6">459 <div className="mb-6">460 <h3 className="text-2xl font-bold">Estimations et Dividendes</h3>461 <p className="text-muted-foreground">462 Prévisions des analystes et historique des dividendes463 </p>464 </div>465 <AnalystView466 analystEstimates={analystEstimates}467 dividends={dividends}468 />469 </TabsContent>470471 {/* Ownership Tab */}472 <TabsContent value="ownership" className="mt-6">473 <div className="mb-6">474 <h3 className="text-2xl font-bold">Structure Actionnariale</h3>475 <p className="text-muted-foreground">476 Actionnaires institutionnels et changements de position477 </p>478 </div>479 <OwnershipView institutionalHolders={institutionalHolders} />480 </TabsContent>481482 {/* News Tab */}483 <TabsContent value="news" className="mt-6">484 <NewsView news={news} />485 </TabsContent>486 </Tabs>487 </div>488 )}489 </main>490 </div>491 );492}493