TradingAgents/frontend/components/analysis/PriceChart.tsx

244 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ComposedChart,
Area,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { PriceData, PriceStats } from "@/lib/types";
interface PriceChartProps {
priceData: PriceData[];
priceStats: PriceStats;
ticker: string;
}
export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
const [chartType, setChartType] = useState<"line" | "candlestick">("line");
// 格式化數字
const formatNumber = (num: number) => {
return num.toLocaleString('zh-TW', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
// 格式化日期(只顯示月-日)
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return `${date.getMonth() + 1}/${date.getDate()}`;
};
return (
<Card className="w-full">
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">{ticker} </CardTitle>
<Tabs value={chartType} onValueChange={(v) => setChartType(v as "line" | "candlestick")}>
<TabsList>
<TabsTrigger value="line"></TabsTrigger>
<TabsTrigger value="candlestick">K線圖</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* 統計資訊 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm text-muted-foreground"></p>
<p className={`text-2xl font-bold ${priceStats.growth_rate >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{priceStats.growth_rate >= 0 ? '+' : ''}{priceStats.growth_rate}%
</p>
</div>
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{priceStats.duration_days} </p>
</div>
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg font-semibold">${formatNumber(priceStats.start_price)}</p>
<p className="text-xs text-muted-foreground">{priceStats.start_date}</p>
</div>
<div className="bg-muted p-4 rounded-lg">
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg font-semibold">${formatNumber(priceStats.end_price)}</p>
<p className="text-xs text-muted-foreground">{priceStats.end_date}</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* 價格圖表 */}
<div>
<h3 className="text-lg font-semibold mb-4"></h3>
<ResponsiveContainer width="100%" height={400}>
{chartType === "line" ? (
<LineChart data={priceData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="Date"
tickFormatter={formatDate}
minTickGap={30}
/>
<YAxis
domain={['auto', 'auto']}
tickFormatter={(value) => `$${value.toFixed(0)}`}
/>
<Tooltip
formatter={(value: number) => [`$${formatNumber(value)}`, '收盤價']}
labelFormatter={(label) => `日期: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="Close"
stroke="#2563eb"
strokeWidth={2}
name="收盤價"
dot={false}
/>
</LineChart>
) : (
// 真正的K線圖蠟燭圖
<ComposedChart data={priceData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="Date"
tickFormatter={formatDate}
minTickGap={30}
/>
<YAxis
domain={['auto', 'auto']}
tickFormatter={(value) => `$${value.toFixed(0)}`}
/>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="text-sm font-semibold mb-2">: {data.Date}</p>
<div className="space-y-1 text-sm">
<p className="text-green-600">: ${formatNumber(data.Open)}</p>
<p className="text-red-600">: ${formatNumber(data.Close)}</p>
<p className="text-blue-600">: ${formatNumber(data.High)}</p>
<p className="text-orange-600">: ${formatNumber(data.Low)}</p>
</div>
</div>
);
}
return null;
}}
/>
<Bar
dataKey="Close"
shape={(props: any) => {
const { x, y, width, payload } = props;
const { Open, Close, High, Low } = payload;
// 計算Y軸的比例尺
const yAxis = props.yAxis;
const yScale = (value: number) => {
const { domain, height } = yAxis;
const [min, max] = domain;
return height - ((value - min) / (max - min)) * height;
};
const openY = yScale(Open);
const closeY = yScale(Close);
const highY = yScale(High);
const lowY = yScale(Low);
// 判斷漲跌
const isUp = Close >= Open;
const color = isUp ? '#22c55e' : '#ef4444'; // 綠色上漲,紅色下跌
const fillColor = isUp ? '#22c55e' : '#ef4444';
// K線寬度
const candleWidth = Math.min(width * 0.6, 8);
const centerX = x + width / 2;
// 實體高度
const bodyHeight = Math.abs(closeY - openY);
const bodyY = Math.min(openY, closeY);
return (
<g>
{/* 上影線 */}
<line
x1={centerX}
y1={highY}
x2={centerX}
y2={Math.min(openY, closeY)}
stroke={color}
strokeWidth={1}
/>
{/* 下影線 */}
<line
x1={centerX}
y1={Math.max(openY, closeY)}
x2={centerX}
y2={lowY}
stroke={color}
strokeWidth={1}
/>
{/* K線實體 */}
<rect
x={centerX - candleWidth / 2}
y={bodyY}
width={candleWidth}
height={bodyHeight || 1} // 至少1px高度避免十字星消失
fill={fillColor}
stroke={color}
strokeWidth={1}
/>
</g>
);
}}
/>
</ComposedChart>
)}
</ResponsiveContainer>
</div>
{/* 交易量圖表 */}
<div>
<h3 className="text-lg font-semibold mb-4"></h3>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={priceData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="Date"
tickFormatter={formatDate}
minTickGap={30}
/>
<YAxis
tickFormatter={(value) => {
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
return value.toString();
}}
/>
<Tooltip
formatter={(value: number) => [value.toLocaleString(), '交易量']}
labelFormatter={(label) => `日期: ${label}`}
/>
<Bar dataKey="Volume" fill="#10b981" name="交易量" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}