Files
Chan/web/app.py
T
2025-07-06 10:17:34 +08:00

1618 lines
72 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.
from flask import Flask, render_template, jsonify, request
import ccxt
import pandas as pd
from datetime import datetime, timedelta
import sys
import os
import matplotlib
matplotlib.use('Agg') # 设置使用非GUI后端,必须在导入pyplot之前设置
import matplotlib.pyplot as plt
import io
import base64
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:
BUY1 = 1 # 一类买点
BUY2 = 2 # 二类买点
BUY3 = 3 # 三类买点
SELL1 = -1 # 一类卖点
SELL2 = -2 # 二类卖点
SELL3 = -3 # 三类卖点
app = Flask(__name__)
# 初始化交易所
exchange = ccxt.binance({
'enableRateLimit': True,
})
# 初始化A股数据获取器
china_stock = ChinaStockData()
# 时间周期映射
TIMEFRAMES = {
'1m': '1分钟',
'3m': '3分钟',
'5m': '5分钟',
'15m': '15分钟',
'30m': '30分钟',
'1h': '1小时',
'2h': '2小时',
'4h': '4小时',
'8h': '8小时',
'12h': '12小时',
'1d': '日线',
'3d': '3日线',
'1w': '周线',
'1M': '月线',
}
# 常见交易对
SYMBOLS = [
'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT',
'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线数据,支持加密货币和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:
return None
def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
"""获取加密货币K线数据,支持分页加载确保获取指定时间范围内的所有数据"""
try:
# 初始化参数
since = None
if start_time:
try:
since = int(start_time)
except ValueError:
pass
# 结束时间处理
until = None
if end_time:
try:
until = int(end_time)
except ValueError:
pass
# 根据时间周期调整每次请求的数据量
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 # 日线及以上可以获取更多
batch_size = 1000 # 默认批次大小
# 初始化存储所有K线数据的列表
all_ohlcv = []
# 初始化当前查询的开始时间
current_since = since
# 添加请求计数和最大限制
request_count = 0
max_requests = 50 # 最大请求次数,防止无限循环
# 分页加载数据
while request_count < max_requests:
request_count += 1
try:
# 获取当前页的数据
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size)
# 如果没有获取到数据,结束循环
if not ohlcv or len(ohlcv) == 0:
break
# 将获取到的数据添加到总列表中
all_ohlcv.extend(ohlcv)
# 获取最后一条数据的时间戳
last_timestamp = ohlcv[-1][0]
# 如果已达到结束时间,结束循环
if until and last_timestamp >= until:
break
# 如果获取的数据条数小于限制数,说明已经获取完所有数据
if len(ohlcv) < batch_size:
break
# 更新下一页的开始时间(加1毫秒避免重复)
current_since = last_timestamp + 1
except Exception as e:
# 如果单个批次失败,继续尝试下一个批次
if current_since:
# 尝试增加时间跳过可能的问题时间点
current_since += 60000 # 跳过1分钟
else:
break
# 防止API请求过于频繁
time.sleep(0.3) # 减少到0.3秒提高效率
# 数据为空的情况
if not all_ohlcv or len(all_ohlcv) == 0:
return None
# 转换为DataFrame
df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df['date'] = pd.to_datetime(df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
# 在客户端进行结束时间过滤
if until:
df = df[df['timestamp'] <= until]
# 去除重复数据
df = df.drop_duplicates(subset=['timestamp'])
# 按时间排序
df = df.sort_values('timestamp')
# 限制数据条数的逻辑 - 优先考虑时间范围
if start_time and end_time:
# 如果指定了明确的时间范围,返回该时间范围内的所有数据
if len(df) > 10000: # 防止数据量过大,设置一个合理的上限
df = df.tail(10000).reset_index(drop=True)
elif limit and len(df) > limit:
# 如果没有指定明确时间范围,使用默认的limit限制
df = df.tail(limit).reset_index(drop=True)
# 如果过滤后没有数据,返回None
if len(df) == 0:
return None
return df
except Exception as e:
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 (ValueError, TypeError):
# 如果不是时间戳,尝试解析datetime-local格式 (YYYY-MM-DDTHH:MM)
try:
if 'T' in str(start_time):
# datetime-local格式:2025-05-19T06:07
start_date = str(start_time).split('T')[0] # 只取日期部分
else:
start_date = str(start_time)
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 (ValueError, TypeError):
# 如果不是时间戳,尝试解析datetime-local格式
try:
if 'T' in str(end_time):
# datetime-local格式:2025-05-26T06:07
end_date = str(end_time).split('T')[0] # 只取日期部分
else:
end_date = str(end_time)
except:
end_date = end_time
# 如果用户指定了时间范围,优先获取该范围内的所有数据
actual_limit = limit
if start_date and end_date:
actual_limit = None # 不限制数据条数,获取完整时间范围数据
# 调用A股数据获取器
df = china_stock.get_kl_data(symbol, timeframe, start_date, end_date, actual_limit)
if df is None:
return None
return df
except Exception as e:
return None
def add_indicators(df):
fast = 8
slow = 16
period = 6
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
df['macd'] = macd['macd']
df['macdsignal'] = macd['macdsignal']
df['macdhist'] = macd['macdhist']
df['ma5'] = (ta.MA(df, timeperiod=5)).fillna(0)
df['ma10'] = (ta.MA(df, timeperiod=10)).fillna(0)
df['ma30'] = (ta.EMA(df, timeperiod=30)).fillna(0)
df['ma250'] = (ta.MA(df, timeperiod=250)).fillna(0)
df['rsi'] = ta.RSI(df, timeperiod=14)
# 计算布林带 (当前周期 - 20周期,2标准差)
bb = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
df['bb_upper'] = bb['upperband'].fillna(0)
df['bb_middle'] = bb['middleband'].fillna(0)
df['bb_lower'] = bb['lowerband'].fillna(0)
# 计算次周期布林带 (14周期,2标准差)
bb_element = ta.BBANDS(df, timeperiod=14, nbdevup=2.0, nbdevdn=2.0, matype=0)
df['element_bb_upper'] = bb_element['upperband'].fillna(0)
df['element_bb_middle'] = bb_element['middleband'].fillna(0)
df['element_bb_lower'] = bb_element['lowerband'].fillna(0)
df['macd'] = df['macd'].fillna(0)
df['macdsignal'] = df['macdsignal'].fillna(0)
df['macdhist'] = df['macdhist'].fillna(0)
df['ma5'] = df['ma5'].fillna(0)
df['ma10'] = df['ma10'].fillna(0)
df['ma30'] = df['ma30'].fillna(0)
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)
# 计算ATR (Average True Range) - 14周期
df['atr'] = ta.ATR(df, timeperiod=14)
df['atr'] = df['atr'].fillna(0)
return df
def calculate_macd(df):
"""计算MACD指标"""
exp1 = df['close'].ewm(span=10, adjust=False).mean()
exp2 = df['close'].ewm(span=26, adjust=False).mean()
macd = exp1 - exp2
signal = macd.ewm(span=9, adjust=False).mean()
histogram = macd - signal
return {
'macd': macd.tolist(),
'signal': signal.tolist(),
'histogram': histogram.tolist()
}
def analyze_chan(df):
"""进行缠论分析"""
chan = ChanLun()
# 获取分析结果
klc_list = chan.get_klc_list(df)
bi_list = chan.cal_bi_list(klc_list)
#for index in range(0, 10):
#print(bi_list[index].start_time, bi_list[index].start_klc.end_time, bi_list[index].dir)
seg_list = chan.get_seg_list(bi_list)
zs_list = chan.calculate_zs(bi_list, seg_list)
# 添加买卖点识别
buy_sell_points = identify_trade_points(bi_list, seg_list, zs_list)
for bi in bi_list:
bi.cal_macdhist()
for bi in bi_list:
bi.cal_macd_div()
#print(bi.start_time, bi.macd_hist, bi.macd_div)
# 获取原始K线数据用于KLU分型分析
klu_list = []
try:
# 尝试获取KLU数据
if hasattr(chan, 'get_klu_list'):
klu_list = chan.get_klu_list(df)
elif hasattr(chan, 'klu_list'):
klu_list = chan.klu_list
else:
# 如果没有专门的KLU方法,尝试从KLC获取原始K线数据
pass
except Exception as e:
klu_list = []
# 提取K线分型信息
klc_fx_info = []
for klc in klc_list:
if hasattr(klc, 'klc_fx_type') and klc.klc_fx_type != Chan_KLC_FX.UNKNOWN:
try:
# 计算分型强度
fx_strength = 0
fx_strength_level = ""
is_strong_fx = False
# 统一使用cal_fx_strength函数
if hasattr(klc, 'cal_fx_strength'):
fx_strength = klc.cal_fx_strength(5)
# 尝试获取分型强度等级
if hasattr(klc, 'get_fx_strength_level'):
fx_strength_level = klc.get_fx_strength_level()
# 尝试判断是否为强分型
if hasattr(klc, 'is_strong_fx'):
is_strong_fx = klc.is_strong_fx()
# 如果分型强度小于1,设为0
if fx_strength < 1:
fx_strength = 0
klc_fx_info.append({
'time': klc.end_time,
'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high,
'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""),
'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM,
'fx_strength': fx_strength, # 分型强度分数 (0-100)
'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱)
'is_strong_fx': is_strong_fx # 是否为强分型
})
except Exception as e:
# 如果出错,仍然添加基本信息,但分型强度为0
klc_fx_info.append({
'time': klc.end_time,
'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high,
'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""),
'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM,
'fx_strength': 0,
'fx_strength_level': "",
'is_strong_fx': False
})
# 提取KLU分型信息
klu_fx_info = []
for klu in klu_list:
if hasattr(klu, 'fx_type') and klu.fx_type != Chan_FX_TYPE.UNKNOWN:
try:
# 计算分型强度
fx_strength = 0
fx_strength_level = ""
is_strong_fx = False
# 尝试调用分型强度计算方法
if hasattr(klu, 'calculate_realtime_fx_strength'):
fx_strength = klu.calculate_realtime_fx_strength()
elif hasattr(klu, 'fx_strength'):
fx_strength = klu.fx_strength
# 尝试获取分型强度等级 - 基于强度值生成等级
if fx_strength >= 2:
fx_strength_level = "强"
is_strong_fx = True
elif fx_strength >= 1:
fx_strength_level = "中"
is_strong_fx = False
elif fx_strength >= 0:
fx_strength_level = "弱"
is_strong_fx = False
else:
fx_strength_level = "极弱"
is_strong_fx = False
# 确保分型确认状态
is_confirmed = getattr(klu, 'fx_confirmed', True)
klu_fx_info.append({
'time': klu.time,
'price': klu.low if klu.fx_type == Chan_FX_TYPE.BOTTOM else klu.high,
'fx_type': str(klu.fx_type).replace("Chan_FX_TYPE.", ""),
'is_bottom': klu.fx_type == Chan_FX_TYPE.BOTTOM,
'fx_strength': fx_strength, # 分型强度分数
'fx_strength_level': fx_strength_level, # 分型强度等级
'is_strong_fx': is_strong_fx, # 是否为强分型
'fx_confirmed': is_confirmed # 分型是否确认
})
except Exception as e:
# 如果出错,仍然添加基本信息,但分型强度为0
klu_fx_info.append({
'time': klu.time,
'price': klu.low if klu.fx_type == Chan_FX_TYPE.BOTTOM else klu.high,
'fx_type': str(klu.fx_type).replace("Chan_FX_TYPE.", ""),
'is_bottom': klu.fx_type == Chan_FX_TYPE.BOTTOM,
'fx_strength': 0,
'fx_strength_level': "",
'is_strong_fx': False,
'fx_confirmed': False
})
return {
'klc_list': klc_list,
'klu_list': klu_list, # 添加KLU列表
'bi_list': bi_list,
'seg_list': seg_list,
'zs_list': zs_list,
'trade_points': buy_sell_points,
'klc_fx_info': klc_fx_info, # KLC分型信息
'klu_fx_info': klu_fx_info # 添加KLU分型信息
}
def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, start_time=None, end_time=None):
"""生成逐步计算的回放数据"""
replay_data = {}
# 预先获取完整的次周期数据(避免重复数据获取)
element_full_data = None
if element_timeframe and symbol:
# 一次性获取完整的次周期数据
element_full_data = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time)
if element_full_data is not None and len(element_full_data) > 0:
# 一次性添加技术指标
element_full_data = add_indicators(element_full_data)
# 为每个K线索引计算分析结果
for i in range(1, len(df) + 1): # 从1开始,至少需要1根K线
try:
# 截取到当前索引的数据
current_df = df.iloc[:i].copy()
# 添加技术指标
current_df = add_indicators(current_df)
# 进行缠论分析
analysis_result = analyze_chan(current_df)
# 计算MACD
macd_data = calculate_macd(current_df)
# 如果有次周期数据,筛选对应时间范围的数据
element_step_data = {}
if element_full_data is not None:
# 获取当前主周期时间范围
current_end_time = current_df['timestamp'].iloc[-1] if len(current_df) > 0 else None
if current_end_time:
# 筛选次周期数据:只取时间戳小于等于当前主周期结束时间的数据
element_current_df = element_full_data[element_full_data['timestamp'] <= current_end_time].copy()
if len(element_current_df) > 0:
# 重新对当前时间范围的次周期数据进行缠论分析
# 这样可以确保数据的准确性,避免时间筛选的复杂性
element_current_analysis = analyze_chan(element_current_df)
# 直接使用分析结果,无需复杂的时间筛选
filtered_bi_list = element_current_analysis['bi_list']
filtered_seg_list = element_current_analysis['seg_list']
filtered_zs_list = element_current_analysis['zs_list']
filtered_trade_points = element_current_analysis['trade_points']
filtered_klc_fx = element_current_analysis['klc_fx_info']
filtered_klu_fx = element_current_analysis['klu_fx_info']
# 计算当前时间范围的MACD
element_macd_data = calculate_macd(element_current_df)
element_step_data = {
'element_kline_data': clean_dataframe_for_json(element_current_df).to_dict('records'),
'element_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,
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in filtered_bi_list if bi.end_klc],
'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(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in filtered_seg_list if seg.end_bi],
'element_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure
} for zs in filtered_zs_list if zs.end_klc],
'element_uncompleted_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure
} for zs in filtered_zs_list if not zs.is_sure],
'element_trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in filtered_trade_points],
'element_macd': element_macd_data,
'element_bollinger': {
'upper': element_current_df['bb_upper'].tolist(),
'middle': element_current_df['bb_middle'].tolist(),
'lower': element_current_df['bb_lower'].tolist()
},
'element_element_bollinger': {
'upper': element_current_df['element_bb_upper'].tolist(),
'middle': element_current_df['element_bb_middle'].tolist(),
'lower': element_current_df['element_bb_lower'].tolist()
},
# 添加次周期ATR数据
'element_atr': element_current_df['atr'].tolist(),
'element_klc_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx'])
} for point in filtered_klc_fx],
'element_klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx']),
'fx_confirmed': bool(point['fx_confirmed'])
} for point in filtered_klu_fx]
}
# 构建该索引对应的分析结果
step_data = {
'step_index': i-1, # 当前步骤索引
'total_steps': len(df), # 总步骤数
'has_element_data': element_timeframe is not None and len(element_step_data) > 0, # 是否包含次周期数据
'element_timeframe': element_timeframe, # 次周期时间框架
'kline_data': clean_dataframe_for_json(current_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,
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in analysis_result['bi_list'] if bi.end_klc],
'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(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in analysis_result['seg_list'] if seg.end_bi],
'zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure
} for zs in analysis_result['zs_list'] if zs.end_klc],
'uncompleted_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure
} for zs in analysis_result['zs_list'] if not zs.is_sure],
'trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in analysis_result['trade_points']],
'macd': macd_data,
'bollinger': {
'upper': current_df['bb_upper'].tolist(),
'middle': current_df['bb_middle'].tolist(),
'lower': current_df['bb_lower'].tolist()
},
'element_bollinger': {
'upper': current_df['element_bb_upper'].tolist(),
'middle': current_df['element_bb_middle'].tolist(),
'lower': current_df['element_bb_lower'].tolist()
},
# 添加ATR数据
'atr': current_df['atr'].tolist(),
'klc_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
} for point in analysis_result['klc_fx_info']],
'klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认
} for point in analysis_result['klu_fx_info']]
}
# 合并次周期数据到step_data中,如果没有次周期数据则提供空的占位符
if element_step_data:
step_data.update(element_step_data)
else:
# 提供空的次周期数据结构,确保前端可以统一处理
step_data.update({
'element_kline_data': [],
'element_bi_list': [],
'element_seg_list': [],
'element_zs_list': [],
'element_uncompleted_zs_list': [],
'element_trade_points': [],
'element_macd': {'macd': [], 'signal': [], 'histogram': []},
'element_bollinger': {'upper': [], 'middle': [], 'lower': []},
'element_element_bollinger': {'upper': [], 'middle': [], 'lower': []},
'element_atr': [],
'element_klc_fx_info': [],
'element_klu_fx_info': []
})
replay_data[i-1] = step_data # 使用0-based索引
except Exception as e:
continue
return replay_data
def identify_trade_points(bi_list, seg_list, zs_list):
"""识别缠论买卖点 - 多级别识别,减少滞后性"""
trade_points = []
# 1. 基于笔的二三类买卖点识别(更及时)
trade_points.extend(identify_bi_trade_points(bi_list, zs_list))
# 2. 基于线段的一类买卖点识别(传统方法)
trade_points.extend(identify_seg_trade_points(seg_list))
# 3. 基于分型强度的预警点识别(最及时)
trade_points.extend(identify_fx_warning_points(bi_list))
# 4. 基于MACD背驰的买卖点识别
trade_points.extend(identify_macd_divergence_points(bi_list))
# 按时间排序
trade_points.sort(key=lambda x: x['time'])
return trade_points
def identify_bi_trade_points(bi_list, zs_list):
"""基于笔识别二三类买卖点 - 更及时的信号"""
trade_points = []
if len(bi_list) < 3:
return trade_points
# 构建中枢映射,便于快速查找
zs_map = {}
for zs in zs_list:
if zs.is_sure: # 只考虑已确认的中枢
zs_map[zs.start_klc.end_time] = zs
for i in range(2, len(bi_list)):
current_bi = bi_list[i]
prev_bi = bi_list[i-1]
prev_prev_bi = bi_list[i-2]
# 确保笔已完成
if not current_bi.end_klc or not prev_bi.end_klc or not prev_prev_bi.end_klc:
continue
# 二类买点:向下笔后的向上笔,且不创新低
if (convert_direction(prev_bi.dir) == -1 and
convert_direction(current_bi.dir) == 1):
prev_low = prev_bi.end_klc.low
current_end_price = current_bi.end_klc.high
# 检查是否不创新低(相对于前面的低点)
if i >= 4: # 至少需要5个笔来判断
earlier_lows = [bi.end_klc.low for bi in bi_list[max(0, i-4):i-1]
if convert_direction(bi.dir) == -1 and bi.end_klc]
if earlier_lows and prev_low > min(earlier_lows):
trade_points.append({
'type': TRADE_POINT_TYPE.BUY2,
'time': current_bi.end_klc.end_time,
'price': current_end_price,
'desc': '二类买点(笔)'
})
# 二类卖点:向上笔后的向下笔,且不创新高
if (convert_direction(prev_bi.dir) == 1 and
convert_direction(current_bi.dir) == -1):
prev_high = prev_bi.end_klc.high
current_end_price = current_bi.end_klc.low
# 检查是否不创新高(相对于前面的高点)
if i >= 4: # 至少需要5个笔来判断
earlier_highs = [bi.end_klc.high for bi in bi_list[max(0, i-4):i-1]
if convert_direction(bi.dir) == 1 and bi.end_klc]
if earlier_highs and prev_high < max(earlier_highs):
trade_points.append({
'type': TRADE_POINT_TYPE.SELL2,
'time': current_bi.end_klc.end_time,
'price': current_end_price,
'desc': '二类卖点(笔)'
})
return trade_points
def identify_seg_trade_points(seg_list):
"""基于线段识别一类买卖点 - 传统方法"""
trade_points = []
if len(seg_list) >= 3:
for i in range(2, len(seg_list)):
# 确保线段已完成
if seg_list[i].end_bi and seg_list[i-1].end_bi and seg_list[i-2].end_bi:
# 一类买点:向下-向上-向下的底分型
if (convert_direction(seg_list[i-2].dir) == -1 and
convert_direction(seg_list[i-1].dir) == 1 and
convert_direction(seg_list[i].dir) == -1):
trade_points.append({
'type': TRADE_POINT_TYPE.BUY1,
'time': seg_list[i].end_bi.end_klc.end_time,
'price': seg_list[i].end_bi.end_klc.low,
'desc': '一类买点(线段)'
})
# 一类卖点:向上-向下-向上的顶分型
if (convert_direction(seg_list[i-2].dir) == 1 and
convert_direction(seg_list[i-1].dir) == -1 and
convert_direction(seg_list[i].dir) == 1):
trade_points.append({
'type': TRADE_POINT_TYPE.SELL1,
'time': seg_list[i].end_bi.end_klc.end_time,
'price': seg_list[i].end_bi.end_klc.high,
'desc': '一类卖点(线段)'
})
return trade_points
def identify_fx_warning_points(bi_list):
"""基于分型强度识别预警点 - 最及时的信号"""
trade_points = []
if len(bi_list) < 2:
return trade_points
# 检查最近的几个笔
recent_bis = bi_list[-3:] if len(bi_list) >= 3 else bi_list
for bi in recent_bis:
if not bi.end_klc:
continue
# 获取分型强度(如果有的话)
fx_strength = 0
if hasattr(bi.end_klc, 'cal_fx_strength'):
try:
fx_strength = bi.end_klc.cal_fx_strength(5)
except:
fx_strength = 0
# 强分型预警(分型强度>=2
if fx_strength >= 2:
if convert_direction(bi.dir) == -1: # 向下笔结束,可能的底部
trade_points.append({
'type': TRADE_POINT_TYPE.BUY3,
'time': bi.end_klc.end_time,
'price': bi.end_klc.low,
'desc': f'强分型预警-买点(强度:{fx_strength})'
})
elif convert_direction(bi.dir) == 1: # 向上笔结束,可能的顶部
trade_points.append({
'type': TRADE_POINT_TYPE.SELL3,
'time': bi.end_klc.end_time,
'price': bi.end_klc.high,
'desc': f'强分型预警-卖点(强度:{fx_strength})'
})
return trade_points
def identify_macd_divergence_points(bi_list):
"""基于MACD背驰识别买卖点"""
trade_points = []
if len(bi_list) < 4:
return trade_points
# 检查最近的笔是否有背驰
for i in range(2, len(bi_list)):
current_bi = bi_list[i]
if not current_bi.end_klc or not hasattr(current_bi, 'macd_div'):
continue
# MACD背驰阈值
divergence_threshold = 0.3
# 向下笔的底背驰 -> 买点
if (convert_direction(current_bi.dir) == -1 and
hasattr(current_bi, 'macd_div') and
current_bi.macd_div > divergence_threshold):
trade_points.append({
'type': TRADE_POINT_TYPE.BUY2,
'time': current_bi.end_klc.end_time,
'price': current_bi.end_klc.low,
'desc': f'MACD底背驰买点(背驰度:{current_bi.macd_div:.2f})'
})
# 向上笔的顶背驰 -> 卖点
elif (convert_direction(current_bi.dir) == 1 and
hasattr(current_bi, 'macd_div') and
current_bi.macd_div > divergence_threshold):
trade_points.append({
'type': TRADE_POINT_TYPE.SELL2,
'time': current_bi.end_klc.end_time,
'price': current_bi.end_klc.high,
'desc': f'MACD顶背驰卖点(背驰度:{current_bi.macd_div:.2f})'
})
return trade_points
# 辅助函数,转换缠论方向枚举为整数
def convert_direction(direction):
"""转换方向枚举为数字"""
if direction == Chan_BI_DIR.UP or direction == Chan_SEG_DIR.UP:
return 1
elif direction == Chan_BI_DIR.DOWN or direction == Chan_SEG_DIR.DOWN:
return -1
else:
return 0
def format_time_safely(time_obj, client_tz):
"""安全地格式化时间对象,处理字符串和datetime两种情况"""
if time_obj is None:
return None
if isinstance(time_obj, str):
# 尝试将字符串解析为datetime
try:
from dateutil import parser
time_obj = parser.parse(time_obj)
return time_obj.astimezone(client_tz).isoformat()
except:
return time_obj
else:
# 已经是datetime对象
return time_obj.astimezone(client_tz).isoformat()
def is_smaller_timeframe(tf1, tf2):
"""判断时间周期tf1是否小于tf2"""
# 定义时间周期的分钟数映射
tf_values = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
'1M': 43200
}
# 获取时间周期对应的分钟数
tf1_value = tf_values.get(tf1)
tf2_value = tf_values.get(tf2)
# 如果某个时间周期不在映射中,返回False
if tf1_value is None or tf2_value is None:
return False
# 返回tf1是否小于tf2
return tf1_value < tf2_value
def is_smaller_or_equal_timeframe(tf1, tf2):
"""判断时间周期tf1是否小于等于tf2"""
# 定义时间周期的分钟数映射
tf_values = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
'1M': 43200
}
# 获取时间周期对应的分钟数
tf1_value = tf_values.get(tf1)
tf2_value = tf_values.get(tf2)
# 如果某个时间周期不在映射中,返回False
if tf1_value is None or tf2_value is None:
return False
# 返回tf1是否小于等于tf2
return tf1_value <= tf2_value
def clean_dataframe_for_json(df):
"""清理DataFrame数据用于JSON序列化"""
# 创建副本避免修改原始数据
clean_df = df.copy()
# 替换NaN值为None
clean_df = clean_df.where(pd.notnull(clean_df), None)
return clean_df
@app.route('/')
def index():
"""主页"""
return render_template('index.html',
timeframes=TIMEFRAMES,
symbols=SYMBOLS,
a_stock_symbols=A_STOCK_SYMBOLS)
@app.route('/api/analyze')
def analyze():
"""分析接口"""
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '5m')
# 验证交易对不为空
if not symbol or symbol.strip() == '':
return jsonify({'error': '交易对不能为空'})
# 获取时间范围参数
start_time = request.args.get('start_time')
end_time = request.args.get('end_time')
# 获取客户端请求的时区
client_timezone = request.args.get('timezone', 'Asia/Shanghai')
# 获取分形元素时间周期
element_timeframe = request.args.get('element_timeframe')
# 获取是否只需要分形元素数据的参数
elements_only_param = request.args.get('elements_only')
elements_only = elements_only_param == 'true'
# 获取是否需要回放数据的参数
need_replay_data = request.args.get('need_replay_data', 'false').lower() == 'true'
# 验证小周期是否小于主周期
if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe):
return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'})
# 获取数据
df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time)
if df is None:
return jsonify({'error': '获取数据失败'})
if len(df) == 0:
return jsonify({'error': '所选时间范围内没有数据'})
# 使用客户端指定的时区
client_tz = timezone(client_timezone)
# 如果只需要分形元素数据而不需要主周期数据,则初始化一个空结果
result = {
'timezone': client_timezone
}
# 如果不是只需要分形元素数据,则添加主周期数据
if not elements_only:
# 添加技术指标(包括布林带)
df = add_indicators(df)
# 进行缠论分析
analysis_result = analyze_chan(df)
# 计算MACD
macd_data = calculate_macd(df)
# 如果需要回放数据,生成逐步计算的回放数据
if need_replay_data:
replay_data = generate_replay_data(df, client_tz, symbol, element_timeframe, start_time, end_time)
else:
replay_data = None
# 添加主周期分析结果到返回数据
result.update({
'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,
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in analysis_result['bi_list'] if bi.end_klc],
'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(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in analysis_result['seg_list'] if seg.end_bi],
'zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure # 添加中枢是否完成的标志
} for zs in analysis_result['zs_list'] if zs.end_klc],
# 添加未完成中枢列表
'uncompleted_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': None, # 未完成中枢没有结束时间
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure # 未完成中枢的is_sure为False
} for zs in analysis_result['zs_list'] if not zs.is_sure],
'trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in analysis_result['trade_points']],
'macd': macd_data,
# 添加布林带数据
'bollinger': {
'upper': df['bb_upper'].tolist(),
'middle': df['bb_middle'].tolist(),
'lower': df['bb_lower'].tolist()
},
'element_bollinger': {
'upper': df['element_bb_upper'].tolist(),
'middle': df['element_bb_middle'].tolist(),
'lower': df['element_bb_lower'].tolist()
},
# 添加ATR数据
'atr': df['atr'].tolist(),
# 添加K线分型信息
'klc_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
} for point in analysis_result['klc_fx_info']],
'klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认
} for point in analysis_result['klu_fx_info']]
})
# 如果生成了回放数据,添加到返回结果中
if replay_data is not None:
result['replay_data'] = replay_data
# 如果有指定分形元素时间周期,获取小周期数据
if element_timeframe:
# 获取小周期数据,使用与主周期相同的时间范围
element_df = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time)
if element_df is not None and len(element_df) > 0:
# 添加小周期技术指标(包括布林带)
element_df = add_indicators(element_df)
# 对小周期数据进行缠论分析
element_analysis = analyze_chan(element_df)
# 计算小周期MACD数据
element_macd_data = calculate_macd(element_df)
# 添加小周期分析结果到返回数据
result['element_timeframe'] = element_timeframe
result['element_macd'] = element_macd_data # 添加小周期MACD数据
# 添加小周期布林带数据
result['element_bollinger'] = {
'upper': element_df['bb_upper'].tolist(),
'middle': element_df['bb_middle'].tolist(),
'lower': element_df['bb_lower'].tolist()
}
result['element_element_bollinger'] = {
'upper': element_df['element_bb_upper'].tolist(),
'middle': element_df['element_bb_middle'].tolist(),
'lower': element_df['element_bb_lower'].tolist()
}
# 添加小周期ATR数据
result['element_atr'] = element_df['atr'].tolist()
result['element_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,
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in element_analysis['bi_list'] if bi.end_klc]
# 添加小周期K线数据
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(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in element_analysis['seg_list'] if seg.end_bi]
result['element_zs_list'] = [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure # 添加中枢是否完成的标志
} for zs in element_analysis['zs_list'] if zs.end_klc]
result['element_uncompleted_zs_list'] = [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': None, # 未完成中枢没有结束时间
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure # 未完成中枢的is_sure为False
} for zs in element_analysis['zs_list'] if not zs.is_sure]
result['element_trade_points'] = [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in element_analysis['trade_points']]
# 添加小周期分型信息
result['element_klc_fx_info'] = [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
} for point in element_analysis['klc_fx_info']]
result['element_klu_fx_info'] = [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认
} for point in element_analysis['klu_fx_info']]
pass
return jsonify(result)
@app.route('/api/symbols')
def get_symbols():
"""获取可用交易对"""
try:
markets = exchange.load_markets()
# 合约交易对通常是以USDT结尾的永续合约
symbols = [symbol for symbol in markets.keys() if '/USDT' in symbol and ':USDT' in symbol]
return jsonify(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)})
@app.route('/api/test_element_data')
def test_element_data():
"""测试次周期数据是否正确生成"""
try:
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '1h')
element_timeframe = request.args.get('element_timeframe', '15m')
# 获取主周期数据
main_df = get_kl_data(symbol, timeframe, limit=3)
if main_df is None or len(main_df) == 0:
return jsonify({'error': '无法获取主周期数据'})
# 获取次周期数据
element_df = get_kl_data(symbol, element_timeframe,
start_time=main_df['timestamp'].iloc[0],
end_time=main_df['timestamp'].iloc[-1])
if element_df is None or len(element_df) == 0:
return jsonify({'error': '无法获取次周期数据'})
# 分析次周期数据
element_df = add_indicators(element_df)
element_analysis = analyze_chan(element_df)
return jsonify({
'main_data_count': len(main_df),
'element_data_count': len(element_df),
'element_analysis': {
'bi_count': len(element_analysis['bi_list']),
'seg_count': len(element_analysis['seg_list']),
'zs_count': len(element_analysis['zs_list']),
'klc_fx_count': len(element_analysis['klc_fx_info']),
'klu_fx_count': len(element_analysis['klu_fx_info']),
'trade_points_count': len(element_analysis['trade_points'])
},
'sample_bi': [{'has_end_klc': bi.end_klc is not None,
'direction': convert_direction(bi.dir)}
for bi in element_analysis['bi_list'][:2]] if len(element_analysis['bi_list']) > 0 else [],
'sample_klc_fx': element_analysis['klc_fx_info'][:3] if len(element_analysis['klc_fx_info']) > 0 else [],
'sample_klu_fx': element_analysis['klu_fx_info'][:3] if len(element_analysis['klu_fx_info']) > 0 else []
})
except Exception as e:
import traceback
return jsonify({'error': str(e), 'traceback': traceback.format_exc()})
@app.route('/api/debug_replay_sample')
def debug_replay_sample():
"""调试接口:返回回放数据样本,方便前端调试"""
try:
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '1h')
element_timeframe = request.args.get('element_timeframe', '15m')
step = int(request.args.get('step', 2)) # 返回第几步的数据
# 获取少量数据进行测试
df = get_kl_data(symbol, timeframe, limit=5)
if df is None or len(df) == 0:
return jsonify({'error': '无法获取测试数据'})
# 生成回放数据
client_tz = timezone('Asia/Shanghai')
replay_data = generate_replay_data(
df, client_tz, symbol, element_timeframe,
start_time=None, end_time=None
)
if step not in replay_data:
return jsonify({'error': f'步骤 {step} 不存在,可用步骤:{list(replay_data.keys())}'})
# 返回指定步骤的完整数据
step_data = replay_data[step]
return jsonify({
'step': step,
'data': step_data,
'summary': {
'has_element_data': step_data.get('has_element_data', False),
'element_timeframe': step_data.get('element_timeframe'),
'main_bi_count': len(step_data.get('bi_list', [])),
'main_klc_fx_count': len(step_data.get('klc_fx_info', [])),
'main_klu_fx_count': len(step_data.get('klu_fx_info', [])),
'element_bi_count': len(step_data.get('element_bi_list', [])),
'element_klc_fx_count': len(step_data.get('element_klc_fx_info', [])),
'element_klu_fx_count': len(step_data.get('element_klu_fx_info', [])),
'element_kline_count': len(step_data.get('element_kline_data', []))
}
})
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/debug_replay_structure')
def debug_replay_structure():
"""调试接口:检查回放数据结构"""
try:
# 获取一个简单的测试案例
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '1h')
element_timeframe = request.args.get('element_timeframe', '15m')
# 获取少量数据进行测试
df = get_kl_data(symbol, timeframe, limit=5) # 只取5根K线
if df is None or len(df) == 0:
return jsonify({'error': '无法获取测试数据'})
# 生成回放数据
client_tz = timezone('Asia/Shanghai')
replay_data = generate_replay_data(
df, client_tz, symbol, element_timeframe,
start_time=None, end_time=None
)
# 返回结构信息
result = {
'total_steps': len(replay_data),
'sample_step_keys': list(replay_data[0].keys()) if len(replay_data) > 0 else [],
'has_element_data_in_steps': [],
'element_data_counts': {}
}
# 检查每个步骤的次周期数据
for step_idx, step_data in replay_data.items():
has_element = step_data.get('has_element_data', False)
result['has_element_data_in_steps'].append({
'step': step_idx,
'has_element_data': has_element,
'element_bi_count': len(step_data.get('element_bi_list', [])),
'element_klc_fx_count': len(step_data.get('element_klc_fx_info', [])),
'element_klu_fx_count': len(step_data.get('element_klu_fx_info', []))
})
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/filter_stocks', methods=['POST'])
def filter_stocks():
"""筛选满足条件的A股股票"""
try:
data = request.get_json()
start_time = data.get('start_time')
end_time = data.get('end_time')
timeframe = data.get('timeframe', '1d')
fx_strength_threshold = data.get('fx_strength_threshold', 1.0)
if not start_time or not end_time:
return jsonify({'error': '开始时间和结束时间不能为空'})
# 获取所有A股股票列表,如果失败则使用热门股票作为备用
stock_list = []
data_source = ""
try:
stock_list = china_stock.get_stock_list()
if stock_list and len(stock_list) > 0:
data_source = "完整股票列表"
else:
raise Exception("获取到的股票列表为空")
except Exception as e:
try:
popular_stocks = china_stock.get_popular_stocks()
stock_list = [{'symbol': stock['symbol'], 'name': stock['name']} for stock in popular_stocks]
data_source = "热门股票列表"
except Exception as e2:
# 检查是否是网络连接问题
if "timeout" in str(e).lower() or "connection" in str(e).lower() or "network" in str(e).lower():
return jsonify({
'error': '网络连接超时,无法获取股票数据。请检查网络连接后重试。',
'error_type': 'network_error',
'suggestion': '请确保网络连接正常,或稍后重试。'
})
else:
return jsonify({'error': f'无法获取股票列表: {str(e)}'})
if not stock_list:
return jsonify({
'error': '无法获取股票列表,请检查网络连接后重试',
'error_type': 'network_error',
'suggestion': '请确保网络连接正常,或稍后重试。'
})
results = []
processed_count = 0
total_count = len(stock_list)
failed_count = 0
for stock in stock_list:
try:
symbol = stock['symbol']
name = stock['name']
processed_count += 1
# 获取股票K线数据
df = get_a_stock_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time)
if df is None or len(df) < 3:
failed_count += 1
# 如果连续失败太多,可能是网络问题
if failed_count > 10 and len(results) == 0:
return jsonify({
'error': '网络连接不稳定,无法获取股票数据。请检查网络连接后重试。',
'error_type': 'network_error',
'processed_count': processed_count,
'failed_count': failed_count
})
continue
# 进行缠论分析
analysis_result = analyze_chan(df)
if not analysis_result or 'klc_fx_info' not in analysis_result:
continue
klc_fx_info = analysis_result['klc_fx_info']
# 检查最近2个KLC是否有满足条件的分型
recent_klcs = klc_fx_info[-2:] if len(klc_fx_info) >= 2 else klc_fx_info
for klc_info in recent_klcs:
fx_strength = klc_info.get('fx_strength', 0)
fx_type = klc_info.get('fx_type', 'UNKNOWN')
# 检查是否满足条件:分型强度>=阈值 且 分型类型不为UNKNOWN
if fx_strength >= fx_strength_threshold and fx_type != 'UNKNOWN':
# 获取当前价格(最新收盘价)
current_price = df['close'].iloc[-1] if len(df) > 0 else None
fx_price = klc_info.get('price', 0)
# 计算涨跌幅
change_percent = 0
if current_price and fx_price and fx_price > 0:
change_percent = ((current_price - fx_price) / fx_price) * 100
# 格式化分型类型显示
fx_type_display = format_fx_type(fx_type)
results.append({
'symbol': symbol,
'name': name,
'fx_time': klc_info.get('time', ''),
'fx_type': fx_type_display,
'fx_strength': fx_strength,
'fx_price': fx_price,
'current_price': current_price,
'change_percent': change_percent
})
break # 找到一个满足条件的就跳出循环
except Exception as e:
failed_count += 1
continue
# 按分型强度降序排列
results.sort(key=lambda x: x['fx_strength'], reverse=True)
return jsonify({
'results': results,
'total_processed': processed_count,
'total_found': len(results),
'failed_count': failed_count,
'data_source': data_source,
'message': f'使用{data_source}进行筛选,共处理{processed_count}只股票,找到{len(results)}只满足条件的股票'
})
except Exception as e:
# 检查是否是网络连接问题
if "timeout" in str(e).lower() or "connection" in str(e).lower() or "network" in str(e).lower():
return jsonify({
'error': '网络连接超时,请检查网络连接后重试。',
'error_type': 'network_error',
'suggestion': '请确保网络连接正常,或稍后重试。'
})
else:
return jsonify({'error': str(e)})
def format_fx_type(fx_type):
"""格式化分型类型显示"""
fx_type_map = {
'TOP1': '顶分型1',
'TOP2': '顶分型2',
'TOP3': '顶分型3',
'BOTTOM1': '底分型1',
'BOTTOM2': '底分型2',
'BOTTOM3': '底分型3',
'TOP': '顶分型',
'BOTTOM': '底分型'
}
return fx_type_map.get(fx_type, fx_type)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8128)