mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 21:24:16 +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
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
"""AI 个股分析报告持久化存储。
|
|
|
|
与 ai_reports.py(财务分析报告)完全独立 —— 单独的文件、字段、上限,
|
|
互不影响。刻意不复用,避免引入 kind 判别字段与分支(解耦 > 抽象)。
|
|
|
|
存储位置: data/user_data/ai_stock_reports.json (数组,按 created_at 降序)
|
|
保留最近 MAX_REPORTS 条;超出自动裁剪最旧的。
|
|
|
|
每条报告结构:
|
|
{
|
|
"id": "sar_xxx", # 唯一 id(stock-analysis-report)
|
|
"symbol": "600519.SH",
|
|
"name": "贵州茅台",
|
|
"focus": "", # 用户追加的关心点(可为空)
|
|
"content": "# ...markdown", # 报告正文
|
|
"summary": "当前价 12.3 · 压力位...", # 价位/数据摘要
|
|
"levels": {...}, # 报告生成时的关键价位(供图表回放)
|
|
"close": 12.3, # 报告生成时的收盘价
|
|
"created_at": "2026-06-26T10:00:00"
|
|
}
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import time
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_REPORTS = 50
|
|
|
|
|
|
def _path() -> Path:
|
|
from app.config import settings
|
|
p = settings.data_dir / "user_data" / "ai_stock_reports.json"
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
return p
|
|
|
|
|
|
def list_reports() -> list[dict]:
|
|
"""返回全部报告(按 created_at 降序)。"""
|
|
p = _path()
|
|
if not p.exists():
|
|
return []
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
if isinstance(data, list):
|
|
return sorted(data, key=lambda r: r.get("created_at", ""), reverse=True)
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("ai_stock_reports.json malformed: %s", e)
|
|
return []
|
|
|
|
|
|
def _save_all(reports: list[dict]) -> None:
|
|
"""全量写入(裁剪到 MAX_REPORTS)。"""
|
|
reports.sort(key=lambda r: r.get("created_at", ""), reverse=True)
|
|
if len(reports) > MAX_REPORTS:
|
|
reports = reports[:MAX_REPORTS]
|
|
_path().write_text(
|
|
json.dumps(reports, indent=2, ensure_ascii=False), encoding="utf-8",
|
|
)
|
|
|
|
|
|
def save_report(report: dict) -> dict:
|
|
"""新增一条报告并持久化。返回保存后的报告(含 id / created_at)。"""
|
|
reports = list_reports()
|
|
if not report.get("id"):
|
|
report["id"] = f"sar_{int(time.time() * 1000)}_{report.get('symbol', 'x')}"
|
|
if not report.get("created_at"):
|
|
report["created_at"] = _now_iso()
|
|
reports.append(report)
|
|
_save_all(reports)
|
|
logger.info("Stock report saved: %s (%s), total %d", report.get("symbol"), report.get("id"), len(reports))
|
|
return report
|
|
|
|
|
|
def delete_report(report_id: str) -> bool:
|
|
"""删除指定报告。返回是否删除成功。"""
|
|
reports = list_reports()
|
|
before = len(reports)
|
|
reports = [r for r in reports if r.get("id") != report_id]
|
|
if len(reports) < before:
|
|
_save_all(reports)
|
|
return True
|
|
return False
|
|
|
|
|
|
def _now_iso() -> str:
|
|
from datetime import datetime
|
|
return datetime.now().isoformat(timespec="seconds")
|