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, yf_ticker,
start=start_date.strftime("%Y-%m-%d"), start=start_date.strftime("%Y-%m-%d"),
end=end_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"),
multi_level_index=False,
progress=False, progress=False,
auto_adjust=False,
timeout=30 timeout=30
) )
@ -145,7 +147,9 @@ class PriceService:
alt_ticker, alt_ticker,
start=start_date.strftime("%Y-%m-%d"), start=start_date.strftime("%Y-%m-%d"),
end=end_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"),
multi_level_index=False,
progress=False, progress=False,
auto_adjust=False,
timeout=30 timeout=30
) )
if not data.empty: if not data.empty:
@ -157,7 +161,9 @@ class PriceService:
alt_ticker, alt_ticker,
start=start_date.strftime("%Y-%m-%d"), start=start_date.strftime("%Y-%m-%d"),
end=end_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"),
multi_level_index=False,
progress=False, progress=False,
auto_adjust=False,
timeout=30 timeout=30
) )
if not data.empty: if not data.empty:

View File

@ -1210,7 +1210,7 @@
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "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=="], "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, CartesianGrid,
Tooltip, Tooltip,
ResponsiveContainer, ResponsiveContainer,
Rectangle,
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { useLanguage } from "@/contexts/LanguageContext";
import type { PriceData, PriceStats } from "@/lib/types"; 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 priceValues = heikinAshiData.flatMap(d => [d.HA_High, d.HA_Low]);
const minPrice = Math.min(...priceValues); const minPrice = Math.min(...priceValues);
const maxPrice = Math.max(...priceValues); const maxPrice = Math.max(...priceValues);
const priceRange = maxPrice - minPrice; const _priceRange = maxPrice - minPrice;
// Localized labels // Localized labels
const labels = { const labels = {
@ -111,10 +110,12 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
lineChart: t.results.priceSection.lineChart, lineChart: t.results.priceSection.lineChart,
candlestick: t.results.priceSection.candlestick, candlestick: t.results.priceSection.candlestick,
volume: t.results.volumeChart, volume: t.results.volumeChart,
adjClosePrice: locale === 'en' ? 'Adj Close' : '調整後收盤價',
closePrice: locale === 'en' ? 'Close' : '收盤價', closePrice: locale === 'en' ? 'Close' : '收盤價',
date: locale === 'en' ? 'Date' : '日期', date: locale === 'en' ? 'Date' : '日期',
open: locale === 'en' ? 'Open' : '開', open: locale === 'en' ? 'Open' : '開',
close: locale === 'en' ? 'Close' : '收', close: locale === 'en' ? 'Close' : '收',
adjClose: locale === 'en' ? 'Adj Close' : '調整收',
high: locale === 'en' ? 'High' : '高', high: locale === 'en' ? 'High' : '高',
low: locale === 'en' ? 'Low' : '低', low: locale === 'en' ? 'Low' : '低',
up: locale === 'en' ? '↑ Up' : '↑ 上漲', up: locale === 'en' ? '↑ Up' : '↑ 上漲',
@ -178,18 +179,38 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
tickFormatter={(value) => `$${value.toFixed(0)}`} tickFormatter={(value) => `$${value.toFixed(0)}`}
/> />
<Tooltip <Tooltip
formatter={(value: number | undefined) => [ content={({ active, payload }) => {
value !== undefined ? `$${formatNumber(value)}` : '-', if (active && payload && payload.length) {
labels.closePrice const data = payload[0].payload as PriceData;
]} const adjClose = data["Adj Close"];
labelFormatter={(label) => `${labels.date}: ${label}`} 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 <Line
type="monotone" type="monotone"
dataKey={(data: PriceData) => getCloseValue(data)} dataKey={(data: PriceData) => getCloseValue(data)}
stroke="#93c5fd" stroke="#93c5fd"
strokeWidth={2} strokeWidth={2}
name={labels.closePrice} name={labels.adjClosePrice}
dot={false} dot={false}
/> />
</LineChart> </LineChart>
@ -212,7 +233,9 @@ export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) {
const data = payload[0].payload as HeikinAshiData; const data = payload[0].payload as HeikinAshiData;
const isUp = data.HA_Close > data.HA_Open; const isUp = data.HA_Close > data.HA_Open;
const isDown = 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 // Trend color coding for the direction indicator
const trendColor = isUp ? 'text-green-600' : isDown ? 'text-red-600' : 'text-gray-600'; 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 ( return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg"> <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> <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"> <div className="space-y-1 text-sm">
<p className="text-xs text-muted-foreground mb-1">Heikin Ashi</p>
<p className="text-purple-600"> <p className="text-purple-600">
{labels.open}: ${formatNumber(data.HA_Open)} {labels.open}: ${formatNumber(data.HA_Open)}
</p> </p>
@ -303,7 +344,7 @@ const HeikinAshiCandlestickShape: React.FC<HeikinAshiCandlestickShapeProps> = (p
const { HA_Open, HA_Close, HA_High, HA_Low } = payload; const { HA_Open, HA_Close, HA_High, HA_Low } = payload;
const isUp = HA_Close > HA_Open; const isUp = HA_Close > HA_Open;
const isDown = 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 // Color coding: green for up, red for down, gray for neutral
let fillColor: string; let fillColor: string;

View File

@ -82,7 +82,7 @@ class StockstatsUtils:
end=end_date_str, end=end_date_str,
multi_level_index=False, multi_level_index=False,
progress=False, progress=False,
auto_adjust=True, auto_adjust=False,
) )
data_yf = data_yf.reset_index() data_yf = data_yf.reset_index()
data_yf.to_csv(data_file, index=False) 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(start_date, "%Y-%m-%d")
datetime.strptime(end_date, "%Y-%m-%d") datetime.strptime(end_date, "%Y-%m-%d")
# 建立股票代碼物件 # 使用 yf.download() 獲取指定日期範圍的歷史數據
ticker = yf.Ticker(symbol.upper())
# 獲取指定日期範圍的歷史數據(添加 timeout
try: 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: except Exception as e:
raise Exception(f"從 Yahoo Finance 獲取 {symbol} 數據失敗: {e}") raise Exception(f"從 Yahoo Finance 獲取 {symbol} 數據失敗: {e}")
@ -297,7 +302,7 @@ def _get_stock_stats_bulk(
end=end_date_str, end=end_date_str,
multi_level_index=False, multi_level_index=False,
progress=False, progress=False,
auto_adjust=True, auto_adjust=False,
timeout=30 timeout=30
) )

View File

@ -37,14 +37,24 @@ class YFinanceUtils:
) -> pl.DataFrame: ) -> pl.DataFrame:
"""檢索指定股票代碼的股價數據""" """檢索指定股票代碼的股價數據"""
from datetime import datetime, timedelta 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_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
end_date = end_date_dt.strftime("%Y-%m-%d") 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 # 轉換為 polars DataFrame
stock_data_pl = pl.from_pandas(stock_data.reset_index()) 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 return stock_data_pl
def get_stock_info( def get_stock_info(