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/analyst-view.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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";20import { TrendingUp, TrendingDown, Award, Target, DollarSign, Users } from "lucide-react";2122interface AnalystEstimate {23 date: string;24 symbol: string;25 estimatedRevenueAvg: number;26 estimatedRevenueLow: number;27 estimatedRevenueHigh: number;28 estimatedEbitdaAvg: number;29 estimatedEbitdaLow: number;30 estimatedEbitdaHigh: number;31 estimatedEbitAvg: number;32 estimatedEbitLow: number;33 estimatedEbitHigh: number;34 estimatedNetIncomeAvg: number;35 estimatedNetIncomeLow: number;36 estimatedNetIncomeHigh: number;37 estimatedSgaExpenseAvg: number;38 estimatedSgaExpenseLow: number;39 estimatedSgaExpenseHigh: number;40 estimatedEpsAvg: number;41 estimatedEpsLow: number;42 estimatedEpsHigh: number;43 numberAnalystEstimatedRevenue: number;44 numberAnalystsEstimatedEps: number;45}4647interface DividendData {48 date: string;49 label: string;50 adjDividend: number;51 dividend: number;52 recordDate: string;53 paymentDate: string;54 declarationDate: string;55}5657interface AnalystViewProps {58 analystEstimates?: AnalystEstimate[];59 dividends?: DividendData[];60}6162export function AnalystView({ analystEstimates, dividends }: AnalystViewProps) {63 const formatCurrency = (value: number) => {64 if (!value) return "N/A";65 const absValue = Math.abs(value);66 if (absValue >= 1e12) return `$${(value / 1e12).toFixed(2)}T`;67 if (absValue >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;68 if (absValue >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;69 if (absValue >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;70 return `$${value.toFixed(2)}`;71 };7273 const formatDate = (dateString: string) => {74 return new Date(dateString).toLocaleDateString('fr-FR', {75 year: 'numeric',76 month: 'short',77 day: 'numeric'78 });79 };8081 const estimateRows = [82 {83 label: "Revenu",84 avgKey: "estimatedRevenueAvg",85 lowKey: "estimatedRevenueLow",86 highKey: "estimatedRevenueHigh",87 analystKey: "numberAnalystEstimatedRevenue",88 isCurrency: true89 },90 {91 label: "EBITDA",92 avgKey: "estimatedEbitdaAvg",93 lowKey: "estimatedEbitdaLow",94 highKey: "estimatedEbitdaHigh",95 isCurrency: true96 },97 {98 label: "EBIT",99 avgKey: "estimatedEbitAvg",100 lowKey: "estimatedEbitLow",101 highKey: "estimatedEbitHigh",102 isCurrency: true103 },104 {105 label: "Revenu Net",106 avgKey: "estimatedNetIncomeAvg",107 lowKey: "estimatedNetIncomeLow",108 highKey: "estimatedNetIncomeHigh",109 isCurrency: true110 },111 {112 label: "Dépenses SG&A",113 avgKey: "estimatedSgaExpenseAvg",114 lowKey: "estimatedSgaExpenseLow",115 highKey: "estimatedSgaExpenseHigh",116 isCurrency: true117 },118 {119 label: "BPA",120 avgKey: "estimatedEpsAvg",121 lowKey: "estimatedEpsLow",122 highKey: "estimatedEpsHigh",123 analystKey: "numberAnalystsEstimatedEps",124 isEPS: true125 },126 ];127128 return (129 <div className="space-y-6">130 {/* Analyst Estimates */}131 {analystEstimates && analystEstimates.length > 0 ? (132 <Card>133 <CardHeader>134 <div className="flex items-center justify-between">135 <div>136 <CardTitle className="flex items-center gap-2">137 <Award className="w-5 h-5" />138 Estimations des Analystes139 </CardTitle>140 <CardDescription>141 Prévisions consensuelles et fourchettes d'estimations142 </CardDescription>143 </div>144 <div className="flex items-center gap-2">145 <Users className="w-4 h-4 text-muted-foreground" />146 <span className="text-sm text-muted-foreground">147 {analystEstimates[0]?.numberAnalystsEstimatedEps || 'N/A'} analystes148 </span>149 </div>150 </div>151 </CardHeader>152 <CardContent>153 <div className="overflow-x-auto">154 <Table>155 <TableHeader>156 <TableRow>157 <TableHead className="w-[200px]">Métrique</TableHead>158 {analystEstimates.slice(0, 4).map((item, idx) => (159 <TableHead key={idx} className="text-center min-w-[200px]">160 <div>161 <div className="font-semibold">{formatDate(item.date)}</div>162 {item.numberAnalystEstimatedRevenue > 0 && (163 <div className="text-xs text-muted-foreground mt-1">164 {item.numberAnalystEstimatedRevenue} analystes165 </div>166 )}167 </div>168 </TableHead>169 ))}170 </TableRow>171 </TableHeader>172 <TableBody>173 {estimateRows.map((row, rowIdx) => (174 <TableRow key={rowIdx} className="hover:bg-muted/50">175 <TableCell className="font-medium">176 <div className="flex items-center gap-2">177 <Target className="w-4 h-4 text-muted-foreground" />178 {row.label}179 </div>180 </TableCell>181 {analystEstimates.slice(0, 4).map((item, idx) => {182 const avg = item[row.avgKey];183 const low = item[row.lowKey];184 const high = item[row.highKey];185186 return (187 <TableCell key={idx} className="text-center">188 <div className="space-y-1">189 <div className="font-bold text-lg">190 {row.isCurrency ? formatCurrency(avg) : row.isEPS ? `$${avg?.toFixed(2) || 'N/A'}` : avg || 'N/A'}191 </div>192 <div className="text-xs text-muted-foreground">193 Fourchette:194 </div>195 <div className="flex items-center justify-center gap-2 text-sm">196 <span className="text-red-500 font-semibold">197 {row.isCurrency ? formatCurrency(low) : row.isEPS ? `$${low?.toFixed(2) || 'N/A'}` : low || 'N/A'}198 </span>199 <span className="text-muted-foreground">-</span>200 <span className="text-green-500 font-semibold">201 {row.isCurrency ? formatCurrency(high) : row.isEPS ? `$${high?.toFixed(2) || 'N/A'}` : high || 'N/A'}202 </span>203 </div>204 </div>205 </TableCell>206 );207 })}208 </TableRow>209 ))}210 </TableBody>211 </Table>212 </div>213 </CardContent>214 </Card>215 ) : (216 <Card>217 <CardHeader>218 <CardTitle className="flex items-center gap-2">219 <Award className="w-5 h-5" />220 Estimations des Analystes221 </CardTitle>222 </CardHeader>223 <CardContent>224 <p className="text-muted-foreground">Aucune estimation d'analyste disponible</p>225 </CardContent>226 </Card>227 )}228229 {/* Dividend History */}230 {dividends && dividends.length > 0 ? (231 <Card>232 <CardHeader>233 <CardTitle className="flex items-center gap-2">234 <DollarSign className="w-5 h-5" />235 Historique des Dividendes236 </CardTitle>237 <CardDescription>238 Paiements de dividendes et dates importantes239 </CardDescription>240 </CardHeader>241 <CardContent>242 <div className="overflow-x-auto">243 <Table>244 <TableHeader>245 <TableRow>246 <TableHead>Date Ex-Dividend</TableHead>247 <TableHead>Libellé</TableHead>248 <TableHead className="text-right">Dividende</TableHead>249 <TableHead className="text-right">Dividende Ajusté</TableHead>250 <TableHead>Date d'Enregistrement</TableHead>251 <TableHead>Date de Paiement</TableHead>252 <TableHead>Date de Déclaration</TableHead>253 </TableRow>254 </TableHeader>255 <TableBody>256 {dividends.slice(0, 20).map((dividend, idx) => (257 <TableRow key={idx} className="hover:bg-muted/50">258 <TableCell className="font-semibold">259 {formatDate(dividend.date)}260 </TableCell>261 <TableCell>262 <Badge variant="outline">{dividend.label}</Badge>263 </TableCell>264 <TableCell className="text-right font-semibold text-green-600">265 ${dividend.dividend?.toFixed(4)}266 </TableCell>267 <TableCell className="text-right">268 ${dividend.adjDividend?.toFixed(4)}269 </TableCell>270 <TableCell className="text-sm text-muted-foreground">271 {dividend.recordDate ? formatDate(dividend.recordDate) : 'N/A'}272 </TableCell>273 <TableCell className="text-sm text-muted-foreground">274 {dividend.paymentDate ? formatDate(dividend.paymentDate) : 'N/A'}275 </TableCell>276 <TableCell className="text-sm text-muted-foreground">277 {dividend.declarationDate ? formatDate(dividend.declarationDate) : 'N/A'}278 </TableCell>279 </TableRow>280 ))}281 </TableBody>282 </Table>283 </div>284285 {dividends.length > 0 && (286 <div className="mt-4 pt-4 border-t">287 <div className="grid grid-cols-3 gap-4 text-sm">288 <div>289 <div className="text-muted-foreground">Total Paiements</div>290 <div className="text-lg font-bold">{dividends.length}</div>291 </div>292 <div>293 <div className="text-muted-foreground">Dernier Dividende</div>294 <div className="text-lg font-bold text-green-600">295 ${dividends[0]?.dividend?.toFixed(4)}296 </div>297 </div>298 <div>299 <div className="text-muted-foreground">Dividende Annuel (TTM)</div>300 <div className="text-lg font-bold">301 ${dividends.slice(0, 4).reduce((sum, d) => sum + (d.dividend || 0), 0).toFixed(4)}302 </div>303 </div>304 </div>305 </div>306 )}307 </CardContent>308 </Card>309 ) : (310 <Card>311 <CardHeader>312 <CardTitle className="flex items-center gap-2">313 <DollarSign className="w-5 h-5" />314 Historique des Dividendes315 </CardTitle>316 </CardHeader>317 <CardContent>318 <p className="text-muted-foreground">Cette entreprise ne verse pas de dividendes</p>319 </CardContent>320 </Card>321 )}322 </div>323 );324}325