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/components/explore/market-overview.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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";18import { Badge } from "@/components/ui/badge";19import { TrendingUp, TrendingDown, Activity, Clock, DollarSign, BarChart3 } from "lucide-react";2021interface MarketIndex {22 symbol: string;23 name: string;24 price: number;25 change: number;26 changesPercentage: number;27 dayLow: number;28 dayHigh: number;29 yearHigh: number;30 yearLow: number;31 volume: number;32 previousClose: number;33}3435interface MarketStatus {36 isTheStockMarketOpen: boolean;37 stockMarketHours: {38 openingHour: string;39 closingHour: string;40 };41}4243interface Stock {44 symbol: string;45 name: string;46 price: number;47 changesPercentage: number;48 change: number;49 volume?: number;50 marketCap?: number;51}5253interface MarketOverviewProps {54 marketIndices?: MarketIndex[];55 marketStatus?: MarketStatus;56 gainers?: Stock[];57 losers?: Stock[];58 activeStocks?: Stock[];59 onTickerClick: (ticker: string) => void;60}6162export function MarketOverview({63 marketIndices,64 marketStatus,65 gainers,66 losers,67 activeStocks,68 onTickerClick69}: MarketOverviewProps) {70 const formatNumber = (num: number) => {71 if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;72 if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;73 if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;74 return `$${num.toFixed(2)}`;75 };7677 const formatVolume = (vol: number) => {78 if (vol >= 1e9) return `${(vol / 1e9).toFixed(2)}B`;79 if (vol >= 1e6) return `${(vol / 1e6).toFixed(2)}M`;80 if (vol >= 1e3) return `${(vol / 1e3).toFixed(2)}K`;81 return vol.toString();82 };8384 return (85 <div className="space-y-8">86 {/* Market Status Banner */}87 {marketStatus && (88 <Card className="bg-gradient-to-br from-card via-card to-primary/5 border-2">89 <CardHeader>90 <div className="flex items-center justify-between">91 <div className="flex items-center gap-3">92 <div className="p-2 rounded-lg bg-primary/10">93 <Clock className="w-6 h-6 text-primary" />94 </div>95 <div>96 <CardTitle className="text-2xl">Statut du Marché</CardTitle>97 <CardDescription className="text-base">98 Heures d'ouverture: {marketStatus.stockMarketHours?.openingHour} - {marketStatus.stockMarketHours?.closingHour}99 </CardDescription>100 </div>101 </div>102 <Badge103 variant={marketStatus.isTheStockMarketOpen ? "default" : "secondary"}104 className="text-lg px-6 py-3"105 >106 {marketStatus.isTheStockMarketOpen ? "🟢 OUVERT" : "🔴 FERMÉ"}107 </Badge>108 </div>109 </CardHeader>110 </Card>111 )}112113 {/* Major Indices */}114 <div>115 <div className="flex items-center gap-3 mb-6">116 <div className="p-2 rounded-lg bg-primary/10">117 <Activity className="w-6 h-6 text-primary" />118 </div>119 <div>120 <h2 className="text-2xl font-bold">Indices Majeurs</h2>121 <p className="text-muted-foreground">Performance en temps réel des principaux indices boursiers</p>122 </div>123 </div>124125 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">126 {marketIndices?.map((index) => {127 const isPositive = index.changesPercentage >= 0;128129 return (130 <Card131 key={index.symbol}132 className="cursor-pointer hover:border-primary/50 hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"133 onClick={() => onTickerClick(index.symbol)}134 >135 <CardHeader className="pb-3">136 <div className="flex items-start justify-between">137 <div>138 <CardTitle className="text-xl mb-1">{index.name}</CardTitle>139 <CardDescription className="text-base font-mono">{index.symbol}</CardDescription>140 </div>141 <div className={`p-2 rounded-lg ${isPositive ? 'bg-green-500/10' : 'bg-red-500/10'}`}>142 {isPositive ? (143 <TrendingUp className="w-5 h-5 text-green-500" />144 ) : (145 <TrendingDown className="w-5 h-5 text-red-500" />146 )}147 </div>148 </div>149 </CardHeader>150 <CardContent className="space-y-4">151 <div className="flex items-end justify-between">152 <div>153 <div className="text-3xl font-bold">154 {index.price?.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}155 </div>156 <div className={`text-base font-semibold flex items-center gap-1.5 mt-1 ${157 isPositive ? 'text-green-500' : 'text-red-500'158 }`}>159 {isPositive ? '+' : ''}{index.change?.toFixed(2)}160 <span className="text-sm">({isPositive ? '+' : ''}{index.changesPercentage?.toFixed(2)}%)</span>161 </div>162 </div>163 </div>164165 <div className="pt-3 border-t grid grid-cols-2 gap-3 text-sm">166 <div>167 <div className="text-muted-foreground text-xs">Jour Bas/Haut</div>168 <div className="font-semibold">169 {index.dayLow > 0 ? `${index.dayLow.toFixed(2)} / ${index.dayHigh.toFixed(2)}` : 'N/A'}170 </div>171 </div>172 <div>173 <div className="text-muted-foreground text-xs">Année Bas/Haut</div>174 <div className="font-semibold">175 {index.yearLow > 0 ? `${index.yearLow.toFixed(2)} / ${index.yearHigh.toFixed(2)}` : 'N/A'}176 </div>177 </div>178 <div>179 <div className="text-muted-foreground text-xs">Clôture Précédente</div>180 <div className="font-semibold">181 {index.previousClose > 0 ? index.previousClose.toFixed(2) : 'N/A'}182 </div>183 </div>184 <div>185 <div className="text-muted-foreground text-xs">Volume</div>186 <div className="font-semibold">187 {index.volume > 0 ? formatVolume(index.volume) : 'N/A'}188 </div>189 </div>190 </div>191 </CardContent>192 </Card>193 );194 })}195 </div>196 </div>197198 {/* Market Movers */}199 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">200 {/* Top Gainers */}201 <Card className="border-2 hover:border-green-500/50 transition-colors">202 <CardHeader>203 <div className="flex items-center gap-3">204 <div className="p-2 rounded-lg bg-green-500/10">205 <TrendingUp className="w-5 h-5 text-green-500" />206 </div>207 <div>208 <CardTitle className="text-xl text-green-500">Top Hausses</CardTitle>209 <CardDescription>Actions les plus performantes</CardDescription>210 </div>211 </div>212 </CardHeader>213 <CardContent>214 <div className="space-y-3">215 {gainers?.slice(0, 8).map((stock, index) => (216 <div217 key={stock.symbol}218 className="flex items-center justify-between p-3 rounded-lg hover:bg-accent cursor-pointer transition-all hover:shadow-md border border-transparent hover:border-green-500/30"219 onClick={() => onTickerClick(stock.symbol)}220 >221 <div className="flex items-center gap-3 flex-1 min-w-0">222 <div className="flex-shrink-0 w-6 h-6 rounded-full bg-green-500/10 flex items-center justify-center text-xs font-bold text-green-500">223 {index + 1}224 </div>225 <div className="min-w-0 flex-1">226 <div className="font-bold text-base">{stock.symbol}</div>227 <div className="text-xs text-muted-foreground truncate">228 {stock.name}229 </div>230 {stock.volume && (231 <div className="text-xs text-muted-foreground">232 Vol: {formatVolume(stock.volume)}233 </div>234 )}235 </div>236 </div>237 <div className="text-right flex-shrink-0 ml-2">238 <div className="font-bold text-base">${stock.price?.toFixed(2)}</div>239 <div className="text-sm font-semibold text-green-500">240 +{stock.changesPercentage?.toFixed(2)}%241 </div>242 <div className="text-xs text-green-500">243 +${stock.change?.toFixed(2)}244 </div>245 </div>246 </div>247 ))}248 </div>249 </CardContent>250 </Card>251252 {/* Top Losers */}253 <Card className="border-2 hover:border-red-500/50 transition-colors">254 <CardHeader>255 <div className="flex items-center gap-3">256 <div className="p-2 rounded-lg bg-red-500/10">257 <TrendingDown className="w-5 h-5 text-red-500" />258 </div>259 <div>260 <CardTitle className="text-xl text-red-500">Top Baisses</CardTitle>261 <CardDescription>Actions les plus en baisse</CardDescription>262 </div>263 </div>264 </CardHeader>265 <CardContent>266 <div className="space-y-3">267 {losers?.slice(0, 8).map((stock, index) => (268 <div269 key={stock.symbol}270 className="flex items-center justify-between p-3 rounded-lg hover:bg-accent cursor-pointer transition-all hover:shadow-md border border-transparent hover:border-red-500/30"271 onClick={() => onTickerClick(stock.symbol)}272 >273 <div className="flex items-center gap-3 flex-1 min-w-0">274 <div className="flex-shrink-0 w-6 h-6 rounded-full bg-red-500/10 flex items-center justify-center text-xs font-bold text-red-500">275 {index + 1}276 </div>277 <div className="min-w-0 flex-1">278 <div className="font-bold text-base">{stock.symbol}</div>279 <div className="text-xs text-muted-foreground truncate">280 {stock.name}281 </div>282 {stock.volume && (283 <div className="text-xs text-muted-foreground">284 Vol: {formatVolume(stock.volume)}285 </div>286 )}287 </div>288 </div>289 <div className="text-right flex-shrink-0 ml-2">290 <div className="font-bold text-base">${stock.price?.toFixed(2)}</div>291 <div className="text-sm font-semibold text-red-500">292 {stock.changesPercentage?.toFixed(2)}%293 </div>294 <div className="text-xs text-red-500">295 ${stock.change?.toFixed(2)}296 </div>297 </div>298 </div>299 ))}300 </div>301 </CardContent>302 </Card>303304 {/* Most Active */}305 <Card className="border-2 hover:border-blue-500/50 transition-colors">306 <CardHeader>307 <div className="flex items-center gap-3">308 <div className="p-2 rounded-lg bg-blue-500/10">309 <Activity className="w-5 h-5 text-blue-500" />310 </div>311 <div>312 <CardTitle className="text-xl text-blue-500">Plus Actifs</CardTitle>313 <CardDescription>Volume de transactions le plus élevé</CardDescription>314 </div>315 </div>316 </CardHeader>317 <CardContent>318 <div className="space-y-3">319 {activeStocks?.slice(0, 8).map((stock, index) => {320 const isPositive = stock.changesPercentage >= 0;321 return (322 <div323 key={stock.symbol}324 className="flex items-center justify-between p-3 rounded-lg hover:bg-accent cursor-pointer transition-all hover:shadow-md border border-transparent hover:border-blue-500/30"325 onClick={() => onTickerClick(stock.symbol)}326 >327 <div className="flex items-center gap-3 flex-1 min-w-0">328 <div className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-500/10 flex items-center justify-center text-xs font-bold text-blue-500">329 {index + 1}330 </div>331 <div className="min-w-0 flex-1">332 <div className="font-bold text-base">{stock.symbol}</div>333 <div className="text-xs text-muted-foreground truncate">334 {stock.name}335 </div>336 {stock.volume && (337 <div className="text-xs text-blue-500 font-semibold">338 Vol: {formatVolume(stock.volume)}339 </div>340 )}341 </div>342 </div>343 <div className="text-right flex-shrink-0 ml-2">344 <div className="font-bold text-base">${stock.price?.toFixed(2)}</div>345 <div className={`text-sm font-semibold ${isPositive ? 'text-green-500' : 'text-red-500'}`}>346 {isPositive ? '+' : ''}{stock.changesPercentage?.toFixed(2)}%347 </div>348 <div className={`text-xs ${isPositive ? 'text-green-500' : 'text-red-500'}`}>349 {isPositive ? '+' : ''}${stock.change?.toFixed(2)}350 </div>351 </div>352 </div>353 );354 })}355 </div>356 </CardContent>357 </Card>358 </div>359 </div>360 );361}362