diff --git a/.env_sample b/.env_sample new file mode 100644 index 0000000..65c0333 --- /dev/null +++ b/.env_sample @@ -0,0 +1,10 @@ +# 大模型接口地址 +OPENAI_BASE_URL= +# 大模型 API 密钥 +OPENAI_API_KEY= +# bot token +TELEGRAM_BOT_TOKEN= +# http代理 +HTTP_PROXY= +HTTPS_PROXY= +TELEGRAM_PROXY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index f5d312f..6688ccd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +# 剔除环境文件 .env +# 剔除不需要的文件夹 .venv/ .idea/ __pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index e69de29..153e15b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,3 @@ +main -- 主程序,所有逻辑都在这 +build_rag -- 构建rag库 +adata_test -- 用来才是adata库的 \ No newline at end of file diff --git a/adata_test.py b/adata_test.py index f51754b..36f0456 100644 --- a/adata_test.py +++ b/adata_test.py @@ -15,34 +15,82 @@ def get_fina_info(stock_code="600699"): 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', - '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']] + print(df.columns) - 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": "报告类型", - "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) + # 字典定义:英文指标名 -> 中文财务指标名 + 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 中,使用 rename 修改列名 + # inplace=True 表示直接在原 DataFrame 上修改 + last_data.rename(columns=column_mapping, inplace=True) formatted_lines = [] - for _, row in recent_df.iterrows(): + for _, row in last_data.iterrows(): 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)): # 如果列名包含"率",则格式化为保留两位小数并加上 % diff --git a/build_rag.py b/build_rag.py new file mode 100644 index 0000000..222d804 --- /dev/null +++ b/build_rag.py @@ -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("完成") \ No newline at end of file diff --git a/knowledge/company_cases/nanda_optical.md b/knowledge/company_cases/nanda_optical.md new file mode 100644 index 0000000..d8d0279 --- /dev/null +++ b/knowledge/company_cases/nanda_optical.md @@ -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. 安全生产情况 diff --git a/knowledge/industry/semiconductor_material.md b/knowledge/industry/semiconductor_material.md new file mode 100644 index 0000000..d8b1093 --- /dev/null +++ b/knowledge/industry/semiconductor_material.md @@ -0,0 +1,137 @@ +# 半导体材料行业分析框架 + + +## 行业特点 + +半导体材料属于高技术壁垒行业。 + +核心竞争因素: + +- 纯度 +- 一致性 +- 良率 +- 客户认证 + + +--- + +# 核心商业模式 + + +半导体材料企业成长路径: + +研发 + +↓ + +客户验证 + +↓ + +小批量供应 + +↓ + +进入量产线 + +↓ + +扩大份额 + + + +--- + +# 主要材料类型 + + +## 前驱体材料 + +应用: + +ALD/CVD薄膜沉积 + + +评价指标: + +- 纯度等级 +- 金属杂质 +- 稳定性 +- 客户认证 + + + +## 电子特气 + +应用: + +- 刻蚀 +- 清洗 +- 外延 + + +关键指标: + +- 气体纯度 +- 供应稳定性 +- 安全体系 + + + +## 光刻胶 + +评价: + +- 分辨率 +- 缺陷率 +- 工艺匹配度 + + +进入先进制程需要长期验证。 + + + +--- + +# 行业护城河 + + +半导体材料不是普通化工。 + +竞争优势来自: + +1. 长期工艺积累 + +2. 客户认证周期 + +3. 高纯制造能力 + +4. 安全生产体系 + + + +--- + +# 行业风险 + + +## 技术路线变化 + +新材料可能替代旧材料。 + + +## 安全风险 + +部分材料: + +- 易燃 +- 易爆 +- 有毒 + + +事故可能导致: + +- 停产 +- 客户流失 +- 估值下降 + diff --git a/knowledge/rules/pandas_chinese_field_rules.md b/knowledge/rules/pandas_chinese_field_rules.md new file mode 100644 index 0000000..c713d88 --- /dev/null +++ b/knowledge/rules/pandas_chinese_field_rules.md @@ -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\%$。 + - **预警**:公司“业绩蓄水池”(未确认的订单或预收款)正在枯竭,未来几个季度的营收增速大概率会出现失速或下滑。 \ No newline at end of file diff --git a/knowledge/rules/semiconductor_material_rules.md b/knowledge/rules/semiconductor_material_rules.md new file mode 100644 index 0000000..3de6b98 --- /dev/null +++ b/knowledge/rules/semiconductor_material_rules.md @@ -0,0 +1,168 @@ +# 半导体材料企业分析规则 + + +## 规则1:利润质量判断 + + +条件: + +如果: + +净利润增速 > 营收增速 + + +结论: + +说明企业可能存在: + +- 产品结构升级 +- 高毛利产品占比提升 +- 成本改善 + + +验证: + +检查: + +- 毛利率趋势 +- 高端产品收入占比 +- 经营现金流 + + + +--- + +# 规则2:判断国产替代真实性 + + +条件: + +企业宣称国产替代。 + + +不能直接认为成功。 + + +需要满足: + +研发 + +↓ + +客户认证 + +↓ + +订单 + +↓ + +收入贡献 + + + +验证: + +观察: + +- 客户验证情况 +- 销售收入 +- 市占率 + + + +--- + +# 规则3:判断半导体材料护城河 + + +条件: + +企业产品需要: + +- 高纯度 +- 高一致性 +- 长认证周期 + + +结论: + +具备较强竞争壁垒。 + + +验证: + +查看: + +- 产品纯度等级 +- 龙头客户 +- 认证周期 +- 良率 + + + +--- + +# 规则4:危险材料企业风险评估 + + +条件: + +产品属于: + +危险化学品。 + + +结论: + +需要给予安全风险折价。 + + +验证: + +检查: + +- 安全事故 +- 环保处罚 +- 自动化水平 +- 安全投入 + + + +--- + +# 规则5:成长空间判断 + + +条件: + +新产品收入占比较低。 + + +不能简单认为没有价值。 + + +需要判断: + +是否存在: + +- 技术突破 +- 客户验证 +- 放量可能 + + +验证: + +观察: + +收入是否: + +百万级 + +↓ + +千万级 + +↓ + +亿元级 diff --git a/main.py b/main.py index c71866e..9f8e5c5 100644 --- a/main.py +++ b/main.py @@ -8,50 +8,224 @@ from adata import stock from langchain_core.tools import tool from telegram import Update from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.vectorstores import Chroma 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 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) - data = df[['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']] + year_data = df[df.report_type == "年报"].iloc[:5, :] # 取5年的年报数据 + year_data.rename(columns=column_mapping, inplace=True) - last_type = data['report_type'].to_list()[0] - recent_df = data[data.report_type == last_type].iloc[:2, :].reset_index(drop=True) - - 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(): + year_formatted_lines = [] + for _, row in year_data.iterrows(): 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 "率" 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: formatted_val = f"{val:,.2f}" else: + # 非数字类型(如日期、文本)直接转为字符串 formatted_val = str(val) + line_items.append(f"{col}:{formatted_val}") - formatted_lines.append("\n".join(line_items)) - final_report = f"近两期{last_type}主要财务数据如下:\n" + "\n\n".join(formatted_lines) + # 将这一期的所有字段用换行符拼接 + 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)) + + # 3. 组装最终报告文本 + final_report = final_report + "\n\n" + f"近两期{last_type}主要财务数据如下:\n" + "\n\n".join(formatted_lines) + return final_report - class MyOutputParser(StrOutputParser): def parse(self, text) -> str: return text.strip() @@ -60,7 +234,179 @@ class MyOutputParser(StrOutputParser): llm = ChatOpenAI(model="deepseek-v4-flash") tools = [get_fina_info] 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([ ("system", template), @@ -71,13 +417,39 @@ chat_prompt = ChatPromptTemplate.from_messages([ agent = create_tool_calling_agent(llm, tools, chat_prompt) 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 机器人交互逻辑 ================= async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): """处理 /start 命令""" await update.message.reply_text("你好!我是你的股票分析助手,请直接发送类似“分析一下600699的基本面”的消息。") - async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): """处理普通文本消息,调用你的 Agent""" user_input = update.message.text @@ -86,8 +458,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): try: # 调用你原有的 Agent 逻辑 - res = agent_executor.invoke({"input": user_input}) - output = res["output"] + output = run_fundamental_agent(user_input) except Exception as e: output = f"分析出错:{str(e)}" @@ -100,10 +471,24 @@ if __name__ == "__main__": if not 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(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) diff --git a/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/data_level0.bin b/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/data_level0.bin new file mode 100644 index 0000000..5fa25f5 Binary files /dev/null and b/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/data_level0.bin differ diff --git a/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/header.bin b/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/header.bin new file mode 100644 index 0000000..42c216a Binary files /dev/null and b/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/header.bin differ diff --git a/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/length.bin b/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/length.bin new file mode 100644 index 0000000..a0077bf Binary files /dev/null and b/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/length.bin differ diff --git a/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/link_lists.bin b/rag_db/372f688c-7ce6-4638-b51f-32e4c9b8a6ca/link_lists.bin new file mode 100644 index 0000000..e69de29 diff --git a/rag_db/chroma.sqlite3 b/rag_db/chroma.sqlite3 new file mode 100644 index 0000000..4505440 Binary files /dev/null and b/rag_db/chroma.sqlite3 differ