Files
tick-stock-panel/backend/app/api/financials.py
T
shy3130 a5a04b4dd8 fix: 端点套餐门控/探测重试/分钟K月单位 + 财务页新功能 + localStorage 残留自愈
## Bug 修复
- 端点测速:Expert+ 用户现可选用专线端点(premium),非 Expert 仍锁定(EndpointTestDialog)
- 能力探测:try_call 对瞬时错误(429/5xx/超时)重试,避免抖动误丢 capability(导致"需 Free"误导徽章)
- 数据画像 CapBadge:tierReq='Free' 时不显示无意义的"需 Free"徽章
- 财务同步:用服务端 is_syncing 标志驱动 UI,刷新后仍正确显示"同步中",防重复点击 + 进度提示

## 新功能
- 财务页重设计:中间模糊搜索框(代码/名称),选股后展示 4 标签页详情
  (核心指标/利润表/资负表/现金流),字段格式化(百分点/金额转亿/每股)
- 分钟K向前扩展:Expert+ 新增「月」单位(1~6 月,180 天),Pro 仍只「天」(15 天)

## localStorage 残留自愈(拉新代码后不再显示本地开发残留)
- 策略池:加载后自动清除失效的自定义策略 ID
- 回测页:校验 selectedStrategy 是否存在,失效则重置;result 不跨会话恢复

## 重构
- 提取共享 tier 工具到 capability-labels.ts(TIER_RANK/isExpertOrAbove)
2026-06-19 00:10:01 +08:00

128 lines
4.2 KiB
Python

"""财务数据 API — 独立路由, Cap.FINANCIAL 门控。"""
from __future__ import annotations
import logging
import polars as pl
from fastapi import APIRouter, HTTPException, Request
from app.services.financial_sync import get_financial_df
from app.tickflow.capabilities import Cap
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/financials", tags=["financials"])
@router.get("/status")
def financial_status(request: Request):
"""返回各财务表的同步状态。无需 FINANCIAL 权限(前端根据 available 决定是否展示)。"""
capset = request.app.state.capabilities
if not capset.has(Cap.FINANCIAL):
return {"available": False, "tables": {}}
data_dir = request.app.state.repo.store.data_dir
tables = {}
for table in ("metrics", "income", "balance_sheet", "cash_flow"):
path = data_dir / "financials" / table / "part.parquet"
if path.exists():
try:
df = pl.read_parquet(path, columns=["symbol"])
tables[table] = {
"rows": len(df),
"symbols": df["symbol"].n_unique() if not df.is_empty() else 0,
}
except Exception:
tables[table] = {"rows": 0, "symbols": 0}
else:
tables[table] = {"rows": 0, "symbols": 0}
fs = getattr(request.app.state, "financial_scheduler", None)
last_sync = fs.last_sync if fs else {}
return {
"available": True,
"tables": tables,
"last_sync": last_sync,
# 服务端是否正在同步(手动触发)——前端据此显示"同步中"并防重复点击,
# 且刷新页面后仍能正确反映服务端状态。
"syncing": bool(fs and fs.is_syncing),
}
@router.get("/metrics")
def get_metrics(request: Request, symbol: str | None = None):
"""查询核心财务指标。"""
capset = request.app.state.capabilities
capset.require(Cap.FINANCIAL)
df = get_financial_df(request.app.state.repo.store.data_dir, "metrics")
if df.is_empty():
return {"data": []}
if symbol:
df = df.filter(pl.col("symbol") == symbol)
return {"data": df.to_dicts()}
@router.get("/income")
def get_income(request: Request, symbol: str | None = None):
"""查询利润表。"""
capset = request.app.state.capabilities
capset.require(Cap.FINANCIAL)
df = get_financial_df(request.app.state.repo.store.data_dir, "income")
if df.is_empty():
return {"data": []}
if symbol:
df = df.filter(pl.col("symbol") == symbol)
return {"data": df.to_dicts()}
@router.get("/balance-sheet")
def get_balance_sheet(request: Request, symbol: str | None = None):
"""查询资产负债表。"""
capset = request.app.state.capabilities
capset.require(Cap.FINANCIAL)
df = get_financial_df(request.app.state.repo.store.data_dir, "balance_sheet")
if df.is_empty():
return {"data": []}
if symbol:
df = df.filter(pl.col("symbol") == symbol)
return {"data": df.to_dicts()}
@router.get("/cash-flow")
def get_cash_flow(request: Request, symbol: str | None = None):
"""查询现金流量表。"""
capset = request.app.state.capabilities
capset.require(Cap.FINANCIAL)
df = get_financial_df(request.app.state.repo.store.data_dir, "cash_flow")
if df.is_empty():
return {"data": []}
if symbol:
df = df.filter(pl.col("symbol") == symbol)
return {"data": df.to_dicts()}
@router.post("/sync/{table}")
def sync_table(request: Request, table: str):
"""手动触发同步。table: metrics / income / balance_sheet / cash_flow / all"""
capset = request.app.state.capabilities
capset.require(Cap.FINANCIAL)
valid_tables = {"metrics", "income", "balance_sheet", "cash_flow", "all"}
if table not in valid_tables:
raise HTTPException(400, f"invalid table: {table}, expected one of {valid_tables}")
fs = getattr(request.app.state, "financial_scheduler", None)
if not fs:
return {"status": "error", "message": "FinancialScheduler not available"}
target = None if table == "all" else table
result = fs.run_now(target)
return {"status": "ok", "synced": result}