""" AKShare HTTP API 服务 提供股票数据接口和数据同步功能 """ import os from datetime import datetime from typing import Optional, List import akshare as ak import pandas as pd from fastapi import FastAPI, HTTPException, Query, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from database import check_connection, init_db, get_db, Stock, StockKLine from data_sync import ( sync_all_stocks, sync_realtime_quotes, sync_stock_kline, sync_sectors, sync_sector_quotes, sync_market_indices, sync_all_klines, sync_all, sync_daily ) app = FastAPI( title="AKShare HTTP API", description="AKShare 数据接口 HTTP 服务 - 支持数据同步到 MySQL", version="2.0.0" ) # 配置 CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) def dataframe_to_records(df: pd.DataFrame) -> List[dict]: """将 DataFrame 转换为可 JSON 序列化的记录列表""" if df is None or df.empty: return [] df = df.replace({pd.NaT: None}) df = df.where(pd.notnull(df), None) return df.to_dict('records') # ==================== 健康检查 ==================== @app.get("/") async def root(): """健康检查""" db_status = "connected" if check_connection() else "disconnected" return { "status": "healthy", "service": "AKShare HTTP API", "version": "2.0.0", "database": db_status, "timestamp": datetime.now().isoformat() } @app.get("/health") async def health_check(): """详细健康检查""" db_ok = check_connection() return { "status": "healthy" if db_ok else "unhealthy", "database": "connected" if db_ok else "disconnected", "timestamp": datetime.now().isoformat() } # ==================== 原始 AKShare 接口 ==================== @app.get("/stock_zh_a_spot") async def stock_zh_a_spot(): """ 获取 A 股实时行情数据 """ try: df = ak.stock_zh_a_spot_em() records = dataframe_to_records(df) mapped_records = [] for record in records: mapped_records.append({ "code": record.get("代码"), "name": record.get("名称"), "price": record.get("最新价", 0), "change": record.get("涨跌额", 0), "change_percent": record.get("涨跌幅", 0), "volume": record.get("成交量", 0), "turnover": record.get("成交额", 0), "open": record.get("开盘价", 0), "high": record.get("最高价", 0), "low": record.get("最低价", 0), "pre_close": record.get("昨收", 0), "turnover_rate": record.get("换手率", 0), "amplitude": record.get("振幅", 0), "market_cap": record.get("总市值", 0), "pe": record.get("市盈率-动态", 0), "pb": record.get("市净率", 0), "industry": record.get("行业", ""), }) return mapped_records except Exception as e: raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}") @app.get("/stock_zh_a_hist") async def stock_zh_a_hist( symbol: str = Query(..., description="股票代码,如 000001"), period: str = Query("daily", description="周期: daily/weekly/monthly"), start_date: Optional[str] = Query(None, description="开始日期 YYYYMMDD"), end_date: Optional[str] = Query(None, description="结束日期 YYYYMMDD"), adjust: str = Query("qfq", description="复权方式: qfq-前复权, hfq-后复权, 不复权") ): """ 获取 A 股历史 K 线数据 """ try: period_map = { "daily": "daily", "weekly": "weekly", "monthly": "monthly" } ak_period = period_map.get(period, "daily") adjust_map = { "qfq": "qfq", "hfq": "hfq", "": "" } ak_adjust = adjust_map.get(adjust, "qfq") df = ak.stock_zh_a_hist( symbol=symbol, period=ak_period, start_date=start_date or "19700101", end_date=end_date or "20500101", adjust=ak_adjust ) records = dataframe_to_records(df) mapped_records = [] for record in records: mapped_records.append({ "date": record.get("日期"), "open": record.get("开盘", 0), "high": record.get("最高", 0), "low": record.get("最低", 0), "close": record.get("收盘", 0), "volume": record.get("成交量", 0), "turnover": record.get("成交额", 0), "amplitude": record.get("振幅", 0), "change": record.get("涨跌幅", 0), "change_amount": record.get("涨跌额", 0), "turnover_rate": record.get("换手率", 0), }) return mapped_records except Exception as e: raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}") @app.get("/stock_zh_index_spot") async def stock_zh_index_spot(): """ 获取股票指数实时行情 """ try: df = ak.index_zh_a_spot_em() records = dataframe_to_records(df) mapped_records = [] for record in records: mapped_records.append({ "code": record.get("代码"), "name": record.get("名称"), "price": record.get("最新价", 0), "change": record.get("涨跌额", 0), "change_percent": record.get("涨跌幅", 0), "volume": record.get("成交量", 0), "turnover": record.get("成交额", 0), "open": record.get("开盘价", 0), "high": record.get("最高价", 0), "low": record.get("最低价", 0), "pre_close": record.get("昨收", 0), }) return mapped_records except Exception as e: raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}") @app.get("/stock_sector_spot") async def stock_sector_spot(): """ 获取板块行情数据 """ try: df = ak.stock_board_industry_name_em() records = dataframe_to_records(df) mapped_records = [] for record in records: mapped_records.append({ "code": record.get("代码"), "name": record.get("名称"), "change_percent": record.get("涨跌幅", 0), }) return mapped_records except Exception as e: raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}") # ==================== 数据库查询接口 ==================== @app.get("/db/stocks") async def db_stocks( limit: int = Query(100, description="限制条数"), offset: int = Query(0, description="偏移量") ): """从数据库获取股票列表""" try: with get_db() as db: stocks = db.query(Stock).offset(offset).limit(limit).all() return [ { "code": s.code, "name": s.name, "pe": s.pe, "pb": s.pb, "sector": s.sectorCode, } for s in stocks ] except Exception as e: raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") @app.get("/db/klines/{symbol}") async def db_klines( symbol: str, period: str = Query("day", description="周期: day/week/month"), limit: int = Query(60, description="限制条数") ): """从数据库获取K线数据""" try: with get_db() as db: klines = db.query(StockKLine).filter( StockKLine.stockCode == symbol, StockKLine.period == period ).order_by(StockKLine.date.desc()).limit(limit).all() return [ { "date": k.date.strftime("%Y-%m-%d"), "open": k.open, "high": k.high, "low": k.low, "close": k.close, "volume": k.volume, } for k in reversed(klines) ] except Exception as e: raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") # ==================== 数据同步接口 ==================== @app.post("/sync/stocks") async def api_sync_stocks(background_tasks: BackgroundTasks): """同步股票列表(后台任务)""" background_tasks.add_task(sync_all_stocks) return {"message": "股票列表同步已启动", "status": "running"} @app.post("/sync/quotes") async def api_sync_quotes(background_tasks: BackgroundTasks): """同步实时行情(后台任务)""" background_tasks.add_task(sync_realtime_quotes) return {"message": "实时行情同步已启动", "status": "running"} @app.post("/sync/klines") async def api_sync_klines( background_tasks: BackgroundTasks, days: int = Query(365, description="同步天数"), max_stocks: Optional[int] = Query(None, description="最大股票数量") ): """同步K线数据(后台任务)""" background_tasks.add_task(sync_all_klines, days=days, max_stocks=max_stocks) return {"message": f"K线同步已启动({days}天)", "status": "running"} @app.post("/sync/all") async def api_sync_all( background_tasks: BackgroundTasks, quick: bool = Query(False, description="快速模式(只同步少量数据)") ): """执行全量同步(后台任务)""" background_tasks.add_task(sync_all, quick=quick) return {"message": "全量同步已启动", "status": "running", "quick": quick} @app.post("/sync/daily") async def api_sync_daily(background_tasks: BackgroundTasks): """执行每日增量同步(后台任务)""" background_tasks.add_task(sync_daily) return {"message": "每日增量同步已启动", "status": "running"} @app.get("/sync/status") async def sync_status(): """获取同步状态""" db_ok = check_connection() # 获取统计数据 stats = {} try: with get_db() as db: from database import StockQuote, Sector, MarketIndex stats["stocks"] = db.query(Stock).count() stats["quotes"] = db.query(StockQuote).count() stats["klines"] = db.query(StockKLine).count() stats["sectors"] = db.query(Sector).count() stats["indices"] = db.query(MarketIndex).count() except Exception as e: stats["error"] = str(e) return { "database_connected": db_ok, "timestamp": datetime.now().isoformat(), "stats": stats } # ==================== 初始化 ==================== @app.on_event("startup") async def startup_event(): """启动时初始化数据库""" print("正在初始化数据库...") if check_connection(): init_db() print("数据库初始化完成") else: print("警告:数据库连接失败") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)