Files
aiagents-stock/smart_monitor_tdx_data.py
T

441 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
智能盯盘 - TDX数据获取模块
使用TDX股票数据API接口获取实时行情和技术指标
"""
import logging
import requests
import pandas as pd
from typing import Dict, Optional
from datetime import datetime, timedelta
class SmartMonitorTDXDataFetcher:
"""TDX数据获取器"""
def __init__(self, base_url: str = "http://192.168.1.222:8181"):
"""
初始化TDX数据获取器
Args:
base_url: TDX API基础地址
"""
self.logger = logging.getLogger(__name__)
self.base_url = base_url.rstrip('/')
self.timeout = 10 # 请求超时时间(秒)
self.logger.info(f"TDX数据源初始化成功,接口地址: {self.base_url}")
def get_realtime_quote(self, stock_code: str) -> Optional[Dict]:
"""
获取实时行情
Args:
stock_code: 股票代码(如:600519
Returns:
实时行情数据
"""
try:
url = f"{self.base_url}/api/quote"
params = {'code': stock_code}
response = requests.get(url, params=params, timeout=self.timeout)
result = response.json()
if result['code'] != 0:
self.logger.error(f"TDX获取行情失败: {result.get('message')}")
return None
data_list = result.get('data', [])
if not data_list:
self.logger.warning(f"TDX未返回股票 {stock_code} 的行情数据")
return None
# 获取第一条数据
quote_data = data_list[0]
k_data = quote_data.get('K', {})
# 价格单位转换:厘 -> 元(1元 = 1000厘)
current_price = k_data.get('Close', 0) / 1000
pre_close = k_data.get('Last', 0) / 1000
open_price = k_data.get('Open', 0) / 1000
high_price = k_data.get('High', 0) / 1000
low_price = k_data.get('Low', 0) / 1000
# 成交量单位:手(已是手,无需转换)
volume = quote_data.get('TotalHand', 0)
# 成交额单位转换:厘 -> 元
amount = quote_data.get('Amount', 0) / 1000
# 计算涨跌幅
change_amount = current_price - pre_close
change_pct = (change_amount / pre_close * 100) if pre_close > 0 else 0
# 计算换手率(需要流通股本,TDX不提供,暂时设为0)
turnover_rate = 0.0
# 计算量比(现量/均量,这里用总手数/平均手数估算)
vol_ma5 = volume / 1.2 # 简化估算
volume_ratio = volume / vol_ma5 if vol_ma5 > 0 else 1.0
# 获取股票名称(需要调用搜索接口)
stock_name = self._get_stock_name(stock_code)
self.logger.info(f"✅ TDX成功获取 {stock_code} ({stock_name}) 实时行情")
return {
'code': stock_code,
'name': stock_name,
'current_price': current_price,
'change_pct': change_pct,
'change_amount': change_amount,
'volume': volume, # 手
'amount': amount, # 元
'high': high_price,
'low': low_price,
'open': open_price,
'pre_close': pre_close,
'turnover_rate': turnover_rate,
'volume_ratio': volume_ratio,
'update_time': datetime.fromtimestamp(int(quote_data.get('ServerTime', 0))).strftime('%Y-%m-%d %H:%M:%S'),
'data_source': 'tdx'
}
except requests.exceptions.Timeout:
self.logger.error(f"TDX请求超时 {stock_code}")
return None
except requests.exceptions.ConnectionError:
self.logger.error(f"TDX连接失败,请检查接口地址: {self.base_url}")
return None
except Exception as e:
self.logger.error(f"TDX获取行情失败 {stock_code}: {type(e).__name__}: {str(e)}")
return None
def _get_stock_name(self, stock_code: str) -> str:
"""
获取股票名称
Args:
stock_code: 股票代码
Returns:
股票名称
"""
try:
url = f"{self.base_url}/api/search"
params = {'keyword': stock_code}
response = requests.get(url, params=params, timeout=self.timeout)
result = response.json()
if result['code'] == 0:
data_list = result.get('data', [])
for item in data_list:
if item.get('code') == stock_code:
return item.get('name', 'N/A')
return 'N/A'
except Exception as e:
self.logger.warning(f"获取股票名称失败 {stock_code}: {e}")
return 'N/A'
def get_kline_data(self, stock_code: str, kline_type: str = 'day', limit: int = 200) -> Optional[pd.DataFrame]:
"""
获取K线数据
Args:
stock_code: 股票代码
kline_type: K线类型(minute1/minute5/minute15/minute30/hour/day/week/month
limit: 返回条数(最多800
Returns:
K线数据DataFrame
"""
try:
url = f"{self.base_url}/api/kline"
params = {
'code': stock_code,
'type': kline_type
}
response = requests.get(url, params=params, timeout=self.timeout)
result = response.json()
if result['code'] != 0:
self.logger.error(f"TDX获取K线失败: {result.get('message')}")
return None
kline_list = result.get('data', {}).get('List', [])
if not kline_list:
self.logger.warning(f"TDX未返回股票 {stock_code} 的K线数据")
return None
# 转换为DataFrame
rows = []
for item in kline_list:
rows.append({
'日期': item.get('Time', '').split('T')[0], # 只取日期部分
'开盘': item.get('Open', 0) / 1000, # 厘转元
'收盘': item.get('Close', 0) / 1000,
'最高': item.get('High', 0) / 1000,
'最低': item.get('Low', 0) / 1000,
'成交量': item.get('Volume', 0), # 手
'成交额': item.get('Amount', 0) / 1000, # 厘转元
})
df = pd.DataFrame(rows)
# TDX返回的数据是倒序(最新的在前),需要反转
df = df.iloc[::-1].reset_index(drop=True)
# 只保留最近limit条
if len(df) > limit:
df = df.tail(limit).reset_index(drop=True)
# 转换日期格式
df['日期'] = pd.to_datetime(df['日期'])
self.logger.info(f"✅ TDX成功获取 {stock_code} K线数据,共{len(df)}条")
return df
except Exception as e:
self.logger.error(f"TDX获取K线失败 {stock_code}: {type(e).__name__}: {str(e)}")
return None
def get_technical_indicators(self, stock_code: str, period: str = 'daily') -> Optional[Dict]:
"""
计算技术指标
Args:
stock_code: 股票代码
period: 周期(daily/weekly/monthly
Returns:
技术指标数据
"""
try:
# 映射周期类型
kline_type_map = {
'daily': 'day',
'weekly': 'week',
'monthly': 'month'
}
kline_type = kline_type_map.get(period, 'day')
# 获取K线数据(需要足够的数据计算指标,至少200条)
df = self.get_kline_data(stock_code, kline_type=kline_type, limit=200)
if df is None or df.empty or len(df) < 60:
self.logger.warning(f"股票 {stock_code} K线数据不足,无法计算技术指标")
return None
# 计算技术指标
return self._calculate_all_indicators(df, stock_code)
except Exception as e:
self.logger.error(f"TDX计算技术指标失败 {stock_code}: {e}")
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}")
import traceback
self.logger.debug(traceback.format_exc())
return None
def get_comprehensive_data(self, stock_code: str) -> Dict:
"""
获取综合数据(实时行情+技术指标)
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)
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
if __name__ == '__main__':
# 测试代码
logging.basicConfig(level=logging.INFO)
# 使用默认地址测试
fetcher = SmartMonitorTDXDataFetcher(base_url="http://192.168.1.222:8181")
# 测试平安银行(000001)
print("测试获取平安银行(000001)数据...")
data = fetcher.get_comprehensive_data('000001')
if data:
print("\n实时行情:")
print(f" 股票名称: {data.get('name')}")
print(f" 当前价: {data.get('current_price')} 元")
print(f" 涨跌幅: {data.get('change_pct')}%")
print(f" 数据源: {data.get('data_source')}")
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}")
print(f" 趋势: {data.get('trend')}")
else:
print("获取数据失败")