增加TDX数据源,增加交易时段盯盘设置
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
"""
|
||||
智能盯盘 - 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("获取数据失败")
|
||||
|
||||
Reference in New Issue
Block a user