diff --git a/backend/app/services/price_service.py b/backend/app/services/price_service.py index 40584dd6..503579e6 100644 --- a/backend/app/services/price_service.py +++ b/backend/app/services/price_service.py @@ -132,7 +132,9 @@ class PriceService: yf_ticker, start=start_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"), + multi_level_index=False, progress=False, + auto_adjust=False, timeout=30 ) @@ -145,7 +147,9 @@ class PriceService: alt_ticker, start=start_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"), + multi_level_index=False, progress=False, + auto_adjust=False, timeout=30 ) if not data.empty: @@ -157,7 +161,9 @@ class PriceService: alt_ticker, start=start_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"), + multi_level_index=False, progress=False, + auto_adjust=False, timeout=30 ) if not data.empty: diff --git a/frontend/bun.lock b/frontend/bun.lock index 053228b7..b29dacc6 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -1210,7 +1210,7 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "ts-api-utils": ["ts-api-utils@2.2.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-L6f5oQRAoLU1RwXz0Ab9mxsE7LtxeVB6AIR1lpkZMsOyg/JXeaxBaXa/FVCBZyNr9S9I4wkHrlZTklX+im+WMw=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], diff --git a/frontend/components/analysis/PriceChart.tsx b/frontend/components/analysis/PriceChart.tsx index aeeb9c47..5ba5dfd9 100644 --- a/frontend/components/analysis/PriceChart.tsx +++ b/frontend/components/analysis/PriceChart.tsx @@ -11,10 +11,9 @@ import { CartesianGrid, Tooltip, ResponsiveContainer, - Rectangle, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useLanguage } from "@/contexts/LanguageContext"; import type { PriceData, PriceStats } from "@/lib/types"; @@ -98,7 +97,7 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { const priceValues = heikinAshiData.flatMap(d => [d.HA_High, d.HA_Low]); const minPrice = Math.min(...priceValues); const maxPrice = Math.max(...priceValues); - const priceRange = maxPrice - minPrice; + const _priceRange = maxPrice - minPrice; // Localized labels const labels = { @@ -111,10 +110,12 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { lineChart: t.results.priceSection.lineChart, candlestick: t.results.priceSection.candlestick, volume: t.results.volumeChart, + adjClosePrice: locale === 'en' ? 'Adj Close' : '調整後收盤價', closePrice: locale === 'en' ? 'Close' : '收盤價', date: locale === 'en' ? 'Date' : '日期', open: locale === 'en' ? 'Open' : '開', close: locale === 'en' ? 'Close' : '收', + adjClose: locale === 'en' ? 'Adj Close' : '調整收', high: locale === 'en' ? 'High' : '高', low: locale === 'en' ? 'Low' : '低', up: locale === 'en' ? '↑ Up' : '↑ 上漲', @@ -178,18 +179,38 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { tickFormatter={(value) => `$${value.toFixed(0)}`} /> [ - value !== undefined ? `$${formatNumber(value)}` : '-', - labels.closePrice - ]} - labelFormatter={(label) => `${labels.date}: ${label}`} + content={({ active, payload }) => { + if (active && payload && payload.length) { + const data = payload[0].payload as PriceData; + const adjClose = data["Adj Close"]; + const close = data.Close; + const hasAdjClose = adjClose !== undefined && adjClose !== null; + + return ( +
+

{labels.date}: {data.Date}

+
+ {hasAdjClose && ( +

+ {labels.adjClosePrice}: ${formatNumber(adjClose)} +

+ )} +

+ {labels.closePrice}: ${formatNumber(close)} +

+
+
+ ); + } + return null; + }} /> getCloseValue(data)} stroke="#93c5fd" strokeWidth={2} - name={labels.closePrice} + name={labels.adjClosePrice} dot={false} /> @@ -212,7 +233,9 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { const data = payload[0].payload as HeikinAshiData; const isUp = data.HA_Close > data.HA_Open; const isDown = data.HA_Close < data.HA_Open; - const isNeutral = data.HA_Close === data.HA_Open; + const adjClose = data["Adj Close"]; + const close = data.Close; + const hasAdjClose = adjClose !== undefined && adjClose !== null; // Trend color coding for the direction indicator const trendColor = isUp ? 'text-green-600' : isDown ? 'text-red-600' : 'text-gray-600'; @@ -221,7 +244,25 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { return (

{labels.date}: {data.Date}

+ + {/* 原始價格 - Adj Close 為主 */} +
+

+ {locale === 'en' ? 'Actual Prices' : '實際價格'} +

+ {hasAdjClose && ( +

+ {labels.adjClosePrice}: ${formatNumber(adjClose)} +

+ )} +

+ {labels.closePrice}: ${formatNumber(close)} +

+
+ + {/* Heikin Ashi 值 */}
+

Heikin Ashi

{labels.open}: ${formatNumber(data.HA_Open)}

@@ -303,7 +344,7 @@ const HeikinAshiCandlestickShape: React.FC = (p const { HA_Open, HA_Close, HA_High, HA_Low } = payload; const isUp = HA_Close > HA_Open; const isDown = HA_Close < HA_Open; - const isNeutral = HA_Close === HA_Open; + const _isNeutral = HA_Close === HA_Open; // Color coding: green for up, red for down, gray for neutral let fillColor: string; diff --git a/tradingagents/dataflows/stockstats_utils.py b/tradingagents/dataflows/stockstats_utils.py index 19471bc9..7843942b 100644 --- a/tradingagents/dataflows/stockstats_utils.py +++ b/tradingagents/dataflows/stockstats_utils.py @@ -82,7 +82,7 @@ class StockstatsUtils: end=end_date_str, multi_level_index=False, progress=False, - auto_adjust=True, + auto_adjust=False, ) data_yf = data_yf.reset_index() data_yf.to_csv(data_file, index=False) diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index f8cf8820..3e8a1354 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -32,12 +32,17 @@ def get_YFin_data_online( datetime.strptime(start_date, "%Y-%m-%d") datetime.strptime(end_date, "%Y-%m-%d") - # 建立股票代碼物件 - ticker = yf.Ticker(symbol.upper()) - - # 獲取指定日期範圍的歷史數據(添加 timeout) + # 使用 yf.download() 獲取指定日期範圍的歷史數據 try: - data = ticker.history(start=start_date, end=end_date, timeout=30) + data = yf.download( + symbol.upper(), + start=start_date, + end=end_date, + multi_level_index=False, + progress=False, + auto_adjust=False, + timeout=30 + ) except Exception as e: raise Exception(f"從 Yahoo Finance 獲取 {symbol} 數據失敗: {e}") @@ -297,7 +302,7 @@ def _get_stock_stats_bulk( end=end_date_str, multi_level_index=False, progress=False, - auto_adjust=True, + auto_adjust=False, timeout=30 ) diff --git a/tradingagents/dataflows/yfin_utils.py b/tradingagents/dataflows/yfin_utils.py index 7c091ea6..3d18394d 100644 --- a/tradingagents/dataflows/yfin_utils.py +++ b/tradingagents/dataflows/yfin_utils.py @@ -37,14 +37,24 @@ class YFinanceUtils: ) -> pl.DataFrame: """檢索指定股票代碼的股價數據""" from datetime import datetime, timedelta - ticker = symbol + ticker = symbol # 這裡 symbol 已被裝飾器轉換為 yf.Ticker 對象 + ticker_symbol = ticker.ticker # 獲取股票代碼字串 # 將結束日期加一天,使數據範圍包含結束日期 end_date_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) end_date = end_date_dt.strftime("%Y-%m-%d") - stock_data = ticker.history(start=start_date, end=end_date) + # 使用 yf.download() 統一獲取數據 + stock_data = yf.download( + ticker_symbol, + start=start_date, + end=end_date, + multi_level_index=False, + progress=False, + auto_adjust=False, + timeout=30 + ) # 轉換為 polars DataFrame stock_data_pl = pl.from_pandas(stock_data.reset_index()) - # save_output(stock_data_pl, f"{ticker.ticker} 的股票數據", save_path) + # save_output(stock_data_pl, f"{ticker_symbol} 的股票數據", save_path) return stock_data_pl def get_stock_info(