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%
19.5 KB · 536 lines tsx
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      client/src/components/financial/advanced-price-chart.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 { Button } from "@/components/ui/button";20import { TrendingUp, TrendingDown, Calendar, BarChart3, LineChart as LineChartIcon } from "lucide-react";21import { useState } from "react";22import {23  ComposedChart,24  Line,25  Bar,26  XAxis,27  YAxis,28  CartesianGrid,29  Tooltip,30  ResponsiveContainer,31  Area,32  ReferenceLine,33  Legend,34  Brush,35} from "recharts";3637interface HistoricalPrice {38  date: string;39  open: number;40  high: number;41  low: number;42  close: number;43  adjClose: number;44  volume: number;45  unadjustedVolume: number;46  change: number;47  changePercent: number;48  vwap: number;49  label: string;50  changeOverTime: number;51}5253interface AdvancedPriceChartProps {54  data: {55    symbol: string;56    historical: HistoricalPrice[];57  };58}5960export function AdvancedPriceChart({ data }: AdvancedPriceChartProps) {61  const [chartType, setChartType] = useState<'area' | 'line'>('area');62  const [showVolume, setShowVolume] = useState(true);63  const [showMA, setShowMA] = useState(true);64  const [timeRange, setTimeRange] = useState<'1M' | '3M' | '6M' | 'YTD' | '1Y' | 'ALL'>('3M');6566  if (!data || !data.historical || data.historical.length === 0) {67    return (68      <Card className="w-full">69        <CardHeader>70          <CardTitle>Advanced Price Chart</CardTitle>71          <CardDescription>No price data available</CardDescription>72        </CardHeader>73      </Card>74    );75  }7677  const symbol = data.symbol;78  const allData = [...data.historical].reverse();7980  // Filter data based on time range81  const filterDataByRange = () => {82    const now = new Date();83    let startDate = new Date();8485    switch (timeRange) {86      case '1M':87        startDate.setMonth(now.getMonth() - 1);88        break;89      case '3M':90        startDate.setMonth(now.getMonth() - 3);91        break;92      case '6M':93        startDate.setMonth(now.getMonth() - 6);94        break;95      case 'YTD':96        startDate = new Date(now.getFullYear(), 0, 1);97        break;98      case '1Y':99        startDate.setFullYear(now.getFullYear() - 1);100        break;101      case 'ALL':102        return allData;103    }104105    return allData.filter(item => new Date(item.date) >= startDate);106  };107108  const filteredData = filterDataByRange();109110  // Calculate moving averages111  const calculateMA = (period: number) => {112    return filteredData.map((item, index) => {113      if (index < period - 1) return null;114      const sum = filteredData115        .slice(index - period + 1, index + 1)116        .reduce((acc, curr) => acc + curr.close, 0);117      return sum / period;118    });119  };120121  const calculateEMA = (period: number) => {122    const multiplier = 2 / (period + 1);123    const ema = [];124125    // Start with SMA126    let sum = 0;127    for (let i = 0; i < period; i++) {128      if (i >= filteredData.length) break;129      sum += filteredData[i].close;130      ema.push(i === period - 1 ? sum / period : null);131    }132133    // Calculate EMA134    for (let i = period; i < filteredData.length; i++) {135      const prevEMA = ema[i - 1] || filteredData[i - 1].close;136      ema.push(filteredData[i].close * multiplier + prevEMA * (1 - multiplier));137    }138139    return ema;140  };141142  const ma20 = calculateMA(20);143  const ma50 = calculateMA(50);144  const ema12 = calculateEMA(12);145  const ema26 = calculateEMA(26);146147  const enrichedData = filteredData.map((item, index) => ({148    ...item,149    dateFormatted: new Date(item.date).toLocaleDateString('en-US', {150      month: 'short',151      day: 'numeric',152      year: '2-digit'153    }),154    ma20: ma20[index],155    ma50: ma50[index],156    ema12: ema12[index],157    ema26: ema26[index],158    isGreen: item.close >= item.open,159  }));160161  const firstPrice = enrichedData[0]?.close || 0;162  const lastPrice = enrichedData[enrichedData.length - 1]?.close || 0;163  const priceChange = lastPrice - firstPrice;164  const priceChangePercent = (priceChange / firstPrice) * 100;165  const isPositive = priceChange >= 0;166167  const minPrice = Math.min(...enrichedData.map(d => d.low));168  const maxPrice = Math.max(...enrichedData.map(d => d.high));169  const avgVolume = enrichedData.reduce((sum, d) => sum + d.volume, 0) / enrichedData.length;170171  const high52w = Math.max(...allData.slice(-252).map(d => d.high));172  const low52w = Math.min(...allData.slice(-252).map(d => d.low));173174  const CustomTooltip = ({ active, payload }: any) => {175    if (active && payload && payload.length) {176      const data = payload[0].payload;177      return (178        <div className="bg-background/95 backdrop-blur border rounded-lg p-4 shadow-lg">179          <div className="text-sm font-semibold mb-2">{data.label}</div>180          <div className="space-y-1.5 text-sm">181            <div className="flex justify-between gap-6">182              <span className="text-muted-foreground">Open:</span>183              <span className="font-mono font-semibold">${data.open?.toFixed(2)}</span>184            </div>185            <div className="flex justify-between gap-6">186              <span className="text-muted-foreground">High:</span>187              <span className="font-mono font-semibold text-green-600">${data.high?.toFixed(2)}</span>188            </div>189            <div className="flex justify-between gap-6">190              <span className="text-muted-foreground">Low:</span>191              <span className="font-mono font-semibold text-red-600">${data.low?.toFixed(2)}</span>192            </div>193            <div className="flex justify-between gap-6">194              <span className="text-muted-foreground">Close:</span>195              <span className="font-mono font-semibold">${data.close?.toFixed(2)}</span>196            </div>197            <div className="border-t pt-1.5 flex justify-between gap-6">198              <span className="text-muted-foreground">Change:</span>199              <span className={`font-mono ${data.changePercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>200                {data.changePercent >= 0 ? '+' : ''}{data.changePercent?.toFixed(2)}%201              </span>202            </div>203            <div className="flex justify-between gap-6">204              <span className="text-muted-foreground">Volume:</span>205              <span className="font-mono">{(data.volume / 1_000_000).toFixed(2)}M</span>206            </div>207            {showMA && (208              <>209                {data.ma20 && (210                  <div className="flex justify-between gap-6">211                    <span className="text-muted-foreground">MA(20):</span>212                    <span className="font-mono text-blue-600">${data.ma20?.toFixed(2)}</span>213                  </div>214                )}215                {data.ma50 && (216                  <div className="flex justify-between gap-6">217                    <span className="text-muted-foreground">MA(50):</span>218                    <span className="font-mono text-purple-600">${data.ma50?.toFixed(2)}</span>219                  </div>220                )}221              </>222            )}223          </div>224        </div>225      );226    }227    return null;228  };229230  return (231    <Card className="w-full">232      <CardHeader>233        <div className="flex flex-col md:flex-row md:items-start justify-between gap-4">234          <div>235            <div className="flex items-center gap-2">236              <BarChart3 className="h-5 w-5 text-primary" />237              <CardTitle className="text-2xl">{symbol} - Historical Price Chart</CardTitle>238            </div>239            <CardDescription className="mt-1">240              Advanced charting with technical indicators · {enrichedData.length} trading days241            </CardDescription>242          </div>243244          <div className="flex flex-col items-end gap-2">245            <div className="text-right">246              <div className="text-3xl font-bold">${lastPrice.toFixed(2)}</div>247              <div className={`flex items-center gap-1 justify-end text-sm ${isPositive ? 'text-green-600' : 'text-red-600'}`}>248                {isPositive ? <TrendingUp className="h-4 w-4" /> : <TrendingDown className="h-4 w-4" />}249                <span className="font-semibold">250                  {isPositive ? '+' : ''}{priceChange.toFixed(2)} ({isPositive ? '+' : ''}{priceChangePercent.toFixed(2)}%)251                </span>252              </div>253            </div>254          </div>255        </div>256257        {/* Controls */}258        <div className="flex flex-wrap gap-2 mt-4">259          {/* Time Range Selector */}260          <div className="flex gap-1 border rounded-lg p-1">261            {(['1M', '3M', '6M', 'YTD', '1Y', 'ALL'] as const).map((range) => (262              <Button263                key={range}264                variant={timeRange === range ? 'default' : 'ghost'}265                size="sm"266                onClick={() => setTimeRange(range)}267              >268                {range}269              </Button>270            ))}271          </div>272273          {/* Chart Type Selector */}274          <div className="flex gap-1 border rounded-lg p-1">275            <Button276              variant={chartType === 'area' ? 'default' : 'ghost'}277              size="sm"278              onClick={() => setChartType('area')}279            >280              <LineChartIcon className="h-4 w-4 mr-1" />281              Area282            </Button>283            <Button284              variant={chartType === 'line' ? 'default' : 'ghost'}285              size="sm"286              onClick={() => setChartType('line')}287            >288              Line289            </Button>290          </div>291292          <Button293            variant={showVolume ? 'default' : 'outline'}294            size="sm"295            onClick={() => setShowVolume(!showVolume)}296          >297            Volume298          </Button>299300          <Button301            variant={showMA ? 'default' : 'outline'}302            size="sm"303            onClick={() => setShowMA(!showMA)}304          >305            Indicators306          </Button>307        </div>308      </CardHeader>309310      <CardContent className="space-y-6">311        {/* Key Stats */}312        <div className="grid grid-cols-2 md:grid-cols-5 gap-3">313          <div className="p-3 border rounded-lg bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-950 dark:to-blue-900">314            <div className="text-xs text-muted-foreground mb-1">Period High</div>315            <div className="text-lg font-bold text-blue-700 dark:text-blue-400">${maxPrice.toFixed(2)}</div>316          </div>317          <div className="p-3 border rounded-lg bg-gradient-to-br from-red-50 to-red-100 dark:from-red-950 dark:to-red-900">318            <div className="text-xs text-muted-foreground mb-1">Period Low</div>319            <div className="text-lg font-bold text-red-700 dark:text-red-400">${minPrice.toFixed(2)}</div>320          </div>321          <div className="p-3 border rounded-lg bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900">322            <div className="text-xs text-muted-foreground mb-1">52W High</div>323            <div className="text-lg font-bold text-green-700 dark:text-green-400">${high52w.toFixed(2)}</div>324          </div>325          <div className="p-3 border rounded-lg bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-950 dark:to-orange-900">326            <div className="text-xs text-muted-foreground mb-1">52W Low</div>327            <div className="text-lg font-bold text-orange-700 dark:text-orange-400">${low52w.toFixed(2)}</div>328          </div>329          <div className="p-3 border rounded-lg bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-950 dark:to-purple-900">330            <div className="text-xs text-muted-foreground mb-1">Avg Volume</div>331            <div className="text-lg font-bold text-purple-700 dark:text-purple-400">332              {(avgVolume / 1_000_000).toFixed(1)}M333            </div>334          </div>335        </div>336337        {/* Main Price Chart */}338        <div className="h-[500px] w-full">339          <ResponsiveContainer width="100%" height="100%">340            <ComposedChart data={enrichedData} margin={{ top: 10, right: 10, left: 0, bottom: 20 }}>341              <defs>342                <linearGradient id="colorPriceArea" x1="0" y1="0" x2="0" y2="1">343                  <stop offset="5%" stopColor={isPositive ? "#10b981" : "#ef4444"} stopOpacity={0.4}/>344                  <stop offset="95%" stopColor={isPositive ? "#10b981" : "#ef4444"} stopOpacity={0.05}/>345                </linearGradient>346              </defs>347              <CartesianGrid strokeDasharray="3 3" opacity={0.2} vertical={false} />348              <XAxis349                dataKey="dateFormatted"350                tick={{ fontSize: 11 }}351                interval="preserveStartEnd"352                height={60}353                angle={-45}354                textAnchor="end"355              />356              <YAxis357                yAxisId="price"358                domain={['auto', 'auto']}359                tick={{ fontSize: 11 }}360                tickFormatter={(value) => `$${value.toFixed(0)}`}361                width={60}362              />363              <Tooltip content={<CustomTooltip />} />364              <Legend wrapperStyle={{ fontSize: '12px', paddingTop: '10px' }} />365366              {/* 52-week high/low reference lines */}367              <ReferenceLine368                yAxisId="price"369                y={high52w}370                stroke="#10b981"371                strokeDasharray="3 3"372                strokeOpacity={0.5}373                label={{ value: '52W High', fontSize: 10, fill: '#10b981', position: 'right' }}374              />375              <ReferenceLine376                yAxisId="price"377                y={low52w}378                stroke="#ef4444"379                strokeDasharray="3 3"380                strokeOpacity={0.5}381                label={{ value: '52W Low', fontSize: 10, fill: '#ef4444', position: 'right' }}382              />383384              {/* Chart Types */}385              {chartType === 'area' && (386                <Area387                  yAxisId="price"388                  type="monotone"389                  dataKey="close"390                  stroke={isPositive ? "#10b981" : "#ef4444"}391                  strokeWidth={2}392                  fill="url(#colorPriceArea)"393                  name="Price"394                />395              )}396397              {chartType === 'line' && (398                <Line399                  yAxisId="price"400                  type="monotone"401                  dataKey="close"402                  stroke={isPositive ? "#10b981" : "#ef4444"}403                  strokeWidth={2}404                  dot={false}405                  name="Price"406                />407              )}408409              {/* Moving Averages */}410              {showMA && (411                <>412                  <Line413                    yAxisId="price"414                    type="monotone"415                    dataKey="ma20"416                    stroke="#3b82f6"417                    strokeWidth={1.5}418                    dot={false}419                    name="MA(20)"420                    strokeDasharray="5 5"421                  />422                  <Line423                    yAxisId="price"424                    type="monotone"425                    dataKey="ma50"426                    stroke="#8b5cf6"427                    strokeWidth={1.5}428                    dot={false}429                    name="MA(50)"430                    strokeDasharray="3 3"431                  />432                </>433              )}434435              <Brush436                dataKey="dateFormatted"437                height={30}438                stroke="#8b5cf6"439                fill="hsl(var(--muted))"440                travellerWidth={10}441              />442            </ComposedChart>443          </ResponsiveContainer>444        </div>445446        {/* Volume Chart */}447        {showVolume && (448          <div>449            <div className="text-sm font-medium mb-3">Trading Volume</div>450            <div className="h-[150px] w-full">451              <ResponsiveContainer width="100%" height="100%">452                <ComposedChart data={enrichedData}>453                  <CartesianGrid strokeDasharray="3 3" opacity={0.2} vertical={false} />454                  <XAxis455                    dataKey="dateFormatted"456                    tick={{ fontSize: 11 }}457                    interval="preserveStartEnd"458                    angle={-45}459                    textAnchor="end"460                    height={60}461                  />462                  <YAxis463                    tick={{ fontSize: 11 }}464                    tickFormatter={(value) => `${(value / 1_000_000).toFixed(0)}M`}465                    width={50}466                  />467                  <Tooltip468                    contentStyle={{469                      backgroundColor: 'hsl(var(--background))',470                      border: '1px solid hsl(var(--border))',471                      borderRadius: '8px',472                      fontSize: '12px'473                    }}474                    formatter={(value: any) => [(value / 1_000_000).toFixed(2) + 'M', 'Volume']}475                  />476                  <Bar477                    dataKey="volume"478                    radius={[4, 4, 0, 0]}479                  >480                    {enrichedData.map((entry, index) => (481                      <Bar482                        key={index}483                        fill={entry.isGreen ? '#10b981' : '#ef4444'}484                        opacity={0.6}485                      />486                    ))}487                  </Bar>488                  <ReferenceLine489                    y={avgVolume}490                    stroke="#f59e0b"491                    strokeDasharray="3 3"492                    label={{ value: 'Avg', fontSize: 10, fill: '#f59e0b' }}493                  />494                </ComposedChart>495              </ResponsiveContainer>496            </div>497          </div>498        )}499500        {/* Price Performance Summary */}501        <div className="p-4 border rounded-lg bg-muted/30">502          <div className="text-sm font-medium mb-3">📊 Period Performance</div>503          <div className="grid grid-cols-2 md:grid-cols-5 gap-3 text-sm">504            <div>505              <div className="text-muted-foreground">Period Start</div>506              <div className="font-semibold">${firstPrice.toFixed(2)}</div>507            </div>508            <div>509              <div className="text-muted-foreground">Period End</div>510              <div className="font-semibold">${lastPrice.toFixed(2)}</div>511            </div>512            <div>513              <div className="text-muted-foreground">Total Return</div>514              <div className={`font-semibold ${isPositive ? 'text-green-600' : 'text-red-600'}`}>515                {isPositive ? '+' : ''}{priceChangePercent.toFixed(2)}%516              </div>517            </div>518            <div>519              <div className="text-muted-foreground">Volatility</div>520              <div className="font-semibold">521                {((maxPrice - minPrice) / firstPrice * 100).toFixed(1)}%522              </div>523            </div>524            <div>525              <div className="text-muted-foreground">Total Vol Traded</div>526              <div className="font-semibold">527                {(enrichedData.reduce((sum, d) => sum + d.volume, 0) / 1_000_000_000).toFixed(2)}B528              </div>529            </div>530          </div>531        </div>532      </CardContent>533    </Card>534  );535}536