feat(market): 添加市场分析数据基础架构和功能模块

- 新增市场分析相关数据库表结构,包括市场工具、每日报价、财务指标、
  宏观指标和公告等数据模型
- 创建市场分析相关的Alembic迁移脚本,包含完整的up和downgrade逻辑
- 集成市场分析路由到主API路由器中
- 添加市场数据定时任务调度,支持盘前、收盘和周度市场分析报告
- 实现市场数据后台任务队列,包含报告生成和收盘分析功能
- 扩展系统配置设置,添加市场分析启用开关和相关参数配置
- 增加AI智能技能支持,包含市场概览分析和股票分析功能
- 添加审计日志记录,支持市场数据同步和自选股更新操作追踪
- 实现飞书命令集成,支持股票分析、自选股管理、公告查询等交互
- 提供市场数据服务层,包含行业分析、股票对比、市场概览等功能
```
This commit is contained in:
2026-07-12 22:05:23 +08:00
parent 9cf7c44393
commit f8020cab56
28 changed files with 2704 additions and 60 deletions

View File

@@ -0,0 +1,62 @@
from io import BytesIO
from typing import Any
from PIL import Image, ImageDraw, ImageFont
def render_market_chart(report: dict[str, Any]) -> bytes:
image = Image.new("RGB", (1200, 720), (248, 250, 252))
draw = ImageDraw.Draw(image)
title = _font(34)
label = _font(22)
chart = report.get("chart_data") or {}
if "quotes" in chart:
draw.text(
(55, 35),
f"Stock Analysis: {chart.get('symbol') or ''}",
fill=(17, 24, 39),
font=title,
)
quotes = chart["quotes"]
values = [float(item["close"]) for item in quotes]
if values:
low, high = min(values), max(values)
span = high - low or 1
points = []
for index, value in enumerate(values):
x = 70 + index * 1060 / max(len(values) - 1, 1)
y = 610 - (value - low) * 480 / span
points.append((x, y))
draw.line(points, fill=(37, 99, 235), width=5)
draw.text(
(70, 630),
f"最低 {low:.2f} 最高 {high:.2f} 最新 {values[-1]:.2f}",
fill=(75, 85, 99),
font=label,
)
else:
draw.text((55, 35), "A-Share Market Overview", fill=(17, 24, 39), font=title)
values = [
("Advances", int(chart.get("advances") or 0)),
("Declines", int(chart.get("declines") or 0)),
("Flat", int(chart.get("flat") or 0)),
]
maximum = max((value for _, value in values), default=1) or 1
for index, (name, value) in enumerate(values):
y = 170 + index * 150
draw.text((70, y), f"{name} {value:,}", fill=(75, 85, 99), font=label)
draw.rectangle((70, y + 45, 1120, y + 82), fill=(209, 213, 219))
draw.rectangle(
(70, y + 45, 70 + 1050 * value / maximum, y + 82),
fill=((37, 99, 235), (220, 38, 38), (245, 158, 11))[index],
)
output = BytesIO()
image.save(output, format="PNG", optimize=True)
return output.getvalue()
def _font(size: int) -> ImageFont.ImageFont:
try:
return ImageFont.truetype("DejaVuSans.ttf", size=size)
except OSError:
return ImageFont.load_default(size=size)