2b5a32ca1e
新增模块: - fund_flow_analyzer.py: 主力资金流向分析(P0, ±20) - market_sentiment.py: 市场情绪指标(P1, ±10) - external_factors.py: 北向资金/美股/大宗商品/汇率(P2-P4,P7) - news_analyzer.py: 公告/并购/政策面LLM分析(P5-P6) - score_engine.py: 综合评分引擎,整合技术面+外部因素 路由更新: - analysis.py: deep_analyze接入综合评分,根据最终评级修正买卖建议 - market.py: 新增4个外部因素API端点 - trades.py: 交易路由更新 算法文档重构: - 章节重排: 技术面(二三)→外部因素(四)→买卖决策(五)→数据源(六)→性能(七) - 架构图更新为五层,标注章节对应 - 5.1/5.2标注纯技术面,5.3整合外部因素修正推荐
311 lines
10 KiB
Python
311 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)) + float(item.get('zmbtdcje', 0))
|
|
main_sell = float(item.get('zmsddcje', 0)) + float(item.get('zmstdcje', 0))
|
|
main_net = main_buy - main_sell
|
|
|
|
flow_data.append({
|
|
'date': datetime.fromtimestamp(item.get('t', 0)).strftime('%Y-%m-%d') if item.get('t') else '',
|
|
'main_net_inflow': main_net,
|
|
'super_buy': float(item.get('zmbtdcje', 0)),
|
|
'super_sell': float(item.get('zmstdcje', 0)),
|
|
'big_buy': float(item.get('zmbddcje', 0)),
|
|
'big_sell': float(item.get('zmsddcje', 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': []}
|