This commit is contained in:
MarkLo 2025-12-28 01:22:45 +08:00
parent c0f25aaafd
commit 3145d08c30
6 changed files with 84 additions and 22 deletions

View File

@ -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:

View File

@ -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=="],

View File

@ -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)}`}
/>
<Tooltip
formatter={(value: number | undefined) => [
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 (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="text-sm font-semibold mb-2">{labels.date}: {data.Date}</p>
<div className="space-y-1 text-sm">
{hasAdjClose && (
<p className="text-blue-600 font-medium">
{labels.adjClosePrice}: ${formatNumber(adjClose)}
</p>
)}
<p className={hasAdjClose ? "text-gray-500 text-xs" : "text-blue-600"}>
{labels.closePrice}: ${formatNumber(close)}
</p>
</div>
</div>
);
}
return null;
}}
/>
<Line
type="monotone"
dataKey={(data: PriceData) => getCloseValue(data)}
stroke="#93c5fd"
strokeWidth={2}
name={labels.closePrice}
name={labels.adjClosePrice}
dot={false}
/>
</LineChart>
@ -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 (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="text-sm font-semibold mb-2">{labels.date}: {data.Date}</p>
{/* 原始價格 - Adj Close 為主 */}
<div className="mb-2 pb-2 border-b border-border">
<p className="text-xs text-muted-foreground mb-1">
{locale === 'en' ? 'Actual Prices' : '實際價格'}
</p>
{hasAdjClose && (
<p className="text-blue-600 font-medium text-sm">
{labels.adjClosePrice}: ${formatNumber(adjClose)}
</p>
)}
<p className={hasAdjClose ? "text-gray-500 text-xs" : "text-blue-600 text-sm"}>
{labels.closePrice}: ${formatNumber(close)}
</p>
</div>
{/* Heikin Ashi 值 */}
<div className="space-y-1 text-sm">
<p className="text-xs text-muted-foreground mb-1">Heikin Ashi</p>
<p className="text-purple-600">
{labels.open}: ${formatNumber(data.HA_Open)}
</p>
@ -303,7 +344,7 @@ const HeikinAshiCandlestickShape: React.FC<HeikinAshiCandlestickShapeProps> = (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;

View File

@ -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)

View File

@ -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
)

View File

@ -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(