From f2d73888a07a43c578ff1d40e95a34a5f853124e Mon Sep 17 00:00:00 2001 From: MarkLo Date: Mon, 1 Dec 2025 19:52:45 +0800 Subject: [PATCH] --- frontend/components/analysis/AnalysisForm.tsx | 2 +- .../components/analysis/AnalystReport.tsx | 4 +- frontend/components/analysis/PriceChart.tsx | 141 ++++++++++++++---- .../components/analysis/TradingDecision.tsx | 2 +- frontend/package-lock.json | 8 +- frontend/package.json | 2 +- 6 files changed, 124 insertions(+), 35 deletions(-) diff --git a/frontend/components/analysis/AnalysisForm.tsx b/frontend/components/analysis/AnalysisForm.tsx index 15480c72..75179c7d 100644 --- a/frontend/components/analysis/AnalysisForm.tsx +++ b/frontend/components/analysis/AnalysisForm.tsx @@ -122,7 +122,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { } return ( - +
+ 分析報告 來自所有代理團隊的詳細報告 @@ -125,7 +125,7 @@ export function AnalystReport({ reports }: AnalystReportProps) { function ReportSection({ title, content }: { title: string; content: string }) { return ( -
+

{title}

diff --git a/frontend/components/analysis/PriceChart.tsx b/frontend/components/analysis/PriceChart.tsx index 3930942e..12a117a9 100644 --- a/frontend/components/analysis/PriceChart.tsx +++ b/frontend/components/analysis/PriceChart.tsx @@ -6,15 +6,12 @@ import { Line, BarChart, Bar, - Cell, XAxis, YAxis, CartesianGrid, Tooltip, - Legend, ResponsiveContainer, - ComposedChart, - Area, + Rectangle, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -40,8 +37,14 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { return `${date.getMonth() + 1}/${date.getDate()}`; }; + // 計算價格範圍用於標準化 + const priceValues = priceData.flatMap(d => [d.High, d.Low]); + const minPrice = Math.min(...priceValues); + const maxPrice = Math.max(...priceValues); + const priceRange = maxPrice - minPrice; + return ( - +
{ticker} 價格走勢 @@ -55,22 +58,22 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { {/* 統計資訊 */}
-
+

增長率

= 0 ? 'text-green-600' : 'text-red-600'}`}> {priceStats.growth_rate >= 0 ? '+' : ''}{priceStats.growth_rate}%

-
+

時長

{priceStats.duration_days} 天

-
+

起始價格

${formatNumber(priceStats.start_price)}

{priceStats.start_date}

-
+

結束價格

${formatNumber(priceStats.end_price)}

{priceStats.end_date}

@@ -99,19 +102,18 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { formatter={(value: number) => [`$${formatNumber(value)}`, '收盤價']} labelFormatter={(label) => `日期: ${label}`} /> - ) : ( - // K線圖:簡化版實現 - + // K線圖:真正的蠟燭圖實現 + `$${value.toFixed(0)}`} /> - {/* 使用 Bar 顯示收盤價,顏色根據漲跌決定 */} + {/* 使用自定義 shape 來繪製蠟燭 */} - {priceData.map((entry: any, index: number) => ( - = entry.Open ? '#22c55e' : '#ef4444'} - /> - ))} - + dataKey="High" + shape={(props: any) => } + /> )} @@ -188,7 +183,7 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { formatter={(value: number) => [value.toLocaleString(), '交易量']} labelFormatter={(label) => `日期: ${label}`} /> - +
@@ -196,3 +191,97 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { ); } + +// 自定義蠟燭圖形狀組件 +interface CandlestickShapeProps { + x?: number; + y?: number; + width?: number; + height?: number; + payload?: PriceData; + minPrice: number; + maxPrice: number; +} + +const CandlestickShape: React.FC = (props) => { + const { x = 0, y = 0, width = 0, height = 0, payload, minPrice, maxPrice } = props; + + if (!payload) return null; + + const { Open, Close, High, Low } = payload; + const isUp = Close >= Open; + + // 粉綠色(上漲)和粉紅色(下跌) + const fillColor = isUp ? '#86efac' : '#fca5a5'; // soft pastel green / soft pastel pink + const strokeColor = isUp ? '#22c55e' : '#ef4444'; // darker green / darker red + + // 計算實際的 Y 坐標位置 + const priceRange = maxPrice - minPrice; + // The height prop passed to shape is the full height of the chart area for the Y-axis. + // We need to calculate the actual chart drawing height based on the Y-axis domain. + // The Y-axis in recharts is typically inverted, so higher values are lower Y coordinates. + // The 'y' prop passed to shape is the y-coordinate of the data point (High in this case). + // We need to adjust calculations based on the actual Y-axis scale. + + // Recharts Y-axis is inverted: higher price -> lower Y coordinate. + // The 'y' prop for the Bar is typically the y-coordinate of the dataKey (High). + // The 'height' prop for the Bar is the height of the bar if it were a standard bar chart. + // For a candlestick, we need to map prices to the chart's pixel height. + + // Let's assume 'y' is the top of the plotting area and 'y + height' is the bottom. + // The total pixel height available for the price range is 'height'. + // We need to map minPrice to y + height and maxPrice to y. + + // Calculate the pixel value per price unit + const pixelsPerPriceUnit = height / priceRange; + + // Calculate Y coordinates for Open, Close, High, Low + // Note: Y-axis is inverted, so (maxPrice - price) gives distance from top. + const highY = y + (maxPrice - High) * pixelsPerPriceUnit; + const lowY = y + (maxPrice - Low) * pixelsPerPriceUnit; + const openY = y + (maxPrice - Open) * pixelsPerPriceUnit; + const closeY = y + (maxPrice - Close) * pixelsPerPriceUnit; + + // 蠟燭主體 + const bodyTop = Math.min(openY, closeY); + const bodyBottom = Math.max(openY, closeY); + const bodyHeight = Math.max(bodyBottom - bodyTop, 1); // 至少 1px + + // 蠟燭寬度和影線位置 + const candleWidth = width * 0.6; // 蠟燭佔 60% 寬度 + const candleX = x + (width - candleWidth) / 2; + const wickX = x + width / 2; // 影線在中間 + + return ( + + {/* 上影線(從最高價到蠟燭頂部) */} + + {/* 下影線(從蠟燭底部到最低價) */} + + {/* 蠟燭主體 */} + + + ); +}; diff --git a/frontend/components/analysis/TradingDecision.tsx b/frontend/components/analysis/TradingDecision.tsx index cbf6e50b..24559aa3 100644 --- a/frontend/components/analysis/TradingDecision.tsx +++ b/frontend/components/analysis/TradingDecision.tsx @@ -41,7 +41,7 @@ export function TradingDecision({ result }: TradingDecisionProps) { }; return ( - +
交易決策 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 918ae425..316fc915 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,7 +31,7 @@ "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0", - "zod": "^4.1.12" + "zod": "^3.25.76" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -9809,9 +9809,9 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "peer": true, "funding": { diff --git a/frontend/package.json b/frontend/package.json index 7230d5e0..0b59e5e4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,7 +32,7 @@ "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0", - "zod": "^4.1.12" + "zod": "^3.25.76" }, "devDependencies": { "@tailwindcss/postcss": "^4",