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/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