feat(market): 添加市场分析数据基础架构和功能模块 - 新增市场分析相关数据库表结构,包括市场工具、每日报价、财务指标、 宏观指标和公告等数据模型 - 创建市场分析相关的Alembic迁移脚本,包含完整的up和downgrade逻辑 - 集成市场分析路由到主API路由器中 - 添加市场数据定时任务调度,支持盘前、收盘和周度市场分析报告 - 实现市场数据后台任务队列,包含报告生成和收盘分析功能 - 扩展系统配置设置,添加市场分析启用开关和相关参数配置 - 增加AI智能技能支持,包含市场概览分析和股票分析功能 - 添加审计日志记录,支持市场数据同步和自选股更新操作追踪 - 实现飞书命令集成,支持股票分析、自选股管理、公告查询等交互 - 提供市场数据服务层,包含行业分析、股票对比、市场概览等功能 ```
63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
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)
|