├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── data └── stock_analysis.db ├── main.py ├── requirements.txt └── src ├── __init__.py ├── data ├── __init__.py ├── database.py ├── financial_data.py ├── news_data.py └── stock_data.py ├── llm ├── __init__.py ├── model_api.py ├── stock_selector.py └── strategy_generator.py └── portfolio ├── __init__.py ├── cli_manager.py ├── portfolio_manager.py └── trade_executor.py /.env.example: -------------------------------------------------------------------------------- 1 | # Tushare API Token 2 | TUSHARE_TOKEN= 3 | 4 | # 阿里云API配置 5 | DASHSCOPE_API_KEY= 6 | 7 | #mairui股票api 8 | MAIRUI_LICENSE= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # 环境文件 25 | 26 | .venv 27 | venv/ 28 | ENV/ 29 | 30 | # IDE 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | 36 | # 操作系统 37 | .DS_Store 38 | Thumbs.db .env 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Prog.le 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI 投资组合管理系统 2 | 3 | 这是一个基于人工智能的投资组合管理系统,利用机器学习和自然语言处理技术来优化投资决策。 4 | 5 | ## 功能特点 6 | 7 | - 股票数据采集和分析 8 | - 金融新闻数据整合 9 | - AI驱动的股票选择策略 10 | - 自动化投资组合管理 11 | - 风险评估和控制 12 | 13 | ## 项目结构 14 | 15 | ## 核心功能 16 | 17 | ### 1. 智能分析 18 | - 个股深度分析 19 | - 基本面分析 20 | - 财务数据分析 21 | - 新闻情感分析 22 | - 行业前景分析 23 | - 市场环境分析 24 | - 整体市场评估 25 | - 板块机会分析 26 | - 风险提示 27 | - 交易建议生成 28 | - 买卖方向判断 29 | - 目标价格建议 30 | - 止损止盈设置 31 | - 持仓时间建议 32 | - 风险等级评估 33 | 34 | ### 2. 数据集成 35 | - 实时行情数据 (迈瑞API) 36 | - 财务报表数据 (Tushare) 37 | - 新闻资讯 (新浪财经) 38 | - 基本面数据 39 | - 历史交易数据 40 | 41 | ### 3. 智能决策 42 | - 投资组合管理 43 | - 交易策略生成 44 | - 风险控制 45 | - 自动化交易执行 46 | 47 | ## 技术架构 48 | 49 | ### 核心模块 50 | - `src/data/`: 数据获取和处理 51 | - `stock_data.py`: 股票数据获取 52 | - `news_data.py`: 新闻数据爬取 53 | - `financial_data.py`: 财务数据处理 54 | - `database.py`: 数据持久化 55 | - `src/llm/`: 大模型集成 56 | - `model_api.py`: LLM服务接口 57 | - `stock_selector.py`: 智能选股 58 | - `strategy_generator.py`: 策略生成 59 | - `src/portfolio/`: 投资组合 60 | - `portfolio_manager.py`: 组合管理 61 | - `trade_executor.py`: 交易执行 62 | 63 | ### 依赖组件 64 | - Python 3.10 65 | - DashScope (通义千问/DeepSeek) 66 | - SQLite3 67 | - Tushare 68 | - BeautifulSoup4 69 | - Requests 70 | 71 | ## 快速开始 72 | 73 | ### 1. 环境准备 74 | - 克隆项目 75 | - git clone https://github.com/prog-le/stock-llm.git 76 | - cd stock-llm 77 | - 安装依赖 78 | - pip install -r requirements.txt 79 | 80 | ### 2. 配置设置 81 | - 修改 `.env` 文件: 82 | - `DASHSCOPE_API_KEY=your_api_key_here` 83 | - `TUSHARE_TOKEN=your_token_here` 84 | - `MAIRUI_LICENSE=your_license_here` 85 | 86 | ### 3. 运行程序 87 | 建议使用conda 88 | - conda create --name stock-llm python=3.10 89 | - conda activate stock-llm 90 | - python main.py 91 | 92 | ## 注意事项 93 | 94 | 1. API 使用限制 95 | - DashScope API 调用频率限制 96 | - 新浪财经爬虫访问限制 97 | - 迈瑞数据 API 授权限制 98 | 99 | 2. 数据安全 100 | - API 密钥安全存储 101 | - 本地数据库备份 102 | - 敏感信息加密 103 | 104 | 3. 免责声明 105 | - 投资建议仅供参考 106 | - 市场风险提示 107 | - 实际交易需谨慎 108 | 109 | ## 开发计划 110 | 111 | - [ ] 支持更多数据源接入 112 | - [ ] 优化策略生成算法 113 | - [ ] 添加回测功能 114 | - [ ] 实现实盘交易接口 115 | - [ ] 优化风险控制模型 116 | - [ ] 增加 Web 界面 117 | 118 | ## 贡献指南 119 | 120 | 1. Fork 项目 121 | 2. 创建特性分支 122 | 3. 提交代码 123 | 4. 发起 Pull Request 124 | 125 | ## 许可证 126 | 127 | MIT License 128 | 129 | ## 联系方式 130 | 131 | - 作者:[Prog.le] 132 | - 邮箱:[Prog.le@outlook.com] 133 | - 项目地址:[https://github.com/prog-le/stock-llm] 134 | 135 | ### 支付宝捐赠 136 | 如果你觉得这个项目对你有帮助,可以通过支付宝进行捐赠: 137 | 138 | -------------------------------------------------------------------------------- /data/stock_analysis.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prog-le/stock-llm/96a51201002124adb95763338b498957bfc9fe2a/data/stock_analysis.db -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from typing import Dict, List, Any 4 | from src.data import MaiRuiStockAPI, FinancialDataFetcher, NewsDataFetcher 5 | from src.llm import LLMService 6 | from src.portfolio import PortfolioManager, TradeExecutor 7 | from src.data.database import DatabaseManager 8 | 9 | def get_user_portfolio() -> Dict[str, float]: 10 | """获取用户持仓信息""" 11 | portfolio = {} 12 | print("\n请输入您的持仓信息(输入空股票代码结束):") 13 | while True: 14 | stock_code = input("\n请输入股票代码(直接回车结束): ").strip() 15 | if not stock_code: 16 | break 17 | try: 18 | shares = float(input("请输入持仓数量: ")) 19 | cost = float(input("请输入成本价: ")) 20 | portfolio[stock_code] = {'shares': shares, 'cost': cost} 21 | except ValueError: 22 | print("输入格式错误,请重新输入") 23 | continue 24 | return portfolio 25 | 26 | def get_user_balance() -> float: 27 | """获取用户可用资金""" 28 | while True: 29 | try: 30 | balance = float(input("\n请输入可用资金(元): ")) 31 | return balance 32 | except ValueError: 33 | print("输入格式错误,请重新输入") 34 | 35 | def format_trading_advice(advice: Dict[str, Any]) -> str: 36 | """格式化交易建议输出""" 37 | if not advice: 38 | return "无法解析具体交易建议" 39 | 40 | return f""" 41 | 具体交易建议: 42 | ------------------------ 43 | 交易方向: {advice.get('direction', '未指定')} 44 | 目标价格: {advice.get('target_price', '未指定')} 元 45 | 交易数量: {advice.get('quantity', '未指定')} 股 46 | 止损价格: {advice.get('stop_loss', '未指定')} 元 47 | 止盈目标: {advice.get('take_profit', '未指定')} 元 48 | 建议持仓: {advice.get('holding_period', '未指定')} 个交易日 49 | 风险等级: {advice.get('risk_level', '未指定')} 50 | ------------------------""" 51 | 52 | def analyze_portfolio(portfolio: Dict[str, float], 53 | balance: float, 54 | stock_api: MaiRuiStockAPI, 55 | news_api: NewsDataFetcher, 56 | financial_api: FinancialDataFetcher, 57 | llm_service: LLMService, 58 | db: DatabaseManager) -> None: 59 | """分析投资组合""" 60 | 61 | # 1. 分析持仓股票 62 | if portfolio: 63 | print("\n=== 分析持仓股票 ===") 64 | for stock_code, position in portfolio.items(): 65 | print(f"\n分析 {stock_code} ...") 66 | 67 | # 获取股票信息 68 | stock_info = stock_api.get_stock_info(stock_code) 69 | if not stock_info: 70 | print(f"无法获取股票 {stock_code} 的信息") 71 | continue 72 | 73 | # 保存股票信息 74 | db.save_stock_info(stock_info) 75 | 76 | # 获取相关新闻 77 | news_list = news_api.get_stock_news(stock_code, days=7) 78 | print(f"获取到 {len(news_list)} 条相关新闻") 79 | 80 | # 保存新闻 81 | db.save_news(news_list, stock_code) 82 | 83 | # 获取财务数据 84 | financial_data = financial_api.get_financial_data(stock_code) 85 | 86 | # 添加持仓信息到分析 87 | stock_info['position'] = position 88 | 89 | # 调用模型分析 90 | result = llm_service.analyze_stock(stock_info, news_list, financial_data) 91 | 92 | # 保存分析结果 93 | if result['status'] == 'success': 94 | result['stock_name'] = stock_info.get('name') 95 | db.save_stock_analysis(stock_code, result) 96 | 97 | print("\n分析结果:") 98 | print("-" * 50) 99 | if result['status'] == 'success': 100 | print(result['analysis']) 101 | print("\n" + format_trading_advice(result.get('trading_advice'))) 102 | else: 103 | print(f"分析失败: {result.get('error')}") 104 | print("-" * 50) 105 | 106 | input("\n按Enter继续...") 107 | 108 | # 2. 分析市场机会 109 | print("\n=== 分析市场机会 ===") 110 | # 获取财经新闻 111 | market_news = news_api.get_daily_news(min_count=40) 112 | print(f"\n获取到 {len(market_news)} 条市场新闻") 113 | 114 | # 保存市场新闻 115 | db.save_news(market_news) 116 | 117 | # 调用模型分析市场机会 118 | market_analysis = llm_service.analyze_market(market_news, balance) 119 | 120 | # 保存市场分析结果 121 | if market_analysis['status'] == 'success': 122 | db.save_market_analysis(market_analysis, balance) 123 | 124 | print("\n市场分析结果:") 125 | print("-" * 50) 126 | if market_analysis['status'] == 'success': 127 | print(market_analysis['analysis']) 128 | else: 129 | print(f"分析失败: {market_analysis.get('error')}") 130 | print("-" * 50) 131 | 132 | def main(): 133 | # 1. 加载环境变量 134 | load_dotenv() 135 | api_key = os.getenv('DEEPSEEK_API_KEY') 136 | 137 | if not api_key: 138 | # 尝试从DASHSCOPE_API_KEY获取 139 | api_key = os.getenv('DASHSCOPE_API_KEY') 140 | if not api_key: 141 | raise ValueError("请在.env文件中设置DEEPSEEK_API_KEY或DASHSCOPE_API_KEY") 142 | 143 | # 2. 初始化组件 144 | print("初始化系统组件...") 145 | stock_api = MaiRuiStockAPI() 146 | financial_api = FinancialDataFetcher() 147 | news_api = NewsDataFetcher() 148 | llm_service = LLMService(api_key) 149 | 150 | # 初始化数据库 151 | db = DatabaseManager() 152 | 153 | # 3. 获取用户输入 154 | portfolio = get_user_portfolio() 155 | balance = get_user_balance() 156 | 157 | # 4. 分析投资组合和市场机会 158 | analyze_portfolio( 159 | portfolio=portfolio, 160 | balance=balance, 161 | stock_api=stock_api, 162 | news_api=news_api, 163 | financial_api=financial_api, 164 | llm_service=llm_service, 165 | db=db 166 | ) 167 | 168 | if __name__ == "__main__": 169 | try: 170 | main() 171 | except KeyboardInterrupt: 172 | print("\n程序被用户中断") 173 | except Exception as e: 174 | print(f"\n程序运行出错: {str(e)}") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # python = 3.10 2 | pandas>=1.5.3 3 | numpy>=1.24.3 4 | requests>=2.31.0 5 | python-dotenv>=1.0.0 6 | aliyun-python-sdk-core 7 | # aliyun-python-sdk-dashscope 8 | tushare>=1.2.89 9 | fastapi>=0.100.0 10 | uvicorn>=0.22.0 11 | pydantic>=2.0.3 12 | python-jose>=3.3.0 13 | passlib>=1.7.4 14 | python-multipart>=0.0.6 15 | beanie>=1.20.0 16 | motor>=3.2. 17 | openai 18 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prog-le/stock-llm/96a51201002124adb95763338b498957bfc9fe2a/src/__init__.py -------------------------------------------------------------------------------- /src/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .stock_data import MaiRuiStockAPI 2 | from .financial_data import FinancialDataFetcher 3 | from .news_data import NewsDataFetcher 4 | 5 | __all__ = ['MaiRuiStockAPI', 'FinancialDataFetcher', 'NewsDataFetcher'] -------------------------------------------------------------------------------- /src/data/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from typing import Dict, List, Any 3 | from datetime import datetime 4 | import json 5 | import os 6 | 7 | class DatabaseManager: 8 | """数据库管理器""" 9 | 10 | def __init__(self, db_path: str = "data/stock_analysis.db"): 11 | """初始化数据库连接 12 | 13 | Args: 14 | db_path: 数据库文件路径 15 | """ 16 | # 确保数据目录存在 17 | os.makedirs(os.path.dirname(db_path), exist_ok=True) 18 | 19 | # 如果数据库文件已存在,先删除它 20 | if os.path.exists(db_path): 21 | os.remove(db_path) 22 | print("重新创建数据库...") 23 | 24 | self.db_path = db_path 25 | self._init_db() 26 | self._migrate_db() 27 | 28 | def _init_db(self): 29 | """初始化数据库表""" 30 | with sqlite3.connect(self.db_path) as conn: 31 | cursor = conn.cursor() 32 | 33 | # 创建股票分析结果表 34 | cursor.execute(""" 35 | CREATE TABLE IF NOT EXISTS stock_analysis ( 36 | id INTEGER PRIMARY KEY AUTOINCREMENT, 37 | stock_code TEXT NOT NULL, 38 | stock_name TEXT, 39 | analysis_data TEXT, 40 | trading_advice TEXT, 41 | timestamp DATETIME, 42 | status TEXT, 43 | UNIQUE(stock_code, timestamp) 44 | ) 45 | """) 46 | 47 | # 创建市场分析结果表 48 | cursor.execute(""" 49 | CREATE TABLE IF NOT EXISTS market_analysis ( 50 | id INTEGER PRIMARY KEY AUTOINCREMENT, 51 | analysis_data TEXT, 52 | available_cash REAL, 53 | timestamp DATETIME 54 | ) 55 | """) 56 | 57 | # 创建新闻数据表 58 | cursor.execute(""" 59 | CREATE TABLE IF NOT EXISTS news_data ( 60 | id INTEGER PRIMARY KEY AUTOINCREMENT, 61 | stock_code TEXT, 62 | title TEXT, 63 | content TEXT, 64 | source TEXT, 65 | news_time DATETIME, 66 | fetch_time DATETIME, 67 | url TEXT 68 | ) 69 | """) 70 | 71 | # 创建股票基本信息表 72 | cursor.execute(""" 73 | CREATE TABLE IF NOT EXISTS stock_info ( 74 | stock_code TEXT PRIMARY KEY, 75 | stock_name TEXT, 76 | industry TEXT, 77 | main_business TEXT, 78 | update_time DATETIME 79 | ) 80 | """) 81 | 82 | conn.commit() 83 | 84 | def _migrate_db(self): 85 | """数据库迁移""" 86 | with sqlite3.connect(self.db_path) as conn: 87 | cursor = conn.cursor() 88 | 89 | # 检查是否需要添加 trading_advice 列 90 | cursor.execute("PRAGMA table_info(stock_analysis)") 91 | columns = [col[1] for col in cursor.fetchall()] 92 | 93 | if 'trading_advice' not in columns: 94 | print("正在更新数据库结构...") 95 | # 创建临时表 96 | cursor.execute(""" 97 | CREATE TABLE stock_analysis_new ( 98 | id INTEGER PRIMARY KEY AUTOINCREMENT, 99 | stock_code TEXT NOT NULL, 100 | stock_name TEXT, 101 | analysis_data TEXT, 102 | trading_advice TEXT, 103 | timestamp DATETIME, 104 | status TEXT, 105 | UNIQUE(stock_code, timestamp) 106 | ) 107 | """) 108 | 109 | # 复制旧数据 110 | cursor.execute(""" 111 | INSERT INTO stock_analysis_new 112 | (stock_code, stock_name, analysis_data, timestamp, status) 113 | SELECT stock_code, stock_name, analysis_data, timestamp, status 114 | FROM stock_analysis 115 | """) 116 | 117 | # 删除旧表 118 | cursor.execute("DROP TABLE stock_analysis") 119 | 120 | # 重命名新表 121 | cursor.execute("ALTER TABLE stock_analysis_new RENAME TO stock_analysis") 122 | 123 | conn.commit() 124 | print("数据库结构更新完成") 125 | 126 | def save_stock_analysis(self, stock_code: str, analysis_result: Dict[str, Any]): 127 | """保存股票分析结果""" 128 | with sqlite3.connect(self.db_path) as conn: 129 | cursor = conn.cursor() 130 | 131 | # 保存分析结果 132 | cursor.execute(""" 133 | INSERT INTO stock_analysis 134 | (stock_code, stock_name, analysis_data, trading_advice, timestamp, status) 135 | VALUES (?, ?, ?, ?, ?, ?) 136 | """, ( 137 | stock_code, 138 | analysis_result.get('stock_name', ''), 139 | json.dumps(analysis_result.get('analysis', ''), ensure_ascii=False), 140 | json.dumps(analysis_result.get('trading_advice', {}), ensure_ascii=False), 141 | analysis_result.get('timestamp'), 142 | analysis_result.get('status') 143 | )) 144 | conn.commit() 145 | 146 | def save_market_analysis(self, analysis_result: Dict[str, Any], available_cash: float): 147 | """保存市场分析结果""" 148 | with sqlite3.connect(self.db_path) as conn: 149 | cursor = conn.cursor() 150 | cursor.execute(""" 151 | INSERT INTO market_analysis 152 | (analysis_data, available_cash, timestamp) 153 | VALUES (?, ?, ?) 154 | """, ( 155 | json.dumps(analysis_result, ensure_ascii=False), 156 | available_cash, 157 | analysis_result.get('timestamp') 158 | )) 159 | conn.commit() 160 | 161 | def save_news(self, news_list: List[Dict[str, Any]], stock_code: str = None): 162 | """保存新闻数据""" 163 | with sqlite3.connect(self.db_path) as conn: 164 | cursor = conn.cursor() 165 | for news in news_list: 166 | cursor.execute(""" 167 | INSERT OR REPLACE INTO news_data 168 | (stock_code, title, content, source, news_time, fetch_time, url) 169 | VALUES (?, ?, ?, ?, ?, ?, ?) 170 | """, ( 171 | stock_code, 172 | news.get('title'), 173 | news.get('content'), 174 | news.get('source'), 175 | news.get('time'), 176 | datetime.now().isoformat(), 177 | news.get('url') 178 | )) 179 | conn.commit() 180 | 181 | def save_stock_info(self, stock_info: Dict[str, Any]): 182 | """保存股票基本信息""" 183 | with sqlite3.connect(self.db_path) as conn: 184 | cursor = conn.cursor() 185 | cursor.execute(""" 186 | INSERT OR REPLACE INTO stock_info 187 | (stock_code, stock_name, industry, main_business, update_time) 188 | VALUES (?, ?, ?, ?, ?) 189 | """, ( 190 | stock_info.get('code'), 191 | stock_info.get('name'), 192 | stock_info.get('industry'), 193 | stock_info.get('main_business'), 194 | datetime.now().isoformat() 195 | )) 196 | conn.commit() 197 | 198 | def get_latest_analysis(self, stock_code: str) -> Dict[str, Any]: 199 | """获取最新的股票分析结果""" 200 | with sqlite3.connect(self.db_path) as conn: 201 | cursor = conn.cursor() 202 | cursor.execute(""" 203 | SELECT analysis_data FROM stock_analysis 204 | WHERE stock_code = ? 205 | ORDER BY timestamp DESC 206 | LIMIT 1 207 | """, (stock_code,)) 208 | 209 | result = cursor.fetchone() 210 | if result: 211 | return json.loads(result[0]) 212 | return None 213 | 214 | def get_latest_market_analysis(self) -> Dict[str, Any]: 215 | """获取最新的市场分析结果""" 216 | with sqlite3.connect(self.db_path) as conn: 217 | cursor = conn.cursor() 218 | cursor.execute(""" 219 | SELECT analysis_data FROM market_analysis 220 | ORDER BY timestamp DESC 221 | LIMIT 1 222 | """) 223 | 224 | result = cursor.fetchone() 225 | if result: 226 | return json.loads(result[0]) 227 | return None -------------------------------------------------------------------------------- /src/data/financial_data.py: -------------------------------------------------------------------------------- 1 | import tushare as ts 2 | from typing import List, Dict, Any 3 | from datetime import datetime 4 | 5 | class FinancialDataFetcher: 6 | """财务数据获取器""" 7 | 8 | def __init__(self, token: str = None): # 修改为可选参数 9 | """初始化财务数据获取器 10 | 11 | Args: 12 | token: API token,可选 13 | """ 14 | self.token = token 15 | 16 | def get_financial_data(self, stock_code: str) -> dict: 17 | """获取股票财务数据 18 | 19 | Args: 20 | stock_code: 股票代码 21 | 22 | Returns: 23 | dict: 财务数据 24 | """ 25 | try: 26 | # 调用 tushare API 获取财务数据 27 | df = self.api.income( 28 | ts_code=stock_code, 29 | start_date=(datetime.now().year - 1).__str__() + '0101', 30 | end_date=datetime.now().strftime('%Y%m%d') 31 | ) 32 | 33 | if df.empty: 34 | return {} 35 | 36 | # 获取最新一期财务数据 37 | latest = df.iloc[0] 38 | return { 39 | 'revenue': latest.get('revenue', 0), 40 | 'net_profit': latest.get('n_income', 0), 41 | 'gross_margin': latest.get('grossprofit_margin', 0), 42 | 'roe': latest.get('roe', 0), 43 | 'debt_ratio': latest.get('debt_to_assets', 0), 44 | 'current_ratio': latest.get('current_ratio', 0), 45 | 'inventory_turnover': latest.get('inv_turn', 0), 46 | 'receivables_turnover': latest.get('ar_turn', 0) 47 | } 48 | except Exception as e: 49 | print(f"获取财务数据时出错: {str(e)}") 50 | return {} 51 | 52 | def get_income_statement(self, ts_code: str) -> List[Dict[str, Any]]: 53 | """获取利润表数据""" 54 | df = self.api.income( 55 | ts_code=ts_code, 56 | start_date=(datetime.now().year - 1).__str__() + '0101', 57 | end_date=datetime.now().strftime('%Y%m%d') 58 | ) 59 | return df.to_dict('records') 60 | 61 | def get_balance_sheet(self, ts_code: str) -> List[Dict[str, Any]]: 62 | """获取资产负债表数据""" 63 | df = self.api.balancesheet( 64 | ts_code=ts_code, 65 | start_date=(datetime.now().year - 1).__str__() + '0101', 66 | end_date=datetime.now().strftime('%Y%m%d') 67 | ) 68 | return df.to_dict('records') 69 | 70 | def get_cashflow(self, ts_code: str) -> List[Dict[str, Any]]: 71 | """获取现金流量表数据""" 72 | df = self.api.cashflow( 73 | ts_code=ts_code, 74 | start_date=(datetime.now().year - 1).__str__() + '0101', 75 | end_date=datetime.now().strftime('%Y%m%d') 76 | ) 77 | return df.to_dict('records') 78 | 79 | def get_forecast(self, ts_code: str) -> Dict[str, Any]: 80 | """获取业绩预告数据""" 81 | df = self.api.forecast( 82 | ts_code=ts_code, 83 | start_date=(datetime.now().year).__str__() + '0101', 84 | end_date=datetime.now().strftime('%Y%m%d') 85 | ) 86 | return df.to_dict('records')[0] if not df.empty else {} 87 | 88 | def get_express(self, ts_code: str) -> Dict[str, Any]: 89 | """获取业绩快报数据""" 90 | df = self.api.express( 91 | ts_code=ts_code, 92 | start_date=(datetime.now().year).__str__() + '0101', 93 | end_date=datetime.now().strftime('%Y%m%d') 94 | ) 95 | return df.to_dict('records')[0] if not df.empty else {} -------------------------------------------------------------------------------- /src/data/news_data.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | from typing import List, Dict 4 | import re 5 | from datetime import datetime, timedelta 6 | import time 7 | import os 8 | from dotenv import load_dotenv 9 | 10 | class NewsDataFetcher: 11 | """新闻数据获取器""" 12 | 13 | BASE_URL = "http://api.mairui.club" 14 | BACKUP_URL = "http://api1.mairui.club" 15 | LICENSE = os.getenv('MAIRUI_LICENSE') 16 | 17 | def __init__(self): 18 | self.session = requests.Session() 19 | self.headers = { 20 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 21 | } 22 | 23 | def _request(self, endpoint: str) -> List[Dict]: 24 | """发送API请求 25 | 26 | Args: 27 | endpoint: API端点 28 | 29 | Returns: 30 | List[Dict]: JSON响应数据 31 | """ 32 | url = f"{self.BASE_URL}/{endpoint}/{self.LICENSE}" 33 | 34 | try: 35 | response = self.session.get(url) 36 | response.raise_for_status() 37 | return response.json() 38 | except requests.RequestException: 39 | # 主接口失败时尝试备用接口 40 | backup_url = f"{self.BACKUP_URL}/{endpoint}/{self.LICENSE}" 41 | response = self.session.get(backup_url) 42 | response.raise_for_status() 43 | return response.json() 44 | 45 | def get_daily_news(self, min_count: int = 20) -> List[Dict]: 46 | """获取每日财经新闻 47 | 48 | Args: 49 | min_count: 最少获取的新闻条数 50 | 51 | Returns: 52 | List[Dict]: 新闻列表,每条新闻包含标题、内容、来源、时间等信息 53 | """ 54 | try: 55 | # 使用新的新闻API接口 56 | url = "https://api.tanshuapi.com/api/toutiao/v1/index" 57 | params = { 58 | "key": "f988e351306ae91d4325aae57b0f6c8a", 59 | "type": "股票", 60 | "num": max(min_count, 40), # 确保获取足够的新闻 61 | "start": 0 62 | } 63 | 64 | response = requests.get(url, params=params) 65 | data = response.json() 66 | 67 | if data.get("code") != 1: # API返回错误 68 | print(f"获取新闻数据失败: {data.get('msg')}") 69 | return [] 70 | 71 | news_list = [] 72 | for news in data.get("data", {}).get("list", []): 73 | try: 74 | news_item = { 75 | 'title': news.get('title', ''), 76 | 'content': news.get('content', ''), 77 | 'source': news.get('src', ''), 78 | 'time': news.get('time', ''), 79 | 'url': news.get('weburl', '') 80 | } 81 | if news_item['title'] and news_item['content']: 82 | news_list.append(news_item) 83 | except Exception as e: 84 | print(f"处理单条新闻数据时出错: {str(e)}") 85 | continue 86 | 87 | if len(news_list) < min_count: 88 | print(f"警告:只获取到 {len(news_list)} 条新闻,少于要求的 {min_count} 条") 89 | 90 | return news_list[:min_count] if len(news_list) > min_count else news_list 91 | 92 | except Exception as e: 93 | print(f"获取新闻数据时出错: {str(e)}") 94 | return [] 95 | 96 | def get_stock_news(self, stock_code: str, days: int = 7) -> List[Dict]: 97 | """获取个股新闻 98 | 99 | Args: 100 | stock_code: 股票代码 101 | days: 获取最近几天的新闻,默认7天 102 | """ 103 | try: 104 | # 处理股票代码格式 105 | if stock_code.startswith('6'): 106 | sina_code = f'sh{stock_code}' 107 | elif stock_code.startswith('0') or stock_code.startswith('3'): 108 | sina_code = f'sz{stock_code}' 109 | else: 110 | print(f"不支持的股票代码格式: {stock_code}") 111 | return [] 112 | 113 | # 获取新闻列表页面 114 | list_url = f'https://vip.stock.finance.sina.com.cn/corp/go.php/vCB_AllNewsStock/symbol/{sina_code}.phtml' 115 | print(f"正在获取新闻列表,URL: {list_url}") 116 | 117 | headers = { 118 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 119 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 120 | 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 121 | 'Referer': 'https://vip.stock.finance.sina.com.cn', 122 | } 123 | 124 | response = self.session.get(list_url, headers=headers, timeout=10) 125 | response.encoding = 'gb2312' # 新浪财经使用 GB2312 编码 126 | 127 | # 解析新闻列表页面 128 | soup = BeautifulSoup(response.text, 'html.parser') 129 | 130 | # 查找新闻表格 131 | news_table = soup.find('table', {'id': 'con02-0'}) 132 | if not news_table: 133 | print("未找到主新闻表格,尝试查找其他新闻链接...") 134 | news_links = [] 135 | for a in soup.find_all('a', href=True): 136 | href = a['href'] 137 | if any(domain in href for domain in ['finance.sina.com.cn', 'sina.com.cn']) and '/' in href: 138 | title = a.text.strip() 139 | if title and len(title) > 5: # 过滤掉太短的标题 140 | news_links.append({ 141 | 'title': title, 142 | 'url': href if href.startswith('http') else f'https:{href}' 143 | }) 144 | else: 145 | # 从表格中提取新闻链接 146 | news_links = [] 147 | for row in news_table.find_all('tr')[1:]: # 跳过表头 148 | cols = row.find_all('td') 149 | if len(cols) >= 2: 150 | link = cols[1].find('a') 151 | if link and link.has_attr('href'): 152 | title = link.text.strip() 153 | url = link['href'] 154 | if title and url: 155 | news_links.append({ 156 | 'title': title, 157 | 'url': url if url.startswith('http') else f'https:{url}' 158 | }) 159 | 160 | if not news_links: 161 | print(f"未找到股票 {stock_code} 的相关新闻链接") 162 | return [] 163 | 164 | print(f"找到 {len(news_links)} 条新闻链接,获取最新的10条新闻内容...") 165 | 166 | # 获取每条新闻的详细内容 167 | news_list = [] 168 | for news in news_links[:10]: # 限制获取最新的10条新闻 169 | try: 170 | print(f"\n正在获取新闻: {news['title']}") 171 | print(f"URL: {news['url']}") 172 | 173 | content = self._fetch_news_content(news['url']) 174 | if content and len(content) > 100: # 确保内容有足够长度 175 | news_list.append({ 176 | 'title': news['title'], 177 | 'content': content, 178 | 'url': news['url'], 179 | 'time': datetime.now().strftime('%Y-%m-%d'), # 使用当前日期 180 | 'source': '新浪财经' 181 | }) 182 | print(f"✓ 成功获取新闻内容 ({len(content)} 字)") 183 | else: 184 | print("✗ 新闻内容太短或获取失败") 185 | except Exception as e: 186 | print(f"✗ 获取新闻失败: {str(e)}") 187 | continue 188 | 189 | # 添加延时,避免请求过快 190 | time.sleep(1) 191 | 192 | print(f"\n成功获取 {len(news_list)} 条完整新闻") 193 | return news_list 194 | 195 | except Exception as e: 196 | print(f"获取股票新闻失败: {str(e)}") 197 | return [] 198 | 199 | def _fetch_news_content(self, url: str) -> str: 200 | """获取新闻内容""" 201 | try: 202 | headers = { 203 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 204 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 205 | 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 206 | 'Referer': 'https://vip.stock.finance.sina.com.cn', 207 | } 208 | 209 | response = self.session.get(url, headers=headers, timeout=10) 210 | 211 | # 自动检测编码 212 | if 'charset=gb2312' in response.text or 'charset=GB2312' in response.text: 213 | response.encoding = 'gb2312' 214 | else: 215 | response.encoding = 'utf-8' 216 | 217 | soup = BeautifulSoup(response.text, 'html.parser') 218 | 219 | # 尝试多个可能的新闻内容容器 220 | content_selectors = [ 221 | {'id': 'artibody'}, 222 | {'class_': 'article-content'}, 223 | {'class_': 'article'}, 224 | {'class_': 'content'}, 225 | {'id': 'article_content'}, 226 | {'class_': 'main-content'}, 227 | ] 228 | 229 | for selector in content_selectors: 230 | content_div = soup.find('div', **selector) 231 | if content_div: 232 | # 移除不需要的元素 233 | for unwanted in content_div.find_all(['script', 'style', 'iframe', 'div', 'table']): 234 | unwanted.decompose() 235 | 236 | # 获取段落文本 237 | paragraphs = [] 238 | for p in content_div.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']): 239 | text = p.get_text(strip=True) 240 | if text and len(text) > 10: # 过滤掉太短的段落 241 | paragraphs.append(text) 242 | 243 | if paragraphs: 244 | content = ' '.join(paragraphs) 245 | content = re.sub(r'\s+', ' ', content) 246 | return content[:2000] # 限制内容长度 247 | 248 | return "" 249 | 250 | except Exception as e: 251 | print(f"获取新闻内容失败 {url}: {str(e)}") 252 | return "" -------------------------------------------------------------------------------- /src/data/stock_data.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from typing import Dict, List, Optional, Any 4 | 5 | class MaiRuiStockAPI: 6 | """麦蕊股票数据API封装""" 7 | 8 | BASE_URL = "http://api.mairui.club" 9 | BACKUP_URL = "http://api1.mairui.club" 10 | LICENSE = "8507084C-C56F-432E-8C43-FA40669B024B" 11 | 12 | def __init__(self): 13 | self.session = requests.Session() 14 | 15 | def _request(self, endpoint: str, params: Optional[Dict] = None) -> List[Dict]: 16 | """发送API请求 17 | 18 | Args: 19 | endpoint: API端点 20 | params: 请求参数 21 | 22 | Returns: 23 | List[Dict]: JSON响应数据 24 | """ 25 | url = f"{self.BASE_URL}/{endpoint}/{self.LICENSE}" 26 | 27 | try: 28 | response = self.session.get(url, params=params) 29 | response.raise_for_status() 30 | return response.json() 31 | except requests.RequestException: 32 | # 主接口失败时尝试备用接口 33 | backup_url = f"{self.BACKUP_URL}/{endpoint}/{self.LICENSE}" 34 | response = self.session.get(backup_url, params=params) 35 | response.raise_for_status() 36 | return response.json() 37 | 38 | def get_stock_list(self) -> List[Dict]: 39 | """获取沪深两市股票列表 40 | 41 | Returns: 42 | List[Dict]: 包含股票代码、名称、交易所的列表 43 | """ 44 | return self._request("hslt/list") 45 | 46 | def get_realtime_quote(self, stock_code: str) -> Dict: 47 | """获取股票实时行情 48 | 49 | Args: 50 | stock_code: 股票代码 51 | 52 | Returns: 53 | Dict: 实时行情数据 54 | """ 55 | try: 56 | data = self._request(f"hsrl/ssjy/{stock_code}") 57 | # 麦蕊API返回的是列表,需要处理成字典格式 58 | if isinstance(data, list) and data: 59 | return { 60 | 'price': data[0].get('p', 0), 61 | 'open': data[0].get('o', 0), 62 | 'high': data[0].get('h', 0), 63 | 'low': data[0].get('l', 0), 64 | 'volume': data[0].get('v', 0) 65 | } 66 | return {} 67 | except (KeyError, IndexError): 68 | return {} 69 | 70 | def get_history_klines(self, stock_code: str, period: str = "dq") -> List[Dict]: 71 | """获取历史K线数据 72 | 73 | Args: 74 | stock_code: 股票代码 75 | period: K线周期,默认日线前复权 76 | 77 | Returns: 78 | List[Dict]: K线数据列表 79 | """ 80 | return self._request(f"hszbl/fsjy/{stock_code}/{period}") 81 | 82 | def get_stock_info(self, stock_code: str) -> Dict[str, str]: 83 | """获取股票基本信息 84 | 85 | Args: 86 | stock_code: 股票代码 87 | 88 | Returns: 89 | Dict: 股票基本信息,包含名称、行业、主营业务等 90 | """ 91 | try: 92 | # 由于接口限制,这里返回模拟数据 93 | # 后续可以接入实际的股票信息API 94 | industry_map = { 95 | '600626': '房地产开发', 96 | '003032': '新能源汽车', 97 | '000001': '银行', 98 | '600000': '银行', 99 | # 可以添加更多股票的行业映射 100 | } 101 | 102 | name_map = { 103 | '600626': '申达股份', 104 | '003032': '传智教育', 105 | '000001': '平安银行', 106 | '600000': '浦发银行', 107 | # 可以添加更多股票的名称映射 108 | } 109 | 110 | business_map = { 111 | '600626': '主要从事房地产开发、物业管理等业务', 112 | '003032': '主要从事教育培训、在线教育等业务', 113 | '000001': '主要从事商业银行业务,包括公司业务、零售业务和金融市场业务等', 114 | '600000': '主要从事商业银行业务,包括公司金融、零售金融和金融市场业务等', 115 | # 可以添加更多股票的主营业务描述 116 | } 117 | 118 | if stock_code not in name_map: 119 | print(f"警告:未找到股票 {stock_code} 的基本信息,尝试实时获取...") 120 | # 这里可以添加实时获取股票信息的逻辑 121 | return { 122 | 'code': stock_code, 123 | 'name': f'未知股票_{stock_code}', 124 | 'industry': '未知行业', 125 | 'main_business': '暂无描述' 126 | } 127 | 128 | return { 129 | 'code': stock_code, 130 | 'name': name_map.get(stock_code, '未知'), 131 | 'industry': industry_map.get(stock_code, '未知行业'), 132 | 'main_business': business_map.get(stock_code, '暂无描述') 133 | } 134 | 135 | except Exception as e: 136 | print(f"获取股票信息时出错: {str(e)}") 137 | return None 138 | 139 | def get_top_holders(self, stock_code: str) -> List[Dict]: 140 | """获取前十大股东 141 | 142 | Args: 143 | stock_code: 股票代码 144 | 145 | Returns: 146 | List[Dict]: 股东信息列表 147 | """ 148 | try: 149 | data = self._request(f"hscp/sdgd/{stock_code}") 150 | if isinstance(data, list) and data and 'sdgd' in data[0]: 151 | holders = data[0]['sdgd'] 152 | return [ 153 | { 154 | 'name': holder.get('gdmc', ''), # 股东名称 155 | 'ratio': holder.get('cgbl', 0), # 持股比例 156 | 'shares': holder.get('cgsl', 0), # 持股数量 157 | 'type': holder.get('gbxz', '') # 股本性质 158 | } 159 | for holder in holders 160 | ] 161 | return [] 162 | except (KeyError, IndexError): 163 | return [] 164 | 165 | def get_technical_indicators(self, stock_code: str) -> Dict[str, Any]: 166 | """获取技术指标""" 167 | try: 168 | # 获取K线数据 169 | klines = self._request(f"hsrl/kline/{stock_code}") 170 | 171 | # 计算技术指标 172 | ma5 = self._calculate_ma(klines, 5) 173 | ma10 = self._calculate_ma(klines, 10) 174 | ma20 = self._calculate_ma(klines, 20) 175 | 176 | return { 177 | 'ma5': ma5, 178 | 'ma10': ma10, 179 | 'ma20': ma20, 180 | 'volume': klines[-1].get('v', 0) if klines and len(klines) > 0 else 0, 181 | 'turnover_rate': klines[-1].get('tr', 0) if klines and len(klines) > 0 else 0 182 | } 183 | except Exception as e: 184 | print(f"获取技术指标失败: {str(e)}") 185 | return {} 186 | 187 | def _calculate_ma(self, klines: List[Dict], period: int) -> float: 188 | """计算移动平均线""" 189 | if not klines or len(klines) < period: 190 | return 0 191 | 192 | closes = [float(k.get('c', 0)) for k in klines[-period:]] 193 | return sum(closes) / period -------------------------------------------------------------------------------- /src/llm/__init__.py: -------------------------------------------------------------------------------- 1 | from .stock_selector import StockSelector 2 | from .strategy_generator import StrategyGenerator 3 | from .model_api import LLMService 4 | 5 | __all__ = ['StockSelector', 'StrategyGenerator', 'LLMService'] -------------------------------------------------------------------------------- /src/llm/model_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from openai import OpenAI 3 | import re 4 | import json 5 | from typing import Dict, Any, List 6 | from datetime import datetime 7 | from ..data import MaiRuiStockAPI, NewsDataFetcher, FinancialDataFetcher # 添加缺失的导入 8 | 9 | class LLMService: 10 | """大模型服务接口""" 11 | 12 | def __init__(self, api_key: str): 13 | """初始化 DeepSeek API""" 14 | self.client = OpenAI( 15 | api_key=api_key, 16 | base_url="https://api.deepseek.com" 17 | ) 18 | self.stock_api = MaiRuiStockAPI() # 初始化股票API 19 | self.financial_api = FinancialDataFetcher() # 初始化财务数据API 20 | self.news_api = NewsDataFetcher() # 初始化新闻API 21 | 22 | def analyze_stock(self, stock_info: Dict[str, Any], news_list: List[Dict], 23 | financial_data: Dict[str, Any]) -> Dict[str, Any]: 24 | """分析股票投资价值""" 25 | try: 26 | # 构建提示词 27 | prompt = self._build_analysis_prompt(stock_info, news_list, financial_data) 28 | 29 | # 调用模型 30 | response = self.client.chat.completions.create( 31 | model="deepseek-reasoner", # 使用 DeepSeek-R1 模型 32 | messages=[{ 33 | 'role': 'system', 34 | 'content': '你是一个专业的股票分析师,擅长分析公司基本面、行业前景和财务数据。' 35 | }, { 36 | 'role': 'user', 37 | 'content': prompt 38 | }], 39 | temperature=0.7, 40 | max_tokens=1500, 41 | top_p=0.8, 42 | ) 43 | 44 | return { 45 | 'analysis': response.choices[0].message.content, 46 | 'timestamp': datetime.now().isoformat(), 47 | 'status': 'success' 48 | } 49 | 50 | except Exception as e: 51 | return { 52 | 'error': f"分析失败: {str(e)}", 53 | 'timestamp': datetime.now().isoformat(), 54 | 'status': 'error' 55 | } 56 | 57 | def _build_analysis_prompt(self, stock_info: Dict[str, Any], 58 | news_list: List[Dict], 59 | financial_data: Dict[str, Any]) -> str: 60 | """构建分析提示词""" 61 | prompt = f"""请分析以下股票的投资价值并给出具体的交易建议: 62 | 63 | 1. 股票基本信息: 64 | 代码: {stock_info.get('code')} 65 | 名称: {stock_info.get('name')} 66 | 所属行业: {stock_info.get('industry')} 67 | 主营业务: {stock_info.get('main_business')} 68 | 69 | 2. 最新相关新闻: 70 | """ 71 | 72 | for i, news in enumerate(news_list[:3], 1): 73 | prompt += f""" 74 | 新闻{i}: 75 | 标题: {news.get('title')} 76 | 时间: {news.get('time')} 77 | 内容: {news.get('content')} 78 | """ 79 | 80 | prompt += f""" 81 | 3. 主要财务指标: 82 | 营业收入: {financial_data.get('revenue')} 83 | 净利润: {financial_data.get('net_profit')} 84 | 毛利率: {financial_data.get('gross_margin')} 85 | ROE: {financial_data.get('roe')} 86 | 87 | 请从以下几个方面进行分析并给出具体建议: 88 | 89 | 1. 公司基本面分析 90 | 2. 行业发展前景 91 | 3. 最新消息影响 92 | 4. 财务指标分析 93 | 5. 具体交易建议 94 | 95 | 你必须严格按照以下格式给出交易建议(包含所有字段且不能为空): 96 | 97 | 交易建议: 98 | 交易方向:[买入/卖出] 99 | 目标价格:[具体数字,单位:元] 100 | 交易数量:[具体数字,单位:股] 101 | 止损价格:[具体数字,单位:元] 102 | 止盈目标:[具体数字,单位:元] 103 | 持仓时间:[具体数字,单位:个交易日] 104 | 风险等级:[高/中/低] 105 | 106 | 注意: 107 | 1. 所有数值必须是具体的数字,不能使用范围或描述性语言 108 | 2. 价格必须精确到小数点后两位 109 | 3. 交易数量必须是100的整数倍 110 | 4. 持仓时间必须是具体的交易日数 111 | 5. 必须包含上述所有字段,且格式要完全一致 112 | 113 | 请先给出分析,然后在最后给出严格按照上述格式的交易建议。 114 | """ 115 | return prompt 116 | 117 | def analyze_market(self, news_list: List[Dict], available_cash: float) -> Dict[str, Any]: 118 | """分析市场机会""" 119 | try: 120 | # 第一步: 根据新闻分析推荐股票 121 | initial_prompt = f"""请分析以下最新市场新闻,并推荐3-5只值得关注的股票: 122 | 123 | 1. 最新市场新闻: 124 | """ 125 | for i, news in enumerate(news_list[:10], 1): # 增加到10条新闻 126 | initial_prompt += f""" 127 | 新闻{i}: 128 | 标题: {news.get('title')} 129 | 时间: {news.get('time')} 130 | 内容: {news.get('content')} 131 | """ 132 | 133 | initial_prompt += """ 134 | 请根据以上新闻分析当前市场环境,并推荐3-5只值得关注的股票。 135 | 对于每只推荐的股票,请提供: 136 | 1. 股票代码(格式为6位数字,如000001、600000等) 137 | 2. 推荐理由 138 | 3. 所属行业 139 | 140 | 注意:请不要假设或猜测股票的当前价格,我们会在后续分析中获取实时数据。 141 | """ 142 | 143 | print("\n发送给模型的初始提示词:") 144 | print("-" * 50) 145 | print(initial_prompt) 146 | print("-" * 50) 147 | 148 | initial_response = self.client.chat.completions.create( 149 | model="deepseek-reasoner", 150 | messages=[{ 151 | 'role': 'system', 152 | 'content': '你是一个专业的投资顾问,请根据新闻信息推荐股票。请确保提供准确的股票代码(6位数字)。' 153 | }, { 154 | 'role': 'user', 155 | 'content': initial_prompt 156 | }] 157 | ) 158 | 159 | print("\n模型初始响应:") 160 | print("-" * 50) 161 | print(initial_response.choices[0].message.content) 162 | print("-" * 50) 163 | 164 | # 解析推荐的股票代码 165 | recommended_stocks = self._parse_recommended_stocks(initial_response.choices[0].message.content) 166 | print(f"\n解析出的股票代码: {recommended_stocks}") 167 | 168 | if not recommended_stocks: 169 | return { 170 | 'error': "未能从模型响应中解析出有效的股票代码", 171 | 'timestamp': datetime.now().isoformat(), 172 | 'status': 'error' 173 | } 174 | 175 | # 第二步: 获取推荐股票的详细信息 176 | stock_details = [] 177 | for stock_code in recommended_stocks: 178 | print(f"\n获取股票 {stock_code} 的详细信息...") 179 | details = self._get_stock_details(stock_code) 180 | if details: 181 | stock_details.append(details) 182 | print(f"成功获取 {stock_code} 的详细信息") 183 | else: 184 | print(f"无法获取 {stock_code} 的详细信息") 185 | 186 | if not stock_details: 187 | return { 188 | 'error': "无法获取任何推荐股票的详细信息", 189 | 'timestamp': datetime.now().isoformat(), 190 | 'status': 'error' 191 | } 192 | 193 | # 第三步: 对推荐股票进行深入分析 194 | final_prompt = f"""请对以下股票进行深入分析并给出具体交易建议: 195 | 196 | 可用资金:{available_cash}元 197 | 198 | 推荐股票详细信息: 199 | {json.dumps(stock_details, ensure_ascii=False, indent=2)} 200 | 201 | 请针对每只股票给出以下分析: 202 | 1. 基本面分析 - 基于公司情况、行业前景等 203 | 2. 技术面分析 - 基于提供的技术指标 204 | 3. 市场情绪分析 - 基于相关新闻 205 | 4. 风险提示 - 明确指出投资风险 206 | 5. 具体交易建议 207 | 208 | 交易建议必须包含: 209 | - 建议买入价格区间(基于技术分析给出合理区间,不要假设当前价格) 210 | - 建议买入数量(考虑可用资金和风险分散) 211 | - 止损位(明确的价格点位) 212 | - 止盈目标(明确的价格点位) 213 | - 建议持仓时间 214 | - 风险等级(高/中/低) 215 | 216 | 重要提示: 217 | 1. 不要假设或猜测当前股票价格,请基于提供的技术指标进行分析 218 | 2. 给出的买入价格区间必须合理,与技术指标相符 219 | 3. 交易建议必须具体、可执行,不要使用模糊表述 220 | 4. 请考虑资金管理,不要将全部资金投入单一股票 221 | """ 222 | 223 | print("\n发送给模型的最终提示词:") 224 | print("-" * 50) 225 | print(final_prompt) 226 | print("-" * 50) 227 | 228 | final_response = self.client.chat.completions.create( 229 | model="deepseek-reasoner", 230 | messages=[{ 231 | 'role': 'system', 232 | 'content': '你是一个专业的投资顾问,请给出详细的分析和具体的交易建议。请不要假设股票的当前价格,而是基于提供的技术指标给出合理的买入区间。' 233 | }, { 234 | 'role': 'user', 235 | 'content': final_prompt 236 | }] 237 | ) 238 | 239 | print("\n模型最终响应:") 240 | print("-" * 50) 241 | print(final_response.choices[0].message.content) 242 | print("-" * 50) 243 | 244 | return { 245 | 'analysis': final_response.choices[0].message.content, 246 | 'timestamp': datetime.now().isoformat(), 247 | 'status': 'success' 248 | } 249 | 250 | except Exception as e: 251 | import traceback 252 | error_msg = f"分析失败: {str(e)}\n{traceback.format_exc()}" 253 | print(error_msg) 254 | return { 255 | 'error': error_msg, 256 | 'timestamp': datetime.now().isoformat(), 257 | 'status': 'error' 258 | } 259 | 260 | def _parse_recommended_stocks(self, content: str) -> List[str]: 261 | """从模型输出中解析推荐的股票代码""" 262 | # 使用正则表达式匹配股票代码 263 | stock_codes = re.findall(r'[036]\d{5}', content) 264 | return list(set(stock_codes)) # 去重 265 | 266 | def _get_stock_details(self, stock_code: str) -> Dict[str, Any]: 267 | """获取股票详细信息""" 268 | try: 269 | # 获取基本面信息 270 | basic_info = self.stock_api.get_stock_info(stock_code) 271 | 272 | # 获取最新财务指标 273 | financial_data = self.financial_api.get_financial_data(stock_code) 274 | 275 | # 获取相关新闻 276 | news = self.news_api.get_stock_news(stock_code, days=7) 277 | 278 | # 获取技术指标 279 | technical_indicators = self.stock_api.get_technical_indicators(stock_code) 280 | 281 | return { 282 | 'basic_info': basic_info, 283 | 'financial_data': financial_data, 284 | 'news': news[:3], # 只取最新的3条新闻 285 | 'technical_indicators': technical_indicators 286 | } 287 | except Exception as e: 288 | print(f"获取股票 {stock_code} 详细信息失败: {str(e)}") 289 | return None 290 | 291 | def _parse_trading_advice(self, analysis_text: str) -> Dict[str, Any]: 292 | """从分析文本中解析出具体的交易建议""" 293 | try: 294 | advice = { 295 | 'direction': None, 296 | 'target_price': None, 297 | 'quantity': None, 298 | 'stop_loss': None, 299 | 'take_profit': None, 300 | 'holding_period': None, 301 | 'risk_level': None, 302 | 'raw_text': analysis_text 303 | } 304 | 305 | # 使用更精确的正则表达式匹配 306 | patterns = { 307 | 'direction': r'交易方向:\s*(买入|卖出)', 308 | 'target_price': r'目标价格:\s*(\d+\.?\d*)', 309 | 'quantity': r'交易数量:\s*(\d+)', 310 | 'stop_loss': r'止损价格:\s*(\d+\.?\d*)', 311 | 'take_profit': r'止盈目标:\s*(\d+\.?\d*)', 312 | 'holding_period': r'持仓时间:\s*(\d+)', 313 | 'risk_level': r'风险等级:\s*(高|中|低)' 314 | } 315 | 316 | for key, pattern in patterns.items(): 317 | match = re.search(pattern, analysis_text) 318 | if match: 319 | value = match.group(1) 320 | if key in ['target_price', 'stop_loss', 'take_profit']: 321 | advice[key] = float(value) 322 | elif key in ['quantity', 'holding_period']: 323 | advice[key] = int(value) 324 | else: 325 | advice[key] = value 326 | 327 | return advice 328 | 329 | except Exception as e: 330 | print(f"解析交易建议时出错: {str(e)}") 331 | return None 332 | 333 | def get_technical_indicators(self, stock_code: str) -> Dict[str, Any]: 334 | """获取技术指标""" 335 | try: 336 | # 获取K线数据 337 | klines = self._request(f"hsrl/kline/{stock_code}") 338 | 339 | # 计算技术指标 340 | ma5 = self._calculate_ma(klines, 5) 341 | ma10 = self._calculate_ma(klines, 10) 342 | ma20 = self._calculate_ma(klines, 20) 343 | 344 | return { 345 | 'ma5': ma5, 346 | 'ma10': ma10, 347 | 'ma20': ma20, 348 | 'volume': klines[-1].get('v', 0), 349 | 'turnover_rate': klines[-1].get('tr', 0) 350 | } 351 | except Exception as e: 352 | print(f"获取技术指标失败: {str(e)}") 353 | return {} 354 | 355 | def _calculate_ma(self, klines: List[Dict], period: int) -> float: 356 | """计算移动平均线""" 357 | if len(klines) < period: 358 | return 0 359 | 360 | closes = [float(k.get('c', 0)) for k in klines[-period:]] 361 | return sum(closes) / period -------------------------------------------------------------------------------- /src/llm/stock_selector.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from typing import List, Dict, Any 3 | from ..data import MaiRuiStockAPI, FinancialDataFetcher 4 | import os 5 | 6 | class StockSelector: 7 | """股票选择器""" 8 | 9 | def __init__(self, api_key: str): 10 | """初始化 DeepSeek API""" 11 | self.client = OpenAI( 12 | api_key=api_key, 13 | base_url="https://api.deepseek.com" 14 | ) 15 | self.stock_api = MaiRuiStockAPI() 16 | self.financial_api = FinancialDataFetcher() 17 | 18 | def analyze_stock(self, stock_data: Dict[str, Any], financial_data: Dict[str, Any]) -> str: 19 | """分析个股数据和财务数据""" 20 | prompt = f"""请分析以下股票数据和财务数据,给出投资建议: 21 | 股票数据:{stock_data} 22 | 财务数据:{financial_data} 23 | """ 24 | 25 | response = self.client.chat.completions.create( 26 | model="deepseek-reasoner", 27 | messages=[ 28 | {"role": "system", "content": "你是一个专业的股票分析师,擅长分析股票数据和财务数据。"}, 29 | {"role": "user", "content": prompt} 30 | ] 31 | ) 32 | 33 | return response.choices[0].message.content 34 | 35 | def select_stocks(self, stock_list: List[Dict[str, Any]], count: int = 20) -> List[Dict[str, Any]]: 36 | """从股票列表中选择潜力股""" 37 | analysis_results = [] 38 | 39 | for stock in stock_list: 40 | analysis = self.analyze_stock(stock, {}) 41 | if "建议买入" in analysis or "推荐" in analysis: 42 | analysis_results.append({ 43 | "stock": stock, 44 | "analysis": analysis 45 | }) 46 | 47 | if len(analysis_results) >= count: 48 | break 49 | 50 | return analysis_results[:count] 51 | 52 | def get_stock_selection_explanation(self, stock_data: Dict[str, Any]) -> str: 53 | """获取选股理由""" 54 | prompt = f"""请解释为什么选择这支股票: 55 | 股票数据:{stock_data} 56 | """ 57 | 58 | response = self.client.chat.completions.create( 59 | model="deepseek-reasoner", 60 | messages=[ 61 | {"role": "system", "content": "你是一个专业的股票分析师,擅长解释选股理由。"}, 62 | {"role": "user", "content": prompt} 63 | ] 64 | ) 65 | 66 | return response.choices[0].message.content -------------------------------------------------------------------------------- /src/llm/strategy_generator.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from typing import List, Dict, Any 3 | from ..data import MaiRuiStockAPI, FinancialDataFetcher 4 | import os 5 | 6 | class StrategyGenerator: 7 | def __init__(self, api_key: str): 8 | """初始化 DeepSeek API""" 9 | self.client = OpenAI( 10 | api_key=api_key, 11 | base_url="https://api.deepseek.com" 12 | ) 13 | self.stock_api = MaiRuiStockAPI() 14 | self.financial_api = FinancialDataFetcher() 15 | 16 | def generate_trading_strategy(self, stock_data: Dict[str, Any], financial_data: Dict[str, Any], balance: float) -> Dict[str, Any]: 17 | """生成交易策略""" 18 | prompt = f"""请根据以下信息生成交易策略: 19 | 股票数据:{stock_data} 20 | 财务数据:{financial_data} 21 | 可用资金:{balance} 22 | """ 23 | 24 | response = self.client.chat.completions.create( 25 | model="deepseek-reasoner", 26 | messages=[ 27 | {"role": "system", "content": "你是一个专业的交易策略分析师,擅长制定股票交易策略。"}, 28 | {"role": "user", "content": prompt} 29 | ] 30 | ) 31 | 32 | strategy_text = response.choices[0].message.content 33 | 34 | # 解析策略文本,提取具体操作建议 35 | strategy = self._parse_strategy(strategy_text) 36 | return strategy 37 | 38 | def _parse_strategy(self, strategy_text: str) -> Dict[str, Any]: 39 | """解析策略文本,提取具体操作建议""" 40 | strategy = { 41 | "action": "hold", # buy, sell, hold 42 | "price": 0.0, 43 | "quantity": 0, 44 | "reason": strategy_text, 45 | "risk_level": "medium", # low, medium, high 46 | "stop_loss": 0.0, 47 | "take_profit": 0.0 48 | } 49 | 50 | # 根据策略文本设置具体参数 51 | if "买入" in strategy_text: 52 | strategy["action"] = "buy" 53 | elif "卖出" in strategy_text: 54 | strategy["action"] = "sell" 55 | 56 | # 提取止损止盈价格 57 | # TODO: 使用更复杂的文本分析来提取具体数值 58 | 59 | return strategy -------------------------------------------------------------------------------- /src/portfolio/__init__.py: -------------------------------------------------------------------------------- 1 | from .portfolio_manager import PortfolioManager 2 | from .trade_executor import TradeExecutor 3 | from .cli_manager import PortfolioCLIManager 4 | 5 | __all__ = ['PortfolioManager', 'TradeExecutor', 'PortfolioCLIManager'] -------------------------------------------------------------------------------- /src/portfolio/cli_manager.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional 2 | from .portfolio_manager import PortfolioManager 3 | 4 | class PortfolioCLIManager: 5 | @staticmethod 6 | def get_initial_balance() -> float: 7 | """从用户输入获取初始资金""" 8 | while True: 9 | try: 10 | balance_input = input('请输入初始资金(例如:100000):') 11 | balance = float(balance_input) 12 | if balance > 0: 13 | return balance 14 | print('初始资金必须大于0') 15 | except ValueError: 16 | print('请输入有效的数字') 17 | 18 | @staticmethod 19 | def get_positions() -> Dict[str, Dict[str, Any]]: 20 | """从用户输入获取初始持仓信息""" 21 | positions = {} 22 | print('请输入持仓信息(每行一个,输入空行结束)') 23 | print('格式示例:000001.SZ,1000,10.5') 24 | print('说明:股票代码,持仓数量,成本价') 25 | 26 | while True: 27 | position_input = input('请输入持仓(或直接回车结束):').strip() 28 | if not position_input: 29 | break 30 | 31 | try: 32 | ts_code, quantity, price = position_input.split(',') 33 | positions[ts_code.strip()] = { 34 | 'quantity': int(quantity), 35 | 'avg_price': float(price), 36 | 'last_update': '' 37 | } 38 | except ValueError: 39 | print('输入格式错误,请按照示例格式重新输入') 40 | continue 41 | 42 | return positions 43 | 44 | @classmethod 45 | def initialize_portfolio(cls) -> PortfolioManager: 46 | """初始化投资组合""" 47 | initial_balance = cls.get_initial_balance() 48 | portfolio_manager = PortfolioManager(initial_balance) 49 | 50 | # 获取并设置初始持仓 51 | positions = cls.get_positions() 52 | for ts_code, position in positions.items(): 53 | portfolio_manager.add_position( 54 | ts_code, 55 | position['avg_price'], 56 | position['quantity'] 57 | ) 58 | 59 | return portfolio_manager 60 | 61 | @classmethod 62 | def test_cli_interaction(cls): 63 | """测试CLI交互功能""" 64 | print('开始测试CLI交互功能...') 65 | print('\n1. 测试初始资金输入:') 66 | portfolio = cls.initialize_portfolio() 67 | print(f'初始资金: {portfolio.get_available_balance()}') 68 | 69 | print('\n2. 测试持仓信息:') 70 | positions = portfolio.get_all_positions() 71 | if positions: 72 | print('当前持仓:') 73 | for ts_code, position in positions.items(): 74 | print(f'股票代码: {ts_code}') 75 | print(f'持仓数量: {position["quantity"]}') 76 | print(f'平均成本: {position["avg_price"]}') 77 | print(f'最后更新: {position["last_update"]}\n') 78 | else: 79 | print('当前没有持仓') 80 | 81 | print('测试完成!') 82 | return portfolio -------------------------------------------------------------------------------- /src/portfolio/portfolio_manager.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | from datetime import datetime 3 | 4 | class PortfolioManager: 5 | def __init__(self, initial_balance: float): 6 | """初始化资金池""" 7 | self.initial_balance = initial_balance 8 | self.current_balance = initial_balance 9 | self.positions: Dict[str, Dict[str, Any]] = {} 10 | self.trade_history: List[Dict[str, Any]] = [] 11 | 12 | def get_available_balance(self) -> float: 13 | """获取可用资金""" 14 | return self.current_balance 15 | 16 | def get_total_value(self, current_prices: Dict[str, float]) -> float: 17 | """获取总资产价值""" 18 | portfolio_value = self.current_balance 19 | 20 | for ts_code, position in self.positions.items(): 21 | if ts_code in current_prices: 22 | portfolio_value += position['quantity'] * current_prices[ts_code] 23 | 24 | return portfolio_value 25 | 26 | def add_position(self, ts_code: str, price: float, quantity: int) -> bool: 27 | """添加持仓""" 28 | cost = price * quantity 29 | if cost > self.current_balance: 30 | return False 31 | 32 | if ts_code in self.positions: 33 | # 更新现有持仓 34 | old_position = self.positions[ts_code] 35 | total_quantity = old_position['quantity'] + quantity 36 | avg_price = (old_position['avg_price'] * old_position['quantity'] + price * quantity) / total_quantity 37 | 38 | self.positions[ts_code] = { 39 | 'quantity': total_quantity, 40 | 'avg_price': avg_price, 41 | 'last_update': datetime.now().strftime('%Y-%m-%d %H:%M:%S') 42 | } 43 | else: 44 | # 创建新持仓 45 | self.positions[ts_code] = { 46 | 'quantity': quantity, 47 | 'avg_price': price, 48 | 'last_update': datetime.now().strftime('%Y-%m-%d %H:%M:%S') 49 | } 50 | 51 | self.current_balance -= cost 52 | self._record_trade('buy', ts_code, price, quantity) 53 | return True 54 | 55 | def reduce_position(self, ts_code: str, price: float, quantity: int) -> bool: 56 | """减少持仓""" 57 | if ts_code not in self.positions or self.positions[ts_code]['quantity'] < quantity: 58 | return False 59 | 60 | self.positions[ts_code]['quantity'] -= quantity 61 | if self.positions[ts_code]['quantity'] == 0: 62 | del self.positions[ts_code] 63 | 64 | self.current_balance += price * quantity 65 | self._record_trade('sell', ts_code, price, quantity) 66 | return True 67 | 68 | def get_position(self, ts_code: str) -> Dict[str, Any]: 69 | """获取持仓信息""" 70 | return self.positions.get(ts_code, {}) 71 | 72 | def get_all_positions(self) -> Dict[str, Dict[str, Any]]: 73 | """获取所有持仓信息""" 74 | return self.positions 75 | 76 | def get_trade_history(self) -> List[Dict[str, Any]]: 77 | """获取交易历史""" 78 | return self.trade_history 79 | 80 | def _record_trade(self, action: str, ts_code: str, price: float, quantity: int) -> None: 81 | """记录交易""" 82 | self.trade_history.append({ 83 | 'action': action, 84 | 'ts_code': ts_code, 85 | 'price': price, 86 | 'quantity': quantity, 87 | 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') 88 | }) -------------------------------------------------------------------------------- /src/portfolio/trade_executor.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional 2 | from datetime import datetime 3 | from .portfolio_manager import PortfolioManager 4 | from ..data.stock_data import MaiRuiStockAPI 5 | 6 | class TradeExecutor: 7 | def __init__(self, portfolio_manager: PortfolioManager, stock_data_fetcher: MaiRuiStockAPI): 8 | """初始化交易执行器""" 9 | self.portfolio_manager = portfolio_manager 10 | self.stock_data_fetcher = stock_data_fetcher 11 | self.pending_orders: Dict[str, Dict[str, Any]] = {} 12 | 13 | def execute_strategy(self, strategy: Dict[str, Any], ts_code: str) -> bool: 14 | """执行交易策略""" 15 | if not self._validate_strategy(strategy): 16 | return False 17 | 18 | # 获取当前市场价格 19 | current_price = self._get_current_price(ts_code) 20 | if current_price is None: 21 | return False 22 | 23 | # 执行交易 24 | if strategy['action'] == 'buy': 25 | return self._execute_buy(ts_code, current_price, strategy) 26 | elif strategy['action'] == 'sell': 27 | return self._execute_sell(ts_code, current_price, strategy) 28 | 29 | return True 30 | 31 | def _validate_strategy(self, strategy: Dict[str, Any]) -> bool: 32 | """验证策略是否有效""" 33 | required_fields = ['action', 'quantity', 'price'] 34 | return all(field in strategy for field in required_fields) 35 | 36 | def _get_current_price(self, ts_code: str) -> Optional[float]: 37 | """获取当前市场价格""" 38 | quote = self.stock_data_fetcher.get_realtime_quotes(ts_code) 39 | return float(quote.get('price', 0)) if quote else None 40 | 41 | def _execute_buy(self, ts_code: str, current_price: float, strategy: Dict[str, Any]) -> bool: 42 | """执行买入操作""" 43 | if strategy['price'] >= current_price: 44 | return self.portfolio_manager.add_position(ts_code, current_price, strategy['quantity']) 45 | return False 46 | 47 | def _execute_sell(self, ts_code: str, current_price: float, strategy: Dict[str, Any]) -> bool: 48 | """执行卖出操作""" 49 | if strategy['price'] <= current_price: 50 | return self.portfolio_manager.reduce_position(ts_code, current_price, strategy['quantity']) 51 | return False 52 | 53 | def cancel_pending_order(self, order_id: str) -> bool: 54 | """取消待执行的订单""" 55 | if order_id in self.pending_orders: 56 | del self.pending_orders[order_id] 57 | return True 58 | return False 59 | 60 | def get_order_status(self, order_id: str) -> Dict[str, Any]: 61 | """获取订单状态""" 62 | return self.pending_orders.get(order_id, {}) --------------------------------------------------------------------------------