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%
4.3 KB · 132 lines tsx
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      client/src/components/market-ticker.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 { useQuery } from "@tanstack/react-query";18import { TrendingUp, TrendingDown } from "lucide-react";1920interface MarketIndex {21  symbol: string;22  name: string;23  price: number;24  change: number;25  changesPercentage: number;26}2728export function MarketTicker() {29  const { data: marketIndices } = useQuery({30    queryKey: ['market-ticker'],31    queryFn: async () => {32      const symbols = [33        '^GSPC',      // S&P 50034        '^DJI',       // Dow Jones35        '^IXIC',      // NASDAQ36        '^RUT',       // Russell 200037        '^FTSE',      // FTSE 10038        '^GDAXI',     // DAX39        '^FCHI',      // CAC 4040        '^N225',      // Nikkei 22541        '^HSI',       // Hang Seng42        '^GSPTSE'     // S&P/TSX43      ];4445      const symbolsParam = symbols.join(',');46      const res = await fetch(`https://financialmodelingprep.com/stable/quote?symbol=${symbolsParam}&apikey=${import.meta.env.VITE_FMP_API_KEY || 'JeWwQMjWS3H6hBGHaVxKy1WfGyU5ZeFq'}`);4748      if (!res.ok) throw new Error('Failed to fetch market indices');49      const data = await res.json();5051      return data52        .filter((index: any) => index && index.symbol && index.price != null)53        .map((index: any) => ({54          symbol: index.symbol,55          name: index.name || index.symbol,56          price: Number(index.price) || 0,57          change: Number(index.change) || 0,58          changesPercentage: Number(index.changesPercentage) || 0,59        }));60    },61    refetchInterval: 30000, // Refresh every 30 seconds62  });6364  if (!marketIndices || marketIndices.length === 0) {65    return null;66  }6768  // Duplicate the data to create seamless loop69  const tickerData = [...marketIndices, ...marketIndices];7071  return (72    <div className="w-full bg-muted/30 border-b border-border/40 overflow-hidden backdrop-blur-sm">73      <div className="relative h-10 flex items-center">74        <div className="animate-ticker flex items-center gap-8 whitespace-nowrap">75          {tickerData.map((index: MarketIndex, i) => {76            const isPositive = index.changesPercentage >= 0;77            const displaySymbol = index.symbol.replace('^', '');7879            return (80              <div81                key={`${index.symbol}-${i}`}82                className="flex items-center gap-2 px-4 py-1 rounded-md bg-card/50 backdrop-blur-sm border border-border/30"83              >84                <span className="font-bold text-sm text-primary">85                  {displaySymbol}86                </span>87                <span className="text-sm font-semibold text-foreground">88                  {index.price.toLocaleString('en-US', {89                    minimumFractionDigits: 2,90                    maximumFractionDigits: 291                  })}92                </span>93                <div className={`flex items-center gap-1 text-xs font-semibold ${94                  isPositive ? 'text-green-500' : 'text-red-500'95                }`}>96                  {isPositive ? (97                    <TrendingUp className="w-3 h-3" />98                  ) : (99                    <TrendingDown className="w-3 h-3" />100                  )}101                  <span>102                    {isPositive ? '+' : ''}{index.changesPercentage.toFixed(2)}%103                  </span>104                </div>105              </div>106            );107          })}108        </div>109      </div>110111      <style>{`112        @keyframes ticker {113          0% {114            transform: translateX(0);115          }116          100% {117            transform: translateX(-50%);118          }119        }120121        .animate-ticker {122          animation: ticker 60s linear infinite;123        }124125        .animate-ticker:hover {126          animation-play-state: paused;127        }128      `}</style>129    </div>130  );131}132