1916 lines
73 KiB
Python
1916 lines
73 KiB
Python
from flask import Flask, render_template, jsonify, request
|
||
from collections import OrderedDict
|
||
import json
|
||
import logging
|
||
import ccxt
|
||
import pandas as pd
|
||
import requests
|
||
from datetime import datetime, timedelta
|
||
import sys
|
||
import os
|
||
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, TF_DF
|
||
from ChanEnum import Chan_BI_DIR, Chan_SEG_DIR, Chan_KLC_FX, Chan_FX_TYPE, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR
|
||
from cn_stock_data import ChinaStockData
|
||
from ChanMACD import ChanMACD
|
||
|
||
# 添加买卖点枚举类型
|
||
class TRADE_POINT_TYPE:
|
||
BUY1 = 1 # 一类买点
|
||
BUY2 = 2 # 二类买点
|
||
BUY3 = 3 # 三类买点
|
||
SELL1 = -1 # 一类卖点
|
||
SELL2 = -2 # 二类卖点
|
||
SELL3 = -3 # 三类卖点
|
||
|
||
app = Flask(__name__)
|
||
macd_factor = 2
|
||
smooth_factor = 1
|
||
macd_fast_period = 12 * macd_factor
|
||
macd_slow_period = 26 * macd_factor
|
||
macd_signal_period = 9 * smooth_factor
|
||
# 初始化交易所
|
||
exchange = ccxt.binance({
|
||
'enableRateLimit': True,
|
||
})
|
||
|
||
# 初始化A股数据获取器
|
||
china_stock = ChinaStockData()
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://127.0.0.1:9009"))
|
||
#DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://192.168.1.9:9009"))
|
||
DEFAULT_TIMEFRAME_LABELS = OrderedDict([
|
||
("1m", "1分钟"),
|
||
("3m", "3分钟"),
|
||
("5m", "5分钟"),
|
||
("15m", "15分钟"),
|
||
("30m", "30分钟"),
|
||
("1h", "1小时"),
|
||
("2h", "2小时"),
|
||
("4h", "4小时"),
|
||
("6h", "6小时"),
|
||
("8h", "8小时"),
|
||
("12h", "12小时"),
|
||
("1d", "日线"),
|
||
("3d", "3日线"),
|
||
("1w", "周线"),
|
||
("1M", "月线"),
|
||
])
|
||
|
||
DEFAULT_SYMBOLS = [
|
||
'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT', 'WIF/USDT:USDT',
|
||
'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT'
|
||
]
|
||
|
||
TIMEFRAMES = DEFAULT_TIMEFRAME_LABELS.copy()
|
||
SYMBOLS = DEFAULT_SYMBOLS.copy()
|
||
DATA_SERVICE_AVAILABLE = False
|
||
SERVICE_METADATA_LAST_REFRESH = 0
|
||
|
||
|
||
def timeframe_to_minutes(tf: str):
|
||
"""将时间周期转换为分钟数,用于排序。"""
|
||
if not tf:
|
||
return None
|
||
unit = tf[-1]
|
||
try:
|
||
value = int(tf[:-1])
|
||
except (ValueError, TypeError):
|
||
return None
|
||
multiplier = {
|
||
'm': 1,
|
||
'h': 60,
|
||
'd': 1440,
|
||
'w': 10080,
|
||
'M': 43200, # 30天近似
|
||
}.get(unit)
|
||
if multiplier is None:
|
||
return None
|
||
return value * multiplier
|
||
|
||
|
||
def format_timeframe_label(tf: str) -> str:
|
||
"""将时间周期转换为可读标签。"""
|
||
if not tf:
|
||
return tf
|
||
unit = tf[-1]
|
||
try:
|
||
value = int(tf[:-1])
|
||
except (ValueError, TypeError):
|
||
return tf
|
||
if unit == 'm':
|
||
return f"{value}分钟"
|
||
if unit == 'h':
|
||
return f"{value}小时"
|
||
if unit == 'd':
|
||
return "日线" if value == 1 else f"{value}日线"
|
||
if unit == 'w':
|
||
return "周线" if value == 1 else f"{value}周线"
|
||
if unit == 'M':
|
||
return "月线" if value == 1 else f"{value}月线"
|
||
return tf
|
||
|
||
|
||
def build_timeframe_labels(timeframes):
|
||
ordered = sorted(
|
||
timeframes,
|
||
key=lambda tf: timeframe_to_minutes(tf) if timeframe_to_minutes(tf) is not None else float('inf'),
|
||
)
|
||
labels = OrderedDict()
|
||
for tf in ordered:
|
||
labels[tf] = format_timeframe_label(tf)
|
||
return labels
|
||
|
||
|
||
def _parse_time_input(value):
|
||
if value in (None, '', 0):
|
||
return None
|
||
try:
|
||
return int(float(value))
|
||
except (ValueError, TypeError):
|
||
return None
|
||
|
||
|
||
def refresh_data_service_metadata(force=False):
|
||
"""刷新数据服务提供的交易对与周期元信息。"""
|
||
global DATA_SERVICE_AVAILABLE, TIMEFRAMES, SYMBOLS, SERVICE_METADATA_LAST_REFRESH
|
||
now = time.time()
|
||
if not force and DATA_SERVICE_AVAILABLE and now - SERVICE_METADATA_LAST_REFRESH < 60:
|
||
return True
|
||
try:
|
||
resp = requests.get(f"{DATA_SERVICE_URL}/health", timeout=5)
|
||
resp.raise_for_status()
|
||
payload = resp.json()
|
||
service_symbols = payload.get("symbols") or payload.get("symbol_list") or []
|
||
base_timeframes = payload.get("timeframes") or payload.get("base_timeframes") or []
|
||
derived = payload.get("derived_timeframes") or []
|
||
service_timeframes = list(base_timeframes)
|
||
for tf in derived:
|
||
if tf not in service_timeframes:
|
||
service_timeframes.append(tf)
|
||
if service_symbols:
|
||
SYMBOLS[:] = service_symbols
|
||
if service_timeframes:
|
||
TIMEFRAMES.clear()
|
||
TIMEFRAMES.update(build_timeframe_labels(service_timeframes))
|
||
DATA_SERVICE_AVAILABLE = True
|
||
SERVICE_METADATA_LAST_REFRESH = now
|
||
return True
|
||
except Exception as exc:
|
||
logger.warning("无法加载数据服务元信息: %s", exc)
|
||
if not DATA_SERVICE_AVAILABLE:
|
||
TIMEFRAMES.clear()
|
||
TIMEFRAMES.update(DEFAULT_TIMEFRAME_LABELS)
|
||
SYMBOLS[:] = DEFAULT_SYMBOLS
|
||
DATA_SERVICE_AVAILABLE = False
|
||
return False
|
||
|
||
|
||
def _fetch_kl_from_datasvc(symbol, timeframe, start_ms=None, end_ms=None, limit=None):
|
||
params = {"symbol": symbol, "tf": timeframe}
|
||
if start_ms is not None:
|
||
params["start"] = int(start_ms)
|
||
if end_ms is not None:
|
||
params["end"] = int(end_ms)
|
||
resp = requests.get(f"{DATA_SERVICE_URL}/api/candles", params=params, timeout=10)
|
||
resp.raise_for_status()
|
||
data = resp.json()
|
||
if not data:
|
||
return None
|
||
df = pd.DataFrame(data)
|
||
if df.empty or "timestamp" not in df.columns:
|
||
return None
|
||
numeric_cols = ["open", "high", "low", "close", "volume"]
|
||
df["timestamp"] = pd.to_numeric(df["timestamp"], errors="coerce")
|
||
df = df.dropna(subset=["timestamp"])
|
||
df["timestamp"] = df["timestamp"].astype("int64")
|
||
for col in numeric_cols:
|
||
if col in df.columns:
|
||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||
df = df.dropna(subset=numeric_cols)
|
||
df = df.sort_values("timestamp")
|
||
if limit and len(df) > limit:
|
||
df = df.tail(limit)
|
||
df = df.reset_index(drop=True)
|
||
df["date"] = pd.to_datetime(df["timestamp"], unit='ms', utc=True).dt.tz_convert('Asia/Shanghai')
|
||
return df
|
||
|
||
|
||
# 模块加载时尝试预取一次元信息,但失败不阻塞后续流程
|
||
refresh_data_service_metadata(force=True)
|
||
|
||
# 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=100000, 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_via_ccxt(symbol, timeframe, limit=100000, 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 = 1000 # 分钟级数据减少批次大小
|
||
elif timeframe in ['15m', '30m', '1h']:
|
||
batch_size = 1000
|
||
else:
|
||
batch_size = 1500 # 日线及以上可以获取更多
|
||
batch_size = 1500 # 默认批次大小
|
||
# 初始化存储所有K线数据的列表
|
||
all_ohlcv = []
|
||
|
||
# 初始化当前查询的开始时间
|
||
current_since = since
|
||
|
||
# 添加请求计数和最大限制
|
||
request_count = 0
|
||
max_requests = 300 # 最大请求次数,防止无限循环
|
||
|
||
# 分页加载数据
|
||
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) > 100000: # 防止数据量过大,设置一个合理的上限
|
||
df = df.tail(100000).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_crypto_kl_data(symbol, timeframe, limit=100000, start_time=None, end_time=None):
|
||
"""优先通过本地数据服务获取加密货币K线,失败时回退至交易所API。"""
|
||
start_ms = _parse_time_input(start_time)
|
||
end_ms = _parse_time_input(end_time)
|
||
|
||
refresh_data_service_metadata()
|
||
if DATA_SERVICE_AVAILABLE:
|
||
try:
|
||
df = _fetch_kl_from_datasvc(
|
||
symbol=symbol,
|
||
timeframe=timeframe,
|
||
start_ms=start_ms,
|
||
end_ms=end_ms,
|
||
limit=limit,
|
||
)
|
||
if df is not None and not df.empty:
|
||
return df
|
||
except Exception as exc:
|
||
logger.warning("数据服务请求失败,准备回退至交易所 API:%s", exc)
|
||
|
||
return _get_crypto_kl_data_via_ccxt(symbol, timeframe, limit, start_time, end_time)
|
||
|
||
|
||
def get_a_stock_kl_data(symbol, timeframe, limit=100000, 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):
|
||
global macd_fast_period, macd_slow_period, macd_signal_period
|
||
macd = ta.MACD(df, fastperiod=macd_fast_period, slowperiod=macd_slow_period, signalperiod=macd_signal_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)
|
||
# 新增 EMA 指标
|
||
df['ema5'] = (ta.EMA(df, timeperiod=5)).fillna(0)
|
||
df['ema10'] = (ta.EMA(df, timeperiod=10)).fillna(0)
|
||
df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0)
|
||
df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0)
|
||
df['ema26'] = (ta.EMA(df, timeperiod=26)).fillna(0)
|
||
df['ema13'] = (ta.EMA(df, timeperiod=13)).fillna(0)
|
||
df['ema7'] = (ta.EMA(df, timeperiod=7)).fillna(0)
|
||
df['ema104'] = (ta.EMA(df, timeperiod=104)).fillna(0)
|
||
df['ema156'] = (ta.EMA(df, timeperiod=156)).fillna(0)
|
||
df['ema208'] = (ta.EMA(df, timeperiod=208)).fillna(0)
|
||
# 常用SMA 24/52
|
||
try:
|
||
df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0)
|
||
df['sma52'] = (ta.SMA(df, timeperiod=52)).fillna(0)
|
||
except Exception:
|
||
df['sma24'] = 0
|
||
df['sma52'] = 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)
|
||
bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0)
|
||
#bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||
df['bbup30'] = bb30['upperband'].fillna(0)
|
||
df['bblow30'] = bb30['lowerband'].fillna(0)
|
||
bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||
#bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||
df['bbup302'] = bb302['upperband'].fillna(0)
|
||
df['bblow302'] = bb302['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['ema5'] = df['ema5'].fillna(0)
|
||
df['ema10'] = df['ema10'].fillna(0)
|
||
df['ema24'] = df['ema24'].fillna(0)
|
||
df['ema52'] = df['ema52'].fillna(0)
|
||
df['sma24'] = df['sma24'].fillna(0)
|
||
df['sma52'] = df['sma52'].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)
|
||
bb2633 = ta.BBANDS(df, timeperiod=26, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||
bbp2633 = (df['close'] - bb2633['lowerband']) / (bb2633['upperband'] - bb2633['lowerband'])
|
||
df['bb2633upper'] = bb2633['upperband'].fillna(0)
|
||
df['bb2633lower'] = bb2633['lowerband'].fillna(0)
|
||
df['bbp2633'] = bbp2633.fillna(0)
|
||
df['bb2633middle'] = bb2633['middleband'].fillna(0)
|
||
return df
|
||
|
||
def calculate_macd(df):
|
||
"""计算MACD指标"""
|
||
global macd_fast_period, macd_slow_period, macd_signal_period
|
||
exp1 = df['close'].ewm(span=macd_fast_period, adjust=False).mean()
|
||
exp2 = df['close'].ewm(span=macd_slow_period, adjust=False).mean()
|
||
macd = exp1 - exp2
|
||
signal = macd.ewm(span=macd_signal_period, adjust=False).mean()
|
||
histogram = macd - signal
|
||
|
||
return {
|
||
'macd': macd.tolist(),
|
||
'signal': signal.tolist(),
|
||
'histogram': histogram.tolist()
|
||
}
|
||
|
||
def analyze_chan(df, symbol=None, timeframe=None):
|
||
"""进行缠论分析"""
|
||
chan = TF_DF()
|
||
|
||
# 初始化多时间周期数据以获取EMA52
|
||
ema52_dict = None
|
||
# 获取分析结果
|
||
klu_list = chan.get_kl_data(df)
|
||
klc_list = chan.get_klc_list(klu_list)
|
||
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_seg_zs(seg_list)
|
||
# 计算笔中枢(BI中枢)并拍平成列表
|
||
|
||
#bi_zs_list = chan.cal_bi_zs_list(bi_list)
|
||
bi_zs_list = chan.cal_bi_zs(seg_list)
|
||
bsp_list = []
|
||
if len(bi_zs_list) > 0:
|
||
bsp_list = chan.find_all_bsp(bi_list, bi_zs_list)
|
||
#for bsp in bsp_list:
|
||
#print(bsp.end_time, bsp.type, bsp.dir)
|
||
# 添加买卖点识别
|
||
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)
|
||
|
||
# 添加ChanMACD分析
|
||
chan_macd = None
|
||
chan_macd_data = {}
|
||
try:
|
||
|
||
if klu_list and len(klu_list) > 0:
|
||
print(f"获取到KLU列表,长度: {len(klu_list)}")
|
||
chan_macd = ChanMACD(klu_list)
|
||
chan_macd_data = {
|
||
'seg_list': chan_macd.seg_list,
|
||
'unittf_list': chan_macd.unittf_list,
|
||
'histset_list': chan_macd.histset_list,
|
||
'klu_list': chan_macd.klu_list,
|
||
'high_position_list': chan_macd.high_position_list,
|
||
'high_empty_list': chan_macd.high_empty_list,
|
||
'low_position_list': getattr(chan_macd, 'low_position_list', []),
|
||
'low_empty_list': getattr(chan_macd, 'low_empty_list', []),
|
||
'return_zero_list': chan_macd.return_zero_list,
|
||
'cross0_up_list': chan_macd.cross0_up_list,
|
||
'cross0_down_list': chan_macd.cross0_down_list
|
||
}
|
||
print(f"ChanMACD分析完成: seg={len(chan_macd.seg_list)}, unittf={len(chan_macd.unittf_list)}, histset={len(chan_macd.histset_list)}")
|
||
else:
|
||
print("未能获取KLU列表或列表为空")
|
||
chan_macd_data = {
|
||
'seg_list': [],
|
||
'unittf_list': [],
|
||
'histset_list': [],
|
||
'high_position_list': [],
|
||
'high_empty_list': [],
|
||
'return_zero_list': [],
|
||
'cross0_up_list': [],
|
||
'cross0_down_list': []
|
||
}
|
||
except Exception as e:
|
||
print(f"ChanMACD分析出错: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
chan_macd_data = {
|
||
'seg_list': [],
|
||
'unittf_list': [],
|
||
'histset_list': [],
|
||
'high_position_list': [],
|
||
'high_empty_list': [],
|
||
'low_position_list': [],
|
||
'low_empty_list': [],
|
||
'return_zero_list': [],
|
||
'cross0_up_list': [],
|
||
'cross0_down_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 分型框(起止时间+高低价):
|
||
# 仅使用 cal_fx_box 通过 display 条件后生成的 klc.fx_box。
|
||
# 若无 fx_box,则前端不应绘制分型框。
|
||
fx_box = getattr(klc, 'fx_box', None)
|
||
box_start_time = getattr(fx_box, 'start_time', None) if fx_box else None
|
||
box_end_time = getattr(fx_box, 'end_time', None) if fx_box else None
|
||
box_high = getattr(fx_box, 'high', None) if fx_box else None
|
||
box_low = getattr(fx_box, 'low', None) if fx_box else None
|
||
|
||
if klc.bb_out:
|
||
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, # 是否为强分型
|
||
|
||
# 虚线分型框信息(给前端画框用)
|
||
'start_time': box_start_time,
|
||
'end_time': box_end_time,
|
||
'high': float(box_high) if box_high is not None else None,
|
||
'low': float(box_low) if box_low is not None else None,
|
||
})
|
||
except Exception as e:
|
||
# 如果出错,仍然添加基本信息,但分型强度为0
|
||
fx_box = getattr(klc, 'fx_box', None)
|
||
box_start_time = getattr(fx_box, 'start_time', None) if fx_box else None
|
||
box_end_time = getattr(fx_box, 'end_time', None) if fx_box else None
|
||
box_high = getattr(fx_box, 'high', None) if fx_box else None
|
||
box_low = getattr(fx_box, 'low', None) if fx_box else None
|
||
|
||
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,
|
||
|
||
# 虚线分型框信息(给前端画框用)
|
||
'start_time': box_start_time,
|
||
'end_time': box_end_time,
|
||
'high': float(box_high) if box_high is not None else None,
|
||
'low': float(box_low) if box_low is not None else None,
|
||
})
|
||
|
||
|
||
return {
|
||
'klc_list': klc_list,
|
||
'klu_list': klu_list, # 添加KLU列表
|
||
'bi_list': bi_list,
|
||
'seg_list': seg_list,
|
||
'zs_list': zs_list,
|
||
'bi_zs_list': bi_zs_list, # 添加BI中枢列表
|
||
'bsp_list': bsp_list, # 添加买卖点列表
|
||
'klc_fx_info': klc_fx_info, # KLC分型信息
|
||
'chan_macd': chan_macd_data, # 添加ChanMACD分析数据
|
||
'ema52_dict': ema52_dict # 添加多时间周期EMA52数据
|
||
}
|
||
|
||
# 辅助函数,转换缠论方向枚举为整数
|
||
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 serialize_chan_macd_data(chan_macd_data, client_tz):
|
||
"""序列化ChanMACD数据为JSON可序列化格式"""
|
||
serialized_data = {
|
||
'seg_list': [],
|
||
'unittf_list': [],
|
||
'histset_list': [],
|
||
# 状态标记数据
|
||
'high_position_list': [],
|
||
'high_empty_list': [],
|
||
'low_position_list': [],
|
||
'low_empty_list': [],
|
||
'return_zero_list': [],
|
||
'cross0_up_list': [],
|
||
'cross0_down_list': [],
|
||
# 新增:输出KLU的继续背驰/分离背驰标志
|
||
'klu_list': []
|
||
}
|
||
|
||
# 序列化seg_list
|
||
for seg in chan_macd_data.get('seg_list', []):
|
||
try:
|
||
seg_data = {
|
||
'start_time': format_time_safely(seg.start_time, client_tz),
|
||
'end_time': format_time_safely(seg.end_time, client_tz) if seg.end_time else None,
|
||
'seg_dir': 'ABOVE' if seg.seg_dir == Chan_MACDSEG_DIR.ABOVE else 'UNDER',
|
||
'klu_count': len(seg.klu_list) if hasattr(seg, 'klu_list') else 0,
|
||
'unittf_count': len(seg.unittf_list) if hasattr(seg, 'unittf_list') else 0,
|
||
'histset_count': len(seg.hist_set) if hasattr(seg, 'hist_set') else 0
|
||
}
|
||
serialized_data['seg_list'].append(seg_data)
|
||
except Exception as e:
|
||
print(f"序列化seg出错: {e}")
|
||
continue
|
||
|
||
# 序列化unittf_list(兼容新结构与枚举类型)
|
||
for unittf in chan_macd_data.get('unittf_list', []):
|
||
try:
|
||
dir_value = getattr(unittf, 'uinttf_dir', None)
|
||
dir_name = getattr(dir_value, 'name', dir_value if isinstance(dir_value, str) else None)
|
||
start_t = getattr(unittf, 'start_type', None)
|
||
start_type = getattr(start_t, 'name', start_t)
|
||
end_t = getattr(unittf, 'end_type', None)
|
||
end_type = getattr(end_t, 'name', end_t)
|
||
peak_abs = getattr(unittf, 'peak_abs', None)
|
||
if peak_abs is None:
|
||
peak_abs = getattr(unittf, 'peak_hist', None)
|
||
length = getattr(unittf, 'length', None)
|
||
if length is None:
|
||
length = len(unittf.klu_list) if hasattr(unittf, 'klu_list') else None
|
||
|
||
unittf_data = {
|
||
'start_time': format_time_safely(getattr(unittf, 'start_time', None), client_tz),
|
||
'end_time': format_time_safely(getattr(unittf, 'end_time', None), client_tz) if getattr(unittf, 'end_time', None) else None,
|
||
'dir': dir_name, # 'ABOVE' | 'UNDER' | None
|
||
'start_type': start_type, # e.g. 'START' | 'CROSS0' | 'NEAR0_UP' | 'NEAR0_DOWN'
|
||
'end_type': end_type,
|
||
'invalid': getattr(unittf, 'invalid', False),
|
||
'peak_abs': peak_abs,
|
||
'length': length,
|
||
'klu_count': len(unittf.klu_list) if hasattr(unittf, 'klu_list') else 0,
|
||
'histset_count': len(unittf.histset_list) if hasattr(unittf, 'histset_list') else 0
|
||
}
|
||
serialized_data['unittf_list'].append(unittf_data)
|
||
except Exception as e:
|
||
print(f"序列化unittf出错: {e}")
|
||
continue
|
||
|
||
# 序列化histset_list
|
||
for histset in chan_macd_data.get('histset_list', []):
|
||
try:
|
||
histset_data = {
|
||
'start_time': format_time_safely(getattr(histset, 'start_time', None), client_tz),
|
||
'end_time': format_time_safely(getattr(histset, 'end_time', None), client_tz),
|
||
'histset_dir': 'ABOVE' if histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE else 'UNDER',
|
||
'klu_count': len(histset.klu_list) if hasattr(histset, 'klu_list') else 0
|
||
}
|
||
serialized_data['histset_list'].append(histset_data)
|
||
except Exception as e:
|
||
print(f"序列化histset出错: {e}")
|
||
continue
|
||
|
||
# 序列化状态标记数据
|
||
# 序列化高位列表
|
||
for high_pos in chan_macd_data.get('high_position_list', []):
|
||
try:
|
||
high_pos_data = {
|
||
'time': format_time_safely(high_pos['time'], client_tz),
|
||
'end_time': format_time_safely(high_pos.get('end_time'), client_tz) if high_pos.get('end_time') else None,
|
||
'type': high_pos.get('type', 'start'),
|
||
'macd': high_pos.get('macd'),
|
||
'signal': high_pos.get('signal'),
|
||
'macdhist': high_pos.get('macdhist'),
|
||
'end_macd': high_pos.get('end_macd'),
|
||
'end_signal': high_pos.get('end_signal'),
|
||
'end_macdhist': high_pos.get('end_macdhist')
|
||
}
|
||
serialized_data['high_position_list'].append(high_pos_data)
|
||
except Exception as e:
|
||
print(f"序列化high_position出错: {e}")
|
||
continue
|
||
|
||
# 序列化高位空列表
|
||
for high_empty in chan_macd_data.get('high_empty_list', []):
|
||
try:
|
||
high_empty_data = {
|
||
'time': format_time_safely(high_empty['time'], client_tz),
|
||
'end_time': format_time_safely(high_empty.get('end_time'), client_tz) if high_empty.get('end_time') else None,
|
||
'type': high_empty.get('type', 'start'),
|
||
'macd': high_empty.get('macd'),
|
||
'signal': high_empty.get('signal'),
|
||
'macdhist': high_empty.get('macdhist'),
|
||
'end_macd': high_empty.get('end_macd'),
|
||
'end_signal': high_empty.get('end_signal'),
|
||
'end_macdhist': high_empty.get('end_macdhist')
|
||
}
|
||
serialized_data['high_empty_list'].append(high_empty_data)
|
||
except Exception as e:
|
||
print(f"序列化high_empty出错: {e}")
|
||
continue
|
||
|
||
# 序列化低位与低位空
|
||
for low_pos in chan_macd_data.get('low_position_list', []):
|
||
try:
|
||
low_pos_data = {
|
||
'time': format_time_safely(low_pos['time'], client_tz),
|
||
'end_time': format_time_safely(low_pos.get('end_time'), client_tz) if low_pos.get('end_time') else None,
|
||
'type': low_pos.get('type', 'start'),
|
||
'macd': low_pos.get('macd'),
|
||
'signal': low_pos.get('signal'),
|
||
'macdhist': low_pos.get('macdhist'),
|
||
'end_macd': low_pos.get('end_macd'),
|
||
'end_signal': low_pos.get('end_signal'),
|
||
'end_macdhist': low_pos.get('end_macdhist')
|
||
}
|
||
serialized_data['low_position_list'].append(low_pos_data)
|
||
except Exception as e:
|
||
print(f"序列化low_position出错: {e}")
|
||
continue
|
||
|
||
for low_empty in chan_macd_data.get('low_empty_list', []):
|
||
try:
|
||
low_empty_data = {
|
||
'time': format_time_safely(low_empty['time'], client_tz),
|
||
'end_time': format_time_safely(low_empty.get('end_time'), client_tz) if low_empty.get('end_time') else None,
|
||
'type': low_empty.get('type', 'start'),
|
||
'macd': low_empty.get('macd'),
|
||
'signal': low_empty.get('signal'),
|
||
'macdhist': low_empty.get('macdhist'),
|
||
'end_macd': low_empty.get('end_macd'),
|
||
'end_signal': low_empty.get('end_signal'),
|
||
'end_macdhist': low_empty.get('end_macdhist')
|
||
}
|
||
serialized_data['low_empty_list'].append(low_empty_data)
|
||
except Exception as e:
|
||
print(f"序列化low_empty出错: {e}")
|
||
continue
|
||
|
||
# 序列化归零轴列表
|
||
for return_zero in chan_macd_data.get('return_zero_list', []):
|
||
try:
|
||
return_zero_data = {
|
||
'time': format_time_safely(return_zero['time'], client_tz),
|
||
'end_time': format_time_safely(return_zero.get('end_time'), client_tz) if return_zero.get('end_time') else None,
|
||
'type': return_zero.get('type', 'start'),
|
||
'macd': return_zero.get('macd'),
|
||
'signal': return_zero.get('signal'),
|
||
'macdhist': return_zero.get('macdhist'),
|
||
'end_macd': return_zero.get('end_macd'),
|
||
'end_signal': return_zero.get('end_signal'),
|
||
'end_macdhist': return_zero.get('end_macdhist')
|
||
}
|
||
serialized_data['return_zero_list'].append(return_zero_data)
|
||
except Exception as e:
|
||
print(f"序列化return_zero出错: {e}")
|
||
continue
|
||
|
||
# 序列化穿越零轴列表
|
||
for cross0_up in chan_macd_data.get('cross0_up_list', []):
|
||
try:
|
||
cross0_up_data = {
|
||
'time': format_time_safely(cross0_up['time'], client_tz),
|
||
'type': cross0_up.get('type', 'start'),
|
||
'macd': cross0_up.get('macd'),
|
||
'signal': cross0_up.get('signal'),
|
||
'macdhist': cross0_up.get('macdhist')
|
||
}
|
||
serialized_data['cross0_up_list'].append(cross0_up_data)
|
||
except Exception as e:
|
||
print(f"序列化cross0_up出错: {e}")
|
||
continue
|
||
|
||
for cross0_down in chan_macd_data.get('cross0_down_list', []):
|
||
try:
|
||
cross0_down_data = {
|
||
'time': format_time_safely(cross0_down['time'], client_tz),
|
||
'type': cross0_down.get('type', 'start'),
|
||
'macd': cross0_down.get('macd'),
|
||
'signal': cross0_down.get('signal'),
|
||
'macdhist': cross0_down.get('macdhist')
|
||
}
|
||
serialized_data['cross0_down_list'].append(cross0_down_data)
|
||
except Exception as e:
|
||
print(f"序列化cross0_down出错: {e}")
|
||
continue
|
||
|
||
# 序列化 KLU 列表(仅导出需要的时间与背驰标志)
|
||
for klu in chan_macd_data.get('klu_list', []):
|
||
try:
|
||
serialized_data['klu_list'].append({
|
||
'time': format_time_safely(getattr(klu, 'time', None), client_tz),
|
||
'continue_div': bool(getattr(klu, 'continue_div', False)),
|
||
'separate_div': int(getattr(klu, 'separate_div', 0)) if getattr(klu, 'separate_div', 0) is not None else 0,
|
||
'near0_return': int(getattr(klu, 'near0_return', 0)) if getattr(klu, 'near0_return', 0) is not None else 0
|
||
})
|
||
except Exception as e:
|
||
print(f"序列化klu出错: {e}")
|
||
continue
|
||
|
||
return serialized_data
|
||
|
||
def is_smaller_timeframe(tf1, tf2):
|
||
"""判断时间周期tf1是否小于tf2"""
|
||
tf1_value = timeframe_to_minutes(tf1)
|
||
tf2_value = timeframe_to_minutes(tf2)
|
||
if tf1_value is None or tf2_value is None:
|
||
return False
|
||
return tf1_value < tf2_value
|
||
|
||
def is_smaller_or_equal_timeframe(tf1, tf2):
|
||
"""判断时间周期tf1是否小于等于tf2"""
|
||
tf1_value = timeframe_to_minutes(tf1)
|
||
tf2_value = timeframe_to_minutes(tf2)
|
||
if tf1_value is None or tf2_value is None:
|
||
return False
|
||
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
|
||
|
||
# ====== 趋势判定与趋势筛选(币对) ======
|
||
|
||
def classify_trend_stage(df):
|
||
"""根据 EMA 斜率与多空排列判断趋势方向与阶段
|
||
返回: direction in {"bull","bear","sideways"}, stage in {"early","mid","late"}, strength_score (0-100)
|
||
"""
|
||
if df is None or len(df) < 60:
|
||
return "sideways", "early", 0
|
||
|
||
# 使用 EMA5/10/24/52
|
||
closes = df['close'].values
|
||
ema5 = df['ema5'].values if 'ema5' in df else ta.EMA(df, timeperiod=5)
|
||
ema10 = df['ema10'].values if 'ema10' in df else ta.EMA(df, timeperiod=10)
|
||
ema24 = df['ema24'].values if 'ema24' in df else ta.EMA(df, timeperiod=24)
|
||
ema52 = df['ema52'].values if 'ema52' in df else ta.EMA(df, timeperiod=52)
|
||
|
||
# 最近N根用于斜率与排列判定
|
||
lookback = min(30, len(df) - 1)
|
||
if lookback <= 5:
|
||
return "sideways", "early", 0
|
||
|
||
# 简单斜率: 最近k根的线性变化率近似
|
||
def slope(arr, k=10):
|
||
k = min(k, len(arr) - 1)
|
||
if k < 2:
|
||
return 0.0
|
||
y = arr[-k:]
|
||
x = np.arange(k)
|
||
# 最小二乘拟合斜率
|
||
denom = np.dot(x - x.mean(), x - x.mean())
|
||
if denom == 0:
|
||
return 0.0
|
||
m = np.dot(y - y.mean(), x - x.mean()) / denom
|
||
return float(m)
|
||
|
||
k_slope = 12 # 斜率窗口
|
||
s5 = slope(ema5, k_slope)
|
||
s10 = slope(ema10, k_slope)
|
||
s24 = slope(ema24, k_slope)
|
||
s52 = slope(ema52, k_slope)
|
||
|
||
# 多空排列
|
||
last5, last10, last24, last52 = ema5[-1], ema10[-1], ema24[-1], ema52[-1]
|
||
bull_stack = last5 > last10 > last24 > last52
|
||
bear_stack = last5 < last10 < last24 < last52
|
||
|
||
# 波动性与动量增强: MACD 柱体最近均值
|
||
macdhist = df['macdhist'].values if 'macdhist' in df else calculate_macd(df)['histogram']
|
||
hist_recent = macdhist[-lookback:]
|
||
hist_power = float(np.mean(np.abs(hist_recent))) if len(hist_recent) else 0.0
|
||
|
||
# 方向
|
||
if bull_stack and s24 > 0 and s52 > 0:
|
||
direction = "bull"
|
||
elif bear_stack and s24 < 0 and s52 < 0:
|
||
direction = "bear"
|
||
else:
|
||
# 用价格相对 EMA52 辅助
|
||
if closes[-1] > last52 and (s24 + s52) > 0:
|
||
direction = "bull"
|
||
elif closes[-1] < last52 and (s24 + s52) < 0:
|
||
direction = "bear"
|
||
else:
|
||
direction = "sideways"
|
||
|
||
# 阶段: 依据(斜率大小、与EMA52距离、MACD柱体扩张/收敛)
|
||
dist52 = float((closes[-1] - last52) / last52) if last52 else 0.0
|
||
slope_score = max(0.0, (abs(s24) + abs(s52)) * 1000.0) # 归一化
|
||
dist_score = min(50.0, abs(dist52) * 200.0)
|
||
hist_score = min(30.0, hist_power * 10.0)
|
||
strength = float(min(100.0, slope_score + dist_score + hist_score))
|
||
|
||
# 简单阶段判定
|
||
if direction == "sideways":
|
||
stage = "early"
|
||
strength = min(strength, 30.0)
|
||
else:
|
||
# 查看最近 hist 是否在扩大或收敛
|
||
if len(hist_recent) >= 6:
|
||
recent_growth = np.mean(np.abs(hist_recent[-3:])) - np.mean(np.abs(hist_recent[-6:-3]))
|
||
else:
|
||
recent_growth = 0.0
|
||
|
||
if recent_growth > 0 and abs(dist52) < 0.05:
|
||
stage = "early"
|
||
elif recent_growth > 0 and abs(dist52) >= 0.05:
|
||
stage = "mid"
|
||
else:
|
||
stage = "late"
|
||
|
||
return direction, stage, strength
|
||
|
||
|
||
def load_crypto_symbols(limit=200):
|
||
"""加载常见USDT永续合约交易对,返回列表"""
|
||
refresh_data_service_metadata()
|
||
if SYMBOLS:
|
||
return SYMBOLS[:limit]
|
||
try:
|
||
markets = exchange.load_markets()
|
||
symbols = [s for s in markets.keys() if '/USDT' in s and ':USDT' in s]
|
||
return symbols[:limit]
|
||
except Exception:
|
||
return DEFAULT_SYMBOLS[:limit]
|
||
|
||
|
||
@app.route('/api/trend_filter', methods=['GET'])
|
||
def trend_filter():
|
||
"""趋势筛选接口(币对)
|
||
参数:
|
||
timeframe: K线周期
|
||
start_time, end_time: 毫秒时间戳,可选
|
||
direction: bull/bear/sideways 可选
|
||
stage: early/mid/late 可选
|
||
min_strength: 0-100 可选
|
||
symbols: 逗号分隔列表,可选;不传则自动加载部分USDT币对
|
||
返回符合条件的币对与简要统计
|
||
"""
|
||
timeframe = request.args.get('timeframe', '1h')
|
||
start_time = request.args.get('start_time')
|
||
end_time = request.args.get('end_time')
|
||
want_direction = request.args.get('direction') # 可为 None
|
||
want_stage = request.args.get('stage') # 可为 None
|
||
try:
|
||
min_strength = float(request.args.get('min_strength', '0'))
|
||
except ValueError:
|
||
min_strength = 0.0
|
||
|
||
symbols_param = request.args.get('symbols')
|
||
if symbols_param:
|
||
symbols_list = [s.strip() for s in symbols_param.split(',') if s.strip()]
|
||
else:
|
||
symbols_list = load_crypto_symbols(limit=150)
|
||
|
||
results = []
|
||
for sym in symbols_list:
|
||
try:
|
||
df = get_crypto_kl_data(sym, timeframe, start_time=start_time, end_time=end_time)
|
||
if df is None or len(df) < 60:
|
||
continue
|
||
df = add_indicators(df)
|
||
direction, stage, strength = classify_trend_stage(df)
|
||
|
||
if want_direction and direction != want_direction:
|
||
continue
|
||
if want_stage and stage != want_stage:
|
||
continue
|
||
if strength < min_strength:
|
||
continue
|
||
|
||
last_row = df.iloc[-1]
|
||
results.append({
|
||
'symbol': sym,
|
||
'time': int(last_row['timestamp']),
|
||
'close': float(last_row['close']),
|
||
'direction': direction,
|
||
'stage': stage,
|
||
'strength': float(round(strength, 2)),
|
||
'ema5': float(last_row['ema5']),
|
||
'ema10': float(last_row['ema10']),
|
||
'ema24': float(last_row['ema24']),
|
||
'ema52': float(last_row['ema52'])
|
||
})
|
||
except Exception:
|
||
continue
|
||
|
||
# 按强度降序
|
||
results.sort(key=lambda x: x['strength'], reverse=True)
|
||
return jsonify({
|
||
'count': len(results),
|
||
'results': results
|
||
})
|
||
|
||
|
||
@app.route('/api/trend_detail', methods=['GET'])
|
||
def trend_detail():
|
||
"""返回单个币对的K线与EMA、用于前端绘制趋势线
|
||
参数: symbol, timeframe, start_time, end_time
|
||
"""
|
||
symbol = request.args.get('symbol')
|
||
timeframe = request.args.get('timeframe', '1h')
|
||
start_time = request.args.get('start_time')
|
||
end_time = request.args.get('end_time')
|
||
timezone_name = request.args.get('timezone', 'Asia/Shanghai')
|
||
|
||
if not symbol:
|
||
return jsonify({'error': 'symbol不能为空'})
|
||
|
||
df = get_crypto_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time)
|
||
if df is None or len(df) == 0:
|
||
return jsonify({'error': '获取数据失败'})
|
||
|
||
df = add_indicators(df)
|
||
direction, stage, strength = classify_trend_stage(df)
|
||
|
||
# 简单趋势线: 用最近N根收盘价做线性拟合
|
||
N = min(80, len(df))
|
||
sub = df.tail(N)
|
||
y = sub['close'].values
|
||
x = np.arange(len(y))
|
||
denom = np.dot(x - x.mean(), x - x.mean())
|
||
if denom != 0:
|
||
m = float(np.dot(y - y.mean(), x - x.mean()) / denom)
|
||
b = float(y.mean() - m * x.mean())
|
||
else:
|
||
m, b = 0.0, float(y[-1])
|
||
|
||
client_tz = timezone(timezone_name)
|
||
|
||
return jsonify({
|
||
'symbol': symbol,
|
||
'timeframe': timeframe,
|
||
'timezone': timezone_name,
|
||
'direction': direction,
|
||
'stage': stage,
|
||
'strength': float(round(strength, 2)),
|
||
'kline_data': clean_dataframe_for_json(df)[['timestamp','open','high','low','close','volume','ema5','ema10','ema24','ema52']].to_dict('records'),
|
||
'trend_line': {
|
||
'offset': int(df.index[-N]),
|
||
'slope': m,
|
||
'intercept': b,
|
||
'length': int(N)
|
||
}
|
||
})
|
||
|
||
@app.route('/')
|
||
def index():
|
||
"""主页"""
|
||
refresh_data_service_metadata()
|
||
timeframe_items = list(TIMEFRAMES.items())
|
||
timeframe_keys = [item[0] for item in timeframe_items]
|
||
symbols = SYMBOLS if SYMBOLS else DEFAULT_SYMBOLS
|
||
|
||
preferred_main = next((tf for tf in ['5m', '15m', '1h'] if tf in TIMEFRAMES), None)
|
||
default_main = preferred_main or (timeframe_keys[0] if timeframe_keys else '1m')
|
||
if default_main not in TIMEFRAMES and timeframe_keys:
|
||
default_main = timeframe_keys[0]
|
||
|
||
if timeframe_keys:
|
||
try:
|
||
idx = timeframe_keys.index(default_main)
|
||
default_element = timeframe_keys[idx - 1] if idx > 0 else timeframe_keys[0]
|
||
except ValueError:
|
||
default_element = timeframe_keys[0]
|
||
else:
|
||
default_element = default_main
|
||
|
||
# 次次周期默认比次周期小一档
|
||
if timeframe_keys:
|
||
try:
|
||
idx_el = timeframe_keys.index(default_element)
|
||
default_sub_sub = timeframe_keys[idx_el - 1] if idx_el > 0 else timeframe_keys[0]
|
||
except ValueError:
|
||
default_sub_sub = timeframe_keys[0]
|
||
else:
|
||
default_sub_sub = default_element
|
||
|
||
default_symbol = 'BTC/USDT:USDT' if 'BTC/USDT:USDT' in symbols else (symbols[0] if symbols else '')
|
||
|
||
return render_template(
|
||
'index.html',
|
||
timeframes=TIMEFRAMES,
|
||
symbols=symbols,
|
||
a_stock_symbols=A_STOCK_SYMBOLS,
|
||
default_main_timeframe=default_main,
|
||
default_element_timeframe=default_element,
|
||
default_sub_sub_timeframe=default_sub_sub,
|
||
default_symbol=default_symbol,
|
||
timeframe_keys_json=json.dumps(timeframe_keys),
|
||
data_service_available=DATA_SERVICE_AVAILABLE,
|
||
)
|
||
|
||
@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')
|
||
sub_sub_timeframe = request.args.get('sub_sub_timeframe')
|
||
|
||
# 获取是否只需要分形元素数据的参数
|
||
elements_only_param = request.args.get('elements_only')
|
||
elements_only = elements_only_param == 'true'
|
||
|
||
# 验证小周期是否小于主周期
|
||
if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe):
|
||
return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'})
|
||
# 验证次次周期是否小于等于次周期
|
||
if sub_sub_timeframe and element_timeframe and not is_smaller_or_equal_timeframe(sub_sub_timeframe, element_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, symbol, timeframe)
|
||
|
||
# 计算MACD
|
||
macd_data = calculate_macd(df)
|
||
|
||
# 基于已有 KLC 列表生成趋势标记(不做额外计算)
|
||
klc_trend = []
|
||
try:
|
||
for klc in analysis_result.get('klc_list', []):
|
||
trend_val = getattr(klc, 'trend', None)
|
||
t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None)
|
||
if trend_val is None or t_obj is None:
|
||
continue
|
||
# 统一成字符串:UP/DOWN/FLAT/UNKNOWN
|
||
trend_name = str(trend_val)
|
||
if '.' in trend_name:
|
||
trend_name = trend_name.split('.')[-1]
|
||
time_str = format_time_safely(t_obj, client_tz)
|
||
if time_str:
|
||
klc_trend.append({'time': time_str, 'trend': trend_name})
|
||
except Exception:
|
||
klc_trend = []
|
||
|
||
# 添加主周期分析结果到返回数据
|
||
result.update({
|
||
'kline_data': clean_dataframe_for_json(df).to_dict('records'),
|
||
'klc_list': [{
|
||
'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(),
|
||
'open': float(klc.open),
|
||
'high': float(klc.high),
|
||
'low': float(klc.low),
|
||
'close': float(klc.close),
|
||
'volume': float(klc.volume) if hasattr(klc, 'volume') else 0,
|
||
'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''),
|
||
'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''),
|
||
'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''),
|
||
'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '')
|
||
} for klc in analysis_result['klc_list'] if hasattr(klc, 'end_time') and klc.end_time],
|
||
'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.is_sure],
|
||
# 添加未完成笔列表
|
||
'uncompleted_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_time, # 未完成笔没有结束时间
|
||
'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.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格
|
||
'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 not bi.is_sure],
|
||
'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.is_sure],
|
||
# 添加未完成线段列表
|
||
'uncompleted_seg_list': get_uncompleted_seg_list(analysis_result['seg_list'], client_tz),
|
||
'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,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'is_sure': zs.is_sure # 添加中枢是否完成的标志
|
||
} for zs in analysis_result['zs_list'] if zs.is_sure],
|
||
# 添加主周期BI中枢列表(已完成)
|
||
'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())
|
||
if getattr(zs.start_klc, 'end_time', None) else
|
||
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
|
||
),
|
||
'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None,
|
||
'zg': zs.zg,
|
||
'zd': zs.zd,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'is_sure': bool(getattr(zs, 'is_sure', False))
|
||
} for zs in analysis_result.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)],
|
||
# 添加未完成中枢列表
|
||
'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,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'is_sure': zs.is_sure # 未完成中枢的is_sure为False
|
||
} for zs in analysis_result['zs_list'] if not zs.is_sure],
|
||
# 添加未完成BI中枢列表
|
||
'uncompleted_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())
|
||
if getattr(zs.start_klc, 'end_time', None) else
|
||
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
|
||
),
|
||
'end_time': None,
|
||
'zg': zs.zg,
|
||
'zd': zs.zd,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'is_sure': bool(getattr(zs, 'is_sure', False))
|
||
} for zs in analysis_result.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)],
|
||
|
||
'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),
|
||
'start_time': format_time_safely(point['start_time'], client_tz),
|
||
'end_time': format_time_safely(point['end_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']), # 是否为强分型
|
||
# 分型框(虚线矩形)用到的高低价
|
||
'high': float(point['high']) if point.get('high') is not None else None,
|
||
'low': float(point['low']) if point.get('low') is not None else None
|
||
} for point in analysis_result['klc_fx_info']],
|
||
# 添加ChanMACD分析数据
|
||
'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz),
|
||
# 添加多时间周期EMA52数据
|
||
'ema52_dict': analysis_result.get('ema52_dict', {}),
|
||
# 直接输出KLC趋势标记(使用已有trend字段)
|
||
'klc_trend': klc_trend,
|
||
# 添加主周期买卖点列表
|
||
# 注意:部分枚举在转为字符串时可能形如 "Chan_BSP_TYPE.BSP1(1)",
|
||
# 这里进行健壮的解析,确保前端拿到的始终是 "BSP1" / "BUY" 这种简洁形式,
|
||
# 以便与前端的 BSP_STYLE 键(如 "BSP1_BUY")正确匹配。
|
||
'bsp_list': [{
|
||
'time': format_time_safely(bsp.end_time, client_tz),
|
||
'price': float(bsp.klc.low if 'BUY' in str(bsp.dir) else bsp.klc.high),
|
||
# -- 规范化 type 名称,例如:
|
||
# "Chan_BSP_TYPE.BSP1" -> "BSP1"
|
||
# "Chan_BSP_TYPE.BSP1(1)" -> "BSP1"
|
||
# "BSP1" -> "BSP1"
|
||
'type': (
|
||
lambda raw: (
|
||
(raw.split('.')[-1] if '.' in raw else raw).split('(')[0]
|
||
)
|
||
)(str(bsp.type)),
|
||
# -- 规范化 dir 名称,例如:
|
||
# "Chan_BSP_DIR.BUY" -> "BUY"
|
||
# "Chan_BSP_DIR.BUY(1)" -> "BUY"
|
||
# "BUY" -> "BUY"
|
||
'dir': (
|
||
lambda raw: (
|
||
(raw.split('.')[-1] if '.' in raw else raw).split('(')[0]
|
||
)
|
||
)(str(bsp.dir)),
|
||
'is_sure': bool(bsp.is_sure),
|
||
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
|
||
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
|
||
} for bsp in analysis_result.get('bsp_list', [])]
|
||
})
|
||
|
||
|
||
# 如果有指定分形元素时间周期,获取小周期数据
|
||
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, symbol, element_timeframe)
|
||
|
||
# 计算小周期MACD数据
|
||
element_macd_data = calculate_macd(element_df)
|
||
|
||
# 组装小周期 KLC 趋势(仅提取已有 trend,不做重算)
|
||
try:
|
||
element_klc_trend = []
|
||
for klc in element_analysis.get('klc_list', []):
|
||
trend_val = getattr(klc, 'trend', None)
|
||
t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None)
|
||
if trend_val is None or t_obj is None:
|
||
continue
|
||
trend_name = str(trend_val)
|
||
if '.' in trend_name:
|
||
trend_name = trend_name.split('.')[-1]
|
||
time_str = format_time_safely(t_obj, client_tz)
|
||
if time_str:
|
||
element_klc_trend.append({'time': time_str, 'trend': trend_name})
|
||
except Exception:
|
||
element_klc_trend = []
|
||
|
||
# 添加小周期分析结果到返回数据
|
||
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.is_sure]
|
||
# 添加次周期未完成笔列表
|
||
result['element_uncompleted_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_time, # 未完成笔没有结束时间
|
||
'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.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格
|
||
'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 not bi.is_sure]
|
||
|
||
# 添加小周期K线数据
|
||
result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records')
|
||
|
||
# 添加小周期KLC列表
|
||
result['element_klc_list'] = [{
|
||
'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(),
|
||
'open': float(klc.open),
|
||
'high': float(klc.high),
|
||
'low': float(klc.low),
|
||
'close': float(klc.close),
|
||
'volume': float(klc.volume) if hasattr(klc, 'volume') else 0,
|
||
'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''),
|
||
'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''),
|
||
'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''),
|
||
'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '')
|
||
} for klc in element_analysis['klc_list'] if hasattr(klc, 'end_time') and klc.end_time]
|
||
|
||
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.is_sure]
|
||
# 添加次周期未完成线段列表
|
||
result['element_uncompleted_seg_list'] = get_uncompleted_seg_list(element_analysis['seg_list'], client_tz)
|
||
|
||
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,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'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,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'is_sure': zs.is_sure # 未完成中枢的is_sure为False
|
||
} for zs in element_analysis['zs_list'] if not zs.is_sure]
|
||
|
||
# 添加次周期 BI 中枢(已完成/未完成)
|
||
result['element_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())
|
||
if getattr(zs.start_klc, 'end_time', None) else
|
||
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
|
||
),
|
||
'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None,
|
||
'zg': zs.zg,
|
||
'zd': zs.zd,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'is_sure': bool(getattr(zs, 'is_sure', False))
|
||
} for zs in element_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)]
|
||
result['element_uncompleted_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())
|
||
if getattr(zs.start_klc, 'end_time', None) else
|
||
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
|
||
),
|
||
'end_time': None,
|
||
'zg': zs.zg,
|
||
'zd': zs.zd,
|
||
'gg': zs.gg,
|
||
'dd': zs.dd,
|
||
'is_sure': bool(getattr(zs, 'is_sure', False))
|
||
} for zs in element_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)]
|
||
|
||
|
||
# 添加小周期分型信息
|
||
result['element_klc_fx_info'] = [{
|
||
'time': format_time_safely(point['time'], client_tz),
|
||
'start_time': format_time_safely(point['start_time'], client_tz),
|
||
'end_time': format_time_safely(point['end_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']), # 是否为强分型
|
||
# 分型框(虚线矩形)用到的高低价
|
||
'high': float(point['high']) if point.get('high') is not None else None,
|
||
'low': float(point['low']) if point.get('low') is not None else None
|
||
} for point in element_analysis['klc_fx_info']]
|
||
|
||
# 添加次周期ChanMACD分析数据
|
||
result['element_chan_macd'] = serialize_chan_macd_data(element_analysis.get('chan_macd', {}), client_tz)
|
||
|
||
# 添加小周期 KLC 趋势标记
|
||
result['element_klc_trend'] = element_klc_trend
|
||
|
||
# 添加次周期买卖点列表
|
||
result['element_bsp_list'] = [{
|
||
'time': format_time_safely(bsp.end_time, client_tz),
|
||
'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high),
|
||
'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''),
|
||
'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''),
|
||
'is_sure': bool(bsp.is_sure),
|
||
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
|
||
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
|
||
} for bsp in element_analysis.get('bsp_list', [])]
|
||
|
||
# 次次周期:仅当已指定次周期且次次周期有效时获取
|
||
if sub_sub_timeframe and is_smaller_or_equal_timeframe(sub_sub_timeframe, element_timeframe):
|
||
sub_sub_df = get_kl_data(symbol, sub_sub_timeframe, start_time=start_time, end_time=end_time)
|
||
if sub_sub_df is not None and len(sub_sub_df) > 0:
|
||
sub_sub_df = add_indicators(sub_sub_df)
|
||
sub_sub_analysis = analyze_chan(sub_sub_df, symbol, sub_sub_timeframe)
|
||
result['sub_sub_timeframe'] = sub_sub_timeframe
|
||
result['sub_sub_kline_data'] = clean_dataframe_for_json(sub_sub_df).to_dict('records')
|
||
result['sub_sub_atr'] = sub_sub_df['atr'].tolist()
|
||
result['sub_sub_macd'] = calculate_macd(sub_sub_df)
|
||
result['sub_sub_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 sub_sub_analysis['bi_list'] if bi.is_sure]
|
||
result['sub_sub_uncompleted_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_time,
|
||
'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.low if convert_direction(bi.dir) == 1 else bi.end_klc.high,
|
||
'direction': convert_direction(bi.dir),
|
||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
|
||
} for bi in sub_sub_analysis['bi_list'] if not bi.is_sure]
|
||
# 次次周期 KLC 列表
|
||
result['sub_sub_klc_list'] = [{
|
||
'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(),
|
||
'open': float(klc.open),
|
||
'high': float(klc.high),
|
||
'low': float(klc.low),
|
||
'close': float(klc.close),
|
||
'volume': float(klc.volume) if hasattr(klc, 'volume') else 0,
|
||
'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''),
|
||
'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''),
|
||
'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''),
|
||
'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '')
|
||
} for klc in sub_sub_analysis.get('klc_list', []) if hasattr(klc, 'end_time') and klc.end_time]
|
||
|
||
result['sub_sub_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 sub_sub_analysis['seg_list'] if seg.is_sure]
|
||
result['sub_sub_uncompleted_seg_list'] = get_uncompleted_seg_list(sub_sub_analysis['seg_list'], client_tz)
|
||
result['sub_sub_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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure
|
||
} for zs in sub_sub_analysis['zs_list'] if zs.end_klc]
|
||
result['sub_sub_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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure
|
||
} for zs in sub_sub_analysis['zs_list'] if not zs.is_sure]
|
||
result['sub_sub_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())
|
||
if getattr(zs.start_klc, 'end_time', None) else
|
||
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
|
||
),
|
||
'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None,
|
||
'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False))
|
||
} for zs in sub_sub_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)]
|
||
result['sub_sub_uncompleted_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())
|
||
if getattr(zs.start_klc, 'end_time', None) else
|
||
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
|
||
),
|
||
'end_time': None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False))
|
||
} for zs in sub_sub_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)]
|
||
result['sub_sub_klc_fx_info'] = [{
|
||
'time': format_time_safely(point['time'], client_tz),
|
||
'start_time': format_time_safely(point['start_time'], client_tz),
|
||
'end_time': format_time_safely(point['end_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']),
|
||
# 分型框(虚线矩形)用到的高低价
|
||
'high': float(point['high']) if point.get('high') is not None else None,
|
||
'low': float(point['low']) if point.get('low') is not None else None
|
||
} for point in sub_sub_analysis['klc_fx_info']]
|
||
result['sub_sub_bsp_list'] = [{
|
||
'time': format_time_safely(bsp.end_time, client_tz),
|
||
'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high),
|
||
'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''),
|
||
'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''),
|
||
'is_sure': bool(bsp.is_sure),
|
||
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
|
||
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
|
||
} for bsp in sub_sub_analysis.get('bsp_list', [])]
|
||
result['sub_sub_chan_macd'] = serialize_chan_macd_data(sub_sub_analysis.get('chan_macd', {}), client_tz)
|
||
try:
|
||
sub_sub_klc_trend = []
|
||
for klc in sub_sub_analysis.get('klc_list', []):
|
||
trend_val = getattr(klc, 'trend', None)
|
||
t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None)
|
||
if trend_val is None or t_obj is None:
|
||
continue
|
||
trend_name = str(trend_val)
|
||
if '.' in trend_name:
|
||
trend_name = trend_name.split('.')[-1]
|
||
time_str = format_time_safely(t_obj, client_tz)
|
||
if time_str:
|
||
sub_sub_klc_trend.append({'time': time_str, 'trend': trend_name})
|
||
result['sub_sub_klc_trend'] = sub_sub_klc_trend
|
||
except Exception:
|
||
result['sub_sub_klc_trend'] = []
|
||
|
||
pass
|
||
|
||
return jsonify(result)
|
||
|
||
@app.route('/api/symbols')
|
||
def get_symbols():
|
||
"""获取可用交易对"""
|
||
refresh_data_service_metadata()
|
||
if SYMBOLS:
|
||
return jsonify(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(DEFAULT_SYMBOLS)
|
||
|
||
@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/macd_config', methods=['GET', 'POST'])
|
||
def macd_config():
|
||
"""获取或设置MACD参数"""
|
||
global macd_fast_period, macd_slow_period, macd_signal_period
|
||
if request.method == 'GET':
|
||
return jsonify({
|
||
'fast': macd_fast_period,
|
||
'slow': macd_slow_period,
|
||
'signal': macd_signal_period
|
||
})
|
||
else:
|
||
data = request.get_json(silent=True) or {}
|
||
fast = data.get('fast')
|
||
slow = data.get('slow')
|
||
signal = data.get('signal')
|
||
if fast is not None:
|
||
macd_fast_period = int(fast)
|
||
if slow is not None:
|
||
macd_slow_period = int(slow)
|
||
if signal is not None:
|
||
macd_signal_period = int(signal)
|
||
return jsonify({
|
||
'fast': macd_fast_period,
|
||
'slow': macd_slow_period,
|
||
'signal': macd_signal_period
|
||
})
|
||
|
||
|
||
def get_uncompleted_seg_list(seg_list, client_tz):
|
||
"""获取未完成线段列表,正确处理倒数第二个和最后一个未完成线段"""
|
||
uncompleted_segs = [seg for seg in seg_list if not seg.is_sure]
|
||
|
||
if len(uncompleted_segs) == 0:
|
||
return []
|
||
|
||
result = []
|
||
|
||
for i, seg in enumerate(uncompleted_segs):
|
||
is_last = (i == len(uncompleted_segs) - 1) # 是否为最后一个未完成线段
|
||
|
||
seg_data = {
|
||
'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(),
|
||
'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,
|
||
'direction': convert_direction(seg.dir)
|
||
}
|
||
|
||
if is_last:
|
||
# 最后一个未完成线段:没有结束时间和价格
|
||
seg_data['end_time'] = None
|
||
seg_data['end_price'] = None
|
||
else:
|
||
# 倒数第二个及之前的未完成线段:使用实际的结束时间和价格
|
||
if seg.end_bi and seg.end_bi.end_klc:
|
||
seg_data['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()
|
||
seg_data['end_price'] = seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low
|
||
else:
|
||
# 如果没有结束笔,设为None
|
||
seg_data['end_time'] = None
|
||
seg_data['end_price'] = None
|
||
|
||
result.append(seg_data)
|
||
|
||
return result
|
||
|
||
if __name__ == '__main__':
|
||
app.run(debug=True, host='0.0.0.0', port=8128) |