feat: K线增加分钟/小时周期切换,拆分独立KLineCard组件
- 后端:腾讯 mkline 新增 /api/stock/history-minute(m1/m5/m15/m30/m60) - 新组件 kline-card.tsx:自包含周期/折线蜡烛/指标开关与K线数据加载, 切周期只重拉K线,不刷新页面其他模块 - 详情页瘦身为独立模块:K线图 / 今开最高最低昨收 / 每日行情明细互不耦合 - 时间统一按 UTC 解析传 Unix 秒,修复日线 invalid date/N/A 与分钟线时区问题 - 资金流向失败降级为非致命,不再导致整页报错 - vite 构建拆分 recharts/lightweight-charts/router 独立 chunk
This commit is contained in:
@@ -84,6 +84,23 @@ async def stock_history(
|
||||
raise HTTPException(status_code=404, detail="未获取到K线数据")
|
||||
|
||||
|
||||
@router.get("/history-minute", summary="分钟/小时级K线")
|
||||
async def stock_history_minute(
|
||||
code: str = Query(..., description="6位股票代码"),
|
||||
period: str = Query("60", description="周期:m1/m5/m15/m30/m60(60=小时线)"),
|
||||
count: int = Query(320, description="K线数量"),
|
||||
):
|
||||
if not re.match(r"^\d{6}$", code):
|
||||
raise HTTPException(status_code=400, detail="股票代码格式错误,需为6位数字")
|
||||
if period not in ("m1", "m5", "m15", "m30", "m60"):
|
||||
raise HTTPException(status_code=400, detail="period 仅支持 m1/m5/m15/m30/m60")
|
||||
|
||||
klines = await tencent.fetch_history_minute(code, period, count)
|
||||
if not klines:
|
||||
raise HTTPException(status_code=404, detail="未获取到分钟K线数据")
|
||||
return {"data": klines, "count": len(klines), "source": "tencent-minute"}
|
||||
|
||||
|
||||
@router.get("/profile", response_model=dict, summary="公司概况")
|
||||
async def company_profile(code: str = Query(..., description="6位股票代码")):
|
||||
"""获取东方财富F10公司概况数据"""
|
||||
|
||||
@@ -257,6 +257,64 @@ async def fetch_history(code: str, days: int = 90) -> List[dict]:
|
||||
return []
|
||||
|
||||
|
||||
async def fetch_history_minute(code: str, period: str = "60", count: int = 320) -> List[dict]:
|
||||
"""获取分钟级 K 线(腾讯 mkline 接口)
|
||||
|
||||
period: m1/m5/m15/m30/m60(60=小时线)
|
||||
返回格式与日K一致,date 为 'YYYY-MM-DD HH:MM'(北京时间)。
|
||||
"""
|
||||
market = get_market_prefix(code)
|
||||
stock_code = f"{market}{code}"
|
||||
# 腾讯 mkline:param=代码,周期,,数量
|
||||
url = f"https://ifzq.gtimg.cn/appstock/app/kline/mkline?param={stock_code},{period},,{count}"
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": "https://stockapp.finance.qq.com/",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
resp = await client.get(url, headers=headers, timeout=10)
|
||||
if resp.status_code != 200:
|
||||
return []
|
||||
data = resp.json()
|
||||
stock_data = data.get("data", {}).get(stock_code, {})
|
||||
raw = stock_data.get(period) or []
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
result = []
|
||||
prev_close = 0
|
||||
for record in raw:
|
||||
if not isinstance(record, list) or len(record) < 6:
|
||||
continue
|
||||
# record[0] = 'YYYYMMDDHHMM',转成 'YYYY-MM-DD HH:MM'
|
||||
raw_dt = str(record[0])
|
||||
try:
|
||||
dt_str = f"{raw_dt[0:4]}-{raw_dt[4:6]}-{raw_dt[6:8]} {raw_dt[8:10]}:{raw_dt[10:12]}"
|
||||
except Exception:
|
||||
continue
|
||||
close = float(record[2]) if record[2] else 0
|
||||
change_pct = 0
|
||||
if prev_close > 0:
|
||||
change_pct = (close - prev_close) / prev_close * 100
|
||||
result.append({
|
||||
"date": dt_str,
|
||||
"open": float(record[1]) if record[1] else 0,
|
||||
"close": close,
|
||||
"high": float(record[3]) if record[3] else 0,
|
||||
"low": float(record[4]) if record[4] else 0,
|
||||
"volume": int(float(record[5])) if record[5] else 0,
|
||||
"changePercent": round(change_pct, 2),
|
||||
})
|
||||
prev_close = close
|
||||
# 丢弃最旧1条(prev_close=0 涨跌幅失真)
|
||||
if len(result) > 1:
|
||||
result = result[1:]
|
||||
return result
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
async def fetch_kline_map(code: str, days: int = 30) -> dict:
|
||||
"""获取K线数据并返回 { date: { close, changePercent, turnover } } 映射"""
|
||||
market = get_market_prefix(code)
|
||||
|
||||
Reference in New Issue
Block a user