claude强化功能
This commit is contained in:
495
backend/financial_analysis.py
Normal file
495
backend/financial_analysis.py
Normal file
@@ -0,0 +1,495 @@
|
||||
"""财报深度解读 — 关键指标趋势、AI摘要、同行对比、异常预警。
|
||||
|
||||
功能:
|
||||
1. 财报关键指标趋势
|
||||
2. AI财报摘要
|
||||
3. 同行对比
|
||||
4. 财报异常预警
|
||||
5. 财报发布日历
|
||||
"""
|
||||
import datetime as dt
|
||||
import json
|
||||
from typing import List, Dict, Any, Optional
|
||||
from collections import defaultdict
|
||||
import numpy as np
|
||||
from sqlalchemy import select, and_, func, desc
|
||||
|
||||
from db import get_session
|
||||
from models import FinancialReport, Security, StockMetric
|
||||
import llm
|
||||
|
||||
|
||||
def seed_sample_reports():
|
||||
"""生成示例财报数据(用于演示)"""
|
||||
with get_session() as s:
|
||||
stocks = s.execute(
|
||||
select(Security.code, Security.name).limit(50)
|
||||
).all()
|
||||
|
||||
saved = 0
|
||||
for code, name in stocks:
|
||||
# 生成最近4个季度的财报
|
||||
base_date = dt.date(2023, 12, 31)
|
||||
|
||||
for i in range(4):
|
||||
report_date = base_date - dt.timedelta(days=i * 90)
|
||||
publish_date = report_date + dt.timedelta(days=30)
|
||||
|
||||
# 检查是否已存在
|
||||
exists = s.execute(
|
||||
select(FinancialReport).where(
|
||||
and_(
|
||||
FinancialReport.code == code,
|
||||
FinancialReport.report_date == report_date
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if exists:
|
||||
continue
|
||||
|
||||
# 模拟财务数据
|
||||
base_revenue = np.random.uniform(10, 500)
|
||||
growth = np.random.uniform(-20, 50)
|
||||
|
||||
report = FinancialReport(
|
||||
code=code,
|
||||
name=name,
|
||||
report_date=report_date,
|
||||
publish_date=publish_date,
|
||||
report_type='Q' + str((report_date.month // 3) or 4),
|
||||
revenue=round(base_revenue * (1 + i * 0.1), 2),
|
||||
net_profit=round(base_revenue * np.random.uniform(0.05, 0.2), 2),
|
||||
roe=round(np.random.uniform(5, 25), 2),
|
||||
gross_margin=round(np.random.uniform(20, 60), 2),
|
||||
revenue_growth=round(growth, 2),
|
||||
profit_growth=round(growth + np.random.uniform(-10, 10), 2),
|
||||
inventory=round(base_revenue * np.random.uniform(0.1, 0.3), 2),
|
||||
receivable=round(base_revenue * np.random.uniform(0.15, 0.4), 2),
|
||||
debt_ratio=round(np.random.uniform(30, 70), 2)
|
||||
)
|
||||
s.add(report)
|
||||
saved += 1
|
||||
|
||||
s.commit()
|
||||
|
||||
return {"ok": True, "saved": saved}
|
||||
|
||||
|
||||
def get_report_trend(code: str, periods: int = 8) -> Dict[str, Any]:
|
||||
"""获取财报关键指标趋势
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
periods: 统计期数
|
||||
|
||||
Returns:
|
||||
趋势数据
|
||||
"""
|
||||
with get_session() as s:
|
||||
reports = s.execute(
|
||||
select(FinancialReport)
|
||||
.where(FinancialReport.code == code)
|
||||
.order_by(desc(FinancialReport.report_date))
|
||||
.limit(periods)
|
||||
).scalars().all()
|
||||
|
||||
if not reports:
|
||||
return {"ok": False, "msg": "暂无财报数据"}
|
||||
|
||||
# 反转顺序(从旧到新)
|
||||
reports = list(reversed(reports))
|
||||
|
||||
# 提取趋势数据
|
||||
dates = [r.report_date.isoformat() for r in reports]
|
||||
|
||||
trend_data = {
|
||||
"revenue": [r.revenue for r in reports],
|
||||
"net_profit": [r.net_profit for r in reports],
|
||||
"roe": [r.roe for r in reports],
|
||||
"gross_margin": [r.gross_margin for r in reports],
|
||||
"revenue_growth": [r.revenue_growth for r in reports],
|
||||
"profit_growth": [r.profit_growth for r in reports],
|
||||
"debt_ratio": [r.debt_ratio for r in reports]
|
||||
}
|
||||
|
||||
# 计算趋势(上升/下降/平稳)
|
||||
def calc_trend(values):
|
||||
if len(values) < 2:
|
||||
return "平稳"
|
||||
recent = np.mean(values[-2:])
|
||||
previous = np.mean(values[:2]) if len(values) >= 4 else values[0]
|
||||
change = (recent - previous) / previous if previous != 0 else 0
|
||||
if change > 0.1:
|
||||
return "上升"
|
||||
elif change < -0.1:
|
||||
return "下降"
|
||||
else:
|
||||
return "平稳"
|
||||
|
||||
trends = {
|
||||
key: calc_trend(values)
|
||||
for key, values in trend_data.items()
|
||||
}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"code": code,
|
||||
"name": reports[0].name,
|
||||
"dates": dates,
|
||||
"data": trend_data,
|
||||
"trends": trends,
|
||||
"latest": {
|
||||
"revenue": reports[-1].revenue,
|
||||
"net_profit": reports[-1].net_profit,
|
||||
"roe": reports[-1].roe,
|
||||
"gross_margin": reports[-1].gross_margin,
|
||||
"revenue_growth": reports[-1].revenue_growth,
|
||||
"profit_growth": reports[-1].profit_growth
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def generate_ai_summary(code: str) -> Dict[str, Any]:
|
||||
"""生成AI财报摘要
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
|
||||
Returns:
|
||||
AI摘要
|
||||
"""
|
||||
with get_session() as s:
|
||||
# 获取最新财报
|
||||
report = s.execute(
|
||||
select(FinancialReport)
|
||||
.where(FinancialReport.code == code)
|
||||
.order_by(desc(FinancialReport.report_date))
|
||||
.limit(1)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if not report:
|
||||
return {"ok": False, "msg": "暂无财报数据"}
|
||||
|
||||
# 如果已有摘要,直接返回
|
||||
if report.ai_summary:
|
||||
return {
|
||||
"ok": True,
|
||||
"summary": report.ai_summary,
|
||||
"report_date": report.report_date.isoformat()
|
||||
}
|
||||
|
||||
# 构建提示词
|
||||
prompt = f"""请用一句话总结以下财报数据(40字以内):
|
||||
|
||||
公司:{report.name}({report.code})
|
||||
报告期:{report.report_date}
|
||||
营业收入:{report.revenue}亿元,同比增长{report.revenue_growth:+.1f}%
|
||||
净利润:{report.net_profit}亿元,同比增长{report.profit_growth:+.1f}%
|
||||
ROE:{report.roe}%
|
||||
毛利率:{report.gross_margin}%
|
||||
|
||||
要求:
|
||||
1. 一句话说明业绩是增长还是下降
|
||||
2. 提及最亮眼或最担忧的指标
|
||||
3. 给出简短评价(优秀/良好/一般/较差)
|
||||
4. 不超过40字
|
||||
|
||||
示例:业绩稳步增长,ROE达20%创新高,盈利能力优秀。
|
||||
"""
|
||||
|
||||
# 调用AI
|
||||
if llm.enabled():
|
||||
try:
|
||||
summary = llm.ask(prompt, max_tokens=100)
|
||||
# 保存摘要
|
||||
report.ai_summary = summary
|
||||
s.commit()
|
||||
except Exception as e:
|
||||
summary = f"营收{report.revenue}亿元({report.revenue_growth:+.1f}%),净利润{report.net_profit}亿元({report.profit_growth:+.1f}%)"
|
||||
else:
|
||||
summary = f"营收{report.revenue}亿元({report.revenue_growth:+.1f}%),净利润{report.net_profit}亿元({report.profit_growth:+.1f}%)"
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"summary": summary,
|
||||
"report_date": report.report_date.isoformat()
|
||||
}
|
||||
|
||||
|
||||
def compare_with_peers(code: str, sector: str = None) -> Dict[str, Any]:
|
||||
"""同行对比
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
sector: 行业(可选)
|
||||
|
||||
Returns:
|
||||
对比结果
|
||||
"""
|
||||
with get_session() as s:
|
||||
# 获取目标股票最新财报
|
||||
target = s.execute(
|
||||
select(FinancialReport)
|
||||
.where(FinancialReport.code == code)
|
||||
.order_by(desc(FinancialReport.report_date))
|
||||
.limit(1)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if not target:
|
||||
return {"ok": False, "msg": "暂无财报数据"}
|
||||
|
||||
# 获取同行业股票(简化:随机选取)
|
||||
peers = s.execute(
|
||||
select(FinancialReport)
|
||||
.where(
|
||||
and_(
|
||||
FinancialReport.code != code,
|
||||
FinancialReport.report_date == target.report_date
|
||||
)
|
||||
)
|
||||
.limit(20)
|
||||
).scalars().all()
|
||||
|
||||
if not peers:
|
||||
return {"ok": False, "msg": "暂无同行数据"}
|
||||
|
||||
# 计算行业均值
|
||||
industry_avg = {
|
||||
"roe": np.mean([p.roe for p in peers]),
|
||||
"gross_margin": np.mean([p.gross_margin for p in peers]),
|
||||
"revenue_growth": np.mean([p.revenue_growth for p in peers]),
|
||||
"profit_growth": np.mean([p.profit_growth for p in peers]),
|
||||
"debt_ratio": np.mean([p.debt_ratio for p in peers])
|
||||
}
|
||||
|
||||
# 计算差异
|
||||
comparison = {
|
||||
"roe": {
|
||||
"value": target.roe,
|
||||
"industry_avg": round(industry_avg["roe"], 2),
|
||||
"diff": round(target.roe - industry_avg["roe"], 2),
|
||||
"better": target.roe > industry_avg["roe"]
|
||||
},
|
||||
"gross_margin": {
|
||||
"value": target.gross_margin,
|
||||
"industry_avg": round(industry_avg["gross_margin"], 2),
|
||||
"diff": round(target.gross_margin - industry_avg["gross_margin"], 2),
|
||||
"better": target.gross_margin > industry_avg["gross_margin"]
|
||||
},
|
||||
"revenue_growth": {
|
||||
"value": target.revenue_growth,
|
||||
"industry_avg": round(industry_avg["revenue_growth"], 2),
|
||||
"diff": round(target.revenue_growth - industry_avg["revenue_growth"], 2),
|
||||
"better": target.revenue_growth > industry_avg["revenue_growth"]
|
||||
},
|
||||
"profit_growth": {
|
||||
"value": target.profit_growth,
|
||||
"industry_avg": round(industry_avg["profit_growth"], 2),
|
||||
"diff": round(target.profit_growth - industry_avg["profit_growth"], 2),
|
||||
"better": target.profit_growth > industry_avg["profit_growth"]
|
||||
},
|
||||
"debt_ratio": {
|
||||
"value": target.debt_ratio,
|
||||
"industry_avg": round(industry_avg["debt_ratio"], 2),
|
||||
"diff": round(target.debt_ratio - industry_avg["debt_ratio"], 2),
|
||||
"better": target.debt_ratio < industry_avg["debt_ratio"] # 负债率越低越好
|
||||
}
|
||||
}
|
||||
|
||||
# 综合评分
|
||||
better_count = sum(1 for v in comparison.values() if v["better"])
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"code": code,
|
||||
"name": target.name,
|
||||
"report_date": target.report_date.isoformat(),
|
||||
"comparison": comparison,
|
||||
"better_count": better_count,
|
||||
"total_metrics": len(comparison),
|
||||
"conclusion": "优于行业" if better_count >= 3 else ("持平行业" if better_count == 2 else "弱于行业")
|
||||
}
|
||||
|
||||
|
||||
def detect_abnormalities(code: str) -> Dict[str, Any]:
|
||||
"""财报异常预警
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
|
||||
Returns:
|
||||
异常预警
|
||||
"""
|
||||
with get_session() as s:
|
||||
# 获取最近2期财报
|
||||
reports = s.execute(
|
||||
select(FinancialReport)
|
||||
.where(FinancialReport.code == code)
|
||||
.order_by(desc(FinancialReport.report_date))
|
||||
.limit(2)
|
||||
).scalars().all()
|
||||
|
||||
if len(reports) < 2:
|
||||
return {"ok": False, "msg": "数据不足"}
|
||||
|
||||
current, previous = reports[0], reports[1]
|
||||
|
||||
warnings = []
|
||||
|
||||
# 1. 存货激增
|
||||
if current.inventory > 0 and previous.inventory > 0:
|
||||
inventory_growth = (current.inventory / previous.inventory - 1) * 100
|
||||
if inventory_growth > 50:
|
||||
warnings.append({
|
||||
"type": "存货激增",
|
||||
"severity": "high",
|
||||
"description": f"存货增长{inventory_growth:.1f}%,可能存在滞销风险",
|
||||
"current": current.inventory,
|
||||
"previous": previous.inventory
|
||||
})
|
||||
|
||||
# 2. 应收账款占比过高
|
||||
receivable_ratio = current.receivable / current.revenue * 100 if current.revenue > 0 else 0
|
||||
if receivable_ratio > 50:
|
||||
warnings.append({
|
||||
"type": "应收账款占比过高",
|
||||
"severity": "medium",
|
||||
"description": f"应收账款占营收{receivable_ratio:.1f}%,回款压力较大",
|
||||
"ratio": round(receivable_ratio, 2)
|
||||
})
|
||||
|
||||
# 3. 毛利率大幅下降
|
||||
if current.gross_margin > 0 and previous.gross_margin > 0:
|
||||
margin_change = current.gross_margin - previous.gross_margin
|
||||
if margin_change < -5:
|
||||
warnings.append({
|
||||
"type": "毛利率大幅下降",
|
||||
"severity": "high",
|
||||
"description": f"毛利率下降{abs(margin_change):.1f}个百分点,盈利能力恶化",
|
||||
"current": current.gross_margin,
|
||||
"previous": previous.gross_margin
|
||||
})
|
||||
|
||||
# 4. 资产负债率过高
|
||||
if current.debt_ratio > 70:
|
||||
warnings.append({
|
||||
"type": "资产负债率过高",
|
||||
"severity": "medium",
|
||||
"description": f"资产负债率{current.debt_ratio}%,财务风险较高",
|
||||
"value": current.debt_ratio
|
||||
})
|
||||
|
||||
# 5. 增收不增利
|
||||
if current.revenue_growth > 10 and current.profit_growth < 0:
|
||||
warnings.append({
|
||||
"type": "增收不增利",
|
||||
"severity": "high",
|
||||
"description": f"营收增长{current.revenue_growth:.1f}%,但净利润下降{abs(current.profit_growth):.1f}%",
|
||||
"revenue_growth": current.revenue_growth,
|
||||
"profit_growth": current.profit_growth
|
||||
})
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"code": code,
|
||||
"name": current.name,
|
||||
"report_date": current.report_date.isoformat(),
|
||||
"warnings": warnings,
|
||||
"risk_level": "高" if any(w["severity"] == "high" for w in warnings) else ("中" if warnings else "低")
|
||||
}
|
||||
|
||||
|
||||
def get_report_calendar(days: int = 30) -> Dict[str, Any]:
|
||||
"""财报发布日历
|
||||
|
||||
Args:
|
||||
days: 未来N天
|
||||
|
||||
Returns:
|
||||
日历数据
|
||||
"""
|
||||
today = dt.date.today()
|
||||
end_date = today + dt.timedelta(days=days)
|
||||
|
||||
with get_session() as s:
|
||||
reports = s.execute(
|
||||
select(FinancialReport)
|
||||
.where(
|
||||
and_(
|
||||
FinancialReport.publish_date >= today,
|
||||
FinancialReport.publish_date <= end_date
|
||||
)
|
||||
)
|
||||
.order_by(FinancialReport.publish_date)
|
||||
).scalars().all()
|
||||
|
||||
if not reports:
|
||||
return {"ok": False, "msg": "暂无即将发布的财报"}
|
||||
|
||||
# 按日期分组
|
||||
calendar = defaultdict(list)
|
||||
for r in reports:
|
||||
calendar[r.publish_date.isoformat()].append({
|
||||
"code": r.code,
|
||||
"name": r.name,
|
||||
"report_date": r.report_date.isoformat(),
|
||||
"report_type": r.report_type
|
||||
})
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"days": days,
|
||||
"total": len(reports),
|
||||
"calendar": dict(calendar)
|
||||
}
|
||||
|
||||
|
||||
def get_top_reports(metric: str = "roe", limit: int = 20) -> Dict[str, Any]:
|
||||
"""获取财报排行榜
|
||||
|
||||
Args:
|
||||
metric: 排序指标(roe/gross_margin/revenue_growth)
|
||||
limit: 返回数量
|
||||
|
||||
Returns:
|
||||
排行榜
|
||||
"""
|
||||
with get_session() as s:
|
||||
# 获取最新一期的所有财报
|
||||
latest_date = s.execute(
|
||||
select(func.max(FinancialReport.report_date))
|
||||
).scalar()
|
||||
|
||||
if not latest_date:
|
||||
return {"ok": False, "msg": "暂无数据"}
|
||||
|
||||
# 根据指标排序
|
||||
order_field = getattr(FinancialReport, metric)
|
||||
|
||||
reports = s.execute(
|
||||
select(FinancialReport)
|
||||
.where(FinancialReport.report_date == latest_date)
|
||||
.order_by(desc(order_field))
|
||||
.limit(limit)
|
||||
).scalars().all()
|
||||
|
||||
results = []
|
||||
for r in reports:
|
||||
results.append({
|
||||
"code": r.code,
|
||||
"name": r.name,
|
||||
"roe": r.roe,
|
||||
"gross_margin": r.gross_margin,
|
||||
"revenue_growth": r.revenue_growth,
|
||||
"profit_growth": r.profit_growth,
|
||||
metric: getattr(r, metric)
|
||||
})
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"metric": metric,
|
||||
"report_date": latest_date.isoformat(),
|
||||
"rankings": results
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user