alphax/tests/test_multi_strategy_infra.py
2026-06-07 20:29:45 +08:00

515 lines
21 KiB
Python

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"]) == "多头二波回踩"