引入rag

This commit is contained in:
lzybetter
2026-08-07 17:13:43 +08:00
parent b9ae90d4aa
commit b43afe4b4a
15 changed files with 1330 additions and 52 deletions
+10
View File
@@ -0,0 +1,10 @@
# 大模型接口地址
OPENAI_BASE_URL=
# 大模型 API 密钥
OPENAI_API_KEY=
# bot token
TELEGRAM_BOT_TOKEN=
# http代理
HTTP_PROXY=
HTTPS_PROXY=
TELEGRAM_PROXY=
+2
View File
@@ -1,4 +1,6 @@
# 剔除环境文件
.env .env
# 剔除不需要的文件夹
.venv/ .venv/
.idea/ .idea/
__pycache__/ __pycache__/
+3
View File
@@ -0,0 +1,3 @@
main -- 主程序,所有逻辑都在这
build_rag -- 构建rag库
adata_test -- 用来才是adata库的
+70 -22
View File
@@ -15,34 +15,82 @@ def get_fina_info(stock_code="600699"):
df = stock.finance.get_core_index(stock_code=stock_code) df = stock.finance.get_core_index(stock_code=stock_code)
data = df[['report_date', 'report_type', 'total_rev', 'total_rev_yoy_gr','net_profit_attr_sh', 'net_profit_yoy_gr', print(df.columns)
'non_gaap_net_profit', 'non_gaap_net_profit_yoy_gr', 'roe_wtd', 'gross_margin', 'net_margin', 'quick_ratio',
'inv_turn_rate', 'acct_recv_turn_rate']]
last_type = data['report_type'].to_list()[0] last_type = df['report_type'].to_list()[0]
recent_df = data[data.report_type == last_type].iloc[:2,:].reset_index(drop=True) last_data = df[df.report_type == last_type].iloc[:2,:]
recent_df.rename(columns={"report_date": "报告日期", # 字典定义:英文指标名 -> 中文财务指标名
"report_type": "报告类型", column_mapping = {
"total_rev":"营业总收入", # 基础与披露信息
"total_rev_yoy_gr":"营业总收入同比增长率", 'stock_code': '股票代码',
"net_profit_attr_sh":"归母净利润", 'short_name': '股票简称',
"net_profit_yoy_gr":"归母净利润同比增长率", 'report_date': '报告期',
"non_gaap_net_profit":"扣非净利润", 'report_type': '报表类型',
"non_gaap_net_profit_yoy_gr":"扣非净利润同比增长率", 'notice_date': '公告日期',
"roe_wtd":"加权净资产收益率",
"gross_margin":"销售毛利率", # 每股指标
"net_margin":"销售净利率", 'basic_eps': '基本每股收益',
"quick_ratio":"速动比率", 'diluted_eps': '稀释每股收益',
"inv_turn_rate":"存货周转率", 'non_gaap_eps': '扣非每股收益',
"acct_recv_turn_rate":"应收账款周转率" 'net_asset_ps': '每股净资产',
}, inplace=True) 'cap_reserve_ps': '每股公积金',
'undist_profit_ps': '每股未分配利润',
'oper_cf_ps': '每股经营现金流',
# 规模与利润指标 (元)
'total_rev': '营业总收入',
'gross_profit': '毛利润',
'net_profit_attr_sh': '归母净利润',
'non_gaap_net_profit': '扣非归母净利润',
# 增长率指标 (同比 YoY / 环比 QoQ)
'total_rev_yoy_gr': '营业总收入同比增长率',
'net_profit_yoy_gr': '归母净利润同比增长率',
'non_gaap_net_profit_yoy_gr': '扣非归母净利润同比增长率',
'total_rev_qoq_gr': '营业总收入环比增长率',
'net_profit_qoq_gr': '归母净利润环比增长率',
'non_gaap_net_profit_qoq_gr': '扣非归母净利润环比增长率',
# 盈利能力与收益率
'roe_wtd': '加权净资产收益率(ROE)',
'roe_non_gaap_wtd': '扣非加权净资产收益率(ROE)',
'roa_wtd': '加权总资产收益率(ROA)',
'gross_margin': '销售毛利率',
'net_margin': '销售净利率',
# 现金流与收入质量比率
'adv_receipts_to_rev': '预收款及合同负债占营收比',
'net_cf_sales_to_rev': '销售收现率(销售现金流/营收)',
'oper_cf_to_rev': '经营现金净流量占营收比',
'eff_tax_rate': '实际有效税率',
# 偿债能力与财务杠杆
'curr_ratio': '流动比率',
'quick_ratio': '速动比率',
'cash_flow_ratio': '现金流量比率',
'asset_liab_ratio': '资产负债率',
'equity_multiplier': '权益乘数',
'equity_ratio': '产权比率',
# 运营效率与周转率/天数
'total_asset_turn_days': '总资产周转天数',
'inv_turn_days': '存货周转天数',
'acct_recv_turn_days': '应收账款周转天数',
'total_asset_turn_rate': '总资产周转率',
'inv_turn_rate': '存货周转率',
'acct_recv_turn_rate': '应收账款周转率'
}
# 假设你的原始数据存储在 df 中,使用 rename 修改列名
# inplace=True 表示直接在原 DataFrame 上修改
last_data.rename(columns=column_mapping, inplace=True)
formatted_lines = [] formatted_lines = []
for _, row in recent_df.iterrows(): for _, row in last_data.iterrows():
line_items = [] line_items = []
for col, val in zip(recent_df.columns, row): for col, val in zip(last_data.columns, row):
# 判断是否为数字类型 # 判断是否为数字类型
if isinstance(val, (int, float)): if isinstance(val, (int, float)):
# 如果列名包含"率",则格式化为保留两位小数并加上 % # 如果列名包含"率",则格式化为保留两位小数并加上 %
+147
View File
@@ -0,0 +1,147 @@
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
# ==========================
# 配置
# ==========================
load_dotenv()
# ======================
# 配置
# ======================
KNOWLEDGE_PATH = "./knowledge"
VECTOR_DB_PATH = "./rag_db"
COLLECTION_NAME = "investment_knowledge"
EMBEDDING_MODEL = "BAAI/bge-small-zh-v1.5"
# ======================
# Embedding
# ======================
print("加载Embedding模型")
embeddings = HuggingFaceEmbeddings(
model_name=EMBEDDING_MODEL,
model_kwargs={
"device":"cpu"
}
)
# ======================
# 加载Markdown
# ======================
print("加载知识文件")
loader = DirectoryLoader(
KNOWLEDGE_PATH,
glob="**/*.md",
loader_cls=TextLoader,
loader_kwargs={
"encoding":"utf-8"
}
)
documents = loader.load()
print(
f"加载 {len(documents)} 个文件"
)
# ======================
# 自动生成metadata
# ======================
def build_metadata(doc):
path = doc.metadata["source"]
metadata={
"source":path
}
# rules
if "/rules/" in path:
metadata["category"]="rule"
metadata["knowledge_type"]="analysis_logic"
# industry
elif "/industry/" in path:
metadata["category"]="industry"
metadata["knowledge_type"]="industry_logic"
# company
elif "/company_cases/" in path:
metadata["category"]="company_case"
metadata["knowledge_type"]="company_fact"
# framework
elif "/investment_framework/" in path:
metadata["category"]="framework"
metadata["knowledge_type"]="workflow"
else:
metadata["category"]="unknown"
return metadata
for doc in documents:
doc.metadata.update(
build_metadata(doc)
)
# ======================
# 文本切分
# ======================
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=80,
separators=[
"\n\n",
"\n",
"",
""
]
)
texts = splitter.split_documents(
documents
)
print(
f"生成 {len(texts)} 个chunk"
)
# ======================
# 创建向量库
# ======================
print("生成Chroma")
vector_store = Chroma.from_documents(
documents=texts,
embedding=embeddings,
persist_directory=VECTOR_DB_PATH,
collection_name=COLLECTION_NAME
)
print("完成")
+288
View File
@@ -0,0 +1,288 @@
# 南大光电(300346.SZ)投资案例
## 基本信息
公司:
江苏南大光电材料股份有限公司
股票代码:
300346.SZ
行业:
半导体材料
主营方向:
- 半导体前驱体材料
- 电子特气
- 光刻胶及配套材料
---
# 财务数据
## 2025年度
营业收入:
25.85亿元
同比增长:
9.93%
归母净利润:
3.20亿元
同比增长:
18%
扣非净利润:
2.54亿元
同比增长:
31.49%
经营现金流:
7.33亿元
同比增长:
16.76%
## 2026年Q1
营业收入:
6.62亿元
同比增长:
5.45%
归母净利润:
1.24亿元
同比增长:
29.97%
扣非净利润:
同比增长:
38.61%
---
# 业务结构
## 1. 先进前驱体材料
2025收入:
7.14亿元
占总收入:
27.61%
增长:
23.46%
毛利率:
50.88%
主要产品:
- DIPAS
- BDEAS
- TDMAT
- 三甲基镓
- 三甲基铝
应用:
- ALD薄膜沉积
- CVD薄膜沉积
- LED
- 5G射频芯片
- 存储芯片
---
## 2. 电子特气
2025收入:
15.36亿元
收入占比:
59.39%
毛利率:
36.74%
产品:
- 高纯磷烷
- 砷烷
- 安全源
- NF3
- SF6
应用:
- IC制造
- LED制造
- 刻蚀
- 腔体清洗
---
## 3. 光刻胶
主要产品:
ArF光刻胶
技术:
193nm
应用:
90nm-14nm先进制程
2025进展:
- 三款ArF光刻胶通过客户验证
- 获得订单
- 光刻胶收入突破2000万元
---
# 技术壁垒
## 高纯材料能力
MO源纯度:
6N以上
核心能力:
- 合成
- 纯化
- 分析检测
- 安全储运
---
## 光刻胶能力
覆盖:
- 单体
- 树脂
- 光敏剂
- 成品
形成完整研发体系。
---
# 重大事件
2025年:
现金2.30亿元收购全椒南大核心团队股份
2026年:
现金7760万元收购乌兰察布南大员工合伙股份
公司参与:
- 国家863计划
- 集成电路02专项
---
# 核心竞争优势
1. 半导体材料国产替代受益者
2. 客户认证周期形成壁垒
3. 高纯材料工艺积累
4. 高端材料产品突破
---
# 后续跟踪指标
重点关注:
1. ArF光刻胶收入是否突破亿元级
2. 前驱体毛利率变化
3. 高K三甲基铝渗透情况
4. 海外客户认证进展
5. 安全生产情况
@@ -0,0 +1,137 @@
# 半导体材料行业分析框架
## 行业特点
半导体材料属于高技术壁垒行业。
核心竞争因素:
- 纯度
- 一致性
- 良率
- 客户认证
---
# 核心商业模式
半导体材料企业成长路径:
研发
客户验证
小批量供应
进入量产线
扩大份额
---
# 主要材料类型
## 前驱体材料
应用:
ALD/CVD薄膜沉积
评价指标:
- 纯度等级
- 金属杂质
- 稳定性
- 客户认证
## 电子特气
应用:
- 刻蚀
- 清洗
- 外延
关键指标:
- 气体纯度
- 供应稳定性
- 安全体系
## 光刻胶
评价:
- 分辨率
- 缺陷率
- 工艺匹配度
进入先进制程需要长期验证。
---
# 行业护城河
半导体材料不是普通化工。
竞争优势来自:
1. 长期工艺积累
2. 客户认证周期
3. 高纯制造能力
4. 安全生产体系
---
# 行业风险
## 技术路线变化
新材料可能替代旧材料。
## 安全风险
部分材料:
- 易燃
- 易爆
- 有毒
事故可能导致:
- 停产
- 客户流失
- 估值下降
@@ -0,0 +1,90 @@
## 一、 盈利质量与现金流验证(现金流与利润质量)
### 规则编号:RULE_P01_CFO_QUALITY
#### 分析主题:净利润现金含量与“虚盈实亏”诊断
- **触发指标**`经营现金净流量占营收比``销售收现率(销售现金流/营收)``归母净利润``每股经营现金流``每股净资产``归母净利润同比增长率`
- **诊断逻辑与阈值**
1. **警告(虚盈实亏/纸面富贵)**
- **条件**:若 `归母净利润同比增长率 > 0`(账面净利润增长),但 `经营现金净流量占营收比 < 0.08`(低于 8%)或 `销售收现率(销售现金流/营收) < 0.8`(低于 80%)。
- **风险判定**:公司账面利润看似增长,但实际现金回款极差,大量利润沦为“应收账款”或纸面数字,流动性变现能力低下。
2. **优质(现金奶牛/议价力强)**
- **条件**:若 `销售收现率(销售现金流/营收) > 1.0``经营现金净流量占营收比 > 0.15`
- **判定**:公司销售回款能力极强,盈利质量高,对下游具备较强的商业话语权。
## 二、 运营效率与存货/应收防雷(周转率与周转天数风险)
### 规则编号:RULE_T01_AR_RISK
#### 分析主题:应收账款回款危机与“赊销冲业绩”风险
- **触发指标**`应收账款周转天数``应收账款周转率``营业总收入同比增长率``归母净利润同比增长率`
- **诊断逻辑与阈值**
1. **周转恶化预警**
- **条件**:若 `应收账款周转天数` 同比显著增加(如较上年同期增加 $> 30$ 天),或 `应收账款周转率` 持续下降。
- **风险判定**:公司下游客户打款周期拉长,需密切关注未来大额坏账准备计提的爆发风险。
2. **赊销暴雷风险**
- **条件**:若 `营业总收入同比增长率 > 15%`,但 `应收账款周转天数` 同比拉长 $> 40\%$。
- **风险判定**:公司可能在通过大幅放宽信用期限(赊销)来强行刺激销量拉动营收增长,业绩含金量极低。
### 规则编号:RULE_T02_INV_RISK
#### 分析主题:存货滞销与毛利率背离诊断(隐蔽成本)
- **触发指标**`存货周转天数``存货周转率``销售毛利率`
- **诊断逻辑与阈值**
1. **滞销与跌价减值风险**
- **条件**:若 `存货周转天数` 呈连续上升趋势(或大幅高于同行业均值)。
- **判定**:产品面临滞销或更新换代淘汰风险,后续需密切关注存货跌价准备对利润的侵蚀。
2. **经典财务造假背离项**
- **条件**:若 `存货周转天数` 显著增加(周转变慢,产品滞销),但 `销售毛利率` 却逆势大幅上升。
- **高风险预警**:严重违背商业逻辑!公司极有可能少转了生产成本(将本应计入损益表的营业成本留在存货资产中),以此虚增毛利和存货资产。
## 三、 偿债能力与杠杆风险(流动性与负债风险)
### 规则编号:RULE_S01_DEBT_RISK
#### 分析主题:资产负债率与流动性压迫风险
- **触发指标**`资产负债率``流动比率``速动比率``权益乘数``现金流量比率`
- **诊断逻辑与阈值**
1. **流动性断裂预警**
- **条件**:若 `流动比率 < 1.0``速动比率 < 0.7`
- **风险判定**:公司短期变现偿债能力极弱,若无新的融资打入,面临短期债务违约或流动性断裂风险。
2. **高杠杆压迫**
- **条件**:若 `资产负债率 > 70%`(或 `权益乘数 > 3.5`)且 `现金流量比率 < 0.1`
- **风险判定**:公司债务负担沉重,且经营活动产生的现金流无法有效覆盖负债,抗风险能力差。
## 四、 ROE 杜邦分析与盈利驱动力(DuPont Analysis
### 规则编号:RULE_D01_ROE_DRIVERS
#### 分析主题:ROE 质量与三要素驱动力拆解
- **触发指标**`加权净资产收益率(ROE)``扣非加权净资产收益率(ROE)``销售净利率``总资产周转率``权益乘数`
- **诊断逻辑与阈值**
1. **扣非背离风险(依赖非经常损益)**
- **条件**:若 `加权净资产收益率(ROE)` 较高,但 `扣非加权净资产收益率(ROE)` 极低(两者差值 $> 3\%$)。
- **判定**:公司的 ROE 主要依赖“非经常性损益”(如卖资产、政府补贴、投资收益),核心主营业务盈利能力低下。
2. **高风险杠杆驱动型**
- **条件**:若 ROE 上升,但 `销售净利率``总资产周转率` 持平或下降,仅靠 `权益乘数` 升高驱动。
- **风险判定**:高杠杆举债驱动的 ROE 扩张,不具备可持续性,财务风险剧增。
3. **优质效率/议价驱动型**
- **条件**:若 ROE 上升主要由 `总资产周转率``销售净利率` 驱动。
- **判定**:公司通过提高运营效率或提升产品附加值/议价权来实现 ROE 增长,属于高质量增长。
## 五、 收入增长与合同负债(成长可持续性)
### 规则编号:RULE_G01_REVENUE_QUALITY
#### 分析主题:业绩增长含金量与预收款(蓄水池)检验
- **触发指标**`营业总收入同比增长率``归母净利润同比增长率``扣非归母净利润同比增长率``预收款及合同负债占营收比`
- **诊断逻辑与阈值**
1. **利润增速质量校验**
- **条件**:若 `归母净利润同比增长率 > 营业总收入同比增长率`
- **分析**:利润增速快于收入增速,需结合 `扣非归母净利润同比增长率` 交叉验证是“降本增效/毛利提升(优质)”还是“少计提费用/依赖非经常损益(劣质)”。
2. **蓄水池池水干涸预警**
- **条件**:若 `营业总收入同比增长率` 保持增长,但 `预收款及合同负债占营收比` 同比连续下滑 $> 30\%$。
- **预警**:公司“业绩蓄水池”(未确认的订单或预收款)正在枯竭,未来几个季度的营收增速大概率会出现失速或下滑。
@@ -0,0 +1,168 @@
# 半导体材料企业分析规则
## 规则1:利润质量判断
条件:
如果:
净利润增速 > 营收增速
结论:
说明企业可能存在:
- 产品结构升级
- 高毛利产品占比提升
- 成本改善
验证:
检查:
- 毛利率趋势
- 高端产品收入占比
- 经营现金流
---
# 规则2:判断国产替代真实性
条件:
企业宣称国产替代。
不能直接认为成功。
需要满足:
研发
客户认证
订单
收入贡献
验证:
观察:
- 客户验证情况
- 销售收入
- 市占率
---
# 规则3:判断半导体材料护城河
条件:
企业产品需要:
- 高纯度
- 高一致性
- 长认证周期
结论:
具备较强竞争壁垒。
验证:
查看:
- 产品纯度等级
- 龙头客户
- 认证周期
- 良率
---
# 规则4:危险材料企业风险评估
条件:
产品属于:
危险化学品。
结论:
需要给予安全风险折价。
验证:
检查:
- 安全事故
- 环保处罚
- 自动化水平
- 安全投入
---
# 规则5:成长空间判断
条件:
新产品收入占比较低。
不能简单认为没有价值。
需要判断:
是否存在:
- 技术突破
- 客户验证
- 放量可能
验证:
观察:
收入是否:
百万级
千万级
亿元级
+414 -29
View File
@@ -8,49 +8,223 @@ from adata import stock
from langchain_core.tools import tool from langchain_core.tools import tool
from telegram import Update from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
load_dotenv() load_dotenv()
# VectorDB 存在 ./rules_data 目录中
# ======================
# Embedding模型
# ======================
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={
"device":"cpu"
}
)
# ======================
# 加载向量库
# ======================
vector_store = Chroma(
persist_directory="./rag_db",
collection_name="investment_knowledge",
embedding_function=embeddings
)
# 公司事实 Retriever
company_retriever = vector_store.as_retriever(
search_kwargs={
"k":5,
"filter":{
"category":"company_case"
}
}
)
# 行业 Retriever
industry_retriever = vector_store.as_retriever(
search_kwargs={
"k":3,
"filter":{
"category":"industry"
}
}
)
# 规则 Retriever
rule_retriever = vector_store.as_retriever(
search_kwargs={
"k":5,
"filter":{
"category":"rule"
}
}
)
def research_retrieve(question):
result = {}
# 公司事实
company_docs = company_retriever.invoke(question)
result["company"] = "\n\n".join(
[
doc.page_content
for doc in company_docs
]
)
# 行业知识
industry_docs = industry_retriever.invoke(question)
result["industry"] = "\n\n".join(
[
doc.page_content
for doc in industry_docs
]
)
# 分析规则
rule_docs = rule_retriever.invoke(question)
result["rules"] = "\n\n".join(
[
doc.page_content
for doc in rule_docs
]
)
return result
# ================= 你的原有逻辑(完全保留) =================
@tool @tool
def get_fina_info(stock_code: str) -> str: def get_fina_info(stock_code: str) -> str:
"""获取指定股票代码的财务数据。当需要获取财务数据的时使用此工具""" """获取指定股票代码的财务数据。当需要获取财务数据的时使用此工具"""
# 字典定义:英文指标名 -> 中文财务指标名
column_mapping = {
# 基础与披露信息
'stock_code': '股票代码',
'short_name': '股票简称',
'report_date': '报告期',
'report_type': '报表类型',
'notice_date': '公告日期',
# 每股指标
'basic_eps': '基本每股收益',
'diluted_eps': '稀释每股收益',
'non_gaap_eps': '扣非每股收益',
'net_asset_ps': '每股净资产',
'cap_reserve_ps': '每股公积金',
'undist_profit_ps': '每股未分配利润',
'oper_cf_ps': '每股经营现金流',
# 规模与利润指标 (元)
'total_rev': '营业总收入',
'gross_profit': '毛利润',
'net_profit_attr_sh': '归母净利润',
'non_gaap_net_profit': '扣非归母净利润',
# 增长率指标 (同比 YoY / 环比 QoQ)
'total_rev_yoy_gr': '营业总收入同比增长率',
'net_profit_yoy_gr': '归母净利润同比增长率',
'non_gaap_net_profit_yoy_gr': '扣非归母净利润同比增长率',
'total_rev_qoq_gr': '营业总收入环比增长率',
'net_profit_qoq_gr': '归母净利润环比增长率',
'non_gaap_net_profit_qoq_gr': '扣非归母净利润环比增长率',
# 盈利能力与收益率
'roe_wtd': '加权净资产收益率(ROE)',
'roe_non_gaap_wtd': '扣非加权净资产收益率(ROE)',
'roa_wtd': '加权总资产收益率(ROA)',
'gross_margin': '销售毛利率',
'net_margin': '销售净利率',
# 现金流与收入质量比率
'adv_receipts_to_rev': '预收款及合同负债占营收比',
'net_cf_sales_to_rev': '销售收现率(销售现金流/营收)',
'oper_cf_to_rev': '经营现金净流量占营收比',
'eff_tax_rate': '实际有效税率',
# 偿债能力与财务杠杆
'curr_ratio': '流动比率',
'quick_ratio': '速动比率',
'cash_flow_ratio': '现金流量比率',
'asset_liab_ratio': '资产负债率',
'equity_multiplier': '权益乘数',
'equity_ratio': '产权比率',
# 运营效率与周转率/天数
'total_asset_turn_days': '总资产周转天数',
'inv_turn_days': '存货周转天数',
'acct_recv_turn_days': '应收账款周转天数',
'total_asset_turn_rate': '总资产周转率',
'inv_turn_rate': '存货周转率',
'acct_recv_turn_rate': '应收账款周转率'
}
df = stock.finance.get_core_index(stock_code=stock_code) df = stock.finance.get_core_index(stock_code=stock_code)
data = df[['report_date', 'report_type', 'total_rev', 'total_rev_yoy_gr', 'net_profit_attr_sh', 'net_profit_yoy_gr', year_data = df[df.report_type == "年报"].iloc[:5, :] # 取5年的年报数据
'non_gaap_net_profit', 'non_gaap_net_profit_yoy_gr', 'roe_wtd', 'gross_margin', 'net_margin', year_data.rename(columns=column_mapping, inplace=True)
'quick_ratio',
'inv_turn_rate', 'acct_recv_turn_rate']]
last_type = data['report_type'].to_list()[0] year_formatted_lines = []
recent_df = data[data.report_type == last_type].iloc[:2, :].reset_index(drop=True) for _, row in year_data.iterrows():
recent_df.rename(columns={"report_date": "报告日期", "report_type": "报告类型", "total_rev": "营业总收入",
"total_rev_yoy_gr": "营业总收入同比增长率", "net_profit_attr_sh": "归母净利润",
"net_profit_yoy_gr": "归母净利润同比增长率", "non_gaap_net_profit": "扣非净利润",
"non_gaap_net_profit_yoy_gr": "扣非净利润同比增长率", "roe_wtd": "加权净资产收益率",
"gross_margin": "销售毛利率", "net_margin": "销售净利率", "quick_ratio": "速动比率",
"inv_turn_rate": "存货周转率", "acct_recv_turn_rate": "应收账款周转率"
}, inplace=True)
formatted_lines = []
for _, row in recent_df.iterrows():
line_items = [] line_items = []
for col, val in zip(recent_df.columns, row): for col, val in zip(year_data.columns, row):
# 判断是否为数字类型
if isinstance(val, (int, float)): if isinstance(val, (int, float)):
# 如果列名包含"率",则格式化为保留两位小数并加上 %
if "" in col: if "" in col:
formatted_val = f"{val:.2f}%" # 如果原始数值本身就是小数(如 0.35 表示 35%),需要乘以 100
val_pct = val * 100 if abs(val) <= 1.0 else val
formatted_val = f"{val_pct:.2f}%"
else: else:
formatted_val = f"{val:,.2f}" formatted_val = f"{val:,.2f}"
else: else:
# 非数字类型(如日期、文本)直接转为字符串
formatted_val = str(val) formatted_val = str(val)
line_items.append(f"{col}{formatted_val}") line_items.append(f"{col}{formatted_val}")
# 将这一期的所有字段用换行符拼接
year_formatted_lines.append("\n".join(line_items))
final_report = f"近五期年报主要财务数据如下:\n" + "\n\n".join(year_formatted_lines)
last_type = df['report_type'].to_list()[0]
if last_type != "年报":
last_data = df[df.report_type == last_type].iloc[:2,:]
# 假设你的原始数据存储在 df 中,使用 rename 修改列名
# inplace=True 表示直接在原 DataFrame 上修改
last_data.rename(columns=column_mapping, inplace=True)
formatted_lines = []
for _, row in last_data.iterrows():
line_items = []
for col, val in zip(last_data.columns, row):
# 判断是否为数字类型
if isinstance(val, (int, float)):
# 如果列名包含"率",则格式化为保留两位小数并加上 %
if "" in col:
# 如果原始数值本身就是小数(如 0.35 表示 35%),需要乘以 100
val_pct = val * 100 if abs(val) <= 1.0 else val
formatted_val = f"{val_pct:.2f}%"
else:
formatted_val = f"{val:,.2f}"
else:
# 非数字类型(如日期、文本)直接转为字符串
formatted_val = str(val)
line_items.append(f"{col}{formatted_val}")
# 将这一期的所有字段用换行符拼接
formatted_lines.append("\n".join(line_items)) formatted_lines.append("\n".join(line_items))
final_report = f"近两期{last_type}主要财务数据如下:\n" + "\n\n".join(formatted_lines) # 3. 组装最终报告文本
return final_report final_report = final_report + "\n\n" + f"近两期{last_type}主要财务数据如下:\n" + "\n\n".join(formatted_lines)
return final_report
class MyOutputParser(StrOutputParser): class MyOutputParser(StrOutputParser):
def parse(self, text) -> str: def parse(self, text) -> str:
@@ -60,7 +234,179 @@ class MyOutputParser(StrOutputParser):
llm = ChatOpenAI(model="deepseek-v4-flash") llm = ChatOpenAI(model="deepseek-v4-flash")
tools = [get_fina_info] tools = [get_fina_info]
template = """ template = """
你是一专业的股票分析助手。用户会输入一个股票代码,你可以使用工具来获取其财务数据,之后根据获取到的财务数据来分析这家公司的基本面情况 你是一专业的股票基本面分析与投资研究专家
你的任务是结合:
1. 实时获取的数据
2. 公司历史资料
3. 行业商业逻辑
4. 专业投资分析规则
对上市公司进行客观、严谨、深度的基本面诊断。
你的目标不是预测股价,而是分析公司的商业质量、竞争优势、成长逻辑以及潜在风险。
=============================
【信息来源优先级】
==============================
进行分析时,请遵循以下信息优先级:
第一优先级:
用户提供的数据、工具返回的数据、最新财务数据。
第二优先级:
公司事实资料(company_facts)。
第三优先级:
行业知识与商业逻辑(industry_knowledge)。
第四优先级:
通用分析规则(analysis_rules)。
如果不同来源之间存在冲突:
- 以最新、可验证的数据为准。
- 明确指出信息冲突。
- 不得自行编造缺失数据。
==============================
【决策与工具使用原则】
==============================
1. 按需调用工具:
根据用户问题类型,自主选择:
- 财务数据查询工具
不要为了调用工具而调用工具。
2. 数据驱动:
所有涉及以下内容:
- 收入
- 利润
- 毛利率
- 现金流
- 估值
- 市占率
- 产品规模
必须基于真实数据。
禁止:
- 编造财务数字
- 推测不存在的订单
- 将行业趋势当作公司事实
==============================
【知识库参考信息】
==============================
以下为公司事实资料:
{company_facts}
说明:
- 这些内容代表已经整理的公司历史信息。
- 可以用于理解公司的业务结构、产品、技术和历史事件。
- 不得擅自修改其中的数据。
------------------------------
以下为行业背景与商业逻辑:
{industry_knowledge}
说明:
- 用于判断行业空间、竞争格局、商业模式。
- 不代表当前公司的实际情况。
- 必须结合公司数据进行分析。
------------------------------
以下为基本面分析规则:
{analysis_rules}
说明:
- 这些规则用于辅助判断公司质量。
- 不应机械套用,需要结合行业特点分析。
==============================
【分析方法要求】
==============================
请按照以下逻辑进行分析:
一、数据与规则交叉验证
将公司的实际数据与分析规则进行匹配:
例如:
- 收入增长是否符合成长标准?
- 利润增速是否超过收入增速?
- 毛利率变化是否体现产品升级?
- 现金流是否支持利润真实性?
- 资产负债结构是否存在风险?
明确指出:
- 触发的积极信号
- 触发的风险信号
- 尚未验证的关键因素
------------------------------
二、商业模式分析
分析:
- 公司主要产品和收入来源
- 产品竞争壁垒
- 客户粘性
- 行业地位
- 上下游关系
重点回答:
“公司为什么能够赚钱?”
以及:
“这种赚钱能力是否可持续?”
------------------------------
三、成长逻辑分析
不要只关注收入增长。
需要分析:
- 新产品放量
- 市场空间
- 技术升级
- 国产替代
- 市占率变化
- 客户拓展
区分:
- 已经兑现的成长
- 预期中的成长
- 尚未验证的成长
------------------------------
四、风险评估
重点关注:
- 盈利质量下降
- 毛利率持续下滑
- 现金流恶化
- 资本开支压力
- 行业竞争加剧
- 技术路线变化
- 客户集中风险
- 安全生产风险
- 政策与国际环境风险
================================================================================
【 Telegram 消息专属输出排版规范 (CRITICAL FORMATTING RULES) 】
================================================================================
由于本诊断报告最终将在 Telegram 移动端客户端展示,为保证最佳的阅读体验与视觉质感,你必须严格遵循以下 Telegram 专属排版规则:
1. 禁用标准 Markdown 表格 (STRICTLY FORBIDDEN)
- 严禁使用 | 列分隔符 | 的 Markdown 表格语法!Telegram 移动端屏幕狭窄,多列表格会导致严重的强行折行与对齐错乱。
- 所有多维财务数据、年度对比数据,必须拆解为【卡片式结构】或【树状分支结构】。
2. 树状分支与数据对齐 (Tree Structure)
- 涉及多项财务指标(如营收、归母、扣非)的平行对比时,使用 ├ 和 └ 字符构建清晰的树状层级:
* 2025年
├ 营收:`25.85亿`+9.93% │ 归母:`3.20亿`+18.00%
└ 扣非:`2.54亿`+31.49%
- 单指标多年趋势,采用带箭头的平铺演进线表示:
* 毛利率走势:`43.42%` (21) ➔ `41.16%` (24) ➔ `39.62%` (25)
3. 视觉高亮与数据卡片 (Inline Code Highlighting)
- 所有具体的【财务数值】、【百分比】、【比率】以及【核心定性结论】,必须用单反引号包围(即 Inline Code 格式,例如 `22.97%`、`中性偏正面`)。
- 这会在 Telegram 界面中形成高亮灰色背景卡片,提升视觉重点的提炼效率。
4. 标题与符号锚点 (Visual Anchors)
- 每一个大章节标题前必须配有明确的 Emoji 视觉锚点(如 📊、💡、🔍、⚠️、🎯)。
- 结论与诊断部分,优先采用带色彩倾向的标识符:
* 正向/优质:✅ 或 🚀 或 `[良好]`
* 中性/提示:📌 或 💡
* 隐忧/警报:⚠️ 或 🔴 或 🟠
5. 控制行宽与分段 (Mobile Readability)
- 单段分析文字切忌过长,移动端每段尽量保持在 2-4 行以内。
- 章节之间使用全划线 `---` 或空行进行视觉隔离,避免形成“文本大山”。
6. Telegram 模版输出参照:
大纲结构示例:
# 📊 [股票名称(代码)] 基本面深度诊断
> **综合评级**`[评级结果]`
---
### 💡 一、核心结论
* **优质面**
├ **[维度]**:[简短分析]
└ **[维度]**:[简短分析]
### 🔍 二、基本面深度诊断
#### 1️⃣ 成长性:[一句话小结]
* **[年份]**
├ 营收:`[数据]`([增速]) │ 归母:`[数据]`([增速])
└ 扣非:`[数据]`([增速])
📌 **关键判断**:[分析文字]
""" """
chat_prompt = ChatPromptTemplate.from_messages([ chat_prompt = ChatPromptTemplate.from_messages([
("system", template), ("system", template),
@@ -71,13 +417,39 @@ chat_prompt = ChatPromptTemplate.from_messages([
agent = create_tool_calling_agent(llm, tools, chat_prompt) agent = create_tool_calling_agent(llm, tools, chat_prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
def run_fundamental_agent(user_input: str):
# ======================
# 多知识源检索
# ======================
rag_context = research_retrieve(
user_input
)
# ======================
# 注入Agent
# ======================
result = agent_executor.invoke(
{
"input": user_input,
"company_facts":
rag_context["company"],
"industry_knowledge":
rag_context["industry"],
"analysis_rules":
rag_context["rules"]
}
)
return result["output"]
# ================= 新增:Telegram 机器人交互逻辑 ================= # ================= 新增:Telegram 机器人交互逻辑 =================
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 /start 命令""" """处理 /start 命令"""
await update.message.reply_text("你好!我是你的股票分析助手,请直接发送类似“分析一下600699的基本面”的消息。") await update.message.reply_text("你好!我是你的股票分析助手,请直接发送类似“分析一下600699的基本面”的消息。")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理普通文本消息,调用你的 Agent""" """处理普通文本消息,调用你的 Agent"""
user_input = update.message.text user_input = update.message.text
@@ -86,8 +458,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
try: try:
# 调用你原有的 Agent 逻辑 # 调用你原有的 Agent 逻辑
res = agent_executor.invoke({"input": user_input}) output = run_fundamental_agent(user_input)
output = res["output"]
except Exception as e: except Exception as e:
output = f"分析出错:{str(e)}" output = f"分析出错:{str(e)}"
@@ -100,10 +471,24 @@ if __name__ == "__main__":
if not token: if not token:
raise ValueError("未在 .env 文件中找到 TELEGRAM_BOT_TOKEN") raise ValueError("未在 .env 文件中找到 TELEGRAM_BOT_TOKEN")
proxy_url = "http://127.0.0.1:8889" proxy_url = os.getenv(
"TELEGRAM_PROXY"
)
builder = (
ApplicationBuilder()
.token(token)
)
if proxy_url:
builder = (
builder
.proxy(proxy_url)
.get_updates_proxy(proxy_url)
)
application = builder.build()
# 启动机器人
application = ApplicationBuilder().token(token).proxy(proxy_url).get_updates_proxy(proxy_url).build()
application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("start", start))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
Binary file not shown.