5 new endpoints under /api/v1/cn/:
- GET /cn/a-share/{symbol}/quote - A-share real-time quote
- GET /cn/a-share/{symbol}/historical - A-share historical OHLCV
- GET /cn/a-share/search?query= - search A-shares by name
- GET /cn/hk/{symbol}/quote - HK stock real-time quote
- GET /cn/hk/{symbol}/historical - HK stock historical OHLCV
Features:
- Chinese column names auto-mapped to English
- Symbol validation: A-share ^[036]\d{5}$, HK ^\d{5}$
- qfq (forward-adjusted) prices by default
- 79 new tests (51 service unit + 28 route integration)
- All 470 tests passing
106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""Routes for A-share (China) and Hong Kong stock market data via AKShare."""
|
|
|
|
from fastapi import APIRouter, HTTPException, Path, Query
|
|
|
|
from models import ApiResponse
|
|
from route_utils import safe
|
|
import akshare_service
|
|
|
|
router = APIRouter(prefix="/api/v1/cn", tags=["China & HK Markets"])
|
|
|
|
|
|
# --- Validation helpers ---
|
|
|
|
|
|
def _validate_a_share(symbol: str) -> str:
|
|
"""Validate A-share symbol format; raise 400 on failure."""
|
|
if not akshare_service.validate_a_share_symbol(symbol):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
f"Invalid A-share symbol '{symbol}'. "
|
|
"Must be 6 digits starting with 0, 3, or 6 (e.g. 000001, 300001, 600519)."
|
|
),
|
|
)
|
|
return symbol
|
|
|
|
|
|
def _validate_hk(symbol: str) -> str:
|
|
"""Validate HK stock symbol format; raise 400 on failure."""
|
|
if not akshare_service.validate_hk_symbol(symbol):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
f"Invalid HK symbol '{symbol}'. "
|
|
"Must be exactly 5 digits (e.g. 00700, 09988)."
|
|
),
|
|
)
|
|
return symbol
|
|
|
|
|
|
# --- A-share routes ---
|
|
# NOTE: /a-share/search MUST be registered before /a-share/{symbol} to avoid shadowing.
|
|
|
|
|
|
@router.get("/a-share/search", response_model=ApiResponse)
|
|
@safe
|
|
async def a_share_search(
|
|
query: str = Query(..., description="Stock name to search for (partial match)"),
|
|
) -> ApiResponse:
|
|
"""Search A-share stocks by name (partial match)."""
|
|
data = await akshare_service.search_a_shares(query)
|
|
return ApiResponse(data=data)
|
|
|
|
|
|
@router.get("/a-share/{symbol}/quote", response_model=ApiResponse)
|
|
@safe
|
|
async def a_share_quote(
|
|
symbol: str = Path(..., min_length=6, max_length=6),
|
|
) -> ApiResponse:
|
|
"""Get real-time A-share quote (沪深 real-time price)."""
|
|
symbol = _validate_a_share(symbol)
|
|
data = await akshare_service.get_a_share_quote(symbol)
|
|
if data is None:
|
|
raise HTTPException(status_code=404, detail=f"A-share symbol '{symbol}' not found.")
|
|
return ApiResponse(data=data)
|
|
|
|
|
|
@router.get("/a-share/{symbol}/historical", response_model=ApiResponse)
|
|
@safe
|
|
async def a_share_historical(
|
|
symbol: str = Path(..., min_length=6, max_length=6),
|
|
days: int = Query(default=365, ge=1, le=3650),
|
|
) -> ApiResponse:
|
|
"""Get A-share daily OHLCV history with qfq (前复权) adjustment."""
|
|
symbol = _validate_a_share(symbol)
|
|
data = await akshare_service.get_a_share_historical(symbol, days=days)
|
|
return ApiResponse(data=data)
|
|
|
|
|
|
# --- HK stock routes ---
|
|
|
|
|
|
@router.get("/hk/{symbol}/quote", response_model=ApiResponse)
|
|
@safe
|
|
async def hk_quote(
|
|
symbol: str = Path(..., min_length=5, max_length=5),
|
|
) -> ApiResponse:
|
|
"""Get real-time HK stock quote (港股 real-time price)."""
|
|
symbol = _validate_hk(symbol)
|
|
data = await akshare_service.get_hk_quote(symbol)
|
|
if data is None:
|
|
raise HTTPException(status_code=404, detail=f"HK symbol '{symbol}' not found.")
|
|
return ApiResponse(data=data)
|
|
|
|
|
|
@router.get("/hk/{symbol}/historical", response_model=ApiResponse)
|
|
@safe
|
|
async def hk_historical(
|
|
symbol: str = Path(..., min_length=5, max_length=5),
|
|
days: int = Query(default=365, ge=1, le=3650),
|
|
) -> ApiResponse:
|
|
"""Get HK stock daily OHLCV history with qfq adjustment."""
|
|
symbol = _validate_hk(symbol)
|
|
data = await akshare_service.get_hk_historical(symbol, days=days)
|
|
return ApiResponse(data=data)
|