alphax/tests/test_multi_strategy_infra.py
2026-06-04 23:33:21 +08:00

387 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_MOMENTUM_BREAKOUT_STRATEGY,
LONG_SECOND_WAVE_PULLBACK_STRATEGY,
MAIN_COMPOSITE_STRATEGY,
SHORT_BREAKDOWN_RETEST_STRATEGY,
is_strategy_allowed_for_side,
registered_strategy_codes,
strategy_direction,
strategy_label,
)
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_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.box_retest_4h import build_box_retest_1h_signal
from app.strategies.short_breakdown import build_breakdown_retest_short_1h_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_only_contains_short_term_three_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,
SHORT_BREAKDOWN_RETEST_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(SHORT_BREAKDOWN_RETEST_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_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_legacy_strategy_builders_no_longer_emit_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": "可即刻买入"},
) is None
assert build_compression_breakout_4h_signal(
symbol="OLD/USDT",
result={"score": 9, "signals": ["4H静K压缩突破箱体上沿"]},
entry_plan={"entry_action": "等回踩"},
) is None
assert build_intraday_momentum_15m_signal(
symbol="OLD/USDT",
result={"score": 9, "signals": ["15min强突破"], "trigger_context": {"current_triggers": ["15m"]}},
entry_plan={"entry_action": "可即刻买入"},
) is None
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"]) == "多头二波回踩"