|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
"""
|
|
|
|
|
===================================
|
|
|
|
|
Stock data endpoints
|
|
|
|
|
===================================
|
|
|
|
|
|
|
|
|
|
Responsibilities:
|
|
|
|
|
1. Provide stock quote endpoint
|
|
|
|
|
2. Provide stock history endpoint
|
|
|
|
|
3. Provide stock momentum recommendations
|
|
|
|
|
4. Provide new high/low stocks
|
|
|
|
|
5. Provide stock K-line chart data
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Depends
|
|
|
|
|
|
|
|
|
|
from api.v1.schemas.stocks import (
|
|
|
|
|
StockQuote,
|
|
|
|
|
StockHistoryResponse,
|
|
|
|
|
KLineData,
|
|
|
|
|
)
|
|
|
|
|
from api.v1.schemas.market import (
|
|
|
|
|
StockMomentum,
|
|
|
|
|
StockMomentumResponse,
|
|
|
|
|
NewHighLowStock,
|
|
|
|
|
KLineChartResponse,
|
|
|
|
|
)
|
|
|
|
|
from api.v1.schemas.common import ErrorResponse
|
|
|
|
|
from src.services.stock_service import StockService
|
|
|
|
|
from src.services.market_service import MarketService
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_stock_service() -> StockService:
|
|
|
|
|
"""Dependency injection for StockService"""
|
|
|
|
|
return StockService()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_market_service() -> MarketService:
|
|
|
|
|
"""Dependency injection for MarketService"""
|
|
|
|
|
return MarketService()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/momentum-recommendation",
|
|
|
|
|
response_model=StockMomentumResponse,
|
|
|
|
|
responses={
|
|
|
|
|
200: {"description": "Stock momentum recommendations"},
|
|
|
|
|
500: {"description": "Server error", "model": ErrorResponse},
|
|
|
|
|
},
|
|
|
|
|
summary="Get stock momentum recommendations",
|
|
|
|
|
description="Get stock momentum recommendations based on technical indicators"
|
|
|
|
|
)
|
|
|
|
|
def get_momentum_recommendation(
|
|
|
|
|
limit: int = Query(15, ge=1, le=50, description="Limit count"),
|
|
|
|
|
service: MarketService = Depends(get_market_service)
|
|
|
|
|
) -> StockMomentumResponse:
|
|
|
|
|
"""
|
|
|
|
|
Get stock momentum recommendations
|
|
|
|
|
|
|
|
|
|
Returns stocks with strong momentum signals
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
stocks = service.get_stock_momentum_recommendation(limit=limit)
|
|
|
|
|
return StockMomentumResponse(
|
|
|
|
|
items=stocks,
|
|
|
|
|
total=len(stocks)
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to get momentum recommendation: {e}", exc_info=True)
|
|
|
|
|
return StockMomentumResponse(items=[], total=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/new-high",
|
|
|
|
|
response_model=List[NewHighLowStock],
|
|
|
|
|
responses={
|
|
|
|
|
200: {"description": "New high stocks"},
|
|
|
|
|
500: {"description": "Server error", "model": ErrorResponse},
|
|
|
|
|
},
|
|
|
|
|
summary="Get new high stocks",
|
|
|
|
|
description="Get stocks that reached new high in recent days"
|
|
|
|
|
)
|
|
|
|
|
def get_new_high(
|
|
|
|
|
days: int = Query(20, ge=1, le=60, description="Days to look back"),
|
|
|
|
|
limit: int = Query(20, ge=1, le=50, description="Limit count"),
|
|
|
|
|
service: MarketService = Depends(get_market_service)
|
|
|
|
|
) -> List[NewHighLowStock]:
|
|
|
|
|
"""
|
|
|
|
|
Get new high stocks
|
|
|
|
|
|
|
|
|
|
Returns stocks that reached new high within specified days
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
stocks = service.get_new_high_stocks(days=days, limit=limit)
|
|
|
|
|
return stocks
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to get new high stocks: {e}", exc_info=True)
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/new-low",
|
|
|
|
|
response_model=List[NewHighLowStock],
|
|
|
|
|
responses={
|
|
|
|
|
200: {"description": "New low stocks"},
|
|
|
|
|
500: {"description": "Server error", "model": ErrorResponse},
|
|
|
|
|
},
|
|
|
|
|
summary="Get new low stocks",
|
|
|
|
|
description="Get stocks that reached new low in recent days"
|
|
|
|
|
)
|
|
|
|
|
def get_new_low(
|
|
|
|
|
days: int = Query(20, ge=1, le=60, description="Days to look back"),
|
|
|
|
|
limit: int = Query(20, ge=1, le=50, description="Limit count"),
|
|
|
|
|
service: MarketService = Depends(get_market_service)
|
|
|
|
|
) -> List[NewHighLowStock]:
|
|
|
|
|
"""
|
|
|
|
|
Get new low stocks
|
|
|
|
|
|
|
|
|
|
Returns stocks that reached new low within specified days
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
stocks = service.get_new_low_stocks(days=days, limit=limit)
|
|
|
|
|
return stocks
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to get new low stocks: {e}", exc_info=True)
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/{stock_code}/quote",
|
|
|
|
|
response_model=StockQuote,
|
|
|
|
|
responses={
|
|
|
|
|
200: {"description": "Stock quote data"},
|
|
|
|
|
404: {"description": "Stock not found", "model": ErrorResponse},
|
|
|
|
|
500: {"description": "Server error", "model": ErrorResponse},
|
|
|
|
|
},
|
|
|
|
|
summary="Get stock quote",
|
|
|
|
|
description="Get real-time stock quote"
|
|
|
|
|
)
|
|
|
|
|
def get_stock_quote(
|
|
|
|
|
stock_code: str,
|
|
|
|
|
service: StockService = Depends(get_stock_service)
|
|
|
|
|
) -> StockQuote:
|
|
|
|
|
"""
|
|
|
|
|
Get stock real-time quote
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
stock_code: Stock code (e.g., 600519, 00700, AAPL)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
StockQuote: Real-time quote data
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
result = service.get_realtime_quote(stock_code)
|
|
|
|
|
|
|
|
|
|
if result is None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=404,
|
|
|
|
|
detail={
|
|
|
|
|
"error": "not_found",
|
|
|
|
|
"message": f"Stock {stock_code} not found"
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return StockQuote(
|
|
|
|
|
stock_code=result.get("stock_code", stock_code),
|
|
|
|
|
stock_name=result.get("stock_name"),
|
|
|
|
|
current_price=result.get("current_price", 0.0),
|
|
|
|
|
change=result.get("change"),
|
|
|
|
|
change_percent=result.get("change_percent"),
|
|
|
|
|
open=result.get("open"),
|
|
|
|
|
high=result.get("high"),
|
|
|
|
|
low=result.get("low"),
|
|
|
|
|
prev_close=result.get("prev_close"),
|
|
|
|
|
volume=result.get("volume"),
|
|
|
|
|
amount=result.get("amount"),
|
|
|
|
|
update_time=result.get("update_time")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to get stock quote: {e}", exc_info=True)
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=500,
|
|
|
|
|
detail={
|
|
|
|
|
"error": "internal_error",
|
|
|
|
|
"message": f"Failed to get stock quote: {str(e)}"
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/{stock_code}/history",
|
|
|
|
|
response_model=StockHistoryResponse,
|
|
|
|
|
responses={
|
|
|
|
|
200: {"description": "Stock history data"},
|
|
|
|
|
422: {"description": "Unsupported period", "model": ErrorResponse},
|
|
|
|
|
500: {"description": "Server error", "model": ErrorResponse},
|
|
|
|
|
},
|
|
|
|
|
summary="Get stock history",
|
|
|
|
|
description="Get stock historical K-line data"
|
|
|
|
|
)
|
|
|
|
|
def get_stock_history(
|
|
|
|
|
stock_code: str,
|
|
|
|
|
period: str = Query("daily", description="K-line period", pattern="^(daily|weekly|monthly)$"),
|
|
|
|
|
days: int = Query(30, ge=1, le=365, description="Number of days"),
|
|
|
|
|
service: StockService = Depends(get_stock_service)
|
|
|
|
|
) -> StockHistoryResponse:
|
|
|
|
|
"""
|
|
|
|
|
Get stock historical data
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
stock_code: Stock code
|
|
|
|
|
period: K-line period (daily/weekly/monthly)
|
|
|
|
|
days: Number of days
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
StockHistoryResponse: Historical data
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
result = service.get_history_data(
|
|
|
|
|
stock_code=stock_code,
|
|
|
|
|
period=period,
|
|
|
|
|
days=days
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
data = [
|
|
|
|
|
KLineData(
|
|
|
|
|
date=item.get("date"),
|
|
|
|
|
open=item.get("open"),
|
|
|
|
|
high=item.get("high"),
|
|
|
|
|
low=item.get("low"),
|
|
|
|
|
close=item.get("close"),
|
|
|
|
|
volume=item.get("volume"),
|
|
|
|
|
amount=item.get("amount"),
|
|
|
|
|
change_percent=item.get("change_percent")
|
|
|
|
|
)
|
|
|
|
|
for item in result.get("data", [])
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return StockHistoryResponse(
|
|
|
|
|
stock_code=stock_code,
|
|
|
|
|
stock_name=result.get("stock_name"),
|
|
|
|
|
period=period,
|
|
|
|
|
data=data
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=422,
|
|
|
|
|
detail={
|
|
|
|
|
"error": "unsupported_period",
|
|
|
|
|
"message": str(e)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to get stock history: {e}", exc_info=True)
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=500,
|
|
|
|
|
detail={
|
|
|
|
|
"error": "internal_error",
|
|
|
|
|
"message": f"Failed to get stock history: {str(e)}"
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/{stock_code}/kline",
|
|
|
|
|
response_model=KLineChartResponse,
|
|
|
|
|
responses={
|
|
|
|
|
200: {"description": "K-line chart data"},
|
|
|
|
|
404: {"description": "Stock not found", "model": ErrorResponse},
|
|
|
|
|
500: {"description": "Server error", "model": ErrorResponse},
|
|
|
|
|
},
|
|
|
|
|
summary="Get stock K-line chart",
|
|
|
|
|
description="Get stock K-line chart data formatted for ECharts"
|
|
|
|
|
)
|
|
|
|
|
def get_stock_kline(
|
|
|
|
|
stock_code: str,
|
|
|
|
|
days: int = Query(60, ge=1, le=365, description="Number of days"),
|
|
|
|
|
service: MarketService = Depends(get_market_service)
|
|
|
|
|
) -> KLineChartResponse:
|
|
|
|
|
"""
|
|
|
|
|
Get stock K-line chart data
|
|
|
|
|
|
|
|
|
|
Returns K-line data formatted for ECharts visualization
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
kline = service.get_stock_kline_chart(stock_code, days=days)
|
|
|
|
|
return kline
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to get stock kline: {e}", exc_info=True)
|
|
|
|
|
return KLineChartResponse(
|
|
|
|
|
categoryData=[],
|
|
|
|
|
values=[],
|
|
|
|
|
volumes=[],
|
|
|
|
|
stock_name=None
|
|
|
|
|
)
|