Add A share to display

This commit is contained in:
Porter
2025-05-25 14:37:47 +08:00
parent b4aef0b728
commit 3f16a5e6ce
16 changed files with 1718 additions and 215 deletions
+232 -50
View File
@@ -13,11 +13,13 @@ import time
import traceback
from pytz import timezone
import talib.abstract as ta
import numpy as np
# 添加父目录到系统路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ChanLun import ChanLun
from ChanEnum import Chan_BI_DIR, Chan_SEG_DIR, Chan_KLC_FX, Chan_FX_TYPE
from cn_stock_data import ChinaStockData
# 添加买卖点枚举类型
class TRADE_POINT_TYPE:
@@ -35,6 +37,9 @@ exchange = ccxt.binance({
'enableRateLimit': True,
})
# 初始化A股数据获取器
china_stock = ChinaStockData()
# 时间周期映射
TIMEFRAMES = {
'1m': '1分钟',
@@ -54,8 +59,32 @@ SYMBOLS = [
'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT'
]
# A股热门股票
A_STOCK_SYMBOLS = china_stock.get_popular_stocks()
def detect_symbol_type(symbol):
"""检测交易对类型:crypto 或 a_stock"""
if '/' in symbol and 'USDT' in symbol:
return 'crypto'
elif len(symbol) == 6 and symbol.isdigit():
return 'a_stock'
else:
return 'unknown'
def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
"""获取K线数据,支持分页加载确保获取指定时间范围内的所有数据"""
"""获取K线数据,支持加密货币和A股"""
symbol_type = detect_symbol_type(symbol)
if symbol_type == 'crypto':
return get_crypto_kl_data(symbol, timeframe, limit, start_time, end_time)
elif symbol_type == 'a_stock':
return get_a_stock_kl_data(symbol, timeframe, limit, start_time, end_time)
else:
print(f"未知的交易对类型: {symbol}")
return None
def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
"""获取加密货币K线数据,支持分页加载确保获取指定时间范围内的所有数据"""
try:
# 初始化参数
since = None
@@ -73,42 +102,73 @@ def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
except ValueError:
print(f"无效的结束时间: {end_time}")
# 根据时间周期调整每次请求的数据量
batch_size = 1000 # 默认批次大小
if timeframe in ['1m', '3m', '5m']:
batch_size = 500 # 分钟级数据减少批次大小
elif timeframe in ['15m', '30m', '1h']:
batch_size = 1000
else:
batch_size = 1500 # 日线及以上可以获取更多
# 初始化存储所有K线数据的列表
all_ohlcv = []
# 初始化当前查询的开始时间
current_since = since
# 添加请求计数和最大限制
request_count = 0
max_requests = 50 # 最大请求次数,防止无限循环
print(f"开始分批获取加密货币数据: {symbol}, {timeframe}")
# 分页加载数据
while True:
print(f"获取数据: {symbol}, {timeframe}, limit={limit}, since={current_since}")
while request_count < max_requests:
request_count += 1
# 获取当前页的数据
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=limit)
print(f"批次 {request_count}: 获取数据 since={current_since}, limit={batch_size}")
# 如果没有获取到数据,结束循环
if not ohlcv or len(ohlcv) == 0:
break
try:
# 获取当前页的数据
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size)
# 将获取到数据添加到总列表中
all_ohlcv.extend(ohlcv)
# 获取最后一条数据的时间戳
last_timestamp = ohlcv[-1][0]
# 如果已达到结束时间,结束循环
if until and last_timestamp >= until:
break
# 如果没有获取到数据,结束循环
if not ohlcv or len(ohlcv) == 0:
print(f"批次 {request_count}: 未获取到数据,结束")
break
# 将获取到的数据添加到总列表中
all_ohlcv.extend(ohlcv)
print(f"批次 {request_count}: 获取到 {len(ohlcv)} 条记录")
# 如果获取的数据条数小于限制数,说明已经获取完所有数据
if len(ohlcv) < limit:
break
# 获取最后一条数据的时间戳
last_timestamp = ohlcv[-1][0]
# 更新下一页的开始时间(加1毫秒避免重复)
current_since = last_timestamp + 1
# 如果已达到结束时间,结束循环
if until and last_timestamp >= until:
print(f"批次 {request_count}: 已达到结束时间,结束")
break
# 如果获取的数据条数小于限制数,说明已经获取完所有数据
if len(ohlcv) < batch_size:
print(f"批次 {request_count}: 数据不足批次大小,已获取完所有数据")
break
# 更新下一页的开始时间(加1毫秒避免重复)
current_since = last_timestamp + 1
except Exception as e:
print(f"批次 {request_count} 获取失败: {e}")
# 如果单个批次失败,继续尝试下一个批次
if current_since:
# 尝试增加时间跳过可能的问题时间点
current_since += 60000 # 跳过1分钟
else:
break
# 防止API请求过于频繁
time.sleep(0.5) # 等待0.5秒
time.sleep(0.3) # 减少到0.3秒提高效率
# 数据为空的情况
if not all_ohlcv or len(all_ohlcv) == 0:
@@ -128,33 +188,76 @@ def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
# 按时间排序
df = df.sort_values('timestamp')
"""
chan = ChanLun()
klc_list = chan.get_klc_list(df)
klc_index = 0
df_copy = df.copy()
ret_df = pd.DataFrame(columns=['timestamp', 'open', 'high', 'low', 'close', 'volume', 'date'])
for index in range(0, len(df)):
if klc_index >= len(klc_list):
klc_index = len(klc_list) - 1
klc = klc_list[klc_index]
if index == klc.start_klu.index:
ret_df.loc[klc_index] = df_copy.loc[index]
klc_index += 1
"""
# 限制数据条数的逻辑 - 优先考虑时间范围
if start_time and end_time:
# 如果指定了明确的时间范围,返回该时间范围内的所有数据
print(f"用户指定了时间范围,返回完整数据 {len(df)} 条记录")
if len(df) > 10000: # 防止数据量过大,设置一个合理的上限
print(f"警告:数据量过大({len(df)}条),为保证性能将限制为最新的10000条记录")
df = df.tail(10000).reset_index(drop=True)
elif limit and len(df) > limit:
# 如果没有指定明确时间范围,使用默认的limit限制
print(f"未指定明确时间范围,应用默认限制,返回最新的 {limit} 条记录")
df = df.tail(limit).reset_index(drop=True)
df = add_indicators(df)
# 如果过滤后没有数据,返回None
if len(df) == 0:
print("过滤后无数据")
return None
print(f"获取到总共 {len(df)}数据")
print(f"成功获取加密货币数据: {len(df)}记录 (共 {request_count} 个批次)")
return df
except Exception as e:
print(f"获取数据错误: {e}")
print(f"获取加密货币数据错误: {e}")
traceback.print_exc()
return None
def get_a_stock_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
"""获取A股K线数据"""
try:
# 处理时间戳参数转换为日期字符串
start_date = None
end_date = None
if start_time:
try:
start_timestamp = int(start_time)
start_date = datetime.fromtimestamp(start_timestamp / 1000).strftime('%Y-%m-%d')
except:
start_date = start_time
if end_time:
try:
end_timestamp = int(end_time)
end_date = datetime.fromtimestamp(end_timestamp / 1000).strftime('%Y-%m-%d')
except:
end_date = end_time
# 如果用户指定了时间范围,优先获取该范围内的所有数据
actual_limit = limit
if start_date and end_date:
print(f"用户指定了时间范围 {start_date}{end_date},将获取该范围内的所有数据")
actual_limit = None # 不限制数据条数,获取完整时间范围数据
# 调用A股数据获取器
df = china_stock.get_kl_data(symbol, timeframe, start_date, end_date, actual_limit)
if df is None:
print(f"未获取到A股数据: {symbol}")
return None
print(f"获取到A股数据: {len(df)} 条记录")
return df
except Exception as e:
print(f"获取A股数据错误: {e}")
traceback.print_exc()
return None
def add_indicators(df):
fast = 8
slow = 16
@@ -178,12 +281,17 @@ def add_indicators(df):
df['ma250'] = df['ma250'].fillna(0)
df['rsi'] = df['rsi'].fillna(0)
df['avg_volume'] = df['volume'].rolling(10).mean()
# 计算量比
# 计算量比,避免产生Infinity值
df['volume_ratio'] = df['volume'] / df['avg_volume']
# 填充缺失值(前N根K线)
df['volume_ratio'] = df['volume_ratio'].fillna(1.0)
df['avg_volume'] = df['avg_volume'].fillna(0)
# 处理Infinity和-Infinity值
df['volume_ratio'] = df['volume_ratio'].replace([float('inf'), float('-inf')], 1.0)
return df
def calculate_macd(df):
"""计算MACD指标"""
exp1 = df['close'].ewm(span=12, adjust=False).mean()
@@ -287,14 +395,10 @@ def identify_trade_points(bi_list, seg_list, zs_list):
# 辅助函数,转换缠论方向枚举为整数
def convert_direction(direction):
"""将缠论方向枚举转换为整数"""
if direction == Chan_BI_DIR.UP:
"""转换方向枚举为数字"""
if direction == Chan_BI_DIR.UP or direction == Chan_SEG_DIR.UP:
return 1
elif direction == Chan_BI_DIR.DOWN:
return -1
elif direction == Chan_SEG_DIR.UP:
return 1
elif direction == Chan_SEG_DIR.DOWN:
elif direction == Chan_BI_DIR.DOWN or direction == Chan_SEG_DIR.DOWN:
return -1
else:
return 0
@@ -380,10 +484,34 @@ def is_smaller_or_equal_timeframe(tf1, tf2):
# 返回tf1是否小于等于tf2
return tf1_value <= tf2_value
def clean_dataframe_for_json(df):
"""清理DataFrame中的NaN值,确保JSON序列化正常"""
# 创建副本以避免修改原数据
df_clean = df.copy()
# 将NaN、inf、-inf替换为None
df_clean = df_clean.replace([np.nan, np.inf, -np.inf], None)
# 处理数值列,确保值为有限数字或None
numeric_columns = df_clean.select_dtypes(include=[np.number]).columns
for col in numeric_columns:
# 确保所有数值都是有限的
df_clean[col] = df_clean[col].apply(lambda x: x if (x is not None and np.isfinite(x)) else None)
# 处理时间列,确保格式正确
datetime_columns = df_clean.select_dtypes(include=['datetime64']).columns
for col in datetime_columns:
df_clean[col] = df_clean[col].dt.strftime('%Y-%m-%d %H:%M:%S')
return df_clean
@app.route('/')
def index():
"""主页"""
return render_template('index.html', timeframes=TIMEFRAMES, symbols=SYMBOLS)
return render_template('index.html',
timeframes=TIMEFRAMES,
symbols=SYMBOLS,
a_stock_symbols=A_STOCK_SYMBOLS)
@app.route('/api/analyze')
def analyze():
@@ -448,7 +576,7 @@ def analyze():
# 添加主周期分析结果到返回数据
result.update({
'kline_data': df.to_dict('records'),
'kline_data': clean_dataframe_for_json(df).to_dict('records'),
'bi_list': [{
'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None,
@@ -522,7 +650,7 @@ def analyze():
} for bi in element_analysis['bi_list'] if bi.end_klc]
# 添加小周期K线数据
result['element_kline_data'] = element_df.to_dict('records')
result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records')
result['element_seg_list'] = [{
'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(),
@@ -584,5 +712,59 @@ def get_symbols():
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/a_stocks')
def get_a_stocks():
"""获取A股股票列表"""
try:
stock_list = china_stock.get_stock_list()
return jsonify(stock_list)
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/popular_a_stocks')
def get_popular_a_stocks():
"""获取热门A股股票"""
try:
return jsonify(china_stock.get_popular_stocks())
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/sectors')
def get_sectors():
"""获取所有行业分类"""
try:
sectors = china_stock.get_all_sectors()
return jsonify(sectors)
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/stocks_by_sector')
def get_stocks_by_sector():
"""根据行业获取股票"""
try:
sector = request.args.get('sector')
if sector:
stocks = china_stock.get_stock_by_sector(sector)
return jsonify(stocks)
else:
# 返回所有行业的股票分组
all_sectors = china_stock.get_stock_by_sector()
return jsonify(all_sectors)
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/search_stock')
def search_stock():
"""搜索股票 - 增强版"""
try:
keyword = request.args.get('keyword', '')
if not keyword:
return jsonify({'error': '搜索关键词不能为空'})
results = china_stock.search_stock(keyword)
return jsonify(results)
except Exception as e:
return jsonify({'error': str(e)})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8123)