diff --git a/.env.example b/.env.example index da38bb48b..569b30f6f 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ TUSHARE_TOKEN=your_tushare_token_here # 【方案一】使用 Gemini(推荐,有免费额度) # 从 https://aistudio.google.com/ 获取免费 API Key # 留空或删除此行则跳过 Gemini,直接使用方案二 -# +# 如果是本地运行,直连gemini/openai官网需要特殊网络环境 GEMINI_API_KEY= GEMINI_MODEL=gemini-3-flash-preview GEMINI_MODEL_FALLBACK=gemini-2.5-flash diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4ff7a14ff..e2c399c14 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,16 +24,16 @@ jobs: uses: actions/stale@v9 with: # ===== Issue 配置 ===== - days-before-issue-stale: 3 - days-before-issue-close: 3 + days-before-issue-stale: 7 + days-before-issue-close: 7 stale-issue-label: 'stale' stale-issue-message: | - 👋 这个 Issue 已经 **3 天** 没有活动了。 + 👋 这个 Issue 已经 **7 天** 没有活动了。 如果问题仍然存在,请回复说明最新情况,我们会继续跟进。 如果问题已解决或不再需要,可以直接关闭此 Issue。 - **如果 3 天内没有回复,此 Issue 将自动关闭。** + **如果 7 天内没有回复,此 Issue 将自动关闭。** close-issue-message: | 🔒 由于长时间没有活动,此 Issue 已自动关闭。 diff --git a/.gitignore b/.gitignore index 6b16be23b..ab2a3bd19 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,8 @@ local/ run.sh -verify_*.py \ No newline at end of file +verify_*.py + +# ignore claude files +.claude/ +CLAUDE.md \ No newline at end of file diff --git a/data_provider/realtime_types.py b/data_provider/realtime_types.py index d968be334..0eb755144 100644 --- a/data_provider/realtime_types.py +++ b/data_provider/realtime_types.py @@ -96,6 +96,7 @@ class RealtimeSource(Enum): AKSHARE_EM = "akshare_em" # 东方财富(akshare库) AKSHARE_SINA = "akshare_sina" # 新浪财经 AKSHARE_QQ = "akshare_qq" # 腾讯财经 + TUSHARE = "tushare" # Tushare Pro TENCENT = "tencent" # 腾讯直连 SINA = "sina" # 新浪直连 FALLBACK = "fallback" # 降级兜底 diff --git a/data_provider/tushare_fetcher.py b/data_provider/tushare_fetcher.py index 2275ba370..615ac3456 100644 --- a/data_provider/tushare_fetcher.py +++ b/data_provider/tushare_fetcher.py @@ -104,7 +104,7 @@ class TushareFetcher(BaseFetcher): 根据 Token 配置和 API 初始化状态确定优先级 策略: - - Token 配置且 API 初始化成功:优先级 0(最高) + - Token 配置且 API 初始化成功:优先级 -1(绝对最高,优于 efinance) - 其他情况:优先级 2(默认) Returns: @@ -114,8 +114,8 @@ class TushareFetcher(BaseFetcher): if config.tushare_token and self._api is not None: # Token 配置且 API 初始化成功,提升为最高优先级 - logger.info("✅ 检测到 TUSHARE_TOKEN 且 API 初始化成功,Tushare 数据源优先级提升为最高 (Priority 0)") - return 0 + logger.info("✅ 检测到 TUSHARE_TOKEN 且 API 初始化成功,Tushare 数据源优先级提升为最高 (Priority -1)") + return -1 # Token 未配置或 API 初始化失败,保持默认优先级 return 2 @@ -391,21 +391,105 @@ class TushareFetcher(BaseFetcher): def get_realtime_quote(self, stock_code: str) -> Optional[dict]: """ - 获取实时行情(Tushare Pro 需要较高积分才能使用实时接口) - - 注意:Tushare 实时行情接口需要较高积分(>=2000), - 普通用户建议使用其他数据源的实时行情。 - + 获取实时行情 + + 策略: + 1. 优先尝试 Pro 接口(需要2000积分):数据全,稳定性高 + 2. 失败降级到旧版接口:门槛低,数据较少 + Args: stock_code: 股票代码 - + Returns: - 实时行情数据字典,失败返回 None + UnifiedRealtimeQuote 对象,失败返回 None """ - # Tushare 实时行情需要高积分,普通用户无法使用 - # 这里仅作为接口预留,实际应使用 efinance 或 akshare 的实时数据 - logger.debug(f"Tushare 实时行情接口需要高积分,建议使用其他数据源: {stock_code}") - return None + if self._api is None: + return None + + from .realtime_types import ( + UnifiedRealtimeQuote, RealtimeSource, + safe_float, safe_int + ) + + # 速率限制检查 + self._check_rate_limit() + + # 尝试 Pro 接口 + try: + ts_code = self._convert_stock_code(stock_code) + # 尝试调用 Pro 实时接口 (需要积分) + df = self._api.quotation(ts_code=ts_code) + + if df is not None and not df.empty: + row = df.iloc[0] + logger.debug(f"Tushare Pro 实时行情获取成功: {stock_code}") + + return UnifiedRealtimeQuote( + code=stock_code, + name=str(row.get('name', '')), + source=RealtimeSource.TUSHARE, + price=safe_float(row.get('price')), + change_pct=safe_float(row.get('pct_chg')), # Pro 接口通常直接返回涨跌幅 + change_amount=safe_float(row.get('change')), + volume=safe_int(row.get('vol')), + amount=safe_float(row.get('amount')), + high=safe_float(row.get('high')), + low=safe_float(row.get('low')), + open_price=safe_float(row.get('open')), + pre_close=safe_float(row.get('pre_close')), + turnover_rate=safe_float(row.get('turnover_ratio')), # Pro 接口可能有换手率 + pe_ratio=safe_float(row.get('pe')), + pb_ratio=safe_float(row.get('pb')), + total_mv=safe_float(row.get('total_mv')), + ) + except Exception as e: + # 仅记录调试日志,不报错,继续尝试降级 + logger.debug(f"Tushare Pro 实时行情不可用 (可能是积分不足): {e}") + + # 降级:尝试旧版接口 + try: + import tushare as ts + + # Tushare 旧版接口使用 6 位代码 + code_6 = stock_code.split('.')[0] if '.' in stock_code else stock_code + + # 调用旧版实时接口 (ts.get_realtime_quotes) + df = ts.get_realtime_quotes(code_6) + + if df is None or df.empty: + return None + + row = df.iloc[0] + + # 计算涨跌幅 + price = safe_float(row['price']) + pre_close = safe_float(row['pre_close']) + change_pct = 0.0 + change_amount = 0.0 + + if price and pre_close and pre_close > 0: + change_amount = price - pre_close + change_pct = (change_amount / pre_close) * 100 + + # 构建统一对象 + return UnifiedRealtimeQuote( + code=stock_code, + name=str(row['name']), + source=RealtimeSource.TUSHARE, + price=price, + change_pct=round(change_pct, 2), + change_amount=round(change_amount, 2), + volume=safe_int(row['volume']) // 100, # 转换为手 + amount=safe_float(row['amount']), + high=safe_float(row['high']), + low=safe_float(row['low']), + open_price=safe_float(row['open']), + pre_close=pre_close, + ) + + except Exception as e: + logger.warning(f"Tushare (旧版) 获取实时行情失败 {stock_code}: {e}") + return None if __name__ == "__main__": diff --git a/main.py b/main.py index 9bf4d8f51..417765cd0 100644 --- a/main.py +++ b/main.py @@ -22,6 +22,8 @@ A股自选股智能分析系统 - 主调度程序 - 买点偏好:缩量回踩 MA5/MA10 支撑 """ import os +from src.config import setup_env +setup_env() # 代理配置 - 通过 USE_PROXY 环境变量控制,默认关闭 # GitHub Actions 环境自动跳过代理配置 diff --git a/src/config.py b/src/config.py index 8587d5e45..d434e639f 100644 --- a/src/config.py +++ b/src/config.py @@ -17,6 +17,13 @@ from dotenv import load_dotenv, dotenv_values from dataclasses import dataclass, field +def setup_env(): + """初始化环境变量(支持从 .env 加载)""" + # src/config.py -> src/ -> root + env_path = Path(__file__).parent.parent / '.env' + load_dotenv(dotenv_path=env_path) + + @dataclass class Config: """ @@ -216,10 +223,8 @@ class Config: 2. .env 文件 3. 代码中的默认值 """ - # 加载项目根目录下的 .env 文件 - # src/config.py -> src/ -> root - env_path = Path(__file__).parent.parent / '.env' - load_dotenv(dotenv_path=env_path) + # 确保环境变量已加载 + setup_env() # === 智能代理配置 (关键修复) === # 如果配置了代理,自动设置 NO_PROXY 以排除国内数据源,避免行情获取失败 diff --git a/src/market_analyzer.py b/src/market_analyzer.py index 3c7e33592..a1f52c44f 100644 --- a/src/market_analyzer.py +++ b/src/market_analyzer.py @@ -461,7 +461,7 @@ class MarketAnalyzer: snippet = n.get('snippet', '')[:100] news_text += f"{i}. {title}\n {snippet}\n" - prompt = f"""你是一位专业的A股市场分析师,请根据以下数据生成一份简洁的大盘复盘报告。 + prompt = f"""你是一位专业的A/H/美股市场分析师,请根据以下数据生成一份简洁的大盘复盘报告。 【重要】输出要求: - 必须输出纯 Markdown 文本格式 @@ -483,7 +483,6 @@ class MarketAnalyzer: - 上涨: {overview.up_count} 家 | 下跌: {overview.down_count} 家 | 平盘: {overview.flat_count} 家 - 涨停: {overview.limit_up_count} 家 | 跌停: {overview.limit_down_count} 家 - 两市成交额: {overview.total_amount:.0f} 亿元 -- 北向资金: {overview.north_flow:+.2f} 亿元 ## 板块表现 领涨: {top_sectors_text if top_sectors_text else "暂无数据"} @@ -507,7 +506,7 @@ class MarketAnalyzer: (分析上证、深证、创业板等各指数走势特点) ### 三、资金动向 -(解读成交额和北向资金流向的含义) +(解读成交额流向的含义) ### 四、热点解读 (分析领涨领跌板块背后的逻辑和驱动因素) diff --git a/test.sh b/test.sh index 4ba68365f..a124ed766 100755 --- a/test.sh +++ b/test.sh @@ -9,6 +9,7 @@ # 测试场景: # market - 仅大盘复盘 # a-stock - A股个股分析(茅台、平安银行) +# etf - etf分析(卫星etf 563230) # hk-stock - 港股分析(腾讯、阿里) # us-stock - 美股分析(苹果、特斯拉) # mixed - 混合市场分析 @@ -93,6 +94,14 @@ test_a_stock() { success "A股分析测试完成" } +# 测试2.5: ETF分析 +test_etf() { + header "测试场景: ETF分析" + info "分析ETF: 563230(卫星ETF)" + python3 main.py --stocks 563230 --no-market-review + success "ETF分析测试完成" +} + # 测试3: 港股分析 test_hk_stock() { header "测试场景: 港股分析" @@ -292,6 +301,9 @@ main() { a-stock|a_stock|astock) test_a_stock ;; + etf) + test_etf + ;; hk-stock|hk_stock|hkstock|hk) test_hk_stock ;; @@ -335,6 +347,7 @@ main() { echo "测试场景:" echo " market - 仅大盘复盘" echo " a-stock - A股个股分析" + echo " etf - ETF分析" echo " hk-stock - 港股分析" echo " us-stock - 美股分析" echo " mixed - 混合市场分析"