mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
根因: 分析/复盘端点 yield meta 后等 LLM 首包期间流上零字节可达数分钟,
反向代理按空闲超时切断连接, 前端报裸 network error; 且 max_retries=0
使上游(DeepSeek)高峰过载时一次失败即终止。
- 新增 services/ndjson_heartbeat.with_heartbeat: 空闲超 15s 插入
{"type":"ping"} 行保活(间隔与 mining.py SSE 心跳一致)
- 5 个流式端点接入: 复盘/个股/财务/轮动/策略生成; 策略生成端点补齐
Cache-Control 与 X-Accel-Buffering 头
- AsyncOpenAI max_retries 0→2: SDK 重试仅覆盖首包前, 未产出内容, 安全
- 前端事件类型联合加入 'ping'(下游对未知类型静默忽略); 新增
friendlyStreamError 把浏览器裸网络错误翻译为可操作提示, 6 处落点应用
- 新增 test_ndjson_heartbeat.py 覆盖心跳插入/异常透传/断连清理/重试配置
兼容性: ping 为新增事件类型, 旧前端与 daily_pipeline 消费方静默跳过,
前后端可独立发布。
77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
"""涨幅轮动矩阵 API。
|
|
|
|
供「概念分析 → 涨幅RPS轮动」对话框调用。返回最近 N 个交易日的概念涨幅
|
|
排名矩阵:每列(日期)各自把所有概念按当天涨幅从高到低排序。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Query, Request
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.services import rps_rotation
|
|
from app.services.concept_rotation_analyzer import analyze_rotation_stream
|
|
from app.services.ndjson_heartbeat import with_heartbeat
|
|
|
|
router = APIRouter(prefix="/api/rps", tags=["rps"])
|
|
|
|
|
|
@router.get("/rotation")
|
|
def get_rotation(
|
|
request: Request,
|
|
days: int = Query(12, ge=7, le=30, description="最近 N 个交易日(7-30)"),
|
|
kind: str = Query("concept", pattern="concept|industry", description="维度: concept 概念 / industry 行业"),
|
|
level: int | None = Query(None, ge=1, le=3, description="行业层级(仅 kind=industry): 1/2/3 级"),
|
|
) -> dict:
|
|
"""维度涨幅轮动矩阵(概念或行业)。
|
|
|
|
Returns:
|
|
dates: 日期字符串列表(最新在最前)
|
|
columns: {日期: [[成员名, 涨幅小数], ...]} 每列各自降序
|
|
concept_count: 去重维度成员总数
|
|
"""
|
|
return rps_rotation.build_rps_rotation(request.app.state.repo, days, kind, level)
|
|
|
|
|
|
class AnalyzeRequest(BaseModel):
|
|
"""AI 维度轮动分析请求(概念或行业)。"""
|
|
days: int = 12 # 分析最近 N 个交易日
|
|
focus: str = "" # 用户追加的关注点
|
|
kind: str = "concept" # "concept" 概念 / "industry" 行业
|
|
level: int | None = None # 行业层级(1/2/3), 仅 kind=industry 有效
|
|
|
|
|
|
@router.post("/rotation-analyze")
|
|
async def analyze_rotation(request: Request, req: AnalyzeRequest):
|
|
"""AI 维度轮动分析 — NDJSON 流式返回。
|
|
|
|
装配轮动矩阵信号 + 大盘背景 → 分析提示词 → 流式调用 LLM →
|
|
逐 chunk 以 NDJSON 推给前端(每行一个 JSON)。
|
|
|
|
协议:
|
|
{"type":"meta","days","summary"}
|
|
{"type":"delta","content":"..."}
|
|
{"type":"error","message":"..."}
|
|
{"type":"done"}
|
|
"""
|
|
repo = request.app.state.repo
|
|
quote_service = getattr(request.app.state, "quote_service", None)
|
|
depth_service = getattr(request.app.state, "depth_service", None)
|
|
days = max(7, min(30, req.days))
|
|
kind = "industry" if req.kind == "industry" else "concept"
|
|
level = req.level if (kind == "industry" and req.level in (1, 2, 3)) else None
|
|
|
|
async def stream_gen():
|
|
async for chunk in with_heartbeat(
|
|
analyze_rotation_stream(
|
|
repo, days, req.focus, quote_service, depth_service, kind, level,
|
|
),
|
|
):
|
|
yield chunk + "\n"
|
|
|
|
return StreamingResponse(
|
|
stream_gen(),
|
|
media_type="application/x-ndjson",
|
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
)
|