feat: 股票详情页增加最新交易日PE/PB/ROE/换手率/市值等行情指标
- 后端tencent.py增加换手率/市盈率/市净率/总市值/流通市值字段提取 - 前端StockQuote接口新增对应字段类型定义 - 股票详情页增加"行情指标"卡片,展示PE/PB/ROE/换手率/总手/成交额/市值 - ROE取自财务分析数据的加权净资产收益率 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -142,10 +142,11 @@ async def fetch_quote(code: str) -> Optional[dict]:
|
||||
return None
|
||||
text = resp.content.decode("gbk", errors="replace")
|
||||
parts = parse_tencent_data(text)
|
||||
if not parts or len(parts) < 38:
|
||||
if not parts or len(parts) < 46:
|
||||
return None
|
||||
# 字段索引(1-based):1=名称, 3=当前价, 4=昨收, 5=今开, 6=成交量(手)
|
||||
# 字段索引:1=名称, 3=当前价, 4=昨收, 5=今开, 6=成交量(手)
|
||||
# 7=外盘, 8=内盘, 31=涨跌额, 32=涨跌幅%, 33=最高, 34=最低, 37=成交额(万)
|
||||
# 38=换手率%, 39=市盈率, 43=总市值, 44=流通市值, 45=市净率
|
||||
name = parts[1] or ""
|
||||
current_price = float(parts[3]) if parts[3] else 0
|
||||
yesterday_close = float(parts[4]) if parts[4] else 0
|
||||
@@ -158,6 +159,11 @@ async def fetch_quote(code: str) -> Optional[dict]:
|
||||
change_pct = float(parts[32]) if parts[32] else 0
|
||||
outer_disk = float(parts[7]) if parts[7] else 0
|
||||
inner_disk = float(parts[8]) if parts[8] else 0
|
||||
turnover_rate = float(parts[38]) if parts[38] else 0 # 换手率%
|
||||
pe = float(parts[39]) if parts[39] else 0 # 市盈率
|
||||
total_market_cap = float(parts[43]) if parts[43] else 0 # 总市值
|
||||
circulating_market_cap = float(parts[44]) if parts[44] else 0 # 流通市值
|
||||
pb = float(parts[45]) if parts[45] else 0 # 市净率
|
||||
|
||||
if not name or current_price == 0:
|
||||
return None
|
||||
@@ -186,6 +192,11 @@ async def fetch_quote(code: str) -> Optional[dict]:
|
||||
"time": now.strftime("%H:%M:%S"),
|
||||
"change": round(change, 2),
|
||||
"changePercent": round(change_percent, 2),
|
||||
"turnoverRate": round(turnover_rate, 2),
|
||||
"pe": round(pe, 2) if pe else 0,
|
||||
"pb": round(pb, 2) if pb else 0,
|
||||
"totalMarketCap": round(total_market_cap, 2),
|
||||
"circulatingMarketCap": round(circulating_market_cap, 2),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -18,6 +18,11 @@ export interface StockQuote {
|
||||
time: string;
|
||||
change: number;
|
||||
changePercent: number;
|
||||
turnoverRate: number; // 换手率%
|
||||
pe: number; // 市盈率
|
||||
pb: number; // 市净率
|
||||
totalMarketCap: number; // 总市值
|
||||
circulatingMarketCap: number; // 流通市值
|
||||
}
|
||||
|
||||
export interface KLineData {
|
||||
|
||||
@@ -63,8 +63,14 @@ interface StockInfo {
|
||||
addedPrice: number | null;
|
||||
outerDisk: number; // 外盘(手)
|
||||
innerDisk: number; // 内盘(手)
|
||||
volume: number; // 成交量(股)
|
||||
amount: number; // 成交额(元)
|
||||
quoteDate: string; // 行情日期 YYYY-MM-DD
|
||||
turnoverRate: number; // 换手率%
|
||||
pe: number; // 市盈率
|
||||
pb: number; // 市净率
|
||||
totalMarketCap: number; // 总市值
|
||||
circulatingMarketCap: number; // 流通市值
|
||||
}
|
||||
|
||||
function StockDetail() {
|
||||
@@ -155,6 +161,12 @@ function StockDetail() {
|
||||
innerDisk: quote.innerDisk || 0,
|
||||
amount: (quote.amount || 0) * 10000,
|
||||
quoteDate: quote.date,
|
||||
volume: quote.volume || 0,
|
||||
turnoverRate: quote.turnoverRate ?? 0,
|
||||
pe: quote.pe ?? 0,
|
||||
pb: quote.pb ?? 0,
|
||||
totalMarketCap: quote.totalMarketCap ?? 0,
|
||||
circulatingMarketCap: quote.circulatingMarketCap ?? 0,
|
||||
});
|
||||
|
||||
// 基础信息已就绪,结束主loading,先渲染页面框架
|
||||
@@ -267,6 +279,14 @@ function StockDetail() {
|
||||
negativeDays: negative,
|
||||
};
|
||||
}, [fundFlowSlice]);
|
||||
|
||||
// 从财务数据中提取最新ROE(加权净资产收益率)
|
||||
const latestROE = useMemo(() => {
|
||||
if (!financialData?.data?.length) return null;
|
||||
const latest = financialData.data[0];
|
||||
const roe = latest.indicators["加权净资产收益率"];
|
||||
return typeof roe === "number" ? roe : null;
|
||||
}, [financialData]);
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
@@ -653,6 +673,53 @@ function StockDetail() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 行情指标:PE / PB / ROE / 换手率 / 总手 / 成交额 / 市值 */}
|
||||
<Card className="mt-4 md:mt-6 shadow-lg">
|
||||
<CardHeader className="pb-2 md:pb-4 px-3 md:px-6 pt-4 md:pt-6">
|
||||
<CardTitle className="text-base md:text-lg">行情指标</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 md:px-6 pb-3 md:pb-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4">
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">市盈率(PE)</p>
|
||||
<p className="text-lg font-semibold">{stockInfo.pe > 0 ? stockInfo.pe.toFixed(2) : '-'}</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">市净率(PB)</p>
|
||||
<p className="text-lg font-semibold">{stockInfo.pb > 0 ? stockInfo.pb.toFixed(2) : '-'}</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">ROE(加权净资产收益率)</p>
|
||||
<p className={`text-lg font-semibold ${latestROE !== null ? (latestROE > 0 ? 'text-red-500' : 'text-green-500') : ''}`}>
|
||||
{latestROE !== null ? `${latestROE.toFixed(2)}%` : '加载中...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">换手率</p>
|
||||
<p className="text-lg font-semibold">{stockInfo.turnoverRate > 0 ? `${stockInfo.turnoverRate}%` : '-'}</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">总手</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{stockInfo.volume > 0 ? `${(stockInfo.volume / 100).toLocaleString()}手` : '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">成交额</p>
|
||||
<p className="text-lg font-semibold">{formatMoney(stockInfo.amount)}</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">总市值</p>
|
||||
<p className="text-lg font-semibold">{stockInfo.totalMarketCap > 0 ? formatMoney(stockInfo.totalMarketCap) : '-'}</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-muted/30">
|
||||
<p className="text-xs text-muted-foreground mb-1">流通市值</p>
|
||||
<p className="text-lg font-semibold">{stockInfo.circulatingMarketCap > 0 ? formatMoney(stockInfo.circulatingMarketCap) : '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Daily Data Table - Independent from fund flow */}
|
||||
{chartData.length > 0 && (
|
||||
<Card className="mt-4 md:mt-6 shadow-lg">
|
||||
|
||||
Reference in New Issue
Block a user