from __future__ import annotations from dataclasses import dataclass from app.services.recommendation_service import ( _build_reasoning, _choose_recommended_action, direction_analyzer, probability_estimator, signal_conflict_detector, target_generator, ) _DEFAULT_CFG = { "recommendation_high_confidence_threshold": 70.0, "recommendation_moderate_confidence_threshold": 50.0, "recommendation_confidence_diff_threshold": 20.0, } @dataclass class _SRLevelStub: id: int price_level: float type: str strength: int def test_high_confidence_long_example(): dimension_scores = { "technical": 75.0, "momentum": 68.0, "fundamental": 55.0, } confidence = direction_analyzer.calculate_confidence( direction="long", dimension_scores=dimension_scores, sentiment_classification="bullish", conflicts=[], ) assert confidence > 70.0 def test_high_confidence_short_example(): dimension_scores = { "technical": 30.0, "momentum": 35.0, "fundamental": 45.0, } confidence = direction_analyzer.calculate_confidence( direction="short", dimension_scores=dimension_scores, sentiment_classification="bearish", conflicts=[], ) assert confidence > 70.0 def test_short_confidence_low_on_strongly_bullish_stock(): """The CNC case: technical 92 / momentum 99 must make SHORT confidence low.""" dims = {"technical": 92.0, "momentum": 99.0, "fundamental": 96.0} short_conf = direction_analyzer.calculate_confidence( direction="short", dimension_scores=dims, sentiment_classification="neutral", conflicts=[], ) long_conf = direction_analyzer.calculate_confidence( direction="long", dimension_scores=dims, sentiment_classification="neutral", conflicts=[], ) assert short_conf < 20.0 # not 55 assert long_conf > 90.0 def test_action_neutral_when_bias_direction_has_no_setup(): """Strong LONG bias but only a SHORT setup is tradeable → NEUTRAL, not LONG_HIGH.""" action = _choose_recommended_action( long_confidence=100.0, short_confidence=5.0, config=_DEFAULT_CFG, available_directions={"short"}, ) assert action == "NEUTRAL" # With the long setup available, the same numbers give LONG_HIGH action_ok = _choose_recommended_action( long_confidence=100.0, short_confidence=5.0, config=_DEFAULT_CFG, available_directions={"long", "short"}, ) assert action_ok == "LONG_HIGH" def test_reasoning_explains_missing_setup(): reasoning = _build_reasoning( action="NEUTRAL", long_confidence=100.0, short_confidence=5.0, conflicts=[], dimension_scores={"technical": 92.0, "momentum": 99.0}, sentiment_classification="neutral", config=_DEFAULT_CFG, available_directions={"short"}, ) assert "bias is LONG" in reasoning assert "no high-conviction long setup" in reasoning.lower() def test_detects_sentiment_technical_conflict(): conflicts = signal_conflict_detector.detect_conflicts( dimension_scores={"technical": 72.0, "momentum": 55.0, "fundamental": 50.0}, sentiment_classification="bearish", ) assert any("sentiment-technical" in conflict for conflict in conflicts) def test_generate_targets_respects_direction_and_order(): sr_levels = [ _SRLevelStub(id=1, price_level=110.0, type="resistance", strength=80), _SRLevelStub(id=2, price_level=115.0, type="resistance", strength=70), _SRLevelStub(id=3, price_level=120.0, type="resistance", strength=60), _SRLevelStub(id=4, price_level=95.0, type="support", strength=75), ] targets = target_generator.generate_targets( direction="long", entry_price=100.0, stop_loss=96.0, sr_levels=sr_levels, # type: ignore[arg-type] atr_value=2.0, ) assert len(targets) >= 1 assert all(target["price"] > 100.0 for target in targets) distances = [target["distance_from_entry"] for target in targets] assert distances == sorted(distances) def test_probability_ranges_by_classification(): config = { "recommendation_signal_alignment_weight": 0.15, "recommendation_sr_strength_weight": 0.20, "recommendation_distance_penalty_factor": 0.10, } dimension_scores = {"technical": 70.0, "momentum": 70.0} conservative = probability_estimator.estimate_probability( { "classification": "Conservative", "sr_strength": 80, "distance_atr_multiple": 1.5, }, dimension_scores, "bullish", "long", config, ) moderate = probability_estimator.estimate_probability( { "classification": "Moderate", "sr_strength": 60, "distance_atr_multiple": 3.0, }, dimension_scores, "bullish", "long", config, ) aggressive = probability_estimator.estimate_probability( { "classification": "Aggressive", "sr_strength": 40, "distance_atr_multiple": 6.0, }, dimension_scores, "bullish", "long", config, ) assert conservative > 60 assert 40 <= moderate <= 70 assert aggressive < 50