mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 19:04:15 +08:00
新增个股分析页,以行情与关键价位为视觉主体,与财务分析(财务质量评级)定位互补: 后端: - indicators/levels.py: 9 类关键价位纯函数计算 · 压力支撑(布林带)/ 成交密集区(POC)/ 枢轴点(可配档位)/ 前高前低 · Keltner通道(MA±n×ATR 短/中/长)/ ATR止损(close±nATR) · 缺口位(未回补跳空)/ 斐波那契回撤 / 整数关口(自适应步长) - services/stock_analyzer.py: AI 四维(技术/基本面/财务/消息面)流式分析,NDJSON - services/stock_reports.py: 报告持久化(最多50条),独立 JSON 存储 - api/stock_analysis.py: levels/analyze/reports 端点 前端: - AnalysisKChart: 专用 ECharts 日K(主图+成交量+滑块),9 类价位开关 - 蓝色胶囊(Bubble/Host/Dialog),与财务分析紫色主题并存 - stockAnalysisStore: useSyncExternalStore 全局存储,后台流式 - useLastStock + LastStockChip: 记忆最近查看个股(财务/个股分析页) 其他优化: - AI 设置页:一键清空配置(保留UA) + 保存后乐观更新连接状态/菜单 - MarkdownRenderer: 去除 space-y-0 遮蔽,各块间距与分隔线可见性优化 - 个股分析页 Beta 标识(标题 + 菜单) - 版本号 0.1.45 → 0.1.50
134 lines
4.2 KiB
Python
134 lines
4.2 KiB
Python
"""个股分析 API — 关键价位 + AI 四维分析 + 报告持久化。
|
|
|
|
路由前缀: /api/stock-analysis
|
|
|
|
端点:
|
|
GET /levels?symbol= 4 类关键价位(图表 markLine 数据源)
|
|
POST /analyze AI 流式四维分析(NDJSON)
|
|
GET /reports 历史报告列表
|
|
POST /reports 保存一条报告
|
|
DELETE /reports/{report_id} 删除一条报告
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import date, timedelta
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.indicators.levels import compute_levels, summarize_levels
|
|
from app.services import stock_reports
|
|
from app.services.stock_analyzer import analyze_stock_stream
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/stock-analysis", tags=["stock-analysis"])
|
|
|
|
|
|
@router.get("/levels")
|
|
def get_levels(
|
|
request: Request,
|
|
symbol: str = Query(..., description="标的代码,如 000001.SZ"),
|
|
days: int = Query(120, ge=30, le=500, description="计算样本天数"),
|
|
):
|
|
"""计算 4 类关键价位(压力支撑 / 成交密集区 / 枢轴点 / 前高前低)。
|
|
|
|
返回 {levels: {sr, profile, pivot, extreme}, close, summary}。
|
|
前端按 levels 的 key 渲染开关按钮,逐组显隐 markLine。
|
|
"""
|
|
if not symbol:
|
|
raise HTTPException(400, "symbol 不能为空")
|
|
|
|
repo = request.app.state.repo
|
|
end = date.today()
|
|
start = end - timedelta(days=days * 2)
|
|
df = repo.get_daily(symbol, start, end)
|
|
if df.is_empty():
|
|
return {"levels": {"sr": [], "profile": [], "pivot": [], "extreme": [],
|
|
"keltner": [], "atr_stop": [], "gap": [], "fib": [], "round": []},
|
|
"close": None, "summary": "无数据", "symbol": symbol}
|
|
|
|
levels = compute_levels(df)
|
|
close = float(df.tail(1)["close"][0]) if "close" in df.columns else None
|
|
return {
|
|
"levels": levels,
|
|
"close": close,
|
|
"summary": summarize_levels(levels, close),
|
|
"symbol": symbol,
|
|
}
|
|
|
|
|
|
class AnalyzeRequest(BaseModel):
|
|
"""AI 个股分析请求。"""
|
|
symbol: str
|
|
focus: str = "" # 可选:用户追加的分析关注点
|
|
|
|
|
|
@router.post("/analyze")
|
|
async def analyze_stock(request: Request, req: AnalyzeRequest):
|
|
"""AI 个股四维分析 — NDJSON 流式返回。
|
|
|
|
组合 K 线(技术指标)+ 财务表 + 关键价位 → 实战派提示词 →
|
|
流式调用 LLM → 逐 chunk 以 NDJSON 推给前端(每行一个 JSON)。
|
|
"""
|
|
if not req.symbol:
|
|
raise HTTPException(400, "symbol 不能为空")
|
|
|
|
repo = request.app.state.repo
|
|
data_dir = repo.store.data_dir
|
|
|
|
async def stream_gen():
|
|
async for chunk in analyze_stock_stream(repo, data_dir, req.symbol, req.focus):
|
|
yield chunk + "\n"
|
|
|
|
return StreamingResponse(
|
|
stream_gen(),
|
|
media_type="application/x-ndjson",
|
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
)
|
|
|
|
|
|
# ================================================================
|
|
# 报告 CRUD(历史报告持久化)
|
|
# ================================================================
|
|
|
|
class SaveReportRequest(BaseModel):
|
|
"""保存一条 AI 个股分析报告。"""
|
|
symbol: str
|
|
name: str = ""
|
|
focus: str = ""
|
|
content: str
|
|
summary: str = ""
|
|
close: float | None = None
|
|
levels: dict | None = None
|
|
|
|
|
|
@router.get("/reports")
|
|
def list_reports(request: Request):
|
|
"""获取全部历史报告(按时间降序,后端已裁剪到上限)。"""
|
|
return {"reports": stock_reports.list_reports()}
|
|
|
|
|
|
@router.post("/reports")
|
|
def save_report(request: Request, req: SaveReportRequest):
|
|
"""保存一条报告。"""
|
|
report = stock_reports.save_report({
|
|
"symbol": req.symbol,
|
|
"name": req.name,
|
|
"focus": req.focus,
|
|
"content": req.content,
|
|
"summary": req.summary,
|
|
"close": req.close,
|
|
"levels": req.levels,
|
|
})
|
|
return {"ok": True, "report": report}
|
|
|
|
|
|
@router.delete("/reports/{report_id}")
|
|
def delete_report(request: Request, report_id: str):
|
|
"""删除一条报告。"""
|
|
ok = stock_reports.delete_report(report_id)
|
|
return {"ok": ok}
|