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