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