from app.core.factor_roles import RISK, TRIGGER, factor_role, factor_roles_for_codes from app.core.signal_direction import sanitize_factor_breakdown_for_side, sanitize_signals_for_side from app.core.strategy_contract import StrategySignal, default_main_composite_signal from app.core.strategy_registry import ( BREAKDOWN_RETEST_SHORT_1H_STRATEGY, LONG_BOX_RETEST_4H_STRATEGY, LONG_COMPRESSION_BREAKOUT_STRATEGY, LONG_MOMENTUM_BREAKOUT_STRATEGY, LONG_SECOND_WAVE_PULLBACK_STRATEGY, MAIN_COMPOSITE_STRATEGY, SHORT_BREAKDOWN_RETEST_STRATEGY, SHORT_WEAK_BOUNCE_FAILURE_STRATEGY, is_strategy_allowed_for_side, registered_strategy_codes, strategy_direction, strategy_label, strategy_paper_config, ) from app.db.recommendation_commands import create_recommendation from app.db.paper_trading import _open_trade, _order_payload_from_rec from app.db.strategy_signal_queries import insert_strategy_signal from app.db.strategy_insights import evaluate_strategy_decision from app.strategies.altcoin_breakout import ( build_long_box_retest_4h_signal, build_long_momentum_breakout_signal, build_long_second_wave_pullback_signal, build_compression_breakout_4h_signal, build_intraday_momentum_15m_signal, build_volume_ignition_1h_signal, ) from app.strategies.orchestrator import arbitrate_strategy_signals from app.strategies.box_retest_4h import build_box_retest_1h_signal from app.strategies.short_breakdown import ( build_breakdown_retest_short_1h_signal, build_short_weak_bounce_failure_signal, detect_breakdown_retest_short_1h, ) def test_factor_roles_never_promote_unknown_to_trigger(): assert factor_role("box_breakout_pullback_4h") == TRIGGER assert factor_role("box_breakout_pullback_1h") == TRIGGER assert factor_role("false_breakout") == RISK assert factor_role("new_unknown_factor") == "unknown" assert factor_roles_for_codes(["box_breakout_pullback_4h", "new_unknown_factor"]) == { "box_breakout_pullback_4h": "trigger", "new_unknown_factor": "unknown", } def test_active_strategy_pool_contains_intraday_long_short_strategies(): signal = default_main_composite_signal( symbol="AAA/USDT", score=70, signal_codes=["vp_fly_1h_current"], entry_plan={"entry_action": "观察"}, ).to_json_dict() assert registered_strategy_codes() == [ LONG_MOMENTUM_BREAKOUT_STRATEGY, LONG_SECOND_WAVE_PULLBACK_STRATEGY, LONG_COMPRESSION_BREAKOUT_STRATEGY, LONG_BOX_RETEST_4H_STRATEGY, SHORT_BREAKDOWN_RETEST_STRATEGY, SHORT_WEAK_BOUNCE_FAILURE_STRATEGY, ] assert signal["strategy_code"] == LONG_MOMENTUM_BREAKOUT_STRATEGY assert signal["strategy_name"] == "多头日内动量启动" assert signal["factor_roles"]["vp_fly_1h_current"] == "trigger" assert strategy_label(LONG_MOMENTUM_BREAKOUT_STRATEGY) == "多头日内动量启动" assert strategy_label(LONG_SECOND_WAVE_PULLBACK_STRATEGY) == "多头二波回踩" assert strategy_label(LONG_COMPRESSION_BREAKOUT_STRATEGY) == "多头压缩突破" assert strategy_label(LONG_BOX_RETEST_4H_STRATEGY) == "多头4H箱体回踩" assert strategy_label(SHORT_BREAKDOWN_RETEST_STRATEGY) == "空头破位反抽" assert strategy_label(SHORT_WEAK_BOUNCE_FAILURE_STRATEGY) == "空头弱反弹失败" assert strategy_direction(LONG_MOMENTUM_BREAKOUT_STRATEGY) == "long" assert strategy_direction(BREAKDOWN_RETEST_SHORT_1H_STRATEGY) == "short" assert is_strategy_allowed_for_side(MAIN_COMPOSITE_STRATEGY, "short") is False assert is_strategy_allowed_for_side(LONG_MOMENTUM_BREAKOUT_STRATEGY, "short") is False assert is_strategy_allowed_for_side(BREAKDOWN_RETEST_SHORT_1H_STRATEGY, "short") is True def test_active_strategy_pool_uses_intraday_paper_gates(): long_momentum = strategy_paper_config(LONG_MOMENTUM_BREAKOUT_STRATEGY) second_wave = strategy_paper_config(LONG_SECOND_WAVE_PULLBACK_STRATEGY) compression = strategy_paper_config(LONG_COMPRESSION_BREAKOUT_STRATEGY) box_retest = strategy_paper_config(LONG_BOX_RETEST_4H_STRATEGY) short_retest = strategy_paper_config(SHORT_BREAKDOWN_RETEST_STRATEGY) short_bounce = strategy_paper_config(SHORT_WEAK_BOUNCE_FAILURE_STRATEGY) assert long_momentum["frequency_profile"] == "intraday_trading" assert long_momentum["entry_min_rr"] == 1.25 assert long_momentum["entry_min_rec_score"] == 25 assert long_momentum["order_require_current_trigger"] is True assert second_wave["order_min_distance_to_entry_pct"] == 0 assert second_wave["order_require_current_trigger"] is False assert compression["order_require_current_trigger"] is True assert box_retest["entry_min_rr"] == 1.3 assert short_retest["entry_min_rr"] == 1.3 assert short_retest["order_require_current_trigger"] is False assert short_bounce["order_require_current_trigger"] is True def test_long_momentum_breakout_strategy_builds_independent_signal(): signal = build_long_momentum_breakout_signal( symbol="VOL/USDT", result={ "score": 8, "signals": ["1H量价齐飞 · 连续放量", "15min即刻入场"], "trigger_context": {"current_triggers": ["15m突破"], "trigger_status": "current"}, "entry_plan": {"entry_action": "可即刻买入"}, }, entry_plan={"entry_action": "可即刻买入", "entry_price": 1.0}, ) payload = signal.to_json_dict() assert payload["strategy_code"] == LONG_MOMENTUM_BREAKOUT_STRATEGY assert payload["status"] == "candidate" assert payload["trigger"]["factor_code"] == "momentum_breakout_15m_1h" assert payload["factor_roles"]["momentum_breakout_15m_1h"] == "trigger" def test_long_builders_reject_short_breakdown_context(): result = { "score": 8, "signals": ["1H破位反抽做空(破位1.0)", "等待反抽失败确认", "15min即刻入场"], "trigger_context": {"current_triggers": ["15m"], "trigger_status": "current"}, "market_context": {"short_breakdown_retest_1h": {"detected": True}}, } assert build_long_momentum_breakout_signal( symbol="SHORTY/USDT", result=result, entry_plan={"entry_action": "可即刻买入"}, ) is None assert build_long_second_wave_pullback_signal( symbol="SHORTY/USDT", result={**result, "signals": [*result["signals"], "1H箱体突破回踩", "量价齐飞"]}, entry_plan={"entry_action": "等回踩"}, ) is None def test_momentum_and_second_wave_are_mutually_exclusive(): result = { "score": 8, "signals": ["1H量价齐飞 · 连续放量", "15min即刻入场", "1H箱体突破回踩"], "trigger_context": {"current_triggers": ["15m突破"], "trigger_status": "current"}, "entry_plan": {"entry_action": "可即刻买入"}, } assert build_long_momentum_breakout_signal( symbol="WAVE/USDT", result=result, entry_plan={"entry_action": "可即刻买入", "entry_price": 1.0}, ) is None signal = build_long_second_wave_pullback_signal( symbol="WAVE/USDT", result=result, entry_plan={"entry_action": "可即刻买入", "entry_price": 1.0}, ) assert signal is not None assert signal.to_json_dict()["strategy_code"] == LONG_SECOND_WAVE_PULLBACK_STRATEGY def test_intraday_strategy_builders_emit_independent_signals(): assert build_volume_ignition_1h_signal( symbol="OLD/USDT", result={"score": 9, "signals": ["1H量价齐飞", "15min即刻入场"], "trigger_context": {"current_triggers": ["15m"]}}, entry_plan={"entry_action": "可即刻买入"}, ).to_json_dict()["strategy_code"] == LONG_MOMENTUM_BREAKOUT_STRATEGY assert build_intraday_momentum_15m_signal( symbol="OLD/USDT", result={"score": 9, "signals": ["1H突破起爆", "15min强突破"], "trigger_context": {"current_triggers": ["15m"]}}, entry_plan={"entry_action": "可即刻买入"}, ).to_json_dict()["strategy_code"] == LONG_MOMENTUM_BREAKOUT_STRATEGY assert build_compression_breakout_4h_signal( symbol="COMP/USDT", result={"score": 9, "signals": ["4H静K蓄力", "压缩突破", "15min即刻入场"], "trigger_context": {"current_triggers": ["15m"]}}, entry_plan={"entry_action": "可即刻买入"}, ).to_json_dict()["strategy_code"] == LONG_COMPRESSION_BREAKOUT_STRATEGY assert build_long_box_retest_4h_signal( symbol="BOX/USDT", result={"score": 9, "signals": ["4H箱体突破回踩", "15min回踩确认"]}, entry_plan={"entry_action": "等回踩"}, ).to_json_dict()["strategy_code"] == LONG_BOX_RETEST_4H_STRATEGY def test_short_weak_bounce_failure_builder_requires_risk_off_context(): base = { "score": 7, "signals": ["15min反抽失败", "弱反弹失败"], "market_regime": {"regime": "risk_off", "risk_level": "high"}, } signal = build_short_weak_bounce_failure_signal( symbol="WEAK/USDT", result=base, entry_plan={"side": "short", "entry_action": "可即刻买入"}, ) assert signal is not None assert signal.to_json_dict()["strategy_code"] == SHORT_WEAK_BOUNCE_FAILURE_STRATEGY assert build_short_weak_bounce_failure_signal( symbol="WEAK/USDT", result={**base, "market_regime": {"regime": "altcoin_rotation", "risk_level": "medium"}}, entry_plan={"side": "short", "entry_action": "可即刻买入"}, ) is None def test_strategy_orchestrator_demotes_same_symbol_long_short_conflict(): long_signal = StrategySignal( strategy_code=LONG_MOMENTUM_BREAKOUT_STRATEGY, symbol="CLASH/USDT", direction="long", status="candidate", confidence=70, ) short_signal = StrategySignal( strategy_code=SHORT_BREAKDOWN_RETEST_STRATEGY, symbol="CLASH/USDT", direction="short", status="candidate", confidence=80, ) result = arbitrate_strategy_signals([long_signal, short_signal]) assert {item.direction for item in result} == {"long", "short"} assert all(item.status == "observe" for item in result) assert all("同币多空信号冲突" in " ".join(item.risk_plan.get("risk_reasons") or []) for item in result) def test_long_strategy_cannot_emit_short_signal(): import pytest with pytest.raises(ValueError): StrategySignal( strategy_code=LONG_MOMENTUM_BREAKOUT_STRATEGY, symbol="BAD/USDT", direction="short", factor_roles={"vp_fly_1h_current": "trigger"}, ) def test_short_direction_filters_bullish_supporting_evidence(): signals = [ "大户偏多(73%)", "BTC回调中独立走强", "板块联动: Layer1 龙头TON/USDT", "1H箱体突破回踩", "1H破位反抽做空", "破位质量高", ] clean, removed = sanitize_signals_for_side(signals, "short") assert "1H破位反抽做空" in clean assert "破位质量高" in clean assert all("大户偏多" not in item for item in clean) assert all("BTC回调中独立走强" not in item for item in clean) assert any("板块联动" in item for item in removed) def test_short_factor_breakdown_removes_long_only_positive_factors(): summary = { "items": [ {"factor_code": "top_trader_long", "factor_group": "positioning", "score_delta": 1}, {"factor_code": "sector_rotation", "factor_group": "narrative", "score_delta": 2}, {"factor_code": "box_breakout_pullback_1h", "factor_group": "structure", "score_delta": 6}, {"factor_code": "breakdown_retest_1h_short", "factor_group": "structure", "score_delta": 7}, {"factor_code": "funding_positive_risk", "factor_group": "risk", "score_delta": -3}, ] } clean, removed = sanitize_factor_breakdown_for_side(summary, "short") codes = [item["factor_code"] for item in clean["items"]] assert "breakdown_retest_1h_short" in codes assert "funding_positive_risk" in codes assert "top_trader_long" not in codes assert "sector_rotation" not in codes assert "box_breakout_pullback_1h" not in codes assert clean["total_delta"] == 4 assert {item["factor_code"] for item in removed} == { "top_trader_long", "sector_rotation", "box_breakout_pullback_1h", } def test_second_wave_strategy_requires_first_wave_and_pullback_context(): signal = build_long_second_wave_pullback_signal( symbol="QUIET/USDT", result={"score": 6, "signals": ["24h强势榜异动", "1H箱体突破回踩"], "entry_plan": {"entry_action": "等回踩"}}, entry_plan={"entry_action": "等回踩", "entry_price": 1.0}, ) payload = signal.to_json_dict() assert payload["strategy_code"] == LONG_SECOND_WAVE_PULLBACK_STRATEGY assert payload["status"] == "candidate" assert payload["factor_roles"]["second_wave_pullback_1h"] == "trigger" assert build_long_second_wave_pullback_signal( symbol="NOBOX/USDT", result={"score": 6, "signals": ["1H量价齐飞"], "entry_plan": {"entry_action": "可即刻买入"}}, entry_plan={"entry_action": "可即刻买入"}, ) is None def test_long_momentum_strategy_requires_current_trigger(): stale = build_long_momentum_breakout_signal( symbol="FAST/USDT", result={"score": 7, "signals": ["15m短周期启动", "1H量价齐飞"], "entry_plan": {"entry_action": "可即刻买入"}}, entry_plan={"entry_action": "可即刻买入"}, ).to_json_dict() fresh = build_long_momentum_breakout_signal( symbol="FAST/USDT", result={ "score": 7, "signals": ["15min强突破", "1H量价齐飞"], "trigger_context": {"current_triggers": ["15m突破"], "trigger_status": "current"}, "entry_plan": {"entry_action": "可即刻买入"}, }, entry_plan={"entry_action": "可即刻买入"}, ).to_json_dict() assert stale["status"] == "observe" assert "缺少当前低周期触发" in stale["risk_plan"]["risk_reasons"] assert fresh["status"] == "candidate" def test_breakdown_retest_short_strategy_is_independent_short_signal(): signal = build_breakdown_retest_short_1h_signal( symbol="WEAK/USDT", current_price=0.98, detection={ "detected": True, "score": 8, "breakdown_level": 1.0, "retest_zone": 1.0, "stop_level": 1.04, "target_1": 0.9, "quality": "优质", "retest_rejected": True, "relative_weakness": True, }, market_regime={"risk_level": "high", "regime": "risk_off"}, ).to_json_dict() assert signal["strategy_code"] == BREAKDOWN_RETEST_SHORT_1H_STRATEGY assert signal["direction"] == "short" assert signal["entry_plan"]["side"] == "short" assert signal["status"] == "candidate" assert signal["factor_roles"]["breakdown_retest_1h_short"] == "trigger" def test_short_breakdown_detector_exposes_numeric_quality_score(): import pandas as pd rows = [] price = 1.0 for i in range(64): rows.append({"timestamp": i, "open": price, "high": 1.02, "low": 0.98, "close": price, "volume": 100}) rows.extend([ {"timestamp": 64, "open": 1.0, "high": 1.01, "low": 0.95, "close": 0.97, "volume": 180}, {"timestamp": 65, "open": 0.97, "high": 1.01, "low": 0.96, "close": 0.99, "volume": 140}, {"timestamp": 66, "open": 0.99, "high": 1.015, "low": 0.955, "close": 0.965, "volume": 180}, {"timestamp": 67, "open": 0.965, "high": 0.98, "low": 0.94, "close": 0.955, "volume": 200}, ]) result = detect_breakdown_retest_short_1h(pd.DataFrame(rows), change_24h=-4) assert result["detected"] is True assert result["quality"] in {"良好", "优质"} assert isinstance(result["quality_score"], int) assert result["quality_score"] >= 5 def test_strategy_evaluation_recommends_promote_or_pause(): strong = evaluate_strategy_decision({ "signal_count": 24, "opportunity_count": 16, "trade_count": 8, "closed_trade_count": 8, "win_rate_pct": 62.5, "avg_realized_pnl_pct": 3.2, "realized_pnl_usdt": 180, "worst_pnl_pct": -3.5, "order_fill_rate_pct": 45, "trade_conversion_pct": 50, }) weak = evaluate_strategy_decision({ "signal_count": 24, "opportunity_count": 16, "trade_count": 8, "closed_trade_count": 8, "win_rate_pct": 25, "avg_realized_pnl_pct": -2.5, "realized_pnl_usdt": -120, "worst_pnl_pct": -9, "order_fill_rate_pct": 20, "trade_conversion_pct": 50, }) unfilled = evaluate_strategy_decision({ "signal_count": 18, "opportunity_count": 12, "trade_count": 0, "closed_trade_count": 0, }) assert strong["decision"] == "promote" assert strong["evaluation_score"] > weak["evaluation_score"] assert weak["decision"] == "pause" assert unfilled["decision"] == "review_entry_gate" def test_strategy_signal_insert_and_recommendation_lineage(pg_conn): signal = insert_strategy_signal( StrategySignal( strategy_code=LONG_SECOND_WAVE_PULLBACK_STRATEGY, symbol="BOX/USDT", score=10, confidence=80, trigger={"factor_code": "second_wave_pullback_1h"}, factor_roles={"second_wave_pullback_1h": "trigger"}, entry_plan={"entry_action": "等回踩", "entry_price": 1.0}, ) ) rec_id = create_recommendation( symbol="BOX/USDT", rec_state="爆发", rec_score=30, entry_price=1.0, stop_loss=0.94, tp1=1.12, signals=["4H箱体突破回踩(箱体上沿 $1, 量2x)"], entry_plan={"entry_action": "等回踩", "entry_price": 1.0, "stop_loss": 0.94, "tp1": 1.12}, strategy_code=signal["strategy_code"], strategy_signal_id=signal["strategy_signal_id"], strategy_snapshot=signal, factor_roles=signal["factor_roles"], ) row = pg_conn.execute("SELECT * FROM recommendation WHERE id=%s", (rec_id,)).fetchone() assert row["strategy_code"] == LONG_SECOND_WAVE_PULLBACK_STRATEGY assert row["strategy_signal_id"] == signal["strategy_signal_id"] assert "second_wave_pullback_1h" in row["factor_roles_json"] def test_box_retest_strategy_preserves_zero_age_as_fresh(): signal = build_box_retest_1h_signal( symbol="FRESH/USDT", current_price=1.01, detection={ "detected": True, "score": 10, "entry_zone": 1.0, "stop_level": 0.94, "quality": "优质", "pullback_age_bars": 0, }, market_regime={"regime": "altcoin_rotation", "risk_level": "medium"}, ) assert signal is None def test_paper_order_and_trade_inherit_strategy_lineage(pg_conn): rec = { "id": 1, "symbol": "BOX/USDT", "rec_score": 100, "entry_price": 1.0, "stop_loss": 0.94, "tp1": 1.12, "tp2": 1.2, "execution_status": "buy_now", "action_status": "可即刻买入", "strategy_version": "v-test", "strategy_code": LONG_SECOND_WAVE_PULLBACK_STRATEGY, "strategy_signal_id": 42, "strategy_snapshot_json": '{"strategy_code":"long_second_wave_pullback_1h_v1"}', "factor_roles_json": '{"second_wave_pullback_1h":"trigger"}', "entry_plan": {"entry_action": "可即刻买入", "entry_price": 1.0, "stop_loss": 0.94, "tp1": 1.12}, } payload = _order_payload_from_rec(rec, 1.01, "2026-05-27T00:00:00", {"trade_notional_usdt": 100}) assert payload["strategy_code"] == LONG_SECOND_WAVE_PULLBACK_STRATEGY assert payload["strategy_signal_id"] == 42 result = _open_trade( pg_conn, rec, 1.0, "2026-05-27T00:00:00", config={ "enabled": True, "trade_notional_usdt": 100, "trade_leverage": 1, "account_equity_usdt": 20000, "fee_rate": 0, "min_rec_score": 0, "min_rr": 0, "max_stop_loss_leverage_risk_pct": 999, "max_cumulative_leverage": 999, "max_account_drawdown_pause_pct": 0, "weak_entry_pause": {"enabled": False}, }, push_open_card=False, ) assert result["opened"] is True row = pg_conn.execute("SELECT * FROM paper_trades WHERE id=%s", (result["trade_id"],)).fetchone() event = pg_conn.execute("SELECT * FROM paper_trade_events WHERE trade_id=%s", (result["trade_id"],)).fetchone() assert row["strategy_code"] == LONG_SECOND_WAVE_PULLBACK_STRATEGY assert row["strategy_signal_id"] == 42 assert event["strategy_code"] == LONG_SECOND_WAVE_PULLBACK_STRATEGY assert strategy_label(row["strategy_code"]) == "多头二波回踩"