SPB Git

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%
18.6 KB · 493 lines tsx
Raw Blame History
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