This commit is contained in:
parent
c0f25aaafd
commit
3145d08c30
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue