fix: Tushare priority and realtime quote support (fixed #185)

- Upgrade Tushare priority to -1 when token configured
  - Implement realtime quote with Pro/legacy API fallback
  - Load .env earlier for proper proxy setup
  - Remove unreliable north-bound capital from market analyzer
  - Add ETF test scenario, extend stale issue timer (3d -> 7d)
This commit is contained in:
zhulinsen
2026-01-31 16:10:41 +08:00
parent 9a60f94df1
commit 6c6eaf8c6a
9 changed files with 135 additions and 27 deletions

View File

@@ -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

View File

@@ -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 已自动关闭。

6
.gitignore vendored
View File

@@ -66,4 +66,8 @@ local/
run.sh
verify_*.py
verify_*.py
# ignore claude files
.claude/
CLAUDE.md

View File

@@ -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" # 降级兜底

View File

@@ -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__":

View File

@@ -22,6 +22,8 @@ A股自选股智能分析系统 - 主调度程序
- 买点偏好:缩量回踩 MA5/MA10 支撑
"""
import os
from src.config import setup_env
setup_env()
# 代理配置 - 通过 USE_PROXY 环境变量控制,默认关闭
# GitHub Actions 环境自动跳过代理配置

View File

@@ -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 以排除国内数据源,避免行情获取失败

View File

@@ -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:
(分析上证、深证、创业板等各指数走势特点)
### 三、资金动向
(解读成交额和北向资金流向的含义)
(解读成交额流向的含义)
### 四、热点解读
(分析领涨领跌板块背后的逻辑和驱动因素)

13
test.sh
View File

@@ -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 - 混合市场分析"