""" 麦蕊智数API服务模块 API文档: https://api.mairuiapi.com Licence: AEB5CE22-155A-4535-AE01-610920EB2751 """ import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry import time from datetime import datetime, timedelta # API配置 from config import Config LICENCE = Config.MAIRUI_LICENCE or "5352ED2F-94E5-4E96-8B7F-B57BA75284E3" BASE_URL = "https://api.mairuiapi.com" # 缓存配置 _cache = { 'realtime_all': {'data': None, 'timestamp': None, 'ttl': 60}, # 全市场实时数据缓存60秒 } # 全局连接池 Session(TCP连接复用,大幅减少连接建立开销) _session = None def _get_session(): """获取全局复用的 requests.Session(带连接池和自动重试)""" global _session if _session is None: _session = requests.Session() retry_strategy = Retry( total=2, # 最多重试2次 backoff_factor=0.3, # 重试间隔: 0.3s, 0.6s status_forcelist=[429, 500, 502, 503, 504], ) adapter = HTTPAdapter( max_retries=retry_strategy, pool_connections=20, # 连接池大小 pool_maxsize=20, # 最大连接数 ) _session.mount("https://", adapter) _session.mount("http://", adapter) return _session def _request(url, timeout=10): """发送API请求(复用连接池)""" try: session = _get_session() resp = session.get(url, timeout=timeout) if resp.status_code == 200: return resp.json() else: print(f"API请求失败: {url}, status={resp.status_code}") return None except Exception as e: print(f"API请求异常: {url}, error={e}") return None # ========== 实时交易数据 ========== def get_realtime_price(stock_code): """ 获取单只股票实时交易数据(券商数据源) API: https://api.mairuiapi.com/hsrl/ssjy/{stock_code}/{licence} """ url = f"{BASE_URL}/hsrl/ssjy/{stock_code}/{LICENCE}" data = _request(url) if data: return { 'success': True, 'data': { 'code': stock_code, 'price': float(data.get('p', 0)), 'change': float(data.get('pc', 0)), 'open': float(data.get('o', 0)), 'high': float(data.get('h', 0)), 'low': float(data.get('l', 0)), 'volume': float(data.get('v', 0)), 'amount': float(data.get('cje', 0)), 'pe': float(data.get('pe', 0)) if data.get('pe') else None, 'pb': float(data.get('sjl', 0)) if data.get('sjl') else None, 'turnover': float(data.get('hs', 0)), 'total_market_cap': float(data.get('sz', 0)), 'circulating_market_cap': float(data.get('lt', 0)), 'update_time': data.get('t', ''), } } return {'success': False, 'error': '获取失败'} def get_realtime_prices_batch(stock_codes): """ 批量获取实时交易数据(最多20只) API: https://api.mairuiapi.com/hsrl/ssjy_more/{licence}?stock_codes=xxx,xxx """ if not stock_codes: return {} # 每次最多20只 codes_str = ','.join(stock_codes[:20]) url = f"{BASE_URL}/hsrl/ssjy_more/{LICENCE}?stock_codes={codes_str}" data = _request(url) results = {} if data and isinstance(data, list): for i, item in enumerate(data): if i < len(stock_codes): code = stock_codes[i] results[code] = { 'code': code, 'price': float(item.get('p', 0)), 'change': float(item.get('pc', 0)), 'pe': float(item.get('pe', 0)) if item.get('pe') else None, 'pb': float(item.get('pb_ratio', 0)) if item.get('pb_ratio') else None, } return results # ========== K线数据 ========== def get_kline(stock_code, period='d', days=30, adjust='f'): """ 获取K线数据 API: https://api.mairuiapi.com/hsstock/history/{code}.{market}/{period}/{adjust}/{licence} 参数: - period: 5/15/30/60/d/w/m/y (分钟/日/周/月/年) - adjust: n(不复权)/f(前复权)/b(后复权) """ # 确定市场 if stock_code.startswith(('0', '3')): market = 'SZ' elif stock_code.startswith(('8', '9')): market = 'BJ' else: market = 'SH' # 计算日期范围 end_date = datetime.now().strftime('%Y%m%d') start_date = (datetime.now() - timedelta(days=days)).strftime('%Y%m%d') url = f"{BASE_URL}/hsstock/history/{stock_code}.{market}/{period}/{adjust}/{LICENCE}?st={start_date}&et={end_date}" data = _request(url) if data and isinstance(data, list): kline_data = [] for item in data: # 处理日期格式,去掉时间部分 date_str = item.get('t', '') if date_str and ' ' in date_str: date_str = date_str.split(' ')[0] # 只保留日期部分 kline_data.append({ 'date': date_str, 'open': float(item.get('o', 0)), 'high': float(item.get('h', 0)), 'low': float(item.get('l', 0)), 'close': float(item.get('c', 0)), 'volume': float(item.get('v', 0)), 'amount': float(item.get('a', 0)), }) return {'success': True, 'data': kline_data} return {'success': True, 'data': []} # ========== 公司信息 ========== def get_company_info(stock_code): """ 获取公司简介 API: https://api.mairuiapi.com/hscp/gsjj/{stock_code}/{licence} """ url = f"{BASE_URL}/hscp/gsjj/{stock_code}/{LICENCE}" data = _request(url) if data: return { 'success': True, 'data': { 'name': data.get('name', ''), 'industry': data.get('idea', '').split(',')[0] if data.get('idea') else '', 'list_date': data.get('ldate', ''), 'issue_price': data.get('sprice', ''), 'description': data.get('desc', ''), 'business_scope': data.get('bscope', ''), } } return {'success': False, 'error': '获取失败'} # ========== 财务指标 ========== def get_financial_indicators(stock_code): """ 获取财务指标 API: https://api.mairuiapi.com/hscp/cwzb/{stock_code}/{licence} """ url = f"{BASE_URL}/hscp/cwzb/{stock_code}/{LICENCE}" data = _request(url) if data and isinstance(data, list) and len(data) > 0: latest = data[0] # 最新一期 return { 'success': True, 'data': { 'report_date': latest.get('date', ''), 'eps': _parse_float(latest.get('tbmg')), # 摊薄每股收益 'bps': _parse_float(latest.get('mgjz')), # 每股净资产 'roe': _parse_float(latest.get('jzsy')), # 净资产收益率 'gross_margin': _parse_float(latest.get('xsml')), # 销售毛利率 'net_margin': _parse_float(latest.get('xsjl')), # 销售净利率 'revenue_yoy': _parse_float(latest.get('zysr')), # 主营业务收入增长率 'profit_yoy': _parse_float(latest.get('jlzz')), # 净利润增长率 'debt_ratio': _parse_float(latest.get('zcfzl')), # 资产负债率 'current_ratio': _parse_float(latest.get('ldbl')), # 流动比率 } } return {'success': False, 'error': '获取失败'} def _parse_float(value): """解析浮点数""" if value is None: return None try: return float(value) except: return None # ========== 资金流向 ========== def get_fund_flow(stock_code, days=3): """ 获取资金流向数据 API: https://api.mairuiapi.com/hsstock/history/transaction/{stock_code}/{licence}?lt={days} """ url = f"{BASE_URL}/hsstock/history/transaction/{stock_code}/{LICENCE}?lt={days}" data = _request(url) if data and isinstance(data, list): flow_data = [] for item in data: # 计算主力净流入 = 主买大单+主买特大单 - 主卖大单-主卖特大单 main_buy = float(item.get('zmbddcje', 0) or 0) + float(item.get('zmbtdcje', 0) or 0) main_sell = float(item.get('zmsddcje', 0) or 0) + float(item.get('zmstdcje', 0) or 0) main_net = main_buy - main_sell # 日期解析:与 fund_flow_analyzer.py 保持一致,用字符串截取 t_str = str(item.get('t', '')) date_str = t_str[:10] if t_str else '' flow_data.append({ 'date': date_str, 'main_net_inflow': main_net, 'super_buy': float(item.get('zmbtdcje', 0) or 0), 'super_sell': float(item.get('zmstdcje', 0) or 0), 'big_buy': float(item.get('zmbddcje', 0) or 0), 'big_sell': float(item.get('zmsddcje', 0) or 0), }) return {'success': True, 'data': flow_data} return {'success': True, 'data': []} # ========== 股票列表 ========== def get_stock_list(): """ 获取股票列表 API: https://api.mairuiapi.com/hslt/list/{licence} """ url = f"{BASE_URL}/hslt/list/{LICENCE}" data = _request(url, timeout=30) if data and isinstance(data, list): return { 'success': True, 'data': [{'code': item.get('dm'), 'name': item.get('mc'), 'market': item.get('jys')} for item in data] } return {'success': False, 'error': '获取失败'} # ========== 涨停股池 ========== def get_limit_up_stocks(date=None): """ 获取涨停股池 API: https://api.mairuiapi.com/hslt/ztgc/{date}/{licence} """ if date is None: date = datetime.now().strftime('%Y-%m-%d') url = f"{BASE_URL}/hslt/ztgc/{date}/{LICENCE}" data = _request(url) if data and isinstance(data, list): return { 'success': True, 'data': [{ 'code': item.get('dm'), 'name': item.get('mc'), 'price': float(item.get('p', 0)), 'change': float(item.get('zf', 0)), 'amount': float(item.get('cje', 0)), 'limit_count': int(item.get('lbc', 0)), 'first_limit_time': item.get('fbt', ''), 'industry': item.get('hy', ''), } for item in data] } return {'success': True, 'data': []}