Files
tick-stock-panel/backend/app/api/sector_rotation.py
T
shy3130 7e379d717a feat(sector-rotation): 默认黑名单+成员上限防污染, 六维切换+强弱对半取样+0轴穿越统计
- 默认排除融资融券/沪深股通等巨型概念(可替换式自定义), 成员数>300 不参与自动统计, 不足展示行数时回退不过滤
- sort_by 扩展为 activity/score/pct/rank_change/momentum/flow 六维, momentum 前半取最强、后半取最弱
- 分桶 0 轴穿越检测: cross_up/cross_down 计数 + cross_events 事件流(最新在前, 上限 120)
- 响应新增 default_exclude_sectors/max_auto_members; exclude_sectors/auto_rows/sort_by 入参入缓存键, 缓存有界淘汰
2026-09-13 14:14:00 +08:00

65 lines
3.1 KiB
Python

"""板块切换(盘中轮动) API — 基于全量分钟数据 + 扩展资金流。薄层, 计算在 services。"""
from __future__ import annotations
import json
from typing import Literal
from fastapi import APIRouter, HTTPException, Query, Request
from app.services import sector_rotation
router = APIRouter(prefix="/api/sector-rotation", tags=["sector-rotation"])
@router.get("")
def get_sector_rotation(
request: Request,
kind: Literal["concept", "industry"] = Query("concept", description="板块维度: 概念/行业 二选一"),
flow: str | None = Query(None, max_length=200, description="资金流扩展列 (表id.列名), 缺省纯涨幅"),
top: int = Query(30, ge=5, le=100, description="返回板块数上限"),
bucket: int = Query(5, description="分钟桶粒度 (1/5/15)"),
series_names: str | None = Query(None, max_length=2000, description='自定义展示板块 JSON 数组, 如 ["A题材","B题材"]; 缺省=自动榜'),
exclude_sectors: str | None = Query(None, max_length=4000, description='自动活跃榜排除板块 JSON 字符串数组; 缺省=内置属性板块名单, 传 [] = 清空名称过滤'),
auto_rows: int | None = Query(None, ge=1, le=20, description="自动模式展示行数 (前N), 缺省 10"),
sort_by: Literal["activity", "score", "pct", "rank_change", "momentum", "flow"] = Query(
"activity",
description="自动榜排序维度: activity=近30分钟成交额, score=综合分, pct=现涨幅, rank_change=1h排名跃升, momentum=近1小时动量(走强→走弱), flow=扩展资金流",
),
):
"""盘中板块切换走势: 全量分钟K聚合到板块, 涨幅 + 扩展资金流综合评分。
series_names 提供时展示矩阵仅含这些板块 (自定义监控, ≤20, 当日无行情的剔除, 不过滤),
缺省为自动榜 (sort_by 维度降序, 先剔除排除名单与超成员数上限的板块,
不足时回退不过滤)。
数据不可用时返回明确的 no_data/empty 状态与原因, 不静默。
"""
names: list[str] | None = None
if series_names:
names = _parse_str_array(series_names, "series_names")
excluded: list[str] | None = None
if exclude_sectors is not None:
excluded = _parse_str_array(exclude_sectors, "exclude_sectors")
return sector_rotation.build_sector_rotation(
request.app.state.repo,
kind=kind,
flow_field=flow,
top=top,
bucket_minutes=bucket,
series_names=names,
exclude_sectors=excluded,
auto_rows=auto_rows,
sort_by=sort_by,
)
def _parse_str_array(raw: str, param_name: str) -> list[str]:
"""解析 JSON 字符串数组查询参数 (strip + 去空), 非法时报 400。"""
try:
parsed = json.loads(raw)
except json.JSONDecodeError as exc:
raise HTTPException(status_code=400, detail=f"{param_name} 需为 JSON 字符串数组") from exc
if not isinstance(parsed, list) or any(not isinstance(x, str) for x in parsed):
raise HTTPException(status_code=400, detail=f"{param_name} 需为字符串数组")
return [x for x in parsed if x.strip()]