实现全部 CHOICE 财务接口, 补充测试/文档/部署配置
- 新增 9 个抓取模块: 三大报表/盈利能力/资本结构/营运能力/成长能力/杜邦/分红 - api.py 新增 10 个 /fin/* 接口并统一无数据返回 - 新增 _emchoice.py 共享请求辅助 (emws 跳转取 URL / datacenter 报表拉取) - 新增冒烟测试 tests/test_interfaces.py (24 用例) - 新增接口文档 api_manual.md 与施工蓝图 blueprint.md - 新增 Dockerfile / docker-compose.yml / deploy.sh 部署配置
This commit is contained in:
@@ -16,6 +16,16 @@ import pandas as pd
|
||||
# 复用 fin_summary.py 里的核心逻辑
|
||||
from utils.fin_summary import get_fin_summary, _fetch_org_codes
|
||||
from utils.fin_quarterly import get_fin_quarterly_summary
|
||||
from utils.fin_main_business import get_main_business
|
||||
from utils.fin_balance import get_balance
|
||||
from utils.fin_income import get_income
|
||||
from utils.fin_cashflow import get_cashflow
|
||||
from utils.fin_profitability import get_profitability
|
||||
from utils.fin_capital_structure import get_capital_structure
|
||||
from utils.fin_operate import get_operate_ability
|
||||
from utils.fin_growth import get_growth
|
||||
from utils.fin_dupont import get_dupont
|
||||
from utils.fin_dividend import get_dividend_detail, get_dividend_statics
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
@@ -89,6 +99,19 @@ def _df_to_json_payload(df: pd.DataFrame) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _empty_response(message: str, codes: list[str], **extra) -> JSONResponse:
|
||||
"""无数据时的统一 404 返回."""
|
||||
content = {
|
||||
"success": False,
|
||||
"count": 0,
|
||||
"message": message,
|
||||
"codes_requested": codes,
|
||||
"data": [],
|
||||
}
|
||||
content.update(extra)
|
||||
return JSONResponse(status_code=404, content=content)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
return {
|
||||
@@ -97,11 +120,25 @@ def root():
|
||||
"endpoints": {
|
||||
"GET /fin/summary": "拉取财务摘要(报告期)",
|
||||
"GET /fin/quarterly-summary": "拉取季度财务摘要",
|
||||
"GET /fin/main-business": "拉取主营构成(按产品/按地区)",
|
||||
"GET /fin/balance": "拉取资产负债表",
|
||||
"GET /fin/income": "拉取利润表",
|
||||
"GET /fin/cashflow": "拉取现金流量表",
|
||||
"GET /fin/profitability": "拉取盈利能力与收益质量",
|
||||
"GET /fin/capital-structure": "拉取资本结构与偿债能力",
|
||||
"GET /fin/operate-ability": "拉取营运能力",
|
||||
"GET /fin/growth": "拉取成长能力",
|
||||
"GET /fin/dupont": "拉取杜邦分析",
|
||||
"GET /fin/dividend-detail": "拉取分红明细",
|
||||
"GET /fin/dividend-statics": "拉取分红统计",
|
||||
"GET /health": "健康检查",
|
||||
},
|
||||
"usage": {
|
||||
"fin_summary": "/fin/summary?stocks=603233.SH:10500736&years=10",
|
||||
"fin_quarterly_summary": "/fin/quarterly-summary?stocks=603233.SH&years=3",
|
||||
"fin_main_business": "/fin/main-business?stocks=603233.SH&classify_type=产品&years=3",
|
||||
"fin_balance": "/fin/balance?stocks=603233.SH&years=3",
|
||||
"fin_dividend_detail": "/fin/dividend-detail?stocks=603233.SH&years=5",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -221,6 +258,246 @@ def fin_quarterly_summary(
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/main-business")
|
||||
def fin_main_business(
|
||||
stocks: str = Query(
|
||||
...,
|
||||
description="股票代码,逗号分隔,例如 '603233.SH,600519.SH'。仅支持代码,不需要 ORG_CODE。",
|
||||
examples=["603233.SH"],
|
||||
),
|
||||
classify_type: str = Query(
|
||||
"产品",
|
||||
description="分类方式: 产品 或 地区",
|
||||
examples=["产品", "地区"],
|
||||
),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的主营构成数据"),
|
||||
):
|
||||
"""拉取主营构成(按产品或按地区)。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_main_business(codes, classify_type=classify_type, years=years)
|
||||
if df.empty:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"success": False,
|
||||
"count": 0,
|
||||
"message": "无主营构成数据, 请检查股票代码或筛选条件",
|
||||
"codes_requested": codes,
|
||||
"classify_type": classify_type,
|
||||
"data": [],
|
||||
},
|
||||
)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["classify_type"] = classify_type
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/balance")
|
||||
def fin_balance(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的资产负债表数据"),
|
||||
):
|
||||
"""拉取资产负债表。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_balance(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无资产负债表数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/income")
|
||||
def fin_income(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的利润表数据"),
|
||||
):
|
||||
"""拉取利润表。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_income(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无利润表数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/cashflow")
|
||||
def fin_cashflow(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的现金流量表数据"),
|
||||
):
|
||||
"""拉取现金流量表。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_cashflow(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无现金流量表数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/profitability")
|
||||
def fin_profitability(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的盈利能力与收益质量数据"),
|
||||
):
|
||||
"""拉取盈利能力与收益质量。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_profitability(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无盈利能力与收益质量数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/capital-structure")
|
||||
def fin_capital_structure(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的资本结构与偿债能力数据"),
|
||||
):
|
||||
"""拉取资本结构与偿债能力。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_capital_structure(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无资本结构与偿债能力数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/operate-ability")
|
||||
def fin_operate_ability(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的营运能力数据"),
|
||||
):
|
||||
"""拉取营运能力。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_operate_ability(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无营运能力数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/growth")
|
||||
def fin_growth(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的成长能力数据"),
|
||||
):
|
||||
"""拉取成长能力。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_growth(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无成长能力数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/dupont")
|
||||
def fin_dupont(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的杜邦分析数据"),
|
||||
):
|
||||
"""拉取杜邦分析。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_dupont(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无杜邦分析数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/dividend-detail")
|
||||
def fin_dividend_detail(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(5, ge=1, le=20, description="拉取最近多少年的分红明细"),
|
||||
):
|
||||
"""拉取分红明细。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_dividend_detail(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无分红明细数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/fin/dividend-statics")
|
||||
def fin_dividend_statics(
|
||||
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
|
||||
years: int = Query(5, ge=1, le=20, description="拉取最近多少年的分红统计"),
|
||||
):
|
||||
"""拉取分红统计。"""
|
||||
codes = [code.strip() for code in stocks.split(",") if code.strip()]
|
||||
if not codes:
|
||||
raise HTTPException(400, detail="stocks 参数不能为空")
|
||||
|
||||
df = get_dividend_statics(codes, years=years)
|
||||
if df.empty:
|
||||
return _empty_response("无分红统计数据, 请检查股票代码或筛选条件", codes, years=years)
|
||||
|
||||
payload = _df_to_json_payload(df)
|
||||
payload["codes_requested"] = codes
|
||||
payload["years"] = years
|
||||
return payload
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8765, log_level="info")
|
||||
Reference in New Issue
Block a user