refactor: 缠论引擎包化与 Web 分层(ECR-001)

将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务;
前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-05 18:48:20 +08:00
co-authored by Cursor
parent e2e45bc1bc
commit 74dec4e50b
160 changed files with 25576 additions and 23104 deletions
+126
View File
@@ -0,0 +1,126 @@
"""趋势相关 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)
}
})