feat(ECR-007): Wyckoff Live Structure with Confirmed/Live isolation
Add live.py lifecycle and event candidates; assemble confirmed vs live in engine; Summary partition; execution_signal source=confirmed only. Keep strategies untouched; do not lower Confirmed thresholds for Live. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+124
-25
@@ -5,6 +5,97 @@ from services import runtime as R
|
||||
|
||||
bp = Blueprint("analyze", __name__)
|
||||
|
||||
_WYCKOFF_EMPTY = {
|
||||
'trading_range': None,
|
||||
'bias': 'unknown',
|
||||
'phases': [],
|
||||
'events': [],
|
||||
'volume_profile': {'bins': [], 'poc': None, 'vah': None, 'val': None, 'bin_count': 0},
|
||||
'volume_confirm': {'avg_volume': 0.0, 'event_checks': {}},
|
||||
'cycles': [],
|
||||
'live': None,
|
||||
'lifecycle': 'UNKNOWN',
|
||||
}
|
||||
|
||||
|
||||
def _localize_wyckoff_payload(w, client_tz):
|
||||
"""把威科夫时间统一成客户端时区 ISO,便于与主图对齐。"""
|
||||
if not w:
|
||||
return w
|
||||
|
||||
def _loc_tr(tr):
|
||||
if not tr:
|
||||
return
|
||||
tr['start_time'] = format_time_safely(tr.get('start_time'), client_tz) or tr.get('start_time')
|
||||
tr['end_time'] = format_time_safely(tr.get('end_time'), client_tz) or tr.get('end_time')
|
||||
|
||||
def _loc_cycle(c):
|
||||
if not c:
|
||||
return
|
||||
per = c.get('period') or {}
|
||||
per['start_time'] = format_time_safely(per.get('start_time'), client_tz) or per.get('start_time')
|
||||
per['end_time'] = format_time_safely(per.get('end_time'), client_tz) or per.get('end_time')
|
||||
c['period'] = per
|
||||
_loc_tr(c.get('trading_range'))
|
||||
for ph in c.get('phases') or []:
|
||||
ph['start_time'] = format_time_safely(ph.get('start_time'), client_tz) or ph.get('start_time')
|
||||
ph['end_time'] = format_time_safely(ph.get('end_time'), client_tz) or ph.get('end_time')
|
||||
for ev in c.get('events') or []:
|
||||
ev['time'] = format_time_safely(ev.get('time'), client_tz) or ev.get('time')
|
||||
|
||||
_loc_tr(w.get('trading_range'))
|
||||
for ph in w.get('phases') or []:
|
||||
ph['start_time'] = format_time_safely(ph.get('start_time'), client_tz) or ph.get('start_time')
|
||||
ph['end_time'] = format_time_safely(ph.get('end_time'), client_tz) or ph.get('end_time')
|
||||
for ev in w.get('events') or []:
|
||||
ev['time'] = format_time_safely(ev.get('time'), client_tz) or ev.get('time')
|
||||
for c in w.get('cycles') or []:
|
||||
_loc_cycle(c)
|
||||
return w
|
||||
|
||||
|
||||
def _compute_wyckoff_from_df(df, tf, vp_bins, client_tz=None, range_start_time=None, prefer_start_time=None):
|
||||
"""直接用该周期已有 DataFrame(与缠论同一份)。
|
||||
搜索窗口 = 整段数据;箱体在窗内评分选取(近优分取更长),
|
||||
次/次次可用 prefer_start_time 对齐主箱起点。
|
||||
"""
|
||||
from chanlun.analysis.wyckoff import analyze_wyckoff
|
||||
|
||||
try:
|
||||
if df is None or len(df) < 30:
|
||||
empty = dict(_WYCKOFF_EMPTY)
|
||||
empty['volume_profile'] = dict(_WYCKOFF_EMPTY['volume_profile'])
|
||||
empty['volume_confirm'] = dict(_WYCKOFF_EMPTY['volume_confirm'])
|
||||
empty['timeframe'] = tf
|
||||
return empty
|
||||
lookback = len(df)
|
||||
min_bars = max(24, min(80, lookback // 12))
|
||||
out = analyze_wyckoff(
|
||||
df,
|
||||
lookback=lookback,
|
||||
vp_bins=vp_bins,
|
||||
min_bars=min_bars,
|
||||
range_start_time=range_start_time,
|
||||
prefer_start_time=prefer_start_time,
|
||||
)
|
||||
out['timeframe'] = tf
|
||||
out['lookback'] = lookback
|
||||
out['min_bars'] = min_bars
|
||||
if client_tz is not None:
|
||||
_localize_wyckoff_payload(out, client_tz)
|
||||
return out
|
||||
except Exception as e:
|
||||
print(f"Wyckoff 分析出错 ({tf}): {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
empty = dict(_WYCKOFF_EMPTY)
|
||||
empty['volume_profile'] = dict(_WYCKOFF_EMPTY['volume_profile'])
|
||||
empty['volume_confirm'] = dict(_WYCKOFF_EMPTY['volume_confirm'])
|
||||
empty['timeframe'] = tf
|
||||
empty['error'] = str(e)
|
||||
return empty
|
||||
|
||||
|
||||
@bp.route('/api/analyze')
|
||||
def analyze():
|
||||
"""分析接口"""
|
||||
@@ -25,6 +116,9 @@ def analyze():
|
||||
# 获取分形元素时间周期与次次周期
|
||||
element_timeframe = request.args.get('element_timeframe')
|
||||
sub_sub_timeframe = request.args.get('sub_sub_timeframe')
|
||||
# 供文末三周期威科夫复用(避免重复拉数)
|
||||
element_df_for_wyckoff = None
|
||||
sub_sub_df_for_wyckoff = None
|
||||
|
||||
# 获取是否只需要分形元素数据的参数
|
||||
elements_only_param = request.args.get('elements_only')
|
||||
@@ -249,6 +343,7 @@ def analyze():
|
||||
if element_df is not None and len(element_df) > 0:
|
||||
# 添加小周期技术指标(包括布林带)
|
||||
element_df = add_indicators(element_df)
|
||||
element_df_for_wyckoff = element_df
|
||||
|
||||
# 对小周期数据进行缠论分析
|
||||
element_analysis = analyze_chan(element_df, symbol, element_timeframe)
|
||||
@@ -427,6 +522,7 @@ def analyze():
|
||||
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_df_for_wyckoff = 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')
|
||||
@@ -656,33 +752,36 @@ def analyze():
|
||||
else:
|
||||
result['structure_zones'] = []
|
||||
|
||||
# 威科夫分析 —— 按需:include_wyckoff=1,且须有主周期分析(非 elements_only)
|
||||
include_wyckoff_param = request.args.get('include_wyckoff', '')
|
||||
include_wyckoff = str(include_wyckoff_param).lower() in ('1', 'true', 'yes')
|
||||
# 威科夫:主 / 次 / 次次各算一份(非 elements_only);前端开关只控制绘制
|
||||
# include_wyckoff=0 可显式跳过;缺省与其它真值均计算
|
||||
include_wyckoff_param = request.args.get('include_wyckoff', '1')
|
||||
include_wyckoff = str(include_wyckoff_param).lower() not in ('0', 'false', 'no')
|
||||
if include_wyckoff and not elements_only:
|
||||
try:
|
||||
from chanlun.analysis.wyckoff import analyze_wyckoff
|
||||
wyckoff_lookback = int(request.args.get('wyckoff_lookback', 120))
|
||||
# ECR-004:默认/上限 24 bins(A+C)
|
||||
wyckoff_bins = int(request.args.get('wyckoff_vp_bins', 24))
|
||||
result['wyckoff'] = analyze_wyckoff(
|
||||
df,
|
||||
lookback=max(40, min(wyckoff_lookback, 500)),
|
||||
vp_bins=max(10, min(wyckoff_bins, 24)),
|
||||
# 主周期先算;次/次次只同步 active=cycles[0] 的 start(WYCKOFF-MULTI-CYCLE-001)
|
||||
wyckoff_bins = max(10, min(int(request.args.get('wyckoff_vp_bins', 24)), 24))
|
||||
result['wyckoff'] = _compute_wyckoff_from_df(df, timeframe, wyckoff_bins, client_tz=None)
|
||||
main_w = result.get('wyckoff') or {}
|
||||
cycles = main_w.get('cycles') or []
|
||||
# active 唯一来源 cycles[0];禁止 cycles[-1]
|
||||
active = cycles[0] if cycles else None
|
||||
prefer_start = None
|
||||
if active:
|
||||
prefer_start = ((active.get('trading_range') or {}).get('start_time')
|
||||
or (active.get('period') or {}).get('start_time'))
|
||||
elif main_w.get('trading_range'):
|
||||
prefer_start = main_w['trading_range'].get('start_time')
|
||||
if client_tz is not None:
|
||||
_localize_wyckoff_payload(result['wyckoff'], client_tz)
|
||||
if element_timeframe:
|
||||
result['element_wyckoff'] = _compute_wyckoff_from_df(
|
||||
element_df_for_wyckoff, element_timeframe, wyckoff_bins, client_tz,
|
||||
prefer_start_time=prefer_start,
|
||||
)
|
||||
if sub_sub_timeframe:
|
||||
result['sub_sub_wyckoff'] = _compute_wyckoff_from_df(
|
||||
sub_sub_df_for_wyckoff, sub_sub_timeframe, wyckoff_bins, client_tz,
|
||||
prefer_start_time=prefer_start,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Wyckoff 分析出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
result['wyckoff'] = {
|
||||
'trading_range': None,
|
||||
'bias': 'unknown',
|
||||
'phases': [],
|
||||
'events': [],
|
||||
'volume_profile': {'bins': [], 'poc': None, 'vah': None, 'val': None, 'bin_count': 0},
|
||||
'volume_confirm': {'avg_volume': 0.0, 'event_checks': {}},
|
||||
'error': str(e),
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user