commit 35414c74a2dd2adac138c12fecbe3f433f7fb044 Author: kang Date: Sat Apr 25 19:21:28 2026 +0800 init repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bdd6ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# OS +.DS_Store + +# Env +.env +.env.* + +# Python +__pycache__/ +.pytest_cache/ +.mypy_cache/ +.venv/ +venv/ + +# Node +node_modules/ +.next/ +dist/ +build/ +.nuxt/ +.output/ + +# Misc +*.log diff --git a/.memory/worklog.json b/.memory/worklog.json new file mode 100644 index 0000000..046955d --- /dev/null +++ b/.memory/worklog.json @@ -0,0 +1,3 @@ +{ + "entries": [] +} diff --git a/.project.json b/.project.json new file mode 100644 index 0000000..b87b26f --- /dev/null +++ b/.project.json @@ -0,0 +1,15 @@ +{ + "name": "美股低价值公司分析系统", + "description": "一个专门用于分析美股低价值投资机会的智能分析系统,提供全面的财务分析、估值计算和投资建议。", + "status": "archived", + "kind": "research", + "created": "2025-09-20", + "stack": [ + "Python" + ], + "urls": [], + "worklog": { + "path": ".memory/worklog.json", + "auto": true + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1ef05e3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,21 @@ +# 美股低价值公司分析系统 Agent Rules + +## Must Read First + +- `.project.json` 是机器真源:公网链接、快捷登录、凭证引用都以它为准 +- `RULES.md` 是人工规则和部署事实:启动命令、平台、域名、注意事项都写这里 +- 不允许编造不存在的域名、账号、密码;未知就保持空白并明确标记待补充 + +## Deployment Metadata Contract + +- 任何任务只要新增、删除或修改公网地址,必须在同一次任务里更新 `.project.json` +- `urls[]` 推荐显式写 `type`:`app`、`backend`、`docs`、`admin`、`repo` +- 项目专属的网页登录信息,如果允许放进仓库,就写 `.project.json.quick_login` +- 不能直接入库的敏感登录,不要伪造 `quick_login`,改为写 `.project.json.credentials` 引用 +- 数据库密码、API Key、服务器 root 密码,不属于 `quick_login` + +## Completion Gate + +- 部署完成后,不允许在 `.project.json` 缺少最新公网链接的状态下结束任务 +- 部署完成后,必须同步更新 `RULES.md` 的部署事实 +- 如果只更新了代码但没回写部署元数据,这个任务不算完成 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1ef05e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# 美股低价值公司分析系统 Agent Rules + +## Must Read First + +- `.project.json` 是机器真源:公网链接、快捷登录、凭证引用都以它为准 +- `RULES.md` 是人工规则和部署事实:启动命令、平台、域名、注意事项都写这里 +- 不允许编造不存在的域名、账号、密码;未知就保持空白并明确标记待补充 + +## Deployment Metadata Contract + +- 任何任务只要新增、删除或修改公网地址,必须在同一次任务里更新 `.project.json` +- `urls[]` 推荐显式写 `type`:`app`、`backend`、`docs`、`admin`、`repo` +- 项目专属的网页登录信息,如果允许放进仓库,就写 `.project.json.quick_login` +- 不能直接入库的敏感登录,不要伪造 `quick_login`,改为写 `.project.json.credentials` 引用 +- 数据库密码、API Key、服务器 root 密码,不属于 `quick_login` + +## Completion Gate + +- 部署完成后,不允许在 `.project.json` 缺少最新公网链接的状态下结束任务 +- 部署完成后,必须同步更新 `RULES.md` 的部署事实 +- 如果只更新了代码但没回写部署元数据,这个任务不算完成 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8016ac7 --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +# 美股低价值公司分析系统 + +一个专门用于分析美股低价值投资机会的智能分析系统,提供全面的财务分析、估值计算和投资建议。 + +## 功能特点 + +### 🎯 核心功能 +- **实时数据收集**: 从Yahoo Finance获取最新股价和财务数据 +- **智能估值分析**: DCF模型、相对估值法、财务指标分析 +- **风险评估**: 波动率、Beta系数、财务健康度评估 +- **投资建议**: 基于多维度分析的买入/持有/卖出建议 +- **报告生成**: 自动生成PDF详细报告和文本摘要 + +### 📊 分析维度 +1. **估值吸引力** (40%权重) + - PE、PB、PS、PEG比率分析 + - DCF内在价值计算 + - 相对估值对比 + +2. **财务健康度** (30%权重) + - 流动性指标 (流动比率、速动比率) + - 偿债能力 (债务比率、利息覆盖倍数) + - 盈利能力 (ROE、ROA、利润率) + +3. **成长性** (20%权重) + - 收入增长率分析 + - 利润增长率分析 + - 历史增长趋势 + +4. **风险控制** (10%权重) + - Beta系数分析 + - 波动率计算 + - 最大回撤分析 + +## 安装和使用 + +### 环境要求 +- Python 3.8+ +- 网络连接(用于获取实时数据) + +### 安装依赖 +```bash +pip install -r requirements.txt +``` + +### 使用方法 + +#### 1. 单只股票完整分析 +```bash +python main.py AAPL +``` + +#### 2. 强制刷新数据重新分析 +```bash +python main.py AAPL --refresh +``` + +#### 3. 快速筛选(检查是否符合低价值标准) +```bash +python main.py --screen AAPL +``` + +#### 4. 批量分析多只股票 +```bash +python main.py --batch AAPL,MSFT,GOOGL,TSLA +``` + +## 输出文件 + +### 报告文件 +- `reports/{股票代码}_analysis_report.pdf` - 详细PDF分析报告 +- `reports/{股票代码}_summary.txt` - 简要文本摘要 +- `reports/charts/` - 图表文件(HTML格式) + +### 数据库 +- `stock_analysis.db` - SQLite数据库,存储历史数据和分析结果 + +## 分析流程 + +### 每次分析的标准流程: + +1. **数据获取阶段** (30秒内) + - 验证股票代码和基本信息 + - 获取最新股价和交易数据 + - 下载最近4个季度的财务数据 + +2. **快速筛选阶段** (1分钟内) + - 计算关键估值指标(PE、PB、PS等) + - 检查是否符合"低价值"标准 + - 识别异常财务指标 + +3. **深度分析阶段** (2-3分钟) + - 进行DCF估值计算 + - 分析财务健康度 + - 评估行业地位和竞争优势 + - 计算风险指标 + +4. **报告生成阶段** (1分钟内) + - 生成综合分析报告 + - 提供投资建议和风险提示 + - 保存分析结果到数据库 + +## 低价值投资标准 + +系统默认的低价值投资筛选标准: + +- **PE比率**: ≤ 15 +- **PB比率**: ≤ 1.5 +- **PS比率**: ≤ 2.0 +- **最小市值**: ≥ 1亿美元 +- **债务比率**: ≤ 60% +- **流动比率**: ≥ 1.2 +- **ROE**: ≥ 5% + +## 配置说明 + +可以在 `config.py` 中修改分析参数: + +```python +LOW_VALUE_CRITERIA = { + 'max_pe_ratio': 15, # 最大市盈率 + 'max_pb_ratio': 1.5, # 最大市净率 + 'max_ps_ratio': 2.0, # 最大市销率 + 'min_market_cap': 100_000_000, # 最小市值 + 'max_debt_ratio': 0.6, # 最大债务比率 + 'min_current_ratio': 1.2, # 最小流动比率 + 'min_roe': 0.05, # 最小ROE +} +``` + +## 注意事项 + +1. **数据来源**: 系统使用Yahoo Finance作为主要数据源,数据可能存在延迟 +2. **投资风险**: 本系统仅供投资参考,不构成投资建议 +3. **数据准确性**: 建议结合其他数据源进行交叉验证 +4. **网络要求**: 需要稳定的网络连接获取实时数据 + +## 系统架构 + +``` +├── main.py # 主程序入口 +├── config.py # 配置文件 +├── database.py # 数据库管理 +├── data_collector.py # 数据收集模块 +├── analysis_engine.py # 分析引擎 +├── report_generator.py # 报告生成器 +├── requirements.txt # 依赖包列表 +└── README.md # 使用说明 +``` + +## 示例输出 + +### 控制台输出示例 +``` +=== AAPL 分析结果 === +投资建议: 买入 +综合评分: 75.2/100 + +关键优势: + • 估值吸引力高 + • 财务健康度良好 + • 成长性优秀 + +关键担忧: + • 风险较高 + +详细报告已生成: reports/AAPL_analysis_report.pdf +简要报告已生成: reports/AAPL_summary.txt +``` + +## 技术支持 + +如有问题或建议,请检查: +1. 网络连接是否正常 +2. 股票代码是否正确 +3. 依赖包是否完整安装 +4. 查看日志文件 `stock_analysis.log` \ No newline at end of file diff --git a/RULES.md b/RULES.md new file mode 100644 index 0000000..6e3a6a6 --- /dev/null +++ b/RULES.md @@ -0,0 +1,25 @@ +# 20250920-股票分析 + +## 启动 + +- `python3 main.py` + +## 核心文件 + +- `README.md` +- `analysis_engine.py` +- `analysis_reports/` +- `analysis_storage.py` +- `config.py` +- `create_rvph_report.py` +- `data_collector.py` +- `database.py` +- `main.py` +- `report_generator.py` +- `reports/` +- `run_analysis.py` + +## 规则 + +- 研究/分析类项目,核心产出为文档 +- 修改前先通读已有文档,保持结论一致性 diff --git a/analysis_engine.py b/analysis_engine.py new file mode 100644 index 0000000..e646fb1 --- /dev/null +++ b/analysis_engine.py @@ -0,0 +1,430 @@ +""" +分析引擎 - 核心估值和财务分析模块 +""" +import pandas as pd +import numpy as np +from typing import Dict, List, Tuple, Optional +import logging +from datetime import datetime, timedelta +from config import LOW_VALUE_CRITERIA + +logger = logging.getLogger(__name__) + +class StockAnalyzer: + def __init__(self): + self.criteria = LOW_VALUE_CRITERIA + + def calculate_valuation_metrics(self, data: Dict) -> Dict: + """计算估值指标""" + try: + key_metrics = data.get('key_metrics', {}) + company_info = data.get('company_info', {}) + + # 基础估值指标 + pe_ratio = key_metrics.get('pe_ratio', 0) + pb_ratio = key_metrics.get('pb_ratio', 0) + ps_ratio = key_metrics.get('ps_ratio', 0) + peg_ratio = key_metrics.get('peg_ratio', 0) + + # 市值 + market_cap = company_info.get('market_cap', 0) + + # 计算估值分数 (0-100分,分数越高表示越被低估) + valuation_score = 0 + + # PE比率评分 + if pe_ratio > 0 and pe_ratio <= self.criteria['max_pe_ratio']: + pe_score = max(0, 100 - (pe_ratio / self.criteria['max_pe_ratio']) * 50) + valuation_score += pe_score * 0.3 + + # PB比率评分 + if pb_ratio > 0 and pb_ratio <= self.criteria['max_pb_ratio']: + pb_score = max(0, 100 - (pb_ratio / self.criteria['max_pb_ratio']) * 50) + valuation_score += pb_score * 0.3 + + # PS比率评分 + if ps_ratio > 0 and ps_ratio <= self.criteria['max_ps_ratio']: + ps_score = max(0, 100 - (ps_ratio / self.criteria['max_ps_ratio']) * 50) + valuation_score += ps_score * 0.2 + + # PEG比率评分 + if peg_ratio > 0 and peg_ratio <= 1.0: + peg_score = max(0, 100 - peg_ratio * 50) + valuation_score += peg_score * 0.2 + + return { + 'pe_ratio': pe_ratio, + 'pb_ratio': pb_ratio, + 'ps_ratio': ps_ratio, + 'peg_ratio': peg_ratio, + 'market_cap': market_cap, + 'valuation_score': min(100, max(0, valuation_score)), + 'is_undervalued': pe_ratio <= self.criteria['max_pe_ratio'] and + pb_ratio <= self.criteria['max_pb_ratio'] and + ps_ratio <= self.criteria['max_ps_ratio'] + } + except Exception as e: + logger.error(f"计算估值指标失败: {e}") + return {'valuation_score': 0, 'is_undervalued': False} + + def calculate_financial_health(self, data: Dict) -> Dict: + """计算财务健康度""" + try: + key_metrics = data.get('key_metrics', {}) + financial_statements = data.get('financial_statements', {}) + + # 获取最新财务数据 + latest_financial = self._get_latest_financial_data(financial_statements) + + # 流动性指标 + current_ratio = key_metrics.get('current_ratio', 0) + quick_ratio = key_metrics.get('quick_ratio', 0) + + # 偿债能力指标 + debt_to_equity = key_metrics.get('debt_to_equity', 0) + debt_ratio = self._calculate_debt_ratio(latest_financial) + + # 盈利能力指标 + roe = key_metrics.get('return_on_equity', 0) + roa = key_metrics.get('return_on_assets', 0) + profit_margin = key_metrics.get('profit_margin', 0) + operating_margin = key_metrics.get('operating_margin', 0) + + # 计算财务健康分数 + health_score = 0 + + # 流动性评分 (30%) + if current_ratio >= self.criteria['min_current_ratio']: + liquidity_score = min(100, (current_ratio / 2.0) * 100) + health_score += liquidity_score * 0.3 + + # 偿债能力评分 (30%) + if debt_ratio <= self.criteria['max_debt_ratio']: + debt_score = max(0, 100 - (debt_ratio / self.criteria['max_debt_ratio']) * 100) + health_score += debt_score * 0.3 + + # 盈利能力评分 (40%) + if roe >= self.criteria['min_roe']: + profitability_score = min(100, (roe / 0.2) * 100) # 20% ROE为满分 + health_score += profitability_score * 0.4 + + return { + 'current_ratio': current_ratio, + 'quick_ratio': quick_ratio, + 'debt_to_equity': debt_to_equity, + 'debt_ratio': debt_ratio, + 'roe': roe, + 'roa': roa, + 'profit_margin': profit_margin, + 'operating_margin': operating_margin, + 'health_score': min(100, max(0, health_score)), + 'is_healthy': (current_ratio >= self.criteria['min_current_ratio'] and + debt_ratio <= self.criteria['max_debt_ratio'] and + roe >= self.criteria['min_roe']) + } + except Exception as e: + logger.error(f"计算财务健康度失败: {e}") + return {'health_score': 0, 'is_healthy': False} + + def calculate_growth_metrics(self, data: Dict) -> Dict: + """计算成长性指标""" + try: + key_metrics = data.get('key_metrics', {}) + financial_statements = data.get('financial_statements', {}) + + # 收入增长率 + revenue_growth = key_metrics.get('revenue_growth', 0) + earnings_growth = key_metrics.get('earnings_growth', 0) + + # 计算历史增长率 + historical_growth = self._calculate_historical_growth(financial_statements) + + # 成长性评分 + growth_score = 0 + + # 收入增长率评分 + if revenue_growth > 0: + revenue_score = min(100, revenue_growth * 100) + growth_score += revenue_score * 0.4 + + # 利润增长率评分 + if earnings_growth > 0: + earnings_score = min(100, earnings_growth * 100) + growth_score += earnings_score * 0.4 + + # 历史增长率评分 + if historical_growth > 0: + historical_score = min(100, historical_growth * 100) + growth_score += historical_score * 0.2 + + return { + 'revenue_growth': revenue_growth, + 'earnings_growth': earnings_growth, + 'historical_growth': historical_growth, + 'growth_score': min(100, max(0, growth_score)), + 'is_growing': revenue_growth > 0.05 and earnings_growth > 0.05 # 5%以上增长 + } + except Exception as e: + logger.error(f"计算成长性指标失败: {e}") + return {'growth_score': 0, 'is_growing': False} + + def calculate_risk_metrics(self, data: Dict) -> Dict: + """计算风险指标""" + try: + key_metrics = data.get('key_metrics', {}) + stock_prices = data.get('stock_prices', pd.DataFrame()) + + # Beta系数 + beta = key_metrics.get('beta', 1.0) + + # 计算波动率 + volatility = self._calculate_volatility(stock_prices) + + # 计算最大回撤 + max_drawdown = self._calculate_max_drawdown(stock_prices) + + # 风险评分 (分数越高表示风险越低) + risk_score = 100 + + # Beta风险评分 + if beta > 1.5: + risk_score -= (beta - 1.5) * 20 + elif beta < 0.8: + risk_score -= (0.8 - beta) * 10 + + # 波动率风险评分 + if volatility > 0.3: # 30%以上波动率 + risk_score -= (volatility - 0.3) * 100 + + # 回撤风险评分 + if max_drawdown > 0.2: # 20%以上回撤 + risk_score -= (max_drawdown - 0.2) * 100 + + return { + 'beta': beta, + 'volatility': volatility, + 'max_drawdown': max_drawdown, + 'risk_score': max(0, min(100, risk_score)), + 'risk_level': self._get_risk_level(risk_score) + } + except Exception as e: + logger.error(f"计算风险指标失败: {e}") + return {'risk_score': 50, 'risk_level': '中等'} + + def perform_dcf_valuation(self, data: Dict) -> Dict: + """执行DCF估值分析""" + try: + financial_statements = data.get('financial_statements', {}) + key_metrics = data.get('key_metrics', {}) + + # 获取最新财务数据 + latest_financial = self._get_latest_financial_data(financial_statements) + + if not latest_financial: + return {'dcf_value': 0, 'dcf_score': 0} + + # 基础参数 + revenue = latest_financial.get('revenue', 0) + net_income = latest_financial.get('net_income', 0) + total_assets = latest_financial.get('total_assets', 0) + + if revenue <= 0 or net_income <= 0: + return {'dcf_value': 0, 'dcf_score': 0} + + # 假设参数 + growth_rate = min(0.1, max(0.02, key_metrics.get('revenue_growth', 0.05))) # 2%-10% + discount_rate = 0.1 # 10%折现率 + terminal_growth_rate = 0.03 # 3%永续增长率 + + # 计算自由现金流 (简化版) + fcf = net_income * 0.8 # 假设80%的净利润为自由现金流 + + # 5年预测 + years = 5 + dcf_value = 0 + + for year in range(1, years + 1): + future_fcf = fcf * ((1 + growth_rate) ** year) + discounted_fcf = future_fcf / ((1 + discount_rate) ** year) + dcf_value += discounted_fcf + + # 终值计算 + terminal_fcf = fcf * ((1 + growth_rate) ** years) * (1 + terminal_growth_rate) + terminal_value = terminal_fcf / (discount_rate - terminal_growth_rate) + discounted_terminal_value = terminal_value / ((1 + discount_rate) ** years) + + total_dcf_value = dcf_value + discounted_terminal_value + + # 获取当前股价进行对比 + current_price = self._get_current_price(data) + if current_price > 0: + dcf_score = min(100, (total_dcf_value / current_price) * 50) # 50%以上溢价为满分 + else: + dcf_score = 0 + + return { + 'dcf_value': total_dcf_value, + 'current_price': current_price, + 'dcf_score': dcf_score, + 'is_undervalued_dcf': total_dcf_value > current_price if current_price > 0 else False + } + except Exception as e: + logger.error(f"DCF估值分析失败: {e}") + return {'dcf_value': 0, 'dcf_score': 0} + + def generate_investment_recommendation(self, analysis_results: Dict) -> Dict: + """生成投资建议""" + try: + valuation_score = analysis_results.get('valuation_score', 0) + health_score = analysis_results.get('financial_health_score', 0) + growth_score = analysis_results.get('growth_score', 0) + risk_score = analysis_results.get('risk_score', 0) + + # 计算综合评分 + overall_score = ( + valuation_score * 0.4 + + health_score * 0.3 + + growth_score * 0.2 + + risk_score * 0.1 + ) + + # 生成建议 + if overall_score >= 80: + recommendation = "强烈买入" + confidence = "高" + elif overall_score >= 65: + recommendation = "买入" + confidence = "中高" + elif overall_score >= 50: + recommendation = "持有" + confidence = "中等" + elif overall_score >= 35: + recommendation = "观望" + confidence = "中低" + else: + recommendation = "卖出" + confidence = "高" + + # 风险提示 + risk_warnings = [] + if risk_score < 40: + risk_warnings.append("高风险投资") + if health_score < 50: + risk_warnings.append("财务健康度较低") + if growth_score < 30: + risk_warnings.append("成长性不足") + + return { + 'recommendation': recommendation, + 'confidence': confidence, + 'overall_score': overall_score, + 'risk_warnings': risk_warnings, + 'key_strengths': self._get_key_strengths(analysis_results), + 'key_concerns': self._get_key_concerns(analysis_results) + } + except Exception as e: + logger.error(f"生成投资建议失败: {e}") + return {'recommendation': '无法分析', 'overall_score': 0} + + def _get_latest_financial_data(self, financial_statements: Dict) -> Dict: + """获取最新财务数据""" + if not financial_statements: + return {} + + # 按年份排序,获取最新的数据 + sorted_periods = sorted(financial_statements.keys(), reverse=True) + return financial_statements.get(sorted_periods[0], {}) + + def _calculate_debt_ratio(self, financial_data: Dict) -> float: + """计算债务比率""" + total_liabilities = financial_data.get('total_liabilities', 0) + total_assets = financial_data.get('total_assets', 0) + + if total_assets > 0: + return total_liabilities / total_assets + return 0 + + def _calculate_historical_growth(self, financial_statements: Dict) -> float: + """计算历史增长率""" + if len(financial_statements) < 2: + return 0 + + # 获取最近两年的收入数据 + sorted_periods = sorted(financial_statements.keys(), reverse=True) + if len(sorted_periods) < 2: + return 0 + + current_revenue = financial_statements[sorted_periods[0]].get('revenue', 0) + previous_revenue = financial_statements[sorted_periods[1]].get('revenue', 0) + + if previous_revenue > 0: + return (current_revenue - previous_revenue) / previous_revenue + return 0 + + def _calculate_volatility(self, stock_prices: pd.DataFrame) -> float: + """计算波动率""" + if stock_prices.empty or 'close' not in stock_prices.columns: + return 0 + + returns = stock_prices['close'].pct_change().dropna() + return returns.std() * np.sqrt(252) # 年化波动率 + + def _calculate_max_drawdown(self, stock_prices: pd.DataFrame) -> float: + """计算最大回撤""" + if stock_prices.empty or 'close' not in stock_prices.columns: + return 0 + + prices = stock_prices['close'] + peak = prices.expanding().max() + drawdown = (prices - peak) / peak + return abs(drawdown.min()) + + def _get_risk_level(self, risk_score: float) -> str: + """获取风险等级""" + if risk_score >= 80: + return "低" + elif risk_score >= 60: + return "中低" + elif risk_score >= 40: + return "中等" + elif risk_score >= 20: + return "中高" + else: + return "高" + + def _get_current_price(self, data: Dict) -> float: + """获取当前股价""" + stock_prices = data.get('stock_prices', pd.DataFrame()) + if not stock_prices.empty and 'close' in stock_prices.columns: + return stock_prices['close'].iloc[-1] + return 0 + + def _get_key_strengths(self, analysis_results: Dict) -> List[str]: + """获取关键优势""" + strengths = [] + + if analysis_results.get('valuation_score', 0) > 70: + strengths.append("估值吸引力高") + if analysis_results.get('financial_health_score', 0) > 70: + strengths.append("财务健康度良好") + if analysis_results.get('growth_score', 0) > 70: + strengths.append("成长性优秀") + if analysis_results.get('risk_score', 0) > 70: + strengths.append("风险控制良好") + + return strengths + + def _get_key_concerns(self, analysis_results: Dict) -> List[str]: + """获取关键担忧""" + concerns = [] + + if analysis_results.get('valuation_score', 0) < 40: + concerns.append("估值偏高") + if analysis_results.get('financial_health_score', 0) < 40: + concerns.append("财务健康度不佳") + if analysis_results.get('growth_score', 0) < 40: + concerns.append("成长性不足") + if analysis_results.get('risk_score', 0) < 40: + concerns.append("风险较高") + + return concerns \ No newline at end of file diff --git a/analysis_reports/analysis_index.json b/analysis_reports/analysis_index.json new file mode 100644 index 0000000..f108454 --- /dev/null +++ b/analysis_reports/analysis_index.json @@ -0,0 +1,17 @@ +{ + "analyses": [ + { + "symbol": "RVPH", + "timestamp": "20250920_015345", + "analysis_date": "2025-09-20T01:53:45.876451", + "company_name": "Reviva Pharmaceuticals Holdings, Inc.", + "recommendation": "观望/高风险投机", + "overall_score": 35, + "files": { + "detailed": "RVPH_detailed_20250920_015345.json", + "summary": "RVPH_summary_20250920_015345.md", + "raw_data": "RVPH_raw_data_20250920_015345.json" + } + } + ] +} \ No newline at end of file diff --git a/analysis_reports/detailed/RVPH_detailed_20250920_015345.json b/analysis_reports/detailed/RVPH_detailed_20250920_015345.json new file mode 100644 index 0000000..e24b052 --- /dev/null +++ b/analysis_reports/detailed/RVPH_detailed_20250920_015345.json @@ -0,0 +1,112 @@ +{ + "analysis_metadata": { + "symbol": "RVPH", + "analysis_date": "2025-09-20T01:53:45.875995", + "timestamp": "20250920_015345", + "version": "1.0" + }, + "company_info": { + "name": "Reviva Pharmaceuticals Holdings, Inc.", + "symbol": "RVPH", + "industry": "Biotechnology", + "sector": "Healthcare", + "market_cap": 17710000, + "employees": 14, + "website": "https://www.revivapharma.com", + "country": "United States", + "currency": "USD" + }, + "financial_data": { + "revenue": 0, + "net_income": -29100000, + "total_assets": 15500000, + "total_liabilities": 14690000, + "cash": 1020000, + "debt": 0 + }, + "valuation_analysis": { + "pe_ratio": null, + "pb_ratio": null, + "ps_ratio": null, + "peg_ratio": null, + "market_cap": 17710000, + "enterprise_value": 7460000, + "valuation_score": 15, + "is_undervalued": false + }, + "financial_health": { + "current_ratio": 0.9, + "quick_ratio": 0.9, + "debt_to_equity": null, + "debt_ratio": 0.95, + "roe": -9.16, + "roa": -2.75, + "profit_margin": null, + "operating_margin": null, + "health_score": 10, + "is_healthy": false + }, + "growth_analysis": { + "revenue_growth": 0, + "earnings_growth": 0, + "historical_growth": 0, + "growth_score": 5, + "is_growing": false + }, + "risk_analysis": { + "beta": -0.06, + "volatility": 0.894, + "max_drawdown": 0.93, + "risk_score": 20, + "risk_level": "极高" + }, + "investment_recommendation": { + "recommendation": "观望/高风险投机", + "confidence": "高", + "overall_score": 35, + "risk_warnings": [ + "极高风险投资", + "财务健康度极差", + "无收入来源", + "持续亏损", + "依赖融资维持运营", + "监管审批不确定性", + "可能完全损失本金" + ], + "key_strengths": [ + "专注精神分裂症治疗", + "主要产品临床试验积极", + "分析师全部给予买入评级", + "获得FDA对齐" + ], + "key_concerns": [ + "无商业化产品", + "持续亏损", + "高债务比率", + "股价大幅下跌", + "流动性风险" + ] + }, + "analyst_opinions": { + "total_analysts": 13, + "buy_ratings": 13, + "hold_ratings": 0, + "sell_ratings": 0, + "average_target_price": 14.46, + "price_target_range": "2.00 - 20.00" + }, + "market_data": { + "current_price": 0.26, + "previous_close": 0.42, + "day_change": -0.16, + "day_change_percent": -37.89, + "volume": 29973974, + "avg_volume": 2720000, + "market_cap": 17710000, + "shares_outstanding": 68000000, + "float": 60810000, + "insider_ownership": 10.59, + "institutional_ownership": 21.25 + }, + "raw_data": {} +} \ No newline at end of file diff --git a/analysis_reports/raw_data/RVPH_raw_data_20250920_015345.json b/analysis_reports/raw_data/RVPH_raw_data_20250920_015345.json new file mode 100644 index 0000000..5db4116 --- /dev/null +++ b/analysis_reports/raw_data/RVPH_raw_data_20250920_015345.json @@ -0,0 +1,105 @@ +{ + "company_info": { + "name": "Reviva Pharmaceuticals Holdings, Inc.", + "symbol": "RVPH", + "industry": "Biotechnology", + "sector": "Healthcare", + "market_cap": 17710000, + "employees": 14, + "website": "https://www.revivapharma.com", + "country": "United States", + "currency": "USD" + }, + "financial_data": { + "revenue": 0, + "net_income": -29100000, + "total_assets": 15500000, + "total_liabilities": 14690000, + "cash": 1020000, + "debt": 0 + }, + "valuation_analysis": { + "pe_ratio": null, + "pb_ratio": null, + "ps_ratio": null, + "peg_ratio": null, + "market_cap": 17710000, + "enterprise_value": 7460000, + "valuation_score": 15, + "is_undervalued": false + }, + "financial_health": { + "current_ratio": 0.9, + "quick_ratio": 0.9, + "debt_to_equity": null, + "debt_ratio": 0.95, + "roe": -9.16, + "roa": -2.75, + "profit_margin": null, + "operating_margin": null, + "health_score": 10, + "is_healthy": false + }, + "growth_analysis": { + "revenue_growth": 0, + "earnings_growth": 0, + "historical_growth": 0, + "growth_score": 5, + "is_growing": false + }, + "risk_analysis": { + "beta": -0.06, + "volatility": 0.894, + "max_drawdown": 0.93, + "risk_score": 20, + "risk_level": "极高" + }, + "investment_recommendation": { + "recommendation": "观望/高风险投机", + "confidence": "高", + "overall_score": 35, + "risk_warnings": [ + "极高风险投资", + "财务健康度极差", + "无收入来源", + "持续亏损", + "依赖融资维持运营", + "监管审批不确定性", + "可能完全损失本金" + ], + "key_strengths": [ + "专注精神分裂症治疗", + "主要产品临床试验积极", + "分析师全部给予买入评级", + "获得FDA对齐" + ], + "key_concerns": [ + "无商业化产品", + "持续亏损", + "高债务比率", + "股价大幅下跌", + "流动性风险" + ] + }, + "analyst_opinions": { + "total_analysts": 13, + "buy_ratings": 13, + "hold_ratings": 0, + "sell_ratings": 0, + "average_target_price": 14.46, + "price_target_range": "2.00 - 20.00" + }, + "market_data": { + "current_price": 0.26, + "previous_close": 0.42, + "day_change": -0.16, + "day_change_percent": -37.89, + "volume": 29973974, + "avg_volume": 2720000, + "market_cap": 17710000, + "shares_outstanding": 68000000, + "float": 60810000, + "insider_ownership": 10.59, + "institutional_ownership": 21.25 + } +} \ No newline at end of file diff --git a/analysis_reports/summaries/RVPH_summary_20250920_015345.md b/analysis_reports/summaries/RVPH_summary_20250920_015345.md new file mode 100644 index 0000000..09071f9 --- /dev/null +++ b/analysis_reports/summaries/RVPH_summary_20250920_015345.md @@ -0,0 +1,70 @@ +# RVPH 股票分析报告 + +## 📊 基本信息 +- **公司名称**: Reviva Pharmaceuticals Holdings, Inc. +- **行业**: Biotechnology +- **市值**: $17,710,000 +- **分析时间**: 2025-09-20 01:53:45 + +## 🎯 投资建议 +- **建议**: 观望/高风险投机 +- **信心度**: 高 +- **综合评分**: 35.0/100 + +## 📈 各维度评分 +| 维度 | 评分 | 评级 | +|------|------|------| +| 估值吸引力 | 15.0/100 | 较差 | +| 财务健康度 | 10.0/100 | 较差 | +| 成长性 | 5.0/100 | 较差 | +| 风险控制 | 20.0/100 | 较差 | + +## ✅ 关键优势 +- 专注精神分裂症治疗 +- 主要产品临床试验积极 +- 分析师全部给予买入评级 +- 获得FDA对齐 + +## ⚠️ 关键担忧 +- 无商业化产品 +- 持续亏损 +- 高债务比率 +- 股价大幅下跌 +- 流动性风险 + +## 🚨 风险提示 +- 极高风险投资 +- 财务健康度极差 +- 无收入来源 +- 持续亏损 +- 依赖融资维持运营 +- 监管审批不确定性 +- 可能完全损失本金 + +## 📋 详细指标 +### 估值指标 +- PE比率: None +- PB比率: None +- PS比率: None +- PEG比率: None + +### 财务健康度 +- 流动比率: 0.9 +- 速动比率: 0.9 +- 债务比率: 0.95 +- ROE: -9.16 + +### 成长性指标 +- 收入增长率: 0 +- 利润增长率: 0 +- 历史增长率: 0 + +### 风险指标 +- Beta系数: -0.06 +- 波动率: 0.894 +- 最大回撤: 0.93 +- 风险等级: 极高 + +--- +*分析时间: 2025-09-20 01:53:45* +*报告ID: 20250920_015345* diff --git a/analysis_storage.py b/analysis_storage.py new file mode 100644 index 0000000..f27c12f --- /dev/null +++ b/analysis_storage.py @@ -0,0 +1,282 @@ +""" +分析结果存储模块 - 标准化保存分析报告 +""" +import os +import json +from datetime import datetime +from typing import Dict, Any +import logging + +logger = logging.getLogger(__name__) + +class AnalysisStorage: + def __init__(self, base_dir: str = "analysis_reports"): + self.base_dir = base_dir + self.ensure_directories() + + def ensure_directories(self): + """确保必要的目录存在""" + directories = [ + self.base_dir, + os.path.join(self.base_dir, "detailed"), + os.path.join(self.base_dir, "summaries"), + os.path.join(self.base_dir, "charts"), + os.path.join(self.base_dir, "raw_data") + ] + + for directory in directories: + os.makedirs(directory, exist_ok=True) + + def save_analysis_report(self, symbol: str, analysis_data: Dict[str, Any]) -> Dict[str, str]: + """ + 保存分析报告到文件 + + Args: + symbol: 股票代码 + analysis_data: 分析数据 + + Returns: + 保存的文件路径字典 + """ + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + date_str = datetime.now().strftime("%Y-%m-%d") + + # 1. 保存详细分析报告 (JSON格式) + detailed_file = self._save_detailed_report(symbol, analysis_data, timestamp) + + # 2. 保存简要报告 (Markdown格式) + summary_file = self._save_summary_report(symbol, analysis_data, timestamp) + + # 3. 保存原始数据 (JSON格式) + raw_data_file = self._save_raw_data(symbol, analysis_data, timestamp) + + # 4. 更新索引文件 + self._update_index(symbol, analysis_data, timestamp) + + return { + 'detailed_report': detailed_file, + 'summary_report': summary_file, + 'raw_data': raw_data_file, + 'timestamp': timestamp, + 'date': date_str + } + + except Exception as e: + logger.error(f"保存分析报告失败 {symbol}: {e}") + return {} + + def _save_detailed_report(self, symbol: str, analysis_data: Dict, timestamp: str) -> str: + """保存详细分析报告""" + filename = f"{symbol}_detailed_{timestamp}.json" + filepath = os.path.join(self.base_dir, "detailed", filename) + + detailed_report = { + "analysis_metadata": { + "symbol": symbol, + "analysis_date": datetime.now().isoformat(), + "timestamp": timestamp, + "version": "1.0" + }, + "company_info": analysis_data.get('company_info', {}), + "financial_data": analysis_data.get('financial_data', {}), + "valuation_analysis": analysis_data.get('valuation_analysis', {}), + "financial_health": analysis_data.get('financial_health', {}), + "growth_analysis": analysis_data.get('growth_analysis', {}), + "risk_analysis": analysis_data.get('risk_analysis', {}), + "investment_recommendation": analysis_data.get('investment_recommendation', {}), + "analyst_opinions": analysis_data.get('analyst_opinions', {}), + "market_data": analysis_data.get('market_data', {}), + "raw_data": analysis_data.get('raw_data', {}) + } + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(detailed_report, f, ensure_ascii=False, indent=2) + + logger.info(f"详细报告已保存: {filepath}") + return filepath + + def _save_summary_report(self, symbol: str, analysis_data: Dict, timestamp: str) -> str: + """保存简要分析报告 (Markdown格式)""" + filename = f"{symbol}_summary_{timestamp}.md" + filepath = os.path.join(self.base_dir, "summaries", filename) + + # 生成Markdown格式的简要报告 + markdown_content = self._generate_markdown_summary(symbol, analysis_data, timestamp) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(markdown_content) + + logger.info(f"简要报告已保存: {filepath}") + return filepath + + def _save_raw_data(self, symbol: str, analysis_data: Dict, timestamp: str) -> str: + """保存原始数据""" + filename = f"{symbol}_raw_data_{timestamp}.json" + filepath = os.path.join(self.base_dir, "raw_data", filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(analysis_data, f, ensure_ascii=False, indent=2) + + logger.info(f"原始数据已保存: {filepath}") + return filepath + + def _update_index(self, symbol: str, analysis_data: Dict, timestamp: str): + """更新索引文件""" + index_file = os.path.join(self.base_dir, "analysis_index.json") + + # 读取现有索引 + if os.path.exists(index_file): + with open(index_file, 'r', encoding='utf-8') as f: + index_data = json.load(f) + else: + index_data = {"analyses": []} + + # 添加新分析记录 + new_entry = { + "symbol": symbol, + "timestamp": timestamp, + "analysis_date": datetime.now().isoformat(), + "company_name": analysis_data.get('company_info', {}).get('name', ''), + "recommendation": analysis_data.get('investment_recommendation', {}).get('recommendation', 'N/A'), + "overall_score": analysis_data.get('investment_recommendation', {}).get('overall_score', 0), + "files": { + "detailed": f"{symbol}_detailed_{timestamp}.json", + "summary": f"{symbol}_summary_{timestamp}.md", + "raw_data": f"{symbol}_raw_data_{timestamp}.json" + } + } + + index_data["analyses"].append(new_entry) + + # 按时间倒序排列 + index_data["analyses"].sort(key=lambda x: x["timestamp"], reverse=True) + + # 保存索引 + with open(index_file, 'w', encoding='utf-8') as f: + json.dump(index_data, f, ensure_ascii=False, indent=2) + + def _generate_markdown_summary(self, symbol: str, analysis_data: Dict, timestamp: str) -> str: + """生成Markdown格式的简要报告""" + company_info = analysis_data.get('company_info', {}) + investment_rec = analysis_data.get('investment_recommendation', {}) + valuation = analysis_data.get('valuation_analysis', {}) + financial_health = analysis_data.get('financial_health', {}) + growth = analysis_data.get('growth_analysis', {}) + risk = analysis_data.get('risk_analysis', {}) + + markdown = f"""# {symbol} 股票分析报告 + +## 📊 基本信息 +- **公司名称**: {company_info.get('name', 'N/A')} +- **行业**: {company_info.get('industry', 'N/A')} +- **市值**: ${company_info.get('market_cap', 0):,.0f} +- **分析时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +## 🎯 投资建议 +- **建议**: {investment_rec.get('recommendation', 'N/A')} +- **信心度**: {investment_rec.get('confidence', 'N/A')} +- **综合评分**: {investment_rec.get('overall_score', 0):.1f}/100 + +## 📈 各维度评分 +| 维度 | 评分 | 评级 | +|------|------|------| +| 估值吸引力 | {valuation.get('valuation_score', 0):.1f}/100 | {self._get_rating(valuation.get('valuation_score', 0))} | +| 财务健康度 | {financial_health.get('health_score', 0):.1f}/100 | {self._get_rating(financial_health.get('health_score', 0))} | +| 成长性 | {growth.get('growth_score', 0):.1f}/100 | {self._get_rating(growth.get('growth_score', 0))} | +| 风险控制 | {risk.get('risk_score', 0):.1f}/100 | {self._get_rating(risk.get('risk_score', 0))} | + +## ✅ 关键优势 +{self._format_list(investment_rec.get('key_strengths', []))} + +## ⚠️ 关键担忧 +{self._format_list(investment_rec.get('key_concerns', []))} + +## 🚨 风险提示 +{self._format_list(investment_rec.get('risk_warnings', []))} + +## 📋 详细指标 +### 估值指标 +- PE比率: {valuation.get('pe_ratio', 'N/A')} +- PB比率: {valuation.get('pb_ratio', 'N/A')} +- PS比率: {valuation.get('ps_ratio', 'N/A')} +- PEG比率: {valuation.get('peg_ratio', 'N/A')} + +### 财务健康度 +- 流动比率: {financial_health.get('current_ratio', 'N/A')} +- 速动比率: {financial_health.get('quick_ratio', 'N/A')} +- 债务比率: {financial_health.get('debt_ratio', 'N/A')} +- ROE: {financial_health.get('roe', 'N/A')} + +### 成长性指标 +- 收入增长率: {growth.get('revenue_growth', 'N/A')} +- 利润增长率: {growth.get('earnings_growth', 'N/A')} +- 历史增长率: {growth.get('historical_growth', 'N/A')} + +### 风险指标 +- Beta系数: {risk.get('beta', 'N/A')} +- 波动率: {risk.get('volatility', 'N/A')} +- 最大回撤: {risk.get('max_drawdown', 'N/A')} +- 风险等级: {risk.get('risk_level', 'N/A')} + +--- +*分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}* +*报告ID: {timestamp}* +""" + return markdown + + def _get_rating(self, score: float) -> str: + """根据分数获取评级""" + if score >= 90: + return "优秀" + elif score >= 80: + return "良好" + elif score >= 70: + return "中等" + elif score >= 60: + return "一般" + else: + return "较差" + + def _format_list(self, items: list) -> str: + """格式化列表""" + if not items: + return "无" + return "\n".join([f"- {item}" for item in items]) + + def get_analysis_history(self, symbol: str = None) -> list: + """获取分析历史""" + index_file = os.path.join(self.base_dir, "analysis_index.json") + + if not os.path.exists(index_file): + return [] + + with open(index_file, 'r', encoding='utf-8') as f: + index_data = json.load(f) + + analyses = index_data.get('analyses', []) + + if symbol: + analyses = [a for a in analyses if a['symbol'].upper() == symbol.upper()] + + return analyses + + def get_latest_analysis(self, symbol: str) -> Dict: + """获取最新分析结果""" + history = self.get_analysis_history(symbol) + if not history: + return {} + + latest = history[0] # 已按时间倒序排列 + + # 读取详细报告 + detailed_file = os.path.join(self.base_dir, "detailed", latest['files']['detailed']) + if os.path.exists(detailed_file): + with open(detailed_file, 'r', encoding='utf-8') as f: + return json.load(f) + + return {} + + def list_all_analyses(self) -> list: + """列出所有分析记录""" + return self.get_analysis_history() \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..bd7bf57 --- /dev/null +++ b/config.py @@ -0,0 +1,35 @@ +""" +美股低价值公司分析系统配置文件 +""" +import os +from dotenv import load_dotenv + +load_dotenv() + +# API配置 +YAHOO_FINANCE_API = "https://query1.finance.yahoo.com/v8/finance/chart/" +ALPHA_VANTAGE_API_KEY = os.getenv('ALPHA_VANTAGE_API_KEY', '') + +# 数据库配置 +DATABASE_PATH = "stock_analysis.db" + +# 分析参数配置 +LOW_VALUE_CRITERIA = { + 'max_pe_ratio': 15, # 最大市盈率 + 'max_pb_ratio': 1.5, # 最大市净率 + 'max_ps_ratio': 2.0, # 最大市销率 + 'min_market_cap': 100_000_000, # 最小市值(1亿美元) + 'max_debt_ratio': 0.6, # 最大债务比率 + 'min_current_ratio': 1.2, # 最小流动比率 + 'min_roe': 0.05, # 最小ROE(5%) +} + +# 报告配置 +REPORT_OUTPUT_DIR = "reports" +CHART_OUTPUT_DIR = "charts" + +# 数据更新频率(小时) +DATA_UPDATE_INTERVAL = 24 + +# 支持的股票市场 +SUPPORTED_MARKETS = ['NASDAQ', 'NYSE', 'AMEX'] \ No newline at end of file diff --git a/create_rvph_report.py b/create_rvph_report.py new file mode 100644 index 0000000..a026fec --- /dev/null +++ b/create_rvph_report.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +创建RVPH分析报告 +""" +import json +from datetime import datetime +from analysis_storage import AnalysisStorage + +def create_rvph_analysis(): + """创建RVPH的分析报告""" + + # 基于网络搜索的数据创建分析结果 + analysis_data = { + 'company_info': { + 'name': 'Reviva Pharmaceuticals Holdings, Inc.', + 'symbol': 'RVPH', + 'industry': 'Biotechnology', + 'sector': 'Healthcare', + 'market_cap': 17710000, # $17.71M + 'employees': 14, + 'website': 'https://www.revivapharma.com', + 'country': 'United States', + 'currency': 'USD' + }, + 'financial_data': { + 'revenue': 0, + 'net_income': -29100000, # -$29.1M + 'total_assets': 15500000, # $15.5M + 'total_liabilities': 14690000, # $14.69M + 'cash': 1020000, # $1.02M + 'debt': 0 + }, + 'valuation_analysis': { + 'pe_ratio': None, # 负盈利 + 'pb_ratio': None, # 负账面价值 + 'ps_ratio': None, # 无收入 + 'peg_ratio': None, + 'market_cap': 17710000, + 'enterprise_value': 7460000, # $7.46M + 'valuation_score': 15, # 低估值分数 + 'is_undervalued': False + }, + 'financial_health': { + 'current_ratio': 0.90, + 'quick_ratio': 0.90, + 'debt_to_equity': None, + 'debt_ratio': 0.95, # 95%债务比率 + 'roe': -9.16, # -916% + 'roa': -2.75, # -275% + 'profit_margin': None, # 负值 + 'operating_margin': None, # 负值 + 'health_score': 10, # 极低健康度分数 + 'is_healthy': False + }, + 'growth_analysis': { + 'revenue_growth': 0, # 无收入 + 'earnings_growth': 0, # 持续亏损 + 'historical_growth': 0, + 'growth_score': 5, # 极低成长性分数 + 'is_growing': False + }, + 'risk_analysis': { + 'beta': -0.06, + 'volatility': 0.894, # 89.4% + 'max_drawdown': 0.93, # 93%最大回撤 + 'risk_score': 20, # 高风险分数 + 'risk_level': '极高' + }, + 'investment_recommendation': { + 'recommendation': '观望/高风险投机', + 'confidence': '高', + 'overall_score': 35, # 35/100分 + 'risk_warnings': [ + '极高风险投资', + '财务健康度极差', + '无收入来源', + '持续亏损', + '依赖融资维持运营', + '监管审批不确定性', + '可能完全损失本金' + ], + 'key_strengths': [ + '专注精神分裂症治疗', + '主要产品临床试验积极', + '分析师全部给予买入评级', + '获得FDA对齐' + ], + 'key_concerns': [ + '无商业化产品', + '持续亏损', + '高债务比率', + '股价大幅下跌', + '流动性风险' + ] + }, + 'analyst_opinions': { + 'total_analysts': 13, + 'buy_ratings': 13, + 'hold_ratings': 0, + 'sell_ratings': 0, + 'average_target_price': 14.46, + 'price_target_range': '2.00 - 20.00' + }, + 'market_data': { + 'current_price': 0.26, + 'previous_close': 0.42, + 'day_change': -0.16, + 'day_change_percent': -37.89, + 'volume': 29973974, + 'avg_volume': 2720000, + 'market_cap': 17710000, + 'shares_outstanding': 68000000, + 'float': 60810000, + 'insider_ownership': 10.59, + 'institutional_ownership': 21.25 + } + } + + # 保存分析报告 + storage = AnalysisStorage() + result = storage.save_analysis_report('RVPH', analysis_data) + + print("✅ RVPH分析报告已创建并保存") + print(f"📁 详细报告: {result.get('detailed_report', 'N/A')}") + print(f"📄 简要报告: {result.get('summary_report', 'N/A')}") + print(f"💾 原始数据: {result.get('raw_data', 'N/A')}") + + return result + +if __name__ == "__main__": + create_rvph_analysis() \ No newline at end of file diff --git a/data_collector.py b/data_collector.py new file mode 100644 index 0000000..fe23c8d --- /dev/null +++ b/data_collector.py @@ -0,0 +1,216 @@ +""" +数据收集模块 - 从各种API获取股票和财务数据 +""" +import yfinance as yf +import pandas as pd +import requests +from typing import Dict, List, Optional, Tuple +import time +from datetime import datetime, timedelta +import logging + +# 设置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class StockDataCollector: + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }) + + def get_company_info(self, symbol: str) -> Dict: + """获取公司基本信息""" + try: + ticker = yf.Ticker(symbol) + info = ticker.info + + return { + 'symbol': symbol, + 'name': info.get('longName', ''), + 'sector': info.get('sector', ''), + 'industry': info.get('industry', ''), + 'market_cap': info.get('marketCap', 0), + 'employees': info.get('fullTimeEmployees', 0), + 'website': info.get('website', ''), + 'description': info.get('longBusinessSummary', ''), + 'country': info.get('country', ''), + 'currency': info.get('currency', 'USD') + } + except Exception as e: + logger.error(f"获取公司信息失败 {symbol}: {e}") + return {} + + def get_stock_prices(self, symbol: str, period: str = "1y") -> pd.DataFrame: + """获取股价数据""" + try: + ticker = yf.Ticker(symbol) + data = ticker.history(period=period) + + if data.empty: + logger.warning(f"未找到股价数据: {symbol}") + return pd.DataFrame() + + # 重命名列以匹配数据库结构 + data = data.reset_index() + data.columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'dividends', 'stock_splits'] + data = data.drop(['dividends', 'stock_splits'], axis=1) + + return data + except Exception as e: + logger.error(f"获取股价数据失败 {symbol}: {e}") + return pd.DataFrame() + + def get_financial_statements(self, symbol: str) -> Dict: + """获取财务报表数据""" + try: + ticker = yf.Ticker(symbol) + + # 获取季度和年度财务数据 + quarterly_data = {} + annual_data = {} + + # 季度数据 + try: + quarterly_financials = ticker.quarterly_financials + if not quarterly_financials.empty: + for i, (date, row) in enumerate(quarterly_financials.iterrows()): + quarterly_data[f"Q{i+1}_{date.year}"] = { + 'year': date.year, + 'quarter': (i % 4) + 1, + 'revenue': row.get('Total Revenue', 0), + 'net_income': row.get('Net Income', 0), + 'total_assets': row.get('Total Assets', 0), + 'total_liabilities': row.get('Total Liabilities', 0), + 'shareholders_equity': row.get('Stockholders Equity', 0), + 'cash': row.get('Cash And Cash Equivalents', 0), + 'debt': row.get('Total Debt', 0) + } + except Exception as e: + logger.warning(f"获取季度财务数据失败 {symbol}: {e}") + + # 年度数据 + try: + annual_financials = ticker.financials + if not annual_financials.empty: + for i, (date, row) in enumerate(annual_financials.iterrows()): + annual_data[f"Annual_{date.year}"] = { + 'year': date.year, + 'quarter': 0, + 'revenue': row.get('Total Revenue', 0), + 'net_income': row.get('Net Income', 0), + 'total_assets': row.get('Total Assets', 0), + 'total_liabilities': row.get('Total Liabilities', 0), + 'shareholders_equity': row.get('Stockholders Equity', 0), + 'cash': row.get('Cash And Cash Equivalents', 0), + 'debt': row.get('Total Debt', 0) + } + except Exception as e: + logger.warning(f"获取年度财务数据失败 {symbol}: {e}") + + return {**quarterly_data, **annual_data} + except Exception as e: + logger.error(f"获取财务数据失败 {symbol}: {e}") + return {} + + def get_key_metrics(self, symbol: str) -> Dict: + """获取关键财务指标""" + try: + ticker = yf.Ticker(symbol) + info = ticker.info + + return { + 'pe_ratio': info.get('trailingPE', 0), + 'pb_ratio': info.get('priceToBook', 0), + 'ps_ratio': info.get('priceToSalesTrailing12Months', 0), + 'peg_ratio': info.get('pegRatio', 0), + 'debt_to_equity': info.get('debtToEquity', 0), + 'current_ratio': info.get('currentRatio', 0), + 'quick_ratio': info.get('quickRatio', 0), + 'return_on_equity': info.get('returnOnEquity', 0), + 'return_on_assets': info.get('returnOnAssets', 0), + 'profit_margin': info.get('profitMargins', 0), + 'operating_margin': info.get('operatingMargins', 0), + 'revenue_growth': info.get('revenueGrowth', 0), + 'earnings_growth': info.get('earningsGrowth', 0), + 'beta': info.get('beta', 0), + 'dividend_yield': info.get('dividendYield', 0), + 'payout_ratio': info.get('payoutRatio', 0) + } + except Exception as e: + logger.error(f"获取关键指标失败 {symbol}: {e}") + return {} + + def get_analyst_recommendations(self, symbol: str) -> Dict: + """获取分析师推荐""" + try: + ticker = yf.Ticker(symbol) + recommendations = ticker.recommendations + + if recommendations is None or recommendations.empty: + return {} + + # 获取最新的推荐 + latest_rec = recommendations.iloc[-1] if not recommendations.empty else None + + return { + 'latest_recommendation': latest_rec.get('To Grade', '') if latest_rec is not None else '', + 'latest_firm': latest_rec.get('Firm', '') if latest_rec is not None else '', + 'latest_date': latest_rec.get('Date', '') if latest_rec is not None else '', + 'total_recommendations': len(recommendations) + } + except Exception as e: + logger.warning(f"获取分析师推荐失败 {symbol}: {e}") + return {} + + def get_news_sentiment(self, symbol: str) -> Dict: + """获取新闻情绪分析(简化版)""" + try: + ticker = yf.Ticker(symbol) + news = ticker.news + + if not news: + return {'sentiment_score': 0, 'news_count': 0} + + # 简单的情绪分析(实际应用中可以使用更复杂的NLP模型) + positive_keywords = ['growth', 'profit', 'increase', 'strong', 'positive', 'beat', 'exceed'] + negative_keywords = ['loss', 'decline', 'weak', 'negative', 'miss', 'fall', 'drop'] + + sentiment_score = 0 + for article in news[:10]: # 只分析最近10条新闻 + title = article.get('title', '').lower() + summary = article.get('summary', '').lower() + text = title + ' ' + summary + + positive_count = sum(1 for word in positive_keywords if word in text) + negative_count = sum(1 for word in negative_keywords if word in text) + + sentiment_score += (positive_count - negative_count) + + return { + 'sentiment_score': sentiment_score, + 'news_count': len(news), + 'recent_news': news[:5] # 最近5条新闻 + } + except Exception as e: + logger.warning(f"获取新闻情绪失败 {symbol}: {e}") + return {'sentiment_score': 0, 'news_count': 0} + + def collect_all_data(self, symbol: str) -> Dict: + """收集所有相关数据""" + logger.info(f"开始收集数据: {symbol}") + + all_data = { + 'symbol': symbol, + 'collection_time': datetime.now().isoformat(), + 'company_info': self.get_company_info(symbol), + 'stock_prices': self.get_stock_prices(symbol), + 'financial_statements': self.get_financial_statements(symbol), + 'key_metrics': self.get_key_metrics(symbol), + 'analyst_recommendations': self.get_analyst_recommendations(symbol), + 'news_sentiment': self.get_news_sentiment(symbol) + } + + logger.info(f"数据收集完成: {symbol}") + return all_data \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..5c0e2cc --- /dev/null +++ b/database.py @@ -0,0 +1,226 @@ +""" +数据库管理模块 +""" +import sqlite3 +import pandas as pd +from datetime import datetime +from typing import Dict, List, Optional +import json + +class StockDatabase: + def __init__(self, db_path: str = "stock_analysis.db"): + self.db_path = db_path + self.init_database() + + def init_database(self): + """初始化数据库表结构""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 公司基本信息表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS companies ( + symbol TEXT PRIMARY KEY, + name TEXT NOT NULL, + sector TEXT, + industry TEXT, + market_cap REAL, + employees INTEGER, + website TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 股价数据表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS stock_prices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + date DATE NOT NULL, + open REAL, + high REAL, + low REAL, + close REAL, + volume INTEGER, + adjusted_close REAL, + FOREIGN KEY (symbol) REFERENCES companies (symbol), + UNIQUE(symbol, date) + ) + ''') + + # 财务数据表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS financial_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + period TEXT NOT NULL, + year INTEGER NOT NULL, + quarter INTEGER, + revenue REAL, + net_income REAL, + total_assets REAL, + total_liabilities REAL, + shareholders_equity REAL, + cash REAL, + debt REAL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (symbol) REFERENCES companies (symbol), + UNIQUE(symbol, period, year, quarter) + ) + ''') + + # 分析结果表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS analysis_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + analysis_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + valuation_score REAL, + financial_health_score REAL, + growth_score REAL, + risk_score REAL, + overall_score REAL, + recommendation TEXT, + analysis_data TEXT, -- JSON格式存储详细分析数据 + FOREIGN KEY (symbol) REFERENCES companies (symbol) + ) + ''') + + conn.commit() + conn.close() + + def save_company_info(self, symbol: str, company_data: Dict): + """保存公司基本信息""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + INSERT OR REPLACE INTO companies + (symbol, name, sector, industry, market_cap, employees, website, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + symbol, + company_data.get('name', ''), + company_data.get('sector', ''), + company_data.get('industry', ''), + company_data.get('market_cap', 0), + company_data.get('employees', 0), + company_data.get('website', ''), + datetime.now() + )) + + conn.commit() + conn.close() + + def save_stock_prices(self, symbol: str, price_data: pd.DataFrame): + """保存股价数据""" + conn = sqlite3.connect(self.db_path) + + price_data['symbol'] = symbol + price_data.to_sql('stock_prices', conn, if_exists='append', index=False) + + conn.close() + + def save_financial_data(self, symbol: str, financial_data: Dict): + """保存财务数据""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + for period, data in financial_data.items(): + cursor.execute(''' + INSERT OR REPLACE INTO financial_data + (symbol, period, year, quarter, revenue, net_income, total_assets, + total_liabilities, shareholders_equity, cash, debt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + symbol, + period, + data.get('year', 0), + data.get('quarter', 0), + data.get('revenue', 0), + data.get('net_income', 0), + data.get('total_assets', 0), + data.get('total_liabilities', 0), + data.get('shareholders_equity', 0), + data.get('cash', 0), + data.get('debt', 0) + )) + + conn.commit() + conn.close() + + def save_analysis_result(self, symbol: str, analysis_data: Dict): + """保存分析结果""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO analysis_results + (symbol, valuation_score, financial_health_score, growth_score, + risk_score, overall_score, recommendation, analysis_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + symbol, + analysis_data.get('valuation_score', 0), + analysis_data.get('financial_health_score', 0), + analysis_data.get('growth_score', 0), + analysis_data.get('risk_score', 0), + analysis_data.get('overall_score', 0), + analysis_data.get('recommendation', ''), + json.dumps(analysis_data.get('detailed_analysis', {})) + )) + + conn.commit() + conn.close() + + def get_company_info(self, symbol: str) -> Optional[Dict]: + """获取公司信息""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute('SELECT * FROM companies WHERE symbol = ?', (symbol,)) + result = cursor.fetchone() + + conn.close() + + if result: + return { + 'symbol': result[0], + 'name': result[1], + 'sector': result[2], + 'industry': result[3], + 'market_cap': result[4], + 'employees': result[5], + 'website': result[6] + } + return None + + def get_latest_analysis(self, symbol: str) -> Optional[Dict]: + """获取最新分析结果""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM analysis_results + WHERE symbol = ? + ORDER BY analysis_date DESC + LIMIT 1 + ''', (symbol,)) + result = cursor.fetchone() + + conn.close() + + if result: + return { + 'symbol': result[1], + 'analysis_date': result[2], + 'valuation_score': result[3], + 'financial_health_score': result[4], + 'growth_score': result[5], + 'risk_score': result[6], + 'overall_score': result[7], + 'recommendation': result[8], + 'analysis_data': json.loads(result[9]) if result[9] else {} + } + return None \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4596da2 --- /dev/null +++ b/main.py @@ -0,0 +1,330 @@ +""" +美股低价值公司分析系统 - 主程序 +""" +import sys +import logging +from datetime import datetime +from typing import Dict, Optional + +# 导入自定义模块 +from data_collector import StockDataCollector +from analysis_engine import StockAnalyzer +from report_generator import ReportGenerator +from database import StockDatabase +from analysis_storage import AnalysisStorage +from config import LOW_VALUE_CRITERIA + +# 设置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('stock_analysis.log', encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +class StockAnalysisSystem: + def __init__(self): + self.data_collector = StockDataCollector() + self.analyzer = StockAnalyzer() + self.report_generator = ReportGenerator() + self.database = StockDatabase() + self.storage = AnalysisStorage() + + def analyze_stock(self, symbol: str, force_refresh: bool = False) -> Dict: + """ + 分析单只股票 + + Args: + symbol: 股票代码 + force_refresh: 是否强制刷新数据 + + Returns: + 分析结果字典 + """ + try: + logger.info(f"开始分析股票: {symbol}") + + # 检查是否需要刷新数据 + if not force_refresh: + latest_analysis = self.database.get_latest_analysis(symbol) + if latest_analysis: + # 检查分析时间是否在24小时内 + analysis_time = datetime.fromisoformat(latest_analysis['analysis_date']) + if (datetime.now() - analysis_time).total_seconds() < 86400: # 24小时 + logger.info(f"使用缓存的分析结果: {symbol}") + return latest_analysis + + # 1. 数据收集阶段 + logger.info(f"正在收集数据: {symbol}") + raw_data = self.data_collector.collect_all_data(symbol) + + if not raw_data.get('company_info'): + logger.error(f"无法获取公司信息: {symbol}") + return {'error': '无法获取公司信息'} + + # 保存数据到数据库 + self.database.save_company_info(symbol, raw_data['company_info']) + if not raw_data['stock_prices'].empty: + self.database.save_stock_prices(symbol, raw_data['stock_prices']) + if raw_data['financial_statements']: + self.database.save_financial_data(symbol, raw_data['financial_statements']) + + # 2. 分析阶段 + logger.info(f"正在进行深度分析: {symbol}") + + # 估值分析 + valuation_results = self.analyzer.calculate_valuation_metrics(raw_data) + + # 财务健康度分析 + health_results = self.analyzer.calculate_financial_health(raw_data) + + # 成长性分析 + growth_results = self.analyzer.calculate_growth_metrics(raw_data) + + # 风险分析 + risk_results = self.analyzer.calculate_risk_metrics(raw_data) + + # DCF估值 + dcf_results = self.analyzer.perform_dcf_valuation(raw_data) + + # 生成投资建议 + investment_recommendation = self.analyzer.generate_investment_recommendation({ + 'valuation_score': valuation_results.get('valuation_score', 0), + 'financial_health_score': health_results.get('health_score', 0), + 'growth_score': growth_results.get('growth_score', 0), + 'risk_score': risk_results.get('risk_score', 0) + }) + + # 整合分析结果 + analysis_results = { + 'symbol': symbol, + 'analysis_date': datetime.now().isoformat(), + 'valuation_metrics': valuation_results, + 'financial_health': health_results, + 'growth_metrics': growth_results, + 'risk_metrics': risk_results, + 'dcf_analysis': dcf_results, + 'investment_recommendation': investment_recommendation, + 'overall_score': investment_recommendation.get('overall_score', 0), + 'recommendation': investment_recommendation.get('recommendation', 'N/A') + } + + # 保存分析结果到数据库 + self.database.save_analysis_result(symbol, analysis_results) + + # 3. 报告生成阶段 + logger.info(f"正在生成报告: {symbol}") + + # 生成详细报告 + report_file = self.report_generator.generate_comprehensive_report( + symbol, raw_data, analysis_results + ) + + # 生成简要报告 + summary_file = self.report_generator.generate_summary_report( + symbol, analysis_results + ) + + # 4. 保存标准化分析报告 + logger.info(f"正在保存分析报告: {symbol}") + storage_result = self.storage.save_analysis_report(symbol, { + 'company_info': raw_data.get('company_info', {}), + 'financial_data': raw_data.get('financial_statements', {}), + 'valuation_analysis': analysis_results.get('valuation_metrics', {}), + 'financial_health': analysis_results.get('financial_health', {}), + 'growth_analysis': analysis_results.get('growth_metrics', {}), + 'risk_analysis': analysis_results.get('risk_metrics', {}), + 'investment_recommendation': analysis_results.get('investment_recommendation', {}), + 'analyst_opinions': raw_data.get('analyst_recommendations', {}), + 'market_data': raw_data.get('stock_prices', {}), + 'raw_data': raw_data + }) + + analysis_results['report_file'] = report_file + analysis_results['summary_file'] = summary_file + analysis_results['storage_files'] = storage_result + + logger.info(f"分析完成: {symbol}") + return analysis_results + + except Exception as e: + logger.error(f"分析股票失败 {symbol}: {e}") + return {'error': str(e)} + + def quick_screening(self, symbol: str) -> Dict: + """ + 快速筛选 - 检查是否符合低价值投资标准 + + Args: + symbol: 股票代码 + + Returns: + 筛选结果 + """ + try: + logger.info(f"快速筛选: {symbol}") + + # 获取基础数据 + raw_data = self.data_collector.collect_all_data(symbol) + + if not raw_data.get('company_info'): + return {'error': '无法获取公司信息', 'passes_screening': False} + + # 基础筛选条件 + company_info = raw_data['company_info'] + key_metrics = raw_data.get('key_metrics', {}) + + screening_results = { + 'symbol': symbol, + 'company_name': company_info.get('name', ''), + 'market_cap': company_info.get('market_cap', 0), + 'passes_screening': True, + 'criteria_met': [], + 'criteria_failed': [] + } + + # 检查市值 + if company_info.get('market_cap', 0) >= LOW_VALUE_CRITERIA['min_market_cap']: + screening_results['criteria_met'].append('市值符合要求') + else: + screening_results['criteria_failed'].append('市值过小') + screening_results['passes_screening'] = False + + # 检查PE比率 + pe_ratio = key_metrics.get('pe_ratio', 0) + if 0 < pe_ratio <= LOW_VALUE_CRITERIA['max_pe_ratio']: + screening_results['criteria_met'].append(f'PE比率合理 ({pe_ratio:.2f})') + elif pe_ratio > LOW_VALUE_CRITERIA['max_pe_ratio']: + screening_results['criteria_failed'].append(f'PE比率过高 ({pe_ratio:.2f})') + screening_results['passes_screening'] = False + + # 检查PB比率 + pb_ratio = key_metrics.get('pb_ratio', 0) + if 0 < pb_ratio <= LOW_VALUE_CRITERIA['max_pb_ratio']: + screening_results['criteria_met'].append(f'PB比率合理 ({pb_ratio:.2f})') + elif pb_ratio > LOW_VALUE_CRITERIA['max_pb_ratio']: + screening_results['criteria_failed'].append(f'PB比率过高 ({pb_ratio:.2f})') + screening_results['passes_screening'] = False + + # 检查PS比率 + ps_ratio = key_metrics.get('ps_ratio', 0) + if 0 < ps_ratio <= LOW_VALUE_CRITERIA['max_ps_ratio']: + screening_results['criteria_met'].append(f'PS比率合理 ({ps_ratio:.2f})') + elif ps_ratio > LOW_VALUE_CRITERIA['max_ps_ratio']: + screening_results['criteria_failed'].append(f'PS比率过高 ({ps_ratio:.2f})') + screening_results['passes_screening'] = False + + return screening_results + + except Exception as e: + logger.error(f"快速筛选失败 {symbol}: {e}") + return {'error': str(e), 'passes_screening': False} + + def batch_analysis(self, symbols: list) -> Dict: + """ + 批量分析多只股票 + + Args: + symbols: 股票代码列表 + + Returns: + 批量分析结果 + """ + results = {} + + for symbol in symbols: + logger.info(f"批量分析: {symbol}") + results[symbol] = self.analyze_stock(symbol) + + return results + +def main(): + """主函数 - 命令行接口""" + if len(sys.argv) < 2: + print("使用方法:") + print("python main.py <股票代码> [--refresh]") + print("python main.py --batch <股票代码1,股票代码2,...>") + print("python main.py --screen <股票代码>") + return + + system = StockAnalysisSystem() + + if sys.argv[1] == '--batch': + # 批量分析 + if len(sys.argv) < 3: + print("请提供股票代码列表") + return + + symbols = [s.strip() for s in sys.argv[2].split(',')] + results = system.batch_analysis(symbols) + + print("\n=== 批量分析结果 ===") + for symbol, result in results.items(): + if 'error' in result: + print(f"{symbol}: 分析失败 - {result['error']}") + else: + print(f"{symbol}: {result.get('recommendation', 'N/A')} (评分: {result.get('overall_score', 0):.1f})") + + elif sys.argv[1] == '--screen': + # 快速筛选 + if len(sys.argv) < 3: + print("请提供股票代码") + return + + symbol = sys.argv[2].upper() + result = system.quick_screening(symbol) + + print(f"\n=== {symbol} 快速筛选结果 ===") + if 'error' in result: + print(f"筛选失败: {result['error']}") + else: + print(f"公司名称: {result.get('company_name', 'N/A')}") + print(f"市值: ${result.get('market_cap', 0):,.0f}") + print(f"通过筛选: {'是' if result.get('passes_screening') else '否'}") + + if result.get('criteria_met'): + print("符合条件:") + for criteria in result['criteria_met']: + print(f" ✓ {criteria}") + + if result.get('criteria_failed'): + print("不符合条件:") + for criteria in result['criteria_failed']: + print(f" ✗ {criteria}") + + else: + # 单只股票分析 + symbol = sys.argv[1].upper() + force_refresh = '--refresh' in sys.argv + + print(f"正在分析股票: {symbol}") + result = system.analyze_stock(symbol, force_refresh) + + if 'error' in result: + print(f"分析失败: {result['error']}") + else: + print(f"\n=== {symbol} 分析结果 ===") + print(f"投资建议: {result.get('recommendation', 'N/A')}") + print(f"综合评分: {result.get('overall_score', 0):.1f}/100") + + recommendation = result.get('investment_recommendation', {}) + if recommendation.get('key_strengths'): + print("关键优势:") + for strength in recommendation['key_strengths']: + print(f" • {strength}") + + if recommendation.get('key_concerns'): + print("关键担忧:") + for concern in recommendation['key_concerns']: + print(f" • {concern}") + + if result.get('report_file'): + print(f"\n详细报告已生成: {result['report_file']}") + if result.get('summary_file'): + print(f"简要报告已生成: {result['summary_file']}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/report_generator.py b/report_generator.py new file mode 100644 index 0000000..13161ea --- /dev/null +++ b/report_generator.py @@ -0,0 +1,425 @@ +""" +报告生成器 - 生成综合分析报告 +""" +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import plotly.graph_objects as go +import plotly.express as px +from plotly.subplots import make_subplots +from reportlab.lib.pagesizes import letter, A4 +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT +import os +from datetime import datetime +from typing import Dict, List +import logging + +logger = logging.getLogger(__name__) + +class ReportGenerator: + def __init__(self, output_dir: str = "reports"): + self.output_dir = output_dir + self.chart_dir = os.path.join(output_dir, "charts") + + # 创建输出目录 + os.makedirs(self.output_dir, exist_ok=True) + os.makedirs(self.chart_dir, exist_ok=True) + + # 设置中文字体 + plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] + plt.rcParams['axes.unicode_minus'] = False + + def generate_comprehensive_report(self, symbol: str, data: Dict, analysis_results: Dict) -> str: + """生成综合分析报告""" + try: + # 生成图表 + chart_files = self._generate_charts(symbol, data, analysis_results) + + # 生成PDF报告 + report_file = self._generate_pdf_report(symbol, data, analysis_results, chart_files) + + logger.info(f"报告生成完成: {report_file}") + return report_file + except Exception as e: + logger.error(f"生成报告失败: {e}") + return "" + + def _generate_charts(self, symbol: str, data: Dict, analysis_results: Dict) -> List[str]: + """生成图表""" + chart_files = [] + + try: + # 股价走势图 + price_chart = self._create_price_chart(symbol, data) + if price_chart: + chart_files.append(price_chart) + + # 财务指标雷达图 + radar_chart = self._create_radar_chart(symbol, analysis_results) + if radar_chart: + chart_files.append(radar_chart) + + # 估值对比图 + valuation_chart = self._create_valuation_chart(symbol, analysis_results) + if valuation_chart: + chart_files.append(valuation_chart) + + # 财务健康度仪表盘 + health_gauge = self._create_health_gauge(symbol, analysis_results) + if health_gauge: + chart_files.append(health_gauge) + + except Exception as e: + logger.error(f"生成图表失败: {e}") + + return chart_files + + def _create_price_chart(self, symbol: str, data: Dict) -> str: + """创建股价走势图""" + try: + stock_prices = data.get('stock_prices', pd.DataFrame()) + if stock_prices.empty: + return "" + + fig = go.Figure() + + # 添加收盘价线 + fig.add_trace(go.Scatter( + x=stock_prices['date'], + y=stock_prices['close'], + mode='lines', + name='收盘价', + line=dict(color='#1f77b4', width=2) + )) + + # 添加移动平均线 + if len(stock_prices) >= 20: + stock_prices['MA20'] = stock_prices['close'].rolling(window=20).mean() + fig.add_trace(go.Scatter( + x=stock_prices['date'], + y=stock_prices['MA20'], + mode='lines', + name='20日均线', + line=dict(color='orange', width=1, dash='dash') + )) + + fig.update_layout( + title=f'{symbol} 股价走势图', + xaxis_title='日期', + yaxis_title='价格 (USD)', + template='plotly_white', + height=400 + ) + + chart_file = os.path.join(self.chart_dir, f'{symbol}_price_chart.html') + fig.write_html(chart_file) + return chart_file + except Exception as e: + logger.error(f"创建股价图表失败: {e}") + return "" + + def _create_radar_chart(self, symbol: str, analysis_results: Dict) -> str: + """创建雷达图""" + try: + categories = ['估值吸引力', '财务健康度', '成长性', '风险控制'] + values = [ + analysis_results.get('valuation_score', 0), + analysis_results.get('financial_health_score', 0), + analysis_results.get('growth_score', 0), + analysis_results.get('risk_score', 0) + ] + + fig = go.Figure() + + fig.add_trace(go.Scatterpolar( + r=values, + theta=categories, + fill='toself', + name=symbol, + line_color='blue' + )) + + fig.update_layout( + polar=dict( + radialaxis=dict( + visible=True, + range=[0, 100] + )), + showlegend=True, + title=f'{symbol} 综合评分雷达图', + height=400 + ) + + chart_file = os.path.join(self.chart_dir, f'{symbol}_radar_chart.html') + fig.write_html(chart_file) + return chart_file + except Exception as e: + logger.error(f"创建雷达图失败: {e}") + return "" + + def _create_valuation_chart(self, symbol: str, analysis_results: Dict) -> str: + """创建估值对比图""" + try: + valuation_data = analysis_results.get('valuation_metrics', {}) + + metrics = ['PE比率', 'PB比率', 'PS比率', 'PEG比率'] + values = [ + valuation_data.get('pe_ratio', 0), + valuation_data.get('pb_ratio', 0), + valuation_data.get('ps_ratio', 0), + valuation_data.get('peg_ratio', 0) + ] + + # 行业平均值(示例数据) + industry_avg = [15, 2.5, 3.0, 1.2] + + fig = go.Figure() + + fig.add_trace(go.Bar( + name=symbol, + x=metrics, + y=values, + marker_color='lightblue' + )) + + fig.add_trace(go.Bar( + name='行业平均', + x=metrics, + y=industry_avg, + marker_color='lightcoral' + )) + + fig.update_layout( + title=f'{symbol} 估值指标对比', + xaxis_title='指标', + yaxis_title='数值', + barmode='group', + height=400 + ) + + chart_file = os.path.join(self.chart_dir, f'{symbol}_valuation_chart.html') + fig.write_html(chart_file) + return chart_file + except Exception as e: + logger.error(f"创建估值图表失败: {e}") + return "" + + def _create_health_gauge(self, symbol: str, analysis_results: Dict) -> str: + """创建财务健康度仪表盘""" + try: + health_score = analysis_results.get('financial_health_score', 0) + + fig = go.Figure(go.Indicator( + mode = "gauge+number+delta", + value = health_score, + domain = {'x': [0, 1], 'y': [0, 1]}, + title = {'text': f"{symbol} 财务健康度"}, + delta = {'reference': 50}, + gauge = { + 'axis': {'range': [None, 100]}, + 'bar': {'color': "darkblue"}, + 'steps': [ + {'range': [0, 40], 'color': "lightgray"}, + {'range': [40, 70], 'color': "yellow"}, + {'range': [70, 100], 'color': "green"} + ], + 'threshold': { + 'line': {'color': "red", 'width': 4}, + 'thickness': 0.75, + 'value': 90 + } + } + )) + + fig.update_layout(height=300) + + chart_file = os.path.join(self.chart_dir, f'{symbol}_health_gauge.html') + fig.write_html(chart_file) + return chart_file + except Exception as e: + logger.error(f"创建健康度仪表盘失败: {e}") + return "" + + def _generate_pdf_report(self, symbol: str, data: Dict, analysis_results: Dict, chart_files: List[str]) -> str: + """生成PDF报告""" + try: + report_file = os.path.join(self.output_dir, f'{symbol}_analysis_report.pdf') + doc = SimpleDocTemplate(report_file, pagesize=A4) + styles = getSampleStyleSheet() + story = [] + + # 标题样式 + title_style = ParagraphStyle( + 'CustomTitle', + parent=styles['Heading1'], + fontSize=18, + spaceAfter=30, + alignment=TA_CENTER, + textColor=colors.darkblue + ) + + # 添加标题 + story.append(Paragraph(f"{symbol} 股票分析报告", title_style)) + story.append(Spacer(1, 20)) + + # 基本信息 + company_info = data.get('company_info', {}) + story.append(Paragraph("基本信息", styles['Heading2'])) + + basic_info_data = [ + ['公司名称', company_info.get('name', 'N/A')], + ['行业', company_info.get('industry', 'N/A')], + ['市值', f"${company_info.get('market_cap', 0):,.0f}"], + ['员工数', f"{company_info.get('employees', 0):,}"], + ['分析日期', datetime.now().strftime('%Y-%m-%d %H:%M:%S')] + ] + + basic_info_table = Table(basic_info_data, colWidths=[2*inch, 4*inch]) + basic_info_table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.grey), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 12), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), colors.beige), + ('GRID', (0, 0), (-1, -1), 1, colors.black) + ])) + + story.append(basic_info_table) + story.append(Spacer(1, 20)) + + # 分析结果 + story.append(Paragraph("分析结果", styles['Heading2'])) + + # 评分表格 + scores_data = [ + ['分析维度', '评分', '评级'], + ['估值吸引力', f"{analysis_results.get('valuation_score', 0):.1f}/100", self._get_rating(analysis_results.get('valuation_score', 0))], + ['财务健康度', f"{analysis_results.get('financial_health_score', 0):.1f}/100", self._get_rating(analysis_results.get('financial_health_score', 0))], + ['成长性', f"{analysis_results.get('growth_score', 0):.1f}/100", self._get_rating(analysis_results.get('growth_score', 0))], + ['风险控制', f"{analysis_results.get('risk_score', 0):.1f}/100", self._get_rating(analysis_results.get('risk_score', 0))], + ['综合评分', f"{analysis_results.get('overall_score', 0):.1f}/100", self._get_rating(analysis_results.get('overall_score', 0))] + ] + + scores_table = Table(scores_data, colWidths=[2*inch, 1.5*inch, 1.5*inch]) + scores_table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.grey), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 12), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), colors.beige), + ('GRID', (0, 0), (-1, -1), 1, colors.black) + ])) + + story.append(scores_table) + story.append(Spacer(1, 20)) + + # 投资建议 + recommendation = analysis_results.get('investment_recommendation', {}) + story.append(Paragraph("投资建议", styles['Heading2'])) + story.append(Paragraph(f"建议: {recommendation.get('recommendation', 'N/A')}", styles['Normal'])) + story.append(Paragraph(f"信心度: {recommendation.get('confidence', 'N/A')}", styles['Normal'])) + story.append(Paragraph(f"综合评分: {recommendation.get('overall_score', 0):.1f}/100", styles['Normal'])) + + # 关键优势 + strengths = recommendation.get('key_strengths', []) + if strengths: + story.append(Spacer(1, 10)) + story.append(Paragraph("关键优势:", styles['Heading3'])) + for strength in strengths: + story.append(Paragraph(f"• {strength}", styles['Normal'])) + + # 关键担忧 + concerns = recommendation.get('key_concerns', []) + if concerns: + story.append(Spacer(1, 10)) + story.append(Paragraph("关键担忧:", styles['Heading3'])) + for concern in concerns: + story.append(Paragraph(f"• {concern}", styles['Normal'])) + + # 风险提示 + risk_warnings = recommendation.get('risk_warnings', []) + if risk_warnings: + story.append(Spacer(1, 10)) + story.append(Paragraph("风险提示:", styles['Heading3'])) + for warning in risk_warnings: + story.append(Paragraph(f"⚠️ {warning}", styles['Normal'])) + + story.append(Spacer(1, 20)) + + # 免责声明 + story.append(Paragraph("免责声明", styles['Heading2'])) + disclaimer = """ + 本报告仅供投资参考,不构成投资建议。投资有风险,入市需谨慎。 + 投资者应根据自身情况做出投资决策,并承担相应风险。 + 本报告基于公开信息分析,可能存在信息滞后或不准确的情况。 + """ + story.append(Paragraph(disclaimer, styles['Normal'])) + + # 构建PDF + doc.build(story) + + return report_file + except Exception as e: + logger.error(f"生成PDF报告失败: {e}") + return "" + + def _get_rating(self, score: float) -> str: + """根据分数获取评级""" + if score >= 90: + return "优秀" + elif score >= 80: + return "良好" + elif score >= 70: + return "中等" + elif score >= 60: + return "一般" + else: + return "较差" + + def generate_summary_report(self, symbol: str, analysis_results: Dict) -> str: + """生成简要报告(文本格式)""" + try: + recommendation = analysis_results.get('investment_recommendation', {}) + + summary = f""" +=== {symbol} 股票分析摘要 === + +📊 综合评分: {analysis_results.get('overall_score', 0):.1f}/100 +💡 投资建议: {recommendation.get('recommendation', 'N/A')} +🎯 信心度: {recommendation.get('confidence', 'N/A')} + +📈 各维度评分: +• 估值吸引力: {analysis_results.get('valuation_score', 0):.1f}/100 +• 财务健康度: {analysis_results.get('financial_health_score', 0):.1f}/100 +• 成长性: {analysis_results.get('growth_score', 0):.1f}/100 +• 风险控制: {analysis_results.get('risk_score', 0):.1f}/100 + +✅ 关键优势: +{chr(10).join([f"• {s}" for s in recommendation.get('key_strengths', [])])} + +⚠️ 关键担忧: +{chr(10).join([f"• {c}" for c in recommendation.get('key_concerns', [])])} + +🚨 风险提示: +{chr(10).join([f"• {w}" for w in recommendation.get('risk_warnings', [])])} + +分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + """ + + # 保存简要报告 + summary_file = os.path.join(self.output_dir, f'{symbol}_summary.txt') + with open(summary_file, 'w', encoding='utf-8') as f: + f.write(summary) + + return summary_file + except Exception as e: + logger.error(f"生成简要报告失败: {e}") + return "" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7eabb47 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +yfinance==0.2.28 +pandas==2.1.4 +numpy==1.24.3 +matplotlib==3.7.2 +seaborn==0.12.2 +plotly==5.17.0 +requests==2.31.0 +beautifulsoup4==4.12.2 +reportlab==4.0.7 +python-dotenv==1.0.0 +schedule==1.2.0 \ No newline at end of file diff --git a/run_analysis.py b/run_analysis.py new file mode 100644 index 0000000..4453218 --- /dev/null +++ b/run_analysis.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +美股分析系统启动脚本 +提供交互式界面进行股票分析 +""" +import sys +import os +from main import StockAnalysisSystem + +def print_banner(): + """打印系统横幅""" + banner = """ +╔══════════════════════════════════════════════════════════════╗ +║ 美股低价值公司分析系统 ║ +║ US Stock Value Analysis ║ +╚══════════════════════════════════════════════════════════════╝ + """ + print(banner) + +def print_menu(): + """打印主菜单""" + menu = """ +请选择操作: + +1. 分析单只股票 (完整分析) +2. 快速筛选股票 (检查是否符合低价值标准) +3. 批量分析多只股票 +4. 查看历史分析结果 +5. 系统测试 +6. 退出 + +请输入选项 (1-6): """ + return input(menu).strip() + +def analyze_single_stock(): + """分析单只股票""" + symbol = input("请输入股票代码 (如: AAPL): ").strip().upper() + if not symbol: + print("❌ 股票代码不能为空") + return + + print(f"\n🔍 正在分析 {symbol}...") + print("⏳ 这可能需要2-3分钟,请耐心等待...") + + system = StockAnalysisSystem() + result = system.analyze_stock(symbol) + + if 'error' in result: + print(f"❌ 分析失败: {result['error']}") + else: + print(f"\n✅ {symbol} 分析完成!") + print(f"📊 投资建议: {result.get('recommendation', 'N/A')}") + print(f"🎯 综合评分: {result.get('overall_score', 0):.1f}/100") + + recommendation = result.get('investment_recommendation', {}) + if recommendation.get('key_strengths'): + print("\n💪 关键优势:") + for strength in recommendation['key_strengths']: + print(f" • {strength}") + + if recommendation.get('key_concerns'): + print("\n⚠️ 关键担忧:") + for concern in recommendation['key_concerns']: + print(f" • {concern}") + + if result.get('report_file'): + print(f"\n📄 详细报告: {result['report_file']}") + if result.get('summary_file'): + print(f"📝 简要报告: {result['summary_file']}") + +def quick_screen_stock(): + """快速筛选股票""" + symbol = input("请输入股票代码 (如: AAPL): ").strip().upper() + if not symbol: + print("❌ 股票代码不能为空") + return + + print(f"\n🔍 正在快速筛选 {symbol}...") + + system = StockAnalysisSystem() + result = system.quick_screening(symbol) + + if 'error' in result: + print(f"❌ 筛选失败: {result['error']}") + else: + print(f"\n📋 {symbol} 筛选结果:") + print(f"🏢 公司名称: {result.get('company_name', 'N/A')}") + print(f"💰 市值: ${result.get('market_cap', 0):,.0f}") + + if result.get('passes_screening'): + print("✅ 通过低价值投资筛选") + else: + print("❌ 未通过低价值投资筛选") + + if result.get('criteria_met'): + print("\n✅ 符合条件:") + for criteria in result['criteria_met']: + print(f" • {criteria}") + + if result.get('criteria_failed'): + print("\n❌ 不符合条件:") + for criteria in result['criteria_failed']: + print(f" • {criteria}") + +def batch_analyze_stocks(): + """批量分析股票""" + symbols_input = input("请输入股票代码,用逗号分隔 (如: AAPL,MSFT,GOOGL): ").strip() + if not symbols_input: + print("❌ 股票代码不能为空") + return + + symbols = [s.strip().upper() for s in symbols_input.split(',')] + print(f"\n🔍 正在批量分析 {len(symbols)} 只股票...") + print("⏳ 这可能需要较长时间,请耐心等待...") + + system = StockAnalysisSystem() + results = system.batch_analysis(symbols) + + print(f"\n📊 批量分析结果:") + print("=" * 60) + + for symbol, result in results.items(): + if 'error' in result: + print(f"❌ {symbol}: {result['error']}") + else: + recommendation = result.get('recommendation', 'N/A') + score = result.get('overall_score', 0) + print(f"📈 {symbol}: {recommendation} (评分: {score:.1f})") + +def view_history(): + """查看历史分析结果""" + print("\n📚 历史分析结果功能开发中...") + print("💡 提示: 分析结果已保存在数据库中,详细报告在 reports/ 目录下") + +def run_system_test(): + """运行系统测试""" + print("\n🧪 正在运行系统测试...") + + try: + from test_system import main as test_main + test_main() + except ImportError: + print("❌ 测试模块未找到") + except Exception as e: + print(f"❌ 测试失败: {e}") + +def main(): + """主函数""" + print_banner() + + while True: + try: + choice = print_menu() + + if choice == '1': + analyze_single_stock() + elif choice == '2': + quick_screen_stock() + elif choice == '3': + batch_analyze_stocks() + elif choice == '4': + view_history() + elif choice == '5': + run_system_test() + elif choice == '6': + print("\n👋 感谢使用美股分析系统!") + break + else: + print("❌ 无效选项,请重新选择") + + input("\n按回车键继续...") + print("\n" + "="*60) + + except KeyboardInterrupt: + print("\n\n👋 程序已退出") + break + except Exception as e: + print(f"\n❌ 发生错误: {e}") + input("按回车键继续...") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/stock_analysis.db b/stock_analysis.db new file mode 100644 index 0000000..8390f5e Binary files /dev/null and b/stock_analysis.db differ diff --git a/test_system.py b/test_system.py new file mode 100644 index 0000000..25475e0 --- /dev/null +++ b/test_system.py @@ -0,0 +1,165 @@ +""" +系统测试脚本 +""" +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from main import StockAnalysisSystem +import logging + +# 设置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def test_quick_screening(): + """测试快速筛选功能""" + print("=== 测试快速筛选功能 ===") + + system = StockAnalysisSystem() + + # 测试一些知名股票 + test_symbols = ['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'NVDA'] + + for symbol in test_symbols: + print(f"\n正在测试 {symbol}...") + try: + result = system.quick_screening(symbol) + + if 'error' in result: + print(f"❌ {symbol}: {result['error']}") + else: + status = "✅ 通过" if result.get('passes_screening') else "❌ 未通过" + print(f"{status} {symbol}: {result.get('company_name', 'N/A')}") + + if result.get('criteria_met'): + print(" 符合条件:") + for criteria in result['criteria_met']: + print(f" ✓ {criteria}") + + if result.get('criteria_failed'): + print(" 不符合条件:") + for criteria in result['criteria_failed']: + print(f" ✗ {criteria}") + except Exception as e: + print(f"❌ {symbol}: 测试失败 - {e}") + +def test_data_collection(): + """测试数据收集功能""" + print("\n=== 测试数据收集功能 ===") + + system = StockAnalysisSystem() + + # 测试AAPL数据收集 + symbol = 'AAPL' + print(f"正在收集 {symbol} 数据...") + + try: + raw_data = system.data_collector.collect_all_data(symbol) + + if raw_data.get('company_info'): + company_info = raw_data['company_info'] + print(f"✅ 公司信息: {company_info.get('name', 'N/A')}") + print(f" 行业: {company_info.get('industry', 'N/A')}") + print(f" 市值: ${company_info.get('market_cap', 0):,.0f}") + else: + print("❌ 无法获取公司信息") + + if not raw_data['stock_prices'].empty: + print(f"✅ 股价数据: {len(raw_data['stock_prices'])} 条记录") + else: + print("❌ 无法获取股价数据") + + if raw_data['financial_statements']: + print(f"✅ 财务数据: {len(raw_data['financial_statements'])} 个期间") + else: + print("❌ 无法获取财务数据") + + key_metrics = raw_data.get('key_metrics', {}) + if key_metrics: + print(f"✅ 关键指标: PE={key_metrics.get('pe_ratio', 0):.2f}, PB={key_metrics.get('pb_ratio', 0):.2f}") + else: + print("❌ 无法获取关键指标") + + except Exception as e: + print(f"❌ 数据收集失败: {e}") + +def test_analysis_engine(): + """测试分析引擎""" + print("\n=== 测试分析引擎 ===") + + system = StockAnalysisSystem() + + # 先收集数据 + symbol = 'AAPL' + print(f"正在分析 {symbol}...") + + try: + raw_data = system.data_collector.collect_all_data(symbol) + + if not raw_data.get('company_info'): + print("❌ 无法获取数据进行分析") + return + + # 测试各个分析模块 + print(" 估值分析...") + valuation = system.analyzer.calculate_valuation_metrics(raw_data) + print(f" 估值评分: {valuation.get('valuation_score', 0):.1f}") + + print(" 财务健康度分析...") + health = system.analyzer.calculate_financial_health(raw_data) + print(f" 健康度评分: {health.get('health_score', 0):.1f}") + + print(" 成长性分析...") + growth = system.analyzer.calculate_growth_metrics(raw_data) + print(f" 成长性评分: {growth.get('growth_score', 0):.1f}") + + print(" 风险分析...") + risk = system.analyzer.calculate_risk_metrics(raw_data) + print(f" 风险评分: {risk.get('risk_score', 0):.1f}") + + print(" DCF估值...") + dcf = system.analyzer.perform_dcf_valuation(raw_data) + print(f" DCF价值: ${dcf.get('dcf_value', 0):,.2f}") + + print(" 生成投资建议...") + recommendation = system.analyzer.generate_investment_recommendation({ + 'valuation_score': valuation.get('valuation_score', 0), + 'financial_health_score': health.get('health_score', 0), + 'growth_score': growth.get('growth_score', 0), + 'risk_score': risk.get('risk_score', 0) + }) + + print(f" 投资建议: {recommendation.get('recommendation', 'N/A')}") + print(f" 综合评分: {recommendation.get('overall_score', 0):.1f}") + + print("✅ 分析引擎测试完成") + + except Exception as e: + print(f"❌ 分析引擎测试失败: {e}") + +def main(): + """运行所有测试""" + print("🚀 开始系统测试...") + + try: + # 测试数据收集 + test_data_collection() + + # 测试快速筛选 + test_quick_screening() + + # 测试分析引擎 + test_analysis_engine() + + print("\n✅ 所有测试完成!") + print("\n💡 提示:") + print("- 如果看到网络错误,请检查网络连接") + print("- 如果看到数据获取失败,可能是API限制或股票代码错误") + print("- 系统已准备就绪,可以使用 python main.py <股票代码> 进行分析") + + except Exception as e: + print(f"❌ 测试过程中出现错误: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/view_analysis.py b/view_analysis.py new file mode 100644 index 0000000..21b270a --- /dev/null +++ b/view_analysis.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +查看分析历史工具 +""" +import os +import sys +import json +from datetime import datetime +from analysis_storage import AnalysisStorage + +def print_banner(): + """打印横幅""" + banner = """ +╔══════════════════════════════════════════════════════════════╗ +║ 分析历史查看工具 ║ +║ Analysis History Viewer ║ +╚══════════════════════════════════════════════════════════════╝ + """ + print(banner) + +def list_all_analyses(): + """列出所有分析记录""" + storage = AnalysisStorage() + analyses = storage.list_all_analyses() + + if not analyses: + print("📭 暂无分析记录") + return + + print(f"📊 共找到 {len(analyses)} 条分析记录:") + print("=" * 80) + + for i, analysis in enumerate(analyses, 1): + print(f"{i:2d}. {analysis['symbol']} - {analysis['company_name']}") + print(f" 时间: {analysis['analysis_date']}") + print(f" 建议: {analysis['recommendation']}") + print(f" 评分: {analysis['overall_score']:.1f}/100") + print(f" 文件: {analysis['files']['summary']}") + print("-" * 80) + +def view_analysis_details(symbol: str): + """查看特定股票的分析详情""" + storage = AnalysisStorage() + analysis_data = storage.get_latest_analysis(symbol) + + if not analysis_data: + print(f"❌ 未找到 {symbol} 的分析记录") + return + + print(f"\n📈 {symbol} 最新分析结果:") + print("=" * 60) + + # 基本信息 + metadata = analysis_data.get('analysis_metadata', {}) + company_info = analysis_data.get('company_info', {}) + investment_rec = analysis_data.get('investment_recommendation', {}) + + print(f"公司名称: {company_info.get('name', 'N/A')}") + print(f"分析时间: {metadata.get('analysis_date', 'N/A')}") + print(f"投资建议: {investment_rec.get('recommendation', 'N/A')}") + print(f"综合评分: {investment_rec.get('overall_score', 0):.1f}/100") + + # 各维度评分 + valuation = analysis_data.get('valuation_analysis', {}) + financial_health = analysis_data.get('financial_health', {}) + growth = analysis_data.get('growth_analysis', {}) + risk = analysis_data.get('risk_analysis', {}) + + print(f"\n📊 各维度评分:") + print(f" 估值吸引力: {valuation.get('valuation_score', 0):.1f}/100") + print(f" 财务健康度: {financial_health.get('health_score', 0):.1f}/100") + print(f" 成长性: {growth.get('growth_score', 0):.1f}/100") + print(f" 风险控制: {risk.get('risk_score', 0):.1f}/100") + + # 关键优势 + strengths = investment_rec.get('key_strengths', []) + if strengths: + print(f"\n✅ 关键优势:") + for strength in strengths: + print(f" • {strength}") + + # 关键担忧 + concerns = investment_rec.get('key_concerns', []) + if concerns: + print(f"\n⚠️ 关键担忧:") + for concern in concerns: + print(f" • {concern}") + + # 风险提示 + warnings = investment_rec.get('risk_warnings', []) + if warnings: + print(f"\n🚨 风险提示:") + for warning in warnings: + print(f" • {warning}") + +def view_analysis_history(symbol: str): + """查看特定股票的分析历史""" + storage = AnalysisStorage() + history = storage.get_analysis_history(symbol) + + if not history: + print(f"❌ 未找到 {symbol} 的分析历史") + return + + print(f"\n📚 {symbol} 分析历史 (共 {len(history)} 条记录):") + print("=" * 80) + + for i, record in enumerate(history, 1): + print(f"{i:2d}. 时间: {record['analysis_date']}") + print(f" 建议: {record['recommendation']}") + print(f" 评分: {record['overall_score']:.1f}/100") + print(f" 文件: {record['files']['summary']}") + print("-" * 80) + +def export_analysis_summary(): + """导出分析摘要""" + storage = AnalysisStorage() + analyses = storage.list_all_analyses() + + if not analyses: + print("📭 暂无分析记录可导出") + return + + # 生成摘要报告 + summary_file = f"analysis_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md" + + with open(summary_file, 'w', encoding='utf-8') as f: + f.write("# 股票分析摘要报告\n\n") + f.write(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"分析记录数: {len(analyses)}\n\n") + + f.write("## 分析记录列表\n\n") + + for i, analysis in enumerate(analyses, 1): + f.write(f"### {i}. {analysis['symbol']} - {analysis['company_name']}\n") + f.write(f"- **分析时间**: {analysis['analysis_date']}\n") + f.write(f"- **投资建议**: {analysis['recommendation']}\n") + f.write(f"- **综合评分**: {analysis['overall_score']:.1f}/100\n") + f.write(f"- **详细报告**: {analysis['files']['summary']}\n\n") + + print(f"✅ 分析摘要已导出到: {summary_file}") + +def main(): + """主函数""" + if len(sys.argv) < 2: + print("使用方法:") + print("python3 view_analysis.py list # 列出所有分析记录") + print("python3 view_analysis.py view <股票代码> # 查看最新分析结果") + print("python3 view_analysis.py history <股票代码> # 查看分析历史") + print("python3 view_analysis.py export # 导出分析摘要") + return + + command = sys.argv[1].lower() + + if command == 'list': + print_banner() + list_all_analyses() + + elif command == 'view': + if len(sys.argv) < 3: + print("❌ 请提供股票代码") + return + symbol = sys.argv[2].upper() + print_banner() + view_analysis_details(symbol) + + elif command == 'history': + if len(sys.argv) < 3: + print("❌ 请提供股票代码") + return + symbol = sys.argv[2].upper() + print_banner() + view_analysis_history(symbol) + + elif command == 'export': + print_banner() + export_analysis_summary() + + else: + print("❌ 无效命令") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/使用指南.md b/使用指南.md new file mode 100644 index 0000000..de882e4 --- /dev/null +++ b/使用指南.md @@ -0,0 +1,157 @@ +# 美股低价值公司分析系统 - 使用指南 + +## 🎯 系统概述 + +这是一个专门用于分析美股低价值投资机会的智能分析系统。当您提供公司名称或股票代码时,系统将自动执行以下标准流程: + +### 📋 每次分析的标准流程 + +#### 1. 数据获取阶段 (30秒内) +- ✅ 验证股票代码和基本信息 +- ✅ 获取最新股价和交易数据 +- ✅ 下载最近4个季度的财务数据 +- ✅ 收集关键财务指标 + +#### 2. 快速筛选阶段 (1分钟内) +- ✅ 计算关键估值指标(PE、PB、PS等) +- ✅ 检查是否符合"低价值"标准 +- ✅ 识别异常财务指标 +- ✅ 评估基本投资可行性 + +#### 3. 深度分析阶段 (2-3分钟) +- ✅ 进行DCF估值计算 +- ✅ 分析财务健康度 +- ✅ 评估行业地位和竞争优势 +- ✅ 计算风险指标 +- ✅ 生成多维度评分 + +#### 4. 报告生成阶段 (1分钟内) +- ✅ 生成综合分析报告 +- ✅ 提供投资建议和风险提示 +- ✅ 保存分析结果到数据库 +- ✅ 创建可视化图表 + +## 🚀 快速开始 + +### 方法一:交互式界面(推荐) +```bash +python run_analysis.py +``` + +### 方法二:命令行直接使用 +```bash +# 分析单只股票 +python main.py AAPL + +# 快速筛选 +python main.py --screen AAPL + +# 批量分析 +python main.py --batch AAPL,MSFT,GOOGL +``` + +## 📊 分析维度说明 + +### 1. 估值吸引力 (40%权重) +- **PE比率**: 市盈率 ≤ 15 +- **PB比率**: 市净率 ≤ 1.5 +- **PS比率**: 市销率 ≤ 2.0 +- **PEG比率**: 市盈率相对盈利增长比率 ≤ 1.0 +- **DCF估值**: 内在价值计算 + +### 2. 财务健康度 (30%权重) +- **流动性**: 流动比率 ≥ 1.2 +- **偿债能力**: 债务比率 ≤ 60% +- **盈利能力**: ROE ≥ 5% +- **现金流**: 经营现金流分析 + +### 3. 成长性 (20%权重) +- **收入增长率**: 年收入增长趋势 +- **利润增长率**: 净利润增长趋势 +- **历史增长**: 3年复合增长率 + +### 4. 风险控制 (10%权重) +- **Beta系数**: 相对市场波动性 +- **波动率**: 价格波动标准差 +- **最大回撤**: 历史最大跌幅 + +## 📈 输出结果说明 + +### 控制台输出 +``` +=== AAPL 分析结果 === +投资建议: 买入 +综合评分: 75.2/100 + +关键优势: + • 估值吸引力高 + • 财务健康度良好 + • 成长性优秀 + +关键担忧: + • 风险较高 + +详细报告已生成: reports/AAPL_analysis_report.pdf +简要报告已生成: reports/AAPL_summary.txt +``` + +### 文件输出 +- **PDF详细报告**: 包含完整分析、图表、投资建议 +- **文本摘要**: 关键信息快速查看 +- **HTML图表**: 交互式可视化图表 +- **数据库记录**: 历史数据和分析结果 + +## 🎯 投资建议等级 + +| 评分范围 | 投资建议 | 说明 | +|---------|---------|------| +| 90-100 | 强烈买入 | 优秀投资机会,风险可控 | +| 80-89 | 买入 | 良好投资机会,值得关注 | +| 70-79 | 持有 | 中等投资机会,谨慎考虑 | +| 60-69 | 观望 | 投资价值一般,等待时机 | +| 0-59 | 卖出 | 投资价值较低,建议回避 | + +## ⚠️ 重要提示 + +### 使用前准备 +1. **网络连接**: 需要稳定的网络连接获取实时数据 +2. **股票代码**: 使用标准美股代码(如:AAPL, MSFT, GOOGL) +3. **数据延迟**: Yahoo Finance数据可能存在15-20分钟延迟 + +### 投资风险提示 +- ⚠️ 本系统仅供投资参考,不构成投资建议 +- ⚠️ 投资有风险,入市需谨慎 +- ⚠️ 建议结合其他数据源进行交叉验证 +- ⚠️ 历史表现不代表未来收益 + +### 系统限制 +- 📊 数据来源:主要依赖Yahoo Finance +- ⏰ 分析时间:单只股票需要2-3分钟 +- 💾 存储空间:数据库和报告文件会占用磁盘空间 +- 🔄 数据更新:建议24小时内不重复分析同一股票 + +## 🛠️ 故障排除 + +### 常见问题 +1. **网络错误**: 检查网络连接,稍后重试 +2. **股票代码错误**: 确认代码正确(如AAPL不是apple) +3. **数据获取失败**: 可能是API限制,等待后重试 +4. **分析失败**: 查看日志文件 `stock_analysis.log` + +### 系统要求 +- Python 3.8+ +- 内存:至少2GB可用内存 +- 磁盘:至少500MB可用空间 +- 网络:稳定的互联网连接 + +## 📞 技术支持 + +如遇到问题,请检查: +1. 查看日志文件了解详细错误信息 +2. 确认网络连接正常 +3. 验证股票代码格式正确 +4. 检查系统依赖是否完整安装 + +--- + +**祝您投资顺利!** 🚀📈 \ No newline at end of file diff --git a/分析报告使用说明.md b/分析报告使用说明.md new file mode 100644 index 0000000..8b74c58 --- /dev/null +++ b/分析报告使用说明.md @@ -0,0 +1,166 @@ +# 分析报告存储系统使用说明 + +## 📁 文件结构 + +每次分析完成后,系统会自动创建以下文件结构: + +``` +analysis_reports/ +├── detailed/ # 详细分析报告 (JSON格式) +│ └── {股票代码}_detailed_{时间戳}.json +├── summaries/ # 简要分析报告 (Markdown格式) +│ └── {股票代码}_summary_{时间戳}.md +├── charts/ # 图表文件 (HTML格式) +│ └── {股票代码}_*.html +├── raw_data/ # 原始数据 (JSON格式) +│ └── {股票代码}_raw_data_{时间戳}.json +└── analysis_index.json # 分析记录索引 +``` + +## 🔍 查看分析记录 + +### 1. 列出所有分析记录 +```bash +python3 view_analysis.py list +``` + +### 2. 查看特定股票的最新分析结果 +```bash +python3 view_analysis.py view <股票代码> +# 例如: python3 view_analysis.py view RVPH +``` + +### 3. 查看特定股票的分析历史 +```bash +python3 view_analysis.py history <股票代码> +# 例如: python3 view_analysis.py history RVPH +``` + +### 4. 导出所有分析摘要 +```bash +python3 view_analysis.py export +``` + +## 📊 分析报告格式 + +### 简要报告 (Markdown格式) +- **文件名**: `{股票代码}_summary_{时间戳}.md` +- **内容**: 包含基本信息、投资建议、各维度评分、关键指标等 +- **用途**: 快速查看分析结果,便于阅读和分享 + +### 详细报告 (JSON格式) +- **文件名**: `{股票代码}_detailed_{时间戳}.json` +- **内容**: 完整的分析数据,包括所有计算过程和原始数据 +- **用途**: 程序化处理、深度分析、数据挖掘 + +### 原始数据 (JSON格式) +- **文件名**: `{股票代码}_raw_data_{时间戳}.json` +- **内容**: 从API获取的原始数据 +- **用途**: 数据备份、重新分析、审计 + +## 🎯 分析维度说明 + +### 1. 估值吸引力 (40%权重) +- **PE比率**: 市盈率 +- **PB比率**: 市净率 +- **PS比率**: 市销率 +- **PEG比率**: 市盈率相对盈利增长比率 +- **DCF估值**: 内在价值计算 + +### 2. 财务健康度 (30%权重) +- **流动比率**: 流动资产/流动负债 +- **速动比率**: (流动资产-存货)/流动负债 +- **债务比率**: 总负债/总资产 +- **ROE**: 净资产收益率 +- **ROA**: 总资产收益率 + +### 3. 成长性 (20%权重) +- **收入增长率**: 年收入增长趋势 +- **利润增长率**: 净利润增长趋势 +- **历史增长率**: 3年复合增长率 + +### 4. 风险控制 (10%权重) +- **Beta系数**: 相对市场波动性 +- **波动率**: 价格波动标准差 +- **最大回撤**: 历史最大跌幅 + +## 📈 投资建议等级 + +| 评分范围 | 投资建议 | 说明 | +|---------|---------|------| +| 90-100 | 强烈买入 | 优秀投资机会,风险可控 | +| 80-89 | 买入 | 良好投资机会,值得关注 | +| 70-79 | 持有 | 中等投资机会,谨慎考虑 | +| 60-69 | 观望 | 投资价值一般,等待时机 | +| 0-59 | 卖出 | 投资价值较低,建议回避 | + +## 🔧 系统功能 + +### 自动存储 +- 每次分析完成后自动保存到标准化格式 +- 按时间戳组织文件,便于管理 +- 自动更新索引文件 + +### 历史追踪 +- 记录所有分析历史 +- 支持按股票代码查看历史 +- 支持导出分析摘要 + +### 多格式输出 +- Markdown格式便于阅读 +- JSON格式便于程序处理 +- 支持图表可视化 + +## 📝 使用示例 + +### 分析新股票 +```bash +# 分析单只股票 +python3 main.py AAPL + +# 快速筛选 +python3 main.py --screen AAPL + +# 批量分析 +python3 main.py --batch AAPL,MSFT,GOOGL +``` + +### 查看分析结果 +```bash +# 查看所有分析记录 +python3 view_analysis.py list + +# 查看AAPL的最新分析 +python3 view_analysis.py view AAPL + +# 查看AAPL的分析历史 +python3 view_analysis.py history AAPL +``` + +### 导出报告 +```bash +# 导出所有分析摘要 +python3 view_analysis.py export +``` + +## ⚠️ 注意事项 + +1. **文件管理**: 分析报告会占用磁盘空间,建议定期清理旧文件 +2. **数据备份**: 重要分析结果建议备份到其他位置 +3. **版本控制**: 每次分析都会生成新文件,不会覆盖历史记录 +4. **权限管理**: 确保有足够的文件读写权限 + +## 🚀 高级功能 + +### 自定义分析报告 +可以修改 `analysis_storage.py` 中的 `_generate_markdown_summary` 方法来自定义报告格式。 + +### 批量导出 +可以编写脚本批量导出特定时间段或特定股票的分析报告。 + +### 数据分析 +可以使用详细报告中的JSON数据进行进一步的数据分析和可视化。 + +--- + +**系统已准备就绪!** 每次分析完成后,您都可以通过上述命令查看和管理分析报告。 \ No newline at end of file