""" 股票数据服务 - 获取、缓存、分析 """ import pandas as pd import numpy as np from datetime import datetime, timedelta import traceback import json import os from config import Config # ========== 股票名称缓存 ========== _stock_name_cache = {} def _load_stock_name_cache(): """从本地文件加载股票名称缓存""" global _stock_name_cache try: if os.path.exists(Config.STOCK_NAME_CACHE_FILE): with open(Config.STOCK_NAME_CACHE_FILE, 'r', encoding='utf-8') as f: _stock_name_cache = json.load(f) print(f"加载股票名称缓存:{len(_stock_name_cache)}条") except Exception as e: print(f"加载股票名称缓存失败: {e}") def _save_stock_name_cache(): """保存股票名称缓存到本地""" try: with open(Config.STOCK_NAME_CACHE_FILE, 'w', encoding='utf-8') as f: json.dump(_stock_name_cache, f, ensure_ascii=False, indent=2) except Exception as e: print(f"保存股票名称缓存失败: {e}") def get_stock_name(stock_code): """获取股票名称 — 使用腾讯财经API""" global _stock_name_cache if stock_code in _stock_name_cache: return _stock_name_cache[stock_code] # 腾讯财经API获取股票名称 try: import requests as _req tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code _r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5, headers={'Referer': 'https://finance.qq.com'}) if _r.status_code == 200 and '\"' in _r.text: _fields = _r.text.split('\"')[1].split('~') if len(_fields) > 2 and _fields[1]: _stock_name_cache[stock_code] = _fields[1] _save_stock_name_cache() return _fields[1] except Exception as e: print(f"获取股票名称失败(腾讯): {e}") return None # ========== 股票数据缓存 ========== def _get_cache_file_path(stock_code): """获取缓存文件路径""" return os.path.join(Config.STOCK_DATA_CACHE_DIR, f'{stock_code}.json') def load_cached_data(stock_code): """加载缓存的股票数据""" cache_file = _get_cache_file_path(stock_code) if os.path.exists(cache_file): try: with open(cache_file, 'r', encoding='utf-8') as f: data = json.load(f) df = pd.DataFrame(data['records']) if not df.empty and '日期' in df.columns: df['日期'] = pd.to_datetime(df['日期']) return df, data.get('stock_name'), data.get('last_update') except Exception as e: print(f"加载缓存数据失败: {e}") return None, None, None def save_cached_data(stock_code, df, stock_name): """保存股票数据到缓存""" cache_file = _get_cache_file_path(stock_code) try: df_copy = df.copy() df_copy['日期'] = df_copy['日期'].dt.strftime('%Y-%m-%d') records = df_copy.to_dict('records') data = { 'stock_code': stock_code, 'stock_name': stock_name, 'last_update': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'records': records } with open(cache_file, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) print(f"已保存 {stock_code} 数据,共 {len(records)} 条") except Exception as e: print(f"保存缓存数据失败: {e}") # ========== 获取股票资金流向数据 ========== def get_stock_fund_flow(stock_code, start_date, end_date, force_refresh=False): """ 获取股票资金流向数据(支持缓存,增量获取) force_refresh: 强制刷新缓存 返回: (DataFrame, stock_name, error_msg) """ try: # 判断市场 if stock_code.startswith('6'): market = 'sh' elif stock_code.startswith('0') or stock_code.startswith('3'): market = 'sz' else: return None, None, "无法识别股票代码所属市场" stock_name = get_stock_name(stock_code) start = pd.to_datetime(start_date) end = pd.to_datetime(end_date) # 加载缓存 cached_df, cached_name, last_update = load_cached_data(stock_code) need_fetch = force_refresh new_data_df = None if cached_df is not None and not cached_df.empty: # 如果缓存是今天的,直接使用 if last_update: try: update_date = pd.to_datetime(last_update.split()[0]) today = pd.to_datetime(datetime.now().strftime('%Y-%m-%d')) if update_date >= today and not force_refresh: # 今天已更新,直接使用缓存 df = cached_df[(cached_df['日期'] >= start) & (cached_df['日期'] <= end)] df = df.sort_values('日期').reset_index(drop=True) return df, cached_name or stock_name, None except: pass cached_max_date = cached_df['日期'].max() today = pd.to_datetime(datetime.now().strftime('%Y-%m-%d')) # 如果缓存数据不超过2天,直接使用(优化分析速度) if cached_max_date >= today - timedelta(days=2) and not force_refresh: if cached_df['日期'].min() <= start: need_fetch = False new_data_df = cached_df print(f"使用缓存数据: {stock_code}, 最新日期: {cached_max_date.strftime('%Y-%m-%d')}") if stock_name is None and cached_name: stock_name = cached_name if need_fetch: # 东方财富资金流向API已不可用(腾讯云网络限制),使用缓存数据 print(f"资金流向API不可用,使用缓存: {stock_code}") if cached_df is not None: new_data_df = cached_df else: return None, None, "资金流向API不可用(东方财富已封锁),且无缓存数据" if new_data_df is None or new_data_df.empty: # 最后尝试使用缓存数据(即使不在日期范围内) if cached_df is not None and not cached_df.empty: print(f"使用全部缓存数据: {stock_code}") df = cached_df.sort_values('日期').reset_index(drop=True) return df, cached_name or stock_name, None return None, None, "无法获取数据" # 筛选日期范围 df = new_data_df[(new_data_df['日期'] >= start) & (new_data_df['日期'] <= end)] # 如果筛选后为空,使用全部数据 if df.empty and not new_data_df.empty: print(f"日期范围无数据,使用全部缓存: {stock_code}") df = new_data_df df = df.sort_values('日期').reset_index(drop=True) return df, stock_name, None except Exception as e: traceback.print_exc() return None, None, f"获取数据失败: {str(e)}" # ========== 分析股票数据 ========== def analyze_fund_flow_impact(df): """分析资金流向对股价的影响(含成交量分析)""" if df is None or df.empty: return None df = df.sort_values('日期').reset_index(drop=True) threshold = 2.0 if '超大单净流入-净占比' not in df.columns: return None df['超大单净流入-净占比'] = df['超大单净流入-净占比'].fillna(0) df['主力净流入-净占比'] = df['主力净流入-净占比'].fillna(0) df['超大单流向'] = df['超大单净流入-净占比'].apply( lambda x: '大额流入' if x >= threshold else ('大额流出' if x <= -threshold else '普通') ) df['主力流向'] = df['主力净流入-净占比'].apply( lambda x: '大额流入' if x >= threshold else ('大额流出' if x <= -threshold else '普通') ) # 计算价格位置(改为60日) latest = df.iloc[-1] lookback = 60 # 从20日改为60日 try: actual_lookback = min(len(df), lookback) if actual_lookback >= 5: # 至少需要5天数据 recent = df.tail(actual_lookback) high = float(recent['收盘价'].max() or 0) low = float(recent['收盘价'].min() or 0) current_price = float(latest.get('收盘价') or 0) price_position = (current_price - low) / (high - low) * 100 if high != low else 50 else: price_position = 50 except: price_position = 50 # 成交量分析(基于主力净流入-净额作为成交额指标) volume_ratio = 1.0 # 默认值 volume_trend = '普通' try: if '主力净流入-净额' in df.columns and len(df) >= 10: # 使用主力净流入绝对值作为活跃度指标 df['活跃度'] = df['主力净流入-净额'].abs() recent_5 = df.tail(5)['活跃度'].mean() recent_20 = df.tail(min(20, len(df)))['活跃度'].mean() if recent_20 > 0: volume_ratio = recent_5 / recent_20 if volume_ratio >= 1.5: volume_trend = '放量' elif volume_ratio <= 0.5: volume_trend = '缩量' else: volume_trend = '正常' except: pass # 计算均线MA5和MA20 ma5 = 0 ma20 = 0 try: if '收盘价' in df.columns and len(df) >= 5: ma5 = df.tail(5)['收盘价'].mean() if '收盘价' in df.columns and len(df) >= 20: ma20 = df.tail(20)['收盘价'].mean() except: pass # 处理日期格式(可能是datetime或字符串) def format_date(d): if hasattr(d, 'strftime'): return d.strftime('%Y-%m-%d') return str(d)[:10] if d else '' def safe_float(val, default=0): try: return float(val) if val is not None else default except: return default return { '最新数据': { '日期': format_date(latest['日期']), '收盘价': safe_float(latest.get('收盘价')), '涨跌幅': safe_float(latest.get('涨跌幅')), '价格位置': safe_float(price_position), '超大单净流入占比': safe_float(latest.get('超大单净流入-净占比')), '主力净流入占比': safe_float(latest.get('主力净流入-净占比')), '超大单流向': latest.get('超大单流向', '普通'), '主力流向': latest.get('主力流向', '普通'), '成交量比': safe_float(volume_ratio, 1.0), '量能趋势': volume_trend, 'MA5': safe_float(ma5), 'MA20': safe_float(ma20) }, '数据概览': { '总交易日数': len(df), '计算周期': min(len(df), lookback), '日期范围': { '开始': format_date(df['日期'].min()), '结束': format_date(df['日期'].max()) } } } # ========== 实时价格 ========== def get_realtime_price(stock_code): """获取实时价格(使用mairuiapi,更稳定)""" try: from services.mairui_api import get_realtime_price as mairui_get_price result = mairui_get_price(stock_code) if result['success']: return result except Exception as e: print(f"mairuiapi获取实时价格失败({stock_code}): {e}") # 备用方案2:使用腾讯财经API(腾讯云可用) try: import requests as _req tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code _r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5, headers={'Referer': 'https://finance.qq.com'}) if _r.status_code == 200 and '\"' in _r.text: _fields = _r.text.split('\"')[1].split('~') if len(_fields) > 35 and _fields[3]: return { 'success': True, 'data': { 'code': stock_code, 'name': _fields[1], 'price': float(_fields[3]), 'change': float(_fields[32]) if _fields[32] else 0, } } except Exception as e: print(f"腾讯财经备用方案失败({stock_code}): {e}") return {'success': False, 'error': '获取失败'} def get_realtime_prices_batch(stock_codes): """批量获取实时价格""" try: from services.mairui_api import get_realtime_prices_batch as mairui_batch return mairui_batch(stock_codes) except Exception as e: print(f"mairuiapi批量获取失败: {e}") return {} # ========== 热门股票 ========== def get_hot_stocks(limit=100): """获取热门股票 — 东方财富API已不可用,返回空""" # stock_hot_rank_em 为东方财富API,已在腾讯云被封锁 return [] # 初始化时加载缓存 _load_stock_name_cache()