""" 智能盯盘 - A股数据获取模块 使用TDX/akshare获取实时行情和技术指标 支持降级到tushare作为备用数据源 """ import logging import os import akshare as ak import pandas as pd from typing import Dict, Optional from datetime import datetime, timedelta class SmartMonitorDataFetcher: """A股数据获取器(支持多数据源降级:TDX -> AKShare -> Tushare)""" def __init__(self, use_tdx: bool = None, tdx_base_url: str = None): """ 初始化数据获取器 Args: use_tdx: 是否使用TDX数据源(可选,从配置读取) tdx_base_url: TDX接口地址(可选,从配置读取) """ self.logger = logging.getLogger(__name__) # TDX数据源配置 if use_tdx is None: from config import TDX_CONFIG use_tdx = TDX_CONFIG.get('enabled', False) if tdx_base_url is None: from config import TDX_CONFIG tdx_base_url = TDX_CONFIG.get('base_url', 'http://192.168.1.222:8181') self.use_tdx = use_tdx self.tdx_fetcher = None if self.use_tdx: try: from smart_monitor_tdx_data import SmartMonitorTDXDataFetcher self.tdx_fetcher = SmartMonitorTDXDataFetcher(base_url=tdx_base_url) self.logger.info(f"TDX数据源已启用: {tdx_base_url}") except Exception as e: self.logger.warning(f"TDX数据源初始化失败: {e},将使用AKShare") self.use_tdx = False # 初始化Tushare(备用数据源) self.ts_pro = None tushare_token = os.getenv('TUSHARE_TOKEN', '') if tushare_token: try: import tushare as ts ts.set_token(tushare_token) self.ts_pro = ts.pro_api() self.logger.info("Tushare备用数据源初始化成功") except Exception as e: self.logger.warning(f"Tushare初始化失败: {e}") else: self.logger.info("未配置Tushare Token,仅使用AKShare数据源") def get_realtime_quote(self, stock_code: str, retry: int = 1) -> Optional[Dict]: """ 获取实时行情(带重试和降级机制) 优先使用TDX,失败时降级到AKShare,最后降级到Tushare Args: stock_code: 股票代码(如:600519) retry: 重试次数(默认1次,避免IP封禁) Returns: 实时行情数据 """ import time # 方法1: 尝试使用TDX(如果启用) if self.use_tdx and self.tdx_fetcher: try: quote = self.tdx_fetcher.get_realtime_quote(stock_code) if quote: return quote else: self.logger.warning(f"TDX获取失败 {stock_code},尝试降级到AKShare") except Exception as e: self.logger.warning(f"TDX获取异常 {stock_code}: {e},尝试降级到AKShare") # 方法2: 组合使用AKShare分钟行情 + 基本信息 for attempt in range(retry): try: # 1.1 获取股票基本信息(名称) info_df = ak.stock_individual_info_em(symbol=stock_code) stock_name = 'N/A' if not info_df.empty: info_dict = dict(zip(info_df['item'], info_df['value'])) stock_name = info_dict.get('股票简称', 'N/A') # 1.2 获取分钟级实时行情 min_df = ak.stock_zh_a_hist_min_em(symbol=stock_code, period='1', adjust='') if min_df.empty: self.logger.warning(f"AKShare未找到股票 {stock_code} 的分钟行情数据") if attempt < retry - 1: time.sleep(2) continue break # 1.3 获取历史数据(计算昨收) hist_df = ak.stock_zh_a_hist(symbol=stock_code, period='daily', adjust='') # 提取最新分钟数据 latest = min_df.iloc[-1] current_price = float(latest['收盘']) # 计算昨收和涨跌幅 if len(hist_df) >= 2: pre_close = float(hist_df.iloc[-2]['收盘']) else: pre_close = current_price change_amount = current_price - pre_close change_pct = (change_amount / pre_close * 100) if pre_close > 0 else 0 # 从历史数据获取今天的统计数据 if len(hist_df) >= 1: today_data = hist_df.iloc[-1] daily_volume = float(today_data.get('成交量', 0)) daily_amount = float(today_data.get('成交额', 0)) daily_high = float(today_data.get('最高', 0)) daily_low = float(today_data.get('最低', 0)) daily_open = float(today_data.get('开盘', 0)) turnover_rate = float(today_data.get('换手率', 0)) else: # 使用分钟数据 daily_volume = min_df['成交量'].sum() daily_amount = min_df['成交额'].sum() daily_high = min_df['最高'].max() daily_low = min_df['最低'].min() daily_open = float(min_df.iloc[0]['开盘']) turnover_rate = 0.0 self.logger.info(f"✅ AKShare成功获取 {stock_code} ({stock_name}) 实时行情") return { 'code': stock_code, 'name': stock_name, 'current_price': current_price, 'change_pct': change_pct, 'change_amount': change_amount, 'volume': daily_volume, # 手 'amount': daily_amount, # 元 'high': daily_high, 'low': daily_low, 'open': daily_open, 'pre_close': pre_close, 'turnover_rate': turnover_rate, 'volume_ratio': 1.0, 'update_time': str(latest['时间']), 'data_source': 'akshare' } except Exception as e: if attempt < retry - 1: self.logger.warning(f"AKShare获取失败 {stock_code},第{attempt+1}次重试... 错误: {type(e).__name__}: {str(e)[:50]}") time.sleep(2) # 等待2秒后重试 else: self.logger.warning(f"AKShare获取失败 {stock_code}(已重试{retry}次),尝试降级") # 降级到Tushare if self.ts_pro: self.logger.info(f"降级到Tushare获取 {stock_code}...") return self._get_realtime_quote_from_tushare(stock_code) else: self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 行情") return None def get_technical_indicators(self, stock_code: str, period: str = 'daily', retry: int = 1) -> Optional[Dict]: """ 计算技术指标(带降级机制) 优先使用TDX,失败时降级到AKShare,最后降级到Tushare Args: stock_code: 股票代码 period: 周期(daily/weekly/monthly) retry: 重试次数(默认1次) Returns: 技术指标数据 """ import time # 方法1: 尝试使用TDX(如果启用) if self.use_tdx and self.tdx_fetcher: try: indicators = self.tdx_fetcher.get_technical_indicators(stock_code, period) if indicators: return indicators else: self.logger.warning(f"TDX计算技术指标失败 {stock_code},尝试降级到AKShare") except Exception as e: self.logger.warning(f"TDX计算技术指标异常 {stock_code}: {e},尝试降级到AKShare") # 方法2: 尝试使用AKShare for attempt in range(retry): try: # 获取历史数据(最近200个交易日,用于计算指标) end_date = datetime.now().strftime('%Y%m%d') start_date = (datetime.now() - timedelta(days=300)).strftime('%Y%m%d') # 获取历史数据 df = ak.stock_zh_a_hist( symbol=stock_code, period=period, start_date=start_date, end_date=end_date, adjust="qfq" # 前复权 ) if df.empty or len(df) < 60: if attempt < retry - 1: self.logger.warning(f"AKShare历史数据不足 {stock_code},第{attempt+1}次重试...") time.sleep(1) continue else: self.logger.warning(f"AKShare历史数据不足 {stock_code},尝试降级") break # 数据充足,计算技术指标 return self._calculate_all_indicators(df, stock_code) except Exception as e: if attempt < retry - 1: self.logger.warning(f"AKShare获取历史数据失败 {stock_code},第{attempt+1}次重试... 错误: {type(e).__name__}: {str(e)[:50]}") time.sleep(1) else: self.logger.warning(f"AKShare获取历史数据失败 {stock_code}(已重试{retry}次),尝试降级到Tushare") break # 方法3: 降级到Tushare if self.ts_pro: self.logger.info(f"降级到Tushare获取 {stock_code} 历史数据...") return self._get_technical_indicators_from_tushare(stock_code, period) else: self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 技术指标") return None def _calculate_all_indicators(self, df: pd.DataFrame, stock_code: str) -> Optional[Dict]: """ 根据历史数据计算所有技术指标 Args: df: 历史数据DataFrame stock_code: 股票代码 Returns: 技术指标数据 """ try: if df.empty or len(df) < 60: self.logger.warning(f"股票 {stock_code} 历史数据不足") return None # 计算均线 df['ma5'] = df['收盘'].rolling(window=5).mean() df['ma20'] = df['收盘'].rolling(window=20).mean() df['ma60'] = df['收盘'].rolling(window=60).mean() # 计算MACD df = self._calculate_macd(df) # 计算RSI df = self._calculate_rsi(df, periods=[6, 12, 24]) # 计算KDJ df = self._calculate_kdj(df) # 计算布林带 df = self._calculate_bollinger(df) # 计算量能均线 df['vol_ma5'] = df['成交量'].rolling(window=5).mean() df['vol_ma10'] = df['成交量'].rolling(window=10).mean() # 取最后一行数据 latest = df.iloc[-1] # 判断趋势 current_price = float(latest['收盘']) ma5 = float(latest['ma5']) ma20 = float(latest['ma20']) ma60 = float(latest['ma60']) if current_price > ma5 > ma20 > ma60: trend = 'up' elif current_price < ma5 < ma20 < ma60: trend = 'down' else: trend = 'sideways' # 布林带位置 boll_upper = float(latest['boll_upper']) boll_mid = float(latest['boll_mid']) boll_lower = float(latest['boll_lower']) if current_price >= boll_upper: boll_position = '上轨附近(超买)' elif current_price <= boll_lower: boll_position = '下轨附近(超卖)' elif current_price > boll_mid: boll_position = '中轨上方' else: boll_position = '中轨下方' return { 'ma5': ma5, 'ma20': ma20, 'ma60': ma60, 'trend': trend, 'macd_dif': float(latest['dif']), 'macd_dea': float(latest['dea']), 'macd': float(latest['macd']), 'rsi6': float(latest['rsi6']), 'rsi12': float(latest['rsi12']), 'rsi24': float(latest['rsi24']), 'kdj_k': float(latest['kdj_k']), 'kdj_d': float(latest['kdj_d']), 'kdj_j': float(latest['kdj_j']), 'boll_upper': boll_upper, 'boll_mid': boll_mid, 'boll_lower': boll_lower, 'boll_position': boll_position, 'vol_ma5': float(latest['vol_ma5']), 'volume_ratio': float(latest['成交量']) / float(latest['vol_ma5']) if latest['vol_ma5'] > 0 else 1.0 } except Exception as e: self.logger.error(f"计算技术指标失败 {stock_code}: {e}") return None def _get_technical_indicators_from_tushare(self, stock_code: str, period: str = 'daily') -> Optional[Dict]: """ 使用Tushare获取历史数据并计算技术指标 Args: stock_code: 股票代码(6位) period: 周期(daily/weekly/monthly) Returns: 技术指标数据 """ try: # 转换股票代码格式(Tushare格式:600519.SH, 000001.SZ) if stock_code.startswith('6'): ts_code = f"{stock_code}.SH" elif stock_code.startswith(('0', '3')): ts_code = f"{stock_code}.SZ" else: ts_code = stock_code # 计算日期范围 end_date = datetime.now().strftime('%Y%m%d') start_date = (datetime.now() - timedelta(days=400)).strftime('%Y%m%d') # 获取历史数据 df = self.ts_pro.daily( ts_code=ts_code, start_date=start_date, end_date=end_date ) if df is None or df.empty: self.logger.error(f"Tushare未返回 {stock_code} 的历史数据") return None # Tushare数据是从新到旧,需要反转 df = df.sort_values('trade_date', ascending=True).reset_index(drop=True) if len(df) < 60: self.logger.warning(f"Tushare历史数据不足 {stock_code}(仅{len(df)}条)") return None # 统一列名为AKShare格式(完整映射) df = df.rename(columns={ 'open': '开盘', 'high': '最高', 'low': '最低', 'close': '收盘', 'vol': '成交量', 'amount': '成交额', 'trade_date': '日期' }) # 如果没有关键列,尝试使用其他可能的列名 column_mapping = { '开盘': ['open', 'Open', 'OPEN'], '最高': ['high', 'High', 'HIGH'], '最低': ['low', 'Low', 'LOW'], '收盘': ['close', 'Close', 'CLOSE'], '成交量': ['vol', 'volume', 'Volume', 'VOLUME'], '成交额': ['amount', 'Amount', 'AMOUNT'] } for target_col, possible_cols in column_mapping.items(): if target_col not in df.columns: for col in possible_cols: if col in df.columns: df[target_col] = df[col] break # 确认必需的列存在 required_cols = ['开盘', '最高', '最低', '收盘', '成交量'] missing_cols = [col for col in required_cols if col not in df.columns] if missing_cols: self.logger.error(f"Tushare数据缺少列 {stock_code}: {missing_cols}") return None self.logger.info(f"✅ Tushare成功获取 {stock_code} 历史数据,共{len(df)}条") # 使用统一的计算方法 return self._calculate_all_indicators(df, stock_code) except Exception as e: self.logger.error(f"Tushare获取历史数据失败 {stock_code}: {type(e).__name__}: {str(e)}") import traceback self.logger.debug(traceback.format_exc()) return None def get_main_force_flow(self, stock_code: str, retry: int = 2) -> Optional[Dict]: """ 获取主力资金流向(带重试机制) Args: stock_code: 股票代码 retry: 重试次数(默认2次) Returns: 主力资金数据 """ import time for attempt in range(retry): try: # 获取个股资金流(新版AKShare API参数调整) try: df = ak.stock_individual_fund_flow_rank(market="今日") except TypeError: # 如果market参数也不支持,尝试无参数调用 try: df = ak.stock_individual_fund_flow_rank() except TypeError as te: self.logger.warning(f"AKShare API参数不兼容: {te}") return None stock_data = df[df['代码'] == stock_code] if stock_data.empty: self.logger.warning(f"未找到股票 {stock_code} 的资金流向数据") return None row = stock_data.iloc[0] # 主力净额 main_net = float(row.get('主力净流入-净额', 0)) / 10000 # 转换为万元 main_net_pct = float(row.get('主力净流入-净占比', 0)) # 判断主力动向 if main_net > 0 and main_net_pct > 5: trend = '大幅流入' elif main_net > 0: trend = '小幅流入' elif main_net < 0 and main_net_pct < -5: trend = '大幅流出' elif main_net < 0: trend = '小幅流出' else: trend = '观望' return { 'main_net': main_net, # 万元 'main_net_pct': main_net_pct, # 百分比 'super_net': float(row.get('超大单净流入-净额', 0)) / 10000, 'big_net': float(row.get('大单净流入-净额', 0)) / 10000, 'mid_net': float(row.get('中单净流入-净额', 0)) / 10000, 'small_net': float(row.get('小单净流入-净额', 0)) / 10000, 'trend': trend, 'data_source': 'akshare' } except Exception as e: if attempt < retry - 1: self.logger.warning(f"AKShare获取资金流向失败 {stock_code},第{attempt+1}次重试... 错误: {type(e).__name__}") time.sleep(1) # 等待1秒后重试 else: self.logger.warning(f"AKShare获取资金流向失败 {stock_code}(已重试{retry}次),尝试降级到Tushare") break # 降级到Tushare if self.ts_pro: return self._get_main_force_from_tushare(stock_code) else: self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 资金流向") return None def get_comprehensive_data(self, stock_code: str) -> Dict: """ 获取综合数据(实时行情+技术指标) 注意:已移除主力资金流向数据,因为该接口不稳定且AI决策不依赖此数据 Args: stock_code: 股票代码 Returns: 综合数据 """ result = {} # 实时行情 quote = self.get_realtime_quote(stock_code) if quote: result.update(quote) # 技术指标 indicators = self.get_technical_indicators(stock_code) if indicators: result.update(indicators) # 主力资金(已禁用 - 接口不稳定) # main_force = self.get_main_force_flow(stock_code) # if main_force: # result['main_force'] = main_force return result # ========== 技术指标计算方法 ========== def _calculate_macd(self, df: pd.DataFrame, fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame: """计算MACD指标""" ema_fast = df['收盘'].ewm(span=fast, adjust=False).mean() ema_slow = df['收盘'].ewm(span=slow, adjust=False).mean() df['dif'] = ema_fast - ema_slow df['dea'] = df['dif'].ewm(span=signal, adjust=False).mean() df['macd'] = (df['dif'] - df['dea']) * 2 return df def _calculate_rsi(self, df: pd.DataFrame, periods: list = [6, 12, 24]) -> pd.DataFrame: """计算RSI指标""" for period in periods: delta = df['收盘'].diff() gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() rs = gain / loss df[f'rsi{period}'] = 100 - (100 / (1 + rs)) return df def _calculate_kdj(self, df: pd.DataFrame, n: int = 9, m1: int = 3, m2: int = 3) -> pd.DataFrame: """计算KDJ指标""" low_list = df['最低'].rolling(window=n).min() high_list = df['最高'].rolling(window=n).max() rsv = (df['收盘'] - low_list) / (high_list - low_list) * 100 df['kdj_k'] = rsv.ewm(com=m1-1, adjust=False).mean() df['kdj_d'] = df['kdj_k'].ewm(com=m2-1, adjust=False).mean() df['kdj_j'] = 3 * df['kdj_k'] - 2 * df['kdj_d'] return df def _calculate_bollinger(self, df: pd.DataFrame, period: int = 20, std_num: int = 2) -> pd.DataFrame: """计算布林带""" df['boll_mid'] = df['收盘'].rolling(window=period).mean() std = df['收盘'].rolling(window=period).std() df['boll_upper'] = df['boll_mid'] + std_num * std df['boll_lower'] = df['boll_mid'] - std_num * std return df # ========== Tushare备用数据源方法 ========== def _get_realtime_quote_from_tushare(self, stock_code: str) -> Optional[Dict]: """ 从Tushare获取实时行情(备用数据源) 使用免费接口,无需积分 Args: stock_code: 股票代码 Returns: 实时行情数据 """ try: # 转换股票代码格式(Tushare格式:600519.SH) if stock_code.startswith('6'): ts_code = f"{stock_code}.SH" elif stock_code.startswith(('0', '3')): ts_code = f"{stock_code}.SZ" else: self.logger.warning(f"无法识别股票代码市场: {stock_code}") return None # 方法1: 尝试使用daily_basic(基础日线,无需积分) try: df = self.ts_pro.daily_basic(ts_code=ts_code, trade_date=datetime.now().strftime('%Y%m%d'), fields='ts_code,trade_date,close,turnover_rate,volume_ratio,pe,pb') if df.empty: # 获取最近交易日 end_date = datetime.now().strftime('%Y%m%d') df = self.ts_pro.daily_basic(ts_code=ts_code, end_date=end_date, fields='ts_code,trade_date,close,turnover_rate,volume_ratio,pe,pb') df = df.head(1) if not df.empty: row = df.iloc[0] # 获取日线数据补充价格信息 df_daily = self.ts_pro.daily(ts_code=ts_code, trade_date=row['trade_date'], fields='open,high,low,pre_close,change,pct_chg,vol,amount') if not df_daily.empty: daily_row = df_daily.iloc[0] # 获取股票名称 stock_basic = self.ts_pro.stock_basic(ts_code=ts_code, fields='name') stock_name = stock_basic.iloc[0]['name'] if not stock_basic.empty else 'N/A' self.logger.info(f"✅ Tushare降级成功(基础接口),获取到 {stock_code} 数据") return { 'code': stock_code, 'name': stock_name, 'current_price': float(row['close']), 'change_pct': float(daily_row.get('pct_chg', 0)), 'change_amount': float(daily_row.get('change', 0)), 'volume': float(daily_row.get('vol', 0)) * 100, 'amount': float(daily_row.get('amount', 0)) * 1000, 'high': float(daily_row.get('high', 0)), 'low': float(daily_row.get('low', 0)), 'open': float(daily_row.get('open', 0)), 'pre_close': float(daily_row.get('pre_close', 0)), 'turnover_rate': float(row.get('turnover_rate', 0)), 'volume_ratio': float(row.get('volume_ratio', 1.0)), 'update_time': row['trade_date'], 'data_source': 'tushare' } except Exception as e: self.logger.warning(f"Tushare基础接口失败: {str(e)[:100]}") # 方法2: 降级使用更基础的stock_basic+pro_bar try: # 获取股票名称 stock_basic = self.ts_pro.stock_basic(ts_code=ts_code, fields='name') stock_name = stock_basic.iloc[0]['name'] if not stock_basic.empty else 'N/A' # 使用pro_bar获取行情(社区版免费) import tushare as ts df = ts.pro_bar(ts_code=ts_code, adj='qfq', ma=[5, 20]) if df is not None and not df.empty: row = df.iloc[0] self.logger.info(f"✅ Tushare降级成功(pro_bar接口),获取到 {stock_code} 数据") return { 'code': stock_code, 'name': stock_name, 'current_price': float(row['close']), 'change_pct': float(row.get('pct_chg', 0)), 'change_amount': float(row.get('change', 0)), 'volume': float(row.get('vol', 0)) * 100, 'amount': float(row.get('amount', 0)) * 1000, 'high': float(row.get('high', 0)), 'low': float(row.get('low', 0)), 'open': float(row.get('open', 0)), 'pre_close': float(row.get('pre_close', 0)), 'turnover_rate': float(row.get('turnover_rate', 0)), 'volume_ratio': 1.0, 'update_time': row['trade_date'], 'data_source': 'tushare' } except Exception as e: self.logger.warning(f"Tushare pro_bar接口失败: {str(e)[:100]}") # 所有方法都失败 self.logger.error(f"Tushare所有接口都失败 {stock_code},可能是积分不足或网络问题") self.logger.info("💡 提示:访问 https://tushare.pro/user/token 查看积分和权限") return None except Exception as e: error_msg = str(e) if "权限" in error_msg or "积分" in error_msg: self.logger.error(f"Tushare权限不足 {stock_code}: 需要更多积分") self.logger.info("💡 获取积分方法:") self.logger.info(" 1. 完善个人信息 +100积分") self.logger.info(" 2. 每日签到 +1积分") self.logger.info(" 3. 参与社区互动") self.logger.info(" 详情访问: https://tushare.pro/document/1?doc_id=13") else: self.logger.error(f"Tushare获取失败 {stock_code}: {error_msg[:100]}") return None def _get_main_force_from_tushare(self, stock_code: str) -> Optional[Dict]: """ 从Tushare获取主力资金流向(备用数据源) 注意:资金流向接口需要较高积分 Args: stock_code: 股票代码 Returns: 主力资金数据 """ try: # 转换股票代码格式 if stock_code.startswith('6'): ts_code = f"{stock_code}.SH" elif stock_code.startswith(('0', '3')): ts_code = f"{stock_code}.SZ" else: return None # 尝试获取资金流向数据(需要120积分) today = datetime.now().strftime('%Y%m%d') df = self.ts_pro.moneyflow(ts_code=ts_code, start_date=today, end_date=today) if df.empty: # 获取最近一个交易日 df = self.ts_pro.moneyflow(ts_code=ts_code, end_date=today) df = df.head(1) if df.empty: self.logger.warning(f"Tushare未找到股票 {stock_code} 的资金流向数据") return None row = df.iloc[0] # 计算主力净额(大单+超大单) buy_lg_amount = float(row.get('buy_lg_amount', 0)) buy_elg_amount = float(row.get('buy_elg_amount', 0)) sell_lg_amount = float(row.get('sell_lg_amount', 0)) sell_elg_amount = float(row.get('sell_elg_amount', 0)) main_net = (buy_lg_amount + buy_elg_amount - sell_lg_amount - sell_elg_amount) / 10000 # 计算净占比 net_mf_amount = float(row.get('net_mf_amount', 0)) main_net_pct = (main_net / net_mf_amount * 100) if net_mf_amount != 0 else 0 # 判断主力动向 if main_net > 0 and main_net_pct > 5: trend = '大幅流入' elif main_net > 0: trend = '小幅流入' elif main_net < 0 and main_net_pct < -5: trend = '大幅流出' elif main_net < 0: trend = '小幅流出' else: trend = '观望' self.logger.info(f"✅ Tushare降级成功,获取到 {stock_code} 资金流向") return { 'main_net': main_net, 'main_net_pct': main_net_pct, 'super_net': (buy_elg_amount - sell_elg_amount) / 10000, 'big_net': (buy_lg_amount - sell_lg_amount) / 10000, 'mid_net': float(row.get('buy_md_amount', 0) - row.get('sell_md_amount', 0)) / 10000, 'small_net': float(row.get('buy_sm_amount', 0) - row.get('sell_sm_amount', 0)) / 10000, 'trend': trend } except Exception as e: error_msg = str(e) if "权限" in error_msg or "积分" in error_msg: self.logger.warning(f"⚠️ Tushare资金流向接口需要120积分,当前积分不足") self.logger.info("💡 获取积分方法:") self.logger.info(" 1. 完善个人信息 +100积分") self.logger.info(" 2. 每日签到累积 +30积分(30天)") self.logger.info(" 3. 参与社区互动获得积分") self.logger.info(" 详情: https://tushare.pro/document/1?doc_id=13") self.logger.info(" 智能盯盘会继续运行,仅缺少资金流向数据") else: self.logger.error(f"Tushare获取资金流向失败 {stock_code}: {error_msg[:100]}") return None if __name__ == '__main__': # 测试代码 logging.basicConfig(level=logging.INFO) fetcher = SmartMonitorDataFetcher() # 测试贵州茅台 print("测试获取贵州茅台(600519)数据...") data = fetcher.get_comprehensive_data('600519') if data: print("\n实时行情:") print(f" 当前价: {data.get('current_price')} 元") print(f" 涨跌幅: {data.get('change_pct')}%") print("\n技术指标:") print(f" MA5: {data.get('ma5', 0):.2f}") print(f" MA20: {data.get('ma20', 0):.2f}") print(f" MACD: {data.get('macd', 0):.4f}") print(f" RSI(6): {data.get('rsi6', 0):.2f}") if 'main_force' in data: print("\n主力资金:") print(f" 主力净额: {data['main_force']['main_net']:.2f}万") print(f" 主力动向: {data['main_force']['trend']}")