Files
freedakgmail 40ce519188 fix: 修复6个数据和代码问题
1. 修复 _generate_plain_summary position key 不匹配 (20d→d20, pct→range_pct)
2. 修复 mairui_api.py 日期解析不一致及 float(None) 崩溃风险
3. 修复 fund_flow_analyzer.py 麦蕊回退数据 close_price=0 导致量价背离误判
4. 修复 db_get_fund_flow_history 缺少完整字段和日期过滤
5. 修复 scheduler.py 连接池泄漏 (conn.close() → put_db(conn))
6. 修复多处北交所股票代码映射缺失 (8/9开头→bj)
2026-07-22 07:20:44 +08:00

315 lines
10 KiB
Python

"""
麦蕊智数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': []}