394 lines
14 KiB
Python
394 lines
14 KiB
Python
import json
|
||
import os
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||
|
||
from app.core.opportunity_lifecycle import apply_entry_quality_gate
|
||
from app.core.strategy_registry import LONG_MOMENTUM_BREAKOUT_STRATEGY, LONG_SECOND_WAVE_PULLBACK_STRATEGY
|
||
from app.services.price_tracker import reconcile_buy_signals_after_gate
|
||
|
||
|
||
def test_risk_reward_false_blocks_buy_now():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '等回踩',
|
||
'entry_price': 0.072,
|
||
'current_price': 0.0758,
|
||
'risk_reward_ok': False,
|
||
'rr1': 0.4,
|
||
},
|
||
signals=['1H 起爆点↑(强度56×)', '⚠️ 等回踩降权(-3分)'],
|
||
current_price=0.0758,
|
||
market_context={'change_24h': 9.0},
|
||
)
|
||
assert action in ('等回踩', '观察')
|
||
assert action != '可即刻买入'
|
||
assert plan['entry_quality_gate']['blocked_action'] == '可即刻买入'
|
||
assert any('risk_reward_ok=false' in r for r in reasons)
|
||
|
||
|
||
def test_buy_now_with_bad_rr_sets_real_pullback_price():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '即刻买入',
|
||
'entry_price': 0.11455,
|
||
'current_price': 0.11455,
|
||
'stop_loss': 0.107457,
|
||
'tp1': 0.120089,
|
||
'risk_reward_ok': False,
|
||
'rr1': 0.83,
|
||
},
|
||
signals=['🟢 15min即刻入场信号', '日线 站稳突破位+19.2%'],
|
||
current_price=0.11455,
|
||
market_context={'change_24h': 3.1},
|
||
)
|
||
|
||
assert action == '等回踩'
|
||
assert plan['entry_price'] < 0.11455
|
||
assert round(plan['entry_price'], 6) == 0.113071
|
||
assert plan['rr_target_entry'] == plan['entry_price']
|
||
assert any('现价不买' in r for r in reasons)
|
||
|
||
|
||
def test_buy_now_with_bad_rr_can_use_stricter_rr_override():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '即刻买入',
|
||
'entry_price': 0.11455,
|
||
'current_price': 0.11455,
|
||
'stop_loss': 0.107457,
|
||
'tp1': 0.120089,
|
||
'risk_reward_ok': False,
|
||
'rr1': 0.83,
|
||
},
|
||
signals=['🟢 15min即刻入场信号', '日线 站稳突破位+19.2%'],
|
||
current_price=0.11455,
|
||
market_context={'change_24h': 3.1},
|
||
cfg={'min_rr_buy_now': 1.5},
|
||
)
|
||
|
||
assert action == '等回踩'
|
||
assert round(plan['entry_price'], 6) == 0.11251
|
||
assert any('现价不买' in r for r in reasons)
|
||
|
||
|
||
def test_low_entry_score_blocks_buy_now_and_weak_pullback():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '可即刻买入',
|
||
'entry_price': 1.0,
|
||
'current_price': 1.0,
|
||
'stop_loss': 0.95,
|
||
'tp1': 1.12,
|
||
'risk_reward_ok': True,
|
||
'rr1': 2.4,
|
||
'entry_trigger_confirmed': True,
|
||
'score_components': {'opportunity_score': 12, 'entry_score': 0, 'risk_score': 1},
|
||
},
|
||
signals=['当前15min即刻入场信号'],
|
||
current_price=1.0,
|
||
market_context={'change_24h': 2.0},
|
||
cfg={'min_entry_score_buy_now': 3, 'min_entry_score_wait_pullback': 1},
|
||
)
|
||
|
||
assert action == '观察'
|
||
assert any('买点分' in r for r in reasons)
|
||
assert plan['entry_quality_gate']['entry_score'] == 0
|
||
|
||
|
||
def test_entry_gate_uses_strategy_specific_thresholds():
|
||
base_plan = {
|
||
'entry_action': '可即刻买入',
|
||
'entry_price': 1.0,
|
||
'current_price': 1.0,
|
||
'stop_loss': 0.95,
|
||
'tp1': 1.12,
|
||
'risk_reward_ok': True,
|
||
'rr1': 2.4,
|
||
'entry_trigger_confirmed': True,
|
||
'score_components': {'opportunity_score': 12, 'entry_score': 2, 'risk_score': 1},
|
||
}
|
||
momentum_action, momentum_plan, _ = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan=dict(base_plan),
|
||
signals=['当前15min即刻入场信号'],
|
||
current_price=1.0,
|
||
market_context={'change_24h': 2.0},
|
||
strategy_code=LONG_MOMENTUM_BREAKOUT_STRATEGY,
|
||
)
|
||
second_wave_action, second_wave_plan, _ = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan=dict(base_plan),
|
||
signals=['当前15min即刻入场信号'],
|
||
current_price=1.0,
|
||
market_context={'change_24h': 2.0},
|
||
strategy_code=LONG_SECOND_WAVE_PULLBACK_STRATEGY,
|
||
)
|
||
|
||
assert momentum_action != '可即刻买入'
|
||
assert momentum_plan['entry_quality_gate']['strategy_code'] == LONG_MOMENTUM_BREAKOUT_STRATEGY
|
||
assert second_wave_action == '可即刻买入'
|
||
assert second_wave_plan['strategy_code'] == LONG_SECOND_WAVE_PULLBACK_STRATEGY
|
||
|
||
|
||
def test_buy_now_requires_current_trigger_and_no_bearish_flow_risk():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '即刻买入',
|
||
'entry_price': 1.0,
|
||
'current_price': 1.0,
|
||
'stop_loss': 0.95,
|
||
'tp1': 1.12,
|
||
'risk_reward_ok': True,
|
||
'rr1': 2.4,
|
||
},
|
||
signals=['1H历史起爆点已过期(12根前)', '⚠️ 1H连续3K空头加速'],
|
||
current_price=1.0,
|
||
market_context={'change_24h': 2.0},
|
||
)
|
||
|
||
assert action != '可即刻买入'
|
||
assert any('缺少当前15min触发' in r for r in reasons)
|
||
assert any('空头加速' in r for r in reasons)
|
||
|
||
|
||
def test_structure_watch_pullback_touch_does_not_upgrade_to_buy_now():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='等回踩',
|
||
entry_plan={
|
||
'entry_action': '等回踩',
|
||
'entry_price': 9.74,
|
||
'current_price': 9.74,
|
||
'stop_loss': 9.253,
|
||
'tp1': 10.5192,
|
||
'risk_reward_ok': True,
|
||
'rr1': 1.6,
|
||
'opportunity_level': 'structure_watch',
|
||
'opportunity_level_label': '结构观察',
|
||
'max_action': 'wait_pullback',
|
||
},
|
||
signals=['1H放量(4.4x)但无量价齐飞(量价背离)', '🟢 15min即刻入场信号'],
|
||
current_price=9.74,
|
||
market_context={'change_24h': 0.3},
|
||
)
|
||
|
||
assert action == '等回踩'
|
||
assert action != '可即刻买入'
|
||
assert any('空头加速' in r for r in reasons)
|
||
|
||
|
||
def test_structure_watch_with_current_trigger_and_good_rr_can_upgrade_to_buy_now():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='等回踩',
|
||
entry_plan={
|
||
'entry_action': '等回踩',
|
||
'entry_price': 0.114,
|
||
'current_price': 0.114,
|
||
'stop_loss': 0.1026,
|
||
'tp1': 0.140743,
|
||
'risk_reward_ok': True,
|
||
'rr1': 2.35,
|
||
'opportunity_level': 'structure_watch',
|
||
'opportunity_level_label': '结构观察',
|
||
'max_action': 'wait_pullback',
|
||
},
|
||
signals=['4H需求区反弹', '🟢 15min即刻入场信号'],
|
||
current_price=0.114,
|
||
market_context={'change_24h': -6.9},
|
||
)
|
||
|
||
assert action == '可即刻买入'
|
||
assert plan['entry_action'] == '可即刻买入'
|
||
assert plan['entry_trigger_confirmed'] is True
|
||
assert any('回踩参考已到或更优' in r for r in reasons)
|
||
|
||
|
||
def test_tracker_pullback_confirmation_signal_counts_as_current_trigger():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '等回踩',
|
||
'entry_price': 0.114,
|
||
'current_price': 0.114,
|
||
'stop_loss': 0.1026,
|
||
'tp1': 0.140743,
|
||
'risk_reward_ok': True,
|
||
'rr1': 2.35,
|
||
'opportunity_level': 'structure_watch',
|
||
'opportunity_level_label': '结构观察',
|
||
'max_action': 'wait_pullback',
|
||
},
|
||
signals=['🟢 回踩确认完毕!可即刻入场(15min动K确认)'],
|
||
current_price=0.114,
|
||
market_context={'change_24h': -6.9},
|
||
)
|
||
|
||
assert action == '可即刻买入'
|
||
assert all('缺少当前15min触发' not in r for r in reasons)
|
||
|
||
|
||
def test_live_rr_recheck_overrides_stale_false_risk_reward_flag():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '即刻买入',
|
||
'entry_price': 1.0,
|
||
'current_price': 1.0,
|
||
'stop_loss': 0.92,
|
||
'tp1': 1.12,
|
||
'risk_reward_ok': False,
|
||
'rr1': 0.8,
|
||
'opportunity_level': 'short_swing',
|
||
'opportunity_level_label': '短波段',
|
||
'max_action': 'buy_now',
|
||
},
|
||
signals=['🟢 15min即刻入场信号', '1H 量价齐飞K(量3.2x)'],
|
||
current_price=0.97,
|
||
market_context={'change_24h': 1.5},
|
||
)
|
||
|
||
assert plan['risk_reward_ok_live'] is True
|
||
assert action == '可即刻买入'
|
||
assert all('risk_reward_ok=false' not in r for r in reasons)
|
||
assert all('rr1=0.8' not in r for r in reasons)
|
||
|
||
|
||
def test_tracker_gate_downgrade_removes_provisional_buy_signal():
|
||
signals = reconcile_buy_signals_after_gate(
|
||
[
|
||
'🟢 回踩确认完毕!可即刻入场(15min动K确认)',
|
||
'其他背景信号',
|
||
],
|
||
'等回踩',
|
||
{'rr_target_entry': 0.11322245, 'entry_price': 0.11322245},
|
||
['rr1=0.82 < 1.2,禁止现价买入', '现价不买,等回落到0.11322245附近再评估'],
|
||
)
|
||
|
||
assert all('可即刻入场' not in signal for signal in signals)
|
||
assert all('回踩确认完毕' not in signal for signal in signals)
|
||
assert any('现价不买' in signal and '$0.1132' in signal for signal in signals)
|
||
|
||
|
||
def test_breakout_distance_over_60_forces_observe():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={'entry_action': '即刻买入', 'risk_reward_ok': True, 'rr1': 2.0, 'entry_price': 0.164},
|
||
signals=['日线站稳突破位 +66.7%', '日线站稳突破位 +71.7%'],
|
||
current_price=0.168,
|
||
market_context={'change_24h': 5.0},
|
||
)
|
||
assert action == '观察'
|
||
assert plan['entry_quality_gate']['breakout_distance_pct'] == 71.7
|
||
assert any('严禁现价追' in r for r in reasons)
|
||
|
||
|
||
def test_low_static_accumulation_builds_ambush_plan():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='等回踩',
|
||
entry_plan={'entry_action': '等回踩', 'entry_price': 2.393, 'risk_reward_ok': True, 'rr1': 1.6, 'support': 2.2, 'resistance': 3.0},
|
||
signals=['4H静K蓄力观察(3静K,量比1.4x)', '大户偏多 62%'],
|
||
current_price=2.393,
|
||
market_context={'change_24h': 3.0},
|
||
derivatives_context={'top_trader_long_pct': 62},
|
||
)
|
||
assert action == '等回踩'
|
||
lifecycle = plan.get('opportunity_lifecycle')
|
||
assert lifecycle['stage'] == '低位潜伏'
|
||
assert lifecycle['plan_type'] == 'ambush'
|
||
assert lifecycle['static_count'] >= 3
|
||
|
||
|
||
def test_invalid_long_geometry_degrades_to_observe():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='等回踩',
|
||
entry_plan={
|
||
'entry_action': '等回踩',
|
||
'entry_price': 0.109065,
|
||
'stop_loss': 0.118914,
|
||
'tp1': 0.135879,
|
||
'risk_reward_ok': True,
|
||
'rr1': 1.5,
|
||
},
|
||
signals=['1H 量价齐飞K(量6.5x)', '15min 强突破K线(ATR×2.1)'],
|
||
current_price=0.1257,
|
||
market_context={'change_24h': 13.8},
|
||
)
|
||
assert action == '观察'
|
||
assert any('止损价不低于计划入场价' in r for r in reasons)
|
||
|
||
|
||
def test_wait_pullback_too_far_above_breakout_degrades_to_observe():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='等回踩',
|
||
entry_plan={
|
||
'entry_action': '等回踩',
|
||
'entry_price': 0.109065,
|
||
'stop_loss': 0.104,
|
||
'tp1': 0.135879,
|
||
'risk_reward_ok': True,
|
||
'rr1': 2.0,
|
||
},
|
||
signals=['1H 量价齐飞K(量6.5x)', '15min 强突破K线(ATR×2.1)'],
|
||
current_price=0.1257,
|
||
market_context={'change_24h': 13.8},
|
||
)
|
||
assert action == '观察'
|
||
assert any('突破已走远' in r for r in reasons)
|
||
|
||
|
||
def test_ws_tracker_does_not_push_when_gate_downgrades_buy_now():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'entry_action': '等回踩',
|
||
'entry_price': 0.072,
|
||
'risk_reward_ok': False,
|
||
'rr1': 0.4,
|
||
'stop_loss': 0.07,
|
||
'tp1': 0.08,
|
||
'tp2': 0.085,
|
||
},
|
||
signals=['1H 起爆点↑(强度56×)', '⚠️ 等回踩降权(-3分)'],
|
||
current_price=0.0719,
|
||
market_context={'change_24h': 9.0},
|
||
)
|
||
assert action in ('等回踩', '观察')
|
||
assert action != '可即刻买入'
|
||
assert plan['risk_reward_ok_live'] is True
|
||
assert any('缺少当前15min触发' in r for r in reasons)
|
||
|
||
|
||
def test_intraday_momentum_uses_strategy_rr_threshold_not_legacy_1_5():
|
||
action, plan, reasons = apply_entry_quality_gate(
|
||
action_status='可即刻买入',
|
||
entry_plan={
|
||
'strategy_code': LONG_MOMENTUM_BREAKOUT_STRATEGY,
|
||
'side': 'long',
|
||
'entry_action': '可即刻买入',
|
||
'entry_price': 100,
|
||
'current_price': 100,
|
||
'stop_loss': 92,
|
||
'tp1': 110,
|
||
'tp2': 116,
|
||
'risk_reward_ok': True,
|
||
'rr1': 1.25,
|
||
'entry_trigger_confirmed': True,
|
||
'opportunity_level': 'intraday_breakout',
|
||
'score_components': {'entry_score': 3},
|
||
},
|
||
signals=['15min即刻入场信号', '1H 量价齐飞K(量2.2x)'],
|
||
current_price=100,
|
||
market_context={'change_24h': 4.0},
|
||
strategy_code=LONG_MOMENTUM_BREAKOUT_STRATEGY,
|
||
)
|
||
|
||
assert action == '可即刻买入'
|
||
assert plan['risk_reward_ok_live'] is True
|
||
assert not any('rr1=' in r and '< 1.5' in r for r in reasons)
|