将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务; 前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。 Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
3.6 KiB
Python
127 lines
3.6 KiB
Python
"""趋势相关 API。"""
|
|
from flask import Blueprint, jsonify, request
|
|
from services.runtime import * # noqa: F403
|
|
from services import runtime as R
|
|
|
|
bp = Blueprint("trend", __name__)
|
|
|
|
@bp.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
|
|
})
|
|
|
|
|
|
@bp.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)
|
|
}
|
|
})
|
|
|